diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index dc55943a72..e23aac8175 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -107,21 +107,6 @@ namespace Tgstation.Server.Host.Components.Session /// public ReattachInformation ReattachInformation { get; } - /// - /// The that completes when DD makes it's first bridge request. - /// - readonly TaskCompletionSource initialBridgeRequestTcs; - - /// - /// The metadata. - /// - readonly Api.Models.Instance metadata; - - /// - /// A used for the topic send operation made on reattaching. - /// - readonly CancellationTokenSource reattachTopicCts; - /// /// The for the . /// @@ -157,6 +142,26 @@ namespace Tgstation.Server.Host.Components.Session /// readonly IAsyncDelayer asyncDelayer; + /// + /// The that completes when DD makes it's first bridge request. + /// + readonly TaskCompletionSource initialBridgeRequestTcs; + + /// + /// The used to prevent concurrent calls into /world/Topic(). + /// + readonly FifoSemaphore topicSendSemaphore; + + /// + /// The metadata. + /// + readonly Api.Models.Instance metadata; + + /// + /// A used for the topic send operation made on reattaching. + /// + readonly CancellationTokenSource reattachTopicCts; + /// /// for port updates and . /// @@ -282,6 +287,8 @@ namespace Tgstation.Server.Host.Components.Session // Worth further investigation as to if that sequence of events is a reliable crash vector and opening a BYOND bug if it is initialBridgeRequestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); reattachTopicCts = new CancellationTokenSource(); + + topicSendSemaphore = new FifoSemaphore(); synchronizationLock = new object(); if (apiValidate || DMApiAvailable) @@ -329,6 +336,11 @@ namespace Tgstation.Server.Host.Components.Session } Logger.LogTrace("Disposing..."); + + // yield then acquire the topic semaphore to prevent new calls from starting + await Task.Yield(); + (await topicSendSemaphore.Lock(CancellationToken.None)).Dispose(); // DCT: None available + if (!released) { process.Terminate(); @@ -345,6 +357,8 @@ namespace Tgstation.Server.Host.Components.Session if (!released) await Lifetime; // finish the async callback + + topicSendSemaphore.Dispose(); } /// @@ -387,7 +401,7 @@ namespace Tgstation.Server.Host.Components.Session { ArgumentNullException.ThrowIfNull(parameters); - if (Lifetime.IsCompleted) + if (Lifetime.IsCompleted || disposed) { Logger.LogWarning( "Attempted to send a command to an inactive SessionController: {commandType}", @@ -960,35 +974,45 @@ namespace Tgstation.Server.Host.Components.Session /// A resulting in the of the topic request. async Task SendRawTopic(string queryString, bool priority, CancellationToken cancellationToken) { + if (disposed) + { + Logger.LogWarning( + "Attempted to send a topic on a disposed SessionController"); + return null; + } + var targetPort = ReattachInformation.Port; var killedOrRebootedTask = Task.WhenAny(Lifetime, OnReboot); global::Byond.TopicSender.TopicResponse byondResponse = null; var firstSend = true; - const int PrioritySendAttempts = 5; - var endpoint = new IPEndPoint(IPAddress.Loopback, targetPort); - for (var i = PrioritySendAttempts - 1; i >= 0 && (priority || firstSend); --i) - try - { - firstSend = false; - if (!killedOrRebootedTask.IsCompleted) - byondResponse = await byondTopicSender.SendTopic( - endpoint, - queryString, - cancellationToken); - - break; - } - catch (Exception ex) when (!cancellationToken.IsCancellationRequested) - { - Logger.LogWarning(ex, "SendTopic exception!{retryDetails}", priority ? $" {i} attempts remaining." : String.Empty); - - if (priority && i > 0) + using (await topicSendSemaphore.Lock(cancellationToken)) + { + const int PrioritySendAttempts = 5; + var endpoint = new IPEndPoint(IPAddress.Loopback, targetPort); + for (var i = PrioritySendAttempts - 1; i >= 0 && (priority || firstSend); --i) + try { - var delayTask = asyncDelayer.Delay(TimeSpan.FromSeconds(2), cancellationToken); - await Task.WhenAny(killedOrRebootedTask, delayTask); + firstSend = false; + if (!killedOrRebootedTask.IsCompleted) + byondResponse = await byondTopicSender.SendTopic( + endpoint, + queryString, + cancellationToken); + + break; } - } + catch (Exception ex) when (!cancellationToken.IsCancellationRequested) + { + Logger.LogWarning(ex, "SendTopic exception!{retryDetails}", priority ? $" {i} attempts remaining." : String.Empty); + + if (priority && i > 0) + { + var delayTask = asyncDelayer.Delay(TimeSpan.FromSeconds(2), cancellationToken); + await Task.WhenAny(killedOrRebootedTask, delayTask); + } + } + } if (byondResponse == null) {