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",

Reply via email to