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

Commit 84f61ee

Browse files
committed
added metadata capture for failed rpcs
1 parent a34c01e commit 84f61ee

2 files changed

Lines changed: 147 additions & 8 deletions

File tree

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import time
1717
from functools import wraps
18+
from grpc import RpcError
1819
from google.cloud.bigtable.data._metrics.data_model import (
1920
OPERATION_INTERCEPTOR_METADATA_KEY,
2021
)
@@ -105,16 +106,24 @@ async def intercept_unary_unary(
105106
self, operation, continuation, client_call_details, request
106107
):
107108
encountered_exc: Exception | None = None
108-
call = None
109+
metadata = None
109110
try:
110111
call = await continuation(client_call_details, request)
112+
metadata = (await call.trailing_metadata() or []) + (await call.initial_metadata() or [])
111113
return call
114+
except RpcError as rpc_error:
115+
# attempt extracting metadata from error
116+
try:
117+
metadata = (await rpc_error.trailing_metadata() or []) + (await rpc_error.initial_metadata() or [])
118+
except Exception:
119+
pass
120+
encountered_exc = rpc_error
121+
raise rpc_error
112122
except Exception as e:
113123
encountered_exc = e
114124
raise
115125
finally:
116-
if call is not None:
117-
metadata = (await call.trailing_metadata() or []) + (await call.initial_metadata() or [])
126+
if metadata is not None:
118127
operation.add_response_metadata(metadata)
119128
if encountered_exc is not None:
120129
# end attempt. If it succeeded, let higher levels decide when to end operation
@@ -138,6 +147,7 @@ async def response_wrapper(call):
138147
has_first_response = True
139148
yield response
140149

150+
141151
except Exception as e:
142152
encountered_exc = e
143153
raise
@@ -149,4 +159,17 @@ async def response_wrapper(call):
149159
# end attempt. If it succeeded, let higher levels decide when to end operation
150160
operation.end_attempt_with_status(encountered_exc)
151161

152-
return response_wrapper(await continuation(client_call_details, request))
162+
try:
163+
return response_wrapper(await continuation(client_call_details, request))
164+
except RpcError as rpc_error:
165+
# attempt extracting metadata from error
166+
try:
167+
metadata = (await rpc_error.trailing_metadata() or []) + (await rpc_error.initial_metadata() or [])
168+
operation.add_response_metadata(metadata)
169+
except Exception:
170+
pass
171+
operation.end_attempt_with_status(rpc_error)
172+
raise rpc_error
173+
except Exception as e:
174+
operation.end_attempt_with_status(e)
175+
raise

tests/unit/data/_async/test_metrics_interceptor.py

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import pytest
1616
import asyncio
17+
from grpc import RpcError
1718

1819
from google.cloud.bigtable.data._cross_sync import CrossSync
1920

@@ -158,7 +159,61 @@ async def test_unary_unary_interceptor_success(self):
158159

159160
@CrossSync.pytest
160161
async def test_unary_unary_interceptor_failure(self):
161-
"""Test that interceptor handles failed unary-unary calls"""
162+
"""test a failed RpcError with metadata"""
163+
from google.cloud.bigtable.data._metrics.data_model import (
164+
OPERATION_INTERCEPTOR_METADATA_KEY,
165+
)
166+
167+
instance = self._make_one()
168+
op = mock.Mock()
169+
op.uuid = "test-uuid"
170+
op.state = 1 # ACTIVE_ATTEMPT
171+
instance.operation_map[op.uuid] = op
172+
exc = RpcError("test")
173+
exc.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")])
174+
exc.initial_metadata = CrossSync.Mock(return_value=[("c", "d")])
175+
continuation = CrossSync.Mock(side_effect=exc)
176+
call = continuation.return_value
177+
details = mock.Mock()
178+
details.metadata = [(OPERATION_INTERCEPTOR_METADATA_KEY, op.uuid)]
179+
request = mock.Mock()
180+
with pytest.raises(RpcError) as e:
181+
await instance.intercept_unary_unary(continuation, details, request)
182+
assert e.value == exc
183+
continuation.assert_called_once_with(details, request)
184+
op.add_response_metadata.assert_called_once_with([("a", "b"), ("c", "d")])
185+
op.end_attempt_with_status.assert_called_once_with(exc)
186+
187+
@CrossSync.pytest
188+
async def test_unary_unary_interceptor_failure_no_metadata(self):
189+
"""test with RpcError without without metadata attached"""
190+
from google.cloud.bigtable.data._metrics.data_model import (
191+
OPERATION_INTERCEPTOR_METADATA_KEY,
192+
)
193+
194+
instance = self._make_one()
195+
op = mock.Mock()
196+
op.uuid = "test-uuid"
197+
op.state = 1 # ACTIVE_ATTEMPT
198+
instance.operation_map[op.uuid] = op
199+
exc = RpcError("test")
200+
continuation = CrossSync.Mock(side_effect=exc)
201+
call = continuation.return_value
202+
call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")])
203+
call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")])
204+
details = mock.Mock()
205+
details.metadata = [(OPERATION_INTERCEPTOR_METADATA_KEY, op.uuid)]
206+
request = mock.Mock()
207+
with pytest.raises(RpcError) as e:
208+
await instance.intercept_unary_unary(continuation, details, request)
209+
assert e.value == exc
210+
continuation.assert_called_once_with(details, request)
211+
op.add_response_metadata.assert_not_called()
212+
op.end_attempt_with_status.assert_called_once_with(exc)
213+
214+
@CrossSync.pytest
215+
async def test_unary_unary_interceptor_failure_generic(self):
216+
"""test generic exception"""
162217
from google.cloud.bigtable.data._metrics.data_model import (
163218
OPERATION_INTERCEPTOR_METADATA_KEY,
164219
)
@@ -180,9 +235,10 @@ async def test_unary_unary_interceptor_failure(self):
180235
await instance.intercept_unary_unary(continuation, details, request)
181236
assert e.value == exc
182237
continuation.assert_called_once_with(details, request)
183-
op.add_response_metadata.assert_called_once_with([("a", "b"), ("c", "d")])
238+
op.add_response_metadata.assert_not_called()
184239
op.end_attempt_with_status.assert_called_once_with(exc)
185240

