feat(otel,affinity): affinity-path span instrumentation and concurrent forward dispatch - #6164
feat(otel,affinity): affinity-path span instrumentation and concurrent forward dispatch#6164jonpspri wants to merge 6 commits into
Conversation
92fa6fc to
7f0d283
Compare
|
Thanks for this one, the trace propagation across the three hop types is clean and well commented. A few findings: Duplicate owner lookup
Tautological span tests
Unbounded dispatch queue
Once these are addressed we're good to go. |
32580a4 to
3dba821
Compare
gandhipratik203
left a comment
There was a problem hiding this comment.
Thanks for the changes, LGTM!
…entation Deep instrumentation of the session-affinity delegation path, which showed that cross-worker RPC delegation costs ~85ms while a much larger pre-handler delay traces to gunicorn workers still initializing (per-worker plugin manager builds and FastAPI route-state analysis). - New spans: mcp.affinity.check (owner lookup + decision attributes), mcp.affinity.forward_http / forward_rpc (delegation calls), mcp.affinity.dispatch_local (same-worker dispatch), mcp.affinity.execute_forwarded (owner-side verify + dispatch, nested via envelope trace-context attach in the Redis listener), and a zero-duration mcp.transport.enter marker at transport-handler entry - Auto-instrument redis/redis.asyncio clients (opentelemetry-instrumentation-redis); pub/sub receive loops are not instrumented, so listeners stay quiet - otel_*_instrumentation_enabled settings now default to false; compose opts in explicitly for the observability stack - Enabled-but-missing instrumentor packages now log at WARNING - Fix latent test failure (protocol= vs _protocol= kwarg) exposed when the observability extra is installed Signed-off-by: Jonathan Springer <jps@s390x.com> feat(affinity): concurrent forward dispatch with per-session in-memory ordering The affinity Redis listener awaited each forwarded execution inline, so under load the Nth envelope for an owner worker waited ~N x execution time (observed 1-4.6s forward waits at ~500ms executions). The listener now spawns bounded dispatch tasks; measured forward wait on the slow trace cohort drops to 2-49ms. - Per-session FIFO preserved without Redis: the listener claims an in-memory per-session lock synchronously (arrival order) and dispatch tasks take it before the global concurrency slot, so same-session forwards serialize while different sessions run concurrently (mcpgateway_affinity_forward_concurrency, default 32) - Deadlock safety: session-lock acquire is bounded by mcpgateway_affinity_session_lock_timeout (default 30s); on timeout the forward executes without the ordering guarantee and logs a warning - Lock lifecycle handled by weakref.WeakValueDictionary: entries evaporate when the last dispatch task drops its reference - locustfile_echo_delay: use fast_test_server's delay fuzz (delay_stddev; ECHO_DELAY_STDDEV_MS, default 25% of ECHO_DELAY_MS) - Regression tests: same-session non-overlap + cross-session concurrency, lock-timeout fallback, lock GC eviction Signed-off-by: Jonathan Springer <jps@s390x.com>
…-newer overrides h2 4.4.1 (2026-08-03) fixes CVE-2026-71554; pinned past the 10-day soak cutoff via tool.uv.exclude-newer-package as a vetted security fix. Also remove 21 exclude-newer-package entries whose dates now fall inside the global 10-day window (no-ops) along with their comment blocks, and drop the dangling override reference on the authlib pin. Verified none of the explicit transitive pins are redundant: every containing package's Requires-Dist floor is looser than the pin. Signed-off-by: Jonathan Springer <jps@s390x.com>
Traces for virtual-server requests appeared flat: the trusted-internal /_internal/mcp/rpc dispatch started a new trace because no W3C context crossed the hop, and the MCP SDK's session task group (created at app startup) never sees per-request contextvars. - OpenTelemetryRequestMiddleware detects requests without a valid remote parent (explicit is_valid + is_remote check) and injects the newly created root span's context into the ASGI scope headers before invoking the app, so downstream raw-header copies (session-task handoff, affinity envelopes, trusted-internal dispatch) carry the trace. - post_rpc_in_process injects the active trace context; the affinity LOCAL and http_forward paths now route through it instead of inline duplicate dispatches, and the http_forward consumer carries traceparent/tracestate explicitly past the passthrough allowlist. - rpc_forward envelopes embed the trace context before signing. Signed-off-by: Jonathan Springer <jps@s390x.com>
Signed-off-by: Jonathan Springer <jps@s390x.com>
Cover the 33 missing lines identified in the diff coverage report: mcpgateway/db.py (lines 252-260) - Add reload-based tests for the OTel SQLAlchemy instrumentation block: success path (SQLAlchemyInstrumentor called with engine) and ImportError path (warning logged when package unavailable). mcpgateway/observability.py (lines 1247-1255) - Add three tests for the Redis instrumentor block inside init_telemetry: successful instrumentation, exception during instrument() (non-fatal warning), and REDIS_INSTRUMENTOR=None (package-unavailable warning). mcpgateway/services/session_affinity.py (lines 88-97, 108-114, 717, 1090-1091, 1100-1104) - _attach_envelope_trace_context: ImportError returns None, missing/empty headers returns None, valid traceparent attaches context (skipif otel absent), attach exception returns None (skipif otel absent). - _detach_envelope_trace_context: None token is no-op, real token calls otel detach (skipif otel absent), import failure is swallowed silently. - drain_all: assert running _forward_tasks are cancelled. - _dispatch_forwarded: broad-except logs warning and does not re-raise. - _on_forward_task_done: cancelled task discarded without calling exception(), task exception logs warning, no exception produces no warning. mcpgateway/transports/streamablehttp_transport.py (lines 4312-4314) - Three direct tests for the affinity span attribute logic: owner matches WORKER_ID (decision='local'), owner is different worker (decision='forward'), owner is None (owner attribute 'none', decision 'local'). Signed-off-by: Jonathan Springer <jps@s390x.com>
- Remove duplicate get_session_owner() Redis round-trip in handle_streamable_http: hoist the single call before the span block so both span attribute recording and the forward decision use the same result (streamablehttp_transport.py:4309-4315). - Replace tautological affinity span attribute tests with real handler tests: each of the three test_affinity_span_attribute_logic_* tests copied the production if-block verbatim and asserted against the copy. Replaced with async tests that drive handle_streamable_http with a stubbed pool, patched create_span (non-None sentinel) and set_span_attribute capture, so production code is exercised. - Add stale-envelope backpressure in _dispatch_forwarded: envelopes carrying a timestamp older than mcpgateway_pool_rpc_forward_timeout are dropped with a WARNING before acquiring the session lock, preventing burst back-log from accumulating work the originating caller has already timed out on. Envelopes without a timestamp (legacy/synthetic) pass through unchanged. Two new tests cover the drop and execute paths; existing listener test updated to set mock_settings.mcpgateway_pool_rpc_forward_timeout. Signed-off-by: Jonathan Springer <jps@s390x.com>
3dba821 to
f0662d3
Compare
Summary
Supersedes #6154. This PR is the rebased origin-branch counterpart and contains the session-affinity, OTEL, and dependency changes:
h2to 4.4.1 for CVE-2026-71554 and prune expiredexclude-neweroverrides.The portal, Grafana, pgAdmin, and py-spy compose infrastructure remains in the companion monitoring PR, superseding the material originally moved here from #6153.
Closes #6284 — affinity-path span instrumentation and concurrent forward dispatch (performance epic #6104).