Skip to content

Add per-instance actual-state partition gauges from CurrentState - #218

Open
LZD-PratyushBhatt wants to merge 7 commits into
devfrom
lzd/per-instance-actual-state-gauges
Open

Add per-instance actual-state partition gauges from CurrentState#218
LZD-PratyushBhatt wants to merge 7 commits into
devfrom
lzd/per-instance-actual-state-gauges

Conversation

@LZD-PratyushBhatt

@LZD-PratyushBhatt LZD-PratyushBhatt commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

PR Description

Issues

Description

  • Here are some details about my PR, including screenshots of any UI changes:

What: Adds two per-instance MBean gauges sourced from each instance's CurrentState:

Attribute Meaning
ActualPartitionGauge Partitions the instance actually hosts, excluding initial-state (typically OFFLINE) partitions
ActualTopStatePartitionGauge Of those, how many are in the resource top state (MASTER, LEADER, ...)

Why: The existing InstanceMonitor.PartitionGauge and TopStatePartitionGauge are derived
from BestPossibleStateOutput, so they describe what the Sharding Controller intends to place on
an instance. There is currently no per-instance metric for what an instance is actually hosting.
Without it, an instance that is failing to complete state transitions looks identical to a healthy
one, since the target assignment is reported either way. These gauges make the gap between intent
and reality directly observable per instance.

How: The counts are computed in ReadClusterDataStage, in the same single pass over
CurrentState that already produces the per-instance ERROR partition count, so no additional
ZooKeeper read or extra traversal is introduced. They are routed to the instance beans through a
new ClusterStatusMonitor#setInstanceActualPartitionStatus entry point, which also resets
instances that are absent from the update (for example, instances that are no longer live) so that
values cannot go stale.

Counting convention, matching the one PerInstanceResourceMonitor#update already uses: a partition
counts as hosted when its state is not the state model's initial state. DROPPED is filtered
defensively only. On a successful drop the participant writes a MergeOperation.SUBTRACT delta that
removes the partition entry from CurrentState rather than persisting DROPPED, so DROPPED is not
expected to be observable there (see HelixStateTransitionHandler, lines 167 to 174).

Resources whose state model definition cannot be resolved are skipped for the actual counts, since
their states cannot be interpreted, but ERROR is still counted because that state is state model
agnostic. Failures are contained per resource, so one unreadable resource cannot discard the counts
already accumulated for an instance.

Files changed

 helix-core/.../controller/stages/ReadClusterDataStage.java         | 155 +++++++---
 helix-core/.../monitoring/mbeans/ClusterStatusMonitor.java         |  25 ++
 helix-core/.../monitoring/mbeans/InstanceMonitor.java              |  39 +++
 helix-core/.../stages/TestReadClusterDataStagePartitionCounts.java | 280 +++++++++++++++
 helix-core/.../monitoring/mbeans/TestClusterStatusMonitor.java     |  77 ++++++
 helix-core/.../monitoring/mbeans/TestInstanceMonitor.java          |  41 +++
 website/1.4.3/src/site/markdown/Metrics.md                         |   4 +
 7 files changed, 583 insertions(+), 38 deletions(-)

Tests

  • The following tests are written for this issue:

TestReadClusterDataStagePartitionCounts (new, 10 tests covering the count computation):

  • testExcludesInitialStateAndDroppedPartitions
  • testErrorPartitionsCountAsHosted
  • testCountsAggregateAcrossResourcesWithDifferentStateModels
  • testUnresolvedStateModelSkipsActualCountsButKeepsErrorCount
  • testFailingResourceDoesNotDiscardOtherResourceCounts
  • testNonLiveInstanceYieldsZeroCounts
  • testCurrentStateReadFailureIsContained
  • testNullCurrentStateMapYieldsZeroCounts
  • testNullPartitionStateIsSkipped
  • testEmptyCurrentStateYieldsZeroCounts

TestClusterStatusMonitor (added):

  • testSetInstanceActualPartitionStatus, covering JMX attribute values, reset to zero for
    instances absent from an update, staleness across successive updates, and null input maps

