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 9b02adb4d fix(audit): disclose a cleanup that stopped at its per-call
ceiling (#7148)
9b02adb4d is described below
commit 9b02adb4ddc6bb87c45bca8d1fa4e07aaa5e19b8
Author: btlqql <[email protected]>
AuthorDate: Mon Oct 26 20:52:43 2026 +0800
fix(audit): disclose a cleanup that stopped at its per-call ceiling (#7148)
* fix(audit): disclose a cleanup that stopped at its per-call ceiling
(#5976)
The cleanup endpoint deletes at most 500 rows per batch in 20 batches and
answered with the
deleted count alone, so a sweep that ran out of budget looked exactly like
a complete one and the
operator was told the retention window had been applied while older rows
survived.
Cleanup now answers with AuditCleanupResultVO: the same deleted count plus
a truncated flag derived
from the per-call row ceiling, and the service logs a warning naming the
ceiling when it is hit.
Fixes #5976
* docs(api-spec): disclose the cleanup ceiling and end the VO with a newline
The cleanup response gained a truncated flag meaning the sweep stopped at
its
per-call ceiling; the spec still documented only the deleted count, so a
caller
had no way to learn the cleanup was partial. Also add the missing trailing
newline to AuditCleanupResultVO.
---
docs/api-spec.md | 3 +-
.../studio/ops/audit/AuditCleanupResultVO.java | 40 ++++++++++++++++++++++
.../rocketmq/studio/ops/audit/AuditController.java | 7 ++--
.../rocketmq/studio/ops/audit/AuditService.java | 18 ++++++++--
.../studio/ops/audit/AuditControllerTest.java | 20 +++++++++--
.../studio/ops/audit/AuditServiceTest.java | 17 +++++++--
6 files changed, 92 insertions(+), 13 deletions(-)
diff --git a/docs/api-spec.md b/docs/api-spec.md
index eaac7d7f5..622640a9a 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -2560,7 +2560,8 @@ POST /api/audit-logs/cleanup
| 字段 | 类型 | 说明 |
|------|------|------|
-| `deleted` | `number` | 清理数量 |
+| `deleted` | `number` | 本次清理删除的记录数 |
+| `truncated` | `boolean` | 为 `true` 表示本次清理达到单次上限即停、仍有更早的记录留存;重复调用直到返回 `false`
即清完 |
---
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditCleanupResultVO.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditCleanupResultVO.java
new file mode 100644
index 000000000..f53916c6b
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditCleanupResultVO.java
@@ -0,0 +1,40 @@
+/*
+ * 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.audit;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Outcome of one audit-log cleanup call. The deleted count alone cannot tell
a complete sweep from
+ * one that ran out of budget, so the answer carries the ceiling signal next
to it.
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AuditCleanupResultVO {
+ /** Rows removed by this call. */
+ private int deleted;
+ /**
+ * True when the sweep stopped at its per-call ceiling, which means rows
older than the retention
+ * window may still be stored: the caller must not report a complete
cleanup.
+ */
+ private boolean truncated;
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditController.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditController.java
index 08d964119..7f3311828 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditController.java
@@ -27,8 +27,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
-import java.util.Map;
-
@RestController
@RequestMapping("/api/audit-logs")
@RequiredArgsConstructor
@@ -90,9 +88,8 @@ public class AuditController {
}
@PostMapping("/cleanup")
- public Result<Map<String, Integer>> cleanupLogs(@Valid
@RequestBody(required = false) AuditCleanupDTO request) {
+ public Result<AuditCleanupResultVO> cleanupLogs(@Valid
@RequestBody(required = false) AuditCleanupDTO request) {
int beforeDays = request == null || request.getBeforeDays() == null ?
30 : request.getBeforeDays();
- int deleted = auditService.cleanupLogs(beforeDays);
- return Result.ok(Map.of("deleted", deleted));
+ return Result.ok(auditService.cleanupLogs(beforeDays));
}
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
index a4d2d345d..d1a6a5582 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
@@ -43,6 +43,12 @@ public class AuditService {
private static final int MAX_EXPORT_RECORDS = 10_000;
private static final int CLEANUP_BATCH_SIZE = 500;
private static final int CLEANUP_MAX_BATCHES = 20;
+ /**
+ * Most rows one cleanup call can delete: {@code CLEANUP_BATCH_SIZE} rows
in each of
+ * {@code CLEANUP_MAX_BATCHES} batches. Reaching it means the sweep ran
out of budget, not that
+ * the retention window was applied.
+ */
+ private static final int CLEANUP_ROW_CEILING = CLEANUP_BATCH_SIZE *
CLEANUP_MAX_BATCHES;
private static final String CSV_COLUMNS =
"operator,operationType,resourceType,target,clusterId,detail,result,errorMessage";
@@ -181,7 +187,7 @@ public class AuditService {
log.info("Audit recorded: {} on {} -> {}", operationType, target,
result);
}
- public int cleanupLogs(int beforeDays) {
+ public AuditCleanupResultVO cleanupLogs(int beforeDays) {
if (beforeDays <= 0) {
throw new BusinessException(400, "beforeDays must be greater than
0");
}
@@ -190,7 +196,15 @@ public class AuditService {
}
log.info("Cleaning up audit logs older than {} days", beforeDays);
LocalDateTime cutoff = LocalDateTime.now().minusDays(beforeDays);
- return auditRepository.deleteBefore(cutoff, CLEANUP_BATCH_SIZE,
CLEANUP_MAX_BATCHES);
+ int deleted = auditRepository.deleteBefore(cutoff, CLEANUP_BATCH_SIZE,
CLEANUP_MAX_BATCHES);
+ // Equal to the ceiling means the delete loop stopped because it ran
out of batches, so rows
+ // older than the cutoff may still be stored. Reporting the count
alone hid that.
+ boolean truncated = deleted >= CLEANUP_ROW_CEILING;
+ if (truncated) {
+ log.warn("Audit cleanup older than {} days stopped at the {}-row
ceiling after deleting {} rows; "
+ + "older rows may remain", beforeDays,
CLEANUP_ROW_CEILING, deleted);
+ }
+ return
AuditCleanupResultVO.builder().deleted(deleted).truncated(truncated).build();
}
private void validatePagination(int page, int pageSize) {
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditControllerTest.java
index 2282242e5..3e7345097 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditControllerTest.java
@@ -50,6 +50,9 @@ import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@Import(LegacyJackson2Config.class)
class AuditControllerTest extends WebMvcAuthTestSupport {
+ private static AuditCleanupResultVO cleanupResult(int deleted, boolean
truncated) {
+ return
AuditCleanupResultVO.builder().deleted(deleted).truncated(truncated).build();
+ }
@Autowired
private MockMvc mockMvc;
@@ -95,7 +98,7 @@ class AuditControllerTest extends WebMvcAuthTestSupport {
@Test
void cleanupLogsShouldUseProvidedRetention() throws Exception {
- when(auditService.cleanupLogs(90)).thenReturn(7);
+ when(auditService.cleanupLogs(90)).thenReturn(cleanupResult(7, false));
mockMvc.perform(post("/api/audit-logs/cleanup")
.contentType(MediaType.APPLICATION_JSON)
@@ -107,9 +110,20 @@ class AuditControllerTest extends WebMvcAuthTestSupport {
verify(auditService).cleanupLogs(90);
}
+ @Test
+ void cleanupLogsShouldDiscloseASweepThatHitItsCeilingTest() throws
Exception {
+ when(auditService.cleanupLogs(90)).thenReturn(cleanupResult(10_000,
true));
+
+ mockMvc.perform(post("/api/audit-logs/cleanup")
+ .contentType(MediaType.APPLICATION_JSON)
+
.content(objectMapper.writeValueAsString(Map.of("beforeDays", 90))))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.deleted").value(10_000))
+ .andExpect(jsonPath("$.data.truncated").value(true));
+ }
@Test
void cleanupLogsShouldDefaultRetentionWhenBodyIsEmpty() throws Exception {
- when(auditService.cleanupLogs(30)).thenReturn(3);
+ when(auditService.cleanupLogs(30)).thenReturn(cleanupResult(3, false));
mockMvc.perform(post("/api/audit-logs/cleanup"))
.andExpect(status().isOk())
@@ -120,7 +134,7 @@ class AuditControllerTest extends WebMvcAuthTestSupport {
@Test
void cleanupLogsShouldDefaultRetentionWhenBeforeDaysIsMissing() throws
Exception {
- when(auditService.cleanupLogs(30)).thenReturn(3);
+ when(auditService.cleanupLogs(30)).thenReturn(cleanupResult(3, false));
mockMvc.perform(post("/api/audit-logs/cleanup")
.contentType(MediaType.APPLICATION_JSON)
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
index 64c5bd063..4cbe5bd10 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
@@ -312,12 +312,25 @@ class AuditServiceTest {
void cleanupLogsUsesBoundedRepositoryBatchesTest() {
when(auditRepository.deleteBefore(any(LocalDateTime.class), eq(500),
eq(20))).thenReturn(500);
- int deleted = auditService.cleanupLogs(90);
+ AuditCleanupResultVO result = auditService.cleanupLogs(90);
- assertThat(deleted).isEqualTo(500);
+ assertThat(result.getDeleted()).isEqualTo(500);
+ assertThat(result.isTruncated()).isFalse();
verify(auditRepository).deleteBefore(any(LocalDateTime.class),
eq(500), eq(20));
}
+ @Test
+ void cleanupLogsReportsTheCeilingInsteadOfACompleteSweepTest() {
+ // 500 rows in each of the 20 batches is the most one call can remove,
so a deletion count
+ // equal to that ceiling means the sweep ran out of budget and older
rows may still exist.
+ when(auditRepository.deleteBefore(any(LocalDateTime.class), eq(500),
eq(20))).thenReturn(10_000);
+
+ AuditCleanupResultVO result = auditService.cleanupLogs(90);
+
+ assertThat(result.getDeleted()).isEqualTo(10_000);
+ assertThat(result.isTruncated()).isTrue();
+ }
+
@Test
void queryLogsShouldTrimSearchTermBeforeDelegating() {
auditService.queryLogs(1, 10, " ops ", null, null, null, null, null,
false,