Environment
- Curvine v0.5.0-alpha (also verified present on current
main as of ff777c31), 3 masters (Raft HA) + 8 workers
block_replication_enabled = true, block_replication_concurrency_limit = 1000 (default)
Symptom
After a worker node reboot took longer than worker_lost_interval (10 min) — the worker was declared lost, re-replication kicked in — the master's replication scheduler deadlocked permanently:
replication_inflight_number froze at exactly 1000 (= block_replication_concurrency_limit)
replication_staging_number froze at 6377
replication_failure_count stayed 0
- Zero replication log activity on the master from that point on — no
Successfully replicated, no errors, nothing
- The worker later came back and re-registered with its original worker_id (cluster fully healthy, live=8/lost=0) — the queue stayed frozen for 80+ minutes, and remained frozen even after we deleted every file it was supposed to replicate
- Only a master restart (failover to a fresh Active) cleared it: metrics went back to 0/0 on the new leader
Root cause (source analysis, curvine-master/src/master/replication/master_replication_manager.rs @ v0.5.0-alpha)
The scheduler is a single serial loop:
recv.recv().await
→ replication_semaphore.acquire_owned().await // L94, no timeout
→ replicate_block(block_id, permit).await // L101
On successful submission, the OwnedSemaphorePermit is moved into an InflightReplicationJob and stored in the inflight_blocks DashMap (L198–207). The only code path that releases the permit is finish_replicated_block() (L249–268), which is triggered only by the source worker sending ReportBlockReplicationResult back.
If a source worker restarts (or its process dies) while jobs are inflight, those reports never arrive. There is:
- No timeout / reaper on inflight jobs.
inflight_blocks has exactly 4 references in the codebase (L46/77/198/249) — insert and remove only, no periodic sweep. Once 1000 permits leak, acquire_owned().await at L94 blocks forever, recv is never polled again, and the scheduler is silently dead.
block_replication_retry_interval is dead config. It's defined (master_conf.rs, default "5s") and has an accessor block_replication_retry_interval_ms(), but that accessor has zero callers. Retry is not implemented — the code even says so: // todo: retry on failure of block replication (L243) and // todo: error handling (master_replication_handler.rs:47).
- Worker re-registration does not clean up its pending inflight jobs. The registration/heartbeat paths never touch
MasterReplicationManager, so a worker coming back with the same worker_id does not unblock anything.
- Worker-side submission is fire-and-forget.
worker_replication_handler.rs:37-50 accept_job returns success: true as soon as the job is queued in the worker's in-memory mpsc. A worker restart evaporates that queue, and the master-side inflight entry leaks. Additionally, on the worker, report_job() early-returns with err_box! when job.storage_type is None (source block unreadable), so in that case the result report is never even sent — same leak, fully silent.
Also worth noting: report_under_replicated_blocks() (L212–240) does no dedup — the _worker_id param is ignored, blocks already inflight or already queued get re-enqueued, and staging_number only decrements on the success path (L206), never on the 6 early-return error paths in replicate_block() — so the staging gauge drifts upward and stops reflecting the real queue depth.
Reproduction
- Cluster with
block_replication_enabled = true, replicas=2 data spread across workers.
- Reboot a worker node so that the outage exceeds
worker_lost_interval (any GPU node with slow driver init easily exceeds 10 min).
- Watch
replication_inflight_number climb to block_replication_concurrency_limit and freeze there, with no further replication activity, even after the worker rejoins.
Impact
Any environment where a worker holding many blocks goes down non-gracefully (node reboot, OOM, power loss) will permanently disable re-replication cluster-wide until the active master is restarted. Since node reboots routinely exceed the 10-minute lost threshold on GPU machines, this makes the self-healing guarantee of block_replication_enabled unreliable in practice. Data itself is not damaged.
Suggested fixes
- Add a timeout/reaper for
inflight_blocks entries (wire up the existing-but-unused block_replication_retry_interval), releasing the permit and either re-queueing or counting the block as failed.
- On worker lost/re-registration, cancel/clean that worker's inflight jobs.
- Worker: always send a result report, including when
storage_type is None / the source block can't be opened (report failure instead of silently dropping).
- Decrement
staging_number on all replicate_block() early-return paths, and dedup enqueues against inflight_blocks + queued set so the gauges stay meaningful.
Recovery workaround (for anyone hitting this)
Restart the active master (or force a failover). The scheduler state is purely in-memory; the new Active starts with a clean queue and the post-registration full block report re-detects genuinely under-replicated blocks.
Environment
mainas of ff777c31), 3 masters (Raft HA) + 8 workersblock_replication_enabled = true,block_replication_concurrency_limit = 1000(default)Symptom
After a worker node reboot took longer than
worker_lost_interval(10 min) — the worker was declared lost, re-replication kicked in — the master's replication scheduler deadlocked permanently:replication_inflight_numberfroze at exactly 1000 (=block_replication_concurrency_limit)replication_staging_numberfroze at 6377replication_failure_countstayed 0Successfully replicated, no errors, nothingRoot cause (source analysis,
curvine-master/src/master/replication/master_replication_manager.rs@ v0.5.0-alpha)The scheduler is a single serial loop:
On successful submission, the
OwnedSemaphorePermitis moved into anInflightReplicationJoband stored in theinflight_blocksDashMap (L198–207). The only code path that releases the permit isfinish_replicated_block()(L249–268), which is triggered only by the source worker sendingReportBlockReplicationResultback.If a source worker restarts (or its process dies) while jobs are inflight, those reports never arrive. There is:
inflight_blockshas exactly 4 references in the codebase (L46/77/198/249) — insert and remove only, no periodic sweep. Once 1000 permits leak,acquire_owned().awaitat L94 blocks forever,recvis never polled again, and the scheduler is silently dead.block_replication_retry_intervalis dead config. It's defined (master_conf.rs, default "5s") and has an accessorblock_replication_retry_interval_ms(), but that accessor has zero callers. Retry is not implemented — the code even says so:// todo: retry on failure of block replication(L243) and// todo: error handling(master_replication_handler.rs:47).MasterReplicationManager, so a worker coming back with the same worker_id does not unblock anything.worker_replication_handler.rs:37-50accept_jobreturnssuccess: trueas soon as the job is queued in the worker's in-memory mpsc. A worker restart evaporates that queue, and the master-side inflight entry leaks. Additionally, on the worker,report_job()early-returns witherr_box!whenjob.storage_typeisNone(source block unreadable), so in that case the result report is never even sent — same leak, fully silent.Also worth noting:
report_under_replicated_blocks()(L212–240) does no dedup — the_worker_idparam is ignored, blocks already inflight or already queued get re-enqueued, andstaging_numberonly decrements on the success path (L206), never on the 6 early-return error paths inreplicate_block()— so the staging gauge drifts upward and stops reflecting the real queue depth.Reproduction
block_replication_enabled = true, replicas=2 data spread across workers.worker_lost_interval(any GPU node with slow driver init easily exceeds 10 min).replication_inflight_numberclimb toblock_replication_concurrency_limitand freeze there, with no further replication activity, even after the worker rejoins.Impact
Any environment where a worker holding many blocks goes down non-gracefully (node reboot, OOM, power loss) will permanently disable re-replication cluster-wide until the active master is restarted. Since node reboots routinely exceed the 10-minute lost threshold on GPU machines, this makes the self-healing guarantee of
block_replication_enabledunreliable in practice. Data itself is not damaged.Suggested fixes
inflight_blocksentries (wire up the existing-but-unusedblock_replication_retry_interval), releasing the permit and either re-queueing or counting the block as failed.storage_typeisNone/ the source block can't be opened (report failure instead of silently dropping).staging_numberon allreplicate_block()early-return paths, and dedup enqueues againstinflight_blocks+ queued set so the gauges stay meaningful.Recovery workaround (for anyone hitting this)
Restart the active master (or force a failover). The scheduler state is purely in-memory; the new Active starts with a clean queue and the post-registration full block report re-detects genuinely under-replicated blocks.