Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 58 additions & 4 deletions src/ClockQuantization.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

142 changes: 83 additions & 59 deletions src/ClockQuantizer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace ClockQuantization
Expand All @@ -12,9 +13,8 @@ namespace ClockQuantization
/// <remarks>Under certain conditions, an advance operation may be incurred by <see cref="EnsureInitializedExactClockOffsetSerialPosition(ref LazyClockOffsetSerialPosition, bool)"/> calls.</remarks>
public class ClockQuantizer : IAsyncDisposable, IDisposable
{
private readonly ISystemClock _clock;
private readonly TemporalContextDriver _driver;
private Interval? _currentInterval;
private System.Threading.Timer? _metronome;


#region Fields & properties
Expand All @@ -41,10 +41,10 @@ public class ClockQuantizer : IAsyncDisposable, IDisposable

/// <value>Returns the <see cref="ISystemClock.UtcNow"/> value of the reference clock.</value>
/// <remarks>Depending on the actual reference clock implementation, this may or may not incur an expensive system call.</remarks>
public DateTimeOffset UtcNow { get => _clock.UtcNow; }
public DateTimeOffset UtcNow { get => _driver.UtcNow; }

/// <value>Returns the <see cref="ISystemClock.UtcNowClockOffset"/> value of the reference clock.</value>
public long UtcNowClockOffset { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _clock.UtcNowClockOffset; }
public long UtcNowClockOffset { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _driver.UtcNowClockOffset; }

#endregion

Expand All @@ -57,7 +57,7 @@ public class ClockQuantizer : IAsyncDisposable, IDisposable
/// <param name="offset">The <see cref="DateTimeOffset"/> to convert</param>
/// <returns>An offset in clock-specific units.</returns>
/// <seealso cref="ISystemClock.ClockOffsetUnitsPerMillisecond"/>
public long DateTimeOffsetToClockOffset(DateTimeOffset offset) => _clock.DateTimeOffsetToClockOffset(offset);
public long DateTimeOffsetToClockOffset(DateTimeOffset offset) => _driver.DateTimeOffsetToClockOffset(offset);

/// <summary>
/// Converts an offset in clock-specific units (ticks) to a <see cref="DateTimeOffset"/>.
Expand All @@ -66,22 +66,22 @@ public class ClockQuantizer : IAsyncDisposable, IDisposable
/// <returns>A <see cref="DateTimeOffset"/> in UTC.</returns>
/// <seealso cref="ISystemClock.ClockOffsetUnitsPerMillisecond"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DateTimeOffset ClockOffsetToUtcDateTimeOffset(long offset) => _clock.ClockOffsetToUtcDateTimeOffset(offset);
public DateTimeOffset ClockOffsetToUtcDateTimeOffset(long offset) => _driver.ClockOffsetToUtcDateTimeOffset(offset);

/// <summary>
/// Converts a <see cref="TimeSpan"/> to a count of clock-specific offset units (ticks).
/// </summary>
/// <param name="timeSpan">The <see cref="TimeSpan"/> to convert</param>
/// <returns>The amount of clock-specific offset units covering the <see cref="TimeSpan"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long TimeSpanToClockOffsetUnits(TimeSpan timeSpan) => (long)(timeSpan.TotalMilliseconds * _clock.ClockOffsetUnitsPerMillisecond);
public long TimeSpanToClockOffsetUnits(TimeSpan timeSpan) => (long)(timeSpan.TotalMilliseconds * _driver.ClockOffsetUnitsPerMillisecond);

/// <summary>
/// Converts an amount of clock-specific offset units (ticks) to a <see cref="TimeSpan"/>.
/// </summary>
/// <param name="units">The amount of units to convert</param>
/// <returns>A <see cref="TimeSpan"/> covering the specified number of <paramref name="units"/>.</returns>
public TimeSpan ClockOffsetUnitsToTimeSpan(long units) => TimeSpan.FromMilliseconds((double)units / _clock.ClockOffsetUnitsPerMillisecond);
public TimeSpan ClockOffsetUnitsToTimeSpan(long units) => TimeSpan.FromMilliseconds((double)units / _driver.ClockOffsetUnitsPerMillisecond);

#endregion

Expand Down Expand Up @@ -218,33 +218,49 @@ internal NewIntervalEventArgs(DateTimeOffset offset, bool metronomic, TimeSpan?
/// <param name="maxIntervalTimeSpan">The maximum <see cref="TimeSpan"/> of each <see cref="Interval"/></param>
/// <remarks>
/// If <paramref name="clock"/> also implements <see cref="ISystemClockTemporalContext"/>, the <see cref="ClockQuantizer"/> will pick up on external
/// <see cref="ISystemClockTemporalContext.ClockAdjusted"/> events. Also, if <see cref="ISystemClockTemporalContext.ProvidesMetronome"/> is <see langword="true"/>,
/// <see cref="ISystemClockTemporalContext.ClockAdjusted"/> events. Also, if <see cref="ISystemClockTemporalContext.MetronomeIntervalTimeSpan"/> is non-<see langword="null"/>,
/// the <see cref="ClockQuantizer"/> will pick up on external <see cref="ISystemClockTemporalContext.MetronomeTicked"/> events, instead of relying on an internal metronome.
/// </remarks>
public ClockQuantizer(ISystemClock clock, TimeSpan maxIntervalTimeSpan)
{
_clock = clock;
MaxIntervalTimeSpan = maxIntervalTimeSpan;
bool metronomic = true;
_driver = new TemporalContextDriver(clock, MaxIntervalTimeSpan = maxIntervalTimeSpan);
_driver.ClockAdjusted += Driver_ClockAdjusted;
_driver.MetronomeTicked += Driver_MetronomeTicked;
}

if (clock is ISystemClockTemporalContext context)
{
context.ClockAdjusted += Context_ClockAdjusted;
if (context.ProvidesMetronome)
{
// Allow external "pulse" on metronome ticks
context.MetronomeTicked += Context_MetronomeTicked;
metronomic = false;
}
}

if (metronomic)
// Quiescing

private readonly object _quiescingLockObject = new object();

/// <summary>
/// Puts the <see cref="ClockQuantizer"/> into a quiescent state, effectively freeing any <em>owned</em> unmanaged resources. While in a quiescent state, the <see cref="ClockQuantizer"/> will not raise any events, nor perform metronomic advance operations.
/// </summary>
/// <remarks>
/// Any externally initiated advance operation will automatically take the <see cref="ClockQuantizer"/> back into normal operation.
/// </remarks>
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)
{
// Create a suspended timer. Timer will be started at first call to Advance().
_metronome = new System.Threading.Timer(Metronome_TimerCallback, null, System.Threading.Timeout.InfiniteTimeSpan, maxIntervalTimeSpan);
_driver.Quiesce();
}
}

