Close log endpoints upon graceful shutdown request

Previously we've kept websocket connection open for as long as caller requested it. During graceful shutdown, ASP.NET normally waits for all pending requests to finish, while no longer accepting new ones - this is a very good approach. In our case however, since we didn't do anything with that event before, the graceful shutdown was timing out after 30 seconds before eventually forcefully killing any still-ongoing requests, websocket connection in our case.

Hook into application lifetime in order to be notified when the graceful shutdown happens. This way we can create linked cancellation token for request abort and graceful shutdown and close the websocket connection when any of those two happens.
This commit is contained in:
Łukasz Domeradzki
2024-10-04 17:58:56 +02:00
parent d609a68dc4
commit f534997e0e

View File

@@ -38,6 +38,7 @@ using ArchiSteamFarm.NLog;
using ArchiSteamFarm.NLog.Targets;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
namespace ArchiSteamFarm.IPC.Controllers.Api;
@@ -45,6 +46,14 @@ namespace ArchiSteamFarm.IPC.Controllers.Api;
public sealed class NLogController : ArchiController {
private static readonly ConcurrentDictionary<WebSocket, (SemaphoreSlim Semaphore, CancellationToken CancellationToken)> ActiveLogWebSockets = new();
private readonly IHostApplicationLifetime ApplicationLifetime;
public NLogController(IHostApplicationLifetime applicationLifetime) {
ArgumentNullException.ThrowIfNull(applicationLifetime);
ApplicationLifetime = applicationLifetime;
}
/// <summary>
/// Fetches ASF log file, this works on assumption that the log file is in fact generated, as user could disable it through custom configuration.
/// </summary>
@@ -91,7 +100,7 @@ public sealed class NLogController : ArchiController {
[HttpGet]
[ProducesResponseType<IEnumerable<GenericResponse<string>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
public async Task<ActionResult> Get(CancellationToken cancellationToken) {
public async Task<ActionResult> Get() {
if (HttpContext == null) {
throw new InvalidOperationException(nameof(HttpContext));
}
@@ -107,7 +116,9 @@ public sealed class NLogController : ArchiController {
SemaphoreSlim sendSemaphore = new(1, 1);
if (!ActiveLogWebSockets.TryAdd(webSocket, (sendSemaphore, cancellationToken))) {
using CancellationTokenSource cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(HttpContext.RequestAborted, ApplicationLifetime.ApplicationStopping);
if (!ActiveLogWebSockets.TryAdd(webSocket, (sendSemaphore, cancellationTokenSource.Token))) {
sendSemaphore.Dispose();
return new EmptyResult();
@@ -116,20 +127,22 @@ public sealed class NLogController : ArchiController {
try {
// Push initial history if available
if (ArchiKestrel.HistoryTarget != null) {
// ReSharper disable once AccessToDisposedClosure - we're waiting for completion with Task.WhenAll(), we're not going to exit using block
await Task.WhenAll(ArchiKestrel.HistoryTarget.ArchivedMessages.Select(archivedMessage => PostLoggedMessageUpdate(webSocket, archivedMessage, sendSemaphore, cancellationToken))).ConfigureAwait(false);
// ReSharper disable AccessToDisposedClosure - we're waiting for completion with Task.WhenAll(), we're not going to exit using block
await Task.WhenAll(ArchiKestrel.HistoryTarget.ArchivedMessages.Select(archivedMessage => PostLoggedMessageUpdate(webSocket, archivedMessage, sendSemaphore, cancellationTokenSource.Token))).ConfigureAwait(false);
// ReSharper restore AccessToDisposedClosure - we're waiting for completion with Task.WhenAll(), we're not going to exit using block
}
while (webSocket.State == WebSocketState.Open) {
WebSocketReceiveResult result = await webSocket.ReceiveAsync(Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
while ((webSocket.State == WebSocketState.Open) && !cancellationTokenSource.IsCancellationRequested) {
WebSocketReceiveResult result = await webSocket.ReceiveAsync(Array.Empty<byte>(), cancellationTokenSource.Token).ConfigureAwait(false);
if (result.MessageType != WebSocketMessageType.Close) {
await webSocket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "You're not supposed to be sending any message but Close!", cancellationToken).ConfigureAwait(false);
await webSocket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "You're not supposed to be sending any message but Close!", cancellationTokenSource.Token).ConfigureAwait(false);
break;
}
await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", cancellationToken).ConfigureAwait(false);
await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", cancellationTokenSource.Token).ConfigureAwait(false);
break;
}