autumn-plugin-grpc
gRPC for Autumn, built on tonic 0.14.
Add one plugin. Autumn serves HTTP on its port. The plugin serves gRPC on a dedicated HTTP/2 port, or on Autumn's port (see Share Autumn's port). Handlers can also call other gRPC services (see Call gRPC services).
use GrpcPlugin;
async
Features
| Feature | Default |
|---|---|
| Dedicated HTTP/2 listener | 127.0.0.1:50051 in dev/test, 0.0.0.0:50051 in other profiles |
| Shared listener on Autumn's HTTP port | off; crate feature multiplex |
Health service grpc.health.v1.Health |
on |
| Server reflection (v1 and v1alpha) | on in dev/test, off in other profiles |
grpc indicator in /actuator/health |
on |
grpc_server_* metrics in /actuator/prometheus |
on |
AppState in the extensions of each request |
always |
| Graceful drain on shutdown | 10 s grace |
| Guards: tower layers and tonic interceptors (user services only) | none |
| Resource limits: connections, HTTP/2 streams, stream resets | 1000, 200, 1024 |
| TLS and mTLS | crate feature tls |
Clients for handlers: GrpcClient<T>, GrpcClients |
off; crate feature client |
Install
[]
= "0.3"
# With TLS:
# autumn-plugin-grpc = { version = "0.3", features = ["tls"] }
# On Autumn's HTTP port:
# autumn-plugin-grpc = { version = "0.3", features = ["multiplex"] }
# To call gRPC services from handlers:
# autumn-plugin-grpc = { version = "0.3", features = ["client"] }
The crate re-exports tonic, tonic_health and tonic_reflection. Use
these re-exports, or use tonic 0.14 in your app.
Generate code
Use tonic-prost-build in your build.rs. Also write the descriptor set,
for reflection and for exact metric labels:
// build.rs
let out = from;
configure
.file_descriptor_set_path
.compile_protos?;
pub const FILE_DESCRIPTOR_SET: & =
include_bytes!;
Use app state
Each request has AppState in its extensions:
async
To build a service from AppState at startup, use add_service_with:
new.add_service_with
Guard calls
A guard wraps user services only. Health and reflection stay open, so probes and tools work.
new
.add_service
.guard_interceptor
guard(layer, label)takes any tower layer that axum accepts.layer(l)andinterceptor(i)also wrap user services. Use them for work that is not access control, for example tracing.
Route listing
autumn routes shows each service as GRPC /<service>/*. A plugin on a
named section, such as grpc_admin, shows GRPC:grpc_admin as the
method. Autumn 0.8 refuses two plugins that declare the same method and
path:
| Plugin call | Classification |
|---|---|
guard or guard_interceptor |
gated, with the label as middleware |
public() |
public |
| none of these | unclassified: autumn routes audit fails |
| health and reflection | framework |
The audit applies HTTP rules. Its CSRF and mTLS columns do not apply to the gRPC listener.
Configure
The plugin reads [grpc] from autumn.toml. It uses the same layers as
Autumn core: base file, [profile.<name>.grpc], autumn-<profile>.toml,
then AUTUMN_GRPC__* environment variables. These stop boot:
- an unknown key,
- a bad value in a file or in an environment variable,
- an unreadable
.envfile.
[]
= true
= "dedicated" # "dedicated" or "shared" (feature `multiplex`)
= "" # IP:port; empty: the profile decides
= true
= "auto" # true, false or "auto"
= true
= 1000
= 10000 # must be more than 0
= 0 # unary calls; 0: off
= 1000 # 0: off
= 0 # 0: off; a stream counts until it responds
= 200 # must be more than 0
= 1024 # must be more than 0
= true
= 0 # 0: off
= 60000 # 0: off
= 20000
= 0 # 0: off
[] # needs the `tls` feature
= ""
= ""
= "" # set it to require client certificates
= false # needs client_ca_path
= 10000 # must be more than 0
[] # needs the `client` feature; one table for each client
= "http://billing:50051" # http:// or https://, no path; necessary
= 10000 # each call; 0: off
= 5000 # must be more than 0
[] # https only; needs the `tls` feature
= "" # necessary for https
= "" # client certificate (mTLS); needs key_path
= ""
= "" # empty: the endpoint host
A Toggle (reflection) accepts true, false, "auto", "on",
"off", "yes", "no", "enabled" and "disabled".
Examples of environment overrides:
AUTUMN_GRPC__BIND=0.0.0.0:6000
AUTUMN_GRPC__TLS__CERT_PATH=/etc/certs/server.pem
AUTUMN_GRPC__CLIENTS__BILLING__ENDPOINT=http://billing:50051
A client env override works for each client in the files and for each name that the code registers.
Set values in code, on top of the file values:
new.bind.configure
If you set [grpc.tls] without the tls feature, boot stops. The plugin
does not fall back to plain text.
Two servers
Use a second section. The environment prefix changes with the section.
.plugin
.plugin
// The second plugin reads [grpc_admin] and AUTUMN_GRPC_ADMIN__*.
- Each server needs its own section. The plugin name contains the section, and Autumn skips a second plugin with the same name.
- Metrics have a
serverlabel with the section. AppStatehas aGrpcServersvalue.GrpcServers::get(section)gives the handle of each server.AppStatehas aGrpcHandleonly for thegrpcsection.
Share Autumn's port
With the multiplex feature, gRPC can use Autumn's HTTP port:
[]
= "shared"
- The plugin binds no port.
GrpcHandle::local_addr()isNone. - HTTP/2 requests with a gRPC content type go to the gRPC services,
before Autumn's HTTP middleware. No Autumn HTTP protection applies to
them: CSRF, sessions, the request timeout, body limits, rate limits,
load shedding, maintenance mode, trusted hosts, bot protection, CORS,
error pages and HTTP metrics. Use
guardandlayerfor gRPC. tonic's message size limits (4 MiB) still apply. - All other requests go to Autumn as before. This includes HTTP/1.1 and
application/grpc-webrequests. - Health, reflection, metrics, guards,
AppState,timeout_msandgrpc-timeoutwork as with a dedicated listener. request.remote_addr()gives the TCP peer.local_addr()isNone. On a Unix socket,remote_addr()isNone.- The feature turns on HTTP/2 (h2c) in axum for the whole app.
- Autumn's server owns the connections. These settings have no effect,
and the plugin logs a warning when you change them:
bind,max_connections,concurrency_limit_per_connection,max_concurrent_streams,http2_max_local_error_reset_streams,tcp_nodelay,tcp_keepalive_ms,http2_keepalive_*,max_connection_age_msandtls.handshake_timeout_ms. hyper's defaults apply (200 streams, 1024 local resets). - Only one plugin in an app can use shared mode.
These stop boot:
listener = "shared"without themultiplexfeature,[grpc.tls]in shared mode (use[server.tls]),[server.tls]on autumn-web 0.7. Its TLS listener does not offer HTTP/2 (ALPNh2). autumn-foundation/autumn#2321 fixes this in the next Autumn release. Until then, end TLS at a proxy or use a dedicated listener.
At shutdown:
- Autumn reports not ready. Health reports
NOT_SERVING. Calls still run duringserver.prestop_grace_secs. - Autumn stops its listener. The plugin starts its drain at the same
time: health
Watchstreams end, new calls getUNAVAILABLE, and open calls get the grace period. - After the grace, the plugin ends open calls with
UNAVAILABLE. Then Autumn's HTTP drain can finish.
A client that stops reading a stream can keep its connection open until Autumn's shutdown timeout. See ADR 0008.
Call gRPC services
With the client feature, a handler calls a downstream service with one
extractor:
use ;
async
new.client
[]
= "http://billing:50051"
-
The client type is
<Generated>Client<GrpcChannel>.GrpcChannelwraps a tonicChannel. It adds request context, a deadline and metrics to each call. -
GrpcClient<T>gives the only client of typeT. For two endpoints of one type, register two names and useGrpcClients:asyncAppStatealso hasGrpcClients(state.extension::<GrpcClients>()). A client fromAppStatesends no request context. -
The clients do not need the server. They work with
enabled = false. -
The channel connects at the first call. A down endpoint does not stop boot and does not change readiness. Its calls fail with
UNAVAILABLE.
Request context
Each call from an extractor sends:
x-request-id: Autumn's request ID.traceparentandtracestate: the values of the incoming request. They go only whentraceparentis a valid W3C value, andtracestateonly when it has 512 bytes or less.grpc-timeout: the smallest of the clienttimeout_ms, the time left on the incoming request, and a timeout that the caller set withRequest::set_timeout.
Rules:
- The channel does not replace metadata that the caller sets. The trace
pair goes only when the caller set neither
traceparentnortracestate. - The time left is
server.timeouts.request_timeout_msminus the time since the extractor ran.0or no value: no limit from the request. Autumn does not publish the request start or per-routetimeoutvalues, so the time before the extractor is not counted (ADR 0009). - When no time is left, the call ends with
DEADLINE_EXCEEDEDand is not sent. - The client trusts the incoming trace context, as Autumn does. The
request ID and the trace pair go to every endpoint, also to services of
other companies. Other headers, such as
authorization, do not go.
Errors
A bare ? on tonic::Status gives a 500: Autumn converts every error
type to 500. Use .or_http()? (or status_to_error) for this map:
| gRPC code | HTTP |
|---|---|
INVALID_ARGUMENT, OUT_OF_RANGE, FAILED_PRECONDITION |
400 |
UNAUTHENTICATED |
401 |
PERMISSION_DENIED |
403 |
NOT_FOUND |
404 |
ALREADY_EXISTS, ABORTED |
409 |
RESOURCE_EXHAUSTED |
429 |
UNAVAILABLE |
503 |
DEADLINE_EXCEEDED (also a client timeout) |
504 |
| all others | 502 |
- A 4xx response has a fixed text, for example
not found. The downstream message does not go to the HTTP caller. - A 5xx response has the code and the downstream message. Autumn shows
it only in
dev. - The log has the full status.
UNAUTHENTICATEDandPERMISSION_DENIEDfrom the downstream service become 401 and 403. If the app's own service credentials fail, the HTTP caller also gets 401 or 403. Match on theStatusyourself if you need a different result.
A failed client lookup gives a 500: no registration, the wrong type, or
two clients of one type for GrpcClient<T>. Autumn shows the message
only in dev.
These errors stop boot:
- a registered client with no endpoint and no test double,
- a client name that two registrations use (one type, or two plugins),
- a bad endpoint (only lowercase
http://andhttps://), httpswithout thetlsfeature, or withouttls.ca_path,- a TLS file that cannot be read.
These log a warning at boot: a [<section>.clients.<name>] table with no
registration, and one client type on two names.
Test doubles
A test double is a fake service in the process. Point a client at it. The client uses no port:
let mut config = default;
config.enabled = false; // this app serves no gRPC itself
let plugin = new
.config
.development
.client
.client_double;
let client = new.routes.plugin.build;
The double uses the same GrpcChannel, so the test sees the metadata,
the timeouts and the metrics.
Security
- The defaults limit connections, HTTP/2 streams and stream resets (a protection against reset floods). The TLS handshake has a timeout.
- The health and reflection services decode requests of 16 KiB at most.
- User services use the tonic decode limit (4 MiB). Set a lower limit on
each generated server, for example
GreeterServer::new(g).max_decoding_message_size(64 * 1024). - In
devandtest, the listener is on loopback. The plugin logs a warning when reflection is on and the listener is not on loopback. - With
client_auth_optional = true, clients without a certificate can connect. Checkrequest.peer_certs()in a guard. The plugin accepts every certificate thatclient_ca_pathsigns. Check the identity in a guard if you need more. - The health
Checkcall shows if a service name exists, also when reflection is off. This is standard gRPC behavior. - Clients send the request ID and a valid
traceparent(and atracestateof 512 bytes or less) of the incoming request to each endpoint. They do not send other headers, such asauthorization. - Client endpoints accept only lowercase
http://andhttps://. Anhttpsendpoint needs thetlsfeature andtls.ca_path. The plugin never falls back to plain text. - The clients add no app layer. Autumn's idempotency replay does not change.
Health
- The gRPC health service reports
SERVINGfor""and for each user service after start. - It follows Autumn readiness. When Autumn starts to shut down (or an
operator drains it), it reports
NOT_SERVINGat once. Calls still run. GrpcHandle::health_reporter()lets the app change a status at runtime.- The Autumn indicator (its name is the section, default
grpc) isUPonly while the server serves. It is in the readiness group. Set[health] detailed = trueto see itsstateandaddress.
Metrics
| Name | Type | Labels |
|---|---|---|
grpc_server_handled_total |
counter | server, grpc_service, grpc_method, grpc_code |
grpc_server_handling_seconds_sum |
counter | server, grpc_service, grpc_method |
grpc_server_handling_seconds_count |
counter | server, grpc_service, grpc_method |
grpc_server_in_flight |
gauge | server |
grpc_server_up |
gauge | server |
grpc_client_handled_total |
counter | client, grpc_service, grpc_method, grpc_code |
grpc_client_handling_seconds_sum |
counter | client, grpc_service, grpc_method |
grpc_client_handling_seconds_count |
counter | client, grpc_service, grpc_method |
grpc_client_in_flight |
gauge | client |
Remote callers control the request path. The plugin limits the label sets:
- It labels an unknown service
unknown. - It labels a method
unknownuntil the method is known. A method is known when a registered descriptor set lists it, or after its firstOKresponse. - After
max_metric_serieslabel sets, it labels new service and method pairsother. The code label stays.
Client labels: the client label is a registered name. App code sets the
path, so service and method keep their names. A value that is not a valid
name is unknown. max_metric_series caps the label sets of each
client.
Shutdown
- Autumn starts to shut down. Health reports
NOT_SERVINGat once. - Autumn drains HTTP, then runs the plugin shutdown hook, inside
server.shutdown_timeout_secs. - The plugin stops accepting connections and sends HTTP/2
GOAWAY. - It waits for in-flight calls, up to the grace period.
- It closes the connections that are still open.
The plugin caps the grace period: it must fit in
server.shutdown_timeout_secs, with 1 s left to close connections. A task
does the drain, so it completes also when Autumn stops the hook.
To drain earlier, call GrpcHandle::shutdown(). Get the handle from
GrpcPlugin::handle(), or from AppState:
let handle = state.;
Tests
bind("127.0.0.1:0") picks a free port. GrpcHandle::local_addr() gives
the real address after boot. autumn_web::test::TestApp runs the startup
hook, so the server is up when build() returns. TestApp does not run
shutdown hooks: call handle.shutdown().await yourself.
Example
Compatibility
| autumn-plugin-grpc | autumn-web | tonic | MSRV |
|---|---|---|---|
| 0.3 | 0.10 | 0.14 | 1.97 |
| 0.2 | 0.9 | 0.14 | 1.88 |
| 0.1 | 0.8 | 0.14 | 1.88 |
Documents
License
Apache-2.0