-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Delay closing SignalR connections on shutdown #34438
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
Conversation
src/SignalR/common/Http.Connections/src/ConnectionOptionsSetup.cs
Outdated
Show resolved
Hide resolved
if (ShutdownDelay != TimeSpan.Zero) | ||
{ | ||
// Delay to let users react to application shutdown before we close the SignalR connections | ||
await Task.Delay(ShutdownDelay); |
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.
Instead of adding a TimeSpan? ShutdownDelay
option, can we add a Func<Task> ShutdownCallback
option? The implementation could just be a Task.Delay
, but it could also be more interesting.
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.
I don't think we should delay at all by default. That slows down the graceful shutdown of the server.
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.
I think we should have a default delay, the same way graceful shutdown delays before aggressively closing connections. Think of this similar to the logic we have in Kestrel. We need to give users the chance to gracefully shutdown their streams
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.
I think we should have a default delay, the same way graceful shutdown delays before aggressively closing connections.
The difference is that Kestrel will shutdown immediately if there are no active requests. With this design, even the app developer proactively closes all their connections as soon as ApplicationStopping fires, this will still delay shutdown by an extra second. 😢
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.
Here's one thing I think we can do to get the best of both worlds. We need to be the last application stopping event in the pipeline so that applications can block stopping before we do our work. That would allow us to shutdown immediately and have customers code be able to do the sleep/wait/processing
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.
With this design, even the app developer proactively closes all their connections as soon as ApplicationStopping fires, this will still delay shutdown by an extra second. 😢
I just realized I was wrong about this because of _ = CloseConnections()
. Why are we even building this list of Tasks then? 😆
We need to be the last application stopping event in the pipeline so that applications can block stopping before we do our work.
@Tratcher had a comment about this when I suggested something similar in triage.
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.
We need to be the last application stopping event in the pipeline so that applications can block stopping before we do our work.
How do you guarantee that ordering?
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.
I don't think that we can guarantee ordering but I think we should start from that premise and work backwards. If we don't like the timeout idea, when we should think about ways to make it possible to achieve what people want. They want to write code that handles the shutdown before our logic kicks in. Maybe we need a new event...
dc7b2d4
to
01f1966
Compare
/// Trigger the registered callbacks. | ||
/// </summary> | ||
/// <returns>Task representing the callbacks running.</returns> | ||
Task TriggerAsync(); |
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.
Does this need to be part of the public API? I was thinking that the IBeforeShutdown implementation cool register the callbacks with an internal service type that we use to trigger the callbacks.
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.
If it isn't part of the API anyone can come along and replace the implementation and now we have no way of running the callbacks
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.
That could be said for any service with internal dependencies. Anyone who wants the default behavior should be registering with the default service. They can register another service that calls into this if they want.
If the way this API was designed to be used was by replacing the service, we could have just TriggerAsync()
on the interface and get rid of Register(Func<Task>)
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.
Why did we decide against something like IDisposable ConnectionOptions.RegisterBeforeShutdown(Func<Task>)
again?
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.
I remember. We wanted this to be usable inside of Hub.OnConnectedAsync() and mutating options felt weird there. I still think leaving TriggerAsync() on the public service type is weird though.
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.
I still think leaving TriggerAsync() on the public service type is weird though.
Agreed, we can decide the interface isn't meant for replacing and remove it
src/SignalR/common/Http.Connections/src/Internal/DefaultBeforeShutdown.cs
Outdated
Show resolved
Hide resolved
@@ -190,7 +198,7 @@ public void CloseConnections() | |||
tasks.Add(DisposeAndRemoveAsync(c.Value.Connection, closeGracefully: false)); | |||
} | |||
|
|||
Task.WaitAll(tasks.ToArray(), TimeSpan.FromSeconds(5)); | |||
await Task.WhenAll(tasks).NoThrow(); |
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.
Why is okay fire and forget when we were blocking ApplicationStopping cancellation on these tasks before?
32aabfd
to
667532a
Compare
Closing for now, we're going to be discussing this and can reopen/make a new PR when design is figured out. |
Fixes #25069