Skip to content

Fix throttle removal verification hang when a default replication throttle is set - #2385

Open
acgtun wants to merge 1 commit into
linkedin:mainfrom
acgtun:fix-throttle-verify-dynamic-default
Open

Fix throttle removal verification hang when a default replication throttle is set#2385
acgtun wants to merge 1 commit into
linkedin:mainfrom
acgtun:fix-throttle-verify-dynamic-default

Conversation

@acgtun

@acgtun acgtun commented Jul 5, 2026

Copy link
Copy Markdown

Summary

  1. Why: When clearing throttles after replica movements, ReplicationThrottleHelper deletes the per-broker leader/follower.replication.throttled.rate configs and then polls describeConfigs until the configs read back as absent. If a cluster-wide dynamic default throttle exists (set via kafka-configs --alter --entity-type brokers --entity-default), the deleted per-broker config resolves to the default value, so describeConfigs keeps returning a non-null entry, configsEqual keeps returning false, and verification never converges. Worse, the retry helper (CruiseControlMetricsUtils.retry) doubles the sleep between attempts with no cap (5s * 2^n), so the executor thread ends up parked in exponentially growing sleeps — attempt 13 alone sleeps ~11 hours; exhausting 30 retries would take centuries.
  2. What: (a) configsEqual already skipped the comparison when a deleted config resolves to STATIC_BROKER_CONFIG; extend the same treatment to DYNAMIC_DEFAULT_BROKER_CONFIG and DEFAULT_CONFIG — seeing a value from one of these sources after a DELETE means the per-entity delete itself succeeded and the remaining value is inherited from a layer Cruise Control does not manage. (b) Cap the verification retry backoff at 30s per attempt, so a genuinely failing verification exhausts its 30 retries in ~15 minutes and fails loudly with the existing IllegalStateException instead of parking the executor for hours/days.

Expected Behavior

After a movement batch completes, throttle removal verification converges and the execution proceeds to the next batch, regardless of whether a cluster-wide default replication throttle is set.

Actual Behavior

Throttle removal verification never converges when an --entity-default replication throttle exists. The executor thread parks in exponentially growing sleeps inside waitForConfigs, the execution appears hung (no further movements dispatched), and new executions are rejected with Cannot start a new execution while there is an ongoing execution.

Steps to Reproduce

  1. Set a cluster-wide default replication throttle: kafka-configs --alter --entity-type brokers --entity-default --add-config leader.replication.throttled.rate=...,follower.replication.throttled.rate=...
  2. Trigger a Cruise Control execution that moves replicas (e.g. rebalance?dryrun=false).
  3. After the first movement batch completes, clearThrottles deletes the per-broker rates, and verification loops forever because the deleted configs resolve to the dynamic default layer.

Known Workarounds

Remove the --entity-default replication throttle before running an execution, or restart Cruise Control to abort the stuck execution.

Additional evidence

  1. Environment: observed in production on a 200+ broker cluster — the executor finished its first batch of movements, then made no progress for hours.
  2. Thread dump of the stuck executor:
"ProposalExecutor-0" TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep
    at com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsUtils.retry
    at com.linkedin.kafka.cruisecontrol.executor.ReplicationThrottleHelper.waitForConfigs
    ...
    at com.linkedin.kafka.cruisecontrol.executor.ReplicationThrottleHelper.clearThrottles
    at com.linkedin.kafka.cruisecontrol.executor.Executor$ProposalExecutionRunnable.interBrokerMoveReplicas
  1. Broker configs showing the deleted per-broker rate resolving to the --entity-default layer:
leader.replication.throttled.rate=... synonyms={DYNAMIC_DEFAULT_BROKER_CONFIG:leader.replication.throttled.rate=...}
  1. Testing: extended testConfigsEqual with cases for DYNAMIC_DEFAULT_BROKER_CONFIG and DEFAULT_CONFIG sources (deleted configs treated as applied; non-null expected values still compared); full ReplicationThrottleHelperTest suite (unit + embedded-Kafka integration) passes; checkstyle clean.
  2. Note: this bug is independent of (and pre-dates) the batching work in Batch AdminClient config RPCs in ReplicationThrottleHelper #2367 — it was uncovered while testing that branch on a real cluster, since the strict batched verification surfaced the hang clearly. The same fix applies to both code paths.

Categorization

  • documentation
  • bugfix
  • new feature
  • refactor
  • security/CVE
  • other

…ottle is set

When clearing throttles, ReplicationThrottleHelper deletes the
per-broker leader/follower.replication.throttled.rate configs and then
polls describeConfigs until the configs read back as absent. If a
cluster-wide dynamic default throttle exists (set via kafka-configs
--alter --entity-type brokers --entity-default), the deleted per-broker
config resolves to the default value, so describeConfigs keeps
returning a non-null entry and verification never converges.

configsEqual already skipped this comparison for STATIC_BROKER_CONFIG;
extend the same treatment to DYNAMIC_DEFAULT_BROKER_CONFIG and
DEFAULT_CONFIG: seeing a value from one of these sources after a DELETE
means the per-entity delete itself succeeded and the remaining value is
inherited from a layer Cruise Control does not manage.

Also cap the verification retry backoff at 30s per attempt. The retry
helper doubles the sleep with no cap (5s * 2^n), so a verification that
keeps failing parks the executor thread in exponentially growing sleeps
(attempt 13 alone sleeps ~11 hours) and the whole execution appears
hung. With the cap, retries exhaust in ~15 minutes and fail loudly.

Observed in production on a 200+ broker cluster: the executor completed
its first batch of movements, then slept for hours inside
clearThrottles verification because an --entity-default throttle was
present; the rebalance made no further progress and new executions were
rejected with "Cannot start a new execution".
@acgtun

acgtun commented Jul 16, 2026

Copy link
Copy Markdown
Author

Updated the description to follow the PR template format.

Copilot AI left a comment

Copy link
Copy Markdown

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 fixes a throttle-removal verification hang in ReplicationThrottleHelper when brokers inherit replication throttle values from cluster-wide defaults (e.g., kafka-configs --entity-default), and bounds the verification retry backoff so a stuck verification fails within a reasonable time instead of sleeping exponentially for hours/days.

Changes:

  • Treat config entries inherited from STATIC_BROKER_CONFIG, DYNAMIC_DEFAULT_BROKER_CONFIG, and DEFAULT_CONFIG as “delete applied” when the expected per-entity value is null, allowing delete verification to converge under default layers.
  • Cap waitForConfigs verification retry backoff at 30s per attempt by using the CruiseControlMetricsUtils.retry(..., maxSleepMs) overload.
  • Extend unit tests for configsEqual to cover dynamic-default and Kafka-default config sources.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/executor/ReplicationThrottleHelper.java Updates config verification semantics for inherited defaults after DELETE and caps retry backoff during verification.
cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/executor/ReplicationThrottleHelperTest.java Adds configsEqual test cases for DYNAMIC_DEFAULT_BROKER_CONFIG and DEFAULT_CONFIG behaviors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 381 to +385
return !configsEqual(getEntityConfigs(cf), expectedConfigs);
} catch (ExecutionException | InterruptedException | TimeoutException e) {
return false;
}
}, _retries);
}, RETRY_BACKOFF_SCALE_MS, RETRY_BACKOFF_BASE, _retries, MAX_RETRY_SLEEP_MS);
Comment on lines +45 to +47
// Backoff parameters for the config verification retry loop. The sleep between attempts is
// scale * base^attempt, capped at MAX_RETRY_SLEEP_MS so that a slow-to-verify config cannot
// park the executor thread for an unbounded exponential sleep.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants