-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
313 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
src/Gameboard.Api.Tests.Unit/Tests/Features/Games/ExternalGameDeployBatchServiceTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
using Gameboard.Api.Common; | ||
using Gameboard.Api.Features.GameEngine; | ||
using Gameboard.Api.Features.Games; | ||
using Gameboard.Api.Features.Games.External; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Gameboard.Api.Tests.Unit; | ||
|
||
public class ExternalGameDeployBatchServiceTests | ||
{ | ||
[Theory, GameboardAutoData] | ||
public void BuildDeployBatches_WithFixedBatchSizeAndChallengeCount_ReturnsCorrectBatchCount(IFixture fixture) | ||
{ | ||
// given a deploy request with 17 challenges and a batch size of 6 | ||
var challengeCount = 17; | ||
var request = BuildFakeGameModeStartRequest(challengeCount, fixture); | ||
|
||
// create sut and its options | ||
var sut = new ExternalGameDeployBatchService | ||
( | ||
new CoreOptions { GameEngineDeployBatchSize = 6 }, | ||
A.Fake<IGameEngineService>(), | ||
A.Fake<IGameHubBus>(), | ||
A.Fake<ILogger<ExternalGameDeployBatchService>>() | ||
); | ||
|
||
// when batches are built | ||
var result = sut.BuildDeployBatches(request); | ||
|
||
// we expect three batches | ||
result.Count().ShouldBe(3); | ||
// and the last should have 5 tasks in it | ||
result.Last().Count().ShouldBe(5); | ||
} | ||
|
||
[Theory, GameboardAutoData] | ||
public void BuildDeployBatches_WithNoConfiguredBatchSize_ReturnsExpectedBatchCount(IFixture fixture) | ||
{ | ||
// given a deploy request with any challenge count and no set batch size | ||
var challengeCount = fixture.Create<int>(); | ||
var request = BuildFakeGameModeStartRequest(challengeCount, fixture); | ||
|
||
var sut = new ExternalGameDeployBatchService | ||
( | ||
new CoreOptions { GameEngineDeployBatchSize = 0 }, | ||
A.Fake<IGameEngineService>(), | ||
A.Fake<IGameHubBus>(), | ||
A.Fake<ILogger<ExternalGameDeployBatchService>>() | ||
); | ||
|
||
// when batches are built | ||
var result = sut.BuildDeployBatches(request); | ||
|
||
// we expect one batch with length equal to the challenge count | ||
result.Count().ShouldBe(1); | ||
result.First().Count().ShouldBe(challengeCount); | ||
} | ||
|
||
private GameModeStartRequest BuildFakeGameModeStartRequest(int challengeCount, IFixture fixture) | ||
{ | ||
var gameId = fixture.Create<string>(); | ||
|
||
var request = new GameModeStartRequest | ||
{ | ||
GameId = fixture.Create<string>(), | ||
State = new GameStartState | ||
{ | ||
Game = new SimpleEntity { Id = gameId, Name = "game" }, | ||
ChallengesTotal = challengeCount, | ||
Now = DateTimeOffset.UtcNow | ||
}, | ||
Context = new GameModeStartRequestContext | ||
{ | ||
SessionLengthMinutes = fixture.Create<int>(), | ||
SpecIds = fixture.CreateMany<string>(challengeCount) | ||
} | ||
}; | ||
|
||
request.State.ChallengesCreated.AddRange(fixture.CreateMany<GameStartStateChallenge>(challengeCount).ToList()); | ||
return request; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
src/Gameboard.Api/Features/Challenge/ChallengeGraderUrlService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
using System; | ||
using System.Linq; | ||
using System.Threading; | ||
using Microsoft.AspNetCore.Hosting.Server; | ||
using Microsoft.AspNetCore.Hosting.Server.Features; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Routing; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Gameboard.Api.Features.Challenges; | ||
|
||
public interface IChallengeGraderUrlService | ||
{ | ||
public string BuildGraderUrl(); | ||
} | ||
|
||
internal class ChallengeGraderUrlService : IChallengeGraderUrlService | ||
{ | ||
private readonly HttpContext _httpContext; | ||
private readonly ILogger<ChallengeGraderUrlService> _logger; | ||
private readonly LinkGenerator _linkGenerator; | ||
private readonly IServer _server; | ||
|
||
public ChallengeGraderUrlService | ||
( | ||
IHttpContextAccessor httpContextAccessor, | ||
LinkGenerator linkGenerator, | ||
ILogger<ChallengeGraderUrlService> logger, | ||
IServer server | ||
) | ||
{ | ||
_httpContext = httpContextAccessor.HttpContext; | ||
_linkGenerator = linkGenerator; | ||
_logger = logger; | ||
_server = server; | ||
} | ||
|
||
public string BuildGraderUrl() | ||
{ | ||
// prefer to get this from the current request, but it can be null if the call | ||
// is coming from a background service that runs without an active request. | ||
// weirdly, HttpContext has a RequestAborted property of type CancellationToken, | ||
// but that property isn't accessible if the HttpContext has been disposed. | ||
// So we try/catch. If the context is disposed, we'll get an ObjectDisposed | ||
// exception, but we catch all exceptions anyway in case we can recover | ||
// with the server.Features.Get strategy. | ||
try | ||
{ | ||
return string.Join | ||
( | ||
'/', | ||
_linkGenerator.GetUriByAction | ||
( | ||
_httpContext, | ||
"Grade", | ||
"Challenge", | ||
null, | ||
_httpContext.Request.Scheme, | ||
_httpContext.Request.Host, | ||
_httpContext.Request.PathBase | ||
) | ||
); | ||
} | ||
catch (ObjectDisposedException ex) | ||
{ | ||
_logger.LogInformation($"Attempt to build grader URL with HttpContextAccessor failed: {ex.GetType().Name} :: {ex.Message} Attempting backup strategy..."); | ||
|
||
if (_server is not null) | ||
{ | ||
var addresses = _server.Features.Get<IServerAddressesFeature>(); | ||
|
||
var rootUrl = addresses.Addresses.FirstOrDefault(a => a.Contains("https")); | ||
if (rootUrl.IsEmpty()) | ||
rootUrl = addresses.Addresses.FirstOrDefault(); | ||
|
||
if (!rootUrl.IsEmpty()) | ||
return $"{rootUrl}/challenge/grade"; | ||
} | ||
} | ||
|
||
throw new GraderUrlResolutionError(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
90 changes: 90 additions & 0 deletions
90
src/Gameboard.Api/Features/Game/External/Services/ExternalGameDeployBatchService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Gameboard.Api.Features.GameEngine; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Gameboard.Api.Features.Games.External; | ||
|
||
public interface IExternalGameDeployBatchService | ||
{ | ||
/// <summary> | ||
/// Create batches of gamespace deploy requests from challenges. | ||
/// | ||
/// An external game can have many challenges, and each challenge has an associated gamespace. For | ||
/// each gamespace, Gameboard must issue a request to the game engine that causes it to deploy | ||
/// the gamespace. Requesting all of these at once can cause issues with request timeouts, so | ||
/// we optionally allow Gameboard sysadmins to configure a batch size appropriate to their game engine. | ||
/// | ||
/// By default, this value is 4, meaning that upon start of an external game, Gameboard will issue | ||
/// requests to the game engine in batches of 4 until all gamespaces have been deployed. Configure this | ||
/// with the Core__GameEngineDeployBatchSize setting in Gameboard's helm chart. | ||
/// </summary> | ||
/// <param name="request"></param> | ||
/// <returns></returns> | ||
IEnumerable<IEnumerable<Task<GameEngineGameState>>> BuildDeployBatches(GameModeStartRequest request); | ||
} | ||
|
||
internal class ExternalGameDeployBatchService : IExternalGameDeployBatchService | ||
{ | ||
private readonly CoreOptions _coreOptions; | ||
private readonly IGameEngineService _gameEngine; | ||
private readonly IGameHubBus _gameHubBus; | ||
private readonly ILogger<ExternalGameDeployBatchService> _logger; | ||
|
||
public ExternalGameDeployBatchService | ||
( | ||
CoreOptions coreOptions, | ||
IGameEngineService gameEngine, | ||
IGameHubBus gameHubBus, | ||
ILogger<ExternalGameDeployBatchService> logger | ||
) | ||
{ | ||
_coreOptions = coreOptions; | ||
_gameEngine = gameEngine; | ||
_gameHubBus = gameHubBus; | ||
_logger = logger; | ||
} | ||
|
||
public IEnumerable<IEnumerable<Task<GameEngineGameState>>> BuildDeployBatches(GameModeStartRequest request) | ||
{ | ||
// first, create a task for each gamespace to be deployed | ||
var gamespaceTasks = request.State.ChallengesCreated.Select(async c => | ||
{ | ||
_logger.LogInformation(message: $"""Starting {c.GameEngineType} gamespace for challenge "{c.Challenge.Id}" (teamId "{c.TeamId}")..."""); | ||
var challengeState = await _gameEngine.StartGamespace(new GameEngineGamespaceStartRequest | ||
{ | ||
ChallengeId = c.Challenge.Id, | ||
GameEngineType = c.GameEngineType | ||
}); | ||
|
||
request.State.GamespacesStarted.Add(challengeState); | ||
await _gameHubBus.SendExternalGameGamespacesDeployProgressChange(request.State); | ||
_logger.LogInformation(message: $"""Gamespace started for challenge "{c.Challenge.Id}"."""); | ||
|
||
// keep the state given to us by the engine | ||
return challengeState; | ||
}).ToArray(); | ||
|
||
// if the setting isn't configured or is a nonsense value, just return all the tasks in one batch | ||
if (_coreOptions.GameEngineDeployBatchSize <= 1) | ||
return new IEnumerable<Task<GameEngineGameState>>[] { gamespaceTasks.ToArray() }; | ||
|
||
// otherwise, create batches of the appropriate size plus an additional batch for any leftovers | ||
var batchList = new List<IEnumerable<Task<GameEngineGameState>>>(); | ||
List<Task<GameEngineGameState>> currentBatch = null; | ||
|
||
for (var challengeIndex = 0; challengeIndex < gamespaceTasks.Length; challengeIndex++) | ||
{ | ||
if (challengeIndex % _coreOptions.GameEngineDeployBatchSize == 0) | ||
{ | ||
currentBatch = new List<Task<GameEngineGameState>>(); | ||
batchList.Add(currentBatch); | ||
} | ||
|
||
currentBatch.Add(gamespaceTasks[challengeIndex]); | ||
} | ||
|
||
return batchList; | ||
} | ||
} |
Oops, something went wrong.