diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs
index beddc9e9cb..282a95df32 100644
--- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs
+++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs
@@ -18,6 +18,7 @@ using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
+using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Extensions.Converters;
using Tgstation.Server.Host.System;
@@ -26,7 +27,7 @@ namespace Tgstation.Server.Host.Swarm
///
/// Helps keep servers connected to the same database in sync by coordinating updates.
///
- sealed class SwarmService : ISwarmService, ISwarmOperations, IRestartHandler, IDisposable
+ sealed class SwarmService : ISwarmService, ISwarmOperations, IDisposable
{
///
/// Interval at which the swarm controller makes health checks on nodes.
@@ -101,11 +102,6 @@ namespace Tgstation.Server.Host.Swarm
///
readonly IServerUpdater serverUpdater;
- ///
- /// The for the .
- ///
- readonly IRestartRegistration restartRegistration;
-
///
/// The for the .
///
@@ -176,11 +172,6 @@ namespace Tgstation.Server.Host.Swarm
///
DateTimeOffset? lastControllerHealthCheck;
- ///
- /// If was called.
- ///
- bool restarting;
-
///
/// If the list has been updated and needs to be resent to clients.
///
@@ -193,7 +184,6 @@ namespace Tgstation.Server.Host.Swarm
/// The value of .
/// The value of .
/// The value of .
- /// The to register ourselves as a with.
/// The value of .
/// The value of .
/// The containing the value of .
@@ -203,7 +193,6 @@ namespace Tgstation.Server.Host.Swarm
IDatabaseSeeder databaseSeeder,
IAssemblyInformationProvider assemblyInformationProvider,
IAbstractHttpClientFactory httpClientFactory,
- IServerControl serverControl,
IServerUpdater serverUpdater,
IAsyncDelayer asyncDelayer,
IOptions swarmConfigurationOptions,
@@ -213,10 +202,6 @@ namespace Tgstation.Server.Host.Swarm
this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
- if (serverControl == null)
- throw new ArgumentNullException(nameof(serverControl));
- restartRegistration = serverControl.RegisterForRestart(this);
-
this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
@@ -253,11 +238,7 @@ namespace Tgstation.Server.Host.Swarm
}
///
- public void Dispose()
- {
- restartRegistration.Dispose();
- serverHealthCheckCancellationTokenSource?.Dispose();
- }
+ public void Dispose() => serverHealthCheckCancellationTokenSource?.Dispose();
///
public async Task RemoteAbortUpdate(CancellationToken cancellationToken)
@@ -278,8 +259,9 @@ namespace Tgstation.Server.Host.Swarm
return;
logger.LogInformation("Aborting swarm update!");
- updateCommitTcs?.TrySetResult(false);
+ var commitTcs = updateCommitTcs;
updateCommitTcs = null;
+ commitTcs?.TrySetResult(false);
nodesThatNeedToBeReadyToCommit = null;
targetUpdateVersion = null;
@@ -332,6 +314,15 @@ namespace Tgstation.Server.Host.Swarm
if (!SwarmMode)
return SwarmCommitResult.ContinueUpdateNonCommitted;
+ // wait for the update commit TCS
+ var commitTcsTask = updateCommitTcs?.Task;
+ if (commitTcsTask == null)
+ {
+ logger.LogDebug("Update commit failed, no pending task completion source!");
+ await AbortUpdate(cancellationToken);
+ return SwarmCommitResult.AbortUpdate;
+ }
+
logger.LogInformation("Waiting to commit update...");
using var httpClient = httpClientFactory.CreateClient();
if (!swarmController)
@@ -357,20 +348,11 @@ namespace Tgstation.Server.Host.Swarm
}
}
- // wait for the update commit TCS
- var commitTcsTask = updateCommitTcs?.Task;
- if (commitTcsTask == null)
- {
- logger.LogDebug("Update commit failed, no pending task completion source!");
- await AbortUpdate(cancellationToken);
- return SwarmCommitResult.AbortUpdate;
- }
-
var timeoutTask = swarmController
? asyncDelayer.Delay(
TimeSpan.FromMinutes(UpdateCommitTimeoutMinutes),
cancellationToken)
- : Extensions.TaskExtensions.InfiniteTask();
+ : Extensions.TaskExtensions.InfiniteTask().WithToken(cancellationToken);
var commitTask = Task.WhenAny(commitTcsTask, timeoutTask);
@@ -491,6 +473,8 @@ namespace Tgstation.Server.Host.Swarm
///
public async Task Shutdown(CancellationToken cancellationToken)
{
+ logger.LogTrace("Begin Shutdown");
+
async Task SendUnregistrationRequest(SwarmServerResponse swarmServer)
{
using var httpClient = httpClientFactory.CreateClient();
@@ -524,15 +508,11 @@ namespace Tgstation.Server.Host.Swarm
if (!swarmController)
{
- // if we restart a node, we don't want to unregister it so the controller doesn't try to update without it
- // if we're shutting it down, though we should unregister it
- if (!restarting)
+ if (controllerRegistration != null)
{
logger.LogInformation("Unregistering from swarm controller...");
await SendUnregistrationRequest(null);
}
- else
- logger.LogTrace("Not unregistering from swarm controller as we are restarting");
return;
}
@@ -541,8 +521,7 @@ namespace Tgstation.Server.Host.Swarm
if (targetUpdateVersion != null
&& targetUpdateVersion < assemblyInformationProvider.Version)
await databaseContextFactory.UseContext(
- db => databaseSeeder.Downgrade(db, targetUpdateVersion, cancellationToken))
- ;
+ db => databaseSeeder.Downgrade(db, targetUpdateVersion, cancellationToken));
if (SwarmMode)
{
@@ -663,13 +642,6 @@ namespace Tgstation.Server.Host.Swarm
return true;
}
- ///
- public Task HandleRestart(Version updateVersion, bool gracefulShutdown, CancellationToken cancellationToken)
- {
- restarting = !gracefulShutdown;
- return Task.CompletedTask;
- }
-
///
public async Task RemoteCommitRecieved(Guid registrationId, CancellationToken cancellationToken)
{
@@ -705,8 +677,7 @@ namespace Tgstation.Server.Host.Swarm
{
logger.LogTrace("All nodes ready, update commit is a go once controller is ready");
var commitTcs = updateCommitTcs;
- commitTcs?.TrySetResult(true);
- return commitTcs != null;
+ return commitTcs?.TrySetResult(true) == true;
}
}
@@ -1017,6 +988,7 @@ namespace Tgstation.Server.Host.Swarm
{
logger.LogWarning(ex, "Error during swarm controller health check! Attempting to re-register...");
controllerRegistration = null;
+ await AbortUpdate(cancellationToken);
}
SwarmRegistrationResult registrationResult;
diff --git a/tests/Tgstation.Server.Host.Tests/Swarm/SwarmRpcMapper.cs b/tests/Tgstation.Server.Host.Tests/Swarm/SwarmRpcMapper.cs
index afdaf505b5..4892d18b82 100644
--- a/tests/Tgstation.Server.Host.Tests/Swarm/SwarmRpcMapper.cs
+++ b/tests/Tgstation.Server.Host.Tests/Swarm/SwarmRpcMapper.cs
@@ -19,40 +19,49 @@ using Newtonsoft.Json;
using Tgstation.Server.Common;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Controllers;
+using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Swarm;
namespace Tgstation.Server.Host.Tests.Swarm
{
- sealed class SwarmRpcMapper : IRequestSwarmRegistrationParser
+ sealed class SwarmRpcMapper : IRequestSwarmRegistrationParser, IDisposable
{
- List<(SwarmConfiguration, TestableSwarmNode)> configToControllers;
- Guid? incomingRegistrationId;
+ public bool AsyncRequests { get; set; }
+
+ readonly Stack incomingRegistrationIds = new();
+
+ readonly List serverErrors = new();
+
+ List<(SwarmConfiguration, TestableSwarmNode)> configToNodes;
public SwarmRpcMapper(Mock clientMock)
{
clientMock
.Setup(x => x.SendAsync(It.IsNotNull(), It.IsAny()))
.Returns(MapRequest);
+ AsyncRequests = true;
}
- public Guid GetRequestRegistrationId(HttpRequest request)
+ public void Dispose()
{
- Assert.IsTrue(incomingRegistrationId.HasValue);
- var result = incomingRegistrationId.Value;
- incomingRegistrationId = null;
- return result;
+ if (serverErrors.Count > 1)
+ throw new AggregateException(serverErrors);
+ else if (serverErrors.Count == 1)
+ throw serverErrors[0];
}
- public void Register(List<(SwarmConfiguration, TestableSwarmNode)> configToControllers)
+ public Guid GetRequestRegistrationId(HttpRequest request) => incomingRegistrationIds.Peek();
+
+ public void Register(List<(SwarmConfiguration, TestableSwarmNode)> configToNodes)
{
- this.configToControllers = configToControllers;
+ this.configToNodes = configToNodes;
}
async Task MapRequest(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
- var (config, node) = configToControllers.FirstOrDefault(
+ var (config, node) = configToNodes.FirstOrDefault(
pair => pair.Item1.Address.IsBaseOf(request.RequestUri));
if (config == default)
@@ -63,6 +72,11 @@ namespace Tgstation.Server.Host.Tests.Swarm
throw new HttpRequestException("Can't connect to uninitialized node!");
}
+ if (node.Shutdown)
+ {
+ throw new HttpRequestException("Can't connect to shutdown node!");
+ }
+
var controller = node.Controller;
Type targetAttribute = null;
@@ -106,7 +120,7 @@ namespace Tgstation.Server.Host.Tests.Swarm
.Where(pair => pair.Item2 != null
&& pair.Item2.HttpMethods.Count() == 1
&& pair.Item2.HttpMethods.All(supportedMethod => supportedMethod.Equals(request.Method.Method))
- && pair.Item2.Template == route)
+ && (pair.Item2.Template ?? String.Empty) == route)
.Select(pair => pair.method)
.SingleOrDefault();
@@ -114,63 +128,76 @@ namespace Tgstation.Server.Host.Tests.Swarm
Assert.Fail($"SwarmController has no method with attribute {targetAttribute}!");
// We're not testing OnActionExecutingAsync, that's covered by integration.
- if (request.Headers.TryGetValues(SwarmConstants.RegistrationIdHeader, out var values) && values.Count() == 1)
- node.RpcMapper.incomingRegistrationId = Guid.Parse(values.First());
-
- var args = new List