TestInstanceMonitor (added):

  • testActualPartitionCountMetrics, covering gauge initialization, updates, and independence
    from the existing best-possible partition gauges

The following is the result of the "mvn test" command on the appropriate module:

$ mvn test -pl helix-core \
    -Dtest='TestReadClusterDataStagePartitionCounts,TestClusterStatusMonitor,TestInstanceMonitor'

[INFO] --------------------< org.apache.helix:helix-core >---------------------
[INFO] -------------------------------[ bundle ]-------------------------------
[INFO] Running TestSuite
[INFO] Tests run: 41, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.797 s - in TestSuite
[INFO] Tests run: 41, Failures: 0, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  5.650 s

Surrounding stages and monitors were also run to check for regressions
(TestReadClusterDataStageDomainValidation, TestCurrentStateComputationStage,
TestBestPossibleStateCalcStage, TestResourceComputationStage): 62 tests, 0 failures.

The new tests were validated by temporarily reintroducing the initial-state counting behaviour and
confirming that 3 of them fail, then restoring the fix and confirming all pass. This checks that
they are genuine regression guards rather than tests that pass either way.

Changes that Break Backward Compatibility (Optional)

No breaking changes. Both gauges are new attributes on the existing InstanceMonitor bean, and
setInstanceActualPartitionStatus is a new method. No existing metric changes value or meaning.

One point reviewers should be aware of, since it affects how these metrics are used rather than
compatibility:

ActualPartitionGauge excludes initial-state (OFFLINE) partitions, while the existing
InstanceMonitor.PartitionGauge counts every entry in the best possible state map, including
OFFLINE and DROPPED. The two are therefore not directly subtractable. Note that
PerInstanceResourceMonitor.PartitionGauge, which shares a name with the InstanceMonitor one,
already excludes both. Aligning InstanceMonitor.PartitionGauge with that convention would make
target versus actual comparisons exact, but it changes an already released metric, so it is
deliberately left out of this PR.

Documentation (Optional)

In case of new functionality, my PR adds documentation in the following wiki page:

Documented in-repo rather than in the wiki: website/1.4.3/src/site/markdown/Metrics.md, under
MBean InstanceMonitor. That table previously listed none of the per-instance partition gauges, so
this adds PartitionGauge, TopStatePartitionGauge, ActualPartitionGauge and
ActualTopStatePartitionGauge.

Commits

  • My commits all reference appropriate Apache Helix GitHub issues in their subject lines. In
    addition, my commits follow the guidelines from "How to write a good git commit message":
    • Subject is separated from body by a blank line
    • Subject is limited to 50 characters (not including Jira issue reference)
    • Subject does not end with a period
    • Subject uses the imperative mood ("add", not "adding")
    • Body wraps at 72 characters
    • Body explains "what" and "why", not "how"

Deviation, stated rather than silently checked: commit subjects run 53 to 67 characters, above the
50 character guideline, which is consistent with existing commits on this branch of the fork. Two
commit bodies have a single line at 73 and 74 characters. Say the word and these can be reworded
and force pushed, since the branch has no review history yet.

Code Quality

  • My diff has been formatted using helix-style.xml
    (helix-style-intellij.xml if IntelliJ IDE is used)

LZD-PratyushBhatt and others added 4 commits August 1, 2026 04:24
Introduce ActualPartitionGauge and ActualTopStatePartitionGauge on the
per-instance MBean, sourced from each instance's CurrentState. They
complement the existing PartitionGauge and TopStatePartitionGauge, which
reflect the controller's target assignment, by exposing what each instance
actually hosts and how many of those partitions are in the resource top
state.

The counts are computed in ReadClusterDataStage in a single pass over
CurrentState, alongside the existing ERROR-partition count, and routed to
the instance beans through a new ClusterStatusMonitor entry point.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ActualPartitionGauge counted partitions sitting in the state model's
initial state (typically OFFLINE) as hosted, which inflated the gauge
during bootstrap and failover. Skip both DROPPED and the initial state,
matching the convention PerInstanceResourceMonitor already uses.

