feat(localdb): gRPC sync adapters, background service, replication DI
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
using System.Threading.Channels;
|
||||
using Grpc.Core;
|
||||
using ZB.MOM.WW.LocalDb.Contracts;
|
||||
|
||||
namespace ZB.MOM.WW.LocalDb.Replication.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Adapts a gRPC duplex stream (an inbound reader + an outbound writer) to the transport-agnostic
|
||||
/// <see cref="SyncDuplex"/> a <see cref="SyncSession"/> runs over. Both the passive service and the
|
||||
/// initiating client build one of these over their respective stream halves.
|
||||
/// </summary>
|
||||
internal static class GrpcSyncDuplex
|
||||
{
|
||||
/// <summary>
|
||||
/// Wires <paramref name="reader"/> into an unbounded inbox channel drained by a background loop,
|
||||
/// and exposes <paramref name="writer"/> as the duplex send. Returns the duplex plus the reader
|
||||
/// loop's task so the caller can observe it. On clean stream end the inbox channel is completed;
|
||||
/// on a reader fault it is completed WITH the exception — the session's handshake and receive
|
||||
/// phases both await the inbox, so terminating it is what unblocks them when the call dies (they
|
||||
/// would otherwise hang on a dead stream).
|
||||
/// </summary>
|
||||
public static (SyncDuplex Duplex, Task ReaderTask) Create(
|
||||
IAsyncStreamReader<SyncMessage> reader,
|
||||
IAsyncStreamWriter<SyncMessage> writer,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var inbox = Channel.CreateUnbounded<SyncMessage>(new UnboundedChannelOptions { SingleWriter = true });
|
||||
var readerTask = PumpInboundAsync(reader, inbox.Writer, ct);
|
||||
var duplex = new SyncDuplex
|
||||
{
|
||||
// A plain delegate is safe: the session's single writer task is the only caller of Send,
|
||||
// so this stream writer never sees the concurrent writes IServerStreamWriter /
|
||||
// IClientStreamWriter both prohibit. The gRPC WriteAsync(message, ct) overload honors the
|
||||
// token even when flow-control blocked, satisfying SyncDuplex.Send's cancellation contract.
|
||||
Send = (message, token) => writer.WriteAsync(message, token),
|
||||
Inbox = inbox.Reader,
|
||||
};
|
||||
return (duplex, readerTask);
|
||||
}
|
||||
|
||||
private static async Task PumpInboundAsync(
|
||||
IAsyncStreamReader<SyncMessage> reader, ChannelWriter<SyncMessage> inbox, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (await reader.MoveNext(ct))
|
||||
inbox.TryWrite(reader.Current);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Fault the inbox so the session's inbox awaiters observe the transport failure (a
|
||||
// cancelled call surfaces here as OperationCanceledException). Swallowed on this task:
|
||||
// the failure travels to the session through the channel, not by faulting the reader task.
|
||||
inbox.TryComplete(ex);
|
||||
return;
|
||||
}
|
||||
inbox.TryComplete();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user