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; } = ""; /// API key presented to / expected from the peer; null disables key auth. 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); /// 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; } }