Skip to content

fix: add delayed segment removal to prevent load/drop race in Broker (#18738) - #20089

Open
zhang-arvin wants to merge 2 commits into
apache:masterfrom
zhang-arvin:fix/issue-18738-segment-load-drop-race
Open

fix: add delayed segment removal to prevent load/drop race in Broker (#18738)#20089
zhang-arvin wants to merge 2 commits into
apache:masterfrom
zhang-arvin:fix/issue-18738-segment-load-drop-race

Conversation

@zhang-arvin

Copy link
Copy Markdown

Description

Fixes the segment load/drop race condition in the Broker (issue #18738).

When a segment is dropped and immediately reloaded, the Broker's segment watcher can incorrectly remove the newly loaded segment from its view. This happens because the segment removal is triggered by the drop announcement, but the reload announcement may arrive before the removal is actually processed.

Changes

  • BrokerSegmentWatcherConfig: Added segmentRemovalDelayMs configuration to delay segment removal after a drop announcement, allowing time for reload announcements to cancel the removal.
  • BrokerServerView: Uses a scheduled executor to delay segment removal, with cancellation logic when a reload is detected during the delay window.
  • BrokerServerViewTest: Added test cases to verify the delayed removal behavior and cancellation on reload.

Testing

  • Unit tests added to cover the race condition scenario
  • Verified that existing tests continue to pass

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Severity Findings
P0 0
P1 1
P2 0
P3 0
Total 1

Reviewed 3 of 3 changed files.


This is an automated review by Codex GPT-5.6-Luna(max)

// See https://github.com/apache/druid/issues/18738
final ScheduledFuture<?> pendingRemoval = delayedRemovalExecutor.schedule(
() -> {
pendingSegmentRemovals.remove(segmentId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Stale timer can remove a newer pending removal

An expired timer removes the segment ID without verifying that its future is still current. If it loses the lock to a reload followed by another drop, the newer timer is installed, then the older task can pass the selector checks and remove the timeline entry before the newer delay expires, reintroducing the load/drop race and causing partial query results. Guard removal with the expected future or a generation token under the same lock.

zhang-arvin added a commit to zhang-arvin/druid that referenced this pull request Aug 21, 2026
Use ConcurrentHashMap.remove(key, value) to atomically verify that the
pending removal future is still current before removing the segment from
the timeline. This prevents a stale timer from removing a newer pending
removal entry when a segment is dropped, reloaded, and dropped again in
quick succession.

Addresses P1 review comment from FrankChen021 on PR apache#20089.
@zhang-arvin

Copy link
Copy Markdown
Author

@FrankChen021 Thanks for the review! Fixed the P1 issue in 653beaa: the timer callback now uses ConcurrentHashMap.remove(segmentId, pendingRemoval) to atomically verify the future is still current before removing the timeline entry.

@clintropolis

clintropolis commented Aug 21, 2026

Copy link
Copy Markdown
Member

BrokerServerView: Uses a scheduled executor to delay segment removal, with cancellation logic when a reload is detected during the delay window.

hmm, drive by comment (haven't really reviewed code changes) but wonder is this good? presumably a lot more pure drops happen than re-loads to the same server, won't this result in more queries getting missing results because we didn't remove a server that has dropped the segment?

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Severity Findings
P0 0
P1 2
P2 1
P3 0
Total 3
Severity Findings
P0 0
P1 2
P2 1
P3 0
Total 3

Reviewed 3 of 3 changed files.


This is an automated review by Codex GPT-5.6-Luna(max)

// See https://github.com/apache/druid/issues/18738
final ScheduledFuture<?> pendingRemoval = delayedRemovalExecutor.schedule(
() -> {
if (pendingSegmentRemovals.remove(segmentId, pendingRemoval)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Stale timer is not rechecked under lock

The callback removes its future from pendingSegmentRemovals before taking the timeline lock. A segment can be re-added in that gap and install a newer delayed removal; the old callback can then remove the segment while the newer callback sees a pending entry and returns, leaving the freshly re-added segment absent from the timeline. Recheck that the callback still owns the current pending future under the same lock, or make replacement and removal atomic.

"Asked to remove timeline entry[interval: %s, version: %s] that doesn't exist",
segment.getInterval(),
segment.getVersion()
final long delayMillis = segmentWatcherConfig.getSegmentDropDelayMillis();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Delayed drops expose empty selectors to queries

During the delay, the empty ServerSelector remains in the timeline. CachingClusteredClient can include that holder, pick() returns null, and groupSegmentsByServer omits the segment while computeUncoveredIntervals still sees the timeline holder, allowing a partial result without an uncovered interval. Remove or mark the holder unavailable during the delay, or make query-side uncovered-interval handling treat an empty selector as uncovered.

delayMillis,
TimeUnit.MILLISECONDS
);
final ScheduledFuture<?> previous = pendingSegmentRemovals.put(segmentId, pendingRemoval);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Timer can fire before pending state is published

The scheduled callback can run before pendingSegmentRemovals.put completes. It then finds no pending entry and returns, after which the future is published with no callback left to remove the segment from the timeline. Publish the pending state before scheduling, or synchronize publication with callback execution.

@zhang-arvin

Copy link
Copy Markdown
Author

Thanks @clintropolis for the feedback on the delay-removal approach. The scheduled executor with cancellation on reload was chosen to avoid the race condition where a segment is dropped while a new load is in progress. Happy to discuss alternative approaches.

…8738)

Addresses review comments from FrankChen021 and clintropolis on PR apache#20089.

P1 fix: timer is now rechecked under lock to prevent stale timer from
removing a newer pending removal entry. The synchronized(lock) block wraps
the remove(key, value) check, ensuring the lock is held during both the
future verification and the selector cleanup.

P1 fix: empty selectors are no longer exposed to queries during the delay
window. The segment is removed from the timeline immediately when the last
server is dropped, while the selector is kept in the selectors map. If a
new server announces the segment during the delay, it is re-added to the
timeline. Only the selector cleanup is delayed.

P2 fix: handle race where timer fires before pendingSegmentRemovals.put()
completes. After the put, we check pendingRemoval.isDone() and run the
cleanup inline if the timer already fired.

Architecture response to clintropolis: The segment is now removed from the
timeline immediately, so pure drops are not affected by the delay — the
timeline entry is gone right away as before. The delay only applies to the
cleanup of the selector from the selectors map, which allows a new server
to re-add the segment to the timeline during the delay window. This
preserves the race-condition fix while avoiding the problem of empty
selectors in the timeline during the delay.

Addresses PR comments:
- P1: stale timer not rechecked under lock
- P1: delayed drops expose empty selectors to queries
- P2: timer can fire before pending state is published
@zhang-arvin
zhang-arvin force-pushed the fix/issue-18738-segment-load-drop-race branch from 653beaa to d28a1a8 Compare August 21, 2026 17:33
@zhang-arvin

Copy link
Copy Markdown
Author

Thanks @clintropolis and @FrankChen021 for the feedback! I've addressed the issues:

  • P1: Stale timer now re-checks under the same lock, preventing race conditions
  • P1: Segment is now immediately removed from timeline — only selector cleanup is delayed, so pure drop scenarios are unaffected
  • P2: Timer that fires before pending state is published now triggers inline cleanup

The fix is pushed — please re-review when you have time.

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.

3 participants