You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by zi...@apache.org on 2026/06/24 10:02:57 UTC

(pulsar) branch master updated: [fix][broker] Guard BucketDelayedDeliveryTracker.nextDeliveryTime against empty queues (#26080)

This is an automated email from the ASF dual-hosted git repository.

nodece pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar.git


The following commit(s) were added to refs/heads/master by this push:
     new 51e2b9a6bc9f [fix][broker] Guard BucketDelayedDeliveryTracker.nextDeliveryTime against empty queues (#26080)
51e2b9a6bc9f is described below

commit 51e2b9a6bc9f739d3fc33b24821df15c588d4055
Author: 道君- Tao Jiuming <da...@apache.org>
AuthorDate: Wed Jun 24 18:02:51 2026 +0800

    [fix][broker] Guard BucketDelayedDeliveryTracker.nextDeliveryTime against empty queues (#26080)
---
 .../bucket/BucketDelayedDeliveryTracker.java       |  5 +++
 .../bucket/BucketDelayedDeliveryTrackerTest.java   | 46 ++++++++++++++++++++++
 2 files changed, 51 insertions(+)

diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedDeliveryTracker.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedDeliveryTracker.java
index ff39545ee9b7..9bfea0604d7c 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedDeliveryTracker.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedDeliveryTracker.java
@@ -617,6 +617,11 @@ public class BucketDelayedDeliveryTracker extends AbstractDelayedDeliveryTracker
             return sharedBucketPriorityQueue.peekN1();
         } else if (sharedBucketPriorityQueue.isEmpty() && !lastMutableBucket.isEmpty()) {
             return lastMutableBucket.nextDeliveryTime();
+        } else if (lastMutableBucket.isEmpty() && sharedBucketPriorityQueue.isEmpty()) {
+            // numberDelayedMessages can be > 0 while both queues are empty (e.g. remaining
+            // messages live in not-yet-loaded snapshot segments). Returning Long.MAX_VALUE
+            // signals "no imminent delivery" without throwing on the empty queues.
+            return Long.MAX_VALUE;
         }
         long timestamp = lastMutableBucket.nextDeliveryTime();
         long bucketTimestamp = sharedBucketPriorityQueue.peekN1();
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedDeliveryTrackerTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedDeliveryTrackerTest.java
index 0d586fc43ecb..7205d8e6815b 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedDeliveryTrackerTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedDeliveryTrackerTest.java
@@ -682,4 +682,50 @@ public class BucketDelayedDeliveryTrackerTest extends AbstractDeliveryTrackerTes
 
         ts.close();
     }
+
+    @Test
+    public void testGetScheduledMessagesWhenAllOrphaned() throws Exception {
+        // Reproduces IAE in nextDeliveryTime: when every delayed message lies below the
+        // mark-delete position, the filter in getScheduledMessages pops the in-memory
+        // messages without returning them. If the immutable bucket has additional messages
+        // still in storage (later snapshot segments), numberDelayedMessages stays > 0
+        // while both the mutable bucket and the shared priority queue are empty.
+        // The trailing updateTimer -> nextDeliveryTime must not throw.
+        long firstLedgerId = 50L;
+        TrackerWithStorage ts = createTrackerWithMockLedger(firstLedgerId, 50);
+
+        // Five delayed messages on the same orphaned ledger (ledgerId < firstLedgerId).
+        // They share a mutable bucket because seal requires a strictly greater ledgerId.
+        // Timestamps are 100ms apart so each lands in its own snapshot segment
+        // (timeStep=10ms); only the first segment is loaded into the shared queue at seal.
+        for (int i = 1; i <= 5; i++) {
+            ts.tracker.addMessage(1, i, i * 100);
+        }
+        // A new orphaned ledgerId triggers the seal, producing immutable bucket [1..1]
+        // with 5 messages across 5 segments; shared queue holds just the first segment.
+        ts.tracker.addMessage(2, 1, 600);
+
+        Awaitility.await().untilAsserted(() ->
+                Assert.assertTrue(ts.tracker.getImmutableBuckets().asMapOfRanges().values().stream()
+                        .noneMatch(x -> x.merging)));
+
+        // In strict deliver-at mode getCutoffTime() is just clock.millis(), so advancing the
+        // clock past the trigger message's deliverAt (600) is enough for
+        // moveScheduledMessageToSharedQueue to flush the mutable bucket into the shared queue.
+        ts.clockTime.set(700);
+
+        // Both queues end up empty (filter pops the two in-memory messages), but
+        // numberDelayedMessages is still 4 (segments 2..5 remain in storage).
+        NavigableSet<Position> scheduledMessages = ts.tracker.getScheduledMessages(10);
+        assertTrue(scheduledMessages.isEmpty(),
+                "Orphaned messages should be filtered out, not returned");
+        assertTrue(ts.tracker.getNumberOfDelayedMessages() > 0,
+                "Remaining storage-only messages should keep the counter > 0");
+
+        // hasMessageAvailable calls nextDeliveryTime while numberDelayedMessages > 0;
+        // it must not throw IAE.
+        assertFalse(ts.tracker.hasMessageAvailable());
+
+        ts.close();
+    }
 }