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 feb96cbea feat(ai): add the rmq.litetopic.extend_ttl mutation tool
(#5116)
feb96cbea is described below
commit feb96cbeaf662148cfe51b62d9336d8e7c1a3c2c
Author: pilichoumao <[email protected]>
AuthorDate: Mon Oct 19 20:59:00 2026 +0800
feat(ai): add the rmq.litetopic.extend_ttl mutation tool (#5116)
Verified against `rocketmq-studio` at 97f668006 before merging: the defect
is still present on the
current trunk and the change merges cleanly.
Merged as-is (squash); no maintainer edits were needed.
Locally verified: targeted suites green on the integration branch; a full
backend and frontend
build plus test pass runs on the trunk once this batch lands.
---
rmqctl/internal/catalog/catalog_gen.go | 20 ++++-
.../litetopic/LiteTopicExtendTtlInput.java | 20 +++++
.../litetopic/LiteTopicExtendTtlOutput.java | 24 ++++++
.../litetopic/LiteTopicExtendTtlToolHandler.java | 85 ++++++++++++++++++++
.../resources/tool-catalog/tools/litetopic.yaml | 78 ++++++++++++++++++
.../LiteTopicExtendTtlToolHandlerTest.java | 92 ++++++++++++++++++++++
.../tool/service/ToolOutputSchemaContractTest.java | 3 +
7 files changed, 321 insertions(+), 1 deletion(-)
diff --git a/rmqctl/internal/catalog/catalog_gen.go
b/rmqctl/internal/catalog/catalog_gen.go
index c286bb75c..ea1dbc8d9 100644
--- a/rmqctl/internal/catalog/catalog_gen.go
+++ b/rmqctl/internal/catalog/catalog_gen.go
@@ -21,7 +21,7 @@ package catalog
var defaultDocument = Document{
Version: "2.0.0",
MinimumClientVersion: "2.0.0",
- Digest:
"e7622ffd8d0017ac44ec759b9a76a475422c4098394968cbc65d2b70153396d9",
+ Digest:
"e07c4c4a447545da57697f9e333a717c4bff6b660cefdaf6acfe4c1074791b39",
Tools: []Tool{
{
Name: "rmq.acl.list",
@@ -413,6 +413,24 @@ var defaultDocument = Document{
},
ViewHint: "object",
},
+ {
+ Name: "rmq.litetopic.extend_ttl",
+ CLI: CLI{Resource: "litetopic", Verb:
"extend-ttl"},
+ Description: "Extend the LiteTopic TTL of a
registered Lite parent topic on the selected Instance (L2 mutation: dry-run
first, then apply with the returned confirm_token). Rewrites the
lite.topic.expiration attribute on every broker master; lite topics under the
pattern inherit the new TTL. Broker granularity is one minute and the cap is 30
days. Fails with 409 when the pattern is not a registered Lite parent topic and
501 when the cluster has no LiteTopic support. Current pa [...]
+ RiskLevel: "L2",
+ Permission: "litetopic:write",
+ RequiredCapabilities: []string{},
+ InputSchema: InputSchema{
+ Fields: []Field{
+ {Name: "instanceId", Flag:
"instance-id", Description: "Studio Instance identifier.", Kind: StringField,
Required: true, MinLength: 1},
+ {Name: "topicPattern", Flag:
"topic-pattern", Description: "Registered Lite parent topic whose lite topics'
TTL is extended.", Kind: StringField, Required: true, MinLength: 1},
+ {Name: "newTtlMillis", Flag:
"new-ttl-millis", Description: "New TTL in milliseconds; the broker rounds to
whole minutes and caps at 30 days.", Kind: IntegerField, Required: true,
Minimum: 1, HasMinimum: true},
+ {Name: "dry_run", Flag: "dry-run",
Description: "When true, preview the mutation without executing it; omit to
apply with a confirm_token.", Kind: BooleanField},
+ {Name: "confirm_token", Flag:
"confirm-token", Kind: StringField, MinLength: 1},
+ },
+ },
+ ViewHint: "object",
+ },
{
Name: "rmq.message.query",
CLI: CLI{Resource: "message", Verb:
"query"},
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/litetopic/LiteTopicExtendTtlInput.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/litetopic/LiteTopicExtendTtlInput.java
new file mode 100644
index 000000000..6a6ef2141
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/litetopic/LiteTopicExtendTtlInput.java
@@ -0,0 +1,20 @@
+/*
+ * 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 org.apache.rocketmq.studio.ops.ai.tool.contract.litetopic;
+
+public record LiteTopicExtendTtlInput(String topicPattern, Long newTtlMillis) {
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/litetopic/LiteTopicExtendTtlOutput.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/litetopic/LiteTopicExtendTtlOutput.java
new file mode 100644
index 000000000..e8b776354
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/litetopic/LiteTopicExtendTtlOutput.java
@@ -0,0 +1,24 @@
+/*
+ * 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 org.apache.rocketmq.studio.ops.ai.tool.contract.litetopic;
+
+/**
+ * Result of an applied TTL extension. Reports the requested value; the broker
applies it at
+ * one-minute granularity capped at 30 days, which the preview warnings state
up front.
+ */
+public record LiteTopicExtendTtlOutput(String topicPattern, long newTtlMillis)
{
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/litetopic/LiteTopicExtendTtlToolHandler.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/litetopic/LiteTopicExtendTtlToolHandler.java
new file mode 100644
index 000000000..e2f50c837
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/litetopic/LiteTopicExtendTtlToolHandler.java
@@ -0,0 +1,85 @@
+/*
+ * 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 org.apache.rocketmq.studio.ops.ai.tool.handler.litetopic;
+
+import org.apache.rocketmq.studio.instance.topic.LiteTopicService;
+import
org.apache.rocketmq.studio.ops.ai.tool.contract.litetopic.LiteTopicExtendTtlInput;
+import
org.apache.rocketmq.studio.ops.ai.tool.contract.litetopic.LiteTopicExtendTtlOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * L2 mutation tool extending the LiteTopic TTL of a registered Lite parent
topic through
+ * {@link LiteTopicService#extendTTL}, which enforces the instance ownership
guard. The
+ * preview is derived from the request alone — no broker state is read — so
the confirmed
+ * plan never drifts between preview and apply; current pattern state belongs
to the
+ * read-only {@code rmq.litetopic.list} / {@code rmq.litetopic.quota} tools.
+ */
+@Component
+public class LiteTopicExtendTtlToolHandler
+ extends MutationToolHandler<LiteTopicExtendTtlInput,
LiteTopicExtendTtlOutput> {
+
+ private static final PlanDescription PLAN_DESCRIPTION = new
PlanDescription(
+ "extend the LiteTopic TTL under parent topic '%s' in instance
'%s'.",
+ List.of("Rewrites the lite.topic.expiration attribute of the
parent topic on every "
+ + "broker master; lite topics under the pattern inherit
the new TTL."),
+ List.of());
+
+ private static final long MILLIS_PER_MINUTE = TimeUnit.MINUTES.toMillis(1);
+ private static final long MAX_TTL_MILLIS = TimeUnit.DAYS.toMillis(30);
+
+ private final LiteTopicService liteTopicService;
+
+ public LiteTopicExtendTtlToolHandler(LiteTopicService liteTopicService) {
+ super(LiteTopicExtendTtlInput.class);
+ this.liteTopicService = liteTopicService;
+ }
+
+ @Override
+ public String name() {
+ return "rmq.litetopic.extend_ttl";
+ }
+
+ @Override
+ public ToolPlan preview(LiteTopicExtendTtlInput input,
ToolExecutionContext context) {
+ long requested = input.newTtlMillis();
+ return PLAN_DESCRIPTION.builder(input.topicPattern(),
context.instanceId())
+ .after(Map.of(
+ "topicPattern", input.topicPattern(),
+ "newTtlMillis", requested))
+ .warningIf(requested % MILLIS_PER_MINUTE != 0,
+ "Broker TTL granularity is one minute; the requested
millisecond value "
+ + "will be applied rounded to whole minutes.")
+ .warningIf(requested > MAX_TTL_MILLIS,
+ "Broker caps the LiteTopic TTL at 30 days; the
requested value exceeds "
+ + "the cap and will be clamped.")
+ .build();
+ }
+
+ @Override
+ public LiteTopicExtendTtlOutput execute(LiteTopicExtendTtlInput input,
ToolExecutionContext context) {
+ liteTopicService.extendTTL(context.instanceId(), input.topicPattern(),
input.newTtlMillis());
+ return new LiteTopicExtendTtlOutput(input.topicPattern(),
input.newTtlMillis());
+ }
+}
diff --git a/server/src/main/resources/tool-catalog/tools/litetopic.yaml
b/server/src/main/resources/tool-catalog/tools/litetopic.yaml
index 53eab20ec..4d397a656 100644
--- a/server/src/main/resources/tool-catalog/tools/litetopic.yaml
+++ b/server/src/main/resources/tool-catalog/tools/litetopic.yaml
@@ -182,3 +182,81 @@ tools:
type: number
viewHint: object
deprecated: false
+ - name: rmq.litetopic.extend_ttl
+ cli:
+ resource: litetopic
+ verb: extend-ttl
+ description: "Extend the LiteTopic TTL of a registered Lite parent topic
on the selected Instance (L2 mutation: dry-run first, then apply with the
returned confirm_token). Rewrites the lite.topic.expiration attribute on every
broker master; lite topics under the pattern inherit the new TTL. Broker
granularity is one minute and the cap is 30 days. Fails with 409 when the
pattern is not a registered Lite parent topic and 501 when the cluster has no
LiteTopic support. Current pattern st [...]
+ riskLevel: L2
+ permission: litetopic:write
+ requiredCapabilities: []
+ inputSchema:
+ type: object
+ required:
+ - instanceId
+ - topicPattern
+ - newTtlMillis
+ additionalProperties: false
+ properties:
+ instanceId:
+ type: string
+ description: Studio Instance identifier.
+ minLength: 1
+ topicPattern:
+ type: string
+ description: Registered Lite parent topic whose lite topics' TTL is
extended.
+ minLength: 1
+ newTtlMillis:
+ type: integer
+ minimum: 1
+ description: New TTL in milliseconds; the broker rounds to whole
minutes and caps at 30 days.
+ dry_run:
+ type: boolean
+ description: When true, preview the mutation without executing it;
omit to apply with a confirm_token.
+ confirm_token:
+ type: string
+ minLength: 1
+ outputSchema:
+ type: object
+ required:
+ - status
+ - instanceId
+ - plan
+ additionalProperties: false
+ properties:
+ status:
+ enum:
+ - PLANNED
+ - EXECUTED
+ instanceId:
+ type: string
+ confirm_token:
+ type: string
+ plan:
+ type: object
+ required:
+ - summary
+ - impact
+ - before
+ - after
+ - warnings
+ additionalProperties: false
+ properties:
+ summary:
+ type: string
+ impact:
+ type: array
+ items:
+ type: string
+ before:
+ type: object
+ after:
+ type: object
+ warnings:
+ type: array
+ items:
+ type: string
+ result:
+ type: object
+ viewHint: object
+ deprecated: false
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/litetopic/LiteTopicExtendTtlToolHandlerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/litetopic/LiteTopicExtendTtlToolHandlerTest.java
new file mode 100644
index 000000000..02a0afad8
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/litetopic/LiteTopicExtendTtlToolHandlerTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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 org.apache.rocketmq.studio.ops.ai.tool.handler.litetopic;
+
+import org.apache.rocketmq.studio.instance.topic.LiteTopicService;
+import
org.apache.rocketmq.studio.ops.ai.tool.contract.litetopic.LiteTopicExtendTtlInput;
+import
org.apache.rocketmq.studio.ops.ai.tool.contract.litetopic.LiteTopicExtendTtlOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+@ExtendWith(MockitoExtension.class)
+class LiteTopicExtendTtlToolHandlerTest {
+
+ @Mock
+ private LiteTopicService liteTopicService;
+
+ @InjectMocks
+ private LiteTopicExtendTtlToolHandler handler;
+
+ @Test
+ void exposesTheCatalogedToolNameTest() {
+ assertThat(handler.name()).isEqualTo("rmq.litetopic.extend_ttl");
+
assertThat(handler.inputType()).isEqualTo(LiteTopicExtendTtlInput.class);
+ }
+
+ @Test
+ void previewIsDerivedFromTheRequestWithoutTouchingTheBrokerTest() {
+ ToolPlan plan = handler.preview(new LiteTopicExtendTtlInput("orders",
86_400_000L), context());
+
+ verifyNoInteractions(liteTopicService);
+ assertThat(plan.summary()).isEqualTo("extend the LiteTopic TTL under
parent topic 'orders' in instance 'instance-a'.");
+ assertThat(plan.after()).containsExactlyInAnyOrderEntriesOf(Map.of(
+ "topicPattern", "orders",
+ "newTtlMillis", 86_400_000L));
+ // A whole-minute value inside the cap carries no clamp warnings.
+ assertThat(plan.warnings()).isEmpty();
+ assertThat(plan.impact()).anyMatch(text ->
text.contains("lite.topic.expiration"));
+ }
+
+ @Test
+ void previewWarnsAboutMinuteGranularityAndTheThirtyDayCapTest() {
+ ToolPlan unaligned = handler.preview(new
LiteTopicExtendTtlInput("orders", 90_000L), context());
+ assertThat(unaligned.warnings()).anyMatch(text -> text.contains("one
minute"));
+
+ ToolPlan overCap = handler.preview(new LiteTopicExtendTtlInput(
+ "orders", TimeUnit.DAYS.toMillis(31)), context());
+ assertThat(overCap.warnings()).anyMatch(text -> text.contains("30
days"));
+
+ ToolPlan exactCap = handler.preview(new LiteTopicExtendTtlInput(
+ "orders", TimeUnit.DAYS.toMillis(30)), context());
+ assertThat(exactCap.warnings()).isEmpty();
+ }
+
+ @Test
+ void executeDelegatesToTheOwnershipGuardedServiceTest() {
+ LiteTopicExtendTtlOutput output = handler.execute(
+ new LiteTopicExtendTtlInput("orders", 86_400_000L), context());
+
+ verify(liteTopicService).extendTTL("instance-a", "orders",
86_400_000L);
+ assertThat(output).isEqualTo(new LiteTopicExtendTtlOutput("orders",
86_400_000L));
+ }
+
+ private static ToolExecutionContext context() {
+ return ToolExecutionContext.of("instance-a", null,
Map.of("instanceId", "instance-a"));
+ }
+}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolOutputSchemaContractTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolOutputSchemaContractTest.java
index dd574bc64..3377edf82 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolOutputSchemaContractTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolOutputSchemaContractTest.java
@@ -35,6 +35,7 @@ import
org.apache.rocketmq.studio.ops.ai.tool.contract.broker.BrokerConfigOutput
import
org.apache.rocketmq.studio.ops.ai.tool.contract.broker.BrokerDescribeOutput;
import org.apache.rocketmq.studio.ops.ai.tool.contract.cluster.ClusterListItem;
import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ListOutput;
+import
org.apache.rocketmq.studio.ops.ai.tool.contract.litetopic.LiteTopicExtendTtlOutput;
import
org.apache.rocketmq.studio.ops.ai.tool.contract.litetopic.LiteTopicListItem;
import
org.apache.rocketmq.studio.ops.ai.tool.contract.litetopic.LiteTopicQuotaOutput;
import
org.apache.rocketmq.studio.ops.ai.tool.contract.litetopic.LiteTopicSessionOutput;
@@ -166,6 +167,8 @@ class ToolOutputSchemaContractTest {
List.of(new LiteTopicSessionOutput.Entry("chat/sess-1/0",
"ACTIVE", 1800L)))));
samples.put("rmq.litetopic.quota", List.of(new LiteTopicQuotaOutput(
120, 1000, 30, 100, 5, 50, 0.12, 0.3, 3600L, 86400L, 880,
2.5)));
+ samples.put("rmq.litetopic.extend_ttl", List.of(planned(), executed(
+ new LiteTopicExtendTtlOutput("orders", 86400000L))));
samples.put("rmq.audit.list", List.of(new PageOutput<>(1, 20, 1L,
List.of(
new AuditItem(1L, "2026-08-22T08:00:00", "admin",
"CREATE_TOPIC", "TOPIC",