~ruther/NosSmooth

393a5e05a779f42076bf68c9b60fdce76bc03452 — František Boháček 2 years ago 72b5ea6
feat(game): add timeouts to contracts
M Core/NosSmooth.Core/Contracts/Contractor.cs => Core/NosSmooth.Core/Contracts/Contractor.cs +1 -0
@@ 87,6 87,7 @@ public class Contractor : IEnumerable<IContract>
    /// <typeparam name="TData">The type of the data.</typeparam>
    /// <returns>The result that may or may not have succeeded.</returns>
    public async Task<Result> Update<TData>(TData data, CancellationToken ct = default)
        where TData : notnull
    {
        var errors = new List<IResult>();
        var toRemove = new List<ContractInfo>();

M Core/NosSmooth.Core/Contracts/DefaultContract.cs => Core/NosSmooth.Core/Contracts/DefaultContract.cs +106 -42
@@ 21,6 21,7 @@ namespace NosSmooth.Core.Contracts;
/// <typeparam name="TError">The errors that may be returned.</typeparam>
public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
    where TState : struct, IComparable
    where TError : struct
    where TData : notnull
{
    /// <summary>


@@ 29,7 30,8 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
    /// <param name="data">The data that led to the state.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>The result that may or may not have succeeded.</returns>
    public delegate Task<Result<(TError? Error, TState? NextState)>> StateActionAsync(object? data, CancellationToken ct);
    public delegate Task<Result<(TError? Error, TState? NextState)>> StateActionAsync
        (object? data, CancellationToken ct);

    /// <summary>
    /// An action to execute when a state that may fill the data is reached.


@@ 40,6 42,9 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
    /// <returns>The result that may or may not have succeeded.</returns>
    public delegate Task<Result<TData>> FillDataAsync(object data, CancellationToken ct);

    private readonly SemaphoreSlim _semaphore;

    private readonly IDictionary<TState, (TimeSpan, TState)> _timeouts;
    private readonly IDictionary<TState, StateActionAsync> _actions;
    private readonly Contractor _contractor;
    private readonly TState _defaultState;


@@ 47,8 52,6 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
    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<TData, TState, TError> : IContract<TData, TState>
    /// <param name="fillAtState">The state to fill data at.</param>
    /// <param name="fillData">The function to fill the data.</param>
    /// <param name="actions">The actions to execute at each state.</param>
    /// <param name="timeout">The timeout.</param>
    /// <param name="timeouts">The timeouts.</param>
    public DefaultContract
    (
        Contractor contractor,


@@ 72,11 75,11 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
        TState fillAtState,
        FillDataAsync fillData,
        IDictionary<TState, StateActionAsync> actions,
        TimeSpan? timeout
        IDictionary<TState, (TimeSpan Timeout, TState NextState)> timeouts
    )
    {
        _timeout = timeout;

        _semaphore = new SemaphoreSlim(1, 1);
        _timeouts = timeouts;
        _defaultState = defaultState;
        _contractor = contractor;
        CurrentState = defaultState;


@@ 117,6 120,7 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>

    /// <inheritdoc />
    public async Task<Result<ContractUpdateResponse>> Update<TAny>(TAny data, CancellationToken ct = default)
        where TAny : notnull
    {
        if (!_actions.ContainsKey(CurrentState))
        {


@@ 135,6 139,7 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
        {
            _error = resultData.Error;
            _waitCancellationSource?.Cancel();
            return ContractUpdateResponse.Interested;
        }

        if (resultData.NextState is null)


@@ 142,36 147,9 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
            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<ContractUpdateResponse>.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);
    }

    /// <inheritdoc />


@@ 184,6 162,25 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
            {
                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<int?>(null, ct);

                if (!newStateResult.IsSuccess)
                {
                    return Result.FromError(newStateResult);
                }
            }
        }

        return Result.FromSuccess();


@@ 205,11 202,6 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
        _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<TData, TState, TError> : IContract<TData, TState>

        if (_error is not null)
        {
            return new ContractError<TError>(_error);
            return new ContractError<TError>(_error.Value);
        }

        if (Data is null)


@@ 260,4 252,76 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>

        return Data;
    }

    private async Task<Result<ContractUpdateResponse>> SetupNewState<TAny>(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<ContractUpdateResponse>.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<int?>(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

M Core/NosSmooth.Core/Contracts/IContract.cs => Core/NosSmooth.Core/Contracts/IContract.cs +2 -1
@@ 55,7 55,8 @@ public interface IContract
    /// <typeparam name="TAny">The type of the data.</typeparam>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>The result that may or may not have succeeded.</returns>
    public Task<Result<ContractUpdateResponse>> Update<TAny>(TAny data, CancellationToken ct = default);
    public Task<Result<ContractUpdateResponse>> Update<TAny>(TAny data, CancellationToken ct = default)
        where TAny : notnull;

    /// <summary>
    /// Executes the contract without registering it,

Do not follow this link