save progress
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Findings.Ledger.Domain;
|
||||
using StellaOps.Findings.Ledger.Hashing;
|
||||
|
||||
namespace LedgerReplayHarness;
|
||||
|
||||
@@ -11,11 +8,15 @@ public sealed class HarnessRunner
|
||||
{
|
||||
private readonly ILedgerClient _client;
|
||||
private readonly int _maxParallel;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly bool _allowParallel;
|
||||
|
||||
public HarnessRunner(ILedgerClient client, int maxParallel = 4)
|
||||
public HarnessRunner(ILedgerClient client, int maxParallel = 4, TimeProvider? timeProvider = null, bool allowParallel = false)
|
||||
{
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
_maxParallel = maxParallel <= 0 ? 1 : maxParallel;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_allowParallel = allowParallel;
|
||||
}
|
||||
|
||||
public async Task<int> RunAsync(IEnumerable<string> fixtures, string tenant, string reportPath, CancellationToken cancellationToken)
|
||||
@@ -34,89 +35,80 @@ public sealed class HarnessRunner
|
||||
var hashesValid = true;
|
||||
DateTimeOffset? earliest = null;
|
||||
DateTimeOffset? latest = null;
|
||||
var leafHashes = new List<string>();
|
||||
var leafEntries = new List<(Guid ChainId, long Sequence, string LeafHash)>();
|
||||
string? expectedMerkleRoot = null;
|
||||
var latencies = new ConcurrentBag<double>();
|
||||
var swTotal = Stopwatch.StartNew();
|
||||
|
||||
var throttler = new TaskThrottler(_maxParallel);
|
||||
TaskThrottler? throttler = _allowParallel && _maxParallel > 1
|
||||
? new TaskThrottler(_maxParallel)
|
||||
: null;
|
||||
|
||||
foreach (var fixture in fixtures)
|
||||
var orderedFixtures = fixtures.OrderBy(f => f, StringComparer.Ordinal).ToArray();
|
||||
foreach (var fixture in orderedFixtures)
|
||||
{
|
||||
await foreach (var line in ReadLinesAsync(fixture, cancellationToken))
|
||||
var fixtureInfo = new FileInfo(fixture);
|
||||
await foreach (var entry in HarnessFixtureReader.ReadEntriesAsync(fixtureInfo, tenant, _timeProvider, cancellationToken))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
var node = JsonNode.Parse(line)?.AsObject();
|
||||
if (node is null) continue;
|
||||
|
||||
eventCount++;
|
||||
var recordedAt = node["recorded_at"]?.GetValue<DateTimeOffset>() ?? DateTimeOffset.UtcNow;
|
||||
earliest = earliest is null ? recordedAt : DateTimeOffset.Compare(recordedAt, earliest.Value) < 0 ? recordedAt : earliest;
|
||||
latest = latest is null
|
||||
? recordedAt
|
||||
: DateTimeOffset.Compare(recordedAt, latest.Value) > 0 ? recordedAt : latest;
|
||||
|
||||
if (node["canonical_envelope"] is JsonObject envelope && node["sequence_no"] is not null)
|
||||
var record = entry.Record;
|
||||
if (!string.IsNullOrEmpty(entry.ExpectedEventHash) &&
|
||||
!string.Equals(entry.ExpectedEventHash, record.EventHash, StringComparison.Ordinal))
|
||||
{
|
||||
var seq = node["sequence_no"]!.GetValue<long>();
|
||||
var computed = LedgerHashing.ComputeHashes(envelope, seq);
|
||||
var expected = node["event_hash"]?.GetValue<string>();
|
||||
if (!string.IsNullOrEmpty(expected) && !string.Equals(expected, computed.EventHash, StringComparison.Ordinal))
|
||||
{
|
||||
hashesValid = false;
|
||||
}
|
||||
|
||||
stats.UpdateHashes(computed.EventHash, computed.MerkleLeafHash);
|
||||
leafHashes.Add(computed.MerkleLeafHash);
|
||||
expectedMerkleRoot ??= node["merkle_root"]?.GetValue<string>();
|
||||
|
||||
// enqueue for concurrent append
|
||||
var record = new LedgerEventRecord(
|
||||
tenant,
|
||||
envelope["chain_id"]?.GetValue<Guid>() ?? Guid.Empty,
|
||||
seq,
|
||||
envelope["event_id"]?.GetValue<Guid>() ?? Guid.Empty,
|
||||
envelope["event_type"]?.GetValue<string>() ?? string.Empty,
|
||||
envelope["policy_version"]?.GetValue<string>() ?? string.Empty,
|
||||
envelope["finding_id"]?.GetValue<string>() ?? string.Empty,
|
||||
envelope["artifact_id"]?.GetValue<string>() ?? string.Empty,
|
||||
envelope["source_run_id"]?.GetValue<Guid?>(),
|
||||
envelope["actor_id"]?.GetValue<string>() ?? "system",
|
||||
envelope["actor_type"]?.GetValue<string>() ?? "system",
|
||||
envelope["occurred_at"]?.GetValue<DateTimeOffset>() ?? recordedAt,
|
||||
recordedAt,
|
||||
envelope,
|
||||
computed.EventHash,
|
||||
envelope["previous_hash"]?.GetValue<string>() ?? string.Empty,
|
||||
computed.MerkleLeafHash,
|
||||
computed.CanonicalJson);
|
||||
|
||||
// fire-and-track latency
|
||||
await throttler.RunAsync(async () =>
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
await _client.AppendAsync(record, cancellationToken).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
latencies.Add(sw.Elapsed.TotalMilliseconds);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
hashesValid = false;
|
||||
}
|
||||
|
||||
earliest = earliest is null ? entry.RecordedAt : DateTimeOffset.Compare(entry.RecordedAt, earliest.Value) < 0 ? entry.RecordedAt : earliest;
|
||||
latest = latest is null
|
||||
? entry.RecordedAt
|
||||
: DateTimeOffset.Compare(entry.RecordedAt, latest.Value) > 0 ? entry.RecordedAt : latest;
|
||||
|
||||
stats.UpdateHashes(record.EventHash, record.MerkleLeafHash);
|
||||
leafEntries.Add((record.ChainId, record.SequenceNumber, record.MerkleLeafHash));
|
||||
expectedMerkleRoot ??= entry.ExpectedMerkleRoot;
|
||||
|
||||
if (throttler is null)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
await _client.AppendAsync(record, cancellationToken).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
latencies.Add(sw.Elapsed.TotalMilliseconds);
|
||||
Interlocked.Increment(ref eventCount);
|
||||
continue;
|
||||
}
|
||||
|
||||
await throttler.RunAsync(async () =>
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
await _client.AppendAsync(record, cancellationToken).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
latencies.Add(sw.Elapsed.TotalMilliseconds);
|
||||
Interlocked.Increment(ref eventCount);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
await throttler.DrainAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (throttler is not null)
|
||||
{
|
||||
await throttler.DrainAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
swTotal.Stop();
|
||||
|
||||
var latencyArray = latencies.ToArray();
|
||||
Array.Sort(latencyArray);
|
||||
double p95 = latencyArray.Length == 0 ? 0 : latencyArray[(int)Math.Ceiling(latencyArray.Length * 0.95) - 1];
|
||||
|
||||
string? computedRoot = leafHashes.Count == 0 ? null : MerkleCalculator.ComputeRoot(leafHashes);
|
||||
var orderedLeafHashes = leafEntries
|
||||
.OrderBy(entry => entry.ChainId)
|
||||
.ThenBy(entry => entry.Sequence)
|
||||
.Select(entry => entry.LeafHash)
|
||||
.ToList();
|
||||
string? computedRoot = orderedLeafHashes.Count == 0 ? null : MerkleCalculator.ComputeRoot(orderedLeafHashes);
|
||||
var merkleOk = expectedMerkleRoot is null || string.Equals(expectedMerkleRoot, computedRoot, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var report = new
|
||||
{
|
||||
tenant,
|
||||
fixtures = fixtures.ToArray(),
|
||||
fixtures = orderedFixtures,
|
||||
eventsWritten = eventCount,
|
||||
durationSeconds = Math.Max(swTotal.Elapsed.TotalSeconds, (latest - earliest)?.TotalSeconds ?? 0),
|
||||
throughputEps = swTotal.Elapsed.TotalSeconds > 0 ? eventCount / swTotal.Elapsed.TotalSeconds : 0,
|
||||
@@ -125,7 +117,7 @@ public sealed class HarnessRunner
|
||||
cpuPercentMax = 0,
|
||||
memoryMbMax = 0,
|
||||
status = hashesValid && merkleOk ? "pass" : "fail",
|
||||
timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
||||
timestamp = _timeProvider.GetUtcNow().ToString("O"),
|
||||
hashSummary = stats.ToReport(),
|
||||
merkleRoot = computedRoot,
|
||||
merkleExpected = expectedMerkleRoot
|
||||
@@ -136,13 +128,4 @@ public sealed class HarnessRunner
|
||||
return hashesValid && merkleOk ? 0 : 1;
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<string> ReadLinesAsync(string path, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await using var stream = File.OpenRead(path);
|
||||
using var reader = new StreamReader(stream);
|
||||
while (!cancellationToken.IsCancellationRequested && await reader.ReadLineAsync() is { } line)
|
||||
{
|
||||
yield return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user