feat(nunciod): bound gRPC server concurrency/streams/timeout/message size #355

Merged
KofTwentyTwo merged 1 commits from story/ws-d8-grpc-limits into dev 2026-07-31 22:27:03 +00:00
KofTwentyTwo commented 2026-07-31 22:25:27 +00:00 (Migrated from github.com)

Summary

  • Adds resource bounds to the tonic Server::builder() chain in crates/nunciod/src/grpc.rs::serve_on_listener_with_overrides_and_shutdown: concurrency_limit_per_connection, a per-call timeout, max_concurrent_streams, TCP keepalive, and HTTP/2 keepalive interval/timeout.
  • Adds a per-service max_decoding_message_size (16 MiB, above tonic's unconfigured 4 MiB default so a real email with an attachment still fits) to every one of the 8 mounted services, applied before each service is wrapped in its existing BearerAuthInterceptor (auth wiring untouched; shutdown wiring from the prior WS-C1 story untouched).
  • Named, documented constants: GRPC_CONCURRENCY_LIMIT_PER_CONNECTION (32), GRPC_REQUEST_TIMEOUT (300s), GRPC_MAX_CONCURRENT_STREAMS (64), GRPC_MAX_DECODING_MESSAGE_SIZE_BYTES (16 MiB), GRPC_TCP_KEEPALIVE (60s), GRPC_HTTP2_KEEPALIVE_INTERVAL (30s), GRPC_HTTP2_KEEPALIVE_TIMEOUT (20s).

Why the timeout doesn't break Subscribe or long Sync calls

tonic's Server::timeout wraps the handler's request future. For a server-streaming RPC (System.Subscribe) that future resolves as soon as the response stream is constructed, not when the stream finishes emitting items -- so a long-lived Subscribe connection is unaffected. For unary RPCs (Mail.Sync/Calendar.Sync/Contacts.Sync) the 300s deadline is a generous bound chosen to cover a slow real-world sync against a large mailbox while still failing a genuinely stuck backend call instead of hanging forever. This is a documented tradeoff, not a per-RPC exemption, since tonic's server-level timeout applies uniformly to the whole router.

Test plan

  • send_message_rejects_body_larger_than_max_decoding_message_size -- a body one byte over the configured 16 MiB limit is rejected with Code::OutOfRange at decode time, before the handler runs.
  • send_message_accepts_body_above_default_but_within_configured_limit -- a 6 MiB body (above tonic's unconfigured 4 MiB default, within the configured 16 MiB) decodes successfully and reaches the handler, proving the raised limit is real and not a no-op.
  • All existing E2E suites unchanged and green, including Subscribe streaming (subscribe_streams_mapped_core_events_from_the_live_event_bus, subscribe_rejects_missing_bearer_token) and Sync (spine_e2e_test, calendar_e2e_test, contacts_e2e_test, sync::tests::*).

Gate output

cargo fmt --all -- --check   -> clean
cargo check-all               -> clean, 0 warnings (clippy --all-targets --workspace -D warnings)
cargo test-all                -> 536 passed; 0 failed; 0 ignored

No proto changes in this PR.

## Summary - Adds resource bounds to the tonic `Server::builder()` chain in `crates/nunciod/src/grpc.rs::serve_on_listener_with_overrides_and_shutdown`: `concurrency_limit_per_connection`, a per-call `timeout`, `max_concurrent_streams`, TCP keepalive, and HTTP/2 keepalive interval/timeout. - Adds a per-service `max_decoding_message_size` (16 MiB, above tonic's unconfigured 4 MiB default so a real email with an attachment still fits) to every one of the 8 mounted services, applied before each service is wrapped in its existing `BearerAuthInterceptor` (auth wiring untouched; shutdown wiring from the prior WS-C1 story untouched). - Named, documented constants: `GRPC_CONCURRENCY_LIMIT_PER_CONNECTION` (32), `GRPC_REQUEST_TIMEOUT` (300s), `GRPC_MAX_CONCURRENT_STREAMS` (64), `GRPC_MAX_DECODING_MESSAGE_SIZE_BYTES` (16 MiB), `GRPC_TCP_KEEPALIVE` (60s), `GRPC_HTTP2_KEEPALIVE_INTERVAL` (30s), `GRPC_HTTP2_KEEPALIVE_TIMEOUT` (20s). ### Why the timeout doesn't break `Subscribe` or long `Sync` calls tonic's `Server::timeout` wraps the handler's request future. For a server-streaming RPC (`System.Subscribe`) that future resolves as soon as the response stream is constructed, not when the stream finishes emitting items -- so a long-lived `Subscribe` connection is unaffected. For unary RPCs (`Mail.Sync`/`Calendar.Sync`/`Contacts.Sync`) the 300s deadline is a generous bound chosen to cover a slow real-world sync against a large mailbox while still failing a genuinely stuck backend call instead of hanging forever. This is a documented tradeoff, not a per-RPC exemption, since tonic's server-level `timeout` applies uniformly to the whole router. ## Test plan - [x] `send_message_rejects_body_larger_than_max_decoding_message_size` -- a body one byte over the configured 16 MiB limit is rejected with `Code::OutOfRange` at decode time, before the handler runs. - [x] `send_message_accepts_body_above_default_but_within_configured_limit` -- a 6 MiB body (above tonic's unconfigured 4 MiB default, within the configured 16 MiB) decodes successfully and reaches the handler, proving the raised limit is real and not a no-op. - [x] All existing E2E suites unchanged and green, including `Subscribe` streaming (`subscribe_streams_mapped_core_events_from_the_live_event_bus`, `subscribe_rejects_missing_bearer_token`) and `Sync` (`spine_e2e_test`, `calendar_e2e_test`, `contacts_e2e_test`, `sync::tests::*`). ### Gate output ``` cargo fmt --all -- --check -> clean cargo check-all -> clean, 0 warnings (clippy --all-targets --workspace -D warnings) cargo test-all -> 536 passed; 0 failed; 0 ignored ``` No proto changes in this PR.
Sign in to join this conversation.