This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/master by this push:
     new ec5564b54f [fix] pre-validate monitor import batch to avoid orphan 
tasks and empty list (#4180)
ec5564b54f is described below

commit ec5564b54f77644ceb2a5f15d9696fb1e5702211
Author: Duansg <[email protected]>
AuthorDate: Mon Jun 29 09:54:43 2026 +0800

    [fix] pre-validate monitor import batch to avoid orphan tasks and empty 
list (#4180)
    
    Co-authored-by: aias00 <[email protected]>
---
 .../common/constants/ImExportTaskConstant.java     |  5 +-
 .../service/impl/AbstractImExportServiceImpl.java  | 28 ++++++++--
 .../manager/service/ExcelImExportServiceTest.java  | 63 ++++++++++++++++++++++
 3 files changed, 89 insertions(+), 7 deletions(-)

diff --git 
a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ImExportTaskConstant.java
 
b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ImExportTaskConstant.java
index 31256dfed7..4d8fb53b97 100644
--- 
a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ImExportTaskConstant.java
+++ 
b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ImExportTaskConstant.java
@@ -24,7 +24,8 @@ package org.apache.hertzbeat.common.constants;
 public interface ImExportTaskConstant {
 
     /**
-     * If the number of tasks exceeds 100, progress information will broadcast
+     * If the number of tasks reaches this threshold, progress information 
will broadcast.
+     * Kept low so medium-sized imports (e.g. dozens of monitors) still report 
incremental progress.
      */
-    Integer IMPORT_TASK_PROCESS_THRESHOLD = 100;
+    Integer IMPORT_TASK_PROCESS_THRESHOLD = 10;
 }
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
index 73115896f9..901c5132a0 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
@@ -31,7 +31,6 @@ import org.apache.hertzbeat.manager.config.ManagerSseManager;
 import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
 import org.apache.hertzbeat.manager.service.ImExportService;
 import org.apache.hertzbeat.manager.service.MonitorService;
-import org.apache.hertzbeat.base.service.LabelService;
 import org.springframework.beans.BeanUtils;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.util.CollectionUtils;
@@ -40,9 +39,11 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.time.LocalDate;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * class AbstractImExportServiceImpl
@@ -54,9 +55,6 @@ public abstract class AbstractImExportServiceImpl implements 
ImExportService {
     @Lazy
     private MonitorService monitorService;
 
-    @Resource
-    private LabelService tagService;
-
     @Resource
     private ManagerSseManager managerSseManager;
 
@@ -65,10 +63,10 @@ public abstract class AbstractImExportServiceImpl 
implements ImExportService {
         var formList = parseImport(is).stream().map(this::convert).toList();
         if (!CollectionUtils.isEmpty(formList)) {
             int totalElements = formList.size();
+            validateImportBatch(formList);
             int progressInterval = Math.max(1, totalElements / 10);
             for (int i = 0; i < totalElements; i++) {
                 MonitorDto monitorDto = formList.get(i);
-                monitorService.validate(monitorDto, false);
                 monitorService.addMonitor(monitorDto.getMonitor(), 
monitorDto.getParams(), monitorDto.getCollector(), 
monitorDto.getGrafanaDashboard());
                 if (totalElements >= 
ImExportTaskConstant.IMPORT_TASK_PROCESS_THRESHOLD && ((i + 1) % 
progressInterval == 0) && (i + 1 < totalElements)) {
                     managerSseManager.broadcastImportTaskInProgress(taskName, 
(int) ((i + 1) * 100.0 / totalElements));
@@ -78,6 +76,26 @@ public abstract class AbstractImExportServiceImpl implements 
ImExportService {
         }
     }
 
+    /**
+     * Validate every entry of an import batch before any monitor is persisted.
+     * Beyond the per-entry checks (params/collector/name uniqueness against 
the DB), this also detects
+     * duplicate monitor names within the import file itself, which the 
single-entry validation cannot
+     * catch because nothing has been persisted yet when the conflicting rows 
are validated.
+     *
+     * @param formList parsed monitors to import
+     * @throws IllegalArgumentException if any entry is invalid or names 
collide within the file
+     */
+    private void validateImportBatch(List<MonitorDto> formList) {
+        Set<String> names = new HashSet<>(formList.size());
+        for (MonitorDto monitorDto : formList) {
+            monitorService.validate(monitorDto, false);
+            String name = monitorDto.getMonitorInfo().getName();
+            if (!names.add(name)) {
+                throw new IllegalArgumentException("Duplicate monitor name in 
import file: " + name);
+            }
+        }
+    }
+
     @Override
     public void exportConfig(OutputStream os, List<Long> configList) {
         var monitorList = configList.stream().map(it -> 
monitorService.getMonitorDto(it)).filter(Objects::nonNull).map(this::convert).toList();
diff --git 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ExcelImExportServiceTest.java
 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ExcelImExportServiceTest.java
index ac83520077..f87adfe46b 100644
--- 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ExcelImExportServiceTest.java
+++ 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ExcelImExportServiceTest.java
@@ -19,11 +19,20 @@ package org.apache.hertzbeat.manager.service;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
+import org.apache.hertzbeat.manager.config.ManagerSseManager;
+import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
 import org.apache.hertzbeat.manager.service.impl.AbstractImExportServiceImpl;
 import org.apache.hertzbeat.manager.service.impl.ExcelImExportServiceImpl;
 import org.apache.poi.ss.usermodel.Workbook;
@@ -32,6 +41,7 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
+import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 /**
@@ -43,6 +53,12 @@ class ExcelImExportServiceTest {
     @InjectMocks
     private ExcelImExportServiceImpl excelImExportService;
 
+    @Mock
+    private MonitorService monitorService;
+
+    @Mock
+    private ManagerSseManager managerSseManager;
+
     @BeforeEach
     public void setUp() {
 
@@ -108,6 +124,53 @@ class ExcelImExportServiceTest {
         assertEquals("Host1", dataRow.getCell(2).getStringCellValue());
     }
 
+    @Test
+    public void testImportConfigAllValid() throws IOException {
+        ByteArrayInputStream is = buildImportStream(List.of("Monitor1", 
"Monitor2"));
+        excelImExportService.importConfig("task", is);
+        verify(monitorService, times(2)).validate(any(MonitorDto.class), 
eq(false));
+        verify(monitorService, times(2)).addMonitor(any(), any(), any(), 
any());
+        verify(managerSseManager).broadcastImportTaskSuccess("task");
+    }
+
+    @Test
+    public void testImportConfigRejectsDuplicateNameInFile() throws 
IOException {
+        ByteArrayInputStream is = buildImportStream(List.of("DupMonitor", 
"DupMonitor"));
+        assertThrows(IllegalArgumentException.class, () -> 
excelImExportService.importConfig("task", is));
+        verify(monitorService, never()).addMonitor(any(), any(), any(), any());
+        verify(managerSseManager, never()).broadcastImportTaskSuccess(any());
+    }
+
+    @Test
+    public void testImportConfigRejectsWholeBatchWhenLaterEntryInvalid() 
throws IOException {
+        doNothing().doThrow(new IllegalArgumentException("Monitoring name 
already exists!"))
+                .when(monitorService).validate(any(MonitorDto.class), 
eq(false));
+        ByteArrayInputStream is = buildImportStream(List.of("Monitor1", 
"Monitor2"));
+        assertThrows(IllegalArgumentException.class, () -> 
excelImExportService.importConfig("task", is));
+        verify(monitorService, never()).addMonitor(any(), any(), any(), any());
+        verify(managerSseManager, never()).broadcastImportTaskSuccess(any());
+    }
+
+    private ByteArrayInputStream buildImportStream(List<String> names) throws 
IOException {
+        try (Workbook workbook = new XSSFWorkbook()) {
+            var sheet = workbook.createSheet();
+            var headerRow = sheet.createRow(0);
+            headerRow.createCell(0).setCellValue("name");
+            headerRow.createCell(1).setCellValue("app");
+            headerRow.createCell(2).setCellValue("host");
+            for (int i = 0; i < names.size(); i++) {
+                var dataRow = sheet.createRow(i + 1);
+                dataRow.createCell(0).setCellValue(names.get(i));
+                dataRow.createCell(1).setCellValue("App");
+                dataRow.createCell(2).setCellValue("Host");
+                dataRow.createCell(4).setCellValue(1);
+            }
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            workbook.write(bos);
+            return new ByteArrayInputStream(bos.toByteArray());
+        }
+    }
+
     @Test
     public void testWriteThenParsePreservesAllFields() throws IOException {
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to