gRPC lifecycle verification
Exercise unary calls, server streams, and gRPC health across the shutdown boundary.
Draincheck can use unary gRPC calls as ordinary in-flight work and can hold one server-streaming RPC
open across the shutdown boundary. Both use plaintext HTTP/2, protobuf descriptors, and JSON
request input. They inherit target.container_port unless their own container_port is set.
gRPC services may also use the standard Health Check RPC for startup and withdrawal instead of
exposing an HTTP readiness endpoint. See the readiness guide; readiness health calls
do not require reflection or a descriptor set.
Unary traffic
traffic:
driver: grpc
container_port: 50051 # optional; defaults to target.container_port
grpc:
method: example.jobs.v1.Worker/Run
request: '{"job_id":"draincheck"}'
# request_file: ./testdata/run-request.json
metadata:
authorization: disposable-ci-token
# descriptor_set: ./api.protoset
expected_codes: [OK]
count: 5
concurrency: 5
shutdown_after: 500ms
request_timeout: 10smethod uses package.Service/Method form; a leading slash is also accepted. The method must be
unary. Draincheck resolves its input and output messages before traffic begins, decodes the request
with protobuf JSON rules, and invokes concurrent calls through one gRPC connection. Empty request
input means {}.
request and request_file are mutually exclusive. File paths are relative to the configuration
file, request input is capped at 1 MiB, response messages are capped at 1 MiB, and payloads are never
included in report evidence. expected_codes contains one or more canonical status names such as
OK, NOT_FOUND, or UNAVAILABLE; it defaults to [OK].
The ordinary traffic assertions retain their existing meanings. A call is active immediately before
the RPC begins, must remain active when signal delivery is confirmed, and succeeds only when its
final status is configured. The JSON traffic summary identifies driver: grpc; failed assertion
messages group failures by gRPC status.
Post-signal accept and reject policies also work with unary gRPC. A configured expected status is
accepted, an unexpected server status is rejected, and setup, timeout, cancellation, or transport
failures are invalid evidence rather than an intentional rejection.
Server-streaming shutdown
streaming:
grpc:
enabled: true
container_port: 50051 # optional; defaults to target.container_port
method: example.jobs.v1.Worker/Watch
request: '{"job_id":"draincheck"}'
metadata: {}
# descriptor_set: ./api.protoset
minimum_messages: 1
expected_code: OK
establish_timeout: 2s
close_timeout: 5sThe method must be server-streaming with one client request. Client-streaming and bidirectional
methods are rejected. Draincheck considers the observation established after minimum_messages
responses, up to 10,000. It establishes all enabled stream observations before starting ordinary
traffic.
At the signal request, the stream must still be active. It must then finish after the signal, no
later than close_timeout, and with expected_code. The close timeout is anchored to the signal
request and cannot exceed shutdown.deadline. A timeout cancels the observation and fails
grpc_stream.closed_gracefully.
Reports expose only bounded protocol evidence under streaming.grpc: enabled and established
state, signal-boundary activity, response count, final status, close timing, verdict, and a
classified error. They do not retain response messages, request JSON, descriptor contents, or
metadata values.
Separate readiness and gRPC ports
target.container_port remains the fallback for every probe. Set readiness.container_port,
traffic.container_port, or streaming.grpc.container_port only when that probe uses another
listener. Draincheck publishes every unique selected port once and maps each one to an independent
random loopback host port. For a common HTTP/gRPC split:
target:
container_port: 8080
readiness:
path: /ready
traffic:
driver: grpc
container_port: 50051
streaming:
grpc:
enabled: true
container_port: 50051Here readiness uses port 8080 while unary and streaming gRPC share port 50051.
Descriptor sources
When descriptor_set is omitted, Draincheck requests the service descriptor and its dependencies
through gRPC server reflection. Both the stable v1 reflection API and the older v1alpha API are
accepted. Reflection must be reachable through the mapped port selected for that gRPC probe.
For services that disable reflection, generate a descriptor set in CI or commit a non-sensitive one:
protoc --include_imports \
--descriptor_set_out=api.protoset \
-I proto proto/example/jobs/v1/worker.protoDescriptor-set paths are relative to draincheck.yaml and files are capped at 8 MiB. Include imports
so referenced request and response messages can be constructed without source .proto files.
Metadata, telemetry, and transport limits
Metadata keys must be lowercase gRPC keys. Pseudo-headers and the reserved grpc- prefix are
rejected. Configured metadata values and inline requests are redacted from debug bundles.
When trace-flush verification is enabled with unary gRPC traffic, Draincheck injects the same unique
W3C traceparent correlation value as outgoing gRPC metadata. The application must extract that
metadata into its trace context for correlated spans to count.
The initial implementation supports plaintext gRPC on a selected published container port. TLS, custom certificate authorities, mutual TLS, client-streaming, bidirectional streaming, compression configuration, and per-message payload matching are deferred. A command traffic adapter remains available when one of those behaviors is required.