Also harden the computation:

- Move the try/catch to per-resource scope. Previously a single catch
  wrapped the whole loop while mutating the counter in place, so an
  exception midway silently emitted a partial undercount even though the
  javadoc promised zeros on failure.
- Log a warning when a resource's state model definition cannot be
  resolved, instead of silently reporting zero top-state partitions.
  ERROR partitions are still counted there, since that state is state
  model agnostic.

Add ClusterStatusMonitor coverage for setInstanceActualPartitionStatus
(JMX values, reset to zero for instances absent from the update, and
null maps), and document the per-instance gauges in Metrics.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restore the defensive boundary around the live-instance lookup. That read
sits inside the async cluster status update, which also refreshes every
other per-instance metric, so an exception escaping here would abort the
whole callable and leave all instance metrics stale rather than just
zeroing one instance's partition counts.

Add direct coverage for the CurrentState-derived counts, which previously
had none. The tests pin the behaviour that matters:

- initial-state (OFFLINE) and DROPPED partitions are not hosted
- ERROR partitions are hosted, and are counted in both totals
- top state is resolved per resource, so MASTER and LEADER each count
  against their own state model
- an unresolved state model contributes no actual counts but still
  reports ERROR
- a resource that throws is skipped without discarding counts already
  accumulated from other resources
- non-live instances, null or empty current states, and null partition
  states all yield zeros instead of throwing

Widen computeInstancePartitionCounts and its counts holder to
package-private so the tests can exercise them, matching the existing
validateAndReportInstanceDomainInfo precedent in this class.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The actual partition gauge documentation described DROPPED as a state
being filtered out of the hosted count, which implied participants
persist it. They do not: on a successful transition to DROPPED the
participant writes a SUBTRACT delta that removes the partition entry
from CurrentState, so only the initial state (typically OFFLINE) and
ERROR are actually observable there.

