feat(query): add New Flight transport - #20397
Conversation
7f734d9 to
d79c785
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. 🎉 Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
af1762d to
a1d76eb
Compare
Docker Image for PR
|
3132523 to
49dfaf2
Compare
87b234a to
8f4438c
Compare
| let mut statistics_receiver = statistics_receiver.lock(); | ||
|
|
||
| statistics_receiver.shutdown(info.res.is_err()); | ||
| let result = statistics_receiver.wait_shutdown(); |
There was a problem hiding this comment.
[P1] This reverses the lifecycle from shutdown -> on_finished_query -> wait_shutdown to shutdown -> wait_shutdown -> on_finished_query, creating a circular wait. wait_shutdown() blocks the pipeline-finish callback until the statistics handler exits; in reliable mode, that handler waits for the remote ReceiverClosed, while the remote statistics stream generally cannot terminate until on_finished_query() removes the coordinator and disconnects the connection. The query can therefore finish while the statistics stream remains open and occupies a pipeline thread for the lease duration. Please call on_finished_query() before waiting, or otherwise break this dependency, and add a regression test where the remote statistics stream has not closed proactively.
| { | ||
| ctx.get_exchange_manager() | ||
| .shutdown_query(&query_id, Some(cause)); | ||
| .shutdown_query(&query_id, Some(cause.clone())); |
There was a problem hiding this comment.
[P1] This changes statistics-send failures from warn-only to shutdown_query() plus tx.fail(), but StatisticsSender serves both New Flight and the existing Flight path. On the existing path, sending after the consumer has closed can return AbortedQuery; normal coordinator cleanup can therefore be promoted to a query-wide failure even when enable_experiment_new_flight=0. Please avoid changing the existing path's terminal semantics without gating this behavior by transport, and add a regression test for an already-closed consumer on the existing path.
| return Ok(None); | ||
| } | ||
|
|
||
| Ok(Some(Self::new( |
There was a problem hiding this comment.
[P2] Enabling New Flight does not necessarily enable reconnection. flight_max_retry_times defaults to 0, so reconnect_attempts() is empty, receiver_lease() is zero, and the first disconnect fails immediately. This implicit dependency makes it easy to believe reliable transport is active when it is not. Please reject this setting combination in from_settings(), or at least emit an explicit warning, and add coverage for the setting combinations.
| } | ||
|
|
||
| fn send_next(&mut self) { | ||
| if self.logical.in_flight.is_some() { |
There was a problem hiding this comment.
[P2] There is only one in_flight request, so every DATA packet must wait for its ACK. With the 256 KiB batch limit, per-stream throughput is bounded at roughly 256 KiB / RTT. This may be acceptable at sub-millisecond rack-local RTT, but it degrades substantially across AZs or regions. Please provide RTT/throughput measurements before merge, or increase the window beyond one and document the supported operating envelope.
| local_node_id: String, | ||
| remote_node_id: String, | ||
| ) -> Result<Self> { | ||
| Ok(Self { |
There was a problem hiding this comment.
[P2] connect() has already established do_exchange, but the driver is not spawned until the later start() call. During this interval no one polls response_stream, so remote FAIL/ReceiverClosed messages and physical disconnects are not detected promptly. If pipeline construction fails and this pending outbound is never consumed, there is also no Drop path that sends SenderFail; the peer can only wait for the lease to expire. Please start a controlled driver immediately after connection, or give the pending state explicit RAII/Drop termination semantics, and cover the connected-but-never-started case.
| channels, | ||
| completion: Some(Arc::new(ReliableCompletion { | ||
| streams, | ||
| remaining_producers: AtomicUsize::new(num_producers), |
There was a problem hiding this comment.
[P2] remaining_producers relies on a caller-supplied count that is not coupled to the actual producer or SharedOutboundChannels clone lifetime. Over-counting prevents FINISH forever; under-counting sends FINISH early; zero or an extra fetch_sub underflows to usize::MAX. Please bind the count to producer/clone lifetime, or at least add a clear invariant/assertion, and add a test that fails deterministically when the producer count is inconsistent.
| } | ||
|
|
||
| let accepted = self.deliver(data).await; | ||
| *next_sequence += 1; |
There was a problem hiding this comment.
[P2] next_sequence is advanced before accepted? is checked. If deliver() returns an error, the logical sequence has already moved forward, so a replay of the same sequence can be treated as previously delivered and ACKed. Please advance the sequence only after DeliveryOutcome::Accepted or an explicit terminal outcome, or encode and prove the invariant that any delivery error permanently terminates this source.
|
|
||
| /// Inbound queue quota per do_exchange connection. | ||
| // TODO: get max_bytes_per_connection from query settings | ||
| const MAX_INBOUND_BYTES_PER_CONNECTION: usize = 20 * 1024 * 1024; |
There was a problem hiding this comment.
[P3] The name still describes a 20 MiB per-connection limit, but New Flight creates the queue per source. For example, ten sources feeding one merge channel allow roughly 200 MiB of aggregate inbound buffering. Please rename and document this as a per-source quota, expose matching setting semantics, and evaluate the aggregate memory bound.
| reconnect.receiver_lease_secs(), | ||
| ) | ||
| } | ||
| NewFlightStream::Statistics => unreachable!( |
There was a problem hiding this comment.
[P3] This unreachable!() is justified only by the current caller handling Statistics first; it is not guaranteed by the wire or protocol layer. A later refactor can pass this valid enum variant here and panic the process. Please separate statistics from this enum or return an explicit error instead of encoding a call-order convention as a process-level panic.
I hereby agree to the terms of the CLA available at: https://docs.databend.com/dev/policies/cla/
Summary
This PR adds the complete default-off New Flight transport and separates transport mechanics from exchange-domain pipeline code.
It includes:
DoExchangetransport/legacy,transport/reliable, and exchange-owned adaptersImplementation
The old mixed
v1/networkmodule is replaced by two explicit transport implementations:v1/transport/legacy: the existing ping-pong transport and outbound bufferv1/transport/reliable: protocol framing, reconnect policy, logical inbound/outbound state machines, and theReliableInboundDeliveryboundaryExchange-specific concerns remain under
v1/exchange, includingDataBlockserialization, local channels, quota queues, TID/batch routing, pipeline processors, and statistics delivery. The reliable transport operates onFlightDataand no longer depends onSettings,DataBlock, pipeline types, orQueryContext.The setting-to-transport conversion is also owned by the exchange layer, so transport selection remains outside the protocol implementation.
Compatibility and rollout
enable_experiment_new_flightdefaults to0.Enable
Tests
Focused tests cover wire compatibility, packet validation, replay deduplication, reconnect lease replacement, reconnect budget exhaustion, sender/receiver failure propagation, legacy ping-pong and buffering, fragment TID routing, statistics completion/failure, and end-to-end exchange setup/cleanup.
Type of change
AI assistance
This change is