HDDS-15412. Add per-volume push replication thread pools on DataNode.#10693
HDDS-15412. Add per-volume push replication thread pools on DataNode.#10693jojochuang wants to merge 3 commits into
Conversation
Introduce optional per-disk replication handler thread pools so slow push replication on one volume does not block replication on other disks. Pull replication was removed upstream; EC/reconcile tasks continue using the global pool. Includes runtime resize support and volume-failure cleanup. Co-authored-by: Cursor <cursoragent@cursor.com> Change-Id: I7cacd6fbeb435f5172fc67141a869cd4d1073811
Co-authored-by: Cursor <cursoragent@cursor.com> Change-Id: I87c4c544a24f99675ef68940b9412367d60c9b8e
There was a problem hiding this comment.
Pull request overview
This PR adds an optional per-data-volume execution model for push-based container replication on the datanode. When enabled, replication tasks are dispatched to a per-volume replication handler thread pool, aiming to prevent slow/backed-off disks from stalling push replication work on other disks.
Changes:
- Introduces
VolumeReplicationThreadPoolsand updatesReplicationSupervisorto route push replication to per-volume executors when configured. - Adds DN reconfiguration support for
hdds.datanode.replication.per.volume.streams.limitand documents it as reconfigurable. - Adds unit tests validating per-volume pool initialization, sizing/resizing, non-push behavior staying on the global pool, isolation across volumes, and volume-failure behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| hadoop-hdds/docs/content/feature/Reconfigurability.md | Documents the new per-volume streams limit as a reconfigurable DN setting. |
| hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/replication/TestReplicationSupervisor.java | Adds unit tests covering per-volume pool behavior, isolation, resize, and failure handling. |
| hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/replication/TestReplicationConfig.java | Adds unit coverage for default and validated per-volume config behavior. |
| hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/HddsDatanodeService.java | Registers and applies runtime reconfig for per-volume streams limit (when feature enabled). |
| hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/VolumeReplicationThreadPools.java | New helper managing per-volume ThreadPoolExecutors for push replication. |
| hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationTask.java | Adds isPushReplication() helper for routing decisions. |
| hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationSupervisor.java | Routes eligible push replication tasks to per-volume executors; adds pool shutdown/resize hooks and queue-size aggregation. |
| hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationServer.java | Adds new config keys and validation for per-volume enablement and per-volume stream limit. |
| hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/DatanodeStateMachine.java | Wires container/volume context into the supervisor and shuts down per-volume pools on volume failure events. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| queuedCounter.get(task.getMetricName()).incrementAndGet(); | ||
| ExecutorService taskExecutor = selectExecutor(task); | ||
| if (taskExecutor == null) { | ||
| LOG.warn("Rejected {} in ReplicationSupervisor: no replication handler " | ||
| + "thread pool available", task); | ||
| queuedCounter.get(task.getMetricName()).decrementAndGet(); | ||
| inFlight.remove(task); | ||
| decrementTaskCounter(task); | ||
| return; | ||
| } | ||
| taskExecutor.execute(new TaskRunner(task)); | ||
| } |
| try { | ||
| pool.shutdownNow(); | ||
| if (!pool.awaitTermination(3, TimeUnit.SECONDS)) { | ||
| LOG.warn("Per-volume replication thread pool for volume {} did not " |
| } finally { | ||
| supervisor.stop(); | ||
| } |
… is unavailable. When a push replication task cannot use its per-volume thread pool, route it to the global replication handler pool instead of rejecting it at queue time. Co-authored-by: Cursor <cursoragent@cursor.com> Change-Id: Ie3524488eecd667f9c16ee8f19bbb1774f4baa6a
jojochuang
left a comment
There was a problem hiding this comment.
Commit summary: 47251206749
Title: HDDS-15412. Fall back to global replication pool when per-volume pool is unavailable.
Problem
On HDDS-15412-by-cursor, when per-volume push replication thread pools were enabled, ReplicationSupervisor.addToQueue() would reject push-replication tasks if no per-volume executor was available (e.g. volume failed, pool shut down, or pool missing). That meant:
- The task never ran
- Logs said "rejecting task" even though rejection happened later in
addToQueue() - Behavior was inconsistent with the container-not-found path, which already fell back to the global pool
Change
ReplicationSupervisor.resolveVolumeExecutor()
- When the volume is null/failed or the per-volume pool is missing, return the global
executorinstead ofnull - Log a unified fallback message:
falling back to global replication handler thread pool
ReplicationSupervisor.addToQueue()
- Removed the
taskExecutor == nullrejection path (counter rollback + early return) - Tasks are always submitted via
selectExecutor(task).execute(...)
TestReplicationSupervisor.volumeFailureShutsDownPool
- After volume failure and per-volume pool shutdown, expects the task to run on the global pool and succeed (
getReplicationSuccessCount() == 1instead of0)
Behavior after this commit
| Scenario | Before | After |
|---|---|---|
| Per-volume pool shut down (volume failure) | Task rejected at queue time | Task runs on global pool |
volume.isFailed() |
Task rejected | Task runs on global pool |
| Container not found | Fallback to global | Unchanged |
SCM impact
No change to SCM notification — replicate commands still do not use CommandStatus. SCM still learns about replication via container reports and pending-op timeouts.
Files changed
ReplicationSupervisor.java— 25 lines changed (net −7)TestReplicationSupervisor.java— 1 assertion updated
| LOG.info("Shutting down per-volume replication thread pool for failed " | ||
| + "volume {}", volumeRoot); | ||
| try { | ||
| pool.shutdownNow(); |
There was a problem hiding this comment.
shutdownNow() discards the queued Runnables it returns. Those queued TaskRunners never execute, so the finally in ReplicationSupervisor.TaskRunner.run() that calls inFlight.remove(task) and decrements the counters never runs for them. Their (containerId, target) entries stay in inFlight permanently, and since addToQueue early returns when inFlight.add(task) is false, every later push replication command for those containers is dropped until the datanode restarts. This fires exactly in the failed volume path this feature adds.
Consider draining the returned list and running the same cleanup, for example have shutdownVolume return pool.shutdownNow() so shutdownFailedVolumePools can call back into the supervisor to remove each drained task from inFlight and fix the counters. Otherwise a single volume failure can strand re-replication for its containers.
|
|
||
| public void setPerVolumePoolSize(int newSize) { | ||
| if (volumePools != null) { | ||
| volumePools.setPoolSize(newSize); |
There was a problem hiding this comment.
This writes the raw newSize straight to the pools and skips the out-of-service scaling that resize() applies (see the per-volume block in resize). setReplicationMaxStreams sets the config then calls resize(state.get()), so the global pool gets newSize * outOfServiceFactor while decommissioning or in maintenance. Reconfiguring per.volume.streams.limit on a node that is already DECOMMISSIONING leaves the per-volume pools at the unscaled value, which is inconsistent with the global pool during the exact scenario this feature targets.
Suggest mirroring setReplicationMaxStreams: set replicationConfig.setPerVolumeStreamsLimit(newSize) first, then call resize(state.get()) to reuse the existing scaling path instead of calling setPoolSize directly.
| * @return true if this is a push replication task (source datanode sends | ||
| * container to target). | ||
| */ | ||
| public boolean isPushReplication() { |
There was a problem hiding this comment.
ReplicateContainerCommand calls Objects.requireNonNull(targetDatanode) in every constructor and in getFromProtobuf, so getTargetDatanode() is never null and this always returns true. The guard if (!replicationTask.isPushReplication()) { return executor; } in ReplicationSupervisor.selectExecutor is therefore unreachable today. If this is meant as a guard for a future pull style task, a short comment saying so would help; otherwise it can be dropped to avoid implying a branch that never runs.
What changes were proposed in this pull request?
HDDS-15412. Add per-volume push replication thread pools on DataNode.
Please describe your PR in detail:
When per.volume.enabled=true:
When per.volume.enabled=false (default): all tasks use the global pool, same as today.
Known limitations / follow-ups
What is the link to the Apache JIRA
https://issues.apache.org/jira/browse/HDDS-15412
How was this patch tested?
Made with Cursor Composer 2.5.
Unit tests added in TestReplicationSupervisor:
Feature disabled → no per-volume pools; legacy behavior
Init logging with volume paths
Per-volume pool size respected
Runtime resize via setPerVolumePoolSize()
Decommission scaling via outofservice.limit.factor
Non-push tasks (reconcile) use global pool when feature enabled
Cross-volume push isolation (blocking on volume A does not block volume B)
Volume failure shuts down pool and rejects subsequent push tasks
TestReplicationConfig: per-volume config validation.
All TestReplicationConfig + TestReplicationSupervisor tests pass.