You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by "oneby-wang (via GitHub)" <gi...@apache.org> on 2026/01/06 04:48:48 UTC

[PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

oneby-wang opened a new pull request, #25127:
URL: https://github.com/apache/pulsar/pull/25127

   <!-- Either this PR fixes an issue, -->
   
   Fixes https://github.com/apache/pulsar/issues/25083
   
   ### Motivation
   
   Use client-side looping instead of increasing broker settings to avoid potential HTTP call timeout in analyze-backlog method of Topics.
   
   ### Modifications
   
   Add client-side looping, add test.
   
   ### Verifying this change
   
   - [x] Make sure that the change passes the CI checks.
   
   ### Does this pull request potentially affect one of the following parts:
   
   <!-- DO NOT REMOVE THIS SECTION. CHECK THE PROPER BOX ONLY. -->
   
   *If the box was checked, please highlight the changes*
   
   - [ ] Dependencies (add or upgrade a dependency)
   - [ ] The public API
   - [ ] The schema
   - [ ] The default values of configurations
   - [ ] The threading model
   - [ ] The binary protocol
   - [ ] The REST endpoints
   - [ ] The admin CLI options
   - [ ] The metrics
   - [ ] Anything that affects deployment
   
   ### Documentation
   
   <!-- DO NOT REMOVE THIS SECTION. CHECK THE PROPER BOX ONLY. -->
   
   - [ ] `doc` <!-- Your PR contains doc changes. -->
   - [ ] `doc-required` <!-- Your PR changes impact docs and you will update later -->
   - [x] `doc-not-needed` <!-- Your PR changes do not impact docs -->
   - [ ] `doc-complete` <!-- Docs have been already added -->
   
   ### Matching PR in forked repository
   
   PR in forked repository: https://github.com/oneby-wang/pulsar/pull/22


-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2767679181


##########
managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/OpScan.java:
##########
@@ -69,29 +69,23 @@ public void readEntriesComplete(List<Entry> entries, Object ctx) {
         try {
             Position lastPositionForBatch = entries.get(entries.size() - 1).getPosition();
             lastSeenPosition = lastPositionForBatch;
-            // filter out the entry if it has been already deleted
-            // filterReadEntries will call entry.release if the entry is filtered out
-            List<Entry> entriesFiltered = this.cursor.filterReadEntries(entries);
-            int skippedEntries = entries.size() - entriesFiltered.size();
-            remainingEntries.addAndGet(-skippedEntries);
-            if (!entriesFiltered.isEmpty()) {
-                for (Entry entry : entriesFiltered) {
-                    if (remainingEntries.decrementAndGet() <= 0) {
-                        log.warn("[{}] Scan abort after reading too many entries", OpScan.this.cursor);
-                        callback.scanComplete(lastSeenPosition, ScanOutcome.ABORTED, OpScan.this.ctx);
-                        return;
-                    }
-                    if (!condition.test(entry)) {
-                        log.warn("[{}] Scan abort due to user code", OpScan.this.cursor);
-                        callback.scanComplete(lastSeenPosition, ScanOutcome.USER_INTERRUPTED, OpScan.this.ctx);
-                        return;
-                    }
+            for (Entry entry : entries) {
+                if (remainingEntries.getAndDecrement() <= 0) {
+                    log.warn("[{}] Scan abort after reading too many entries", OpScan.this.cursor);

Review Comment:
   Think so. Addressed.



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2764483189


##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1613,103 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        return analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPosition,
+                (backlogResult) -> backlogResult.getEntries() >= backlogScanMaxEntries);
+    }
+
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                       String subscriptionName, Optional<MessageId> startPosition,
+                                                       Predicate<AnalyzeSubscriptionBacklogResult> terminatePredicate) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || terminatePredicate.test(mergedResult)) {
+                    future.complete(mergedResult);
+                    return;
+                }
+
+                // To avoid infinite loops, we ensure the entry count is incremented after each loop.
+                // Should never happen.
+                if (currentResult.getEntries() <= 0) {
+                    log.warn("[{}][{}] Scanned total entry count is zero or negative, abort analyze backlog, start "
+                            + "position is: {}", topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect total entry count returned from server"));

Review Comment:
   I think that this shouldn't complete exceptionally. I would assume that it's possible to have a case where there's exactly the requested amount of entries available on the broker side and it would return the results because of reaching the max limit of an individual call. On the next call, the request would return 0 entries.



##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1613,103 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        return analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPosition,
+                (backlogResult) -> backlogResult.getEntries() >= backlogScanMaxEntries);
+    }
+
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                       String subscriptionName, Optional<MessageId> startPosition,
+                                                       Predicate<AnalyzeSubscriptionBacklogResult> terminatePredicate) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || terminatePredicate.test(mergedResult)) {
+                    future.complete(mergedResult);
+                    return;
+                }
+
+                // To avoid infinite loops, we ensure the entry count is incremented after each loop.
+                // Should never happen.
+                if (currentResult.getEntries() <= 0) {
+                    log.warn("[{}][{}] Scanned total entry count is zero or negative, abort analyze backlog, start "
+                            + "position is: {}", topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect total entry count returned from server"));
+                    return;
+                }
+
+                // In analyze-backlog, lastMessageId is null only when: total entries is 0, with false aborted flag
+                // returned. Should never happen.
+                if (StringUtils.isBlank(mergedResult.getLastMessageId())) {
+                    log.warn("[{}][{}] Scanned last message id is blank, abort analyze backlog, start position is: {}",
+                            topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(

Review Comment:
   If the given message id as input doesn't return any results, this would be expected. Similar case as mentioned in the previous comment.



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2710553326


##########
pulsar-client-admin-api/src/main/java/org/apache/pulsar/client/admin/Topics.java:
##########
@@ -2220,21 +2220,88 @@ AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String
      * This is a potentially expensive operation, as it requires
      * to read the messages from storage.
      * This function takes into consideration batch messages
-     * and also Subscription filters.
+     * and also Subscription filters. <br/>
+     * See also: {@link #analyzeSubscriptionBacklogAsync(String, String, Optional, long)}
      * @param topic
      *            Topic name
      * @param subscriptionName
      *            the subscription
      * @param startPosition
      *           the position to start the scan from (empty means the last processed message)
+     * @param backlogScanMaxEntries
+     *           the maximum number of backlog entries the client will scan before terminating its loop
      * @return an accurate analysis of the backlog
      * @throws PulsarAdminException
      *            Unexpected error
      */
+    AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String subscriptionName,
+                                                                Optional<MessageId> startPosition,
+                                                                long backlogScanMaxEntries) throws PulsarAdminException;
+
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     * @param topic
+     *            Topic name
+     * @param subscriptionName
+     *            the subscription
+     * @param startPosition
+     *           the position to start the scan from (empty means the last processed message)
+     * @return an accurate analysis of the backlog
+     */
     CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
                                                                            String subscriptionName,
                                                                            Optional<MessageId> startPosition);
 
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     *
+     *<p>
+     * What's the purpose of this overloaded method? <br/>
+     * There are broker side configurable maximum limits how many entries will be read and how long the scanning can
+     * take. The subscriptionBacklogScanMaxTimeMs (default 2 minutes) and subscriptionBacklogScanMaxEntries
+     * (default 10000) control this behavior. <br/>
+     * Increasing these settings is possible. However, it's possible that the HTTP request times out (also idle timeout
+     * in NAT/firewall etc.) before the command completes so increasing the limits might not be useful beyond a few
+     * minutes.
+     *</p>
+     *
+     *<p>
+     * How does this method work? <br/>
+     * 1. Add a new parameter backlogScanMaxEntries in client side method to control the client-side loop termination
+     *    condition. <br/>
+     * 2. If subscriptionBacklogScanMaxEntries(server side) >= backlogScanMaxEntries(client side), then
+     *    backlogScanMaxEntries parameter will take no effect. <br/>
+     * 3. If subscriptionBacklogScanMaxEntries < backlogScanMaxEntries, the client will call analyze-backlog method in
+     *    a loop until server return ScanOutcome.COMPLETED or the total entries exceeds backlogScanMaxEntries. <br/>
+     * 4. This means that backlogScanMaxEntries cannot be used to precisely control the number of entries scanned by
+     *    the server, it only serves to determine when the loop should terminate. <br/>
+     * 5. With this method, the server can reduce the values of the two parameters subscriptionBacklogScanMaxTimeMs and
+     *    subscriptionBacklogScanMaxEntries, so user can retrieve the desired number of backlog entries through
+     *    client-side looping.
+     *</p>
+     * @param topic
+     *            Topic name
+     * @param subscriptionName
+     *            the subscription
+     * @param startPosition
+     *           the position to start the scan from (empty means the last processed message)
+     * @param backlogScanMaxEntries
+     *           the maximum number of backlog entries the client will scan before terminating its loop
+     * @return an accurate analysis of the backlog
+     */
+    CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                            String subscriptionName,
+                                                                            Optional<MessageId> startPosition,
+                                                                            long backlogScanMaxEntries);

Review Comment:
   Nice suggestion!I'd prefer to get this PR merged first, so the admin CLI in https://github.com/apache/pulsar/pull/25126 could reuse the code.



##########
pulsar-client-admin-api/src/main/java/org/apache/pulsar/client/admin/Topics.java:
##########
@@ -2220,21 +2220,88 @@ AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String
      * This is a potentially expensive operation, as it requires
      * to read the messages from storage.
      * This function takes into consideration batch messages
-     * and also Subscription filters.
+     * and also Subscription filters. <br/>
+     * See also: {@link #analyzeSubscriptionBacklogAsync(String, String, Optional, long)}
      * @param topic
      *            Topic name
      * @param subscriptionName
      *            the subscription
      * @param startPosition
      *           the position to start the scan from (empty means the last processed message)
+     * @param backlogScanMaxEntries
+     *           the maximum number of backlog entries the client will scan before terminating its loop
      * @return an accurate analysis of the backlog
      * @throws PulsarAdminException
      *            Unexpected error
      */
+    AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String subscriptionName,
+                                                                Optional<MessageId> startPosition,
+                                                                long backlogScanMaxEntries) throws PulsarAdminException;
+
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     * @param topic
+     *            Topic name
+     * @param subscriptionName
+     *            the subscription
+     * @param startPosition
+     *           the position to start the scan from (empty means the last processed message)
+     * @return an accurate analysis of the backlog
+     */
     CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
                                                                            String subscriptionName,
                                                                            Optional<MessageId> startPosition);
 
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     *
+     *<p>
+     * What's the purpose of this overloaded method? <br/>
+     * There are broker side configurable maximum limits how many entries will be read and how long the scanning can
+     * take. The subscriptionBacklogScanMaxTimeMs (default 2 minutes) and subscriptionBacklogScanMaxEntries
+     * (default 10000) control this behavior. <br/>
+     * Increasing these settings is possible. However, it's possible that the HTTP request times out (also idle timeout
+     * in NAT/firewall etc.) before the command completes so increasing the limits might not be useful beyond a few
+     * minutes.
+     *</p>
+     *
+     *<p>
+     * How does this method work? <br/>
+     * 1. Add a new parameter backlogScanMaxEntries in client side method to control the client-side loop termination
+     *    condition. <br/>
+     * 2. If subscriptionBacklogScanMaxEntries(server side) >= backlogScanMaxEntries(client side), then
+     *    backlogScanMaxEntries parameter will take no effect. <br/>
+     * 3. If subscriptionBacklogScanMaxEntries < backlogScanMaxEntries, the client will call analyze-backlog method in
+     *    a loop until server return ScanOutcome.COMPLETED or the total entries exceeds backlogScanMaxEntries. <br/>
+     * 4. This means that backlogScanMaxEntries cannot be used to precisely control the number of entries scanned by
+     *    the server, it only serves to determine when the loop should terminate. <br/>
+     * 5. With this method, the server can reduce the values of the two parameters subscriptionBacklogScanMaxTimeMs and
+     *    subscriptionBacklogScanMaxEntries, so user can retrieve the desired number of backlog entries through
+     *    client-side looping.
+     *</p>
+     * @param topic
+     *            Topic name
+     * @param subscriptionName
+     *            the subscription
+     * @param startPosition
+     *           the position to start the scan from (empty means the last processed message)
+     * @param backlogScanMaxEntries
+     *           the maximum number of backlog entries the client will scan before terminating its loop
+     * @return an accurate analysis of the backlog
+     */
+    CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                            String subscriptionName,
+                                                                            Optional<MessageId> startPosition,
+                                                                            long backlogScanMaxEntries);

