fix(localdb): telemetry review fixes — meter naming, snapshot status, session-count Connected

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
This commit is contained in:
Joseph Doherty
2026-07-18 00:14:57 -04:00
parent be4ca76f5f
commit eb40cd8f29
9 changed files with 130 additions and 40 deletions
@@ -163,6 +163,65 @@ public sealed class MetricsTests : IAsyncLifetime
Assert.True(lags[^1] < 60, $"lag {lags[^1]} should be small");
}
[Fact]
public async Task SnapshotOnlySync_UpdatesLastSyncAndLag()
{
var statusA = new SyncStatus();
var statusB = new SyncStatus();
using var metricsB = new LocalDbMetrics { LastSyncUtcProvider = () => statusB.LastSyncUtc };
using var collector = new Collector(metricsB.Meter);
var a = await NewSideAsync(status: statusA);
var b = await NewSideAsync(metrics: metricsB, status: statusB);
await a.Db.ExecuteAsync("INSERT INTO orders (id, sku, qty) VALUES (1, 'SNAP', 10)");
// A owes B a full snapshot; the pump then skips the tail deltas the snapshot covers, so
// B converges via the snapshot path alone (no delta batch ever reaches it).
await a.Store.SetNeedsSnapshotAsync(true, default);
// Before any sync the lag gauge yields no measurement.
collector.Observe();
Assert.Empty(collector.Doubles("localdb.sync.lag.seconds"));
var (da, db) = DuplexPair();
using var cts = new CancellationTokenSource(RunTimeout);
var runA = a.Session.RunAsync(da, cts.Token);
var runB = b.Session.RunAsync(db, cts.Token);
await WaitForAsync(async () => (await ReadOrders(b.Db)).Count == 1, RunTimeout);
await WaitForAsync(() => Task.FromResult(statusB.LastSyncUtc is not null), RunTimeout);
Assert.NotNull(statusB.LastSyncUtc); // receive path: SnapshotComplete
Assert.NotNull(statusA.LastSyncUtc); // send path: SnapshotSender returned
collector.Observe();
var lags = collector.Doubles("localdb.sync.lag.seconds");
Assert.NotEmpty(lags);
Assert.True(lags[^1] >= 0, $"lag {lags[^1]} should be non-negative");
cts.Cancel();
await SwallowAsync(runA);
await SwallowAsync(runB);
}
[Fact]
public void BothRoles_ActiveSessions_ConnectedUntilLastEnds()
{
// A node running both roles holds two concurrent sessions; one ending must not clobber
// the other's Connected signal.
var status = new SyncStatus();
Assert.False(status.Connected);
status.SessionStarted();
Assert.True(status.Connected);
status.SessionStarted();
Assert.True(status.Connected);
status.SessionEnded();
Assert.True(status.Connected);
status.SessionEnded();
Assert.False(status.Connected);
}
[Fact]
public async Task Metrics_NullSafe_NoMetricsRegistered()
{
@@ -229,23 +288,29 @@ public sealed class MetricsTests : IAsyncLifetime
var clientDb = clientProvider.GetRequiredService<ILocalDb>();
await clientDb.ExecuteAsync("INSERT INTO orders (id, sku, qty) VALUES (1, 'C', 10)");
var status = clientProvider.GetRequiredService<ISyncStatus>();
var serverStatus = serverHost.Services.GetRequiredService<ISyncStatus>();
Assert.False(status.Connected);
Assert.False(serverStatus.Connected);
using var cts = new CancellationTokenSource(RunTimeout);
await bg.StartAsync(cts.Token);
await WaitForAsync(() => Task.FromResult(status.Connected), RunTimeout);
await WaitForAsync(() => Task.FromResult(serverStatus.Connected), RunTimeout);
await WaitForAsync(
async () => (await ReadOrders(clientDb)).Count == 2 && (await ReadOrders(serverDb)).Count == 2,
RunTimeout);
Assert.NotNull(status.PeerNodeId);
Assert.NotNull(status.LastSyncUtc);
Assert.NotNull(serverStatus.PeerNodeId);
await bg.StopAsync(CancellationToken.None);
await WaitForAsync(() => Task.FromResult(!status.Connected), RunTimeout);
Assert.False(status.Connected);
await WaitForAsync(() => Task.FromResult(!serverStatus.Connected), RunTimeout);
Assert.False(serverStatus.Connected);
}
// ---- in-memory harness ----------------------------------------------------------------
@@ -265,6 +330,9 @@ public sealed class MetricsTests : IAsyncLifetime
var store = new OplogStore(db, options);
var applier = new LwwApplier(db);
var session = new SyncSession(db, store, applier, options, NullLogger.Instance, null, metrics, status);
var streamer = new SnapshotStreamer(db, store, options, NullLogger.Instance);
session.SnapshotSender = streamer.SendAsync;
session.SnapshotApplier = new SnapshotApplier(db, applier, store, NullLogger.Instance);
return new Side(db, store, applier, session);
}