This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 2a6810b99 fix(instance): stop a cancelled count batch before the
second lookup (#7142)
2a6810b99 is described below
commit 2a6810b99c62f0863f34f1592dc29d68ca0ad3c1
Author: btlqql <[email protected]>
AuthorDate: Mon Oct 26 20:50:33 2026 +0800
fix(instance): stop a cancelled count batch before the second lookup (#7142)
* fix(instance): stop a cancelled count batch before the second lookup
(#7077)
InstanceService.loadCounts started countGroups unconditionally after
countTopics. A batch that had
already reached its deadline or been cancelled interrupts its worker, but a
provider that ignores
the interrupt can still return the first count afterwards, so the worker
went on to start the
second provider lookup for a response nobody was waiting for.
loadCounts now checks the worker interrupt flag between the two lookups and
throws
InterruptedException when it is still set. The exception takes the runner's
existing failure path
(InstanceResourceCountRunner.execute -> CompletedCount.failure), so there
is no executor policy
change: the caller keeps its existing unavailable response and a healthy
batch still performs both
lookups. Thread.interrupted() consumes the flag the way any
InterruptedException producer does, so
the pooled worker does not inherit a stale interrupt for its next count
task.
Because that exception surfaces as a failed outcome, fillCountsInParallel
now treats an
InterruptedException as a cancellation rather than a provider failure: the
instance still loses its
counts, but the event is logged at debug instead of counting as a failed
lookup. A cancelled batch
is routine, and counting it as a failure would have made the failure signal
meaningless.
The pre-existing late-result test signalled its "provider finished" latch
from countGroups and thus
required the unwanted lookup; it now signals that latch when the first
lookup returns.
* test(instance): keep a blank line before the create-instance cases
Mechanical follow-up to the cancelled-batch change: the new case had been
appended
without the separating blank line. The extra coverage for the interrupt
reclassification was dropped on review instead — the runner reports a
deadline-cancelled worker as TIMED_OUT/INTERRUPTED with a null failure, so
the
instanceof-InterruptedException branch is not reachable from that scenario
and a
log-asserting case could not be made mutation-sensitive.
---
.../rocketmq/studio/instance/InstanceService.java | 19 ++++++-
.../studio/instance/InstanceServiceTest.java | 63 ++++++++++++++++++++--
2 files changed, 77 insertions(+), 5 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
index 6fce8cae2..633766836 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
@@ -166,6 +166,13 @@ public class InstanceService {
}
clearCounts(instance);
+ // A cancelled batch is routine (the caller stopped waiting or the
deadline passed),
+ // so the interrupt the worker raises at the checkpoint must not
be counted or logged as
+ // a provider failure: the instance keeps the same unavailable
counts as a timeout.
+ if (outcome.failure() instanceof InterruptedException) {
+ log.debug("Resource count lookup cancelled for instance {}",
instance.getId());
+ continue;
+ }
switch (outcome.status()) {
case REJECTED -> rejected++;
case TIMED_OUT -> timedOut++;
@@ -198,10 +205,20 @@ public class InstanceService {
* unique name first, so a name that happens to equal another instance's
numeric id would
* otherwise shadow it and attribute the counts to the wrong instance.
*/
- private InstanceResourceCountRunner.ResourceCounts loadCounts(InstanceVO
instance) {
+ private InstanceResourceCountRunner.ResourceCounts loadCounts(InstanceVO
instance)
+ throws InterruptedException {
InstanceVendor vendor = instance.getVendor() == null ?
InstanceVendor.APACHE : instance.getVendor();
InstanceProvider provider = providerRegistry.forVendor(vendor);
int topicCount = provider.countTopics(instance.getName());
+ // The batch deadline or a caller cancellation interrupts this worker,
but a provider that
+ // ignores the interrupt can still return a count afterwards. Check
the flag between the two
+ // lookups so a batch nobody waits for does not start the second
remote call.
+ // Thread.interrupted() consumes the flag the way any
InterruptedException producer does, so
+ // the pooled worker does not hand a stale interrupt to its next count
task.
+ if (Thread.interrupted()) {
+ throw new InterruptedException(
+ "resource count batch cancelled after counting topics of "
+ instance.getName());
+ }
int consumerGroupCount = provider.countGroups(instance.getName());
return new InstanceResourceCountRunner.ResourceCounts(topicCount,
consumerGroupCount);
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
index a5182abe2..b1b041cdf 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
@@ -440,11 +440,10 @@ class InstanceServiceTest {
if (interrupted) {
Thread.currentThread().interrupt();
}
- return 7;
- });
- when(instanceProvider.countGroups("slow")).thenAnswer(invocation -> {
+ // Signal the finished lookup here: this test is about the
returned response, and it must
+ // not require the cancelled batch to reach the consumer-group
lookup.
providerFinished.countDown();
- return 5;
+ return 7;
});
try {
@@ -470,6 +469,62 @@ class InstanceServiceTest {
}
}
+ @Test
+ void cancelledCountBatchShouldNotStartTheGroupLookupTest() throws
Exception {
+ InstanceVO slow = InstanceVO.builder().name("slow").build();
+ slow.setId(25L);
+ when(instanceRepository.findAll()).thenReturn(List.of(slow));
+
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
+
+ CountDownLatch topicCountStarted = new CountDownLatch(1);
+ CountDownLatch releaseProvider = new CountDownLatch(1);
+ CountDownLatch groupCountStarted = new CountDownLatch(1);
+ when(instanceProvider.countTopics("slow")).thenAnswer(invocation -> {
+ topicCountStarted.countDown();
+ boolean interrupted = false;
+ while (true) {
+ try {
+ releaseProvider.await();
+ break;
+ } catch (InterruptedException exception) {
+ // A provider that outlives Future.cancel(true): it keeps
waiting and restores
+ // the interrupt instead of aborting the in-flight lookup.
+ interrupted = true;
+ }
+ }
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ return 7;
+ });
+ // Lenient because the fixed path never reaches this lookup; on the
buggy path it is invoked.
+
org.mockito.Mockito.lenient().when(instanceProvider.countGroups("slow")).thenAnswer(invocation
-> {
+ groupCountStarted.countDown();
+ return 5;
+ });
+
+ try {
+ List<InstanceVO> result = instanceService.listInstances(null,
null, true);
+
+ // The batch deadline passed while the first lookup was still
running, so the caller
+ // keeps its existing unavailable response.
+ assertThat(topicCountStarted.await(1, TimeUnit.SECONDS)).isTrue();
+ assertThat(result).singleElement().satisfies(instance -> {
+ assertThat(instance.isResourceCountsAvailable()).isFalse();
+ assertThat(instance.getConsumerGroupCount()).isZero();
+ });
+
+ releaseProvider.countDown();
+ // The worker finishes the first lookup with the interruption
still pending. Wait for that
+ // detached worker within a bound (the unwanted lookup must never
complete, so the wait
+ // cannot be latched on it) and then require that it never started.
+ groupCountStarted.await(2, TimeUnit.SECONDS);
+ verify(instanceProvider, never()).countGroups("slow");
+ } finally {
+ releaseProvider.countDown();
+ }
+ }
+
@Test
void createInstanceShouldThrowWhenRequestIsNull() {
assertThatThrownBy(() -> instanceService.createInstance(null))