Files
scadaproj/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb.Replication/LocalDbSyncService.cs
T

81 lines
3.2 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;
// 0 = idle, 1 = a stream is active. Interlocked-guarded across concurrent calls (singleton service).
private int _active;
public LocalDbSyncService(ILocalDb db, IOptions<ReplicationOptions> options, ILoggerFactory loggerFactory)
{
_db = SyncSessionFactory.RequireSqlite(db, nameof(LocalDbSyncService));
_options = options.Value;
_loggerFactory = loggerFactory;
_logger = loggerFactory.CreateLogger<LocalDbSyncService>();
}
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);
try
{
await session.RunAsync(duplex, linkedCts.Token);
}
finally
{
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);
}
}
}