Lab: gRPC client interceptors and streaming RPCs¶
An export worker calls a real local reporting service. The lab checks registration, streaming outcomes, context and cleanup. Companion to Why gRPC interceptors break on streaming RPCs.
Run¶
Install uv, then run from the website repository root. uv downloads Python or dependencies if needed; the RPCs use only loopback, with an automatically selected port.
cd docs/blog/lab/2026-09-07-grpc-interceptors-and-streams
uv run --no-project --python 3.13 --with-requirements requirements.txt python interceptors_lab.py
uv run --no-project --python 3.13 --with-requirements requirements.txt python probe.py
Pinned versions: grpc-client-kit==0.4.0, grpcio==1.84.0; Python 3.13. Byte messages and generic handlers keep protobuf generation out of this example. This is a client-interceptor experiment, not a benchmark or a deployment configuration.
Assertions¶
| Scenario | What must hold |
|---|---|
| One native object inheriting four bases | Only unary-unary runs in this grpcio version |
| One logical interceptor, four adapters | Each RPC shape produces one successful observation |
| Timer around continuation | It really runs, finishes before the first row and misses the server error that arrives later |
| Timer around iteration | Counts the mid-stream error; retains status, details and trailing metadata |
| Library observer | Five rows on success; two rows then UNAVAILABLE on failure |
| Early exit | Explicit cancellation stops the server handler and produces one CANCELLED record |
break without cancellation |
The held RPC remains active until explicitly cancelled |
| Fast stream | The observer can finish before the consumer resumes past the final row |
Streaming request with write() |
done_writing() allows a final response without an interceptor deadlock |
| Context scope | Token reset succeeds for all four shapes; the caller's context stays unchanged |
Teardown raises RuntimeError |
Four successes and one UNAVAILABLE keep their outcomes; five failures are logged |
Both scripts fail on a violated assertion or an unhandled asyncio error. Completion is synchronized through queues and events with timeouts, not arbitrary sleeps. Server delays simulate row production; printed elapsed times vary between runs and are not exact assertions.
Files¶
report_service.py: four RPC handlers, failure and cancellation fixtures, server lifecycle.observers.py: native and logical interceptors copied into the article.consumer.py: full download and caller-owned context with cancellation on early exit.interceptors_lab.py: the streaming scenarios and call-interface checks.probe.py: context isolation and teardown-error logging.requirements.txt: exact library versions.
The small observation queues are test collectors. Application processing can outlive the RPC; measure the full export separately. A context value set inside an interceptor does not automatically become the caller's value or a gRPC metadata field.