-
Notifications
You must be signed in to change notification settings - Fork 173
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
fix(ws server): fix shutdown on connection closed #1103
Changes from 5 commits
7579dee
0199743
24d48d1
e2a8e31
0a564f8
1e35bb6
98626f5
3cd4c09
91cde24
51b3a84
c2d6d25
b216f77
1115b9a
7b6b58e
19106c1
d17676f
47abd27
d58a496
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 |
---|---|---|
|
@@ -230,7 +230,7 @@ pub(crate) async fn background_task<L: Logger>( | |
sender: Sender, | ||
mut receiver: Receiver, | ||
svc: ServiceData<L>, | ||
) -> Result<(), Error> { | ||
) -> Result<Shutdown, Error> { | ||
let ServiceData { | ||
methods, | ||
max_request_body_size, | ||
|
@@ -250,7 +250,7 @@ pub(crate) async fn background_task<L: Logger>( | |
} = svc; | ||
|
||
let (tx, rx) = mpsc::channel::<String>(message_buffer_capacity as usize); | ||
let (mut conn_tx, conn_rx) = oneshot::channel(); | ||
let (conn_tx, conn_rx) = oneshot::channel(); | ||
let sink = MethodSink::new_with_limit(tx, max_response_body_size, max_log_length); | ||
let bounded_subscriptions = BoundedSubscriptions::new(max_subscriptions_per_connection); | ||
let pending_calls = FuturesUnordered::new(); | ||
|
@@ -272,11 +272,11 @@ pub(crate) async fn background_task<L: Logger>( | |
stopped = stop; | ||
permit | ||
} | ||
None => break Ok(()), | ||
None => break Ok(Shutdown::ConnectionClosed), | ||
}; | ||
|
||
match try_recv(&mut receiver, &mut data, stopped).await { | ||
Receive::Shutdown => break Ok(()), | ||
Receive::Shutdown => break Ok(Shutdown::Stopped), | ||
Receive::Ok(stop) => { | ||
stopped = stop; | ||
} | ||
|
@@ -286,7 +286,7 @@ pub(crate) async fn background_task<L: Logger>( | |
match err { | ||
SokettoError::Closed => { | ||
tracing::debug!("WS transport: remote peer terminated the connection: {}", conn_id); | ||
break Ok(()); | ||
break Ok(Shutdown::ConnectionClosed); | ||
} | ||
SokettoError::MessageTooLarge { current, maximum } => { | ||
tracing::debug!( | ||
|
@@ -330,14 +330,33 @@ pub(crate) async fn background_task<L: Logger>( | |
// This is not strictly not needed because `tokio::spawn` will drive these the completion | ||
// but it's preferred that the `stop_handle.stopped()` should not return until all methods has been | ||
// executed and the connection has been closed. | ||
tokio::select! { | ||
// All pending calls executed. | ||
_ = pending_calls.for_each(|_| async {}) => { | ||
_ = conn_tx.send(()); | ||
match result { | ||
Ok(Shutdown::Stopped) | Err(_) => { | ||
// Soketto doesn't have a way to signal when the connection is closed | ||
// thus just throw away the data and terminate the stream once the connection has | ||
// been terminated. | ||
// | ||
// The receiver is not cancel-safe such that it used in a stream to enforce that. | ||
niklasad1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let disconnect_stream = futures_util::stream::unfold((receiver, data), |(mut receiver, mut data)| async { | ||
if let Err(SokettoError::Closed) = receiver.receive(&mut data).await { | ||
None | ||
} else { | ||
Some(((), (receiver, data))) | ||
} | ||
}); | ||
|
||
let pending = pending_calls.for_each(|_| async {}); | ||
niklasad1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let disconnect = disconnect_stream.for_each(|_| async {}); | ||
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. it's possible that the connection is terminated during the graceful shutdown and this ensures that it is aborted once the close message is sent. however, this is slightly error prone if the no proper close is sent but yeah would neat if soketto had something to indicate when the connection is closed |
||
|
||
tokio::select! { | ||
niklasad1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_ = pending => (), | ||
niklasad1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_ = disconnect => (), | ||
} | ||
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. So basically, whatever finishes first out of disconnect (which looks like any final things to be received?) and pending (whihc looks like anything to be sent back?) will lead to the thing ending? 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. I find it a bit hard to follow the logic here! 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. yeah, it's way to make it possible to detect whether connection has terminated while we try to wait for the pending futures to complete |
||
} | ||
// The connection was closed, no point of waiting for the pending calls. | ||
_ = conn_tx.closed() => {} | ||
} | ||
Ok(Shutdown::ConnectionClosed) => (), | ||
}; | ||
|
||
_ = conn_tx.send(()); | ||
|
||
logger.on_disconnect(remote_addr, TransportProtocol::WebSocket); | ||
drop(conn); | ||
|
@@ -352,7 +371,11 @@ async fn send_task( | |
stop: oneshot::Receiver<()>, | ||
) { | ||
// Interval to send out continuously `pings`. | ||
let ping_interval = IntervalStream::new(tokio::time::interval(ping_interval)); | ||
let mut ping_interval = tokio::time::interval(ping_interval); | ||
// This returns immediately so make sure it doesn't resolve before the ping_interval has been elapsed. | ||
ping_interval.tick().await; | ||
|
||
let ping_interval = IntervalStream::new(ping_interval); | ||
let rx = ReceiverStream::new(rx); | ||
|
||
tokio::pin!(ping_interval, rx, stop); | ||
|
@@ -384,15 +407,18 @@ async fn send_task( | |
} | ||
|
||
// Handle timer intervals. | ||
Either::Right((Either::Left((_, stop)), next_rx)) => { | ||
Either::Right((Either::Left((Some(_instant), stop)), next_rx)) => { | ||
if let Err(err) = send_ping(&mut ws_sender).await { | ||
tracing::debug!("WS transport error: send ping failed: {}", err); | ||
break; | ||
} | ||
|
||
rx_item = next_rx; | ||
futs = future::select(ping_interval.next(), stop); | ||
} | ||
|
||
Either::Right((Either::Left((None, _)), _)) => unreachable!("IntervalStream never terminates"), | ||
|
||
// Server is stopped. | ||
Either::Right((Either::Right(_), _)) => { | ||
break; | ||
|
@@ -558,3 +584,8 @@ async fn execute_unchecked_call<L: Logger>(params: ExecuteCallParams<L>) { | |
} | ||
}; | ||
} | ||
|
||
pub(crate) enum Shutdown { | ||
Stopped, | ||
ConnectionClosed, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I right in thinking that:
Stopped
means the user has manually stopped the server, so we want to gracefully close the eonnctionConnectionClosed
means the connection was closed for some other reason (eg network issue or whatever)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, this is only fails if the
send_task
has been completed and the receiver of the bounded channel has been dropped.this can only occur if the
send_ping
fails I think i.e, connection closed