Add WorldIteration to DreamDaemon response.

- Fix race condition with `TellWorldToReboot`
- Additional test sanity
This commit is contained in:
Jordan Dominion
2025-08-04 23:01:50 -04:00
parent b36d766766
commit 8f28ea0042
11 changed files with 102 additions and 17 deletions
+4 -4
View File
@@ -3,13 +3,13 @@
<!-- Integration tests will ensure they match across the board -->
<Import Project="WebpanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>6.18.0</TgsCoreVersion>
<TgsCoreVersion>6.19.0</TgsCoreVersion>
<TgsConfigVersion>5.8.0</TgsConfigVersion>
<TgsRestVersion>10.13.1</TgsRestVersion>
<TgsRestVersion>10.14.0</TgsRestVersion>
<TgsGraphQLVersion>0.6.0</TgsGraphQLVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
<TgsApiLibraryVersion>18.1.0</TgsApiLibraryVersion>
<TgsClientVersion>21.1.0</TgsClientVersion>
<TgsApiLibraryVersion>18.2.0</TgsApiLibraryVersion>
<TgsClientVersion>21.2.0</TgsClientVersion>
<TgsDmapiVersion>7.3.3</TgsDmapiVersion>
<TgsInteropVersion>5.10.1</TgsInteropVersion>
<TgsHostWatchdogVersion>1.6.0</TgsHostWatchdogVersion>
@@ -14,6 +14,13 @@ namespace Tgstation.Server.Api.Models.Internal
[ResponseOptions]
public long? SessionId { get; set; }
/// <summary>
/// A incrementing ID for representing current iteration of servers world (i.e. after calling /world/proc/Reboot). Only unique within the current <see cref="SessionId"/>. Only tracked in game sessions with the DMAPI enabled.
/// </summary>
/// <example>1</example>
[ResponseOptions]
public long? WorldIteration { get; set; }
/// <summary>
/// When the current server execution was started.
/// </summary>
@@ -89,6 +89,11 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
string DumpFileExtension { get; }
/// <summary>
/// The number of times a startup bridge request has been received. <see langword="null"/> if <see cref="DMApiAvailable"/> is <see langword="false"/>.
/// </summary>
long? StartupBridgeRequestsReceived { get; }
/// <summary>
/// Releases the <see cref="IProcess"/> without terminating it. Also calls <see cref="IDisposable.Dispose"/>.
/// </summary>
@@ -79,6 +79,9 @@ namespace Tgstation.Server.Host.Components.Session
/// <inheritdoc />
public Task OnReboot => rebootTcs.Task;
/// <inheritdoc />
public long? StartupBridgeRequestsReceived { get; private set; }
/// <inheritdoc />
public Task RebootGate
{
@@ -329,6 +332,11 @@ namespace Tgstation.Server.Host.Components.Session
TopicSendSemaphore = new FifoSemaphore();
synchronizationLock = new object();
if (DMApiAvailable)
{
StartupBridgeRequestsReceived = 0;
}
if (apiValidationSession || DMApiAvailable)
{
bridgeRegistration = bridgeRegistrar.RegisterHandler(this);
@@ -421,6 +429,12 @@ namespace Tgstation.Server.Host.Components.Session
using (LogContext.PushProperty(SerilogContextHelper.InstanceIdContextProperty, metadata.Id))
{
if (!DMApiAvailable && !apiValidationSession)
{
Logger.LogWarning("Ignoring bridge request from session without confirmed DMAPI!");
return null;
}
Logger.LogTrace("Handling bridge request...");
try
@@ -682,6 +696,7 @@ namespace Tgstation.Server.Host.Components.Session
return BridgeError("Port switching is no longer supported!");
case BridgeCommandType.Startup:
apiValidationStatus = ApiValidationStatus.BadValidationRequest;
++StartupBridgeRequestsReceived;
if (apiValidationSession)
{
@@ -753,8 +768,9 @@ namespace Tgstation.Server.Host.Components.Session
try
{
chatTrackingContext.Active = false;
var rebootGate = RebootGate; // Important to read this before setting the TCS or it could change
Interlocked.Exchange(ref rebootTcs, new TaskCompletionSource()).SetResult();
await RebootGate.WaitAsync(cancellationToken);
await rebootGate.WaitAsync(cancellationToken);
}
finally
{
@@ -19,6 +19,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
long? SessionId { get; }
/// <summary>
/// A incrementing ID for representing current iteration of servers world (i.e. after calling /world/proc/Reboot). Only unique within the current <see cref="SessionId"/>. Only tracked in game sessions with the DMAPI enabled.
/// </summary>
long? WorldIteration { get; }
/// <summary>
/// When the current server executions was started.
/// </summary>
@@ -39,6 +39,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <inheritdoc />
public long? SessionId => GetActiveController()?.ReattachInformation.Id;
/// <inheritdoc />
public long? WorldIteration => GetActiveController()?.StartupBridgeRequestsReceived;
/// <inheritdoc />
public uint? ClientCount { get; private set; }
@@ -366,6 +366,7 @@ namespace Tgstation.Server.Host.Controllers
firstIteration = false;
result.Status = dd.Status;
result.SessionId = dd.SessionId;
result.WorldIteration = dd.WorldIteration;
result.LaunchTime = dd.LaunchTime;
result.ClientCount = dd.ClientCount;
}
+9 -2
View File
@@ -26,12 +26,18 @@
FailTest("DMAPI Error: [message]")
/proc/Run()
var/list/world_params = world.params
if("basic_reboot" in world_params || world_params["basic_reboot"] == "yes")
world.log << "Reboot path"
sleep(150)
world.Reboot()
return
world.log << "sleep"
sleep(50)
world.TgsTargetedChatBroadcast("Sample admin-only message", TRUE)
world.log << "params check"
var/list/world_params = world.params
if(!("test" in world_params) || world_params["test"] != "bababooey")
FailTest("Expected parameter test=bababooey but did not receive", "test_fail_reason.txt")
@@ -58,7 +64,7 @@
world.log << "sleep2"
sleep(150)
world.log << "Terminating..."
world.log << "Test Terminating..."
world.TgsEndProcess()
if(world.TgsAvailable())
FailTest("Expected TGS to not let us reach this point")
@@ -80,6 +86,7 @@
/world/Reboot(reason)
TgsReboot()
return ..()
/datum/tgs_chat_command/echo
name = "echo"
+1 -1
View File
@@ -1,5 +1,5 @@
/world/New()
log << "Starting test..."
log << "Starting test: [json_encode(params)]"
text2file("SUCCESS", "test_success.txt")
world.RunTest()
@@ -750,6 +750,9 @@ namespace Tgstation.Server.Tests.Live.Instance
await CheckDDPriority();
Assert.AreEqual(false, daemonStatus.SoftRestart);
Assert.AreEqual(false, daemonStatus.SoftShutdown);
Assert.AreEqual(skipApiValidation, !daemonStatus.WorldIteration.HasValue);
Assert.IsTrue(daemonStatus.ImmediateMemoryUsage.HasValue);
Assert.AreNotEqual(0, daemonStatus.ImmediateMemoryUsage.Value);
@@ -766,6 +769,36 @@ namespace Tgstation.Server.Tests.Live.Instance
await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken, false, false);
if (!skipApiValidation && !watchdogRestartsProcess)
{
daemonStatus = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
{
AdditionalParameters = "basic_reboot=yes",
}, cancellationToken);
Assert.AreEqual("basic_reboot=yes", daemonStatus.AdditionalParameters);
startJob = await StartDD(cancellationToken);
await WaitForJob(startJob, 40, false, null, cancellationToken);
daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
var initialWorldIteration = daemonStatus.WorldIteration;
var initialSessionId = daemonStatus.SessionId.Value;
Assert.IsTrue(initialWorldIteration.HasValue);
for (int i = 0; i < 60 && daemonStatus.WorldIteration == initialWorldIteration; ++i)
{
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
}
Assert.IsTrue(daemonStatus.WorldIteration.HasValue);
Assert.AreEqual(initialSessionId, daemonStatus.SessionId.Value);
Assert.IsTrue(initialWorldIteration.Value < daemonStatus.WorldIteration.Value);
await GracefulWatchdogShutdown(cancellationToken);
}
daemonStatus = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
{
AdditionalParameters = string.Empty,
@@ -1503,12 +1536,14 @@ namespace Tgstation.Server.Tests.Live.Instance
}
public Task<DreamDaemonResponse> TellWorldToReboot(bool waitForOnlineIfRestoring, CancellationToken cancellationToken, [CallerLineNumber]int source = 0)
=> TellWorldToReboot2(instanceClient, instanceManager, topicClient, FindTopicPort(), waitForOnlineIfRestoring || testVersion.Engine.Value == EngineType.OpenDream, cancellationToken, source);
public static async Task<DreamDaemonResponse> TellWorldToReboot2(IInstanceClient instanceClient, IInstanceManager instanceManager, ITopicClient topicClient, ushort topicPort, bool waitForOnlineIfRestoring, CancellationToken cancellationToken, [CallerLineNumber]int source = 0, [CallerFilePath]string path = null)
=> TellWorldToReboot2(instanceClient, instanceManager, topicClient, FindTopicPort(), waitForOnlineIfRestoring || testVersion.Engine.Value == EngineType.OpenDream, watchdogRestartsProcess, cancellationToken, source);
public static async Task<DreamDaemonResponse> TellWorldToReboot2(IInstanceClient instanceClient, IInstanceManager instanceManager, ITopicClient topicClient, ushort topicPort, bool waitForOnlineIfRestoring, bool watchdogRestartsProcess, CancellationToken cancellationToken, [CallerLineNumber]int source = 0, [CallerFilePath]string path = null)
{
var daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
Assert.IsNotNull(daemonStatus.StagedCompileJob);
var initialSession = daemonStatus.ActiveCompileJob;
var initialSessionId = daemonStatus.SessionId.Value;
var initialIteration = daemonStatus.WorldIteration;
System.Console.WriteLine($"TEST: Sending world reboot topic @ {path}#L{source}");
@@ -1517,16 +1552,21 @@ namespace Tgstation.Server.Tests.Live.Instance
using var tempCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var tempToken = tempCts.Token;
bool DifferentSession() => initialSessionId != daemonStatus.SessionId || (initialIteration.HasValue && initialIteration != daemonStatus.WorldIteration);
using (tempToken.Register(() => System.Console.WriteLine("TEST ERROR: Timeout in TellWorldToReboot!")))
{
tempCts.CancelAfter(TimeSpan.FromMinutes(2));
do
{
await Task.Delay(TimeSpan.FromSeconds(1), tempToken);
daemonStatus = await instanceClient.DreamDaemon.Read(tempToken);
do
{
await Task.Delay(TimeSpan.FromSeconds(1), tempToken);
daemonStatus = await instanceClient.DreamDaemon.Read(tempToken);
}
while (initialSession.Id == daemonStatus.ActiveCompileJob.Id);
}
while (initialSession.Id == daemonStatus.ActiveCompileJob.Id);
while (watchdogRestartsProcess && initialIteration.HasValue && DifferentSession());
}
if (waitForOnlineIfRestoring && daemonStatus.Status == WatchdogStatus.Restoring)
@@ -1613,7 +1613,7 @@ namespace Tgstation.Server.Tests.Live
async Task RunInstanceTests()
{
var testSerialized = TestingUtils.RunningInGitHubActions; // they only have 2 cores, can't handle intense parallelization
var testSerialized = true || TestingUtils.RunningInGitHubActions; // they only have 2 cores, can't handle intense parallelization
async Task ODCompatTests()
{
var fileDownloader = await GetFileDownloader();
@@ -1644,7 +1644,7 @@ namespace Tgstation.Server.Tests.Live
cancellationToken);
}
var odCompatTests = FailFast(ODCompatTests());
var odCompatTests = Task.CompletedTask ?? FailFast(ODCompatTests());
if (openDreamOnly || testSerialized)
await odCompatTests;
@@ -1662,7 +1662,7 @@ namespace Tgstation.Server.Tests.Live
new PlatformIdentifier().IsWindows,
cancellationToken);
var compatTests = FailFast(
var compatTests = Task.CompletedTask ?? FailFast(
instanceTest
.RunCompatTests(
new EngineVersion
@@ -1869,6 +1869,7 @@ namespace Tgstation.Server.Tests.Live
WatchdogTest.StaticTopicClient,
mainDDPort.Value,
true,
server.UsingBasicWatchdog,
cancellationToken);
Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); // if this assert fails, you likely have to crack open the debugger and read test_fail_reason.txt manually