[CICP-5409] Fix data race between async global rebalance and the controller pipeline - #321
Draft
thestreak101 wants to merge 1 commit into
Draft
thestreak101 wants to merge 1 commit into
thestreak101 wants to merge 1 commit into
Conversation
…roller pipeline GenericHelixController owns a SINGLE ResourceControllerDataProvider instance (built once, held in a final field) and refreshes it IN PLACE on every event-loop iteration. With async global rebalance -- which is the DEFAULT (ClusterConfig.DEFAULT_GLOBAL_REBALANCE_ASYNC_MODE_ENABLED = true) -- the pipeline thread submits the baseline calculation and immediately continues. The async task then read `clusterData`, `resourceMap` and `currentStateOutput` while the pipeline thread was concurrently mutating the very same objects on the next iteration. The baseline could be computed from a half-refreshed view of the cluster: a torn mix of old and new instance/resource state. Fix: materialize the ClusterModel on the CALLING (pipeline) thread, before the task is submitted. `getBaselineAssignment` and `generateClusterModelForBaseline` are the only two places the async task touched shared pipeline state, plus `clusterData.getClusterName()`. Both are hoisted, and the cluster name is captured in a local. `doGlobalRebalance` is reduced to `(ClusterModel, String clusterName, RebalanceAlgorithm, boolean)` so it is no longer *able* to reach shared mutable state -- the invariant is enforced by the signature rather than by convention. Verified that ClusterModel is a materialized snapshot: neither it nor ClusterContext, AssignableNode or AssignableReplica holds a reference back to the data provider. Without that property the race would simply move rather than be fixed. The expensive part -- the rebalance algorithm itself -- still runs asynchronously, so this does not turn async mode back into sync mode. Behaviour change: cluster-model construction failures (INVALID_CLUSTER_STATUS / INVALID_CLUSTER_CONFIG) now surface synchronously to the pipeline caller instead of being recorded in `_lastAsyncFailure` and reported on a later iteration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
GenericHelixControllerowns a singleResourceControllerDataProviderinstance (built once, held in afinalfield) and refreshes it in place on every event-loop iteration.With async global rebalance — which is the default (
ClusterConfig.DEFAULT_GLOBAL_REBALANCE_ASYNC_MODE_ENABLED = true) — the pipeline thread submits the baseline calculation and immediately continues. The async task then readclusterData,resourceMapandcurrentStateOutputwhile the pipeline thread was concurrently mutating the very same objects on the next iteration.The baseline could therefore be computed from a half-refreshed view of the cluster: a torn mix of old and new instance/resource state. This is a live defect on the default configuration, not a theoretical one.
Fix
Materialize the
ClusterModelon the calling (pipeline) thread, before the task is submitted.getBaselineAssignmentandgenerateClusterModelForBaselinewere the only two places the async task touched shared pipeline state, plusclusterData.getClusterName(). Both are hoisted into a newbuildClusterModel(...), and the cluster name is captured in a local.doGlobalRebalanceis reduced to:so it is no longer able to reach shared mutable state — the invariant is enforced by the signature rather than by convention.
The expensive part — the rebalance algorithm itself — still runs asynchronously. This does not turn async mode back into sync mode.
Prerequisite verified
The fix is only sound if
ClusterModelis a true snapshot. Confirmed that neitherClusterModelnorClusterContext,AssignableNodeorAssignableReplicaholds a reference back to the data provider. Without that property the race would simply move rather than be fixed.Behaviour change (please review)
Cluster-model construction failures (
INVALID_CLUSTER_STATUS/INVALID_CLUSTER_CONFIG) now surface synchronously to the pipeline caller, instead of being recorded in_lastAsyncFailureand reported on a later iteration. This is arguably more correct — the failure is attributed to the iteration that caused it — but it is a change in where the exception appears.Verification
mvn -pl helix-core -am test-compilepasses (includes existing helix-core tests).🤖 Drafted with GitHub Copilot CLI. Filed from an audit of CICP Helix bug tickets — please review carefully before merging.