Review Comment:
   Nice suggestion! I'd prefer to get this PR merged first, so the admin CLI in https://github.com/apache/pulsar/pull/25126 could reuse the code.



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2671791952


##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1604,92 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || mergedResult.getEntries() >= backlogScanMaxEntries) {
+                    future.complete(mergedResult);
+                    return;
+                }
+
+                // To avoid infinite loops, we ensure the entry count is incremented after each loop.
+                if (currentResult.getEntries() <= 0) {
+                    log.warn("[{}][{}] Scanned total entry count is null, abort analyze backlog, start position is: {}",
+                            topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect total entry count returned from server"));
+                }
+
+                // In analyze-backlog, lastMessageId is null only when: total entries is 0,
+                // with false aborted flag returned.
+                if (StringUtils.isBlank(mergedResult.getLastMessageId())) {
+                    log.warn("[{}][{}] Scanned last message id is blank, abort analyze backlog, start position is: {}",
+                            topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect last message id returned from server"));
+                }
+
+                String[] messageIdSplits = mergedResult.getLastMessageId().split(":");
+                MessageIdImpl nextScanMessageId =
+                        new MessageIdImpl(Long.parseLong(messageIdSplits[0]), Long.parseLong(messageIdSplits[1]) + 1,
+                                partitionIndex);

Review Comment:
   Actually, `lastMessageId` is returned by the server and is guaranteed to be valid. Keep it simple here.



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#issuecomment-3852533546

   /pulsarbot rerun-failure-checks


-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#issuecomment-3851994328

   Thanks for your great suggestions! @lhotari 
   


-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2671812514


##########
managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/OpScan.java:
##########
@@ -69,29 +69,23 @@ public void readEntriesComplete(List<Entry> entries, Object ctx) {
         try {
             Position lastPositionForBatch = entries.get(entries.size() - 1).getPosition();
             lastSeenPosition = lastPositionForBatch;
-            // filter out the entry if it has been already deleted
-            // filterReadEntries will call entry.release if the entry is filtered out
-            List<Entry> entriesFiltered = this.cursor.filterReadEntries(entries);
-            int skippedEntries = entries.size() - entriesFiltered.size();
-            remainingEntries.addAndGet(-skippedEntries);
-            if (!entriesFiltered.isEmpty()) {
-                for (Entry entry : entriesFiltered) {
-                    if (remainingEntries.decrementAndGet() <= 0) {
-                        log.warn("[{}] Scan abort after reading too many entries", OpScan.this.cursor);
-                        callback.scanComplete(lastSeenPosition, ScanOutcome.ABORTED, OpScan.this.ctx);
-                        return;
-                    }
-                    if (!condition.test(entry)) {
-                        log.warn("[{}] Scan abort due to user code", OpScan.this.cursor);
-                        callback.scanComplete(lastSeenPosition, ScanOutcome.USER_INTERRUPTED, OpScan.this.ctx);
-                        return;
-                    }
+            for (Entry entry : entries) {
+                if (remainingEntries.getAndDecrement() <= 0) {
+                    log.warn("[{}] Scan abort after reading too many entries", OpScan.this.cursor);
+                    callback.scanComplete(lastSeenPosition, ScanOutcome.ABORTED, OpScan.this.ctx);
+                    return;
+                }
+                if (!condition.test(entry)) {
+                    log.warn("[{}] Scan abort due to user code", OpScan.this.cursor);
+                    callback.scanComplete(lastSeenPosition, ScanOutcome.USER_INTERRUPTED, OpScan.this.ctx);
+                    return;
                 }
             }

Review Comment:
   Filter process is already handled by `OpReadEntry`. I think filtering is not needed here.



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2671561014


##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1604,92 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || mergedResult.getEntries() >= backlogScanMaxEntries) {
+                    future.complete(mergedResult);
+                    return;
+                }
+
+                // To avoid infinite loops, we ensure the entry count is incremented after each loop.
+                if (currentResult.getEntries() <= 0) {
+                    log.warn("[{}][{}] Scanned total entry count is null, abort analyze backlog, start position is: {}",
+                            topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect total entry count returned from server"));
+                }
+
+                // In analyze-backlog, lastMessageId is null only when: total entries is 0,
+                // with false aborted flag returned.
+                if (StringUtils.isBlank(mergedResult.getLastMessageId())) {
+                    log.warn("[{}][{}] Scanned last message id is blank, abort analyze backlog, start position is: {}",
+                            topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect last message id returned from server"));
+                }

Review Comment:
   After completing the future exceptionally, the method should return to prevent further execution. Without a return statement, the code continues to execute lines 1651-1657, which will attempt to parse the lastMessageId and schedule another iteration even though an error has already been reported.



##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1604,92 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || mergedResult.getEntries() >= backlogScanMaxEntries) {
+                    future.complete(mergedResult);
+                    return;
+                }
+
+                // To avoid infinite loops, we ensure the entry count is incremented after each loop.
+                if (currentResult.getEntries() <= 0) {
+                    log.warn("[{}][{}] Scanned total entry count is null, abort analyze backlog, start position is: {}",
+                            topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect total entry count returned from server"));
+                }
+
+                // In analyze-backlog, lastMessageId is null only when: total entries is 0,
+                // with false aborted flag returned.
+                if (StringUtils.isBlank(mergedResult.getLastMessageId())) {
+                    log.warn("[{}][{}] Scanned last message id is blank, abort analyze backlog, start position is: {}",
+                            topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect last message id returned from server"));
+                }
+
+                String[] messageIdSplits = mergedResult.getLastMessageId().split(":");
+                MessageIdImpl nextScanMessageId =
+                        new MessageIdImpl(Long.parseLong(messageIdSplits[0]), Long.parseLong(messageIdSplits[1]) + 1,
+                                partitionIndex);

Review Comment:
   The message ID parsing logic is fragile and lacks error handling. If the lastMessageId format is unexpected (e.g., doesn't contain a colon, has non-numeric values, or has fewer than 2 parts), this will throw ArrayIndexOutOfBoundsException or NumberFormatException, causing an unhandled exception. Consider adding validation and proper error handling for the parsing operation.
   ```suggestion
                       return;
                   }
   
                   String[] messageIdSplits = mergedResult.getLastMessageId().split(":");
                   if (messageIdSplits.length < 2) {
                       log.warn("[{}][{}] Scanned last message id has unexpected format '{}', "
                                       + "abort analyze backlog, start position is: {}",
                               topic, subscriptionName, mergedResult.getLastMessageId(), startPositionRef.get());
                       future.completeExceptionally(
                               new PulsarAdminException("Incorrect last message id format returned from server"));
                       return;
                   }
   
                   final long ledgerId;
                   final long entryId;
                   try {
                       ledgerId = Long.parseLong(messageIdSplits[0]);
                       entryId = Long.parseLong(messageIdSplits[1]);
                   } catch (NumberFormatException e) {
                       log.warn("[{}][{}] Failed to parse last message id '{}', abort analyze backlog, "
                                       + "start position is: {}",
                               topic, subscriptionName, mergedResult.getLastMessageId(), startPositionRef.get(), e);
                       future.completeExceptionally(
                               new PulsarAdminException("Incorrect last message id format returned from server", e));
                       return;
                   }
   
                   MessageIdImpl nextScanMessageId =
                           new MessageIdImpl(ledgerId, entryId + 1, partitionIndex);
   ```



##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1604,92 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || mergedResult.getEntries() >= backlogScanMaxEntries) {
+                    future.complete(mergedResult);
+                    return;
+                }
+
+                // To avoid infinite loops, we ensure the entry count is incremented after each loop.
+                if (currentResult.getEntries() <= 0) {
+                    log.warn("[{}][{}] Scanned total entry count is null, abort analyze backlog, start position is: {}",
+                            topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect total entry count returned from server"));
+                }
+
+                // In analyze-backlog, lastMessageId is null only when: total entries is 0,
+                // with false aborted flag returned.
+                if (StringUtils.isBlank(mergedResult.getLastMessageId())) {
+                    log.warn("[{}][{}] Scanned last message id is blank, abort analyze backlog, start position is: {}",
+                            topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect last message id returned from server"));
+                }
+
+                String[] messageIdSplits = mergedResult.getLastMessageId().split(":");
+                MessageIdImpl nextScanMessageId =
+                        new MessageIdImpl(Long.parseLong(messageIdSplits[0]), Long.parseLong(messageIdSplits[1]) + 1,
+                                partitionIndex);
+                startPositionRef.set(Optional.of(nextScanMessageId));
+
+                resultSupplier.get().whenComplete(this);
+            }
+        };
+
+        resultSupplier.get().whenComplete(completeAction);
+        return future;
+    }
+
+    private AnalyzeSubscriptionBacklogResult mergeBacklogResults(AnalyzeSubscriptionBacklogResult current,
+                                                                 AnalyzeSubscriptionBacklogResult previous) {
+        if (previous == null) {
+            return current;
+        }
+
+        AnalyzeSubscriptionBacklogResult mergedRes = new AnalyzeSubscriptionBacklogResult();
+        mergedRes.setEntries(current.getEntries() + previous.getEntries());
+        mergedRes.setMessages(current.getMessages() + previous.getMessages());
+        mergedRes.setMarkerMessages(current.getMarkerMessages() + previous.getMarkerMessages());
+
+        mergedRes.setFilterAcceptedEntries(current.getFilterAcceptedEntries() + previous.getFilterAcceptedEntries());
+        mergedRes.setFilterRejectedEntries(current.getFilterRejectedEntries() + previous.getFilterRejectedEntries());
+        mergedRes.setFilterRescheduledEntries(
+                current.getFilterRescheduledEntries() + previous.getFilterRescheduledEntries());
+
+        mergedRes.setFilterAcceptedMessages(current.getFilterAcceptedMessages() + previous.getFilterAcceptedMessages());
+        mergedRes.setFilterRejectedMessages(current.getFilterRejectedMessages() + previous.getFilterRejectedMessages());
+        mergedRes.setFilterRescheduledMessages(
+                current.getFilterRescheduledMessages() + previous.getFilterRescheduledMessages());
+
+        mergedRes.setAborted(current.isAborted());
+        mergedRes.setFirstMessageId(current.getFirstMessageId());

Review Comment:
   The mergeBacklogResults method should use the firstMessageId from the previous result (if available) instead of always using the current result's firstMessageId. When merging multiple scan results, the first message ID should refer to the very first message from the initial scan, not the first message from the latest scan iteration.
   ```suggestion
           mergedRes.setFirstMessageId(previous.getFirstMessageId() != null
                   ? previous.getFirstMessageId()
                   : current.getFirstMessageId());
   ```



##########
pulsar-client-admin-api/src/main/java/org/apache/pulsar/client/admin/Topics.java:
##########
@@ -2220,21 +2220,88 @@ AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String
      * This is a potentially expensive operation, as it requires
      * to read the messages from storage.
      * This function takes into consideration batch messages
-     * and also Subscription filters.
+     * and also Subscription filters. <br/>
+     * See also: {@link #analyzeSubscriptionBacklogAsync(String, String, Optional, long)}
      * @param topic
      *            Topic name
      * @param subscriptionName
      *            the subscription
      * @param startPosition
      *           the position to start the scan from (empty means the last processed message)
+     * @param backlogScanMaxEntries
+     *           the maximum number of backlog entries the client will scan before terminating its loop
      * @return an accurate analysis of the backlog
      * @throws PulsarAdminException
      *            Unexpected error
      */
+    AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String subscriptionName,
+                                                                Optional<MessageId> startPosition,
+                                                                long backlogScanMaxEntries) throws PulsarAdminException;
+
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     * @param topic
+     *            Topic name
+     * @param subscriptionName
+     *            the subscription
+     * @param startPosition
+     *           the position to start the scan from (empty means the last processed message)
+     * @return an accurate analysis of the backlog
+     */
     CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
                                                                            String subscriptionName,
                                                                            Optional<MessageId> startPosition);
 
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     *
+     *<p>
+     * What's the purpose of this overloaded method? <br/>
+     * There are broker side configurable maximum limits how many entries will be read and how long the scanning can
+     * take. The subscriptionBacklogScanMaxTimeMs (default 2 minutes) and subscriptionBacklogScanMaxEntries
+     * (default 10000) control this behavior. <br/>
+     * Increasing these settings is possible. However, it's possible that the HTTP request times out (also idle timeout
+     * in NAT/firewall etc.) before the command completes so increasing the limits might not be useful beyond a few
+     * minutes.
+     *<p/>

Review Comment:
   There's a formatting inconsistency in the documentation. The opening `<p>` tag uses lowercase, but the closing tag uses uppercase and has an extra forward slash (`<p/>` instead of `</p>`). While HTML tags are generally case-insensitive, it's better to be consistent. Consider using either `<p>...</p>` or removing the tags if they're not needed in the JavaDoc.



##########
pulsar-client-admin-api/src/main/java/org/apache/pulsar/client/admin/Topics.java:
##########
@@ -2220,21 +2220,88 @@ AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String
      * This is a potentially expensive operation, as it requires
      * to read the messages from storage.
      * This function takes into consideration batch messages
-     * and also Subscription filters.
+     * and also Subscription filters. <br/>
+     * See also: {@link #analyzeSubscriptionBacklogAsync(String, String, Optional, long)}
      * @param topic
      *            Topic name
      * @param subscriptionName
      *            the subscription
      * @param startPosition
      *           the position to start the scan from (empty means the last processed message)
+     * @param backlogScanMaxEntries
+     *           the maximum number of backlog entries the client will scan before terminating its loop
      * @return an accurate analysis of the backlog
      * @throws PulsarAdminException
      *            Unexpected error
      */
+    AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String subscriptionName,
+                                                                Optional<MessageId> startPosition,
+                                                                long backlogScanMaxEntries) throws PulsarAdminException;
+
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     * @param topic
+     *            Topic name
+     * @param subscriptionName
+     *            the subscription
+     * @param startPosition
+     *           the position to start the scan from (empty means the last processed message)
+     * @return an accurate analysis of the backlog
+     */
     CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
                                                                            String subscriptionName,
                                                                            Optional<MessageId> startPosition);
 
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     *
+     *<p>
+     * What's the purpose of this overloaded method? <br/>
+     * There are broker side configurable maximum limits how many entries will be read and how long the scanning can
+     * take. The subscriptionBacklogScanMaxTimeMs (default 2 minutes) and subscriptionBacklogScanMaxEntries
+     * (default 10000) control this behavior. <br/>
+     * Increasing these settings is possible. However, it's possible that the HTTP request times out (also idle timeout
+     * in NAT/firewall etc.) before the command completes so increasing the limits might not be useful beyond a few
+     * minutes.
+     *<p/>
+     *
+     *<p>
+     * How does this method work? <br/>
+     * 1. Add a new parameter backlogScanMaxEntries in client side method to control the client-side loop termination
+     *    condition. <br/>
+     * 2. If subscriptionBacklogScanMaxEntries(server side) >= backlogScanMaxEntries(client side), then
+     *    backlogScanMaxEntries parameter will take no effect. <br/>
+     * 3. If subscriptionBacklogScanMaxEntries < backlogScanMaxEntries, the client will call analyze-backlog method in
+     *    a loop until server return ScanOutcome.COMPLETED or the total entries exceeds backlogScanMaxEntries. <br/>
+     * 4. This means that backlogScanMaxEntries cannot be used to precisely control the number of entries scanned by
+     *    the server, it only serves to determine when the loop should terminate. <br/>
+     * 5. With this method, the server can reduce the values of the two parameters subscriptionBacklogScanMaxTimeMs and
+     *    subscriptionBacklogScanMaxEntries, so user can retrieve the desired number of backlog entries through
+     *    client-side looping.
+     *<p/>

Review Comment:
   There's a formatting inconsistency in the documentation. The opening `<p>` tag uses lowercase, but the closing tag uses uppercase and has an extra forward slash (`<p/>` instead of `</p>`). While HTML tags are generally case-insensitive, it's better to be consistent. Consider using either `<p>...</p>` or removing the tags if they're not needed in the JavaDoc.



##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1604,92 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || mergedResult.getEntries() >= backlogScanMaxEntries) {
+                    future.complete(mergedResult);
+                    return;
+                }
+
+                // To avoid infinite loops, we ensure the entry count is incremented after each loop.
+                if (currentResult.getEntries() <= 0) {
+                    log.warn("[{}][{}] Scanned total entry count is null, abort analyze backlog, start position is: {}",
+                            topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect total entry count returned from server"));
+                }

Review Comment:
   After completing the future exceptionally, the method should return to prevent further execution. Without a return statement, the code continues to execute lines 1644-1657, which will attempt to parse the lastMessageId and schedule another iteration even though an error has already been reported.



##########
managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/OpScan.java:
##########
@@ -69,29 +69,23 @@ public void readEntriesComplete(List<Entry> entries, Object ctx) {
         try {
             Position lastPositionForBatch = entries.get(entries.size() - 1).getPosition();
             lastSeenPosition = lastPositionForBatch;
-            // filter out the entry if it has been already deleted
-            // filterReadEntries will call entry.release if the entry is filtered out
-            List<Entry> entriesFiltered = this.cursor.filterReadEntries(entries);
-            int skippedEntries = entries.size() - entriesFiltered.size();
-            remainingEntries.addAndGet(-skippedEntries);
-            if (!entriesFiltered.isEmpty()) {
-                for (Entry entry : entriesFiltered) {
-                    if (remainingEntries.decrementAndGet() <= 0) {
-                        log.warn("[{}] Scan abort after reading too many entries", OpScan.this.cursor);
-                        callback.scanComplete(lastSeenPosition, ScanOutcome.ABORTED, OpScan.this.ctx);
-                        return;
-                    }
-                    if (!condition.test(entry)) {
-                        log.warn("[{}] Scan abort due to user code", OpScan.this.cursor);
-                        callback.scanComplete(lastSeenPosition, ScanOutcome.USER_INTERRUPTED, OpScan.this.ctx);
-                        return;
-                    }
+            for (Entry entry : entries) {
+                if (remainingEntries.getAndDecrement() <= 0) {
+                    log.warn("[{}] Scan abort after reading too many entries", OpScan.this.cursor);
+                    callback.scanComplete(lastSeenPosition, ScanOutcome.ABORTED, OpScan.this.ctx);
+                    return;
+                }
+                if (!condition.test(entry)) {
+                    log.warn("[{}] Scan abort due to user code", OpScan.this.cursor);
+                    callback.scanComplete(lastSeenPosition, ScanOutcome.USER_INTERRUPTED, OpScan.this.ctx);
+                    return;
                 }
             }

Review Comment:
   This change fundamentally alters the behavior of the scan operation by removing the filtering of individually deleted entries. The removed code filtered out entries that were already individually deleted and didn't count them against the scan limits. Without this filtering, deleted entries are now counted and processed, which may cause the scan to abort prematurely when hitting the entry limit. This appears to be an unintended change that's not mentioned in the PR description, which only discusses adding client-side looping to avoid HTTP timeouts. Consider whether this behavioral change is intentional and document it if so.



##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1604,92 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || mergedResult.getEntries() >= backlogScanMaxEntries) {
+                    future.complete(mergedResult);
+                    return;
+                }
+
+                // To avoid infinite loops, we ensure the entry count is incremented after each loop.
+                if (currentResult.getEntries() <= 0) {
+                    log.warn("[{}][{}] Scanned total entry count is null, abort analyze backlog, start position is: {}",

Review Comment:
   The log message says "Scanned total entry count is null" but the condition checks if it's <= 0, not if it's null. The message should say "Scanned total entry count is zero or negative" to accurately describe the condition being checked.
   ```suggestion
                       log.warn("[{}][{}] Scanned total entry count is zero or negative, abort analyze backlog, start position is: {}",
   ```



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2767228687


##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1613,103 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        return analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPosition,
+                (backlogResult) -> backlogResult.getEntries() >= backlogScanMaxEntries);
+    }
+
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                       String subscriptionName, Optional<MessageId> startPosition,
+                                                       Predicate<AnalyzeSubscriptionBacklogResult> terminatePredicate) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || terminatePredicate.test(mergedResult)) {
+                    future.complete(mergedResult);
+                    return;
+                }
+
+                // To avoid infinite loops, we ensure the entry count is incremented after each loop.
+                // Should never happen.
+                if (currentResult.getEntries() <= 0) {
+                    log.warn("[{}][{}] Scanned total entry count is zero or negative, abort analyze backlog, start "
+                            + "position is: {}", topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect total entry count returned from server"));

Review Comment:
   Addressed.
   
   > I would assume that it's possible to have a case where there's exactly the requested amount of entries available on the broker side and it would return the results because of reaching the max limit of an individual call. On the next call, the request would return 0 entries.
   
   I think this would not happen. If `ScanOutcome != ScanOutcome.COMPLETED` in `OpScan`, there should be more entries to scan.
   
   But I strongly agree with the comment, completing exceptionally is less developer-friendly. Since the broker scan logic could be changed in the future, and it is every reasonable to treat 0 entries or a null `lastMessageId` as no more entries.
   
   `ScanOutcome.USER_INTERRUPTED` would not returned in `OpScan`, `ScanOutcome.COMPLETED` is returned in the following conditions:
   
   ```
   if (cursor.hasMoreEntries(searchPosition)) {
       OpReadEntry opReadEntry = OpReadEntry.create(cursor, searchPosition, batchSize,
       this, OpScan.this.ctx, null, null, false);
       ledger.asyncReadEntries(opReadEntry);
   } else {
       callback.scanComplete(lastSeenPosition, ScanOutcome.COMPLETED, OpScan.this.ctx);
   }
   ```
   
   or
   
   ```
   searchPosition = ledger.getPositionAfterN(lastPositionForBatch, 1,
                       PositionBound.startExcluded);
   if (searchPosition.compareTo(lastPositionForBatch) == 0) {
       // we have reached the end of the ledger, as we are not doing progress
       callback.scanComplete(lastSeenPosition, ScanOutcome.COMPLETED, OpScan.this.ctx);
       return;
   }
   ```
   



##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1613,103 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        return analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPosition,
+                (backlogResult) -> backlogResult.getEntries() >= backlogScanMaxEntries);
+    }
+
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                       String subscriptionName, Optional<MessageId> startPosition,
+                                                       Predicate<AnalyzeSubscriptionBacklogResult> terminatePredicate) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || terminatePredicate.test(mergedResult)) {
+                    future.complete(mergedResult);
+                    return;
+                }
+
+                // To avoid infinite loops, we ensure the entry count is incremented after each loop.
+                // Should never happen.
+                if (currentResult.getEntries() <= 0) {
+                    log.warn("[{}][{}] Scanned total entry count is zero or negative, abort analyze backlog, start "
+                            + "position is: {}", topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(
+                            new PulsarAdminException("Incorrect total entry count returned from server"));
+                    return;
+                }
+
+                // In analyze-backlog, lastMessageId is null only when: total entries is 0, with false aborted flag
+                // returned. Should never happen.
+                if (StringUtils.isBlank(mergedResult.getLastMessageId())) {
+                    log.warn("[{}][{}] Scanned last message id is blank, abort analyze backlog, start position is: {}",
+                            topic, subscriptionName, startPositionRef.get());
+                    future.completeExceptionally(

Review Comment:
   Addressed.



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2710688628


##########
pulsar-client-admin-api/src/main/java/org/apache/pulsar/client/admin/Topics.java:
##########
@@ -2220,21 +2220,88 @@ AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String
      * This is a potentially expensive operation, as it requires
      * to read the messages from storage.
      * This function takes into consideration batch messages
-     * and also Subscription filters.
+     * and also Subscription filters. <br/>
+     * See also: {@link #analyzeSubscriptionBacklogAsync(String, String, Optional, long)}
      * @param topic
      *            Topic name
      * @param subscriptionName
      *            the subscription
      * @param startPosition
      *           the position to start the scan from (empty means the last processed message)
+     * @param backlogScanMaxEntries
+     *           the maximum number of backlog entries the client will scan before terminating its loop
      * @return an accurate analysis of the backlog
      * @throws PulsarAdminException
      *            Unexpected error
      */
+    AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String subscriptionName,
+                                                                Optional<MessageId> startPosition,
+                                                                long backlogScanMaxEntries) throws PulsarAdminException;
+
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     * @param topic
+     *            Topic name
+     * @param subscriptionName
+     *            the subscription
+     * @param startPosition
+     *           the position to start the scan from (empty means the last processed message)
+     * @return an accurate analysis of the backlog
+     */
     CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
                                                                            String subscriptionName,
                                                                            Optional<MessageId> startPosition);
 
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     *
+     *<p>
+     * What's the purpose of this overloaded method? <br/>
+     * There are broker side configurable maximum limits how many entries will be read and how long the scanning can
+     * take. The subscriptionBacklogScanMaxTimeMs (default 2 minutes) and subscriptionBacklogScanMaxEntries
+     * (default 10000) control this behavior. <br/>
+     * Increasing these settings is possible. However, it's possible that the HTTP request times out (also idle timeout
+     * in NAT/firewall etc.) before the command completes so increasing the limits might not be useful beyond a few
+     * minutes.
+     *</p>
+     *
+     *<p>
+     * How does this method work? <br/>
+     * 1. Add a new parameter backlogScanMaxEntries in client side method to control the client-side loop termination
+     *    condition. <br/>
+     * 2. If subscriptionBacklogScanMaxEntries(server side) >= backlogScanMaxEntries(client side), then
+     *    backlogScanMaxEntries parameter will take no effect. <br/>
+     * 3. If subscriptionBacklogScanMaxEntries < backlogScanMaxEntries, the client will call analyze-backlog method in
+     *    a loop until server return ScanOutcome.COMPLETED or the total entries exceeds backlogScanMaxEntries. <br/>
+     * 4. This means that backlogScanMaxEntries cannot be used to precisely control the number of entries scanned by
+     *    the server, it only serves to determine when the loop should terminate. <br/>
+     * 5. With this method, the server can reduce the values of the two parameters subscriptionBacklogScanMaxTimeMs and
+     *    subscriptionBacklogScanMaxEntries, so user can retrieve the desired number of backlog entries through
+     *    client-side looping.
+     *</p>
+     * @param topic
+     *            Topic name
+     * @param subscriptionName
+     *            the subscription
+     * @param startPosition
+     *           the position to start the scan from (empty means the last processed message)
+     * @param backlogScanMaxEntries
+     *           the maximum number of backlog entries the client will scan before terminating its loop
+     * @return an accurate analysis of the backlog
+     */
+    CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                            String subscriptionName,
+                                                                            Optional<MessageId> startPosition,
+                                                                            long backlogScanMaxEntries);

Review Comment:
   Applied `java.util.function.Predicate<AnalyzeSubscriptionBacklogResult> terminatePredicate` as a parameter.



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2710688628


##########
pulsar-client-admin-api/src/main/java/org/apache/pulsar/client/admin/Topics.java:
##########
@@ -2220,21 +2220,88 @@ AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String
      * This is a potentially expensive operation, as it requires
      * to read the messages from storage.
      * This function takes into consideration batch messages
-     * and also Subscription filters.
+     * and also Subscription filters. <br/>
+     * See also: {@link #analyzeSubscriptionBacklogAsync(String, String, Optional, long)}
      * @param topic
      *            Topic name
      * @param subscriptionName
      *            the subscription
      * @param startPosition
      *           the position to start the scan from (empty means the last processed message)
+     * @param backlogScanMaxEntries
+     *           the maximum number of backlog entries the client will scan before terminating its loop
      * @return an accurate analysis of the backlog
      * @throws PulsarAdminException
      *            Unexpected error
      */
+    AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String subscriptionName,
+                                                                Optional<MessageId> startPosition,
+                                                                long backlogScanMaxEntries) throws PulsarAdminException;
+
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     * @param topic
+     *            Topic name
+     * @param subscriptionName
+     *            the subscription
+     * @param startPosition
+     *           the position to start the scan from (empty means the last processed message)
+     * @return an accurate analysis of the backlog
+     */
     CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
                                                                            String subscriptionName,
                                                                            Optional<MessageId> startPosition);
 
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     *
+     *<p>
+     * What's the purpose of this overloaded method? <br/>
+     * There are broker side configurable maximum limits how many entries will be read and how long the scanning can
+     * take. The subscriptionBacklogScanMaxTimeMs (default 2 minutes) and subscriptionBacklogScanMaxEntries
+     * (default 10000) control this behavior. <br/>
+     * Increasing these settings is possible. However, it's possible that the HTTP request times out (also idle timeout
+     * in NAT/firewall etc.) before the command completes so increasing the limits might not be useful beyond a few
+     * minutes.
+     *</p>
+     *
+     *<p>
+     * How does this method work? <br/>
+     * 1. Add a new parameter backlogScanMaxEntries in client side method to control the client-side loop termination
+     *    condition. <br/>
+     * 2. If subscriptionBacklogScanMaxEntries(server side) >= backlogScanMaxEntries(client side), then
+     *    backlogScanMaxEntries parameter will take no effect. <br/>
+     * 3. If subscriptionBacklogScanMaxEntries < backlogScanMaxEntries, the client will call analyze-backlog method in
+     *    a loop until server return ScanOutcome.COMPLETED or the total entries exceeds backlogScanMaxEntries. <br/>
+     * 4. This means that backlogScanMaxEntries cannot be used to precisely control the number of entries scanned by
+     *    the server, it only serves to determine when the loop should terminate. <br/>
+     * 5. With this method, the server can reduce the values of the two parameters subscriptionBacklogScanMaxTimeMs and
+     *    subscriptionBacklogScanMaxEntries, so user can retrieve the desired number of backlog entries through
+     *    client-side looping.
+     *</p>
+     * @param topic
+     *            Topic name
+     * @param subscriptionName
+     *            the subscription
+     * @param startPosition
+     *           the position to start the scan from (empty means the last processed message)
+     * @param backlogScanMaxEntries
+     *           the maximum number of backlog entries the client will scan before terminating its loop
+     * @return an accurate analysis of the backlog
+     */
+    CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                            String subscriptionName,
+                                                                            Optional<MessageId> startPosition,
+                                                                            long backlogScanMaxEntries);

