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 2a428b84 fix: fail closed when LiteTopic is unavailable (#932)
2a428b84 is described below
commit 2a428b84535a12615aa3c422ff801d239f45638f
Author: aias00 <[email protected]>
AuthorDate: Tue Aug 4 03:05:02 2026 -0700
fix: fail closed when LiteTopic is unavailable (#932)
* [ISSUE #793] Stop returning sample LiteTopic data
* [ISSUE #795] Fail closed on LiteTopic capability errors
---
.../studio/instance/topic/LiteTopicService.java | 90 ++--------------------
.../instance/topic/LiteTopicControllerTest.java | 19 +++++
.../instance/topic/LiteTopicServiceTest.java | 50 +++++++-----
web/src/pages/studio/LiteTopic.tsx | 7 +-
web/src/pages/studio/__tests__/LiteTopic.test.tsx | 12 +++
5 files changed, 74 insertions(+), 104 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicService.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicService.java
index af8129fd..f7715617 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicService.java
@@ -17,43 +17,22 @@
package org.apache.rocketmq.studio.instance.topic;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.springframework.stereotype.Service;
import java.util.List;
-import java.util.Locale;
@Service
public class LiteTopicService {
+ private static final String PROVIDER_UNAVAILABLE_MESSAGE = "LiteTopic
provider integration is not available";
+ private static final int NOT_IMPLEMENTED = 501;
public List<LiteTopicItemVO> listLiteTopics(String pattern, String
namespace) {
- return sampleItems().stream()
- .filter(item -> matchesPattern(pattern,
item.getTopicPattern()))
- .filter(item -> matchesNamespace(namespace,
item.getNamespace()))
- .toList();
+ return List.of();
}
public LiteTopicSessionVO getSession(String sessionId) {
- long now = System.currentTimeMillis();
- return LiteTopicSessionVO.builder()
- .sessionId(sessionId)
- .clientId("grpc-client-" + sessionId)
- .clientAddress("192.168.1.10:8081")
- .parentTopic("chat/{sessionId}")
- .consumerGroup("cg-chat-session")
- .createTime(now - 3_600_000L)
- .lastActiveTime(now - 30_000L)
- .ttl(3_600_000L)
- .ttlRemaining(1_800_000L)
- .status("ACTIVE")
- .totalMessages(5_000L)
- .consumedMessages(4_800L)
- .pendingMessages(200L)
- .popProgress(96)
- .liteTopicCreationCount(2)
- .liteTopics(List.of(
- new LiteTopicSessionVO.SessionLiteTopic("chat/" +
sessionId, "ACTIVE", 1_800_000L),
- new LiteTopicSessionVO.SessionLiteTopic("agent/" +
sessionId, "ACTIVE", 1_200_000L)))
- .build();
+ throw new BusinessException(NOT_IMPLEMENTED,
PROVIDER_UNAVAILABLE_MESSAGE);
}
public void extendTTL(String topicPattern, Long newTTL) {
@@ -63,67 +42,14 @@ public class LiteTopicService {
if (newTTL == null || newTTL <= 0) {
throw new IllegalArgumentException("newTTL must be positive");
}
+ throw new BusinessException(NOT_IMPLEMENTED,
PROVIDER_UNAVAILABLE_MESSAGE);
}
public LiteTopicQuotaVO getQuota(String namespace) {
- return LiteTopicQuotaVO.builder()
- .currentTopicCount(128)
- .maxTopicCount(1_000_000)
- .currentSessionCount(32)
- .maxSessionCount(100_000)
- .currentCreationRate(12)
- .maxCreationRate(1_000)
- .usageRate(0.000128)
- .sessionUsageRate(0.00032)
- .defaultTTL(3_600_000L)
- .maxTTL(86_400_000L)
- .remainingQuota(999_872)
- .consumerDensity(0.25)
- .build();
+ throw new BusinessException(NOT_IMPLEMENTED,
PROVIDER_UNAVAILABLE_MESSAGE);
}
public LiteTopicCapabilityVO getCapability() {
- return new LiteTopicCapabilityVO(true);
- }
-
- private List<LiteTopicItemVO> sampleItems() {
- long now = System.currentTimeMillis();
- return List.of(
- LiteTopicItemVO.builder()
- .topicPattern("chat/{sessionId}")
- .namespace("default")
- .topicCount(96)
- .consumerCount(12)
- .totalBacklog(1_200L)
- .averageTTL(3_600_000L)
- .ttlStatus("ACTIVE")
- .lastActiveTime(now - 60_000L)
- .sessionIds(List.of("sess-001", "sess-002"))
- .build(),
- LiteTopicItemVO.builder()
- .topicPattern("agent/{sessionId}")
- .namespace("ai")
- .topicCount(32)
- .consumerCount(4)
- .totalBacklog(180L)
- .averageTTL(1_800_000L)
- .ttlStatus("EXPIRING_SOON")
- .lastActiveTime(now - 120_000L)
- .sessionIds(List.of("agent-001"))
- .build());
- }
-
- private boolean matchesPattern(String filter, String value) {
- if (filter == null || filter.isBlank()) {
- return true;
- }
- return value != null &&
value.toLowerCase(Locale.ROOT).contains(filter.toLowerCase(Locale.ROOT));
- }
-
- private boolean matchesNamespace(String namespace, String value) {
- if (namespace == null || namespace.isBlank()) {
- return true;
- }
- return value != null && value.equalsIgnoreCase(namespace.trim());
+ return new LiteTopicCapabilityVO(false);
}
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/LiteTopicControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/LiteTopicControllerTest.java
index 8c1609f4..c58d6e31 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/LiteTopicControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/LiteTopicControllerTest.java
@@ -18,6 +18,7 @@
package org.apache.rocketmq.studio.instance.topic;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
@@ -29,6 +30,7 @@ import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@@ -126,6 +128,23 @@ class LiteTopicControllerTest {
verify(liteTopicService).extendTTL(eq("chat/{sessionId}"),
eq(7_200_000L));
}
+ @Test
+ void extendTTLShouldReturnUnsupportedWhenProviderIsUnavailable() throws
Exception {
+ LiteTopicTTLUpdateDTO request = new LiteTopicTTLUpdateDTO();
+ request.setTopicPattern("chat/{sessionId}");
+ request.setNewTTL(7_200_000L);
+ doThrow(new BusinessException(501, "LiteTopic provider integration is
not available"))
+ .when(liteTopicService)
+ .extendTTL(eq("chat/{sessionId}"), eq(7_200_000L));
+
+ mockMvc.perform(post("/api/liteTopic/extendTTL")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().isNotImplemented())
+ .andExpect(jsonPath("$.code").value(501))
+ .andExpect(jsonPath("$.message").value("LiteTopic provider
integration is not available"));
+ }
+
@Test
void extendTTLShouldRejectMissingTopicPattern() throws Exception {
LiteTopicTTLUpdateDTO request = new LiteTopicTTLUpdateDTO();
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/LiteTopicServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/LiteTopicServiceTest.java
index d2677d11..a6aafd50 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/LiteTopicServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/LiteTopicServiceTest.java
@@ -17,6 +17,7 @@
package org.apache.rocketmq.studio.instance.topic;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.junit.jupiter.api.Test;
import java.util.List;
@@ -29,35 +30,28 @@ class LiteTopicServiceTest {
private final LiteTopicService liteTopicService = new LiteTopicService();
@Test
- void listLiteTopicsShouldFilterByPatternAndNamespace() {
+ void listLiteTopicsShouldNotReturnSampleData() {
List<LiteTopicItemVO> result = liteTopicService.listLiteTopics("hat",
" DEFAULT ");
- assertThat(result).hasSize(1);
-
assertThat(result.get(0).getTopicPattern()).isEqualTo("chat/{sessionId}");
- assertThat(result.get(0).getNamespace()).isEqualTo("default");
+ assertThat(result).isEmpty();
}
@Test
- void listLiteTopicsShouldMatchNamespaceExactly() {
- assertThat(liteTopicService.listLiteTopics(null, "def")).isEmpty();
- assertThat(liteTopicService.listLiteTopics(null, " ")).hasSize(2);
+ void getQuotaShouldReturnUnsupportedWhenProviderIsUnavailable() {
+ assertThatThrownBy(() -> liteTopicService.getQuota("default"))
+ .isInstanceOfSatisfying(BusinessException.class, ex -> {
+ assertThat(ex.getCode()).isEqualTo(501);
+ assertThat(ex.getMessage()).isEqualTo("LiteTopic provider
integration is not available");
+ });
}
@Test
- void getQuotaShouldReturnDefaultLimits() {
- LiteTopicQuotaVO quota = liteTopicService.getQuota("default");
-
- assertThat(quota.getCurrentTopicCount()).isEqualTo(128);
- assertThat(quota.getMaxTopicCount()).isEqualTo(1_000_000);
- assertThat(quota.getRemainingQuota()).isEqualTo(999_872);
- }
-
- @Test
- void getSessionShouldReturnRequestedSessionId() {
- LiteTopicSessionVO session = liteTopicService.getSession("sess-001");
-
- assertThat(session.getSessionId()).isEqualTo("sess-001");
-
assertThat(session.getLiteTopics()).extracting("topicName").contains("chat/sess-001");
+ void getSessionShouldReturnUnsupportedWhenProviderIsUnavailable() {
+ assertThatThrownBy(() -> liteTopicService.getSession("sess-001"))
+ .isInstanceOfSatisfying(BusinessException.class, ex -> {
+ assertThat(ex.getCode()).isEqualTo(501);
+ assertThat(ex.getMessage()).isEqualTo("LiteTopic provider
integration is not available");
+ });
}
@Test
@@ -69,4 +63,18 @@ class LiteTopicServiceTest {
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("newTTL must be positive");
}
+
+ @Test
+ void extendTTLShouldReturnUnsupportedWhenProviderIsUnavailable() {
+ assertThatThrownBy(() ->
liteTopicService.extendTTL("chat/{sessionId}", 7_200_000L))
+ .isInstanceOfSatisfying(BusinessException.class, ex -> {
+ assertThat(ex.getCode()).isEqualTo(501);
+ assertThat(ex.getMessage()).isEqualTo("LiteTopic provider
integration is not available");
+ });
+ }
+
+ @Test
+ void getCapabilityShouldReportUnsupportedByDefault() {
+ assertThat(liteTopicService.getCapability().isSupported()).isFalse();
+ }
}
diff --git a/web/src/pages/studio/LiteTopic.tsx
b/web/src/pages/studio/LiteTopic.tsx
index 90f765b9..332f5620 100644
--- a/web/src/pages/studio/LiteTopic.tsx
+++ b/web/src/pages/studio/LiteTopic.tsx
@@ -204,7 +204,12 @@ const LiteTopicPage: React.FC = () => {
}
} catch {
if (!bootstrapIsActive()) return;
- // Assume supported when capability detection is unavailable.
+ ++displayCounter.current;
+ setCapabilitySupported(false);
+ setTopicList([]);
+ setQuota(null);
+ setLoading(false);
+ return;
}
if (displayCounter.current === initialDisplayRequestId) {
diff --git a/web/src/pages/studio/__tests__/LiteTopic.test.tsx
b/web/src/pages/studio/__tests__/LiteTopic.test.tsx
index 905692ec..7113e8fa 100644
--- a/web/src/pages/studio/__tests__/LiteTopic.test.tsx
+++ b/web/src/pages/studio/__tests__/LiteTopic.test.tsx
@@ -328,6 +328,18 @@ describe('LiteTopic Page', () => {
expect(screen.queryByText('late-result-*')).not.toBeInTheDocument();
});
+ it('fails closed when capability detection is unavailable', async () => {
+ apiMocks.queryLiteTopicCapability.mockRejectedValue(new Error('capability
unavailable'));
+
+ renderPage();
+
+ expect(
+ await screen.findByText('当前集群不支持 LiteTopic,请升级到 RocketMQ 5.x'),
+ ).toBeInTheDocument();
+ expect(apiMocks.queryLiteTopicList).not.toHaveBeenCalled();
+ expect(apiMocks.queryLiteTopicQuota).not.toHaveBeenCalled();
+ });
+
it('ignores stale list and quota responses when namespaces change quickly',
async () => {
const alphaList = createDeferred<LiteTopicItem[]>();
const betaList = createDeferred<LiteTopicItem[]>();