-
Notifications
You must be signed in to change notification settings - Fork 13
feat: driver high-water marks #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
e81e3f4
1a32535
998e96f
88aff13
15b0aa6
3196ba8
b98636b
fc90afc
204210a
1341c03
e118d05
05af34d
79854fd
4758403
5424fef
8260946
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,4 @@ | ||
| use std::{ | ||
| collections::VecDeque, | ||
| io, | ||
| pin::Pin, | ||
| sync::Arc, | ||
|
|
@@ -56,7 +55,10 @@ pub(crate) struct PeerState<T: AsyncRead + AsyncWrite, A: Address> { | |
| write_buffer_size: usize, | ||
| /// The address of the peer. | ||
| addr: A, | ||
| egress_queue: VecDeque<WithSpan<reqrep::Message>>, | ||
| /// Single pending outgoing message waiting to be sent. | ||
| pending_egress: Option<WithSpan<reqrep::Message>>, | ||
| /// High-water mark for pending responses. When reached, new responses are dropped. | ||
| pending_responses_hwm: Option<usize>, | ||
| state: Arc<SocketState>, | ||
| /// The optional message compressor. | ||
| compressor: Option<Arc<dyn Compressor>>, | ||
|
|
@@ -166,7 +168,8 @@ where | |
| linger_timer, | ||
| write_buffer_size: this.options.write_buffer_size, | ||
| addr: auth.addr, | ||
| egress_queue: VecDeque::with_capacity(128), | ||
| pending_egress: None, | ||
| pending_responses_hwm: this.options.pending_responses_hwm, | ||
| state: Arc::clone(&this.state), | ||
| compressor: this.compressor.clone(), | ||
| }), | ||
|
|
@@ -262,7 +265,8 @@ where | |
| linger_timer, | ||
| write_buffer_size: self.options.write_buffer_size, | ||
| addr, | ||
| egress_queue: VecDeque::with_capacity(128), | ||
| pending_egress: None, | ||
| pending_responses_hwm: self.options.pending_responses_hwm, | ||
| state: Arc::clone(&self.state), | ||
| compressor: self.compressor.clone(), | ||
| }), | ||
|
|
@@ -313,24 +317,24 @@ where | |
| } | ||
|
|
||
| impl<T: AsyncRead + AsyncWrite + Unpin, A: Address> PeerState<T, A> { | ||
| /// Prepares for shutting down by sending and flushing all messages in [`Self::egress_queue`]. | ||
| /// Prepares for shutting down by sending and flushing all pending messages. | ||
| /// When [`Poll::Ready`] is returned, the connection with this peer can be shutdown. | ||
| /// | ||
| /// TODO: there might be some [`Self::pending_requests`] yet to processed. TBD how to handle | ||
| /// them, for now they're dropped. | ||
|
Comment on lines
363
to
364
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this still relevant?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, because we would still have requests in |
||
| fn poll_shutdown(&mut self, cx: &mut Context<'_>) -> Poll<()> { | ||
| let messages = std::mem::take(&mut self.egress_queue); | ||
| let pending_msg = self.pending_egress.take(); | ||
| let buffer_size = self.conn.write_buffer().len(); | ||
| if messages.is_empty() && buffer_size == 0 { | ||
| if pending_msg.is_none() && buffer_size == 0 { | ||
| debug!("flushed everything, closing connection"); | ||
| return Poll::Ready(()); | ||
| } | ||
|
|
||
| debug!(messages = ?messages.len(), write_buffer_size = ?buffer_size, "found data to send"); | ||
| debug!(has_pending = ?pending_msg.is_some(), write_buffer_size = ?buffer_size, "found data to send"); | ||
|
|
||
| for msg in messages { | ||
| if let Some(msg) = pending_msg { | ||
| if let Err(e) = self.conn.start_send_unpin(msg.inner) { | ||
| error!(?e, "failed to send final messages to socket, closing"); | ||
| error!(?e, "failed to send final message to socket, closing"); | ||
| return Poll::Ready(()); | ||
| } | ||
| } | ||
|
|
@@ -351,15 +355,15 @@ impl<T: AsyncRead + AsyncWrite + Unpin, A: Address + Unpin> Stream for PeerState | |
| let this = self.get_mut(); | ||
|
|
||
| loop { | ||
| let mut progress = false; | ||
| if let Some(msg) = this.egress_queue.pop_front().enter() { | ||
| // First, try to send the pending egress message if we have one. | ||
| if let Some(msg) = this.pending_egress.take().enter() { | ||
| let msg_len = msg.size(); | ||
| debug!(msg_id = msg.id(), "sending response"); | ||
| match this.conn.start_send_unpin(msg.inner) { | ||
| Ok(_) => { | ||
| this.state.stats.specific.increment_tx(msg_len); | ||
|
|
||
| // We might be able to send more queued messages | ||
| progress = true; | ||
| // Continue to potentially send more or flush | ||
| continue; | ||
| } | ||
| Err(e) => { | ||
| this.state.stats.specific.increment_failed_requests(); | ||
|
|
@@ -392,68 +396,54 @@ impl<T: AsyncRead + AsyncWrite + Unpin, A: Address + Unpin> Stream for PeerState | |
| } | ||
| } | ||
|
|
||
| // Then, try to drain the egress queue. | ||
| if this.conn.poll_ready_unpin(cx).is_ready() { | ||
| if let Some(msg) = this.egress_queue.pop_front().enter() { | ||
| let msg_len = msg.size(); | ||
|
|
||
| debug!(msg_id = msg.id(), "sending response"); | ||
| match this.conn.start_send_unpin(msg.inner) { | ||
| Ok(_) => { | ||
| this.state.stats.specific.increment_tx(msg_len); | ||
|
|
||
| // We might be able to send more queued messages | ||
| continue; | ||
| } | ||
| Err(e) => { | ||
| this.state.stats.specific.increment_failed_requests(); | ||
| error!(?e, "failed to send message to socket"); | ||
| // End this stream as we can't send any more messages | ||
| return Poll::Ready(None); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Then we check for completed requests, and push them onto the egress queue. | ||
| if let Poll::Ready(Some(result)) = this.pending_requests.poll_next_unpin(cx).enter() { | ||
| match result.inner { | ||
| Err(_) => tracing::error!("response channel closed unexpectedly"), | ||
| Ok(Response { msg_id, mut response }) => { | ||
| let mut compression_type = 0; | ||
| let len_before = response.len(); | ||
| if let Some(ref compressor) = this.compressor { | ||
| match compressor.compress(&response) { | ||
| Ok(compressed) => { | ||
| response = compressed; | ||
| compression_type = compressor.compression_type() as u8; | ||
| } | ||
| Err(e) => { | ||
| error!(?e, "failed to compress message"); | ||
| continue; | ||
| // Check for completed requests, and set pending_egress (only if empty). | ||
| if this.pending_egress.is_none() { | ||
| if let Poll::Ready(Some(result)) = this.pending_requests.poll_next_unpin(cx).enter() | ||
| { | ||
| match result.inner { | ||
| Err(_) => tracing::error!("response channel closed unexpectedly"), | ||
| Ok(Response { msg_id, mut response }) => { | ||
| let mut compression_type = 0; | ||
| let len_before = response.len(); | ||
| if let Some(ref compressor) = this.compressor { | ||
| match compressor.compress(&response) { | ||
| Ok(compressed) => { | ||
| response = compressed; | ||
| compression_type = compressor.compression_type() as u8; | ||
| } | ||
| Err(e) => { | ||
| error!(?e, "failed to compress message"); | ||
| continue; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| debug!( | ||
| msg_id, | ||
| len_before, | ||
| len_after = response.len(), | ||
| "compressed message" | ||
| ) | ||
| } | ||
| debug!( | ||
| msg_id, | ||
| len_before, | ||
| len_after = response.len(), | ||
| "compressed message" | ||
| ) | ||
| } | ||
|
|
||
| debug!(msg_id, "received response to send"); | ||
| debug!(msg_id, "received response to send"); | ||
|
|
||
| let msg = reqrep::Message::new(msg_id, compression_type, response); | ||
| this.egress_queue.push_back(msg.with_span(result.span)); | ||
| let msg = reqrep::Message::new(msg_id, compression_type, response); | ||
| this.pending_egress = Some(msg.with_span(result.span)); | ||
|
|
||
| continue; | ||
| continue; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Finally we accept incoming requests from the peer. | ||
| { | ||
| // Accept incoming requests from the peer. | ||
| // Only accept new requests if we're under the HWM for pending responses. | ||
| let under_hwm = this | ||
| .pending_responses_hwm | ||
| .map(|hwm| this.pending_requests.len() < hwm) | ||
| .unwrap_or(true); | ||
|
|
||
| if under_hwm { | ||
| let _g = this.span.clone().entered(); | ||
| match this.conn.poll_next_unpin(cx) { | ||
| Poll::Ready(Some(result)) => { | ||
|
|
@@ -504,10 +494,13 @@ impl<T: AsyncRead + AsyncWrite + Unpin, A: Address + Unpin> Stream for PeerState | |
| } | ||
| Poll::Pending => {} | ||
| } | ||
| } | ||
|
|
||
| if progress { | ||
| continue; | ||
| } else { | ||
| // At HWM - log warning and don't accept new requests until responses drain | ||
| trace!( | ||
| hwm = ?this.pending_responses_hwm, | ||
| pending = this.pending_requests.len(), | ||
| "at high-water mark, not accepting new requests" | ||
| ); | ||
| } | ||
|
|
||
| return Poll::Pending; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,4 @@ | ||
| use std::{ | ||
| collections::VecDeque, | ||
| pin::Pin, | ||
| sync::Arc, | ||
| task::{Context, Poll, ready}, | ||
|
|
@@ -14,11 +13,8 @@ use tokio::{ | |
| time::Interval, | ||
| }; | ||
|
|
||
| use super::{ReqError, ReqOptions}; | ||
| use crate::{ | ||
| SendCommand, | ||
| req::{SocketState, conn_manager::ConnManager}, | ||
| }; | ||
| use super::{ReqError, ReqOptions, SendCommand}; | ||
| use crate::req::{SocketState, conn_manager::ConnManager}; | ||
|
|
||
| use msg_common::span::{EnterSpan as _, SpanExt as _, WithSpan}; | ||
| use msg_transport::{Address, Transport}; | ||
|
|
@@ -42,8 +38,8 @@ pub(crate) struct ReqDriver<T: Transport<A>, A: Address> { | |
| pub(crate) conn_manager: ConnManager<T, A>, | ||
| /// The timer for the write buffer linger. | ||
| pub(crate) linger_timer: Option<Interval>, | ||
| /// The outgoing message queue. | ||
| pub(crate) egress_queue: VecDeque<WithSpan<reqrep::Message>>, | ||
| /// The single pending outgoing message waiting to be sent. | ||
| pub(crate) pending_egress: Option<WithSpan<reqrep::Message>>, | ||
| /// The currently pending requests waiting for a response. | ||
| pub(crate) pending_requests: FxHashMap<u32, WithSpan<PendingRequest>>, | ||
| /// Interval for checking for request timeouts. | ||
|
|
@@ -106,8 +102,23 @@ where | |
| } | ||
|
|
||
| /// Handle an incoming command from the socket frontend. | ||
| fn on_send(&mut self, cmd: SendCommand) { | ||
| /// Returns `true` if the command was accepted, `false` if HWM was reached. | ||
| fn on_send(&mut self, cmd: SendCommand) -> bool { | ||
| let SendCommand { mut message, response } = cmd; | ||
|
|
||
| // Check high-water mark before accepting the request | ||
| if let Some(hwm) = self.options.pending_requests_hwm { | ||
| if self.pending_requests.len() >= hwm { | ||
| tracing::warn!( | ||
| hwm, | ||
| pending = self.pending_requests.len(), | ||
| "high-water mark reached, rejecting request" | ||
| ); | ||
| let _ = response.send(Err(ReqError::HighWaterMarkReached(hwm))); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| let start = Instant::now(); | ||
|
|
||
| // We want ot inherit the span from the socket frontend | ||
|
|
@@ -135,9 +146,11 @@ where | |
| let msg = message.inner.into_wire(self.id_counter); | ||
| let msg_id = msg.id(); | ||
| self.id_counter = self.id_counter.wrapping_add(1); | ||
| self.egress_queue.push_back(msg.with_span(span.clone())); | ||
| self.pending_egress = Some(msg.with_span(span.clone())); | ||
| self.pending_requests | ||
| .insert(msg_id, PendingRequest { start, sender: response }.with_span(span)); | ||
|
|
||
| true | ||
| } | ||
|
|
||
| /// Check for request timeouts and notify the sender if any requests have timed out. | ||
|
|
@@ -215,12 +228,10 @@ where | |
| Poll::Pending => {} | ||
| } | ||
|
|
||
| // NOTE: We try to drain the egress queue first (the `continue`), writing everything to | ||
| // the `Framed` internal buffer. When all messages are written, we move on to flushing | ||
| // the connection in the block below. We DO NOT rely on the `Framed` internal | ||
| // backpressure boundary, because we do not call `poll_ready`. | ||
| if let Some(msg) = this.egress_queue.pop_front().enter() { | ||
| // Generate the new message | ||
| // Try to send the pending egress message if we have one. | ||
| // We only hold a single pending message here; the channel serves as the actual queue. | ||
| // This pattern ensures we respect backpressure and don't accumulate unbounded messages. | ||
| if let Some(msg) = this.pending_egress.take().enter() { | ||
| let size = msg.size(); | ||
| tracing::debug!("Sending msg {}", msg.id()); | ||
| // Write the message to the buffer. | ||
|
|
@@ -236,7 +247,7 @@ where | |
| } | ||
| } | ||
|
|
||
| // We might be able to write more queued messages to the buffer. | ||
| // Continue to potentially send more or flush | ||
| continue; | ||
|
||
| } | ||
|
|
||
|
|
@@ -267,25 +278,28 @@ where | |
| this.check_timeouts(); | ||
| } | ||
|
|
||
| // Check for outgoing messages from the socket handle | ||
| match this.from_socket.poll_recv(cx) { | ||
| Poll::Ready(Some(cmd)) => { | ||
| this.on_send(cmd); | ||
| // Check for outgoing messages from the socket handle. | ||
| // Only poll when pending_egress is empty to maintain backpressure. | ||
| if this.pending_egress.is_none() { | ||
| match this.from_socket.poll_recv(cx) { | ||
| Poll::Ready(Some(cmd)) => { | ||
| this.on_send(cmd); | ||
|
|
||
| continue; | ||
| } | ||
| Poll::Ready(None) => { | ||
| tracing::debug!( | ||
| "socket dropped, shutting down backend and flushing connection" | ||
| ); | ||
|
|
||
| if let Some(channel) = this.conn_manager.active_connection() { | ||
| let _ = ready!(channel.poll_close_unpin(cx)); | ||
| continue; | ||
| } | ||
| Poll::Ready(None) => { | ||
| tracing::debug!( | ||
| "socket dropped, shutting down backend and flushing connection" | ||
| ); | ||
|
|
||
| return Poll::Ready(()); | ||
| if let Some(channel) = this.conn_manager.active_connection() { | ||
| let _ = ready!(channel.poll_close_unpin(cx)); | ||
| } | ||
|
|
||
| return Poll::Ready(()); | ||
| } | ||
| Poll::Pending => {} | ||
| } | ||
| Poll::Pending => {} | ||
| } | ||
|
|
||
| return Poll::Pending; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.