Skip to content

Commit

Permalink
Isolate handling of temporal context and metronome (#7)
Browse files Browse the repository at this point in the history
* Initial extraction of temporal context massaging and metronome abstraction.

* Missing EOL at EOF

* XMLDOC updates

* First stab at timer quiescing support

* Minor cleanup

* Renaming and initial refinement of internal resource management and (async) disposal

* Reinstate (async) disposal for ClockQuantizer

* Finalizing (async) disposal code for ClockQuantizer & TemporalContextDriver

* Simplification of (async) disposal logic - leverage re-entrancy

* Improve TemporalContextDriver.DisposeInternalMetronomeAsync()

* - Update ISystemClockTemporalContext metronome feature
- Small tweak to ClockQuantizer.PrepareAdvance() in light of quiescing

* Final tweaks for quiscing support; updated XMLDOCs

* Final tweaks to quiescing feature
  • Loading branch information
edevoogd authored May 4, 2021
1 parent be31ce9 commit 9158c88
Show file tree
Hide file tree
Showing 5 changed files with 420 additions and 69 deletions.
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

0 comments on commit 9158c88

Please sign in to comment.