Keep the DROPPED check as a cheap defensive guard, but describe it as
such so the exclusion that carries real weight, the initial state, is
not mistaken for a redundant one.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@LZD-PratyushBhatt LZD-PratyushBhatt changed the title Lzd/per instance actual state gauges Add per-instance actual-state partition gauges from CurrentState Aug 3, 2026
* Accumulate the partition counts contributed by a single resource's CurrentState into
* {@code counts}.
*/
private void accumulatePartitionCounts(BaseControllerDataProvider dataProvider,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

why do we need to exclude "OFFLINE" from hosted partitions, since they actually live on the disk, just that they are not "SERVING" ? whats the definition of "actual partition gauge" ?

"DROPPED" makes sense since it will not occupy disk on the instance.

@LZD-PratyushBhatt LZD-PratyushBhatt Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

You're right. OFFLINE also means "parked on purpose" (instance, partition, or resource disabled), so blanket-excluding it made the gauge false-fire on intentional disablement. Fixed now: initial-state partitions now count when disablement is the reason

long errorCount = countErrorPartitions(dataProvider, instanceName);
instanceErrorPartitionCounts.put(instanceName, errorCount);
// Count partitions this live instance actually hosts, from its CurrentState
InstancePartitionCounts partitionCounts =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

will this have any performance impact on overall rebalance pipeline?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No impact. It runs off the pipeline thread via asyncExecute, and it's the same single pass countErrorPartitions already did, now accumulating three counters instead of one. The only new work is one in-memory getStateModelDef per resource, no extra ZK read.

.setClusterInstanceStatus(liveInstanceSet, instanceSet, disabledInstanceSet,
disabledPartitions, oldDisabledPartitions, tags, instanceMessageMap,
instanceConfigMap, instanceErrorPartitionCounts);
clusterStatusMonitor.setInstanceActualPartitionStatus(instanceActualPartitionCounts,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These two monitor updates aren't atomic. setInstanceActualPartitionStatus here and the setClusterInstanceStatus call just above each acquire synchronized (_instanceMonitorMap) independently, so the pair isn't a single critical section. If registerInstances/unregisterInstances mutate _instanceMonitorMap in the window between the two calls, a live instance can momentarily publish ActualPartitionGauge/ActualTopStatePartitionGauge = 0 via the getOrDefault(instanceName, 0L) reset path, even though it is actually hosting partitions.

It is transient and self-heals on the next pipeline run, so it isn't a steady-state correctness bug — but it can surface as spurious zero dips on dashboards/alerts.

Why not fold the actual counts into setClusterInstanceStatus (passed alongside instanceErrorPartitionCounts) so everything updates under a single lock acquisition, with guaranteed ordering and no interleaving window? That also removes the implicit "must run after setClusterInstanceStatus has registered the beans" dependency this separate call currently relies on.

@LZD-PratyushBhatt LZD-PratyushBhatt Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, fixed now. Both count maps now go into setClusterInstanceStatus and are applied inside the same synchronized (_instanceMonitorMap) block, and setInstanceActualPartitionStatus is deleted so the window can't be reintroduced.

LZD-PratyushBhatt and others added 3 commits August 14, 2026 06:38
The actual partition counts were published through a separate
setInstanceActualPartitionStatus call made right after
setClusterInstanceStatus. Each acquired the _instanceMonitorMap monitor
independently, so the pair was not a single critical section.

The controller async task pool has ten threads, so these updates can
genuinely interleave. A bean registered by one thread could be observed
by another between the two calls and reset through the getOrDefault
path, letting a live instance briefly publish zero for partitions it is
really hosting. It self healed on the next pipeline run, but it could
surface as spurious zero dips on dashboards and alerts.

Fold the counts into setClusterInstanceStatus so bean registration and
gauge population happen under one lock acquisition. This also removes
the implicit requirement that the separate call must run after the beans
have been registered. The previous nine argument signature is kept and
delegates with nulls, which now mean no information supplied and leave
the gauges untouched rather than zeroing them.

Also document what ActualPartitionGauge counts: state machine progress,
not disk residency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review feedback noted that "actual" reads as disk residency, which
invites the conclusion that an OFFLINE partition should count, since its
data may well already be on the instance. The gauges measure something
different: how far the instance has progressed through its state
transitions. A partition can occupy disk while sitting in OFFLINE and is
deliberately not counted.

Rename so the name matches what is counted:

  ActualPartitionGauge         -> ActiveStatePartitionGauge
  ActualTopStatePartitionGauge -> ActiveStateTopStatePartitionGauge

Java identifiers, tests and Metrics.md follow the same rename. The
counting semantics are unchanged, since counting the initial state would
collapse the only signal these gauges provide: a live instance stuck part
way through its transitions would report the same value as one that is
fully caught up.

Javadoc and the docs table now state explicitly that this measures state
machine progress rather than disk residency. No released metric is
affected, as these gauges have not shipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
OFFLINE in CurrentState carries two different meanings. It can mean a
partition has not finished, or has failed, its state transition, but it
can equally mean the controller parked it in the initial state on
purpose. The rebalancer does exactly that when the instance is disabled,
when the partition is disabled on the instance, or when the whole
resource is disabled.

Excluding every initial-state partition conflated the two, so the gauge
under-reported healthy hosts and made a naive comparison against
PartitionGauge fire on intentional disablement.

A partition now counts as held when it is in a non-initial state, or
when it is in the initial state and one of those disablement reasons
applies. Detection mirrors what BaseControllerDataProvider itself treats
as disabled, so the gauge cannot drift from the rebalancer, and is
resolved once per instance and once per resource rather than per
partition. A missing IdealState is not read as a disabled resource,
since that means the resource is being removed. A failed disablement
lookup degrades to no disablement instead of dropping the instance.

Also revert the gauges to their original ActualPartitionGauge and
ActualTopStatePartitionGauge names, since the earlier rename was an
attempt to answer this same review point by wording rather than by
behaviour.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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