Skip to content

Commit 8c631eb

Browse files
committed
tentantive fix
1 parent 891bffc commit 8c631eb

2 files changed

Lines changed: 84 additions & 6 deletions

File tree

sentry_sdk/integrations/grpc/aio/server.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __init__(
2525
self: "ServerInterceptor",
2626
find_name: "Callable[[ServicerContext], str] | None" = None,
2727
) -> None:
28-
self._find_method_name = find_name or self._find_name
28+
self._custom_find_name = find_name
2929

3030
super().__init__()
3131

@@ -34,17 +34,21 @@ async def intercept_service(
3434
continuation: "Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]]",
3535
handler_call_details: "HandlerCallDetails",
3636
) -> "Optional[Awaitable[RpcMethodHandler]]":
37-
self._handler_call_details = handler_call_details
3837
handler = await continuation(handler_call_details)
3938
if handler is None:
4039
return None
4140

41+
method_name = handler_call_details.method
42+
custom_find_name = self._custom_find_name
43+
4244
if not handler.request_streaming and not handler.response_streaming:
4345
handler_factory = grpc.unary_unary_rpc_method_handler
4446

4547
async def wrapped(request: "Any", context: "ServicerContext") -> "Any":
4648
with sentry_sdk.isolation_scope():
47-
name = self._find_method_name(context)
49+
name = (
50+
custom_find_name(context) if custom_find_name else method_name
51+
)
4852
if not name:
4953
return await handler(request, context)
5054

@@ -96,6 +100,3 @@ async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type:
96100
request_deserializer=handler.request_deserializer,
97101
response_serializer=handler.response_serializer,
98102
)
99-
100-
def _find_name(self, context: "ServicerContext") -> str:
101-
return self._handler_call_details.method

tests/integrations/grpc/test_grpc_aio.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,83 @@ async def test_stream_unary(grpc_server_and_channel):
268268
assert response.text == "test"
269269

270270

271+
@pytest.mark.asyncio
272+
async def test_concurrent_requests_do_not_leak_method_name(sentry_init, capture_events):
273+
"""
274+
Regression test for SDK-1184: the async ServerInterceptor must not stash
275+
request-specific state on the shared interceptor instance, otherwise
276+
concurrent requests can overwrite each other's method names.
277+
"""
278+
from sentry_sdk.integrations.grpc.aio.server import ServerInterceptor
279+
280+
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
281+
events = capture_events()
282+
283+
interceptor = ServerInterceptor()
284+
285+
request_a_can_finish = asyncio.Event()
286+
request_a_started = asyncio.Event()
287+
288+
async def handler_a(request, context):
289+
request_a_started.set()
290+
await request_a_can_finish.wait()
291+
return "a-response"
292+
293+
async def handler_b(request, context):
294+
return "b-response"
295+
296+
def make_handler(behavior):
297+
h = type(
298+
"H",
299+
(),
300+
{
301+
"request_streaming": False,
302+
"response_streaming": False,
303+
"unary_unary": behavior,
304+
"request_deserializer": None,
305+
"response_serializer": None,
306+
},
307+
)()
308+
return h
309+
310+
class FakeHandlerCallDetails:
311+
def __init__(self, method):
312+
self.method = method
313+
314+
class FakeContext:
315+
def invocation_metadata(self):
316+
return ()
317+
318+
async def continuation_a(_):
319+
return make_handler(handler_a)
320+
321+
async def continuation_b(_):
322+
return make_handler(handler_b)
323+
324+
wrapped_a_handler = await interceptor.intercept_service(
325+
continuation_a, FakeHandlerCallDetails("/svc/MethodA")
326+
)
327+
# Kick off request A; it parks inside the handler awaiting the event.
328+
task_a = asyncio.create_task(
329+
wrapped_a_handler.unary_unary(None, FakeContext()) # type: ignore[attr-defined]
330+
)
331+
await request_a_started.wait()
332+
333+
# While A is parked, intercept B. Pre-fix this overwrote shared state on
334+
# the interceptor and A would later read B's method name.
335+
wrapped_b_handler = await interceptor.intercept_service(
336+
continuation_b, FakeHandlerCallDetails("/svc/MethodB")
337+
)
338+
await wrapped_b_handler.unary_unary(None, FakeContext()) # type: ignore[attr-defined]
339+
340+
request_a_can_finish.set()
341+
await task_a
342+
343+
transactions = [e for e in events if e.get("type") == "transaction"]
344+
names = sorted(t["transaction"] for t in transactions)
345+
assert names == ["/svc/MethodA", "/svc/MethodB"]
346+
347+
271348
@pytest.mark.asyncio
272349
async def test_span_origin(grpc_server_and_channel, capture_events_forksafe):
273350
_, channel = grpc_server_and_channel

0 commit comments

Comments
 (0)