Skip to content

Move realtime stream offset fetch out of the ideal-state update lock - #19170

Open
shounakmk219 wants to merge 1 commit into
apache:masterfrom
shounakmk219:rvm-is-lock-stall-fix
Open

Move realtime stream offset fetch out of the ideal-state update lock#19170
shounakmk219 wants to merge 1 commit into
apache:masterfrom
shounakmk219:rvm-is-lock-stall-fix

Conversation

@shounakmk219

Copy link
Copy Markdown
Collaborator

Problem

RealtimeSegmentValidationManager (RVM) calls PinotLLCRealtimeSegmentManager.ensureAllPartitionsConsuming, which fetched the stream offsets inside the Helix ideal-state update lambda (HelixHelper.updateIdealStateIdealStateGroupCommit). On a table with many partitions the offset I/O can take minutes, and because it ran inside the updater it (a) held the per-table ideal-state lock for that whole time — stalling concurrent segment commits — and (b) was re-executed on every ZK version-checked CAS retry.

This is the follow-up to #19116 (which batched the Kafka fetch): batching shrinks the fetch, this change removes it from the lock entirely.

Fix

  • Pre-fetch outside the lock. ensureAllPartitionsConsuming now reads a read-only snapshot of the ideal state (HelixHelper.getTableIdealState), does the early enabled/paused check, and calls a new @VisibleForTesting preFetchOffsets(...) to compute streamMetadataList and the per-partition smallest offsets before entering HelixHelper.updateIdealState.
  • Lambda does in-memory work only. The updater lambda re-checks enabled/paused on the fresh IS and calls the package-private ensureAllPartitionsConsuming(..., preFetchedPartitionIdToSmallestOffset), which mutates the fresh IS from the pre-fetched offsets. No stream I/O runs under the lock, so lock hold-time is proportional to the mutation and the fetch is not repeated on CAS retries.
  • Gated smallest-offset fetch. The smallest-offset round-trip only happens on a reset (offsetCriteria != null) or when anyPartitionNeedsSmallestOffset(...) finds a partition whose latest segment has no CONSUMING replica (the only repair path that consults it). Healthy tables fetch nothing.
  • Snapshot/fresh divergence is safe. The IS mutation is still fresh + version-checked CAS, so no concurrent update is lost. Offsets are the only snapshot input; the three-way smallest-offset fallback plus a null-safe skip guard mean a partition that starts needing repair after the snapshot is deferred to the next run rather than being repaired with a substituted (checkpoint) offset. The actual start offset written in the periodic path comes from fresh ZK segment metadata.

Testing

  • PinotLLCRealtimeSegmentManagerTest + RealtimeSegmentValidationManagerTest: 57 tests pass, including new cases — testEnsureAllPartitionsConsumingHonorsPreFetchedSmallestOffset, testEnsureAllPartitionsConsumingDefersRepairWhenSmallestOffsetsNotPreFetched, testPreFetchOffsetsSkipsSmallestOffsetFetchForHealthyTable, and testPreFetchOffsetsRestoresOffsetCriteriaOnFailure.
  • spotless / checkstyle / license clean.

Concurrency notes (IS updated by another process during an RVM run)

The mutation path is unchanged (serialized per table via IdealStateGroupCommit, version-checked CAS), so concurrent commits are not lost — a commit landing mid-run causes a CAS retry and RVM re-runs on the newer IS. Only the offset inputs are a snapshot. Residual, all self-healing within one 15-min cycle:

  • A partition that loses its CONSUMING replica between snapshot and lock is deferred one cycle (not repaired with a wrong offset).
  • A stale-low smallest offset can only under-report data loss for one cycle (never over-report); a truncated-stream segment briefly goes OFFLINE and is re-repaired next cycle with a fresh smallest.
  • New stream partitions / unpause are picked up on the next cycle.

Depends on

Complementary to #19116 (batching). Independent at the code level — this PR only touches pinot-controller and calls pre-existing stream methods — but both together give the full win (fast fetch + off-lock).

RealtimeSegmentValidationManager's ensureAllPartitionsConsuming fetched
the stream offsets inside the Helix ideal-state update lambda, so on a
table with many partitions the offset I/O (which can take minutes) was
held under the per-table ideal-state lock and re-run on every ZK CAS
retry, stalling concurrent segment commits.

Pre-fetch the offsets from a read-only snapshot of the ideal state,
outside the lock (preFetchOffsets), and pass them into the package-private
ensureAllPartitionsConsuming, whose updater lambda now performs only
in-memory ideal-state mutation. Lock hold-time is proportional to the
mutation, and the offset fetch runs once regardless of CAS retries.

The smallest-offset stream fetch is gated (anyPartitionNeedsSmallestOffset)
so it runs only on a reset or when a partition actually needs a new
CONSUMING segment. A partition that starts needing repair after the
lock-free snapshot is deferred to the next validation run rather than
being repaired with substituted start offsets.
@codecov-commenter

codecov-commenter commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.42857% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.62%. Comparing base (cd2538d) to head (7a1f65f).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
.../core/realtime/PinotLLCRealtimeSegmentManager.java 71.42% 11 Missing and 3 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19170      +/-   ##
============================================
+ Coverage     66.59%   66.62%   +0.02%     
  Complexity     1423     1423              
============================================
  Files          3443     3443              
  Lines        218536   218568      +32     
  Branches      34780    34786       +6     
============================================
+ Hits         145538   145621      +83     
+ Misses        61273    61231      -42     
+ Partials      11725    11716       -9     
Flag Coverage Δ
custom-integration1 100.00% <ø> (ø)
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 66.62% <71.42%> (+0.02%) ⬆️
temurin 66.62% <71.42%> (+0.02%) ⬆️
unittests 66.62% <71.42%> (+0.02%) ⬆️
unittests1 57.12% <ø> (+<0.01%) ⬆️
unittests2 38.93% <71.42%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@shounakmk219 shounakmk219 added ingestion Related to data ingestion pipeline performance Related to performance optimization real-time Related to realtime table ingestion and serving labels Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ingestion Related to data ingestion pipeline performance Related to performance optimization real-time Related to realtime table ingestion and serving

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants