test(localdb): pin drift-watermark branch; guard writer loop against completed-lane spin

This commit is contained in:
Joseph Doherty
2026-07-17 22:50:33 -04:00
parent 92ccbac8db
commit 183901e8ac
2 changed files with 105 additions and 6 deletions
@@ -157,6 +157,8 @@ internal sealed class SyncSession
{
Task<bool>? controlWait = null;
Task<bool>? dataWait = null;
var controlDone = false;
var dataDone = false;
while (true)
{
ct.ThrowIfCancellationRequested();
@@ -165,14 +167,27 @@ internal sealed class SyncSession
await duplex.Send(message, ct);
continue;
}
// WaitToReadAsync => false means the lane was COMPLETED and is drained: stop waiting on
// it (re-creating its waiter would instantly re-complete false — a 100% CPU spin) and
// exit once both lanes are terminal. Nothing completes the lanes today, but Task 12 or
// a future refactor must not silently detonate this loop.
if (controlDone && dataDone)
return;
// At most one outstanding waiter per lane (SingleReader); a stale already-completed
// waiter only costs one extra TryRead pass.
controlWait ??= control.WaitToReadAsync(ct).AsTask();
dataWait ??= data.WaitToReadAsync(ct).AsTask();
var completed = await Task.WhenAny(controlWait, dataWait);
await completed;
if (controlWait.IsCompleted) controlWait = null;
if (dataWait.IsCompleted) dataWait = null;
if (!controlDone) controlWait ??= control.WaitToReadAsync(ct).AsTask();
if (!dataDone) dataWait ??= data.WaitToReadAsync(ct).AsTask();
await Task.WhenAny(controlWait ?? dataWait!, dataWait ?? controlWait!);
if (controlWait is { IsCompleted: true })
{
controlDone = !await controlWait;
controlWait = null;
}
if (dataWait is { IsCompleted: true })
{
dataDone = !await dataWait;
dataWait = null;
}
}
}
@@ -278,6 +293,8 @@ internal sealed class SyncSession
await controlLane.WriteAsync(new SyncMessage { DeltaAck = new DeltaAck { AppliedThruSeq = ackThru } }, ct);
}
// Not transactional with the watermark update: a crash between them redelivers the batch and
// dead-letters the same entry again (at-least-once — a benign duplicate dead-letter row).
private Task DeadLetterDriftedAsync(
OplogEntryRecord entry, long physicalMs, long nowMs, DateTimeOffset now, CancellationToken ct) =>
_db.ExecuteAsync(