namespace ZB.MOM.WW.LocalDb.Replication; /// /// Configuration for the bidirectional 2-node replication engine. All durations are wall-clock UTC. /// public sealed class ReplicationOptions { /// gRPC address of the peer to dial. Required for the initiator role (validated in the sync host). public string PeerAddress { get; set; } = ""; /// /// Shared key that authenticates the pair partner. Set the SAME value on both nodes. The initiator /// sends it as Authorization: Bearer <key>; since 0.3.6 the passive /// also requires it (constant-time compare) and refuses any other /// stream with Unauthenticated, before the handshake and before displacing the current /// stream. Null, empty or whitespace disables key auth on both sides: the initiator sends no /// header and the endpoint accepts any caller (a warning is logged at startup), so protect it some /// other way — network isolation or a host interceptor. /// public string? ApiKey { get; set; } /// How often the change pump flushes pending oplog deltas to the peer. public TimeSpan FlushInterval { get; set; } = TimeSpan.FromMilliseconds(250); /// /// Secondary, row-count cap on an outbound batch (and the snapshot streamer's DB page size). /// is the primary cap. /// /// /// Historically this was the ONLY cap, which made it a proxy for a byte budget that consumers /// had to tune by hand against their widest row — ScadaBridge pinned it to 16 because a 70 KB /// config_json times the 500 default is ~35 MB against a 4 MB gRPC limit. With /// doing that job directly, this is back to being what its name /// says: a bound on how many rows one message may carry. /// public int MaxBatchSize { get; set; } = 500; /// /// Primary cap on an outbound batch: the summed serialized size of the entries in one /// DeltaBatch / SnapshotBatch message, in bytes. Default 2 MB. /// /// /// /// Sized at half the default 4 MB gRPC receive limit, so the message framing, the oneof wrapper /// and any per-entry overhead the estimate does not model all fit inside the margin. /// /// /// A single row larger than the budget is still sent, alone. Skipping it would stall the /// stream permanently on one oversized row; sending it gives the transport the chance to accept /// it (gRPC limits are configurable) and, if it refuses, a loud transport error naming the size /// rather than silent non-convergence. /// /// /// Wire compatibility: none required. Batch size is a sender-side choice the protocol /// already leaves free — DeltaBatch/SnapshotBatch carry a repeated field with no /// declared count — so a node on this version interoperates with a peer on any earlier one in /// both directions, with no proto change and no lib_schema_version bump. (ScadaBridge /// site pairs stop and start together regardless; this simply does not depend on it.) /// /// public int MaxBatchBytes { get; set; } = 2 * 1024 * 1024; /// Backlog ceiling: exceeding it flags a snapshot resync and prunes the oldest oplog rows. public long MaxOplogRows { get; set; } = 1_000_000; /// Age ceiling: an unpruned oplog entry older than this flags a snapshot resync. public TimeSpan MaxOplogAge { get; set; } = TimeSpan.FromDays(7); /// How long deleted-row tombstones are retained before pruning. public TimeSpan TombstoneRetention { get; set; } = TimeSpan.FromDays(7); /// Upper bound on the exponential reconnect backoff. public TimeSpan ReconnectBackoffMax { get; set; } = TimeSpan.FromSeconds(60); /// /// How long the initiator's HTTP/2 connection may sit without a keepalive PING before one is /// sent. Default 60s. /// /// /// /// A sync stream is long-lived and, on a converged pair, almost entirely silent — which is /// exactly the shape a half-open TCP connection hides behind. Without pings, a hard crash or a /// partition of one node leaves the other holding a stream that will never produce another byte /// and never fail either: the initiator sits in a session it believes is healthy, and the /// passive node's handler sits inside its single-stream guard, refusing the reconnect that would /// heal the pair. Pings turn that into a detected failure on both ends. /// /// /// Sent with KeepAlivePingPolicy.Always, so they flow on an idle connection too — an /// active-streams-only policy would not fire in precisely the silent-pair case above. Safe over /// h2c: a PING is an HTTP/2 frame, nothing to do with TLS. /// /// public TimeSpan KeepAlivePingDelay { get; set; } = TimeSpan.FromSeconds(60); /// /// How long the initiator waits for a keepalive PING ack before tearing the connection down. /// Default 20s. Detection therefore lands within roughly /// + this. /// public TimeSpan KeepAlivePingTimeout { get; set; } = TimeSpan.FromSeconds(20); /// How often the maintenance service enforces the oplog caps and prunes expired tombstones. public TimeSpan MaintenanceInterval { get; set; } = TimeSpan.FromSeconds(60); /// /// Ceiling on the snapshot rows a receiver holds in memory while their foreign-key parent has not /// arrived yet. Default 100,000. Past it the OLDEST held rows are applied once more and, if their /// parent is still missing, dead-lettered. /// /// /// /// Since 0.3.6 a snapshot row that fails only its FK check is deferred and retried as the rest of /// the snapshot lands, instead of being dead-lettered. A 0.3.6 sender streams parent tables first, /// so in practice only self-referencing rows whose parent sorts later wait; but an OLDER sender /// streams tables by name, and a child table that sorts before its parent then defers every one of /// its rows. This bounds that memory. /// /// /// A spilled row is lost to this snapshot, exactly like an orphan dead-lettered at /// SnapshotComplete: the sender's debt clears on the ack, so it is not re-sent. It is /// visible in __localdb_dead_letter and in localdb.sync.dead_lettered. Upgrade the /// sender (parents first) or raise this cap if spills appear. /// /// public int MaxSnapshotDeferredRows { get; set; } = 100_000; /// Maximum tolerated HLC drift where a peer's clock runs ahead of ours. public TimeSpan MaxHlcDriftAhead { get; set; } = TimeSpan.FromMinutes(5); /// /// When true, entries whose HLC physical time exceeds are /// dead-lettered individually (the rest of the batch still applies); when false, drifted /// entries are applied and a single warning is logged per session. /// public bool FailClosedOnDrift { get; set; } }