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 c2c7d9286 fix(ai): reject non-positive reset timestamp before any
broker call (#6893)
c2c7d9286 is described below
commit c2c7d92860c4ee90a114ce3d9e3e7f340171cf06
Author: δΉζ€ <[email protected]>
AuthorDate: Mon Oct 26 16:28:28 2026 +0800
fix(ai): reject non-positive reset timestamp before any broker call (#6893)
Fixes #5984
rmq.group.reset_offset only rejected a null timestamp; 0 or negative values
passed preview and execute. The broker resolves such a timestamp to each queue
minimum offset, rewinding every queue (mass replay) via the AI tool API, MCP
tools/call and rmqctl. Reject timestamp <= 0 in the shared handler with the
REST contract message (400), guarding both preview and execute before any
broker call. Regression tests cover 0/-1 on both paths.
---
.../handler/group/GroupResetOffsetToolHandler.java | 10 ++++++-
.../group/GroupResetOffsetToolHandlerTest.java | 35 ++++++++++++++++++++++
2 files changed, 44 insertions(+), 1 deletion(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/group/GroupResetOffsetToolHandler.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/group/GroupResetOffsetToolHandler.java
index 4121e8916..6b2c5f0bf 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/group/GroupResetOffsetToolHandler.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/group/GroupResetOffsetToolHandler.java
@@ -79,11 +79,19 @@ public class GroupResetOffsetToolHandler extends
MutationToolHandler<GroupResetO
return new ResetOffsetOutput(input.groupName(), input.topicName(),
timestamp);
}
- /** Decision 13: no server-side default β rmqctl fills the current time
client-side. */
+ /**
+ * Decision 13: no server-side default β rmqctl fills the current time
client-side.
+ * A non-positive timestamp rewinds every queue to its minimum offset (the
broker
+ * resolves such a value before the first stored message), so it must be
rejected
+ * with the same contract as {@code POST /api/groups/reset-offset}.
+ */
private static long requireTimestamp(GroupResetOffsetInput input) {
if (input.timestamp() == null) {
throw new BusinessException(400, "timestamp is required");
}
+ if (input.timestamp() <= 0L) {
+ throw new BusinessException(400, "timestamp must be positive");
+ }
return input.timestamp();
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/group/GroupResetOffsetToolHandlerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/group/GroupResetOffsetToolHandlerTest.java
index 546127439..b3d9cdd00 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/group/GroupResetOffsetToolHandlerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/group/GroupResetOffsetToolHandlerTest.java
@@ -15,6 +15,8 @@ import
org.apache.rocketmq.studio.ops.ai.tool.contract.group.GroupResetOffsetInp
import org.apache.rocketmq.studio.ops.ai.tool.contract.group.ResetOffsetOutput;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -65,4 +67,37 @@ class GroupResetOffsetToolHandlerTest {
.satisfies(error -> assertThat(((BusinessException)
error).getCode()).isEqualTo(400));
verifyNoInteractions(metadataService);
}
+
+ @ParameterizedTest
+ @ValueSource(longs = {0L, -1L})
+ void applyShouldRejectANonPositiveTimestampTest(long timestamp) {
+ // The broker resolves a non-positive timestamp to the minimum offset,
which
+ // would rewind every queue; the tool must match the REST contract's
@Positive.
+ GroupResetOffsetInput input = new GroupResetOffsetInput(
+ "untrusted-instance", "group-1", "TopicA", timestamp);
+ ToolExecutionContext execution = ToolExecutionContext.of(
+ "instance-a", null, Map.of("instanceId",
"untrusted-instance"));
+
+ assertThatThrownBy(() -> handler.execute(input, execution))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("timestamp must be positive")
+ .satisfies(error -> assertThat(((BusinessException)
error).getCode()).isEqualTo(400));
+ verifyNoInteractions(metadataService);
+ }
+
+ @ParameterizedTest
+ @ValueSource(longs = {0L, -1L})
+ void previewShouldRejectANonPositiveTimestampTest(long timestamp) {
+ // The L2 preview must fail before issuing a confirm token for the
destructive rewind.
+ GroupResetOffsetInput input = new GroupResetOffsetInput(
+ "untrusted-instance", "group-1", "TopicA", timestamp);
+ ToolExecutionContext execution = ToolExecutionContext.of(
+ "instance-a", null, Map.of("instanceId",
"untrusted-instance"));
+
+ assertThatThrownBy(() -> handler.preview(input, execution))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("timestamp must be positive")
+ .satisfies(error -> assertThat(((BusinessException)
error).getCode()).isEqualTo(400));
+ verifyNoInteractions(metadataService);
+ }
}