Skip to content

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

Closed
wants to merge 10 commits into from

Conversation

BrennanConroy
Copy link
Member

@BrennanConroy BrennanConroy commented Jul 16, 2021

Fixes #25069

namespace Microsoft.AspNetCore.Http.Connections.Features;

public interface IBeforeShutdown
{
    IDisposable Register(Func<Task> callback);
}

@BrennanConroy BrennanConroy added the area-signalr Includes: SignalR clients and servers label Jul 16, 2021
@BrennanConroy BrennanConroy requested a review from davidfowl July 16, 2021 21:38
@BrennanConroy BrennanConroy requested a review from halter73 as a code owner July 16, 2021 21:38
if (ShutdownDelay != TimeSpan.Zero)
{
// Delay to let users react to application shutdown before we close the SignalR connections
await Task.Delay(ShutdownDelay);
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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

Copy link
Member

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. 😢

Copy link
Member

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

Copy link
Member

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.

Copy link
Member

@Tratcher Tratcher Jul 20, 2021

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?

Copy link
Member

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...

/// Trigger the registered callbacks.
/// </summary>
/// <returns>Task representing the callbacks running.</returns>
Task TriggerAsync();
Copy link
Member

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.

Copy link
Member Author

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

Copy link
Member

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>)

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member Author

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

@@ -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();
Copy link
Member

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?

@BrennanConroy
Copy link
Member Author

Closing for now, we're going to be discussing this and can reopen/make a new PR when design is figured out.

@BrennanConroy BrennanConroy deleted the brecon/shutdowndelay branch November 5, 2024 21:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-signalr Includes: SignalR clients and servers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SignalR connections are shutdown directly during graceful shutdowns
4 participants