be4ca76f5f
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
91 lines
3.7 KiB
C#
91 lines
3.7 KiB
C#
using Grpc.Core;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.LocalDb.Contracts;
|
|
using ZB.MOM.WW.LocalDb.Internal;
|
|
using ZB.MOM.WW.LocalDb.Replication.Internal;
|
|
|
|
namespace ZB.MOM.WW.LocalDb.Replication;
|
|
|
|
/// <summary>
|
|
/// The passive (server) side of replication: a gRPC service that accepts an inbound bidirectional
|
|
/// <c>Sync</c> stream and runs the symmetric <see cref="SyncSession"/> over it. Registered as a
|
|
/// singleton so its single-stream guard spans concurrent calls (a node syncs with exactly one peer).
|
|
/// </summary>
|
|
public sealed class LocalDbSyncService : LocalDbSync.LocalDbSyncBase
|
|
{
|
|
private readonly SqliteLocalDb _db;
|
|
private readonly ReplicationOptions _options;
|
|
private readonly ILoggerFactory _loggerFactory;
|
|
private readonly ILogger<LocalDbSyncService> _logger;
|
|
private readonly LocalDbMetrics? _metrics;
|
|
private readonly SyncStatus? _status;
|
|
|
|
// 0 = idle, 1 = a stream is active. Interlocked-guarded across concurrent calls (singleton service).
|
|
private int _active;
|
|
|
|
// status is taken as the public ISyncStatus (SyncStatus is internal and cannot appear in a
|
|
// public signature); DI always supplies the concrete SyncStatus this cast recovers.
|
|
public LocalDbSyncService(
|
|
ILocalDb db, IOptions<ReplicationOptions> options, ILoggerFactory loggerFactory,
|
|
LocalDbMetrics? metrics = null, ISyncStatus? status = null)
|
|
{
|
|
_db = SyncSessionFactory.RequireSqlite(db, nameof(LocalDbSyncService));
|
|
_options = options.Value;
|
|
_loggerFactory = loggerFactory;
|
|
_logger = loggerFactory.CreateLogger<LocalDbSyncService>();
|
|
_metrics = metrics;
|
|
_status = status as SyncStatus;
|
|
}
|
|
|
|
public override async Task Sync(
|
|
IAsyncStreamReader<SyncMessage> requestStream,
|
|
IServerStreamWriter<SyncMessage> responseStream,
|
|
ServerCallContext context)
|
|
{
|
|
if (Interlocked.CompareExchange(ref _active, 1, 0) != 0)
|
|
throw new RpcException(new Status(StatusCode.AlreadyExists, "a sync stream is already active"));
|
|
|
|
try
|
|
{
|
|
// 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
|
|
// call token never fires and the pump would otherwise keep reading a stream whose
|
|
// handler has exited.
|
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken);
|
|
var (duplex, readerTask) = GrpcSyncDuplex.Create(requestStream, responseStream, linkedCts.Token);
|
|
var session = SyncSessionFactory.Create(_db, _options, _loggerFactory, _metrics, _status);
|
|
_status?.SetConnected(true);
|
|
try
|
|
{
|
|
await session.RunAsync(duplex, linkedCts.Token);
|
|
}
|
|
finally
|
|
{
|
|
_status?.SetConnected(false);
|
|
linkedCts.Cancel();
|
|
try
|
|
{
|
|
await readerTask;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogDebug(ex, "Sync reader pump ended during teardown.");
|
|
}
|
|
}
|
|
}
|
|
catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested)
|
|
{
|
|
// Client disconnect / server shutdown: the normal end of a long-lived streaming RPC.
|
|
}
|
|
catch (LocalDbSchemaMismatchException ex)
|
|
{
|
|
throw new RpcException(new Status(StatusCode.FailedPrecondition, ex.Message));
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Exchange(ref _active, 0);
|
|
}
|
|
}
|
|
}
|