using System; using System.Diagnostics; using System.Threading; using static System.Threading.Timeout; namespace Npgsql.Util; /// /// A wrapper around to simplify reset management. /// /// /// Since there's no way to reset a once it was cancelled, /// we need to make sure that an existing cancellation token source hasn't been cancelled, /// every time we start it (see https://github.com/dotnet/runtime/issues/4694). /// class ResettableCancellationTokenSource : IDisposable { bool isDisposed; public TimeSpan Timeout { get; set; } volatile CancellationTokenSource _cts = new(); CancellationTokenRegistration _registration; /// /// Used, so we wouldn't concurently use the cts for the cancellation, while it's being disposed /// readonly object lockObject = new(); #if DEBUG bool _isRunning; #endif public ResettableCancellationTokenSource() => Timeout = InfiniteTimeSpan; public ResettableCancellationTokenSource(TimeSpan timeout) => Timeout = timeout; /// /// Set the timeout on the wrapped /// and make sure that it hasn't been cancelled yet /// /// /// An optional token to cancel the asynchronous operation. The default value is . /// /// The from the wrapped public CancellationToken Start(CancellationToken cancellationToken = default) { #if DEBUG Debug.Assert(!_isRunning); #endif lock (lockObject) { // if there was an attempt to cancel while the connector was breaking // we do nothing and return the default token // as we're going to fail while reading or writing anyway if (isDisposed) { #if DEBUG _isRunning = true; #endif return CancellationToken.None; } _cts.CancelAfter(Timeout); if (_cts.IsCancellationRequested) { _cts.Dispose(); _cts = new CancellationTokenSource(Timeout); } } if (cancellationToken.CanBeCanceled) _registration = cancellationToken.Register(cts => ((CancellationTokenSource)cts!).Cancel(), _cts); #if DEBUG _isRunning = true; #endif return _cts.Token; } /// /// Restart the timeout on the wrapped without reinitializing it, /// even if is already set to /// public void RestartTimeoutWithoutReset() { lock (lockObject) { // if there was an attempt to cancel while the connector was breaking // we do nothing and return the default token // as we're going to fail while reading or writing anyway if (!isDisposed) _cts.CancelAfter(Timeout); } } /// /// Reset the wrapper to contain a unstarted and uncancelled /// in order make sure the next call to will not invalidate /// the cancellation token. /// /// /// An optional token to cancel the asynchronous operation. The default value is . /// /// The from the wrapped public CancellationToken Reset(CancellationToken cancellationToken = default) { _registration.Dispose(); lock (lockObject) { // if there was an attempt to cancel while the connector was breaking // we do nothing and return // as we're going to fail while reading or writing anyway if (isDisposed) { #if DEBUG _isRunning = false; #endif return CancellationToken.None; } _cts.CancelAfter(InfiniteTimeSpan); if (_cts.IsCancellationRequested) { _cts.Dispose(); _cts = new CancellationTokenSource(); } } if (cancellationToken.CanBeCanceled) _registration = cancellationToken.Register(cts => ((CancellationTokenSource)cts!).Cancel(), _cts); #if DEBUG _isRunning = false; #endif return _cts.Token; } /// /// Reset the wrapper to contain a unstarted and uncancelled /// in order make sure the next call to will not invalidate /// the cancellation token. /// public void ResetCts() { if (_cts.IsCancellationRequested) { _cts.Dispose(); _cts = new CancellationTokenSource(); } } /// /// Set the timeout on the wrapped /// to /// /// /// can still arrive at a state /// where it's value is if the /// passed to gets a cancellation request. /// If this is the case it will be resolved upon the next call to /// or . Calling multiple times or without calling /// first will do no any harm (besides eating a tiny amount of CPU cycles). /// public void Stop() { _registration.Dispose(); lock (lockObject) { // if there was an attempt to cancel while the connector was breaking // we do nothing if (!isDisposed) _cts.CancelAfter(InfiniteTimeSpan); } #if DEBUG _isRunning = false; #endif } /// /// Cancel the wrapped /// public void Cancel() { lock (lockObject) { // if there was an attempt to cancel while the connector was breaking // we do nothing if (isDisposed) return; _cts.Cancel(); } } /// /// Cancel the wrapped after delay /// public void CancelAfter(int delay) { lock (lockObject) { // if there was an attempt to cancel while the connector was breaking // we do nothing if (isDisposed) return; _cts.CancelAfter(delay); } } /// /// The from the wrapped /// . /// /// /// The token is only valid after calling /// and before calling the next time. /// Otherwise you may end up with a token that has already been /// cancelled or belongs to a cancellation token source that has /// been disposed. /// public CancellationToken Token => _cts.Token; public bool IsCancellationRequested => _cts.IsCancellationRequested; public void Dispose() { Debug.Assert(!isDisposed); lock (lockObject) { _registration.Dispose(); _cts.Dispose(); isDisposed = true; } } }