fix(localdb): adapter review fixes — deterministic pump teardown, always-hosted initiator, backoff clamp
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
This commit is contained in:
@@ -12,7 +12,8 @@ namespace ZB.MOM.WW.LocalDb.Replication;
|
||||
/// <summary>
|
||||
/// The initiating (client) side of replication: a hosted service that dials the configured peer,
|
||||
/// runs the symmetric <see cref="SyncSession"/> over the outbound bidirectional stream, and
|
||||
/// reconnects with jittered exponential backoff whenever the session ends or faults.
|
||||
/// reconnects with jittered exponential backoff whenever the session ends or faults. Always
|
||||
/// registered; on a passive node (no <see cref="ReplicationOptions.PeerAddress"/>) it idles.
|
||||
/// </summary>
|
||||
public sealed class SyncBackgroundService : BackgroundService
|
||||
{
|
||||
@@ -34,10 +35,7 @@ public sealed class SyncBackgroundService : BackgroundService
|
||||
|
||||
public SyncBackgroundService(ILocalDb db, IOptions<ReplicationOptions> options, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_db = db as SqliteLocalDb
|
||||
?? throw new InvalidOperationException(
|
||||
$"SyncBackgroundService requires the ILocalDb registered by AddZbLocalDb (a {nameof(SqliteLocalDb)}); " +
|
||||
$"got {db.GetType().FullName}.");
|
||||
_db = SyncSessionFactory.RequireSqlite(db, nameof(SyncBackgroundService));
|
||||
_options = options.Value;
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = loggerFactory.CreateLogger<SyncBackgroundService>();
|
||||
@@ -45,6 +43,12 @@ public sealed class SyncBackgroundService : BackgroundService
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_options.PeerAddress))
|
||||
{
|
||||
_logger.LogInformation("passive node — no peer configured, sync initiator idle");
|
||||
return;
|
||||
}
|
||||
|
||||
var backoff = InitialBackoff;
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -65,12 +69,30 @@ public sealed class SyncBackgroundService : BackgroundService
|
||||
headers.Add("Authorization", $"Bearer {_options.ApiKey}");
|
||||
|
||||
using var call = client.Sync(headers, cancellationToken: stoppingToken);
|
||||
var (duplex, readerTask) = GrpcSyncDuplex.Create(call.ResponseStream, call.RequestStream, stoppingToken);
|
||||
// The linked source exists so the reader pump is deterministically torn down on
|
||||
// EVERY exit path — including a session fault for a local (non-transport) reason,
|
||||
// where the stopping token never fires and the pump would otherwise keep reading a
|
||||
// stream this iteration has abandoned.
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
||||
var (duplex, readerTask) = GrpcSyncDuplex.Create(call.ResponseStream, call.RequestStream, linkedCts.Token);
|
||||
var session = SyncSessionFactory.Create(_db, _options, _loggerFactory);
|
||||
|
||||
await session.RunAsync(duplex, stoppingToken);
|
||||
await call.RequestStream.CompleteAsync();
|
||||
await readerTask;
|
||||
try
|
||||
{
|
||||
await session.RunAsync(duplex, linkedCts.Token);
|
||||
await call.RequestStream.CompleteAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
linkedCts.Cancel();
|
||||
try
|
||||
{
|
||||
await readerTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Sync reader pump ended during teardown.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -107,6 +129,8 @@ public sealed class SyncBackgroundService : BackgroundService
|
||||
break;
|
||||
}
|
||||
|
||||
// Overflow-safe without checked math: the validator caps ReconnectBackoffMax at 1 day
|
||||
// and backoff never exceeds it, so doubling stays far below long.MaxValue ticks.
|
||||
backoff = TimeSpan.FromTicks(Math.Min(backoff.Ticks * 2, _options.ReconnectBackoffMax.Ticks));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user