From f534997e0e878eee760144f2439bce968aa01922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Domeradzki?= Date: Fri, 4 Oct 2024 17:58:56 +0200 Subject: [PATCH] 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. --- .../IPC/Controllers/Api/NLogController.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/ArchiSteamFarm/IPC/Controllers/Api/NLogController.cs b/ArchiSteamFarm/IPC/Controllers/Api/NLogController.cs index f2b8e7a8f..42f770905 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/NLogController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/NLogController.cs @@ -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 ActiveLogWebSockets = new(); + private readonly IHostApplicationLifetime ApplicationLifetime; + + public NLogController(IHostApplicationLifetime applicationLifetime) { + ArgumentNullException.ThrowIfNull(applicationLifetime); + + ApplicationLifetime = applicationLifetime; + } + /// /// Fetches ASF log file, this works on assumption that the log file is in fact generated, as user could disable it through custom configuration. /// @@ -91,7 +100,7 @@ public sealed class NLogController : ArchiController { [HttpGet] [ProducesResponseType>>((int) HttpStatusCode.OK)] [ProducesResponseType((int) HttpStatusCode.BadRequest)] - public async Task Get(CancellationToken cancellationToken) { + public async Task 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(), cancellationToken).ConfigureAwait(false); + while ((webSocket.State == WebSocketState.Open) && !cancellationTokenSource.IsCancellationRequested) { + WebSocketReceiveResult result = await webSocket.ReceiveAsync(Array.Empty(), 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; }