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
30 changes: 27 additions & 3 deletions src/Wolfgang.Etl.Abstractions/EtlPipeline/EtlRunState.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Diagnostics;


namespace Wolfgang.Etl.Abstractions;
Expand All @@ -12,7 +11,8 @@ namespace Wolfgang.Etl.Abstractions;
/// </summary>
internal sealed class EtlRunState
{
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private readonly ITimeSource _timeSource;
private readonly long _startTimestamp;

public long ExtractedItemCount;

Expand All @@ -23,9 +23,33 @@ internal sealed class EtlRunState
public Func<int>? ErrorCountReader;


public EtlRunState()
: this(SystemTimeSource.Instance)
{
}


// Test seam (#338): inject a fake time source so the elapsed metric is deterministic.
internal EtlRunState(ITimeSource timeSource)
{
_timeSource = timeSource;
_startTimestamp = timeSource.GetTimestamp();
}


private TimeSpan Elapsed
{
get
{
var ticks = _timeSource.GetTimestamp() - _startTimestamp;
return TimeSpan.FromSeconds(ticks / (double)_timeSource.TimestampFrequency);
}
}


public EtlPipelineProgress Snapshot()
{
return new EtlPipelineProgress(ExtractedItemCount, LoadedItemCount, _stopwatch.Elapsed)
return new EtlPipelineProgress(ExtractedItemCount, LoadedItemCount, Elapsed)
{
ErrorItemCount = ErrorCountReader?.Invoke() ?? 0,
};
Expand Down
19 changes: 15 additions & 4 deletions src/Wolfgang.Etl.Abstractions/ExtractorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ public abstract class ExtractorBase<TSource, TProgress>



// Test seam (#338): when set, StartedAt/Elapsed derive from this time source instead of the
// system clock, so the timing-derived Report metrics can be driven deterministically. Left null
// in production (real clock). Internal + InternalsVisibleTo, mirroring the IProgressTimer
// injection pattern, so Test-Kit doubles can advance a fake clock.
internal ITimeSource? TimeSource;




/// <summary>
/// The UTC time at which the first item was processed (extracted or skipped), or
/// <c>null</c> if extraction has not produced any items yet. Captured automatically
Expand Down Expand Up @@ -61,8 +70,9 @@ protected TimeSpan Elapsed
return TimeSpan.Zero;
}

var ticks = Stopwatch.GetTimestamp() - start;
return TimeSpan.FromSeconds(ticks / (double)Stopwatch.Frequency);
var source = TimeSource ?? SystemTimeSource.Instance;
var ticks = source.GetTimestamp() - start;
return TimeSpan.FromSeconds(ticks / (double)source.TimestampFrequency);
}
}

Expand Down Expand Up @@ -503,8 +513,9 @@ private void EnsureStarted()
return;
}

var now = DateTimeOffset.UtcNow;
var timestamp = Stopwatch.GetTimestamp();
var source = TimeSource ?? SystemTimeSource.Instance;
var now = source.UtcNow;
var timestamp = source.GetTimestamp();
if (Interlocked.CompareExchange(ref _startTimestamp, timestamp, 0) == 0)
{
_startedAtUtc = now;
Expand Down
22 changes: 22 additions & 0 deletions src/Wolfgang.Etl.Abstractions/ITimeSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;

namespace Wolfgang.Etl.Abstractions;

/// <summary>
/// Internal seam over the wall-clock and monotonic timer the base classes read when they compute
/// their <see cref="Report"/> timing metrics (<c>StartedAt</c>, <c>Elapsed</c>, and the throughput
/// values derived from them). The default is the system clock; a fake can be injected in tests — via
/// <c>InternalsVisibleTo</c> — so those metrics become deterministic. Deliberately internal: no
/// public API surface.
/// </summary>
internal interface ITimeSource
{
/// <summary>The current UTC wall-clock time (used to capture <c>StartedAt</c>).</summary>
DateTimeOffset UtcNow { get; }

/// <summary>A monotonic timestamp tick count (used to measure <c>Elapsed</c>).</summary>
long GetTimestamp();

/// <summary>The number of <see cref="GetTimestamp"/> ticks per second.</summary>
long TimestampFrequency { get; }
}
19 changes: 15 additions & 4 deletions src/Wolfgang.Etl.Abstractions/LoaderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ public abstract class LoaderBase<TDestination, TProgress>



// Test seam (#338): when set, StartedAt/Elapsed derive from this time source instead of the
// system clock, so the timing-derived Report metrics can be driven deterministically. Left null
// in production (real clock). Internal + InternalsVisibleTo, mirroring the IProgressTimer
// injection pattern, so Test-Kit doubles can advance a fake clock.
internal ITimeSource? TimeSource;




/// <summary>
/// The UTC time at which the first item was processed (loaded or skipped), or
/// <c>null</c> if loading has not produced any items yet. Captured automatically
Expand Down Expand Up @@ -60,8 +69,9 @@ protected TimeSpan Elapsed
return TimeSpan.Zero;
}

