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
@@ -145,6 +145,12 @@ public sealed class SyncSessionTests : IDisposable
return r[0];
}
private static async Task<long> LastAppliedRemoteSeq(SqliteLocalDb db)
{
var r = await db.QueryAsync("SELECT last_applied_remote_seq FROM __localdb_peer_state WHERE id = 1", x => x.GetInt64(0));
return r[0];
}
private static OplogEntry ProtoEntry(long seq, long id, string sku, long hlc, string nodeId = "peer") =>
new()
{
@@ -461,6 +467,82 @@ public sealed class SyncSessionTests : IDisposable
await SwallowAsync(run);
}
[Fact]
public async Task TopSeqDrifted_WatermarkStillAdvances()
{
// The batch's TOP seq is dead-lettered, so the applier's watermark stops below it; the
// session's standalone MAX update must still advance the durable watermark and the ack.
var a = await NewSideAsync(
options: new ReplicationOptions { FlushInterval = TimeSpan.FromMilliseconds(20), FailClosedOnDrift = true });
var (duplex, toSession, fromSession) = ScriptedPeer();
using var cts = new CancellationTokenSource(RunTimeout);
var run = a.Session.RunAsync(duplex, cts.Token);
await DriveHandshakeAsync(a.Db, toSession, fromSession, cts.Token);
await toSession.WriteAsync(new SyncMessage
{
DeltaBatch = new DeltaBatch
{
Entries =
{
ProtoEntry(1, 1, "CLEAN", 5_000_000),
ProtoEntry(2, 2, "DRIFTED", AheadHlc(TimeSpan.FromMinutes(10))),
},
},
}, cts.Token);
var ack = await NextOfCaseAsync(fromSession, SyncMessage.MsgOneofCase.DeltaAck, cts.Token);
Assert.Equal(2, ack.DeltaAck.AppliedThruSeq);
Assert.Equal(2, await LastAppliedRemoteSeq(a.Db));
var rows = await ReadOrders(a.Db);
Assert.Single(rows);
Assert.Equal(1, rows[0].Id);
var dead = await a.Db.QueryAsync("SELECT pk_json FROM __localdb_dead_letter", x => x.GetString(0));
Assert.Equal(["{\"id\":2}"], dead);
cts.Cancel();
await SwallowAsync(run);
}
[Fact]
public async Task AllEntriesDrifted_WatermarkStillAdvances()
{
// Every entry dead-lettered: the applier sees an empty batch (AppliedThruSeq 0), yet the
// durable watermark and the ack must still cover the batch's max seq.
var a = await NewSideAsync(
options: new ReplicationOptions { FlushInterval = TimeSpan.FromMilliseconds(20), FailClosedOnDrift = true });
var (duplex, toSession, fromSession) = ScriptedPeer();
using var cts = new CancellationTokenSource(RunTimeout);
var run = a.Session.RunAsync(duplex, cts.Token);
await DriveHandshakeAsync(a.Db, toSession, fromSession, cts.Token);
await toSession.WriteAsync(new SyncMessage
{
DeltaBatch = new DeltaBatch
{
Entries =
{
ProtoEntry(1, 1, "DRIFTED1", AheadHlc(TimeSpan.FromMinutes(10))),
ProtoEntry(2, 2, "DRIFTED2", AheadHlc(TimeSpan.FromMinutes(11))),
},
},
}, cts.Token);
var ack = await NextOfCaseAsync(fromSession, SyncMessage.MsgOneofCase.DeltaAck, cts.Token);
Assert.Equal(2, ack.DeltaAck.AppliedThruSeq);
Assert.Equal(2, await LastAppliedRemoteSeq(a.Db));
Assert.Empty(await ReadOrders(a.Db));
var dead = await a.Db.QueryAsync("SELECT pk_json FROM __localdb_dead_letter ORDER BY pk_json", x => x.GetString(0));
Assert.Equal(["{\"id\":1}", "{\"id\":2}"], dead);
cts.Cancel();
await SwallowAsync(run);
}
[Fact]
public async Task PeerNeedsSnapshot_InvokesSnapshotHooks()
{