test(auditlog): close register row 95 — stop the three remaining leaked fast-tick actors
Row 82 (da23f2dc) fixed this leak shape in PartitionPurgeTests and NotifyDispatcherAuditTrailTests; three classes in the same assembly still had it. Each creates an actor with a fast periodic tick (100 ms purge override, ~0 s reconciliation override) against a shared MsSqlMigrationFixture database and never stops it deterministically, relying on TestKit's Dispose -> Shutdown — which is timeout-bounded and does NOT fail when it expires. A survivor keeps mutating the fixture DB while LATER test classes run. All three now use the same Watch(actor) / Sys.Stop(actor) / ExpectTerminated(actor) helperda23f2dcestablished, applied at EVERY actor-creation site — 11 in AuditLogPurgeActorTests, 10 in SiteAuditReconciliationActorTests, 2 in OutageReconciliationTests. The two real-DB cases are the ones that could corrupt another class: AuditLogPurgeActorTests' EndToEnd_RealPartition_… keeps switching partitions out of the shared DB, and SiteAuditReconciliationActorTests' Tick_Duplicates_NotDoubleInserted holds a live AuditLogRepository over a DbContext the test is about to dispose. The stub-repo cases are cheaper but not free: each burns a DI scope and a thread every 100 ms for the rest of the assembly run. OutageReconciliationTests needed more than the helper. It already called Sys.Stop, but (a) fire-and-forget, so the call returns before the in-flight tick has finished writing to MSSQL, and (b) from the LAST line of the enclosing `await using` block, so any earlier assertion failure skipped it entirely. Both stops move to immediately after the last step that needs the actor alive — before the read-back that counts rows — and the AuditLogIngestActor that BuildCentralPipeline creates is now stopped too (test 2 was discarding its ref outright, leaving it holding a service provider the block then disposed). Where a test's remaining assertions read a collection the actor appends to (client.Calls indexing, Assert.Empty on a switched-boundaries list), the stop is placed BEFORE them so they no longer read a moving target. AuditLog.Tests: 383/383 green (1 m 51 s), 0 skipped.
This commit is contained in:
+68
-11
@@ -252,6 +252,37 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
NullLogger<SiteAuditReconciliationActor>.Instance)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops a reconciliation actor and blocks until it is actually dead.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Deferred-work register row 95, the same leak shape row 82 fixed in
|
||||
/// <c>PartitionPurgeTests</c> / <c>NotifyDispatcherAuditTrailTests</c>. Every actor this
|
||||
/// class creates ticks on a ~0 s repeat interval (<see cref="FastTickOptions"/>) and was
|
||||
/// only ever stopped implicitly by <see cref="TestKit"/>'s <c>Dispose</c> →
|
||||
/// <c>Shutdown</c>, which is bounded by a timeout and does NOT fail when it expires. A
|
||||
/// survivor keeps pulling and INSERTING audit rows: in
|
||||
/// <c>Tick_Duplicates_NotDoubleInserted</c> it holds a real
|
||||
/// <see cref="AuditLogRepository"/> over the shared <see cref="MsSqlMigrationFixture"/>
|
||||
/// database (and over a <c>DbContext</c> the test is about to dispose), so it writes into
|
||||
/// the DB LATER test classes read from.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Stopping the actor inside the test that created it removes the dependency on
|
||||
/// ActorSystem-shutdown timing entirely.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void StopReconciliationActor(IActorRef actor)
|
||||
{
|
||||
Watch(actor);
|
||||
Sys.Stop(actor);
|
||||
// A stop is processed after the in-flight tick completes, so this also guarantees no
|
||||
// pull or insert is still running against the fixture DB.
|
||||
ExpectTerminated(actor, TimeSpan.FromSeconds(30));
|
||||
Unwatch(actor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the EventStream and collects every
|
||||
/// <see cref="SiteAuditTelemetryStalledChanged"/> publication into a list
|
||||
@@ -278,7 +309,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
var repo = new RecordingRepo();
|
||||
var opts = FastTickOptions();
|
||||
|
||||
CreateActor(sites, client, repo, opts);
|
||||
var actor = CreateActor(sites, client, repo, opts);
|
||||
|
||||
// The first scheduled tick fires after `ReconciliationIntervalSeconds`,
|
||||
// which is 0 for the test — Akka's scheduler still respects the
|
||||
@@ -289,6 +320,8 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
() => Assert.True(client.Calls.Count >= 1, $"expected >= 1 pull call, got {client.Calls.Count}"),
|
||||
duration: TimeSpan.FromSeconds(3),
|
||||
interval: TimeSpan.FromMilliseconds(50));
|
||||
|
||||
StopReconciliationActor(actor);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -304,7 +337,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
var client = new ScriptedPullClient();
|
||||
var repo = new RecordingRepo();
|
||||
|
||||
CreateActor(sites, client, repo, FastTickOptions());
|
||||
var actor = CreateActor(sites, client, repo, FastTickOptions());
|
||||
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
@@ -313,6 +346,8 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
},
|
||||
duration: TimeSpan.FromSeconds(3),
|
||||
interval: TimeSpan.FromMilliseconds(50));
|
||||
|
||||
StopReconciliationActor(actor);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -329,11 +364,14 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
new PullAuditEventsResponse(new[] { e1, e2 }, MoreAvailable: false));
|
||||
var repo = new RecordingRepo();
|
||||
|
||||
CreateActor(sites, client, repo, FastTickOptions());
|
||||
var actor = CreateActor(sites, client, repo, FastTickOptions());
|
||||
|
||||
AwaitAssert(() => Assert.Equal(2, repo.InsertCallCount),
|
||||
duration: TimeSpan.FromSeconds(3),
|
||||
interval: TimeSpan.FromMilliseconds(50));
|
||||
|
||||
StopReconciliationActor(actor);
|
||||
|
||||
Assert.Contains(repo.Inserted, e => e.EventId == e1.EventId);
|
||||
Assert.Contains(repo.Inserted, e => e.EventId == e2.EventId);
|
||||
}
|
||||
@@ -371,13 +409,18 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
await using var context = CreateContext();
|
||||
var repo = new AuditLogRepository(context);
|
||||
|
||||
CreateActor(sites, client, repo, FastTickOptions());
|
||||
var actor = CreateActor(sites, client, repo, FastTickOptions());
|
||||
|
||||
// Wait for the actor to ingest both rows.
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
AwaitAssert(() => Assert.True(client.Calls.Count >= 1),
|
||||
duration: TimeSpan.FromSeconds(3));
|
||||
|
||||
// Stop before reading: this actor holds a real repository over the SHARED fixture
|
||||
// database (and over the DbContext this test is about to dispose), so it must not
|
||||
// outlive the test (register row 95).
|
||||
StopReconciliationActor(actor);
|
||||
|
||||
// Even though the pull returned 2 events, only 1 fresh row should
|
||||
// exist in MSSQL alongside the pre-existing one — InsertIfNotExistsAsync
|
||||
// is first-write-wins on EventId.
|
||||
@@ -413,7 +456,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
new PullAuditEventsResponse(new[] { e1, e2, e3 }, MoreAvailable: false));
|
||||
var repo = new RecordingRepo();
|
||||
|
||||
CreateActor(sites, client, repo, FastTickOptions());
|
||||
var actor = CreateActor(sites, client, repo, FastTickOptions());
|
||||
|
||||
// Wait until we have at least two pulls — the second one must use t3
|
||||
// as its `since` argument because that was the max OccurredAtUtc in
|
||||
@@ -423,6 +466,10 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
duration: TimeSpan.FromSeconds(5),
|
||||
interval: TimeSpan.FromMilliseconds(50));
|
||||
|
||||
// Stop before indexing into client.Calls: the list must not grow mid-assertion
|
||||
// (register row 95).
|
||||
StopReconciliationActor(actor);
|
||||
|
||||
Assert.Equal(DateTime.MinValue, client.Calls[0].SinceUtc);
|
||||
Assert.Equal(t3, client.Calls[1].SinceUtc);
|
||||
|
||||
@@ -451,13 +498,15 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
new PullAuditEventsResponse(new[] { low, high, mid }, MoreAvailable: true));
|
||||
var repo = new RecordingRepo();
|
||||
|
||||
CreateActor(sites, client, repo, FastTickOptions());
|
||||
var actor = CreateActor(sites, client, repo, FastTickOptions());
|
||||
|
||||
AwaitAssert(() => Assert.True(client.Calls.Count >= 2,
|
||||
$"need at least 2 pulls, got {client.Calls.Count}"),
|
||||
duration: TimeSpan.FromSeconds(5),
|
||||
interval: TimeSpan.FromMilliseconds(50));
|
||||
|
||||
StopReconciliationActor(actor);
|
||||
|
||||
Assert.Equal(t, client.Calls[1].SinceUtc);
|
||||
Assert.Equal(high.EventId.ToString(), client.Calls[1].AfterId);
|
||||
}
|
||||
@@ -475,13 +524,15 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
new PullAuditEventsResponse(new[] { evt }, MoreAvailable: false));
|
||||
var repo = new AlwaysThrowingRepo();
|
||||
|
||||
CreateActor(sites, client, repo, FastTickOptions());
|
||||
var actor = CreateActor(sites, client, repo, FastTickOptions());
|
||||
|
||||
AwaitAssert(() => Assert.True(client.Calls.Count >= 2,
|
||||
$"need at least 2 pulls, got {client.Calls.Count}"),
|
||||
duration: TimeSpan.FromSeconds(5),
|
||||
interval: TimeSpan.FromMilliseconds(50));
|
||||
|
||||
StopReconciliationActor(actor);
|
||||
|
||||
Assert.Equal(DateTime.MinValue, client.Calls[1].SinceUtc);
|
||||
Assert.Null(client.Calls[1].AfterId);
|
||||
}
|
||||
@@ -543,7 +594,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
new PullAuditEventsResponse(new[] { bEvent }, MoreAvailable: false));
|
||||
var repo = new RecordingRepo();
|
||||
|
||||
CreateActor(sites, client, repo, FastTickOptions());
|
||||
var actor = CreateActor(sites, client, repo, FastTickOptions());
|
||||
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
@@ -552,6 +603,8 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
},
|
||||
duration: TimeSpan.FromSeconds(3),
|
||||
interval: TimeSpan.FromMilliseconds(50));
|
||||
|
||||
StopReconciliationActor(actor);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -574,12 +627,13 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
var repo = new RecordingRepo();
|
||||
var (probe, _) = SubscribeStalled();
|
||||
|
||||
CreateActor(sites, client, repo, FastTickOptions(stalledAfter: 2));
|
||||
var actor = CreateActor(sites, client, repo, FastTickOptions(stalledAfter: 2));
|
||||
|
||||
// Expect Stalled=true after the second non-draining tick. The probe
|
||||
// waits with its own timeout (a few seconds gives the 0 s repeat
|
||||
// interval ample slack).
|
||||
var msg = probe.ExpectMsg<SiteAuditTelemetryStalledChanged>(TimeSpan.FromSeconds(5));
|
||||
StopReconciliationActor(actor);
|
||||
Assert.Equal("siteA", msg.SiteId);
|
||||
Assert.True(msg.Stalled);
|
||||
}
|
||||
@@ -606,7 +660,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
var repo = new RecordingRepo();
|
||||
var (probe, _) = SubscribeStalled();
|
||||
|
||||
CreateActor(sites, client, repo, FastTickOptions(stalledAfter: 2));
|
||||
var actor = CreateActor(sites, client, repo, FastTickOptions(stalledAfter: 2));
|
||||
|
||||
// First publication is the stalled=true transition; second is the
|
||||
// back-to-draining flip. The actor publishes ONLY on transitions so we
|
||||
@@ -615,6 +669,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
Assert.True(first.Stalled);
|
||||
|
||||
var second = probe.ExpectMsg<SiteAuditTelemetryStalledChanged>(TimeSpan.FromSeconds(5));
|
||||
StopReconciliationActor(actor);
|
||||
Assert.False(second.Stalled);
|
||||
Assert.Equal("siteA", second.SiteId);
|
||||
}
|
||||
@@ -643,7 +698,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
var repo = new PoisonInsertRepo(
|
||||
poison.EventId, new InvalidOperationException("poison row will never insert"));
|
||||
|
||||
CreateActor(sites, client, repo, FastTickOptions());
|
||||
var actor = CreateActor(sites, client, repo, FastTickOptions());
|
||||
|
||||
// After the 5th failed attempt the actor abandons the row and writes ONE
|
||||
// synthetic ReconciliationAbandoned row (fresh EventId) whose details carry
|
||||
@@ -669,5 +724,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
() => Assert.Contains(client.Calls, c => c.SinceUtc == poisonTime),
|
||||
duration: TimeSpan.FromSeconds(5),
|
||||
interval: TimeSpan.FromMilliseconds(100));
|
||||
|
||||
StopReconciliationActor(actor);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user