241+
186242
@CrossSync.pytest
187243
async def test_unary_stream_interceptor_op_not_found(self):
188244
"""Test that interceptor calls continuation if op is not found"""
@@ -263,7 +319,67 @@ async def mock_generator():
263319

264320
@CrossSync.pytest
265321
async def test_unary_stream_interceptor_failure_start_stream(self):
266-
"""Test that interceptor handles failures at start of stream"""
322+
"""Test that interceptor handles failures at start of stream with RpcError with metadata"""
323+
from google.cloud.bigtable.data._metrics.data_model import (
324+
OPERATION_INTERCEPTOR_METADATA_KEY,
325+
)
326+
327+
instance = self._make_one()
328+
op = mock.Mock()
329+
op.uuid = "test-uuid"
330+
op.state = 1 # ACTIVE_ATTEMPT
331+
op.start_time_ns = 0
332+
op.first_response_latency = None
333+
instance.operation_map[op.uuid] = op
334+
exc = RpcError("test")
335+
exc.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")])
336+
exc.initial_metadata = CrossSync.Mock(return_value=[("c", "d")])
337+
338+
continuation = CrossSync.Mock()
339+
continuation.side_effect = exc
340+
details = mock.Mock()
341+
details.metadata = [(OPERATION_INTERCEPTOR_METADATA_KEY, op.uuid)]
342+
request = mock.Mock()
343+
with pytest.raises(RpcError) as e:
344+
await instance.intercept_unary_stream(continuation, details, request)
345+
assert e.value == exc
346+
continuation.assert_called_once_with(details, request)
347+
assert op.first_response_latency_ns is not None
348+
op.add_response_metadata.assert_called_once_with([("a", "b"), ("c", "d")])
349+
op.end_attempt_with_status.assert_called_once_with(exc)
350+
351+
@CrossSync.pytest
352+
async def test_unary_stream_interceptor_failure_start_stream_no_metadata(self):
353+
"""Test that interceptor handles failures at start of stream with RpcError with no metadata"""
354+
from google.cloud.bigtable.data._metrics.data_model import (
355+
OPERATION_INTERCEPTOR_METADATA_KEY,
356+
)
357+
358+
instance = self._make_one()
359+
op = mock.Mock()
360+
op.uuid = "test-uuid"
361+
op.state = 1 # ACTIVE_ATTEMPT
362+
op.start_time_ns = 0
363+
op.first_response_latency = None
364+
instance.operation_map[op.uuid] = op
365+
exc = RpcError("test")
366+
367+
continuation = CrossSync.Mock()
368+
continuation.side_effect = exc
369+
details = mock.Mock()
370+
details.metadata = [(OPERATION_INTERCEPTOR_METADATA_KEY, op.uuid)]
371+
request = mock.Mock()
372+
with pytest.raises(RpcError) as e:
373+
await instance.intercept_unary_stream(continuation, details, request)
374+
assert e.value == exc
375+
continuation.assert_called_once_with(details, request)
376+
assert op.first_response_latency_ns is not None
377+
op.add_response_metadata.assert_not_called()
378+
op.end_attempt_with_status.assert_called_once_with(exc)
379+
380+
@CrossSync.pytest
381+
async def test_unary_stream_interceptor_failure_start_stream_generic(self):
382+
"""Test that interceptor handles failures at start of stream with generic exception"""
267383
from google.cloud.bigtable.data._metrics.data_model import (
268384
OPERATION_INTERCEPTOR_METADATA_KEY,
269385
)
@@ -287,5 +403,5 @@ async def test_unary_stream_interceptor_failure_start_stream(self):
287403
assert e.value == exc
288404
continuation.assert_called_once_with(details, request)
289405
assert op.first_response_latency_ns is not None
290-
op.add_response_metadata.assert_called_once_with([("a", "b"), ("c", "d")])
406+
op.add_response_metadata.assert_not_called()
291407
op.end_attempt_with_status.assert_called_once_with(exc)

0 commit comments

Comments
 (0)