var ticks = Stopwatch.GetTimestamp() - start;
return TimeSpan.FromSeconds(ticks / (double)Stopwatch.Frequency);
var source = TimeSource ?? SystemTimeSource.Instance;
var ticks = source.GetTimestamp() - start;
return TimeSpan.FromSeconds(ticks / (double)source.TimestampFrequency);
}
}

Expand Down Expand Up @@ -501,8 +511,9 @@ private void EnsureStarted()
return;
}

var now = DateTimeOffset.UtcNow;
var timestamp = Stopwatch.GetTimestamp();
var source = TimeSource ?? SystemTimeSource.Instance;
var now = source.UtcNow;
var timestamp = source.GetTimestamp();
if (Interlocked.CompareExchange(ref _startTimestamp, timestamp, 0) == 0)
{
_startedAtUtc = now;
Expand Down
28 changes: 28 additions & 0 deletions src/Wolfgang.Etl.Abstractions/SystemTimeSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Diagnostics;

namespace Wolfgang.Etl.Abstractions;

/// <summary>
/// The default <see cref="ITimeSource"/>: reads the real system clock via
/// <see cref="DateTimeOffset.UtcNow"/> and <see cref="Stopwatch"/>. A shared stateless singleton —
/// the base classes use it whenever no test time source has been injected.
/// </summary>
internal sealed class SystemTimeSource : ITimeSource
{
internal static readonly SystemTimeSource Instance = new();


private SystemTimeSource()
{
}


public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;


public long GetTimestamp() => Stopwatch.GetTimestamp();


public long TimestampFrequency => Stopwatch.Frequency;
}
19 changes: 15 additions & 4 deletions src/Wolfgang.Etl.Abstractions/TransformerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ public abstract class TransformerBase<TSource, TDestination, TProgress>



// Test seam (#338): when set, StartedAt/Elapsed derive from this time source instead of the
// system clock, so the timing-derived Report metrics can be driven deterministically. Left null
// in production (real clock). Internal + InternalsVisibleTo, mirroring the IProgressTimer
// injection pattern, so Test-Kit doubles can advance a fake clock.
internal ITimeSource? TimeSource;




/// <summary>
/// The UTC time at which the first item was processed (transformed or skipped), or
/// <c>null</c> if transformation has not produced any items yet. Captured automatically
Expand Down Expand Up @@ -63,8 +72,9 @@ protected TimeSpan Elapsed
return TimeSpan.Zero;
}

var ticks = Stopwatch.GetTimestamp() - start;
return TimeSpan.FromSeconds(ticks / (double)Stopwatch.Frequency);
var source = TimeSource ?? SystemTimeSource.Instance;
var ticks = source.GetTimestamp() - start;
return TimeSpan.FromSeconds(ticks / (double)source.TimestampFrequency);
}
}

Expand Down Expand Up @@ -511,8 +521,9 @@ private void EnsureStarted()
return;
}

var now = DateTimeOffset.UtcNow;
var timestamp = Stopwatch.GetTimestamp();
var source = TimeSource ?? SystemTimeSource.Instance;
var now = source.UtcNow;
var timestamp = source.GetTimestamp();
if (Interlocked.CompareExchange(ref _startTimestamp, timestamp, 0) == 0)
{
_startedAtUtc = now;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@

<ItemGroup>
<InternalsVisibleTo Include="Wolfgang.Etl.Abstractions.Tests.Unit" />
<!-- #338: lets ETL-Test-Kit doubles inject a fake ITimeSource for deterministic Report timing. -->
<InternalsVisibleTo Include="Wolfgang.Etl.TestKit" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading