FrankChen021 commented on code in PR #20089:
URL: https://github.com/apache/druid/pull/20089#discussion_r4082450242


##########
server/src/main/java/org/apache/druid/client/BrokerServerView.java:
##########
@@ -351,26 +420,249 @@ private void serverRemovedSegment(DruidServerMetadata 
server, DataSegment segmen
       }
 
       if (selector.isEmpty()) {
-        VersionedIntervalTimeline<String, ServerSelector> timeline = 
timelines.get(segment.getDataSource());
-        selectors.remove(segmentId);
+        final long delayMillis = 
segmentWatcherConfig.getSegmentDropDelayMillis();
+        if (delayMillis > 0) {
+          // Remove the segment from the timeline immediately so that queries 
never observe an
+          // empty ServerSelector on the timeline (an empty selector makes 
groupSegmentsByServer
+          // skip the holder while computeUncoveredIntervals still counts it 
as covered, which
+          // yields a silent partial result). The selector is kept in the 
selectors map so that
+          // a new server announcing the segment during the delay can re-add 
it to the timeline.
+          // segmentRemoved is not fired until the selector is actually 
cleaned up, because the
+          // segment may come back before the delay expires.
+          // See https://github.com/apache/druid/issues/18738
+          removeFromTimelineQuietly(segment, selector);
+
+          // Schedule the delayed cleanup of the selector from the selectors 
map. If a new server
+          // announces the segment before the delay expires, the entry is 
cancelled and the
+          // segment is re-added to the timeline.
+          //
+          // The callback closes over `entry`, which is created before 
scheduling and therefore
+          // definitely assigned. Passing a *different* object to the 
scheduled task would half-fix
+          // this: the entry identity is what lets the callback tell "I am 
still the current entry"
+          // from "a newer entry replaced me", which a self-comparison of the 
future cannot do.
+          final long scheduledAtNanos = System.nanoTime();
+          final EntryFuture entry = new EntryFuture();
+          entry.setDelayedRemovalFuture(
+              delayedRemovalExecutor.schedule(
+                  () -> runDelayedRemoval(segment, selector, entry, 
scheduledAtNanos),
+                  delayMillis,
+                  TimeUnit.MILLISECONDS
+              )
+          );
 
-        final PartitionChunk<ServerSelector> removedPartition = 
timeline.remove(
-            segment.getInterval(), segment.getVersion(), 
segment.getShardSpec().createChunk(selector)
-        );
+          // Register the entry as the current pending removal. The timer may 
already have fired
+          // (delayMillis can be shorter than this thread's scheduling 
latency); that is safe
+          // because the callback re-validates the entry under `lock` and 
bails out if it is not
+          // yet present, in which case the inline cleanup below performs the 
removal instead.
+          final EntryFuture previous = pendingSegmentRemovals.put(segmentId, 
entry);
+          if (previous != null) {
+            previous.cancel(false);
+            log.warn("Replaced existing pending removal for segment[%s].", 
segmentId);
+          }
+
+          // Close the window where the timer fired before the put completed: 
the callback did
+          // nothing in that case, so clean up inline under the same lock that 
the callback uses.
+          if (entry.isDone()) {
+            cleanupSelectorIfCurrent(segmentId, selector, entry);
+          }
 
-        if (removedPartition == null) {
-          log.warn(
-              "Asked to remove timeline entry[interval: %s, version: %s] that 
doesn't exist",
-              segment.getInterval(),
-              segment.getVersion()
+          log.debug(
+              "Scheduled delayed cleanup of segment[%s] in [%d]ms. Waiting for 
a new server to announce it.",
+              segmentId,
+              delayMillis
           );
         } else {
-          runTimelineCallbacks(callback -> callback.segmentRemoved(segment));
+          removeSegmentFromTimeline(segment, selector);
         }
       }
     }
   }
 
+  /**
+   * Removes a segment's timeline entry without firing {@code segmentRemoved} 
and without
+   * dropping the selector from {@link #selectors}. Used by the delayed-drop 
path, where the
+   * segment may be re-announced before the delay expires.
+   */
+  private void removeFromTimelineQuietly(final DataSegment segment, final 
ServerSelector selector)
+  {
+    final VersionedIntervalTimeline<String, ServerSelector> timeline = 
timelines.get(segment.getDataSource());
+    if (timeline != null) {
+      timeline.remove(segment.getInterval(), segment.getVersion(), 
segment.getShardSpec().createChunk(selector));
+    }
+  }
+
+  /**
+   * Body of the scheduled delayed-removal task. Runs on {@link 
#delayedRemovalExecutor}.
+   *
+   * @param scheduledAtNanos {@link System#nanoTime()} sampled when the task 
was scheduled; see
+   *                         the ordering note in {@link 
#cleanupSelectorIfCurrent}.
+   */
+  private void runDelayedRemoval(
+      final DataSegment segment,
+      final ServerSelector selector,
+      final EntryFuture entry,
+      final long scheduledAtNanos
+  )
+  {
+    cleanupSelectorIfCurrent(segment.getId(), selector, entry);

Review Comment:
   [P1] Do not emit removal after losing cleanup ownership
   
   **Finding:** When the delayed task is canceled after it has started, or 
loses the identity check because a reload or newer drop replaced its 
EntryFuture, cleanupSelectorIfCurrent returns false. runDelayedRemoval discards 
that result and still enqueues segmentRemoved at line 512. The load/drop race 
can therefore evict metadata for a segment that was re-added (this callback is 
the authority used by the metadata caches); even when cleanup wins, the enqueue 
happens after releasing lock, so a re-add can enqueue segmentAdded first and 
the same single-thread callback executor will process the stale removal after 
it.
   
   **Suggestion:** Only emit segmentRemoved when this entry actually completes 
the cleanup, and serialize that successful cleanup plus callback submission 
with the same lock/re-add transition so callback order cannot put a stale 
removal after a re-add.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to