feat(localdb): telemetry meters + ISyncStatus

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
This commit is contained in:
Joseph Doherty
2026-07-18 00:03:39 -04:00
parent 8befebd476
commit be4ca76f5f
9 changed files with 765 additions and 8 deletions
@@ -31,6 +31,8 @@ internal sealed class SyncSession
private readonly ReplicationOptions _options;
private readonly ILogger _logger;
private readonly Func<DateTimeOffset> _utcNow;
private readonly LocalDbMetrics? _metrics;
private readonly SyncStatus? _status;
// Highest oplog seq handed to the wire this session; the pump reads strictly above it so a batch
// is never re-sent before its ack (the reliable ordered stream delivers exactly once in-session).
@@ -45,7 +47,7 @@ internal sealed class SyncSession
public SyncSession(
SqliteLocalDb db, OplogStore store, LwwApplier applier, ReplicationOptions options, ILogger logger,
Func<DateTimeOffset>? utcNow = null)
Func<DateTimeOffset>? utcNow = null, LocalDbMetrics? metrics = null, SyncStatus? status = null)
{
_db = db;
_store = store;
@@ -53,6 +55,8 @@ internal sealed class SyncSession
_options = options;
_logger = logger;
_utcNow = utcNow ?? (() => DateTimeOffset.UtcNow);
_metrics = metrics;
_status = status;
}
/// <summary>
@@ -140,6 +144,7 @@ internal sealed class SyncSession
await _store.SetPeerNodeIdAsync(peerHandshake.NodeId, ct);
PeerNodeId = peerHandshake.NodeId;
_status?.SetPeerNodeId(peerHandshake.NodeId);
_sentThruSeq = peerHandshake.LastAppliedRemoteSeq;
Interlocked.Exchange(ref _peerAckedSeq, peerHandshake.LastAppliedRemoteSeq);
@@ -164,6 +169,7 @@ internal sealed class SyncSession
if (SnapshotSender is null)
throw new NotSupportedException("snapshot required but no snapshot sender configured");
var snapshotAsOfSeq = await SnapshotSender(dataSend, ct);
_metrics?.RecordSnapshot("sent");
// The snapshot already carries every row state up to as-of; skipping the pump past it
// avoids re-sending tail deltas the peer would only discard via LWW.
if (snapshotAsOfSeq > _sentThruSeq)
@@ -269,6 +275,7 @@ internal sealed class SyncSession
"Replication protocol error: SnapshotComplete with no active snapshot (missing SnapshotBegin).");
await SnapshotApplier!.OnCompleteAsync(msg.SnapshotComplete, ct);
_snapshotReceiving = false;
_metrics?.RecordSnapshot("received");
break;
default:
throw new InvalidOperationException(
@@ -288,6 +295,7 @@ internal sealed class SyncSession
var driftLimitMs = nowMs + (long)_options.MaxHlcDriftAhead.TotalMilliseconds;
var toApply = new List<OplogEntryRecord>(batch.Entries.Count);
var maxSeq = 0L;
var driftDeadLettered = 0;
foreach (var proto in batch.Entries)
{
@@ -310,6 +318,7 @@ internal sealed class SyncSession
if (_options.FailClosedOnDrift)
{
await DeadLetterDriftedAsync(entry, physicalMs, nowMs, now, ct);
driftDeadLettered++;
continue;
}
}
@@ -317,6 +326,10 @@ internal sealed class SyncSession
}
var result = await _applier.ApplyBatchAsync(toApply, ct);
_metrics?.RecordApplied(result.Applied);
_metrics?.RecordConflictsDiscarded(result.Discarded);
_metrics?.RecordDeadLettered(result.DeadLettered + driftDeadLettered);
_status?.SetLastSyncUtc(now);
var ackThru = Math.Max(result.AppliedThruSeq, maxSeq);
if (ackThru > result.AppliedThruSeq)
// Dead-lettered entries carried the batch's top seqs; advance the durable remote