feat(volo-grpc): derive HTTP/2 :authority from the callee endpoint on a shared core transport pool - #670
Open
tonyvelichko wants to merge 3 commits into
Open
feat(volo-grpc): derive HTTP/2 :authority from the callee endpoint on a shared core transport pool#670tonyvelichko wants to merge 3 commits into
tonyvelichko wants to merge 3 commits into
Conversation
Introduce `volo::pool`, a generic transport pool for the protocol crates, ported from volo-thrift's pool and generalized: - `Pool<K, T: Poolable>` keyed by peer (usually `net::Address`), with `Mode::Unique` (exclusive, handed back via `Pooled::reuse`) and `Mode::Shared` (multiplexed, one transport per key handed out as clones, concurrent connects deduplicated) - new transports come from any `motore::UnaryService<K>`; errors surface as `pool::Error<E>` so each crate maps them to its own error type - the connect runs as its own task, so a caller giving up (rpc timeout) no longer cancels the transport other waiters are queued on - the idle-eviction task starts lazily on first `get`, making `Pool::new` safe outside a runtime volo-grpc switches to it in the next commit; volo-thrift and volo-http still carry their own copies and are to be migrated.
The client transport used hyper-util's legacy `Client`, whose pool key,
dial target and `:authority` are all the request URI's authority, so the
wire `:authority` was always the resolved `ip:port`. That breaks any
TLS-terminating proxy or ingress routing on virtual host.
Manage HTTP/2 connections in `volo::pool::Pool<Address, _>` (shared mode,
90s idle timeout) keyed by the load-balanced address, and build the
request URI from the callee `Endpoint` instead:
0. an explicit `client::Authority` faststr tag on the callee, if set
(per call via `CallOpt::callee_faststr_tags`, or from a layer)
1. the TLS server name (SNI), plus the dialed port unless it is 443
2. the callee `service_name` when it is a valid authority (the
`host:port` given to the DNS resolver, or a logical service name)
3. the dialed address
`:scheme` is now `https` when TLS is configured. Load balancing keeps one
connection per instance regardless of `:authority`. A request handed
back untouched by hyper because the connection died is retried once.
Behaviour change: plaintext clients with a logical `service_name` now
send that name as `:authority` instead of `ip:port`.
Drops the `hex` dependency and hyper-util's `client-legacy` feature.
Adds docs/grpc-client-transport.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
volo-grpc clients cannot be put behind a TLS-terminating proxy or ingress that routes on the HTTP/2
:authority(virtual host). The client transport used hyper-util's legacyClient, whose pool key, dial target and wire:authorityare all the same string — the request URI's authority — sobuild_uri()had to put the resolvedip:portthere, and every call arrived at the proxy as:authority: 10.42.3.17:50051. HTTP/2 derives:authoritysolely from the URI (noHostheader fallback), so this could not be fixed at the header level.Fixing it inside hyper's pool is not possible either: giving the URI a host name either forces DNS resolution back into the connector (bypassing volo's
Discover/LoadBalance) or collapses load balancing to one connection per host name, because hyper pools by authority.Solution
Two commits, one per crate:
feat(volo): add generic transport pool— introducesvolo::pool, a generic transport pool for the protocol crates, ported from volo-thrift's pool and generalized:Pool<K, T: Poolable>keyed by peer (usuallynet::Address), withMode::Unique(exclusive; handed back viaPooled::reuse) andMode::Shared(multiplexed; one transport per key handed out as clones, concurrent connects deduplicated).motore::UnaryService<K>; errors surface aspool::Error<E>, so each crate maps them to its own error type (no thriftTransportExceptioncoupling).get, soPool::newis safe outside a runtime (e.g. in aLazyLock).volo-thrift and volo-http still carry their own copies of this design; migrating them is left as a follow-up to keep this PR reviewable.
feat(volo-grpc): derive HTTP/2 :authority from the callee endpoint— the transport keeps HTTP/2 connections involo::pool::Pool<Address, _>(shared mode, 90s idle timeout, matching hyper's default) keyed by the load-balanced address, and builds the request URI from the calleeEndpoint.:authorityis chosen by the first rule that applies:volo_grpc::client::Authorityfaststr tag on the callee (per call viaCallOpt::callee_faststr_tags, or from a layer);:authorityalways agree;service_namewhen it is a valid authority — thehost:portgiven to the DNS resolver, or a logical service name with a customDiscover(what grpc-go and tonic send);:schemeis nowhttpswhen TLS is configured. Load balancing keeps one connection per instance regardless of:authority. A request that hyper hands back untouched because the connection died underneath it is retried once on a fresh connection.hexand hyper-util'sclient-legacyfeature are no longer needed.Usage for the motivating case needs no new configuration:
Behaviour change
Plaintext clients whose
service_nameis a logical name (e.g.hello) now send that name as:authorityinstead of127.0.0.1:8080. gRPC servers ignore:authorityunless they route on it.Release note
volomust be released beforevolo-grpc(new public modulevolo::pool).Tests
volo::pool: 10 unit tests — unique reuse/discard, shared dedupe of concurrent connects, the connect surviving the first caller's cancellation, closed-transport replacement, connect-error reporting, idle eviction, idle task stopping with the pool.:authorityrule (incl. invalid tag/service names falling through), and wire-level tests against a real hyper HTTP/2 server asserting the:authorityreceived, connection reuse per address, distinct connections per address, 32 concurrent first calls → 1 handshake, and theAuthoritytag on the wire.cargo +nightly fmt --all --check;cargo clippy --deny warningsfor volo (default,rustls-aws-lc-rs,rustls-ring), volo-grpc (no-default,rustls,native-tls,grpc-web) and--all;cargo test -p volo,cargo test -p volo-grpc --features rustls;hello,hello-tlsandloadbalancegRPC examples run end-to-end.Docs:
docs/grpc-client-transport.mddescribes the:authorityrules, the pooling behaviour, and how a protocol crate adoptsvolo::pool.