LANCommander/LANCommander.Server.Services/PlaySessionSweepService.cs
Pat Hartl 4fc1a45f88 Add support for horizontal scaling
This commit adds support to allow the LANCommander server to scale horizontally. Multiple instances of the application can be deployed for load-balancing purposes. This will require a load balancer in front of the instances, as well as either MySQL or PostgreSQL and a Redis cache.
2026-07-22 20:19:31 -05:00

56 lines
2.3 KiB
C#

using LANCommander.Server.Services.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace LANCommander.Server.Services;
/// <summary>
/// Periodically ends play sessions that have stopped sending keepalives (e.g. the launcher crashed
/// or lost its connection), so the active-session list stays accurate for autostop and player
/// counts. Stale sessions are ended at their last known-alive time. Each affected game is handed to
/// <see cref="ServerManager.ScheduleStop"/> so on-player-activity servers still autostop.
/// </summary>
public sealed class PlaySessionSweepService(
IServiceScopeFactory scopeFactory,
ServerManager serverManager,
ICoordinatorElection coordinatorElection,
SettingsProvider<Settings.Settings> settingsProvider,
ILogger<PlaySessionSweepService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
var gameServers = settingsProvider.CurrentValue.Server.GameServers;
var interval = TimeSpan.FromSeconds(Math.Max(5, gameServers.KeepAliveSweepInterval));
var timeout = TimeSpan.FromSeconds(Math.Max(1, gameServers.KeepAliveTimeout));
await Task.Delay(interval, stoppingToken);
// Only the coordinator sweeps, so autostop scheduling isn't duplicated across
// instances. ScheduleStop keeps in-memory state that must live on one node.
if (!coordinatorElection.IsLeader)
continue;
using var scope = scopeFactory.CreateScope();
var playSessionService = scope.ServiceProvider.GetRequiredService<PlaySessionService>();
var affectedGameIds = await playSessionService.EndStaleSessionsAsync(timeout);
foreach (var gameId in affectedGameIds)
serverManager.ScheduleStop(gameId);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
logger.LogError(ex, "Play session sweep failed");
}
}
}
}