Skip to content

[server] Add off-loop coordinator health cache (mechanism only) - #4061

Draft
affo wants to merge 2 commits into
apache:mainfrom
affo:la-coordinator-health-cache
Draft

[server] Add off-loop coordinator health cache (mechanism only)#4061
affo wants to merge 2 commits into
apache:mainfrom
affo:la-coordinator-health-cache

Conversation

@affo

@affo affo commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Companion to the discussion on #1389 about splitting getServerNodes() / getClusterHealth() / describeTabletServers() by cost and update frequency (comment).

Adds CoordinatorHealthCache: a lock-free, coordinator-thread-published snapshot of cluster-wide and per-tablet-server replica/leader health, kept warm incrementally as mutations happen, instead of recomputed from scratch on every RPC call via AccessContextEvent. Wired into real mutation handling (leader/ISR changes, server death/registration, topology changes). No RPC reads from it yetgetClusterHealth()/describeTabletServers() are untouched; this PR is the mechanism only.

Performance & consistency trade-offs

  • Read cost: today, getClusterHealth()/describeTabletServers() each enqueue a fresh O(buckets) scan onto the single coordinator event thread on every call. This cache makes reads O(1) — a lock-free field read — by moving the scan to the write side, where it's decoupled from call frequency and triggered by mutation instead.
  • Write cost: still O(buckets) per recompute, unavoidably — the point isn't to make the scan cheaper, it's to make it run less often. Every mutation is coalesced: a burst of many changes (e.g. a full failover storm re-electing leaders for hundreds of buckets) costs at most one recompute, not one per event, bounded by how long the coordinator's event queue stays busy.
  • Consistency is bounded, and deliberately asymmetric: changes that represent degradation (a server dying, a bucket going under-replicated, a leader missing or inactive) are guaranteed to be reflected within 200ms regardless of how busy the coordinator is; everything else (topology changes, tag updates, healthy ISR churn) is only guaranteed eventually, once the event queue next drains. This is safe specifically because staleness always lags toward the previous known state — a stale read can show a server as busier/healthier than it now is (bounded to 200ms of lag for degradation), but never masks a live problem for longer than that bound, and a consumer using this as a scale-in/rolling-upgrade safety gate can only end up waiting longer than strictly necessary, never acting on data that's wrong in the unsafe direction.

Impact on other open PRs

Known, deliberate gap: leader-activation flips (CoordinatorRequestBatch) aren't wired to this cache yet — bounded staleness, not a correctness issue (the next refresh for any other reason reports it correctly regardless), left for a follow-up.

Test Plan

  • New unit tests: CoordinatorHealthCacheTest (15), CoalescingRefreshCacheTest (6).
  • Full existing suite for every touched/related file passes unchanged: CoordinatorEventProcessorTest, ClusterHealthTest, CoordinatorEventManagerTest, TableBucketStateMachineTest, CoordinatorContextTest, CoordinatorMetadataCacheTest.
  • mvn spotless:check clean.

🤖 AI-assisted changes - reviewed by human developer

affo and others added 2 commits August 21, 2026 11:21
Adds CoordinatorHealthCache, a copy-on-write cache of cluster/per-tablet-server
replica and leader health, published lock-free without going through
AccessContextEvent -- the same pattern CoordinatorMetadataCache already uses
for server topology. The coalescing/urgency mechanics are extracted into a
reusable CoalescingRefreshCache<T>, since the same "callers report facts, this
decides when to act" shape applies to more than just this one cache.

Wired into real coordinator event processing: every mutation that matters
(leader/ISR changes, server death/registration, table/partition create-delete,
tag add/remove, reassignment) reports through it, and refreshIfNeeded() runs
on every event-loop tick, right next to the existing metrics-timer check.

Does not wire any RPC to it yet -- getClusterHealth()/describeTabletServers()
are untouched and still compute on demand via AccessContextEvent. The rewrite
is confirmed mechanically straightforward (CoordinatorService already derives
eventManagerSupplier the same way a healthCacheSupplier would) but intentionally
left for a follow-up.

Related discussion: apache#1389 (comment)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e) method

Collapses update()/refreshIfNeeded(compute, idle) into a single
refresh(compute, force). The caller now computes "force" itself (typically
from queue.isEmpty()) instead of the cache interpreting a separate "idle"
signal -- same information, smaller interface.

The dirty check is now an unconditional first gate that force never bypasses,
only the timing/urgency gate does. This matters: without it, "queue empty ->
force=true" would trigger a full rescan on every idle tick regardless of
whether anything changed, since a healthy coordinator is idle most of the
time. A freshly constructed cache starts dirty so warm-up still works without
needing force to bypass the dirty check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@affo

affo commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

CC @fresh-borzoni if you have some time to review this one related to all others metadata-related PRs 😅

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.

1 participant