Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 996acf2

Browse files
committed
instrumented read_rows
1 parent 0f8067c commit 996acf2

2 files changed

Lines changed: 76 additions & 35 deletions

File tree

google/cloud/bigtable/data/_async/_read_rows.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
from typing import Sequence, TYPE_CHECKING
1919

20+
import time
21+
2022
from google.cloud.bigtable_v2.types import ReadRowsRequest as ReadRowsRequestPB
2123
from google.cloud.bigtable_v2.types import ReadRowsResponse as ReadRowsResponsePB
2224
from google.cloud.bigtable_v2.types import RowSet as RowSetPB
@@ -29,13 +31,16 @@
2931
from google.cloud.bigtable.data.exceptions import _ResetRow
3032
from google.cloud.bigtable.data._helpers import _attempt_timeout_generator
3133
from google.cloud.bigtable.data._helpers import _retry_exception_factory
34+
from google.cloud.bigtable.data._helpers import TrackedBackoffGenerator
3235

3336
from google.api_core import retry as retries
3437
from google.api_core.retry import exponential_sleep_generator
3538

3639
from google.cloud.bigtable.data._cross_sync import CrossSync
3740

3841
if TYPE_CHECKING:
42+
from google.cloud.bigtable.data._metrics import ActiveOperationMetric
43+
3944
if CrossSync.is_async:
4045
from google.cloud.bigtable.data._async.client import (
4146
_DataApiTargetAsync as TargetType,
@@ -64,6 +69,7 @@ class _ReadRowsOperationAsync:
6469
target: The table or view to send the request to
6570
operation_timeout: The total time to allow for the operation, in seconds
6671
attempt_timeout: The time to allow for each individual attempt, in seconds
72+
metric: the metric object representing the active operation
6773
retryable_exceptions: A list of exceptions that should trigger a retry
6874
"""
6975

@@ -75,6 +81,7 @@ class _ReadRowsOperationAsync:
7581
"_predicate",
7682
"_last_yielded_row_key",
7783
"_remaining_count",
84+
"_operation_metric",
7885
)
7986

8087
def __init__(
@@ -83,6 +90,7 @@ def __init__(
8390
target: TargetType,
8491
operation_timeout: float,
8592
attempt_timeout: float,
93+
metric: ActiveOperationMetric,
8694
retryable_exceptions: Sequence[type[Exception]] = (),
8795
):
8896
self.attempt_timeout_gen = _attempt_timeout_generator(
@@ -101,6 +109,7 @@ def __init__(
101109
self._predicate = retries.if_exception_type(*retryable_exceptions)
102110
self._last_yielded_row_key: bytes | None = None
103111
self._remaining_count: int | None = self.request.rows_limit or None
112+
self._operation_metric = metric
104113

105114
def start_operation(self) -> CrossSync.Iterable[Row]:
106115
"""
@@ -109,10 +118,13 @@ def start_operation(self) -> CrossSync.Iterable[Row]:
109118
Yields:
110119
Row: The next row in the stream
111120
"""
121+
self._operation_metric.backoff_generator = TrackedBackoffGenerator(
122+
0.01, 60, multiplier=2
123+
)
112124
return CrossSync.retry_target_stream(
113125
self._read_rows_attempt,
114126
self._predicate,
115-
exponential_sleep_generator(0.01, 60, multiplier=2),
127+
self._operation_metric.backoff_generator,
116128
self.operation_timeout,
117129
exception_factory=_retry_exception_factory,
118130
)
@@ -127,6 +139,7 @@ def _read_rows_attempt(self) -> CrossSync.Iterable[Row]:
127139
Yields:
128140
Row: The next row in the stream
129141
"""
142+
self._operation_metric.start_attempt()
130143
# revise request keys and ranges between attempts
131144
if self._last_yielded_row_key is not None:
132145
# if this is a retry, try to trim down the request to avoid ones we've already processed
@@ -137,20 +150,20 @@ def _read_rows_attempt(self) -> CrossSync.Iterable[Row]:
137150
)
138151
except _RowSetComplete:
139152
# if we've already seen all the rows, we're done
140-
return self.merge_rows(None)
153+
return self.merge_rows(None, self._operation_metric)
141154
# revise the limit based on number of rows already yielded
142155
if self._remaining_count is not None:
143156
self.request.rows_limit = self._remaining_count
144157
if self._remaining_count == 0:
145-
return self.merge_rows(None)
158+
return self.merge_rows(None, self._operation_metric)
146159
# create and return a new row merger
147160
gapic_stream = self.target.client._gapic_client.read_rows(
148161
self.request,
149162
timeout=next(self.attempt_timeout_gen),
150163
retry=None,
151164
)
152165
chunked_stream = self.chunk_stream(gapic_stream)
153-
return self.merge_rows(chunked_stream)
166+
return self.merge_rows(chunked_stream, self._operation_metric)
154167

