potential fix for locking issues

This commit is contained in:
Vinzenz Schroeter 2024-04-28 15:34:32 +02:00
parent 7044ffda79
commit c0172963d5
20 changed files with 112 additions and 141 deletions

View file

@ -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();
} }
} }

View file

@ -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)

View file

@ -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)

View file

@ -2,5 +2,5 @@ namespace TanksServer.GameLogic;
public interface ITickStep public interface ITickStep
{ {
Task TickAsync(TimeSpan delta); ValueTask TickAsync(TimeSpan delta);
} }

View file

@ -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)

View file

@ -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)

View file

@ -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;
} }
} }

View file

@ -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)

View file

@ -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;
} }
} }

View file

@ -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;
} }
} }

View file

@ -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);

View file

@ -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);
} }

View file

@ -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());
} }

View file

@ -8,45 +8,26 @@ 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();
try
{ {
if (_nextPixels == null) if (_nextPixels == null)
{ {
_wantedFramesOnTick.Release(); _wantsFrameOnTick = true;
return; return;
} }
_lastSentPixels = _nextPixels; await SendNowAsync();
_nextPixels = null;
await SendNowAsync(_lastSentPixels);
}
catch (SemaphoreFullException)
{
logger.LogWarning("ignoring request for more frames");
}
finally
{
_mutex.Release();
}
} }
public async ValueTask OnGameTickAsync(PixelGrid pixels, GamePixelGrid gamePixelGrid) public ValueTask OnGameTickAsync(PixelGrid pixels, GamePixelGrid gamePixelGrid) => LockedAsync(async () =>
{
await _mutex.WaitAsync();
try
{ {
if (pixels == _lastSentPixels) if (pixels == _lastSentPixels)
return; return;
@ -62,39 +43,31 @@ internal sealed class ClientScreenServerConnection(
} }
} }
var sendImmediately = await _wantedFramesOnTick.WaitAsync(TimeSpan.Zero);
if (sendImmediately)
{
await SendNowAsync(pixels);
return;
}
_wantedFramesOnTick.Release();
_nextPixels = pixels; _nextPixels = pixels;
} if (_wantsFrameOnTick)
finally _ = await SendNowAsync();
{ });
_mutex.Release();
}
}
private async ValueTask SendNowAsync(PixelGrid pixels) private async ValueTask<bool> SendNowAsync()
{ {
Logger.LogTrace("sending"); var pixels = _nextPixels
?? throw new InvalidOperationException("next pixels not set");
try try
{ {
Logger.LogTrace("sending {} bytes of pixel data", pixels.Data.Length);
await Socket.SendBinaryAsync(pixels.Data, _nextPlayerData == null); await Socket.SendBinaryAsync(pixels.Data, _nextPlayerData == null);
if (_nextPlayerData != null) if (_nextPlayerData != null)
{
await Socket.SendBinaryAsync(_nextPlayerData.GetPacket()); await Socket.SendBinaryAsync(_nextPlayerData.GetPacket());
}
_lastSentPixels = _nextPixels;
_nextPixels = null;
_wantsFrameOnTick = false;
return true;
} }
catch (WebSocketException ex) catch (WebSocketException ex)
{ {
Logger.LogWarning(ex, "send failed"); Logger.LogWarning(ex, "send failed");
return false;
} }
} }
public void Dispose() => _wantedFramesOnTick.Dispose();
} }

View file

@ -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];

View file

@ -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();
} }

View file

@ -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());
} }

View file

@ -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;

View file

@ -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

View file

@ -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();
} }