test(transport): row 74 — gauge asserts become delta-over-scoped-action, not absolute
BundleSessionObservabilityTests' two gauge facts asserted absolute values (Assert.Equal(0, baseline), Assert.Equal(2, ReadGauge())) against what is a single process-global provider slot — the same latent shape as the row-32 QueueDepthGaugeTests flake, correct only while this file is the slot's sole registrant. Converted to the delta-over-scoped-action pattern the row-66 rejection-counter facts in the same file already use: a new ReadGaugeDelta helper measures how far the instrument MOVED across an action driven by this test's own store, so an inherited non-zero baseline cancels out. Two assertions got stronger in the process. The post-clear check was Assert.Equal(0, ReadGauge()) — "the instrument reports zero", which any unregistered slot satisfies; it is now "opening more sessions on the detached store moves the gauge by nothing", which is the actual claim. And the compare-and-clear fact gained a negative control: the superseded store's opens must NOT reach the instrument, so a slot still pointing at oldStore can no longer satisfy the assertion by coincidence when both stores hold equal counts. Fail-before (temporary probe, not committed): with a foreign provider returning 7 registered before the service starts, the OLD body fails `Assert.Equal(0, baseline)` with `Expected: 0 / Actual: 7`; the NEW delta body passes the identical scenario. Honest residual documented on the helper: a gauge has one slot and last-writer-wins, so a registrant that clobbers it BETWEEN the two reads is unrecoverable read-side — that needs row-32's mutual exclusion. It cannot arise today (this is the only file in the assembly constructing a BundleSessionEvictionService, and test assemblies are separate processes); a second such file should bring the collection treatment with it. Transport.Tests: 180/180 green.
This commit is contained in:
+77
-16
@@ -215,30 +215,39 @@ public sealed class BundleSessionObservabilityTests
|
|||||||
public async Task EvictionService_RegistersTheGaugeProvider_AndClearsItOnStop()
|
public async Task EvictionService_RegistersTheGaugeProvider_AndClearsItOnStop()
|
||||||
{
|
{
|
||||||
// The eviction service is Transport's only lifecycle seam, so it owns the
|
// The eviction service is Transport's only lifecycle seam, so it owns the
|
||||||
// process-global gauge slot. The gauge is read as a delta against a baseline
|
// process-global gauge slot. Every assertion here is a DELTA over a scoped action
|
||||||
// because the instrument is a process-wide static.
|
// against this test's own store (register row 74) — see ReadGaugeDelta.
|
||||||
var clock = new TestClock(DateTimeOffset.UtcNow);
|
var clock = new TestClock(DateTimeOffset.UtcNow);
|
||||||
var store = new BundleSessionStore(Options.Create(new TransportOptions()), clock, NullLogger<BundleSessionStore>.Instance);
|
var store = new BundleSessionStore(Options.Create(new TransportOptions()), clock, NullLogger<BundleSessionStore>.Instance);
|
||||||
var service = new BundleSessionEvictionService(store, NullLogger<BundleSessionEvictionService>.Instance);
|
var service = new BundleSessionEvictionService(store, NullLogger<BundleSessionEvictionService>.Instance);
|
||||||
|
|
||||||
var baseline = ReadGauge();
|
|
||||||
|
|
||||||
await service.StartAsync(CancellationToken.None);
|
await service.StartAsync(CancellationToken.None);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)));
|
// While registered, this store's opens are what the gauge reports.
|
||||||
store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)));
|
var whileRegistered = ReadGaugeDelta(() =>
|
||||||
|
{
|
||||||
|
store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)));
|
||||||
|
store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)));
|
||||||
|
});
|
||||||
|
|
||||||
Assert.Equal(2, ReadGauge());
|
Assert.Equal(2, whileRegistered);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
await service.StopAsync(CancellationToken.None);
|
await service.StopAsync(CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// After the clear the gauge falls back to reporting 0, not through a dead store.
|
// After the clear the gauge no longer reads through this store: opening more
|
||||||
Assert.Equal(0, ReadGauge());
|
// sessions on it moves the instrument by nothing. Stated as a delta this is a
|
||||||
Assert.Equal(0, baseline);
|
// STRONGER claim than the old `Assert.Equal(0, ReadGauge())` — that one merely
|
||||||
|
// said "the instrument reports zero", which any unregistered slot satisfies,
|
||||||
|
// and which a foreign registrant would have failed for reasons unrelated to
|
||||||
|
// the clear.
|
||||||
|
var afterClear = ReadGaugeDelta(() =>
|
||||||
|
store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))));
|
||||||
|
|
||||||
|
Assert.Equal(0, afterClear);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -257,29 +266,81 @@ public sealed class BundleSessionObservabilityTests
|
|||||||
await newService.StartAsync(CancellationToken.None);
|
await newService.StartAsync(CancellationToken.None);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
newStore.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)));
|
// Late stop of the superseded instance, then prove the NEWER store is still
|
||||||
|
// the one driving the gauge — by moving it and measuring the movement.
|
||||||
// Late stop of the superseded instance.
|
|
||||||
await oldService.StopAsync(CancellationToken.None);
|
await oldService.StopAsync(CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal(1, ReadGauge());
|
var newStoreStillDrivesIt = ReadGaugeDelta(() =>
|
||||||
|
newStore.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))));
|
||||||
|
|
||||||
|
Assert.Equal(1, newStoreStillDrivesIt);
|
||||||
|
|
||||||
|
// Negative control on the same window: the superseded store is detached, so
|
||||||
|
// its opens must not reach the instrument. Without this, a provider slot left
|
||||||
|
// pointing at oldStore would still satisfy the assertion above by coincidence
|
||||||
|
// whenever both stores happened to hold equal counts.
|
||||||
|
var oldStoreIsDetached = ReadGaugeDelta(() =>
|
||||||
|
oldStore.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))));
|
||||||
|
|
||||||
|
Assert.Equal(0, oldStoreIsDetached);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
await newService.StopAsync(CancellationToken.None);
|
await newService.StopAsync(CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.Equal(0, ReadGauge());
|
var afterClear = ReadGaugeDelta(() =>
|
||||||
|
newStore.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))));
|
||||||
|
|
||||||
|
Assert.Equal(0, afterClear);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs <paramref name="act"/> between two observations of the open-import-sessions
|
||||||
|
/// gauge and returns how far the instrument MOVED across it.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Register row 74. These tests used to assert absolute gauge values —
|
||||||
|
/// <c>Assert.Equal(0, baseline)</c>, <c>Assert.Equal(2, ReadGauge())</c> — against
|
||||||
|
/// what is a single process-global provider slot
|
||||||
|
/// (<c>SetImportSessionCountProvider</c> / <c>ClearImportSessionCountProvider</c>).
|
||||||
|
/// That is the same shape as the register row 32 flake in
|
||||||
|
/// <c>QueueDepthGaugeTests</c>: correct only for as long as this file is the slot's
|
||||||
|
/// sole registrant, and silently wrong the day a sibling suite writes it. A delta
|
||||||
|
/// carries no assumption about what the slot held on entry, so a non-zero inherited
|
||||||
|
/// baseline cancels out.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Honest residual, and the reason this is a delta rather than a full fix: unlike
|
||||||
|
/// <see cref="ReadRejectionCounter"/> — where every measurement in the window is
|
||||||
|
/// driven synchronously by this test's own <c>Open</c> call, making it genuinely
|
||||||
|
/// immune — a gauge has ONE slot and last-writer-wins. If a concurrent registrant
|
||||||
|
/// overwrote the slot BETWEEN the two reads, no read-side technique could recover;
|
||||||
|
/// that case needs mutual exclusion on the slot (row 32's serialized collection).
|
||||||
|
/// It cannot arise today: this is the only file in the assembly that constructs a
|
||||||
|
/// <c>BundleSessionEvictionService</c>, and test assemblies run in separate
|
||||||
|
/// processes. A second such file should bring the collection treatment with it.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="act">The action to measure across.</param>
|
||||||
|
/// <returns>The gauge's movement while <paramref name="act"/> ran.</returns>
|
||||||
|
private static long ReadGaugeDelta(Action act)
|
||||||
|
{
|
||||||
|
var before = ReadGauge();
|
||||||
|
act();
|
||||||
|
return ReadGauge() - before;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Forces one synchronous observation of the open-import-sessions gauge and returns
|
/// Forces one synchronous observation of the open-import-sessions gauge and returns
|
||||||
/// the measured value. Mirrors the MeterListener helper used by the Communication
|
/// the measured value. Mirrors the MeterListener helper used by the Communication
|
||||||
/// and StoreAndForward telemetry suites.
|
/// and StoreAndForward telemetry suites. Private to <see cref="ReadGaugeDelta"/> by
|
||||||
|
/// convention — see its remarks for why no test reads this absolutely.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The gauge's current value, or 0 if it never reported.</returns>
|
/// <returns>The gauge's current value, or 0 if it never reported.</returns>
|
||||||
private static long ReadGauge()
|
private static long ReadGauge()
|
||||||
|
|||||||
Reference in New Issue
Block a user