155168
@CrossSync.convert()
156169
async def chunk_stream(
@@ -210,6 +223,7 @@ async def chunk_stream(
210223
)
211224
async def merge_rows(
212225
chunks: CrossSync.Iterable[ReadRowsResponsePB.CellChunk] | None,
226+
operation_metric: ActiveOperationMetric,
213227
) -> CrossSync.Iterable[Row]:
214228
"""
215229
Merge chunks into rows
@@ -222,6 +236,7 @@ async def merge_rows(
222236
if chunks is None:
223237
return
224238
it = chunks.__aiter__()
239+
is_first_row = True
225240
# For each row
226241
while True:
227242
try:
@@ -304,7 +319,17 @@ async def merge_rows(
304319
Cell(value, row_key, family, qualifier, ts, list(labels))
305320
)
306321
if c.commit_row:
322+
if is_first_row:
323+
# record first row latency in metrics
324+
is_first_row = False
325+
operation_metric.attempt_first_response()
326+
block_time = time.monotonic()
307327
yield Row(row_key, cells)
328+
# most metric operations use setters, but this one updates
329+
# the value directly to avoid extra overhead
330+
operation_metric.active_attempt.application_blocking_time_ms += ( # type: ignore
331+
time.monotonic() - block_time
332+
) * 1000
308333
break
309334
c = await it.__anext__()
310335
except _ResetRow as e:

google/cloud/bigtable/data/_async/client.py

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
if TYPE_CHECKING:
126126
from google.cloud.bigtable.data._helpers import RowKeySamples
127127
from google.cloud.bigtable.data._helpers import ShardedQuery
128+
from google.cloud.bigtable.data._metrics import ActiveOperationMetric
128129

129130
if CrossSync.is_async:
130131
from google.cloud.bigtable.data._async.mutations_batcher import (
@@ -1020,14 +1021,18 @@ async def read_rows_stream(
10201021
)
10211022
retryable_excs = _get_retryable_errors(retryable_errors, self)
10221023

1023-
row_merger = CrossSync._ReadRowsOperation(
1024-
query,
1025-
self,
1026-
operation_timeout=operation_timeout,
1027-
attempt_timeout=attempt_timeout,
1028-
retryable_exceptions=retryable_excs,
1029-
)
1030-
return row_merger.start_operation()
1024+
with self._metrics.create_operation(
1025+
OperationType.READ_ROWS, streaming=True
1026+
) as operation_metric:
1027+
row_merger = CrossSync._ReadRowsOperation(
1028+
query,
1029+
self,
1030+
operation_timeout=operation_timeout,
1031+
attempt_timeout=attempt_timeout,
1032+
metric=operation_metric,
1033+
retryable_exceptions=retryable_excs,
1034+
)
1035+
return row_merger.start_operation()
10311036

10321037
@CrossSync.convert
10331038
async def read_rows(
@@ -1077,7 +1082,9 @@ async def read_rows(
10771082
)
10781083
return [row async for row in row_generator]
10791084

1080-
@CrossSync.convert
1085+
@CrossSync.convert(
1086+
replace_symbols={"__anext__": "__next__", "StopAsyncIteration": "StopIteration"}
1087+
)
10811088
async def read_row(
10821089
self,
10831090
row_key: str | bytes,
@@ -1117,15 +1124,28 @@ async def read_row(
11171124
if row_key is None:
11181125
raise ValueError("row_key must be string or bytes")
11191126
query = ReadRowsQuery(row_keys=row_key, row_filter=row_filter, limit=1)
1120-
results = await self.read_rows(
1121-
query,
1122-
operation_timeout=operation_timeout,
1123-
attempt_timeout=attempt_timeout,
1124-
retryable_errors=retryable_errors,
1127+
1128+
operation_timeout, attempt_timeout = _get_timeouts(
1129+
operation_timeout, attempt_timeout, self
11251130
)
1126-
if len(results) == 0:
1127-
return None
1128-
return results[0]
1131+
retryable_excs = _get_retryable_errors(retryable_errors, self)
1132+
1133+
with self._metrics.create_operation(
1134+
OperationType.READ_ROWS, streaming=False
1135+
) as operation_metric:
1136+
row_merger = CrossSync._ReadRowsOperation(
1137+
query,
1138+
self,
1139+
operation_timeout=operation_timeout,
1140+
attempt_timeout=attempt_timeout,
1141+
metric=operation_metric,
1142+
retryable_exceptions=retryable_excs,
1143+
)
1144+
results_generator = row_merger.start_operation()
1145+
try:
1146+
return results_generator.__anext__()
1147+
except StopAsyncIteration:
1148+
return None
11291149

11301150
@CrossSync.convert
11311151
async def read_rows_sharded(
@@ -1264,20 +1284,17 @@ async def row_exists(
12641284
from any retries that failed
12651285
google.api_core.exceptions.GoogleAPIError: raised if the request encounters an unrecoverable error
12661286
"""
1267-
if row_key is None:
1268-
raise ValueError("row_key must be string or bytes")
1269-
12701287
strip_filter = StripValueTransformerFilter(flag=True)
12711288
limit_filter = CellsRowLimitFilter(1)
12721289
chain_filter = RowFilterChain(filters=[limit_filter, strip_filter])
1273-
query = ReadRowsQuery(row_keys=row_key, limit=1, row_filter=chain_filter)
1274-
results = await self.read_rows(
1275-
query,
1290+
result = await self.read_row(
1291+
row_key=row_key,
1292+
row_filter=chain_filter,
12761293
operation_timeout=operation_timeout,
12771294
attempt_timeout=attempt_timeout,
12781295
retryable_errors=retryable_errors,
12791296
)
1280-
return len(results) > 0
1297+
return result is not None
12811298

12821299
@CrossSync.convert
12831300
async def sample_row_keys(
@@ -1334,6 +1351,7 @@ async def sample_row_keys(
13341351
async with self._metrics.create_operation(
13351352
OperationType.SAMPLE_ROW_KEYS, backoff_generator=sleep_generator
13361353
):
1354+
13371355
@CrossSync.convert
13381356
async def execute_rpc():
13391357
results = await self.client._gapic_client.sample_row_keys(
@@ -1598,14 +1616,14 @@ async def check_and_mutate_row(
15981616
false_case_mutations = [false_case_mutations]
15991617
false_case_list = [m._to_pb() for m in false_case_mutations or []]
16001618

1601-
async with self._metrics.create_operation(
1602-
OperationType.CHECK_AND_MUTATE
1603-
):
1619+
async with self._metrics.create_operation(OperationType.CHECK_AND_MUTATE):
16041620
result = await self.client._gapic_client.check_and_mutate_row(
16051621
request=CheckAndMutateRowRequest(
16061622
true_mutations=true_case_list,
16071623
false_mutations=false_case_list,
1608-
predicate_filter=predicate._to_pb() if predicate is not None else None,
1624+
predicate_filter=predicate._to_pb()
1625+
if predicate is not None
1626+
else None,
16091627
row_key=row_key.encode("utf-8")
16101628
if isinstance(row_key, str)
16111629
else row_key,
@@ -1656,9 +1674,7 @@ async def read_modify_write_row(
16561674
if not rules:
16571675
raise ValueError("rules must contain at least one item")
16581676

1659-
async with self._metrics.create_operation(
1660-
OperationType.READ_MODIFY_WRITE
1661-
):
1677+
async with self._metrics.create_operation(OperationType.READ_MODIFY_WRITE):
16621678
result = await self.client._gapic_client.read_modify_write_row(
16631679
request=ReadModifyWriteRowRequest(
16641680
rules=[rule._to_pb() for rule in rules],

0 commit comments

Comments
 (0)