mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-30 00:22:40 +01:00
Got rid of that useless swarm registration abstraction AND fixed race condition in SwarmRpcMapper calls
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses the swarm registration header from a <see cref="HttpRequest"/>.
|
||||
/// </summary>
|
||||
public interface IRequestSwarmRegistrationParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the swarm registration <see cref="Guid"/> from the headers of a given <paramref name="request"/>.
|
||||
/// </summary>
|
||||
/// <param name="request">The <see cref="HttpRequest"/>, must contain a valid <see cref="Swarm.SwarmConstants.RegistrationIdHeader"/>.</param>
|
||||
/// <returns>The parsed registration ID.</returns>
|
||||
Guid GetRequestRegistrationId(HttpRequest request);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
using Tgstation.Server.Host.Swarm;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class RequestSwarmRegistrationParser : IRequestSwarmRegistrationParser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Guid GetRequestRegistrationId(HttpRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
|
||||
return Guid.Parse(request.Headers[SwarmConstants.RegistrationIdHeader].First());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Serilog.Context;
|
||||
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
@@ -29,18 +30,13 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Get the current registration <see cref="Guid"/> from the <see cref="ControllerBase.Request"/>.
|
||||
/// </summary>
|
||||
internal Guid RequestRegistrationId => requestRegistrationParser.GetRequestRegistrationId(Request);
|
||||
internal Guid RequestRegistrationId => Guid.Parse(Request.Headers[SwarmConstants.RegistrationIdHeader].First());
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ISwarmOperations"/> for the <see cref="SwarmController"/>.
|
||||
/// </summary>
|
||||
readonly ISwarmOperations swarmOperations;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IRequestSwarmRegistrationParser"/> for the <see cref="SwarmController"/>.
|
||||
/// </summary>
|
||||
readonly IRequestSwarmRegistrationParser requestRegistrationParser;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="SwarmController"/>.
|
||||
/// </summary>
|
||||
@@ -60,19 +56,16 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// Initializes a new instance of the <see cref="SwarmController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="swarmOperations">The value of <see cref="swarmOperations"/>.</param>
|
||||
/// <param name="requestRegistrationParser">The value of <see cref="requestRegistrationParser"/>.</param>
|
||||
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
|
||||
/// <param name="swarmConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="swarmConfiguration"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
public SwarmController(
|
||||
ISwarmOperations swarmOperations,
|
||||
IRequestSwarmRegistrationParser requestRegistrationParser,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
IOptions<SwarmConfiguration> swarmConfigurationOptions,
|
||||
ILogger<SwarmController> logger)
|
||||
{
|
||||
this.swarmOperations = swarmOperations ?? throw new ArgumentNullException(nameof(swarmOperations));
|
||||
this.requestRegistrationParser = requestRegistrationParser ?? throw new ArgumentNullException(nameof(requestRegistrationParser));
|
||||
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
|
||||
swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
|
||||
this.logger = logger;
|
||||
|
||||
@@ -367,7 +367,6 @@ namespace Tgstation.Server.Host.Core
|
||||
services.AddSingleton<IServerPortProvider, ServerPortProivder>();
|
||||
services.AddSingleton<ITopicClientFactory, TopicClientFactory>();
|
||||
services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
|
||||
services.AddSingleton<IRequestSwarmRegistrationParser, RequestSwarmRegistrationParser>();
|
||||
|
||||
// configure root services
|
||||
services.AddSingleton<IJobManager, JobManager>();
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public sealed class TestRequestSwarmRegistrationParser
|
||||
{
|
||||
[TestMethod]
|
||||
public void TestConstructorThrows()
|
||||
{
|
||||
var parser = new RequestSwarmRegistrationParser();
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(() => parser.GetRequestRegistrationId(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using Moq;
|
||||
@@ -24,20 +25,21 @@ using Tgstation.Server.Host.Controllers;
|
||||
|
||||
namespace Tgstation.Server.Host.Swarm.Tests
|
||||
{
|
||||
sealed class SwarmRpcMapper : IRequestSwarmRegistrationParser, IDisposable
|
||||
sealed class SwarmRpcMapper : IDisposable
|
||||
{
|
||||
public bool AsyncRequests { get; set; }
|
||||
|
||||
readonly ILogger logger;
|
||||
|
||||
readonly Stack<Guid> incomingRegistrationIds = new();
|
||||
readonly Func<SwarmService, SwarmController> createSwarmController;
|
||||
|
||||
List<(SwarmConfiguration, TestableSwarmNode)> configToNodes;
|
||||
|
||||
int serverErrorCount;
|
||||
|
||||
public SwarmRpcMapper(Mock<IHttpClient> clientMock, ILogger logger)
|
||||
public SwarmRpcMapper(Func<SwarmService, SwarmController> createSwarmController, Mock<IHttpClient> clientMock, ILogger logger)
|
||||
{
|
||||
this.createSwarmController = createSwarmController;
|
||||
clientMock
|
||||
.Setup(x => x.SendAsync(It.IsNotNull<HttpRequestMessage>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(MapRequest);
|
||||
@@ -50,8 +52,6 @@ namespace Tgstation.Server.Host.Swarm.Tests
|
||||
Assert.AreEqual(0, serverErrorCount);
|
||||
}
|
||||
|
||||
public Guid GetRequestRegistrationId(HttpRequest request) => incomingRegistrationIds.Peek();
|
||||
|
||||
public void Register(List<(SwarmConfiguration, TestableSwarmNode)> configToNodes)
|
||||
{
|
||||
this.configToNodes = configToNodes;
|
||||
@@ -77,7 +77,7 @@ namespace Tgstation.Server.Host.Swarm.Tests
|
||||
throw new HttpRequestException("Can't connect to shutdown node!");
|
||||
}
|
||||
|
||||
var controller = node.Controller;
|
||||
var controller = createSwarmController(node.Service);
|
||||
|
||||
Type targetAttribute = null;
|
||||
bool isDataRequest = false;
|
||||
@@ -128,58 +128,59 @@ namespace Tgstation.Server.Host.Swarm.Tests
|
||||
Assert.Fail($"SwarmController has no method with attribute {targetAttribute}!");
|
||||
|
||||
IActionResult result;
|
||||
var hasRegistrationHeader = request.Headers.TryGetValues(SwarmConstants.RegistrationIdHeader, out var values) && values.Count() == 1;
|
||||
var hasRegistrationHeader = request.Headers.TryGetValues(SwarmConstants.RegistrationIdHeader, out var values)
|
||||
&& values.Count() == 1;
|
||||
var response = new HttpResponseMessage();
|
||||
try
|
||||
{
|
||||
var response = new HttpResponseMessage();
|
||||
try
|
||||
// We're not testing OnActionExecutingAsync, that's covered by integration.
|
||||
if (hasRegistrationHeader)
|
||||
{
|
||||
// We're not testing OnActionExecutingAsync, that's covered by integration.
|
||||
if (hasRegistrationHeader)
|
||||
var mockRequest = new Mock<HttpRequest>();
|
||||
mockRequest.SetupGet(x => x.Headers).Returns(new HeaderDictionary
|
||||
{
|
||||
node.RpcMapper.incomingRegistrationIds.Push(Guid.Parse(values.First()));
|
||||
var args = new List<object>();
|
||||
if (isDataRequest && request.Content != null)
|
||||
{
|
||||
var dataType = controllerMethod.GetParameters().First().ParameterType;
|
||||
var json = await request.Content.ReadAsStringAsync(cancellationToken);
|
||||
var parameter = JsonConvert.DeserializeObject(json, dataType, SwarmService.SerializerSettings);
|
||||
args.Add(parameter);
|
||||
}
|
||||
SwarmConstants.RegistrationIdHeader,
|
||||
new StringValues(values.First())
|
||||
},
|
||||
});
|
||||
var mockHttpContext = new Mock<HttpContext>();
|
||||
mockHttpContext.SetupGet(x => x.Request).Returns(mockRequest.Object);
|
||||
|
||||
if (AsyncRequests)
|
||||
await Task.Yield();
|
||||
controller
|
||||
.ControllerContext
|
||||
.HttpContext = mockHttpContext.Object;
|
||||
|
||||
if (controllerMethod.ReturnType != typeof(IActionResult))
|
||||
{
|
||||
Assert.AreEqual(typeof(Task<IActionResult>), controllerMethod.ReturnType);
|
||||
args.Add(cancellationToken);
|
||||
var invocationTask = (Task<IActionResult>)controllerMethod.Invoke(controller, args.ToArray());
|
||||
result = await invocationTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = (IActionResult)controllerMethod.Invoke(controller, args.ToArray());
|
||||
var args = new List<object>();
|
||||
if (isDataRequest && request.Content != null)
|
||||
{
|
||||
var dataType = controllerMethod.GetParameters().First().ParameterType;
|
||||
var json = await request.Content.ReadAsStringAsync(cancellationToken);
|
||||
var parameter = JsonConvert.DeserializeObject(json, dataType, SwarmService.SerializerSettings);
|
||||
args.Add(parameter);
|
||||
}
|
||||
|
||||
// simulate worst case, request completed but was aborted before server replied
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
if (AsyncRequests)
|
||||
await Task.Yield();
|
||||
|
||||
if (controllerMethod.ReturnType != typeof(IActionResult))
|
||||
{
|
||||
Assert.AreEqual(typeof(Task<IActionResult>), controllerMethod.ReturnType);
|
||||
args.Add(cancellationToken);
|
||||
var invocationTask = (Task<IActionResult>)controllerMethod.Invoke(controller, args.ToArray());
|
||||
result = await invocationTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = controller.BadRequest();
|
||||
result = (IActionResult)controllerMethod.Invoke(controller, args.ToArray());
|
||||
|
||||
// simulate worst case, request completed but was aborted before server replied
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
if (ex is not OperationCanceledException)
|
||||
{
|
||||
logger.LogCritical(ex, "Error in request to {nodeId}!", config.Identifier);
|
||||
++serverErrorCount;
|
||||
}
|
||||
|
||||
response.Dispose();
|
||||
throw;
|
||||
result = controller.BadRequest();
|
||||
}
|
||||
|
||||
// manually checked all controller response types
|
||||
@@ -196,10 +197,16 @@ namespace Tgstation.Server.Host.Swarm.Tests
|
||||
|
||||
return response;
|
||||
}
|
||||
finally
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (hasRegistrationHeader)
|
||||
node.RpcMapper.incomingRegistrationIds.Pop();
|
||||
if (ex is not OperationCanceledException)
|
||||
{
|
||||
logger.LogCritical(ex, "Error in request to {nodeId}!", config.Identifier);
|
||||
++serverErrorCount;
|
||||
}
|
||||
|
||||
response.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ namespace Tgstation.Server.Host.Swarm.Tests
|
||||
{
|
||||
sealed class TestableSwarmNode : IAsyncDisposable
|
||||
{
|
||||
public SwarmController Controller { get; private set; }
|
||||
|
||||
public SwarmService Service { get; private set; }
|
||||
|
||||
public SwarmConfiguration Config { get; }
|
||||
@@ -106,7 +104,14 @@ namespace Tgstation.Server.Host.Swarm.Tests
|
||||
|
||||
var mockServerUpdater = new Mock<IServerUpdater>();
|
||||
|
||||
RpcMapper = new SwarmRpcMapper(mockHttpClient, loggerFactory.CreateLogger($"SwarmRpcMapper-{swarmConfiguration.Identifier}"));
|
||||
RpcMapper = new SwarmRpcMapper(
|
||||
targetService => new SwarmController(
|
||||
targetService,
|
||||
mockAssemblyInformationProvider.Object,
|
||||
mockOptions.Object,
|
||||
loggerFactory.CreateLogger<SwarmController>()),
|
||||
mockHttpClient,
|
||||
loggerFactory.CreateLogger($"SwarmRpcMapper-{swarmConfiguration.Identifier}"));
|
||||
|
||||
mockServerUpdater
|
||||
.Setup(x => x.BeginUpdate(It.IsNotNull<SwarmService>(), It.IsNotNull<Version>(), It.IsAny<CancellationToken>()))
|
||||
@@ -144,13 +149,6 @@ namespace Tgstation.Server.Host.Swarm.Tests
|
||||
mockAsyncDelayer.Object,
|
||||
mockOptions.Object,
|
||||
serviceLogger);
|
||||
|
||||
Controller = new SwarmController(
|
||||
Service,
|
||||
RpcMapper,
|
||||
mockAssemblyInformationProvider.Object,
|
||||
mockOptions.Object,
|
||||
loggerFactory.CreateLogger<SwarmController>());
|
||||
}
|
||||
|
||||
RecreateControllerAndService();
|
||||
|
||||
Reference in New Issue
Block a user