Skip to content

Commit

Permalink
Add a lock on most interactions with game objects
Browse files Browse the repository at this point in the history
We see rare problems where multiple messages on the same game arriving at
the same game cause issues. This commit adds a coarse-grained lock on
all interactions with a game by adding a binary semaphore.

Because manually adding try{} finally{} blocks seems verbose, I added a
RAII-style LockGuard that locks the semaphore on creation and drops it
when disposed.
  • Loading branch information
miniduikboot committed Nov 16, 2023
1 parent c592bd1 commit a544425
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 24 deletions.
6 changes: 6 additions & 0 deletions src/Impostor.Api/Games/IGame.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Impostor.Api.Events;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.GameOptions;
using Impostor.Api.Net;
Expand Down Expand Up @@ -52,6 +53,11 @@ public interface IGame
T? FindObjectByNetId<T>(uint netId)
where T : IInnerNetObject;

/// <summary>Lock an IGame to prevent concurrent modification.</summary>
/// <returns>A <see cref="LockGuard" /> that should be awaited before and disposed after modifying the game.</returns>
/// <remarks>This method should be called when modifying a game outside of an <see cref="IGameEvent" /> handler.</remarks>
Task<LockGuard> LockAsync();

/// <summary>
/// Adds an <see cref="IPAddress" /> to the ban list of this game.
/// Prevents all future joins from this <see cref="IPAddress" />.
Expand Down
61 changes: 61 additions & 0 deletions src/Impostor.Api/Net/LockGuard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Impostor.Api.Net
{
/// <summary>RAII-style lock guard for use in Impostor.</summary>
/// <remarks>
/// To lock a semaphore, use the LockAsync method and await the result.
/// To unlock the semaphore, dispose of the LockGuard.
/// </remarks>
public sealed class LockGuard : IDisposable
{
private readonly SemaphoreSlim _semaphore;

private bool _disposedValue;

private LockGuard(SemaphoreSlim semaphore)
{
_semaphore = semaphore;
}

private void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_semaphore.Release();
}

_disposedValue = true;
}
}

/// <summary>Create a RAII-style lock guard for an instance of <see cref="SemaphoreSlim"/>.</summary>
/// <param name="semaphore">The semaphore to Wait and Release on.</param>
/// <param name="timeout">The amount of milliseconds to wait for the lock becoming available.</param>
/// <exception cref="ImpostorException">When the timeout expires.</exception>
/// <returns>The lock guard if the semaphore could be locked succesfully.</returns>
public static async Task<LockGuard> LockAsync(SemaphoreSlim semaphore, int timeout = 1000)
{
var success = await semaphore.WaitAsync(timeout);
if (success)
{
return new LockGuard(semaphore);
}
else
{
throw new ImpostorException("Could not lock semaphore");
}
}

/// <inheritdoc/>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
2 changes: 2 additions & 0 deletions src/Impostor.Server/Net/State/Game.Data.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ internal partial class Game

public async ValueTask<bool> HandleGameDataAsync(IMessageReader parent, ClientPlayer sender, bool toPlayer)
{
using var guard = await LockAsync();

// Find target player.
ClientPlayer? target = null;

Expand Down
30 changes: 6 additions & 24 deletions src/Impostor.Server/Net/State/Game.Incoming.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Impostor.Api.Games;
using Impostor.Api.Innersloth;
Expand All @@ -13,10 +11,9 @@ namespace Impostor.Server.Net.State
{
internal partial class Game
{
private readonly SemaphoreSlim _clientAddLock = new SemaphoreSlim(1, 1);

public async ValueTask HandleStartGame(IMessageReader message)
{
using var guard = await LockAsync();
GameState = GameStates.Starting;

using var packet = MessageWriter.Get(MessageType.Reliable);
Expand All @@ -28,6 +25,7 @@ public async ValueTask HandleStartGame(IMessageReader message)

public async ValueTask HandleEndGame(IMessageReader message, GameOverReason gameOverReason)
{
using var guard = await LockAsync();
GameState = GameStates.Ended;

// Broadcast end of the game.
Expand All @@ -48,6 +46,7 @@ public async ValueTask HandleEndGame(IMessageReader message, GameOverReason game

public async ValueTask HandleAlterGame(IMessageReader message, IClientPlayer sender, bool isPublic)
{
using var guard = await LockAsync();
IsPublic = isPublic;

using var packet = MessageWriter.Get(MessageType.Reliable);
Expand All @@ -74,6 +73,7 @@ public async ValueTask HandleRemovePlayer(int playerId, DisconnectReason reason)

public async ValueTask HandleKickPlayer(int playerId, bool isBan)
{
using var guard = await LockAsync();
_logger.LogInformation("{0} - Player {1} has left.", Code, playerId);

using var message = MessageWriter.Get(MessageType.Reliable);
Expand All @@ -96,26 +96,8 @@ public async ValueTask HandleKickPlayer(int playerId, bool isBan)

public async ValueTask<GameJoinResult> AddClientAsync(ClientBase client)
{
var hasLock = false;

try
{
hasLock = await _clientAddLock.WaitAsync(TimeSpan.FromMinutes(1));

if (hasLock)
{
return await AddClientSafeAsync(client);
}
}
finally
{
if (hasLock)
{
_clientAddLock.Release();
}
}

return GameJoinResult.FromError(GameJoinError.InvalidClient);
using var guard = await LockAsync();
return await AddClientSafeAsync(client);
}

private async ValueTask HandleJoinGameNew(ClientPlayer sender, bool isNew)
Expand Down
8 changes: 8 additions & 0 deletions src/Impostor.Server/Net/State/Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Impostor.Api.Config;
using Impostor.Api.Events.Managers;
Expand Down Expand Up @@ -32,6 +33,7 @@ internal partial class Game
private readonly ICompatibilityManager _compatibilityManager;
private readonly CompatibilityConfig _compatibilityConfig;
private readonly TimeoutConfig _timeoutConfig;
private readonly SemaphoreSlim _lock;

public Game(
ILogger<Game> logger,
Expand Down Expand Up @@ -66,6 +68,7 @@ internal partial class Game
_compatibilityConfig = compatibilityConfig.Value;
_timeoutConfig = timeoutConfig.Value;
Items = new ConcurrentDictionary<object, object>();
_lock = new SemaphoreSlim(1, 1);
}

public IPEndPoint PublicIp { get; }
Expand Down Expand Up @@ -113,6 +116,11 @@ public bool TryGetPlayer(int id, [MaybeNullWhen(false)] out ClientPlayer player)
return _players.TryGetValue(clientId, out var clientPlayer) ? clientPlayer : null;
}

public async Task<LockGuard> LockAsync()
{
return await LockGuard.LockAsync(_lock);
}

internal async ValueTask StartedAsync()
{
if (GameState == GameStates.Starting)
Expand Down

0 comments on commit a544425

Please sign in to comment.