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