From 393a5e05a779f42076bf68c9b60fdce76bc03452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Thu, 12 Jan 2023 20:51:52 +0100 Subject: [PATCH] feat(game): add timeouts to contracts --- Core/NosSmooth.Core/Contracts/Contractor.cs | 1 + .../Contracts/DefaultContract.cs | 148 +++++++++++++----- Core/NosSmooth.Core/Contracts/IContract.cs | 3 +- 3 files changed, 109 insertions(+), 43 deletions(-) diff --git a/Core/NosSmooth.Core/Contracts/Contractor.cs b/Core/NosSmooth.Core/Contracts/Contractor.cs index 3075029..028bcff 100644 --- a/Core/NosSmooth.Core/Contracts/Contractor.cs +++ b/Core/NosSmooth.Core/Contracts/Contractor.cs @@ -87,6 +87,7 @@ public class Contractor : IEnumerable /// The type of the data. /// The result that may or may not have succeeded. public async Task Update(TData data, CancellationToken ct = default) + where TData : notnull { var errors = new List(); var toRemove = new List(); diff --git a/Core/NosSmooth.Core/Contracts/DefaultContract.cs b/Core/NosSmooth.Core/Contracts/DefaultContract.cs index a73dead..cc360a5 100644 --- a/Core/NosSmooth.Core/Contracts/DefaultContract.cs +++ b/Core/NosSmooth.Core/Contracts/DefaultContract.cs @@ -21,6 +21,7 @@ namespace NosSmooth.Core.Contracts; /// The errors that may be returned. public class DefaultContract : IContract where TState : struct, IComparable + where TError : struct where TData : notnull { /// @@ -29,7 +30,8 @@ public class DefaultContract : IContract /// The data that led to the state. /// The cancellation token used for cancelling the operation. /// The result that may or may not have succeeded. - public delegate Task> StateActionAsync(object? data, CancellationToken ct); + public delegate Task> StateActionAsync + (object? data, CancellationToken ct); /// /// An action to execute when a state that may fill the data is reached. @@ -40,6 +42,9 @@ public class DefaultContract : IContract /// The result that may or may not have succeeded. public delegate Task> FillDataAsync(object data, CancellationToken ct); + private readonly SemaphoreSlim _semaphore; + + private readonly IDictionary _timeouts; private readonly IDictionary _actions; private readonly Contractor _contractor; private readonly TState _defaultState; @@ -47,8 +52,6 @@ public class DefaultContract : IContract private readonly TState _fillAtState; private readonly FillDataAsync _fillData; - private readonly TimeSpan? _timeout; - private TError? _error; private Result? _resultError; @@ -64,7 +67,7 @@ public class DefaultContract : IContract /// The state to fill data at. /// The function to fill the data. /// The actions to execute at each state. - /// The timeout. + /// The timeouts. public DefaultContract ( Contractor contractor, @@ -72,11 +75,11 @@ public class DefaultContract : IContract TState fillAtState, FillDataAsync fillData, IDictionary actions, - TimeSpan? timeout + IDictionary timeouts ) { - _timeout = timeout; - + _semaphore = new SemaphoreSlim(1, 1); + _timeouts = timeouts; _defaultState = defaultState; _contractor = contractor; CurrentState = defaultState; @@ -117,6 +120,7 @@ public class DefaultContract : IContract /// public async Task> Update(TAny data, CancellationToken ct = default) + where TAny : notnull { if (!_actions.ContainsKey(CurrentState)) { @@ -135,6 +139,7 @@ public class DefaultContract : IContract { _error = resultData.Error; _waitCancellationSource?.Cancel(); + return ContractUpdateResponse.Interested; } if (resultData.NextState is null) @@ -142,36 +147,9 @@ public class DefaultContract : IContract return ContractUpdateResponse.NotInterested; } - CurrentState = resultData.NextState.Value; - if (_fillAtState.CompareTo(CurrentState) == 0) - { - var filledResult = await _fillData(data!, ct); - - if (!filledResult.IsDefined(out var filled)) - { - _resultError = Result.FromError(filledResult); - _waitCancellationSource?.Cancel(); - return Result.FromError(filledResult); - } - - Data = filled; - } - - if (_waitingFor is not null && _waitingFor.Value.CompareTo(CurrentState) == 0) - { - IsRegistered = false; // avoid deadlock. The cancellation will trigger unregister, - - // but we are inside of the lock now. - _waitCancellationSource?.Cancel(); - - if (_unregisterAtWaitingFor) - { - return ContractUpdateResponse.InterestedAndUnregister; - } - } + await SetCurrentState(resultData.NextState.Value, ct); - // TODO: timeouts! - return ContractUpdateResponse.Interested; + return await SetupNewState(data!, ct); } /// @@ -184,6 +162,25 @@ public class DefaultContract : IContract { return Result.FromError(result); } + + var (error, state) = result.Entity; + + if (error is not null) + { + _error = error; + _waitCancellationSource?.Cancel(); + } + + if (state is not null) + { + await SetCurrentState(state.Value, ct); + var newStateResult = await SetupNewState(null, ct); + + if (!newStateResult.IsSuccess) + { + return Result.FromError(newStateResult); + } + } } return Result.FromSuccess(); @@ -205,11 +202,6 @@ public class DefaultContract : IContract _unregisterAtWaitingFor = unregisterAfter; _waitCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(ct); - if (_timeout is not null) - { - _waitCancellationSource.CancelAfter(_timeout.Value); - } - Register(); if (CurrentState.CompareTo(_defaultState) == 0) @@ -250,7 +242,7 @@ public class DefaultContract : IContract if (_error is not null) { - return new ContractError(_error); + return new ContractError(_error.Value); } if (Data is null) @@ -260,4 +252,76 @@ public class DefaultContract : IContract return Data; } + + private async Task> SetupNewState(TAny data, CancellationToken ct) + { + if (_fillAtState.CompareTo(CurrentState) == 0) + { + if (data is not null) + { + var filledResult = await _fillData(data, ct); + + if (!filledResult.IsDefined(out var filled)) + { + _resultError = Result.FromError(filledResult); + _waitCancellationSource?.Cancel(); + return Result.FromError(filledResult); + } + + Data = filled; + } + else + { + throw new InvalidOperationException + ( + $"Got to a state {CurrentState} without data, but the state should fill data. That's not possible." + ); + } + } + if (_waitingFor is not null && _waitingFor.Value.CompareTo(CurrentState) == 0) + { + IsRegistered = false; // avoid deadlock. The cancellation will trigger unregister, + + // but we are inside of the lock now. + _waitCancellationSource?.Cancel(); + + if (_unregisterAtWaitingFor) + { + return ContractUpdateResponse.InterestedAndUnregister; + } + } + + SetupTimeout(); + return ContractUpdateResponse.Interested; + } + + private void SetupTimeout() + { + if (_timeouts.ContainsKey(CurrentState)) + { + var currentState = CurrentState; + var (timeout, state) = _timeouts[CurrentState]; + + Task.Run + ( + async () => + { + await Task.Delay(timeout); + + if (CurrentState.CompareTo(currentState) == 0) + { + await SetCurrentState(state); + await SetupNewState(null!, default); + } + } + ); + } + } + + private async Task SetCurrentState(TState state, CancellationToken ct = default) + { + await _semaphore.WaitAsync(ct); + CurrentState = state; + _semaphore.Release(); + } } \ No newline at end of file diff --git a/Core/NosSmooth.Core/Contracts/IContract.cs b/Core/NosSmooth.Core/Contracts/IContract.cs index d755f23..31b0968 100644 --- a/Core/NosSmooth.Core/Contracts/IContract.cs +++ b/Core/NosSmooth.Core/Contracts/IContract.cs @@ -55,7 +55,8 @@ public interface IContract /// The type of the data. /// The cancellation token used for cancelling the operation. /// The result that may or may not have succeeded. - public Task> Update(TAny data, CancellationToken ct = default); + public Task> Update(TAny data, CancellationToken ct = default) + where TAny : notnull; /// /// Executes the contract without registering it, -- 2.49.0