/// <summary>
/// Takes the <see cref="ClockQuantizer"/> out of a quiescent state into normal operation.
/// </summary>
public void Unquiesce() => _driver.Unquiesce();

/// <value>
/// Returns <see langword="true"/> if the <see cref="ClockQuantizer"/> is in a quiescent state, <see langword="false"/> otherwise.
/// </value>
public bool IsQuiescent { get => _driver.IsQuiescent; }


// Advance primitives

private struct AdvancePreparationInfo
{
public Interval Interval;
Expand All @@ -269,10 +285,10 @@ private Interval Advance(bool metronomic)
private AdvancePreparationInfo PrepareAdvance(bool metronomic)
{
// Start metronome (if not imposed externally) on first Advance and consider first Advance as a metronomic event.
if (_currentInterval is null && _metronome is not null)
bool unquiescing = _driver.Unquiesce();
if (unquiescing || _currentInterval is null)
{
metronomic = true;
_metronome.Change(MaxIntervalTimeSpan, MaxIntervalTimeSpan);
}

var previousInterval = _currentInterval;
Expand All @@ -281,7 +297,7 @@ private AdvancePreparationInfo PrepareAdvance(bool metronomic)
if (previousInterval is not null)
{
// Ignore potential *internal* metronome gap due to tiny clock jitter
if (!metronomic || _metronome is null)
if (unquiescing || !metronomic || (metronomic && !_driver.HasInternalMetronome))
{
var gap = ClockOffsetUnitsToTimeSpan(interval.ClockOffset - previousInterval.ClockOffset) - MaxIntervalTimeSpan;
if (gap > TimeSpan.Zero)
Expand All @@ -291,7 +307,7 @@ private AdvancePreparationInfo PrepareAdvance(bool metronomic)
}
}

var e = new NewIntervalEventArgs(_clock.ClockOffsetToUtcDateTimeOffset(interval.ClockOffset), metronomic, detectedGap);
var e = new NewIntervalEventArgs(_driver.ClockOffsetToUtcDateTimeOffset(interval.ClockOffset), metronomic, detectedGap);

return new AdvancePreparationInfo(interval, e);
}
Expand All @@ -303,7 +319,7 @@ private Interval CommitAdvance(AdvancePreparationInfo preparation)
var e = preparation.EventArgs;
if (e.IsMetronomic)
{
NextMetronomicClockOffset = _clock.DateTimeOffsetToClockOffset(e.DateTimeOffset + MaxIntervalTimeSpan);
NextMetronomicClockOffset = _driver.DateTimeOffsetToClockOffset(e.DateTimeOffset + MaxIntervalTimeSpan);
}

OnAdvanced(e);
Expand All @@ -316,24 +332,53 @@ private Interval CommitAdvance(AdvancePreparationInfo preparation)
return preparation.Interval;
}

