diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/queue/ExecutionQueue.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/queue/ExecutionQueue.java new file mode 100644 index 0000000000..6293422da0 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/queue/ExecutionQueue.java @@ -0,0 +1,58 @@ +package io.metersphere.sdk.dto.queue; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +@Data +public class ExecutionQueue implements Serializable { + /** + * 队列ID, 用于区分不同的队列,如果测试计划执行,队列ID为测试计划报告ID + */ + private String queueId; + + /** + * 报告类型/测试计划类型/测试用例类型/测试集类型/场景集合类型 + */ + private String reportType; + + /** + * 运行模式 + */ + private String runMode; + + /** + * 资源池ID + */ + private String poolId; + + /** + * 创建时间 + */ + private Long createTime; + + /** + * 是否失败继续 + */ + private Boolean failure; + + /** + * 开启重试 + */ + private Boolean retryEnable; + + /** + * 重试次数 + */ + private Long retryNumber; + + /** + * 环境,key= projectID,value=envID + */ + private Map envMap; + + @Serial + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/queue/ExecutionQueueDetail.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/queue/ExecutionQueueDetail.java new file mode 100644 index 0000000000..ea712f8cc9 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/queue/ExecutionQueueDetail.java @@ -0,0 +1,38 @@ +package io.metersphere.sdk.dto.queue; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +@Data +public class ExecutionQueueDetail implements Serializable { + /** + * 资源id,每个资源在同一个运行队列中唯一 + */ + private String resourceId; + + /** + * 排序 + */ + private Integer sort; + + /** + * 当前资源产生的执行报告id + */ + private String reportId; + + /** + * 资源类型 / API,CASE,PLAN_CASE,PLAN_SCENARIO,API_SCENARIO + */ + private String resourceType; + + /** + * 环境,key= projectID,value=envID,优先使用Queue上的环境,如果没有则使用资源上的环境 + */ + private Map envMap; + + @Serial + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/service/queue/ApiExecutionQueueService.java b/backend/services/api-test/src/main/java/io/metersphere/api/service/queue/ApiExecutionQueueService.java new file mode 100644 index 0000000000..aacab7ca00 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/service/queue/ApiExecutionQueueService.java @@ -0,0 +1,101 @@ +package io.metersphere.api.service.queue; + +import io.metersphere.sdk.dto.queue.ExecutionQueue; +import io.metersphere.sdk.dto.queue.ExecutionQueueDetail; +import io.metersphere.sdk.util.JSON; +import jakarta.annotation.Resource; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.LinkedList; +import java.util.List; + +@Service +public class ApiExecutionQueueService { + + public static final String QUEUE_PREFIX = "queue:"; + public static final String QUEUE_DETAIL_PREFIX = "queue:detail:"; + + @Resource + private RedisTemplate redisTemplate; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void insertQueue(ExecutionQueue queue, List queues) { + // 保存队列信息 + redisTemplate.opsForValue().setIfAbsent(QUEUE_PREFIX + queue.getQueueId(), JSON.toJSONString(queue)); + // 保存队列详情信息 + queues.forEach(n -> redisTemplate.opsForList().rightPush(QUEUE_DETAIL_PREFIX + queue.getQueueId(), JSON.toJSONString(n))); + } + + /** + * 获取下一个节点 + */ + public ExecutionQueueDetail getNextDetail(String queueId) throws Exception { + String queueKey = QUEUE_DETAIL_PREFIX + queueId; + ListOperations listOps = redisTemplate.opsForList(); + String queueDetail = listOps.leftPop(queueKey); + if (StringUtils.isBlank(queueDetail)) { + // 重试3次获取 + for (int i = 0; i < 3; i++) { + queueDetail = redisTemplate.opsForList().leftPop(queueKey); + if (StringUtils.isNotBlank(queueDetail)) { + break; + } + Thread.sleep(1000); + } + } + + if (StringUtils.isNotBlank(queueDetail)) { + // 将节点重新放回列表尾部,实现轮询 + return JSON.parseObject(queueDetail, ExecutionQueueDetail.class); + } + + // 整体获取完,清理队列 + redisTemplate.delete(queueKey); + redisTemplate.delete(QUEUE_PREFIX + queueId); + + return null; + } + + /** + * 获取所有节点 + */ + public List getDetails(String queueId) { + String queueKey = QUEUE_DETAIL_PREFIX + queueId; + List details = new LinkedList<>(); + ListOperations listOps = redisTemplate.opsForList(); + Long listSize = listOps.size(queueKey); + if (listSize == null) { + return details; + } + + for (int i = 0; i < listSize; i++) { + String element = listOps.index(queueKey, i); + details.add(JSON.parseObject(element, ExecutionQueueDetail.class)); + } + + return details; + } + + /** + * 获取队列信息 + */ + public ExecutionQueue getQueue(String queueId) { + String queue = redisTemplate.opsForValue().get(QUEUE_PREFIX + queueId); + if (StringUtils.isNotBlank(queue)) { + return JSON.parseObject(queue, ExecutionQueue.class); + } + return null; + } + + public Long size(String queueId) { + ListOperations listOps = redisTemplate.opsForList(); + + String queueKey = QUEUE_DETAIL_PREFIX + queueId; + return listOps.size(queueKey); + } +} diff --git a/backend/services/api-test/src/test/java/io/metersphere/api/service/ApiExecutionQueueServiceTest.java b/backend/services/api-test/src/test/java/io/metersphere/api/service/ApiExecutionQueueServiceTest.java new file mode 100644 index 0000000000..e3f99b9cfc --- /dev/null +++ b/backend/services/api-test/src/test/java/io/metersphere/api/service/ApiExecutionQueueServiceTest.java @@ -0,0 +1,115 @@ +package io.metersphere.api.service; + +import io.metersphere.api.service.queue.ApiExecutionQueueService; +import io.metersphere.sdk.dto.queue.ExecutionQueue; +import io.metersphere.sdk.dto.queue.ExecutionQueueDetail; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.mockito.Mock; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@AutoConfigureMockMvc +public class ApiExecutionQueueServiceTest { + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ListOperations listOps; + + @Resource + private ApiExecutionQueueService apiExecutionQueueService; + + @Test + @Order(1) + void testInsertQueue() { + ExecutionQueue queue = new ExecutionQueue(); + queue.setQueueId("queueId1"); + queue.setReportType("REPORT"); + queue.setRunMode("SEQUENTIAL"); + queue.setPoolId("poolId1"); + queue.setCreateTime(System.currentTimeMillis()); + queue.setFailure(true); + queue.setRetryEnable(true); + queue.setRetryNumber(3L); + queue.setEnvMap(Map.of("projectID1", "envID1", "projectID2", "envID2")); + + ExecutionQueueDetail queueDetail1 = new ExecutionQueueDetail(); + queueDetail1.setResourceId("resourceId1"); + queueDetail1.setSort(1); + queueDetail1.setReportId("reportId1"); + queueDetail1.setResourceType("API"); + queueDetail1.setEnvMap(Map.of("projectID1", "envID1", "projectID2", "envID2")); + + ExecutionQueueDetail queueDetail2 = new ExecutionQueueDetail(); + queueDetail2.setResourceId("resourceId2"); + queueDetail2.setSort(2); + queueDetail2.setReportId("reportId2"); + queueDetail2.setResourceType("CASE"); + queueDetail2.setEnvMap(Map.of("projectID1", "envID1", "projectID2", "envID2")); + + List queueDetails = List.of(queueDetail1, queueDetail2); + + when(redisTemplate.opsForList()).thenReturn(listOps); + + apiExecutionQueueService.insertQueue(queue, queueDetails); + } + + @Test + @Order(2) + void testGetNextDetail() throws Exception { + String queueId = "queueId1"; + System.out.println("start : " + apiExecutionQueueService.size(queueId)); + + when(redisTemplate.opsForList()).thenReturn(listOps); + when(listOps.leftPop(queueId)).thenReturn("{\"resourceId\":\"resourceId1\"}"); + + ExecutionQueueDetail result = apiExecutionQueueService.getNextDetail(queueId); + + assertNotNull(result); + assertEquals("resourceId1", result.getResourceId()); + + System.out.println("end : " + apiExecutionQueueService.size(queueId)); + + } + + @Test + @Order(3) + void testGetDetails() throws Exception { + String queueId = "queueId1"; + + when(redisTemplate.opsForList()).thenReturn(listOps); + when(listOps.size(queueId)).thenReturn(2L); + when(listOps.index(eq(queueId), anyLong())).thenReturn("{\"resourceId\":\"resourceId1\"}", "{\"resourceId\":\"resourceId2\"}"); + + List result = apiExecutionQueueService.getDetails(queueId); + + assertNotNull(result); + assertEquals(1, result.size()); + } + + @Test + @Order(4) + void testGetQueue() throws Exception { + String queueId = "queueId1"; + ExecutionQueue result = apiExecutionQueueService.getQueue(queueId); + + assertNotNull(result); + assertEquals("queueId1", result.getQueueId()); + } +} diff --git a/backend/services/api-test/src/test/java/io/metersphere/api/config/RoundRobinServiceTests.java b/backend/services/api-test/src/test/java/io/metersphere/api/service/RoundRobinServiceTests.java similarity index 96% rename from backend/services/api-test/src/test/java/io/metersphere/api/config/RoundRobinServiceTests.java rename to backend/services/api-test/src/test/java/io/metersphere/api/service/RoundRobinServiceTests.java index 8a95330ede..7a04b084df 100644 --- a/backend/services/api-test/src/test/java/io/metersphere/api/config/RoundRobinServiceTests.java +++ b/backend/services/api-test/src/test/java/io/metersphere/api/service/RoundRobinServiceTests.java @@ -1,7 +1,6 @@ -package io.metersphere.api.config; +package io.metersphere.api.service; import io.metersphere.api.dto.NodeDTO; -import io.metersphere.api.service.RoundRobinService; import io.metersphere.sdk.util.LogUtils; import jakarta.annotation.Resource; import org.junit.jupiter.api.MethodOrderer;