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
commit de05e8c198c741720c8412f9f02c9d545282614e Author: aias00 <[email protected]> AuthorDate: Thu Jul 23 22:51:00 2026 -0700 feat: add LiteTopic backend endpoints (#502) Add backend stub endpoints for LiteTopic page: list, session details, TTL extension, quota, and capability check. --- .../instance/topic/LiteTopicCapabilityVO.java | 29 +++++ .../studio/instance/topic/LiteTopicController.java | 66 +++++++++++ .../studio/instance/topic/LiteTopicItemVO.java | 37 ++++++ .../studio/instance/topic/LiteTopicQuotaVO.java | 38 ++++++ .../studio/instance/topic/LiteTopicService.java | 122 ++++++++++++++++++++ .../studio/instance/topic/LiteTopicSessionVO.java | 55 +++++++++ .../instance/topic/LiteTopicTTLUpdateDTO.java | 26 +++++ .../instance/topic/LiteTopicControllerTest.java | 127 +++++++++++++++++++++ .../instance/topic/LiteTopicServiceTest.java | 66 +++++++++++ 9 files changed, 566 insertions(+) diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicCapabilityVO.java b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicCapabilityVO.java new file mode 100644 index 00000000..c31ba621 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicCapabilityVO.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.instance.topic; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LiteTopicCapabilityVO { + private boolean supported; +} diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicController.java b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicController.java new file mode 100644 index 00000000..b4ce5235 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicController.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.instance.topic; + +import com.rocketmq.studio.common.domain.Result; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/liteTopic") +@RequiredArgsConstructor +public class LiteTopicController { + + private final LiteTopicService liteTopicService; + + @GetMapping("/list") + public Result<List<LiteTopicItemVO>> listLiteTopics( + @RequestParam(required = false) String pattern, + @RequestParam(required = false) String namespace) { + return Result.ok(liteTopicService.listLiteTopics(pattern, namespace)); + } + + @GetMapping("/session/{sessionId}") + public Result<LiteTopicSessionVO> getSession(@PathVariable String sessionId) { + return Result.ok(liteTopicService.getSession(sessionId)); + } + + @PostMapping("/extendTTL") + public Result<Void> extendTTL(@RequestBody LiteTopicTTLUpdateDTO request) { + liteTopicService.extendTTL(request.getTopicPattern(), request.getNewTTL()); + return Result.ok(); + } + + @GetMapping("/quota") + public Result<LiteTopicQuotaVO> getQuota(@RequestParam(required = false) String namespace) { + return Result.ok(liteTopicService.getQuota(namespace)); + } + + @GetMapping("/capability") + public Result<LiteTopicCapabilityVO> getCapability() { + return Result.ok(liteTopicService.getCapability()); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicItemVO.java b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicItemVO.java new file mode 100644 index 00000000..4b156074 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicItemVO.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.instance.topic; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class LiteTopicItemVO { + private String topicPattern; + private String namespace; + private Integer topicCount; + private Integer consumerCount; + private Long totalBacklog; + private Long averageTTL; + private String ttlStatus; + private Long lastActiveTime; + private List<String> sessionIds; +} diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicQuotaVO.java b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicQuotaVO.java new file mode 100644 index 00000000..976f9d8c --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicQuotaVO.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.instance.topic; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class LiteTopicQuotaVO { + private Integer currentTopicCount; + private Integer maxTopicCount; + private Integer currentSessionCount; + private Integer maxSessionCount; + private Integer currentCreationRate; + private Integer maxCreationRate; + private Double usageRate; + private Double sessionUsageRate; + private Long defaultTTL; + private Long maxTTL; + private Integer remainingQuota; + private Double consumerDensity; +} diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicService.java b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicService.java new file mode 100644 index 00000000..4e48ff74 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicService.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.instance.topic; + +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Locale; + +@Service +public class LiteTopicService { + + public List<LiteTopicItemVO> listLiteTopics(String pattern, String namespace) { + return sampleItems().stream() + .filter(item -> matches(pattern, item.getTopicPattern())) + .filter(item -> matches(namespace, item.getNamespace())) + .toList(); + } + + 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(); + } + + public void extendTTL(String topicPattern, Long newTTL) { + if (topicPattern == null || topicPattern.isBlank()) { + throw new IllegalArgumentException("topicPattern is required"); + } + if (newTTL == null || newTTL <= 0) { + throw new IllegalArgumentException("newTTL must be positive"); + } + } + + 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(); + } + + 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 matches(String filter, String value) { + if (filter == null || filter.isBlank()) { + return true; + } + return value != null && value.toLowerCase(Locale.ROOT).contains(filter.toLowerCase(Locale.ROOT)); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicSessionVO.java b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicSessionVO.java new file mode 100644 index 00000000..ae6aa497 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicSessionVO.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.instance.topic; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +public class LiteTopicSessionVO { + private String sessionId; + private String clientId; + private String clientAddress; + private String parentTopic; + private String consumerGroup; + private Long createTime; + private Long lastActiveTime; + private Long ttl; + private Long ttlRemaining; + private String status; + private Long totalMessages; + private Long consumedMessages; + private Long pendingMessages; + private Integer popProgress; + private Integer liteTopicCreationCount; + private List<SessionLiteTopic> liteTopics; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class SessionLiteTopic { + private String topicName; + private String status; + private Long ttlRemaining; + } +} diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java new file mode 100644 index 00000000..2a445d4d --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.instance.topic; + +import lombok.Data; + +@Data +public class LiteTopicTTLUpdateDTO { + private String topicPattern; + private Long newTTL; +} diff --git a/server/src/test/java/com/rocketmq/studio/instance/topic/LiteTopicControllerTest.java b/server/src/test/java/com/rocketmq/studio/instance/topic/LiteTopicControllerTest.java new file mode 100644 index 00000000..9d018193 --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/instance/topic/LiteTopicControllerTest.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.instance.topic; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(LiteTopicController.class) +@AutoConfigureMockMvc(addFilters = false) +class LiteTopicControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private LiteTopicService liteTopicService; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void listLiteTopicsShouldPassFilters() throws Exception { + LiteTopicItemVO item = LiteTopicItemVO.builder() + .topicPattern("chat/{sessionId}") + .namespace("default") + .topicCount(96) + .build(); + + when(liteTopicService.listLiteTopics("chat", "default")).thenReturn(List.of(item)); + + mockMvc.perform(get("/api/liteTopic/list") + .param("pattern", "chat") + .param("namespace", "default")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data[0].topicPattern").value("chat/{sessionId}")); + + verify(liteTopicService).listLiteTopics("chat", "default"); + } + + @Test + void getQuotaShouldReturnQuota() throws Exception { + LiteTopicQuotaVO quota = LiteTopicQuotaVO.builder() + .currentTopicCount(128) + .maxTopicCount(1_000_000) + .remainingQuota(999_872) + .build(); + + when(liteTopicService.getQuota("default")).thenReturn(quota); + + mockMvc.perform(get("/api/liteTopic/quota").param("namespace", "default")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.currentTopicCount").value(128)) + .andExpect(jsonPath("$.data.remainingQuota").value(999872)); + } + + @Test + void getCapabilityShouldReturnSupportedFlag() throws Exception { + when(liteTopicService.getCapability()).thenReturn(new LiteTopicCapabilityVO(true)); + + mockMvc.perform(get("/api/liteTopic/capability")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.supported").value(true)); + } + + @Test + void getSessionShouldReturnSession() throws Exception { + LiteTopicSessionVO session = LiteTopicSessionVO.builder() + .sessionId("sess-001") + .clientId("grpc-client-sess-001") + .popProgress(96) + .build(); + + when(liteTopicService.getSession("sess-001")).thenReturn(session); + + mockMvc.perform(get("/api/liteTopic/session/sess-001")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.sessionId").value("sess-001")) + .andExpect(jsonPath("$.data.popProgress").value(96)); + } + + @Test + void extendTTLShouldDelegateToService() throws Exception { + LiteTopicTTLUpdateDTO request = new LiteTopicTTLUpdateDTO(); + request.setTopicPattern("chat/{sessionId}"); + request.setNewTTL(7_200_000L); + + mockMvc.perform(post("/api/liteTopic/extendTTL") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + + verify(liteTopicService).extendTTL(eq("chat/{sessionId}"), eq(7_200_000L)); + } +} diff --git a/server/src/test/java/com/rocketmq/studio/instance/topic/LiteTopicServiceTest.java b/server/src/test/java/com/rocketmq/studio/instance/topic/LiteTopicServiceTest.java new file mode 100644 index 00000000..25f5fc99 --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/instance/topic/LiteTopicServiceTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.instance.topic; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LiteTopicServiceTest { + + private final LiteTopicService liteTopicService = new LiteTopicService(); + + @Test + void listLiteTopicsShouldFilterByPatternAndNamespace() { + List<LiteTopicItemVO> result = liteTopicService.listLiteTopics("chat", "default"); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getTopicPattern()).isEqualTo("chat/{sessionId}"); + assertThat(result.get(0).getNamespace()).isEqualTo("default"); + } + + @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"); + } + + @Test + void extendTTLShouldRejectInvalidInput() { + assertThatThrownBy(() -> liteTopicService.extendTTL("", 1L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("topicPattern is required"); + assertThatThrownBy(() -> liteTopicService.extendTTL("chat/{sessionId}", 0L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("newTTL must be positive"); + } +}