Review Comment:
   Applied.



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#issuecomment-3852793863

   /pulsarbot rerun-failure-checks
   


-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2767614874


##########
managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/OpScan.java:
##########


Review Comment:
   Please change the log level to `info`.



##########
managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/OpScan.java:
##########
@@ -69,29 +69,23 @@ public void readEntriesComplete(List<Entry> entries, Object ctx) {
         try {
             Position lastPositionForBatch = entries.get(entries.size() - 1).getPosition();
             lastSeenPosition = lastPositionForBatch;
-            // filter out the entry if it has been already deleted
-            // filterReadEntries will call entry.release if the entry is filtered out
-            List<Entry> entriesFiltered = this.cursor.filterReadEntries(entries);
-            int skippedEntries = entries.size() - entriesFiltered.size();
-            remainingEntries.addAndGet(-skippedEntries);
-            if (!entriesFiltered.isEmpty()) {
-                for (Entry entry : entriesFiltered) {
-                    if (remainingEntries.decrementAndGet() <= 0) {
-                        log.warn("[{}] Scan abort after reading too many entries", OpScan.this.cursor);
-                        callback.scanComplete(lastSeenPosition, ScanOutcome.ABORTED, OpScan.this.ctx);
-                        return;
-                    }
-                    if (!condition.test(entry)) {
-                        log.warn("[{}] Scan abort due to user code", OpScan.this.cursor);
-                        callback.scanComplete(lastSeenPosition, ScanOutcome.USER_INTERRUPTED, OpScan.this.ctx);
-                        return;
-                    }
+            for (Entry entry : entries) {
+                if (remainingEntries.getAndDecrement() <= 0) {
+                    log.warn("[{}] Scan abort after reading too many entries", OpScan.this.cursor);

Review Comment:
   It's not a problem caused by this PR, but I think that `warn` level is too high for this log message. It would be expected that the broker would scan a certain amount of entries in a single request and therefore there's no need to warn about it. 
   
   Please change the log level to `info`.



##########
managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/OpScan.java:
##########


Review Comment:
   Please change the log level to `info`.



##########
managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/OpScan.java:
##########
@@ -69,29 +69,23 @@ public void readEntriesComplete(List<Entry> entries, Object ctx) {
         try {
             Position lastPositionForBatch = entries.get(entries.size() - 1).getPosition();
             lastSeenPosition = lastPositionForBatch;
-            // filter out the entry if it has been already deleted
-            // filterReadEntries will call entry.release if the entry is filtered out
-            List<Entry> entriesFiltered = this.cursor.filterReadEntries(entries);
-            int skippedEntries = entries.size() - entriesFiltered.size();
-            remainingEntries.addAndGet(-skippedEntries);
-            if (!entriesFiltered.isEmpty()) {
-                for (Entry entry : entriesFiltered) {
-                    if (remainingEntries.decrementAndGet() <= 0) {
-                        log.warn("[{}] Scan abort after reading too many entries", OpScan.this.cursor);
-                        callback.scanComplete(lastSeenPosition, ScanOutcome.ABORTED, OpScan.this.ctx);
-                        return;
-                    }
-                    if (!condition.test(entry)) {
-                        log.warn("[{}] Scan abort due to user code", OpScan.this.cursor);
-                        callback.scanComplete(lastSeenPosition, ScanOutcome.USER_INTERRUPTED, OpScan.this.ctx);
-                        return;
-                    }
+            for (Entry entry : entries) {
+                if (remainingEntries.getAndDecrement() <= 0) {
+                    log.warn("[{}] Scan abort after reading too many entries", OpScan.this.cursor);
+                    callback.scanComplete(lastSeenPosition, ScanOutcome.ABORTED, OpScan.this.ctx);
+                    return;
+                }
+                if (!condition.test(entry)) {
+                    log.warn("[{}] Scan abort due to user code", OpScan.this.cursor);

Review Comment:
   Please change the log level to `info`.



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2707526751


##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1604,94 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || mergedResult.getEntries() >= backlogScanMaxEntries) {

Review Comment:
   the `!mergedResult.isAborted()` part isn't correct.
   
   https://github.com/apache/pulsar/blob/da0d11644f097ae657e79b2eb7835478d79ccd0e/managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/OpScan.java#L119-L128
   
   The purpose of the loop is to keep on iterating until the result is COMPLETED or the `java.util.function.Predicate<AnalyzeSubscriptionBacklogResult> continuePredicate` returns false (condition in continuePredicate could be `result.getEntries() < backlogScanMaxEntries`).
   
   It seems that `conf.setSubscriptionBacklogScanMaxEntries(serverSubscriptionBacklogScanMaxEntries);` isn't effective in the tests since some of the tests should have failed.



##########
pulsar-client-admin-api/src/main/java/org/apache/pulsar/client/admin/Topics.java:
##########
@@ -2220,21 +2220,88 @@ AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String
      * This is a potentially expensive operation, as it requires
      * to read the messages from storage.
      * This function takes into consideration batch messages
-     * and also Subscription filters.
+     * and also Subscription filters. <br/>
+     * See also: {@link #analyzeSubscriptionBacklogAsync(String, String, Optional, long)}
      * @param topic
      *            Topic name
      * @param subscriptionName
      *            the subscription
      * @param startPosition
      *           the position to start the scan from (empty means the last processed message)
+     * @param backlogScanMaxEntries
+     *           the maximum number of backlog entries the client will scan before terminating its loop
      * @return an accurate analysis of the backlog
      * @throws PulsarAdminException
      *            Unexpected error
      */
+    AnalyzeSubscriptionBacklogResult analyzeSubscriptionBacklog(String topic, String subscriptionName,
+                                                                Optional<MessageId> startPosition,
+                                                                long backlogScanMaxEntries) throws PulsarAdminException;
+
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     * @param topic
+     *            Topic name
+     * @param subscriptionName
+     *            the subscription
+     * @param startPosition
+     *           the position to start the scan from (empty means the last processed message)
+     * @return an accurate analysis of the backlog
+     */
     CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
                                                                            String subscriptionName,
                                                                            Optional<MessageId> startPosition);
 
+    /**
+     * Analyze subscription backlog.
+     * This is a potentially expensive operation, as it requires
+     * to read the messages from storage.
+     * This function takes into consideration batch messages
+     * and also Subscription filters.
+     *
+     *<p>
+     * What's the purpose of this overloaded method? <br/>
+     * There are broker side configurable maximum limits how many entries will be read and how long the scanning can
+     * take. The subscriptionBacklogScanMaxTimeMs (default 2 minutes) and subscriptionBacklogScanMaxEntries
+     * (default 10000) control this behavior. <br/>
+     * Increasing these settings is possible. However, it's possible that the HTTP request times out (also idle timeout
+     * in NAT/firewall etc.) before the command completes so increasing the limits might not be useful beyond a few
+     * minutes.
+     *</p>
+     *
+     *<p>
+     * How does this method work? <br/>
+     * 1. Add a new parameter backlogScanMaxEntries in client side method to control the client-side loop termination
+     *    condition. <br/>
+     * 2. If subscriptionBacklogScanMaxEntries(server side) >= backlogScanMaxEntries(client side), then
+     *    backlogScanMaxEntries parameter will take no effect. <br/>
+     * 3. If subscriptionBacklogScanMaxEntries < backlogScanMaxEntries, the client will call analyze-backlog method in
+     *    a loop until server return ScanOutcome.COMPLETED or the total entries exceeds backlogScanMaxEntries. <br/>
+     * 4. This means that backlogScanMaxEntries cannot be used to precisely control the number of entries scanned by
+     *    the server, it only serves to determine when the loop should terminate. <br/>
+     * 5. With this method, the server can reduce the values of the two parameters subscriptionBacklogScanMaxTimeMs and
+     *    subscriptionBacklogScanMaxEntries, so user can retrieve the desired number of backlog entries through
+     *    client-side looping.
+     *</p>
+     * @param topic
+     *            Topic name
+     * @param subscriptionName
+     *            the subscription
+     * @param startPosition
+     *           the position to start the scan from (empty means the last processed message)
+     * @param backlogScanMaxEntries
+     *           the maximum number of backlog entries the client will scan before terminating its loop
+     * @return an accurate analysis of the backlog
+     */
+    CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                            String subscriptionName,
+                                                                            Optional<MessageId> startPosition,
+                                                                            long backlogScanMaxEntries);

Review Comment:
   It would be useful to have a method where `long backlogScanMaxEntries` would be replaced with `java.util.function.Predicate<AnalyzeSubscriptionBacklogResult> continuePredicate`. This would accept the accumulated `AnalyzeSubscriptionBacklogResult` as a parameter. The benefit of this is that the user of the API can choose to add logging when the accumulated result gets updated.
   
   The method signature with the `long backlogScanMaxEntries` can delegate to this method with the `java.util.function.Predicate<AnalyzeSubscriptionBacklogResult> continuePredicate` parameter.
   
   



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2671812514


##########
managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/OpScan.java:
##########
@@ -69,29 +69,23 @@ public void readEntriesComplete(List<Entry> entries, Object ctx) {
         try {
             Position lastPositionForBatch = entries.get(entries.size() - 1).getPosition();
             lastSeenPosition = lastPositionForBatch;
-            // filter out the entry if it has been already deleted
-            // filterReadEntries will call entry.release if the entry is filtered out
-            List<Entry> entriesFiltered = this.cursor.filterReadEntries(entries);
-            int skippedEntries = entries.size() - entriesFiltered.size();
-            remainingEntries.addAndGet(-skippedEntries);
-            if (!entriesFiltered.isEmpty()) {
-                for (Entry entry : entriesFiltered) {
-                    if (remainingEntries.decrementAndGet() <= 0) {
-                        log.warn("[{}] Scan abort after reading too many entries", OpScan.this.cursor);
-                        callback.scanComplete(lastSeenPosition, ScanOutcome.ABORTED, OpScan.this.ctx);
-                        return;
-                    }
-                    if (!condition.test(entry)) {
-                        log.warn("[{}] Scan abort due to user code", OpScan.this.cursor);
-                        callback.scanComplete(lastSeenPosition, ScanOutcome.USER_INTERRUPTED, OpScan.this.ctx);
-                        return;
-                    }
+            for (Entry entry : entries) {
+                if (remainingEntries.getAndDecrement() <= 0) {
+                    log.warn("[{}] Scan abort after reading too many entries", OpScan.this.cursor);
+                    callback.scanComplete(lastSeenPosition, ScanOutcome.ABORTED, OpScan.this.ctx);
+                    return;
+                }
+                if (!condition.test(entry)) {
+                    log.warn("[{}] Scan abort due to user code", OpScan.this.cursor);
+                    callback.scanComplete(lastSeenPosition, ScanOutcome.USER_INTERRUPTED, OpScan.this.ctx);
+                    return;
                 }
             }

Review Comment:
   Filter process is already handled by `OpReadEntry`. I think filtering here is not needed.



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari merged PR #25127:
URL: https://github.com/apache/pulsar/pull/25127


-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2710604267


##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1604,94 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || mergedResult.getEntries() >= backlogScanMaxEntries) {

Review Comment:
   > the !mergedResult.isAborted() part isn't correct.
   
   In `PersistentTopicsBase`, we convert `rawResult.getScanOutcome() != ScanOutcome.COMPLETED` to `boolean aborted`, and the orginal `scanOutcome` result is not returned back to the client, so I used `!aborted` to replace `scanOutcome == ScanOutcome.COMPLETED`.
   
   https://github.com/apache/pulsar/blob/85625e0f100479dd95fb1311aeb52411b6b0a25d/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/PersistentTopicsBase.java#L1704-L1709
   
   `OpScan` would only completes with `ScanOutcome.COMPLETED` if there is no more entries to scan.
   
   https://github.com/apache/pulsar/blob/85625e0f100479dd95fb1311aeb52411b6b0a25d/managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/OpScan.java#L97-L101
   
   https://github.com/apache/pulsar/blob/85625e0f100479dd95fb1311aeb52411b6b0a25d/managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/OpScan.java#L129-L135
   
   Only this if block returns the result of ScanOutcome.USER_INTERRUPTED.
   
   https://github.com/apache/pulsar/blob/85625e0f100479dd95fb1311aeb52411b6b0a25d/managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/OpScan.java#L84-L88
   
   In `analyzeBacklog()` method, we always return `true`, so the `scanOutcome` is `ScanOutcome.COMPLETED` or `ScanOutcome.ABORTED`.
   
   https://github.com/apache/pulsar/blob/85625e0f100479dd95fb1311aeb52411b6b0a25d/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/PersistentSubscription.java#L657-L710
   
   Should I add a field in `AnalyzeSubscriptionBacklogResult` to return the orginal `scanOutcome` result?



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [improve][admin] Add client side looping to analyze-backlog in Topics to avoid potential HTTP call timeout [pulsar]

Posted by "oneby-wang (via GitHub)" <gi...@apache.org>.
oneby-wang commented on code in PR #25127:
URL: https://github.com/apache/pulsar/pull/25127#discussion_r2710612049


##########
pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/TopicsImpl.java:
##########
@@ -1591,6 +1604,94 @@ public void failed(Throwable throwable) {
         return future;
     }
 
+    @Override
+    public CompletableFuture<AnalyzeSubscriptionBacklogResult> analyzeSubscriptionBacklogAsync(String topic,
+                                                                               String subscriptionName,
+                                                                               Optional<MessageId> startPosition,
+                                                                               long backlogScanMaxEntries) {
+        final CompletableFuture<AnalyzeSubscriptionBacklogResult> future = new CompletableFuture<>();
+        AtomicReference<AnalyzeSubscriptionBacklogResult> resultRef = new AtomicReference<>();
+        int partitionIndex = TopicName.get(topic).getPartitionIndex();
+        AtomicReference<Optional<MessageId>> startPositionRef = new AtomicReference<>(startPosition);
+
+        Supplier<CompletableFuture<AnalyzeSubscriptionBacklogResult>> resultSupplier =
+                () -> analyzeSubscriptionBacklogAsync(topic, subscriptionName, startPositionRef.get());
+        BiConsumer<AnalyzeSubscriptionBacklogResult, Throwable> completeAction = new BiConsumer<>() {
+            @Override
+            public void accept(AnalyzeSubscriptionBacklogResult currentResult, Throwable throwable) {
+                if (throwable != null) {
+                    future.completeExceptionally(throwable);
+                    return;
+                }
+
+                AnalyzeSubscriptionBacklogResult mergedResult = mergeBacklogResults(currentResult, resultRef.get());
+                resultRef.set(mergedResult);
+                if (!mergedResult.isAborted() || mergedResult.getEntries() >= backlogScanMaxEntries) {

Review Comment:
   > It seems that conf.setSubscriptionBacklogScanMaxEntries(serverSubscriptionBacklogScanMaxEntries); isn't effective in the tests since some of the tests should have failed.
   
   I use `conf.setSubscriptionBacklogScanMaxEntries(serverSubscriptionBacklogScanMaxEntries)` to crontol the loop condition. 
   
   I ran all these tests locally, and they all passed(as I expected). Please let me know which unit tests are expected to fail.
   



-- 
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: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org