Skip to content

HDDS-15412. Add per-volume push replication thread pools on DataNode.#10693

Draft
jojochuang wants to merge 3 commits into
apache:masterfrom
jojochuang:HDDS-15412-by-cursor
Draft

HDDS-15412. Add per-volume push replication thread pools on DataNode.#10693
jojochuang wants to merge 3 commits into
apache:masterfrom
jojochuang:HDDS-15412-by-cursor

Conversation

@jojochuang

Copy link
Copy Markdown
Contributor

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:

  • Adds optional per-volume replication handler thread pools on the DataNode for push-based container replication. When enabled, each data volume gets its own thread pool, so a slow or backed-off disk no longer blocks push replication for containers on other disks.
  • This targets the EC decommission scenario described in HDDS-15412: with a single global pool, replication starts fast on all disks, then slows until only one disk stays busy while others idle.
  • Default is off (hdds.datanode.replication.per.volume.enabled=false), so existing clusters keep current behavior unless the feature is explicitly enabled.

When per.volume.enabled=true:

  • Push replication is routed to a per-volume thread pool (sized by hdds.datanode.replication.per.volume.streams.limit, default 1)
  • EC reconstruction and reconcile tasks continue using the global pool (streams.limit)
  • Volume is resolved from the source container via ContainerSet → container.getContainerData().getVolume()
  • On volume failure, that volume’s pool is shut down and new push tasks for containers on that volume are rejected
    When per.volume.enabled=false (default): all tasks use the global pool, same as today.

Known limitations / follow-ups

  • per.volume.enabled requires DN restart (not reconfigurable)
  • Reconfig of per.volume.streams.limit does not re-apply outofservice.limit.factor on decommissioning nodes (node state transitions do)
  • Rejected tasks are logged but not counted in replication metrics
  • No per-volume metrics yet (queue depth, active threads)
  • No integration test in this PR (unit tests only)

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.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 VolumeReplicationThreadPools and updates ReplicationSupervisor to route push replication to per-volume executors when configured.
  • Adds DN reconfiguration support for hdds.datanode.replication.per.volume.streams.limit and 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.

Comment on lines +312 to +323
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));
}
Comment on lines +91 to +94
try {
pool.shutdownNow();
if (!pool.awaitTermination(3, TimeUnit.SECONDS)) {
LOG.warn("Per-volume replication thread pool for volume {} did not "
Comment on lines +1130 to +1132
} 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 jojochuang left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 executor instead of null
  • Log a unified fallback message: falling back to global replication handler thread pool

ReplicationSupervisor.addToQueue()

  • Removed the taskExecutor == null rejection 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() == 1 instead of 0)

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants