// // DefaultContract.cs // // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Remora.Results; namespace NosSmooth.Core.Contracts; /// /// A generic implementation of contract /// supporting any data. /// /// The data type. /// The states. /// The errors that may be returned. public class DefaultContract : IContract where TState : struct, IComparable where TError : struct where TData : notnull { /// /// An action to execute when a state is reached. /// /// 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); /// /// An action to execute when a state that may fill the data is reached. /// Returns the data to fill. /// /// 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> 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; private readonly TState _fillAtState; private readonly FillDataAsync _fillData; private TError? _error; private Result? _resultError; private TState? _waitingFor; private bool _unregisterAtWaitingFor; private CancellationTokenSource? _waitCancellationSource; /// /// Initializes a new instance of the class. /// /// The contractor. /// The default state. /// The state to fill data at. /// The function to fill the data. /// The actions to execute at each state. /// The timeouts. public DefaultContract ( Contractor contractor, TState defaultState, TState fillAtState, FillDataAsync fillData, IDictionary actions, IDictionary timeouts ) { _semaphore = new SemaphoreSlim(1, 1); _timeouts = timeouts; _defaultState = defaultState; _contractor = contractor; CurrentState = defaultState; _actions = actions; _fillData = fillData; _fillAtState = fillAtState; } /// public TState CurrentState { get; private set; } /// public TData? Data { get; private set; } /// public bool IsRegistered { get; private set; } /// public void Register() { if (!IsRegistered) { _contractor.Register(this); IsRegistered = true; } } /// public void Unregister() { if (IsRegistered) { _contractor.Unregister(this); IsRegistered = false; } } /// public async Task> Update(TAny data, CancellationToken ct = default) where TAny : notnull { if (!_actions.ContainsKey(CurrentState)) { throw new Exception(); // ? } var result = await _actions[CurrentState](data, ct); if (!result.IsDefined(out var resultData)) { _resultError = Result.FromError(result); _waitCancellationSource?.Cancel(); return Result.FromError(result); } if (resultData.Error is not null) { _error = resultData.Error; _waitCancellationSource?.Cancel(); return ContractUpdateResponse.Interested; } if (resultData.NextState is null) { return ContractUpdateResponse.NotInterested; } await SetCurrentState(resultData.NextState.Value, ct); return await SetupNewState(data!, ct); } /// public async Task OnlyExecuteAsync(CancellationToken ct = default) { if (_actions.ContainsKey(_defaultState)) { var result = await _actions[_defaultState](default, ct); if (!result.IsSuccess) { 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(); } /// public async Task> WaitForAsync (TState state, bool unregisterAfter = true, CancellationToken ct = default) { if (_fillAtState.CompareTo(state) > 0) { throw new InvalidOperationException ( $"The requested state {state} does not guarantee data filled. The state that fills data is {_defaultState}" ); } _waitingFor = state; _unregisterAtWaitingFor = unregisterAfter; _waitCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(ct); Register(); if (CurrentState.CompareTo(_defaultState) == 0) { var result = await OnlyExecuteAsync(ct); if (!result.IsSuccess) { Unregister(); return Result.FromError(result); } } try { await Task.Delay(-1, _waitCancellationSource.Token); } catch { // ignored } finally { if (unregisterAfter) { Unregister(); } } if (ct.IsCancellationRequested) { throw new TaskCanceledException(); } if (_resultError is not null) { return Result.FromError(_resultError.Value); } if (_error is not null) { return new ContractError(_error.Value); } if (Data is null) { throw new Exception("Data was null, but shouldn't have. There is an error in DefaultContract."); } 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(); } }