private void Metronome_TimerCallback(object? _) => Context_MetronomeTicked(null, EventArgs.Empty);
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 Context_MetronomeTicked(object? _, EventArgs __) => 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);
}

private void Context_ClockAdjusted(object? _, EventArgs __) => 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;
}
}

/// <inheritdoc/>
public void Dispose()
{
// This method is re-entrant
DetachEventHandlers();

Dispose(disposing: true);
GC.SuppressFinalize(this);
}

/// <inheritdoc/>
public async ValueTask DisposeAsync()
{
// This method is re-entrant
DetachEventHandlers();

await DisposeAsyncCore();

Dispose(disposing: false);
Expand All @@ -345,39 +390,18 @@ protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_metronome?.Dispose();
// This method is re-entrant and mutually co-existent with _driver.DisposeAsyncCore()
_driver.Dispose();
}

_metronome = null;
}

/// <inheritdoc/>
protected virtual async ValueTask DisposeAsyncCore()
{
if (_metronome is null)
{
goto done;
}

#if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER
if (_metronome is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
goto finish;
}
#else
await default(ValueTask).ConfigureAwait(false);
#endif
_metronome!.Dispose();

#if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER
finish:
#endif
_metronome = null;
done:
;
// This method is re-entrant and mutually co-existent with _driver.Dispose()
await _driver.DisposeAsync().ConfigureAwait(false);
}

#endregion
#endregion
}
}
6 changes: 3 additions & 3 deletions src/TemporalContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public interface ISystemClock
long ClockOffsetUnitsPerMillisecond { get; }

/// <summary>
/// Converts <paramref name="offset"/> to a <see cref="DateTimeOffset"/> in UTC.
/// Converts clock-specific <paramref name="offset"/> to a <see cref="DateTimeOffset"/> in UTC.
/// </summary>
/// <param name="offset">The offset to convert</param>
/// <returns>The corresponding <see cref="DateTimeOffset"/></returns>
Expand All @@ -47,9 +47,9 @@ public interface ISystemClock
public interface ISystemClockTemporalContext
{
/// <value>
/// <see langword="true"/> if the temporal context provides a metronome feature - i.e., if it fires <see cref="MetronomeTicked"/> events.
/// A non-<see langword="null"/> value if the temporal context provides a metronome feature - i.e., if it fires <see cref="MetronomeTicked"/> events.
/// </value>
bool ProvidesMetronome { get; }
TimeSpan? MetronomeIntervalTimeSpan { get; }

/// <summary>
/// An event that can be raised to inform listeners that the <see cref="ISystemClock"/> was adjusted.
Expand Down
Loading