diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/src/ClockQuantization/ClockQuantizer.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/src/ClockQuantization/ClockQuantizer.cs new file mode 100644 index 0000000000000..f027264713d47 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Caching.Memory/src/ClockQuantization/ClockQuantizer.cs @@ -0,0 +1,414 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + + +namespace Microsoft.Extensions.Internal.ClockQuantization +{ + /// + /// is a utility class that abstracts quantization of the reference clock. Essentially, the reference clock continuum is divided into discrete intervals with a maximum length of . + /// A so-called metronome is used to start a new every time when has passed. A may be cut short when an "out-of-cadance" advance operation is performed - such operation is triggered by + /// calls, as well as by and events. + /// + /// Under certain conditions, an advance operation may be incurred by calls. + internal class ClockQuantizer : IAsyncDisposable, IDisposable + { + private readonly TemporalContextDriver _driver; + private Interval? _currentInterval; + + + #region Fields & properties + + /// + /// The maximum of each , defined at construction. + /// + public readonly TimeSpan MaxIntervalTimeSpan; + + /// The current in the 's temporal context. + /// A starts in an inhibited state. Only after the first advance operation, will have a non- value. + public Interval? CurrentInterval { get => _currentInterval; } + + /// + /// Represents the clock-specific offset at which the next event is expected. + /// + /// + /// While uninitialized initially, will always have a value after the first advance operation. Basically, having + /// CurrentInterval.ClockOffset + TimeSpanToClockOffsetUnits(MaxIntervalTimeSpan) pre-calculated at the start of each metronomic interval, ammortizes the cost of this typical calculation during time-based decisions. + /// When an "out-of-cadance" (i.e. non-metronomic) advance operation is performed, (and its offset) will update, but not . + /// + public long? NextMetronomicClockOffset { get; private set; } + + + /// Returns the value of the reference clock. + /// Depending on the actual reference clock implementation, this may or may not incur an expensive system call. + public DateTimeOffset UtcNow { get => _driver.UtcNow; } + + /// Returns the value of the reference clock. + public long UtcNowClockOffset { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _driver.UtcNowClockOffset; } + + #endregion + + + #region Time representation conversions + + /// + /// Converts a to an offset in clock-specific units (ticks). + /// + /// The to convert + /// An offset in clock-specific units. + /// + public long DateTimeOffsetToClockOffset(DateTimeOffset offset) => _driver.DateTimeOffsetToClockOffset(offset); + + /// + /// Converts an offset in clock-specific units (ticks) to a . + /// + /// The clock-specific offset to convert + /// A in UTC. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DateTimeOffset ClockOffsetToUtcDateTimeOffset(long offset) => _driver.ClockOffsetToUtcDateTimeOffset(offset); + + /// + /// Converts a to a count of clock-specific offset units (ticks). + /// + /// The to convert + /// The amount of clock-specific offset units covering the . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long TimeSpanToClockOffsetUnits(TimeSpan timeSpan) => (long)(timeSpan.TotalMilliseconds * _driver.ClockOffsetUnitsPerMillisecond); + + /// + /// Converts an amount of clock-specific offset units (ticks) to a . + /// + /// The amount of units to convert + /// A covering the specified number of . + public TimeSpan ClockOffsetUnitsToTimeSpan(long units) => TimeSpan.FromMilliseconds((double)units / _driver.ClockOffsetUnitsPerMillisecond); + + #endregion + + + #region Basic quantizer & clock-offset-serial position operations + + /// + /// Establishes a new lower bound on the "last seen" exact within the + /// 's temporal context: the reference clock's . + /// + /// The newly started . + public Interval Advance() => Advance(metronomic: false); + + /// + /// If does not have an exact yet, it will be initialized with one. In every + /// situation where initialization is still required, this will incur a call into the reference clock's . + /// + /// Reference to an (on-stack) which may or may not have been initialized. + /// Indicates if the should perform an advance operation. This is advised in situations where non-exact + /// positions may still be acquired in the same and exact ordering (e.g. in a cache LRU eviction algorithm) might be adversely affected. + /// + /// An advance operation will incur an event. + /// Depending on the actual reference clock implementation, this may or may not incur an expensive system call. + /// + public void EnsureInitializedExactClockOffsetSerialPosition(ref LazyClockOffsetSerialPosition position, bool advance) + { + if (!position.IsExact) // test here as well to prevent unnecessary/unexpected Advance() if position was already initialzed + { + if (advance) + { + var preparation = PrepareAdvance(metronomic: false); + Interval.EnsureInitializedClockOffsetSerialPosition(preparation.Interval, ref position); + CommitAdvance(preparation); + } + else + { + Interval.EnsureInitializedClockOffsetSerialPosition(NewDisconnectedInterval(), ref position); + } + } + } + + /// + /// If does not have an yet, it will be initialized with one. + /// + /// Reference to an (on-stack) which may or may not have been initialized. + /// + /// If the had not performed a first advance operation yet, the result will be an exact position + /// (incurring a call into the reference clock's ). Otherwise, returns a position bound to + /// 's , but with an incremented . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void EnsureInitializedClockOffsetSerialPosition(ref LazyClockOffsetSerialPosition position) + { + if (!position.HasValue) + { + Interval.EnsureInitializedClockOffsetSerialPosition(_currentInterval ?? NewDisconnectedInterval(), ref position); + } + } + + #endregion + + + #region Events + + /// + /// Represents the ephemeral conditions at the time of an advance operation. + /// + public class NewIntervalEventArgs : EventArgs + { + /// + /// The within the temporal context when the new was started. + /// + public readonly DateTimeOffset DateTimeOffset; + + /// + /// if the new was created due to a metronome "tick", otherwise. + /// + public readonly bool IsMetronomic; + + /// + /// An optional value representing the gap between the start of the new interval and the expected end of + /// , if such gap is in fact detected. + /// + public readonly TimeSpan? GapToPriorIntervalExpectedEnd; + + internal NewIntervalEventArgs(DateTimeOffset offset, bool metronomic, TimeSpan? gap) + { + DateTimeOffset = offset; + IsMetronomic = metronomic; + GapToPriorIntervalExpectedEnd = gap; + } + } + + /// + /// This event is fired direclty after the start of a new within the 's temporal context. + /// + public event EventHandler? Advanced; + + /// + /// This event is fired almost immediately after each "tick" of the metronome. Every event is preceeded by an advance operation and a corresponding event, ensuring that a new reference + /// has been established in the 's temporal context at the time of firing. + /// + /// + /// Under typical operating conditions, the intermittent elapse of every interval is signaled by the 's built-in metronome. + /// Alternatively, metronome "ticks" may be generated by an external source that is firing events. + /// + /// + public event EventHandler? MetronomeTicked; + + /// + /// Raises the event. May be overriden in derived implementations. + /// + /// A instance + protected virtual void OnAdvanced(NewIntervalEventArgs e) => Advanced?.Invoke(this, e); + + /// + /// Raises the event. May be overriden in derived implementations. + /// + /// A instance + /// + /// events are always preceded with an event. The value of is the same in both consecutive events. + /// + protected virtual void OnMetronomeTicked(NewIntervalEventArgs e) => MetronomeTicked?.Invoke(this, e); + + #endregion + + + // Construction + + /// + /// Creates a new instance. + /// + /// The reference + /// The maximum of each + /// + /// If also implements , the will pick up on external + /// events. Also, if is non-, + /// the will pick up on external events, instead of relying on an internal metronome. + /// + public ClockQuantizer(ISystemClock clock, TimeSpan maxIntervalTimeSpan) + { + _driver = new TemporalContextDriver(clock, MaxIntervalTimeSpan = maxIntervalTimeSpan); + _driver.ClockAdjusted += Driver_ClockAdjusted; + _driver.MetronomeTicked += Driver_MetronomeTicked; + } + + + // Quiescing + + private readonly object _quiescingLockObject = new object(); + + /// + /// Puts the into a quiescent state, effectively freeing any owned unmanaged resources. While in a quiescent state, the will not raise any events, nor perform metronomic advance operations. + /// + /// + /// Any externally initiated advance operation will automatically take the back into normal operation. + /// + public void Quiesce() + { + // Ensure that quiesent state can be achieved without immediately being knocked out of it by a MetronomeTicked event that just happened to be in flight. + lock (_quiescingLockObject) + { + _driver.Quiesce(); + } + } + + /// + /// Takes the out of a quiescent state into normal operation. + /// + public void Unquiesce() => _driver.Unquiesce(); + + /// + /// Returns if the is in a quiescent state, otherwise. + /// + public bool IsQuiescent { get => _driver.IsQuiescent; } + + + // Advance primitives + + private struct AdvancePreparationInfo + { + public Interval Interval; + public ClockQuantizer.NewIntervalEventArgs EventArgs; + + public AdvancePreparationInfo(Interval interval, ClockQuantizer.NewIntervalEventArgs eventArgs) + { + Interval = interval; + EventArgs = eventArgs; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Interval NewDisconnectedInterval() => new Interval(UtcNowClockOffset); + + private Interval Advance(bool metronomic) + { + var preparation = PrepareAdvance(metronomic); + return CommitAdvance(preparation); + } + + private AdvancePreparationInfo PrepareAdvance(bool metronomic) + { + // Start metronome (if not imposed externally) on first Advance and consider first Advance as a metronomic event. + bool unquiescing = _driver.Unquiesce(); + if (unquiescing || _currentInterval is null) + { + metronomic = true; + } + + var previousInterval = _currentInterval; + var interval = NewDisconnectedInterval(); + TimeSpan? detectedGap = null; + if (previousInterval is not null) + { + // Ignore potential *internal* metronome gap due to tiny clock jitter + if (unquiescing || !metronomic || (metronomic && !_driver.HasInternalMetronome)) + { + var gap = ClockOffsetUnitsToTimeSpan(interval.ClockOffset - previousInterval.ClockOffset) - MaxIntervalTimeSpan; + if (gap > TimeSpan.Zero) + { + detectedGap = gap; + } + } + } + + var e = new NewIntervalEventArgs(_driver.ClockOffsetToUtcDateTimeOffset(interval.ClockOffset), metronomic, detectedGap); + + return new AdvancePreparationInfo(interval, e); + } + + private Interval CommitAdvance(AdvancePreparationInfo preparation) + { + _currentInterval = preparation.Interval.Seal(); + + var e = preparation.EventArgs; + if (e.IsMetronomic) + { + NextMetronomicClockOffset = _driver.DateTimeOffsetToClockOffset(e.DateTimeOffset + MaxIntervalTimeSpan); + } + + OnAdvanced(e); + + if (e.IsMetronomic) + { + OnMetronomeTicked(e); + } + + return preparation.Interval; + } + + private void Driver_MetronomeTicked(object? _, EventArgs __) + { + lock (_quiescingLockObject) + { + // Ensure that any in-flight MetronomeTicked event during quiescing transition does not knock us out of a quiesent state that was juuuuuuust established. + if (!IsQuiescent) + { + Advance(metronomic: true); + } + } + } + + private void Driver_ClockAdjusted(object? _, EventArgs __) + { + // Allow clock adjustemts to take us out of quiesent state (race possible; small chance of in-flight event, as underlying driver is quisced as well). + Advance(metronomic: false); + } + + + #region IAsyncDisposable/IDisposable + + private int _areEventHandlersDetached; + private void DetachEventHandlers() + { + if (Interlocked.CompareExchange(ref _areEventHandlersDetached, 1, 0) == 0) + { + _driver.ClockAdjusted -= Driver_ClockAdjusted; + _driver.MetronomeTicked -= Driver_MetronomeTicked; + } + } + + /// + public void Dispose() + { + // This method is re-entrant + DetachEventHandlers(); + + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + public async ValueTask DisposeAsync() + { + // This method is re-entrant + DetachEventHandlers(); + + await DisposeAsyncCore().ConfigureAwait(false); + + Dispose(disposing: false); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // This method is re-entrant and mutually co-existent with _driver.DisposeAsyncCore() + _driver.Dispose(); + } + } + + protected virtual async ValueTask DisposeAsyncCore() + { + // This method is re-entrant and mutually co-existent with _driver.Dispose() + await _driver.DisposeAsync().ConfigureAwait(false); + } + + #endregion + } +} + +#nullable restore + diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/src/ClockQuantization/TemporalContextDriver.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/src/ClockQuantization/TemporalContextDriver.cs new file mode 100644 index 0000000000000..349210c6dd0ec --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Caching.Memory/src/ClockQuantization/TemporalContextDriver.cs @@ -0,0 +1,280 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + + +namespace Microsoft.Extensions.Internal.ClockQuantization +{ + // Isolate some of the context/metronome madness from the core ClockQuantizer implementation + internal class TemporalContextDriver : ClockQuantization.ISystemClock, ISystemClockTemporalContext, IDisposable, IAsyncDisposable + { + private readonly ClockQuantization.ISystemClock _clock; + private readonly TimeSpan _metronomeIntervalTimeSpan; + private System.Threading.Timer? _metronome; + private EventArgs? _pendingClockAdjustedEventArgs; + + public TemporalContextDriver(ISystemClock clock, TimeSpan? metronomeIntervalTimeSpan = null) + { + AttachExternalTemporalContext(this, clock, out var externalMetronomeIntervalTimeSpan, out var clockIsManual); + + if (externalMetronomeIntervalTimeSpan.HasValue) + { + IsQuiescent = false; + _metronomeIntervalTimeSpan = externalMetronomeIntervalTimeSpan.Value; + } + else + { + if (!metronomeIntervalTimeSpan.HasValue) + { + // Need metronome interval timespan + DetachExternalTemporalContext(); + throw new ArgumentNullException(nameof(metronomeIntervalTimeSpan), "Must provide a valid metronome interval timespan or supply an external metronome."); + } + + _metronomeIntervalTimeSpan = metronomeIntervalTimeSpan.Value; + } + + _clock = clock; + ClockIsManual = clockIsManual; + + static void AttachExternalTemporalContext(TemporalContextDriver driver, ISystemClock clock, out TimeSpan? externalMetronomeIntervalTimeSpan, out bool clockIsManual) + { + externalMetronomeIntervalTimeSpan = null; + clockIsManual = default; + + if (clock is ISystemClockTemporalContext context) + { + context.ClockAdjusted += driver.Context_ClockAdjusted; + if (context.MetronomeIntervalTimeSpan.HasValue) + { + externalMetronomeIntervalTimeSpan = context.MetronomeIntervalTimeSpan.Value; + // Allow external "pulse" on metronome ticks + context.MetronomeTicked += driver.Context_MetronomeTicked; + } + + clockIsManual = context.ClockIsManual; + } + } + } + + private int _isExternalTemporalContextDetached; + private void DetachExternalTemporalContext() + { + if (Interlocked.CompareExchange(ref _isExternalTemporalContextDetached, 1, 0) == 0) + { + if (_clock is ISystemClockTemporalContext context) + { + context.ClockAdjusted -= Context_ClockAdjusted; + if (context.MetronomeIntervalTimeSpan.HasValue) + { + context.MetronomeTicked -= Context_MetronomeTicked; + } + } + } + } + + public bool ClockIsManual { get; private set; } + + public bool HasInternalMetronome + { + get => _clock is not ISystemClockTemporalContext context || !context.MetronomeIntervalTimeSpan.HasValue; + } + + private void EnsureInternalMetronome() + { + if (_metronome is null && HasInternalMetronome) + { + // Create a paused metronome timer + var metronome = new Timer(Metronome_TimerCallback, null, Timeout.InfiniteTimeSpan, _metronomeIntervalTimeSpan); + if (Interlocked.CompareExchange(ref _metronome, metronome, null) is null) + { + // Resume the newly created metronome timer + metronome.Change(_metronomeIntervalTimeSpan, _metronomeIntervalTimeSpan); + } + else + { + // Wooops... another thread outpaced us... + metronome.Dispose(); + } + } + } + + private void DisposeInternalMetronome() + { + if (Interlocked.Exchange(ref _metronome, null) is Timer metronome) + { + metronome.Dispose(); + } + } + + private async ValueTask DisposeInternalMetronomeAsync() + { +#if !(NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER) + await default(ValueTask).ConfigureAwait(continueOnCapturedContext: false); +#endif + + if (Interlocked.Exchange(ref _metronome, null) is Timer metronome) + { +#if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER + if (metronome is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync().ConfigureAwait(continueOnCapturedContext: false); + return; + } +#endif + metronome.Dispose(); + } + } + + public bool IsQuiescent { get; private set; } = true; + + public void Quiesce() + { + IsQuiescent = true; + + // Dispose of internal metronome, if applicable. + DisposeInternalMetronome(); + } + + private readonly object _unquiescingLockObject = new object(); + public bool Unquiesce() + { + bool unquiescing = IsQuiescent; + + if (unquiescing) + { + EventArgs? pendingClockAdjustedEventArgs = Interlocked.Exchange(ref _pendingClockAdjustedEventArgs, null); + + if (pendingClockAdjustedEventArgs is not null) + { + // Make sure that we briefly postpone any metronome event that may occur during the process of unquiescing + lock (_unquiescingLockObject) + { + IsQuiescent = false; // Set to false already to make sure that the ClockAdjusted event fires + OnClockAdjusted(pendingClockAdjustedEventArgs); + } + } + + IsQuiescent = false; + } + + // Ensure that we have an internal metronome, if applicable. + EnsureInternalMetronome(); + + return unquiescing; + } + + protected virtual void OnMetronomeTicked(EventArgs e) + { + if (!IsQuiescent) + { + // Make sure that we briefly postpone a metronome event that occurs during the process of unquiescing + lock (_unquiescingLockObject) + { + MetronomeTicked?.Invoke(this, e); + } + } + } + + protected virtual void OnClockAdjusted(EventArgs e) + { + if (!IsQuiescent) + { + ClockAdjusted?.Invoke(this, e); + } + else + { + // Retain the latest ClockAdjusted event until we unquiesce + Interlocked.Exchange(ref _pendingClockAdjustedEventArgs, e); + } + } + + private void Context_ClockAdjusted(object? _, EventArgs __) => OnClockAdjusted(EventArgs.Empty); + + private void Context_MetronomeTicked(object? _, EventArgs __) => OnMetronomeTicked(EventArgs.Empty); + + private void Metronome_TimerCallback(object? _) => OnMetronomeTicked(EventArgs.Empty); + + + #region ISystemClock + + /// + public DateTimeOffset UtcNow { get => _clock.UtcNow; } + /// + public long UtcNowClockOffset { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _clock.UtcNowClockOffset; } + /// + public long ClockOffsetUnitsPerMillisecond { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _clock.ClockOffsetUnitsPerMillisecond; } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DateTimeOffset ClockOffsetToUtcDateTimeOffset(long offset) => _clock.ClockOffsetToUtcDateTimeOffset(offset); + /// + public long DateTimeOffsetToClockOffset(DateTimeOffset offset) => _clock.DateTimeOffsetToClockOffset(offset); + + #endregion + + #region ISystemClockTemporalContext + + /// + public TimeSpan? MetronomeIntervalTimeSpan { get => _metronomeIntervalTimeSpan; } + /// + public event EventHandler? ClockAdjusted; + /// + public event EventHandler? MetronomeTicked; + + #endregion + + #region IAsyncDisposable/IDisposable + + /// + public void Dispose() + { + IsQuiescent = true; + + // This method is re-entrant + DetachExternalTemporalContext(); + + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + public async ValueTask DisposeAsync() + { + IsQuiescent = true; + + // This method is re-entrant + DetachExternalTemporalContext(); + + await DisposeAsyncCore().ConfigureAwait(false); + + Dispose(disposing: false); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // This method is re-entrant + DisposeInternalMetronome(); + } + } + + protected virtual async ValueTask DisposeAsyncCore() + { + // This method is re-entrant + await DisposeInternalMetronomeAsync().ConfigureAwait(false); + } + + #endregion + } +} + +#nullable restore