fix(health-monitoring): resolve HealthMonitoring-001/002 — populate S&F buffer depth, make SiteHealthState immutable
This commit is contained in:
@@ -161,7 +161,62 @@ public class CentralHealthAggregatorTests
|
||||
_aggregator.ProcessReport(report);
|
||||
|
||||
var state = _aggregator.GetSiteState("site-1");
|
||||
Assert.Equal(42, state!.LatestReport.ScriptErrorCount);
|
||||
Assert.Equal(42, state!.LatestReport!.ScriptErrorCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-002 regression: SiteHealthState is mutated from multiple
|
||||
/// threads (ProcessReport, MarkHeartbeat, CheckForOfflineSites). With a mutable
|
||||
/// class and unsynchronized field writes, a snapshot read could observe a torn
|
||||
/// or half-applied state. The state must be immutable and every transition an
|
||||
/// atomic reference swap, so a snapshot is always internally consistent and the
|
||||
/// monotonic sequence-number guard is never subverted by a lost update.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ProcessReport_ConcurrentUpdates_NeverLoseSequenceOrTearState()
|
||||
{
|
||||
const int iterations = 5_000;
|
||||
// SiteHealthState must be an immutable record so handing the reference to
|
||||
// UI callers (and reading it concurrently) is safe.
|
||||
Assert.True(typeof(SiteHealthState).GetMethod("<Clone>$") != null,
|
||||
"SiteHealthState must be an immutable record for safe concurrent reads.");
|
||||
|
||||
_aggregator.ProcessReport(MakeReport("site-1", 0));
|
||||
|
||||
var writer = Task.Run(() =>
|
||||
{
|
||||
for (long seq = 1; seq <= iterations; seq++)
|
||||
_aggregator.ProcessReport(MakeReport("site-1", seq));
|
||||
});
|
||||
|
||||
var heartbeater = Task.Run(() =>
|
||||
{
|
||||
for (int i = 0; i < iterations; i++)
|
||||
_aggregator.MarkHeartbeat("site-1", _timeProvider.GetUtcNow());
|
||||
});
|
||||
|
||||
long maxObserved = 0;
|
||||
var reader = Task.Run(() =>
|
||||
{
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
var state = _aggregator.GetSiteState("site-1");
|
||||
if (state == null) continue;
|
||||
// A consistent snapshot: the stored report's sequence number must
|
||||
// always match the state's LastSequenceNumber (no half-applied update).
|
||||
Assert.Equal(state.LastSequenceNumber, state.LatestReport!.SequenceNumber);
|
||||
if (state.LastSequenceNumber > maxObserved)
|
||||
maxObserved = state.LastSequenceNumber;
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(writer, heartbeater, reader);
|
||||
|
||||
// The final state must reflect the highest sequence — no lost update.
|
||||
var final = _aggregator.GetSiteState("site-1");
|
||||
Assert.Equal(iterations, final!.LastSequenceNumber);
|
||||
Assert.Equal(iterations, final.LatestReport!.SequenceNumber);
|
||||
Assert.True(final.IsOnline);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -173,11 +228,11 @@ public class CentralHealthAggregatorTests
|
||||
_aggregator.ProcessReport(MakeReport("site-1", 10));
|
||||
_aggregator.ProcessReport(MakeReport("site-1", 1));
|
||||
|
||||
var state = _aggregator.GetSiteState("site-1");
|
||||
Assert.Equal(10, state!.LastSequenceNumber);
|
||||
Assert.Equal(10, _aggregator.GetSiteState("site-1")!.LastSequenceNumber);
|
||||
|
||||
// Once it exceeds the old max, it works again
|
||||
// Once it exceeds the old max, it works again. SiteHealthState is an
|
||||
// immutable snapshot, so re-fetch to observe the new state.
|
||||
_aggregator.ProcessReport(MakeReport("site-1", 11));
|
||||
Assert.Equal(11, state.LastSequenceNumber);
|
||||
Assert.Equal(11, _aggregator.GetSiteState("site-1")!.LastSequenceNumber);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user