You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
During a bulk table migration on a 4-node cluster, the replication stream for one database silently dropped ~700k of 803k writes on all three subscribing peers. Every health surface looked fine: sockets connected, backpressure 0, lastReceivedRemoteTime current, and new writes kept replicating normally after the incident window. No base-copy/resync was triggered and no error or warning was logged on any node. The gap was only discovered by manually comparing SELECT COUNT(*) across nodes.
This is data-loss-grade divergence with a fully healthy-looking cluster — a concrete field case for #432 (cursor correctness & divergence detection).
Database render_service, table Target (string PK url), replicated (no residency pinning on this table)
Timeline (2026-08-02, ~17:00–19:30Z)
A component migration on yc0 bulk-wrote 803,324 rows into render_service.Target (chunked writes over ~30 min).
Concurrently, duplicate sweeps (same component, other workers) raced the migration and died with repeated "After 40 retries, unable to commit transaction" — i.e., the table was under a sustained write-conflict storm while the subscriber stream was consuming the same backlog.
Peers ended up with 103,658 rows each. Delivery stopped mid-backlog, roughly coincident with the conflict storm.
Writes issued after the window replicated instantly and correctly (verified with a control-row write that appeared on all peers within seconds).
Key observations
The skipped range is not a clean prefix. Peer copies had holes: rows at higher PK offsets were present while earlier rows were missing — consistent with the subscriber advancing its resume cursor past transactions it never applied (or the sender skipping log segments), rather than a simple disconnect.
cluster_status on all nodes: connected: true, backpressure 0, lastReceivedRemoteTime current — before, during, and after.
So the cluster considered the cursors valid while ~700k committed, audited transactions between them and head were never delivered/applied — and nothing detected the divergence.
Impact
3 of 4 nodes served a 103k-row view of an 803k-row table for ~2.5 h. Downstream logic that treats a local miss as "row does not exist" took destructive actions (deleting sibling schedule rows). Nothing in Harper surfaced the problem; discovery was a human noticing a wrong dashboard count.
Suspected area / repro hypothesis
Bulk sequential writes to one table + concurrent conflicting writers on the same keys (enough to produce "After 40 retries, unable to commit transaction" aborts) while ≥1 subscriber is consuming the resulting backlog over the per-database stream. Suspect the interplay between the conflict/retry machinery's audit entries and the subscriber's cursor advance — the cursor appears to have jumped over a large undelivered span while remaining "valid" to the leader's retention check.
Recovery that worked (for the record)
Re-emitting every row from the holder via ops-API upsert with the complete record (161 × 5k chunks) pushed the backlog through the same stream successfully; peers converged to 803,324 with zero retries. Note for anyone repeating this: update/upsert on an existing row audit as a patch delta of only the supplied fields (core/resources/Table.js ~1887), and a subscriber missing the base row stores the patch body verbatim (tracked.js updateAndFreeze: if (!target) return changes) — so a partial-field "touch" re-emit would have materialized field-stripped stubs on the peers. Re-emits must carry every field.
Asks
Root-cause the cursor advance across an undelivered span under write-conflict storms.
Summary
During a bulk table migration on a 4-node cluster, the replication stream for one database silently dropped ~700k of 803k writes on all three subscribing peers. Every health surface looked fine: sockets connected, backpressure 0,
lastReceivedRemoteTimecurrent, and new writes kept replicating normally after the incident window. No base-copy/resync was triggered and no error or warning was logged on any node. The gap was only discovered by manually comparingSELECT COUNT(*)across nodes.This is data-loss-grade divergence with a fully healthy-looking cluster — a concrete field case for #432 (cursor correctness & divergence detection).
Environment
harperfast/harper-pro:5.1.26)prerender.kohls.harperfabric.com(yc0-us-central-1 / cd5-us-southeast-1 / e9v-us-east-1 / v3t-us-sea-1)render_service, tableTarget(string PKurl), replicated (no residency pinning on this table)Timeline (2026-08-02, ~17:00–19:30Z)
render_service.Target(chunked writes over ~30 min).Key observations
cluster_statuson all nodes:connected: true, backpressure 0,lastReceivedRemoteTimecurrent — before, during, and after.replication/replicationConnection.js~line 3046 in the 5.1.26 dist, never fired). No full-copy triggered either (the no-resume-cursor path near ~4160, where the rapid-reconnect stress: follower permanently loses replication backlog after restart churn (data loss; fast-skip ruled out) #426 comment lives, correctly did not apply — cursors existed and looked "valid").Impact
3 of 4 nodes served a 103k-row view of an 803k-row table for ~2.5 h. Downstream logic that treats a local miss as "row does not exist" took destructive actions (deleting sibling schedule rows). Nothing in Harper surfaced the problem; discovery was a human noticing a wrong dashboard count.
Suspected area / repro hypothesis
Bulk sequential writes to one table + concurrent conflicting writers on the same keys (enough to produce "After 40 retries, unable to commit transaction" aborts) while ≥1 subscriber is consuming the resulting backlog over the per-database stream. Suspect the interplay between the conflict/retry machinery's audit entries and the subscriber's cursor advance — the cursor appears to have jumped over a large undelivered span while remaining "valid" to the leader's retention check.
Recovery that worked (for the record)
Re-emitting every row from the holder via ops-API
upsertwith the complete record (161 × 5k chunks) pushed the backlog through the same stream successfully; peers converged to 803,324 with zero retries. Note for anyone repeating this:update/upserton an existing row audit as a patch delta of only the supplied fields (core/resources/Table.js~1887), and a subscriber missing the base row stores the patch body verbatim (tracked.js updateAndFreeze:if (!target) return changes) — so a partial-field "touch" re-emit would have materialized field-stripped stubs on the peers. Re-emits must carry every field.Asks
Filed by an agent (Claude) from the incident session; evidence gathered live from the cluster (UDS ops, docker logs, 5.1.26 dist source).