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; /// /// The passive (server) side of replication: a gRPC service that accepts an inbound bidirectional /// Sync stream and runs the symmetric over it. Registered as a /// singleton so its single-stream guard spans concurrent calls (a node syncs with exactly one peer). /// public sealed class LocalDbSyncService : LocalDbSync.LocalDbSyncBase { private readonly SqliteLocalDb _db; private readonly ReplicationOptions _options; private readonly ILoggerFactory _loggerFactory; private readonly ILogger _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 options, ILoggerFactory loggerFactory, LocalDbMetrics? metrics = null, ISyncStatus? status = null) { _db = SyncSessionFactory.RequireSqlite(db, nameof(LocalDbSyncService)); _options = options.Value; _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); _metrics = metrics; _status = status as SyncStatus; } public override async Task Sync( IAsyncStreamReader requestStream, IServerStreamWriter 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); } } }