potential fix for locking issues
This commit is contained in:
parent
7044ffda79
commit
c0172963d5
|
@ -80,8 +80,6 @@ internal sealed class Endpoints(
|
||||||
if (name.Length > 12) return TypedResults.BadRequest("name too long");
|
if (name.Length > 12) return TypedResults.BadRequest("name too long");
|
||||||
|
|
||||||
var player = playerService.GetOrAdd(name);
|
var player = playerService.GetOrAdd(name);
|
||||||
return player != null
|
return TypedResults.Ok(player.Name);
|
||||||
? TypedResults.Ok(player.Name)
|
|
||||||
: TypedResults.Unauthorized();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,10 @@ internal sealed class CollectPowerUp(
|
||||||
MapEntityManager entityManager
|
MapEntityManager entityManager
|
||||||
) : ITickStep
|
) : ITickStep
|
||||||
{
|
{
|
||||||
public Task TickAsync(TimeSpan delta)
|
public ValueTask TickAsync(TimeSpan delta)
|
||||||
{
|
{
|
||||||
entityManager.RemoveWhere(TryCollect);
|
entityManager.RemoveWhere(TryCollect);
|
||||||
return Task.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryCollect(PowerUp obj)
|
private bool TryCollect(PowerUp obj)
|
||||||
|
|
|
@ -9,12 +9,12 @@ internal sealed class CollideBullets(
|
||||||
{
|
{
|
||||||
private const int ExplosionRadius = 3;
|
private const int ExplosionRadius = 3;
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan _)
|
public ValueTask TickAsync(TimeSpan _)
|
||||||
{
|
{
|
||||||
entityManager.RemoveWhere(BulletHitsTank);
|
entityManager.RemoveWhere(BulletHitsTank);
|
||||||
entityManager.RemoveWhere(BulletHitsWall);
|
entityManager.RemoveWhere(BulletHitsWall);
|
||||||
entityManager.RemoveWhere(BulletTimesOut);
|
entityManager.RemoveWhere(BulletTimesOut);
|
||||||
return Task.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool BulletTimesOut(Bullet bullet)
|
private bool BulletTimesOut(Bullet bullet)
|
||||||
|
|
|
@ -2,5 +2,5 @@ namespace TanksServer.GameLogic;
|
||||||
|
|
||||||
public interface ITickStep
|
public interface ITickStep
|
||||||
{
|
{
|
||||||
Task TickAsync(TimeSpan delta);
|
ValueTask TickAsync(TimeSpan delta);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@ internal sealed class MoveBullets(
|
||||||
IOptions<GameRules> config
|
IOptions<GameRules> config
|
||||||
) : ITickStep
|
) : ITickStep
|
||||||
{
|
{
|
||||||
public Task TickAsync(TimeSpan delta)
|
public ValueTask TickAsync(TimeSpan delta)
|
||||||
{
|
{
|
||||||
foreach (var bullet in entityManager.Bullets)
|
foreach (var bullet in entityManager.Bullets)
|
||||||
MoveBullet(bullet, delta);
|
MoveBullet(bullet, delta);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MoveBullet(Bullet bullet, TimeSpan delta)
|
private void MoveBullet(Bullet bullet, TimeSpan delta)
|
||||||
|
|
|
@ -8,12 +8,12 @@ internal sealed class MoveTanks(
|
||||||
{
|
{
|
||||||
private readonly GameRules _config = options.Value;
|
private readonly GameRules _config = options.Value;
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan delta)
|
public ValueTask TickAsync(TimeSpan delta)
|
||||||
{
|
{
|
||||||
foreach (var tank in entityManager.Tanks)
|
foreach (var tank in entityManager.Tanks)
|
||||||
tank.Moving = TryMoveTank(tank, delta);
|
tank.Moving = TryMoveTank(tank, delta);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryMoveTank(Tank tank, TimeSpan delta)
|
private bool TryMoveTank(Tank tank, TimeSpan delta)
|
||||||
|
|
|
@ -8,7 +8,7 @@ internal sealed class RotateTanks(
|
||||||
{
|
{
|
||||||
private readonly GameRules _config = options.Value;
|
private readonly GameRules _config = options.Value;
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan delta)
|
public ValueTask TickAsync(TimeSpan delta)
|
||||||
{
|
{
|
||||||
foreach (var tank in entityManager.Tanks)
|
foreach (var tank in entityManager.Tanks)
|
||||||
{
|
{
|
||||||
|
@ -30,6 +30,6 @@ internal sealed class RotateTanks(
|
||||||
logger.LogTrace("rotated tank to {}", tank.Rotation);
|
logger.LogTrace("rotated tank to {}", tank.Rotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,12 @@ internal sealed class ShootFromTanks(
|
||||||
{
|
{
|
||||||
private readonly GameRules _config = options.Value;
|
private readonly GameRules _config = options.Value;
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan _)
|
public ValueTask TickAsync(TimeSpan _)
|
||||||
{
|
{
|
||||||
foreach (var tank in entityManager.Tanks.Where(t => !t.Moving))
|
foreach (var tank in entityManager.Tanks.Where(t => !t.Moving))
|
||||||
Shoot(tank);
|
Shoot(tank);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Shoot(Tank tank)
|
private void Shoot(Tank tank)
|
||||||
|
|
|
@ -8,14 +8,14 @@ internal sealed class SpawnPowerUp(
|
||||||
private readonly double _spawnChance = options.Value.PowerUpSpawnChance;
|
private readonly double _spawnChance = options.Value.PowerUpSpawnChance;
|
||||||
private readonly int _maxCount = options.Value.MaxPowerUpCount;
|
private readonly int _maxCount = options.Value.MaxPowerUpCount;
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan delta)
|
public ValueTask TickAsync(TimeSpan delta)
|
||||||
{
|
{
|
||||||
if (entityManager.PowerUps.Count() >= _maxCount)
|
if (entityManager.PowerUps.Count() >= _maxCount)
|
||||||
return Task.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
if (Random.Shared.NextDouble() > _spawnChance * delta.TotalSeconds)
|
if (Random.Shared.NextDouble() > _spawnChance * delta.TotalSeconds)
|
||||||
return Task.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
|
|
||||||
entityManager.SpawnPowerUp();
|
entityManager.SpawnPowerUp();
|
||||||
return Task.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,12 +43,12 @@ internal sealed class TankSpawnQueue(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan _)
|
public ValueTask TickAsync(TimeSpan _)
|
||||||
{
|
{
|
||||||
if (!TryDequeueNext(out var player))
|
if (!TryDequeueNext(out var player))
|
||||||
return Task.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
|
|
||||||
entityManager.SpawnTank(player);
|
entityManager.SpawnTank(player);
|
||||||
return Task.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ internal sealed class GeneratePixelsTickStep(
|
||||||
private readonly List<IDrawStep> _drawSteps = drawSteps.ToList();
|
private readonly List<IDrawStep> _drawSteps = drawSteps.ToList();
|
||||||
private readonly List<IFrameConsumer> _consumers = consumers.ToList();
|
private readonly List<IFrameConsumer> _consumers = consumers.ToList();
|
||||||
|
|
||||||
public async Task TickAsync(TimeSpan _)
|
public async ValueTask TickAsync(TimeSpan _)
|
||||||
{
|
{
|
||||||
PixelGrid observerPixelGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn);
|
PixelGrid observerPixelGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn);
|
||||||
|
|
||||||
|
|
|
@ -4,5 +4,5 @@ namespace TanksServer.Graphics;
|
||||||
|
|
||||||
internal interface IFrameConsumer
|
internal interface IFrameConsumer
|
||||||
{
|
{
|
||||||
Task OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels);
|
ValueTask OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,6 @@ internal sealed class ClientScreenServer(
|
||||||
player
|
player
|
||||||
));
|
));
|
||||||
|
|
||||||
public Task OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels)
|
public ValueTask OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels)
|
||||||
=> ParallelForEachConnectionAsync(c => c.OnGameTickAsync(observerPixels, gamePixelGrid).AsTask());
|
=> ParallelForEachConnectionAsync(c => c.OnGameTickAsync(observerPixels, gamePixelGrid).AsTask());
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,93 +8,66 @@ internal sealed class ClientScreenServerConnection(
|
||||||
WebSocket webSocket,
|
WebSocket webSocket,
|
||||||
ILogger<ClientScreenServerConnection> logger,
|
ILogger<ClientScreenServerConnection> logger,
|
||||||
string? playerName = null
|
string? playerName = null
|
||||||
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(webSocket, logger, 0)),
|
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(webSocket, logger, 0))
|
||||||
IDisposable
|
|
||||||
{
|
{
|
||||||
private readonly SemaphoreSlim _wantedFramesOnTick = new(0, 2);
|
private bool _wantsFrameOnTick = true;
|
||||||
private readonly SemaphoreSlim _mutex = new(1);
|
|
||||||
|
|
||||||
private PixelGrid? _lastSentPixels = null;
|
private PixelGrid? _lastSentPixels;
|
||||||
private PixelGrid? _nextPixels = null;
|
private PixelGrid? _nextPixels;
|
||||||
private readonly PlayerScreenData? _nextPlayerData = playerName != null ? new PlayerScreenData(logger) : null;
|
private readonly PlayerScreenData? _nextPlayerData = playerName != null ? new PlayerScreenData(logger) : null;
|
||||||
|
|
||||||
protected override async ValueTask HandleMessageAsync(Memory<byte> _)
|
protected override async ValueTask HandleMessageLockedAsync(Memory<byte> _)
|
||||||
{
|
{
|
||||||
await _mutex.WaitAsync();
|
if (_nextPixels == null)
|
||||||
|
{
|
||||||
|
_wantsFrameOnTick = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendNowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask OnGameTickAsync(PixelGrid pixels, GamePixelGrid gamePixelGrid) => LockedAsync(async () =>
|
||||||
|
{
|
||||||
|
if (pixels == _lastSentPixels)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_nextPlayerData != null)
|
||||||
|
{
|
||||||
|
_nextPlayerData.Clear();
|
||||||
|
foreach (var gamePixel in gamePixelGrid)
|
||||||
|
{
|
||||||
|
if (!gamePixel.EntityType.HasValue)
|
||||||
|
continue;
|
||||||
|
_nextPlayerData.Add(gamePixel.EntityType.Value, gamePixel.BelongsTo?.Name == playerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_nextPixels = pixels;
|
||||||
|
if (_wantsFrameOnTick)
|
||||||
|
_ = await SendNowAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
private async ValueTask<bool> SendNowAsync()
|
||||||
|
{
|
||||||
|
var pixels = _nextPixels
|
||||||
|
?? throw new InvalidOperationException("next pixels not set");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_nextPixels == null)
|
await Socket.SendBinaryAsync(pixels.Data, _nextPlayerData == null);
|
||||||
{
|
if (_nextPlayerData != null)
|
||||||
_wantedFramesOnTick.Release();
|
await Socket.SendBinaryAsync(_nextPlayerData.GetPacket());
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastSentPixels = _nextPixels;
|
_lastSentPixels = _nextPixels;
|
||||||
_nextPixels = null;
|
_nextPixels = null;
|
||||||
await SendNowAsync(_lastSentPixels);
|
_wantsFrameOnTick = false;
|
||||||
}
|
return true;
|
||||||
catch (SemaphoreFullException)
|
|
||||||
{
|
|
||||||
logger.LogWarning("ignoring request for more frames");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_mutex.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask OnGameTickAsync(PixelGrid pixels, GamePixelGrid gamePixelGrid)
|
|
||||||
{
|
|
||||||
await _mutex.WaitAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (pixels == _lastSentPixels)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_nextPlayerData != null)
|
|
||||||
{
|
|
||||||
_nextPlayerData.Clear();
|
|
||||||
foreach (var gamePixel in gamePixelGrid)
|
|
||||||
{
|
|
||||||
if (!gamePixel.EntityType.HasValue)
|
|
||||||
continue;
|
|
||||||
_nextPlayerData.Add(gamePixel.EntityType.Value, gamePixel.BelongsTo?.Name == playerName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendImmediately = await _wantedFramesOnTick.WaitAsync(TimeSpan.Zero);
|
|
||||||
if (sendImmediately)
|
|
||||||
{
|
|
||||||
await SendNowAsync(pixels);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_wantedFramesOnTick.Release();
|
|
||||||
_nextPixels = pixels;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_mutex.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask SendNowAsync(PixelGrid pixels)
|
|
||||||
{
|
|
||||||
Logger.LogTrace("sending");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Logger.LogTrace("sending {} bytes of pixel data", pixels.Data.Length);
|
|
||||||
await Socket.SendBinaryAsync(pixels.Data, _nextPlayerData == null);
|
|
||||||
if (_nextPlayerData != null)
|
|
||||||
{
|
|
||||||
await Socket.SendBinaryAsync(_nextPlayerData.GetPacket());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (WebSocketException ex)
|
catch (WebSocketException ex)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(ex, "send failed");
|
Logger.LogWarning(ex, "send failed");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => _wantedFramesOnTick.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ internal sealed class ControlsServerConnection(
|
||||||
Shoot = 0x05
|
Shoot = 0x05
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override ValueTask HandleMessageAsync(Memory<byte> buffer)
|
protected override ValueTask HandleMessageLockedAsync(Memory<byte> buffer)
|
||||||
{
|
{
|
||||||
var type = (MessageType)buffer.Span[0];
|
var type = (MessageType)buffer.Span[0];
|
||||||
var control = (InputType)buffer.Span[1];
|
var control = (InputType)buffer.Span[1];
|
||||||
|
|
|
@ -10,19 +10,19 @@ internal sealed class PlayerInfoConnection(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
WebSocket rawSocket,
|
WebSocket rawSocket,
|
||||||
MapEntityManager entityManager
|
MapEntityManager entityManager
|
||||||
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(rawSocket, logger, 0)), IDisposable
|
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(rawSocket, logger, 0))
|
||||||
{
|
{
|
||||||
private readonly SemaphoreSlim _wantedFrames = new(1);
|
|
||||||
private readonly AppSerializerContext _context = new(new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
private readonly AppSerializerContext _context = new(new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||||
|
private bool _wantsInfoOnTick;
|
||||||
private byte[] _lastMessage = [];
|
private byte[] _lastMessage = [];
|
||||||
|
|
||||||
protected override ValueTask HandleMessageAsync(Memory<byte> buffer)
|
protected override ValueTask HandleMessageLockedAsync(Memory<byte> buffer)
|
||||||
{
|
{
|
||||||
var response = GetMessageToSend();
|
var response = GetMessageToSend();
|
||||||
if (response == null)
|
if (response == null)
|
||||||
{
|
{
|
||||||
Logger.LogTrace("cannot respond directly, increasing wanted frames");
|
Logger.LogTrace("cannot respond directly, increasing wanted frames");
|
||||||
_wantedFrames.Release();
|
_wantsInfoOnTick = true;
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,21 +30,18 @@ internal sealed class PlayerInfoConnection(
|
||||||
return Socket.SendTextAsync(response);
|
return Socket.SendTextAsync(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task OnGameTickAsync()
|
public ValueTask OnGameTickAsync() => LockedAsync(() =>
|
||||||
{
|
{
|
||||||
if (!await _wantedFrames.WaitAsync(TimeSpan.Zero))
|
if (!_wantsInfoOnTick)
|
||||||
return;
|
return ValueTask.CompletedTask;
|
||||||
|
|
||||||
var response = GetMessageToSend();
|
var response = GetMessageToSend();
|
||||||
if (response == null)
|
if (response == null)
|
||||||
{
|
return ValueTask.CompletedTask;
|
||||||
_wantedFrames.Release();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.LogTrace("responding indirectly");
|
Logger.LogTrace("responding indirectly");
|
||||||
await Socket.SendTextAsync(response);
|
return Socket.SendTextAsync(response);
|
||||||
}
|
});
|
||||||
|
|
||||||
private byte[]? GetMessageToSend()
|
private byte[]? GetMessageToSend()
|
||||||
{
|
{
|
||||||
|
@ -77,6 +74,4 @@ internal sealed class PlayerInfoConnection(
|
||||||
str.Append(']');
|
str.Append(']');
|
||||||
return str.ToString();
|
return str.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => _wantedFrames.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ internal sealed class PlayerServer(
|
||||||
private readonly Dictionary<string, Player> _players = [];
|
private readonly Dictionary<string, Player> _players = [];
|
||||||
private readonly SemaphoreSlim _mutex = new(1, 1);
|
private readonly SemaphoreSlim _mutex = new(1, 1);
|
||||||
|
|
||||||
public Player? GetOrAdd(string name)
|
public Player GetOrAdd(string name)
|
||||||
{
|
{
|
||||||
_mutex.Wait();
|
_mutex.Wait();
|
||||||
try
|
try
|
||||||
|
@ -67,6 +67,6 @@ internal sealed class PlayerServer(
|
||||||
public Task HandleClientAsync(WebSocket webSocket, Player player)
|
public Task HandleClientAsync(WebSocket webSocket, Player player)
|
||||||
=> HandleClientAsync(new PlayerInfoConnection(player, connectionLogger, webSocket, entityManager));
|
=> HandleClientAsync(new PlayerInfoConnection(player, connectionLogger, webSocket, entityManager));
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan delta)
|
public ValueTask TickAsync(TimeSpan delta)
|
||||||
=> ParallelForEachConnectionAsync(connection => connection.OnGameTickAsync());
|
=> ParallelForEachConnectionAsync(connection => connection.OnGameTickAsync().AsTask());
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels)
|
public async ValueTask OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels)
|
||||||
{
|
{
|
||||||
if (DateTime.Now < _nextFrameAfter)
|
if (DateTime.Now < _nextFrameAfter)
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -14,7 +14,7 @@ internal abstract class WebsocketServer<T>(
|
||||||
public async Task StoppingAsync(CancellationToken cancellationToken)
|
public async Task StoppingAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
logger.LogInformation("closing connections");
|
logger.LogInformation("closing connections");
|
||||||
await Locked(async () =>
|
await LockedAsync(async () =>
|
||||||
{
|
{
|
||||||
_closing = true;
|
_closing = true;
|
||||||
await Task.WhenAll(_connections.Select(c => c.CloseAsync()));
|
await Task.WhenAll(_connections.Select(c => c.CloseAsync()));
|
||||||
|
@ -22,35 +22,24 @@ internal abstract class WebsocketServer<T>(
|
||||||
logger.LogInformation("closed connections");
|
logger.LogInformation("closed connections");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Task ParallelForEachConnectionAsync(Func<T, Task> body)
|
protected ValueTask ParallelForEachConnectionAsync(Func<T, Task> body) =>
|
||||||
{
|
LockedAsync(async () => await Task.WhenAll(_connections.Select(body)), CancellationToken.None);
|
||||||
_mutex.Wait();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Task.WhenAll(_connections.Select(body));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_mutex.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task AddConnectionAsync(T connection) => Locked(() =>
|
private ValueTask AddConnectionAsync(T connection) => LockedAsync(async () =>
|
||||||
{
|
{
|
||||||
if (_closing)
|
if (_closing)
|
||||||
{
|
{
|
||||||
logger.LogWarning("refusing connection because server is shutting down");
|
logger.LogWarning("refusing connection because server is shutting down");
|
||||||
return connection.CloseAsync();
|
await connection.CloseAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
_connections.Add(connection);
|
_connections.Add(connection);
|
||||||
return Task.CompletedTask;
|
|
||||||
}, CancellationToken.None);
|
}, CancellationToken.None);
|
||||||
|
|
||||||
private Task RemoveConnectionAsync(T connection) => Locked(() =>
|
private ValueTask RemoveConnectionAsync(T connection) => LockedAsync(() =>
|
||||||
{
|
{
|
||||||
_connections.Remove(connection);
|
_connections.Remove(connection);
|
||||||
return Task.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}, CancellationToken.None);
|
}, CancellationToken.None);
|
||||||
|
|
||||||
protected async Task HandleClientAsync(T connection)
|
protected async Task HandleClientAsync(T connection)
|
||||||
|
@ -60,7 +49,7 @@ internal abstract class WebsocketServer<T>(
|
||||||
await RemoveConnectionAsync(connection);
|
await RemoveConnectionAsync(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Locked(Func<Task> action, CancellationToken cancellationToken)
|
private async ValueTask LockedAsync(Func<ValueTask> action, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _mutex.WaitAsync(cancellationToken);
|
await _mutex.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
|
|
|
@ -3,8 +3,9 @@ namespace TanksServer.Interactivity;
|
||||||
internal abstract class WebsocketServerConnection(
|
internal abstract class WebsocketServerConnection(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
ByteChannelWebSocket socket
|
ByteChannelWebSocket socket
|
||||||
)
|
) : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly SemaphoreSlim _mutex = new(1);
|
||||||
protected readonly ByteChannelWebSocket Socket = socket;
|
protected readonly ByteChannelWebSocket Socket = socket;
|
||||||
protected readonly ILogger Logger = logger;
|
protected readonly ILogger Logger = logger;
|
||||||
|
|
||||||
|
@ -17,9 +18,24 @@ internal abstract class WebsocketServerConnection(
|
||||||
public async Task ReceiveAsync()
|
public async Task ReceiveAsync()
|
||||||
{
|
{
|
||||||
await foreach (var buffer in Socket.ReadAllAsync())
|
await foreach (var buffer in Socket.ReadAllAsync())
|
||||||
await HandleMessageAsync(buffer);
|
await LockedAsync(() => HandleMessageLockedAsync(buffer));
|
||||||
Logger.LogTrace("done receiving");
|
Logger.LogTrace("done receiving");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract ValueTask HandleMessageAsync(Memory<byte> buffer);
|
protected abstract ValueTask HandleMessageLockedAsync(Memory<byte> buffer);
|
||||||
|
|
||||||
|
protected async ValueTask LockedAsync(Func<ValueTask> action)
|
||||||
|
{
|
||||||
|
await _mutex.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await action();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mutex.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => _mutex.Dispose();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue