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;
}
}
}