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