From 0c93e76be6c9603588df3ed0e9807ff0eac877b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=84=B1=ED=95=84?= Date: Mon, 30 Dec 2024 19:01:12 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B9=84=EB=8F=99=EA=B8=B0=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/MockApiController.java | 66 ++++-- .../api/dto/MockRouteRegisterDTO.java | 10 + .../api/dto/MockRouteUpdateDTO.java | 11 + .../testmaster/api/entity/MockResponse.java | 11 + .../testmaster/api/entity/MockRoute.java | 11 + .../api/service/MockRouteService.java | 3 +- .../testmaster/config/HazelcastJSBridge.java | 78 +++++++ .../config/TestMasterHazelcastConfig.java | 39 ++++ .../loadtest/service/VirtualUser.java | 103 +++++---- .../QueueHandlerMgmtController.java | 149 ++++++++++++ .../testmaster/queue/dto/QueueHandlerDTO.java | 24 ++ .../queue/dto/QueueHandlerSearch.java | 30 +++ .../testmaster/queue/entity/QueueHandler.java | 31 +++ .../queue/mapper/QueueHandlerMapper.java | 20 ++ .../repository/QueueHandlerRepository.java | 9 + .../queue/service/DynamicQueueManager.java | 73 ++++++ .../queue/service/QueueHandlerService.java | 51 +++++ .../queue/service/QueueListenerService.java | 162 +++++++++++++ .../eactive/testmaster/user/entity/User.java | 1 + .../resources/templates/fragment/header.html | 3 + .../page/queues/queueHandlerEdit.html | 110 +++++++++ .../page/queues/queueHandlerList.html | 215 ++++++++++++++++++ .../templates/page/routes/mockRouteEdit.html | 29 ++- 23 files changed, 1180 insertions(+), 59 deletions(-) create mode 100644 src/main/java/com/eactive/testmaster/config/HazelcastJSBridge.java create mode 100644 src/main/java/com/eactive/testmaster/config/TestMasterHazelcastConfig.java create mode 100644 src/main/java/com/eactive/testmaster/queue/controller/QueueHandlerMgmtController.java create mode 100644 src/main/java/com/eactive/testmaster/queue/dto/QueueHandlerDTO.java create mode 100644 src/main/java/com/eactive/testmaster/queue/dto/QueueHandlerSearch.java create mode 100644 src/main/java/com/eactive/testmaster/queue/entity/QueueHandler.java create mode 100644 src/main/java/com/eactive/testmaster/queue/mapper/QueueHandlerMapper.java create mode 100644 src/main/java/com/eactive/testmaster/queue/repository/QueueHandlerRepository.java create mode 100644 src/main/java/com/eactive/testmaster/queue/service/DynamicQueueManager.java create mode 100644 src/main/java/com/eactive/testmaster/queue/service/QueueHandlerService.java create mode 100644 src/main/java/com/eactive/testmaster/queue/service/QueueListenerService.java create mode 100644 src/main/resources/templates/page/queues/queueHandlerEdit.html create mode 100644 src/main/resources/templates/page/queues/queueHandlerList.html diff --git a/src/main/java/com/eactive/testmaster/api/controller/MockApiController.java b/src/main/java/com/eactive/testmaster/api/controller/MockApiController.java index b38553c..3f7a4d7 100644 --- a/src/main/java/com/eactive/testmaster/api/controller/MockApiController.java +++ b/src/main/java/com/eactive/testmaster/api/controller/MockApiController.java @@ -7,6 +7,20 @@ import com.eactive.testmaster.api.entity.MockCondition; import com.eactive.testmaster.api.entity.MockResponse; import com.eactive.testmaster.api.entity.MockRoute; import com.eactive.testmaster.api.service.MockRouteService; +import com.eactive.testmaster.config.HazelcastJSBridge; +import com.eactive.testmaster.loadtest.service.Console; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import javax.script.SimpleBindings; +import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,29 +30,46 @@ import org.springframework.http.ResponseEntity; import org.springframework.util.AntPathMatcher; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - @RestController public class MockApiController { private final MockRouteService mockRouteService; - + private final HazelcastJSBridge hazelcastJSBridge; private final ConditionMatcherService conditionMatcherService; - - public MockApiController(MockRouteService mockRouteService, ConditionMatcherService conditionMatcherService) { - this.mockRouteService = mockRouteService; - this.conditionMatcherService = conditionMatcherService; - } + private final ScriptEngine scriptEngine; // Add ScriptEngine as a field + private final Console console; // Add Console as a field private static final Logger logger = LoggerFactory.getLogger(MockApiController.class); + public MockApiController(MockRouteService mockRouteService, HazelcastJSBridge hazelcastJSBridge, ConditionMatcherService conditionMatcherService) { + this.mockRouteService = mockRouteService; + this.hazelcastJSBridge = hazelcastJSBridge; + this.conditionMatcherService = conditionMatcherService; + // Initialize ScriptEngine once + ScriptEngineManager manager = new ScriptEngineManager(); + this.scriptEngine = manager.getEngineByName("nashorn"); + this.hazelcastJSBridge.enhanceEngine(this.scriptEngine); + + // Initialize Console once + this.console = new Console(); + } + + + private void executeScript(String script, MockRequest httpRequest) { + if (script != null && !script.isEmpty()) { + Bindings scriptBindings = new SimpleBindings(); + scriptBindings.put("request", httpRequest); + scriptBindings.put("console", this.console); + try { + scriptEngine.eval(script, scriptBindings); + } catch (ScriptException e) { + logger.error("Error executing request script", e); + e.printStackTrace(); + } + } + } + public ResponseEntity handleRequest(HttpServletRequest httpRequest) throws IOException { MockRequest request = new MockRequest(httpRequest); @@ -53,9 +84,13 @@ public class MockApiController { for (MockRoute mockRoute : mockRouteService.getAllRoutes()) { if (isRouteMatch(mockRoute, request)) { + if (mockRoute.getRequestScript() != null && !mockRoute.getRequestScript().isEmpty()) { + executeScript(mockRoute.getRequestScript(), request); + } Map pathVariables = pathMatcher.extractUriTemplateVariables(mockRoute.getPathPattern(), request.getUri()); MockResponse matchingResponse = findMatchingResponse(mockRoute, request, pathVariables); + if (matchingResponse != null) { if (matchingResponse.getDelay() > 0) { try { @@ -66,6 +101,9 @@ public class MockApiController { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error processing request due to interruption."); } } + + executeScript(matchingResponse.getResponseScript(), request); + HttpHeaders responseHeaders = new HttpHeaders(); matchingResponse.getHeaders().forEach(responseHeaders::set); diff --git a/src/main/java/com/eactive/testmaster/api/dto/MockRouteRegisterDTO.java b/src/main/java/com/eactive/testmaster/api/dto/MockRouteRegisterDTO.java index 682d5e5..2339c87 100644 --- a/src/main/java/com/eactive/testmaster/api/dto/MockRouteRegisterDTO.java +++ b/src/main/java/com/eactive/testmaster/api/dto/MockRouteRegisterDTO.java @@ -22,6 +22,8 @@ public class MockRouteRegisterDTO { private String description; + private String requestScript; + @Size(min = 1, message = "Response must have at least one item") private List responses = new ArrayList<>(); @@ -72,4 +74,12 @@ public class MockRouteRegisterDTO { public void setResponses(List responses) { this.responses = responses; } + + public String getRequestScript() { + return requestScript; + } + + public void setRequestScript(String requestScript) { + this.requestScript = requestScript; + } } diff --git a/src/main/java/com/eactive/testmaster/api/dto/MockRouteUpdateDTO.java b/src/main/java/com/eactive/testmaster/api/dto/MockRouteUpdateDTO.java index f7aac22..ee42882 100644 --- a/src/main/java/com/eactive/testmaster/api/dto/MockRouteUpdateDTO.java +++ b/src/main/java/com/eactive/testmaster/api/dto/MockRouteUpdateDTO.java @@ -20,6 +20,9 @@ public class MockRouteUpdateDTO { private String description; + private String requestScript; + + @Size(min = 1, message = "Response must have at least one item") private List responses = new ArrayList<>(); @@ -70,4 +73,12 @@ public class MockRouteUpdateDTO { public void setResponses(List responses) { this.responses = responses; } + + public String getRequestScript() { + return requestScript; + } + + public void setRequestScript(String requestScript) { + this.requestScript = requestScript; + } } diff --git a/src/main/java/com/eactive/testmaster/api/entity/MockResponse.java b/src/main/java/com/eactive/testmaster/api/entity/MockResponse.java index 7ecc44a..a34257b 100644 --- a/src/main/java/com/eactive/testmaster/api/entity/MockResponse.java +++ b/src/main/java/com/eactive/testmaster/api/entity/MockResponse.java @@ -52,6 +52,9 @@ public class MockResponse { private String encodingCharset; + @Lob + private String responseScript; + public Long getId() { return id; } @@ -123,4 +126,12 @@ public class MockResponse { public void setEncodingCharset(String encodingCharset) { this.encodingCharset = encodingCharset; } + + public String getResponseScript() { + return responseScript; + } + + public void setResponseScript(String responseScript) { + this.responseScript = responseScript; + } } diff --git a/src/main/java/com/eactive/testmaster/api/entity/MockRoute.java b/src/main/java/com/eactive/testmaster/api/entity/MockRoute.java index 8784775..c2c640c 100644 --- a/src/main/java/com/eactive/testmaster/api/entity/MockRoute.java +++ b/src/main/java/com/eactive/testmaster/api/entity/MockRoute.java @@ -8,6 +8,7 @@ import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.Lob; import javax.persistence.OneToMany; import javax.persistence.Table; @@ -28,6 +29,8 @@ public class MockRoute { private String description; + @Lob + private String requestScript; @OneToMany(mappedBy = "route", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true) private List responses = new ArrayList<>(); @@ -80,4 +83,12 @@ public class MockRoute { this.description = description; } + + public String getRequestScript() { + return requestScript; + } + + public void setRequestScript(String requestScript) { + this.requestScript = requestScript; + } } diff --git a/src/main/java/com/eactive/testmaster/api/service/MockRouteService.java b/src/main/java/com/eactive/testmaster/api/service/MockRouteService.java index ac68e89..af723a1 100644 --- a/src/main/java/com/eactive/testmaster/api/service/MockRouteService.java +++ b/src/main/java/com/eactive/testmaster/api/service/MockRouteService.java @@ -73,8 +73,7 @@ public class MockRouteService { route.setName(updated.getName()); route.setDescription(updated.getDescription()); -// route.setMethod(updated.getMethod()); -// route.setPathPattern(updated.getPathPattern()); + route.setRequestScript(updated.getRequestScript()); if (!route.getPathPattern().startsWith("/")) { route.setPathPattern("/" + route.getPathPattern()); } diff --git a/src/main/java/com/eactive/testmaster/config/HazelcastJSBridge.java b/src/main/java/com/eactive/testmaster/config/HazelcastJSBridge.java new file mode 100644 index 0000000..ee94fd1 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/config/HazelcastJSBridge.java @@ -0,0 +1,78 @@ +package com.eactive.testmaster.config; + +import static javax.script.ScriptContext.GLOBAL_SCOPE; + +import com.hazelcast.collection.IQueue; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.scheduledexecutor.IScheduledExecutorService; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.SimpleBindings; +import org.apache.commons.lang3.function.TriFunction; +import org.springframework.stereotype.Service; + +@Service +public class HazelcastJSBridge { + + private final HazelcastInstance hazelcastInstance; + private final IScheduledExecutorService scheduledExecutor; + + + public HazelcastJSBridge(HazelcastInstance hazelcastInstance) { + this.hazelcastInstance = hazelcastInstance; + this.scheduledExecutor = hazelcastInstance.getScheduledExecutorService("delayed-queue-executor"); + + } + + /** + * Creates bindings for Hazelcast operations that can be used in JavaScript + */ + public Bindings createHazelcastBindings() { + Bindings bindings = new SimpleBindings(); + + bindings.put("tmOfferToQueue", (java.util.function.BiFunction) + (queueName, item) -> { + IQueue queue = hazelcastInstance.getQueue(queueName); + return queue.offer(item); + }); + + bindings.put("tmPollFromQueue", (java.util.function.Function) + queueName -> { + IQueue queue = hazelcastInstance.getQueue(queueName); + return queue.poll(); + }); + + bindings.put("tmScheduleToQueue", (TriFunction) + (queueName, item, delaySeconds) -> { + try { + scheduledExecutor.schedule( + () -> { + IQueue queue = hazelcastInstance.getQueue(queueName); + queue.offer(item); + }, + delaySeconds, + TimeUnit.SECONDS + ); + return true; + } catch (Exception e) { + return false; + } + }); + + // Utility operations + bindings.put("tmGetDistributedObjects", + (java.util.function.Supplier>) + () -> hazelcastInstance.getDistributedObjects()); + + return bindings; + } + + /** + * Enhances the JavaScript engine with Hazelcast capabilities + */ + public void enhanceEngine(ScriptEngine engine) { + engine.setBindings(createHazelcastBindings(), GLOBAL_SCOPE); + } +} diff --git a/src/main/java/com/eactive/testmaster/config/TestMasterHazelcastConfig.java b/src/main/java/com/eactive/testmaster/config/TestMasterHazelcastConfig.java new file mode 100644 index 0000000..9e5767a --- /dev/null +++ b/src/main/java/com/eactive/testmaster/config/TestMasterHazelcastConfig.java @@ -0,0 +1,39 @@ +package com.eactive.testmaster.config; + + +import com.hazelcast.config.Config; +import com.hazelcast.config.NetworkConfig; +import com.hazelcast.config.QueueConfig; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TestMasterHazelcastConfig { + + @Bean + public Config hazelcastConfig() { + Config config = new Config(); + config.setInstanceName("hazelcast-instance"); + + // Configure network + NetworkConfig network = config.getNetworkConfig(); + network.setPort(5701); + network.setPortAutoIncrement(true); + + // Configure queue + QueueConfig queueConfig = new QueueConfig(); + queueConfig.setName("taskQueue"); + queueConfig.setMaxSize(1000); + queueConfig.setBackupCount(1); + config.addQueueConfig(queueConfig); + + return config; + } + + @Bean + public HazelcastInstance hazelcastInstance(Config config) { + return Hazelcast.newHazelcastInstance(config); + } +} diff --git a/src/main/java/com/eactive/testmaster/loadtest/service/VirtualUser.java b/src/main/java/com/eactive/testmaster/loadtest/service/VirtualUser.java index ac21e41..d4343bc 100644 --- a/src/main/java/com/eactive/testmaster/loadtest/service/VirtualUser.java +++ b/src/main/java/com/eactive/testmaster/loadtest/service/VirtualUser.java @@ -17,9 +17,11 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.regex.Pattern; +import javax.script.Bindings; import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; import javax.script.ScriptException; +import javax.script.SimpleBindings; +import jdk.nashorn.api.scripting.NashornScriptEngineFactory; import org.apache.commons.lang3.SerializationUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,7 +30,11 @@ public class VirtualUser implements Runnable { //logger private static final Logger logger = LoggerFactory.getLogger(VirtualUser.class); + private ScriptEngine scriptEngine; + private Console console; private final Object pauseLock = new Object(); + private final Object scriptLock = new Object(); // New lock for script execution + private boolean paused = false; private volatile boolean running = true; @@ -43,10 +49,9 @@ public class VirtualUser implements Runnable { private Map variables = new ConcurrentHashMap<>(); private String testId; - private ScenarioNode scenario; + private final ThreadLocal> visitedNodes = ThreadLocal.withInitial(HashSet::new); - private Set visitedNodes = new HashSet<>(); private CountDownLatch latch; @@ -57,11 +62,19 @@ public class VirtualUser implements Runnable { this.maxCount = maxCount; this.apiClientFactory = apiClientFactory; this.latch = latch; + + NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); + this.scriptEngine = factory.getScriptEngine(new String[]{"--language=es5"}); + this.console = new Console(); } public VirtualUser(String testId, ApiClientFactory apiClientFactory) { this.testId = testId; this.apiClientFactory = apiClientFactory; + + NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); + this.scriptEngine = factory.getScriptEngine(new String[]{"--language=es5"}); + this.console = new Console(); } public void pause() { @@ -80,9 +93,8 @@ public class VirtualUser implements Runnable { public void stop() { running = false; latch.countDown(); - resume(); // To ensure the user exits if it's paused + resume(); if (runningThread != null) { - logger.debug("Virtual User Interrupting thread"); runningThread.interrupt(); } } @@ -90,8 +102,6 @@ public class VirtualUser implements Runnable { @Override public void run() { try { - - logger.debug("Virtual user started"); runningThread = Thread.currentThread(); int count = 0; while (maxCount == 0 || running && count < maxCount) { @@ -108,8 +118,6 @@ public class VirtualUser implements Runnable { if (!running) { break; } - //clear visited nodes - visitedNodes.clear(); performAction(); logger.debug("Count: " + (count + 1) + " of " + maxCount); count++; @@ -120,6 +128,8 @@ public class VirtualUser implements Runnable { } private void performAction() { + Set threadLocalVisited = visitedNodes.get(); + threadLocalVisited.clear(); ScenarioNode currentNode = scenario; while (running) { @@ -127,24 +137,23 @@ public class VirtualUser implements Runnable { break; } if (currentNode != null) { - if (visitedNodes.contains(currentNode)) { + if (threadLocalVisited.contains(currentNode)) { logger.debug("Loop detected, stopping"); break; } - visitedNodes.add(currentNode); + threadLocalVisited.add(currentNode); if (currentNode.getClassName().equalsIgnoreCase("api")) { processApi(currentNode.getApiRequest()); } if (currentNode.getConnections() != null && !currentNode.getConnections().isEmpty()) { - currentNode = currentNode.getConnections().get(0); //현재는 1개의 connection만 지원 + currentNode = currentNode.getConnections().get(0); } else { break; } } } - logger.debug("Action performed or interrupted"); } @@ -193,38 +202,48 @@ public class VirtualUser implements Runnable { } } - private void executePreRequestScript(ApiRequestDTO apiRequest) { - ScriptEngineManager manager = new ScriptEngineManager(); - ScriptEngine engine = manager.getEngineByName("nashorn"); - engine.put("console", new Console()); - engine.put("globals", globals); - engine.put("variables", variables); - engine.put("request", apiRequest); - try { - if(apiRequest.getPreRequestScript() != null) - engine.eval(apiRequest.getPreRequestScript()); - } catch (ScriptException e) { - logger.error("Error executing pre-request script", e); - e.printStackTrace(); + private void executeScript(Map bindings, String script) { + if (script == null || script.isEmpty()) { + return; + } + + synchronized (scriptLock) { + try { + Bindings scriptBindings = new SimpleBindings(); + scriptBindings.put("console", console); + scriptBindings.put("globals", globals); + scriptBindings.put("variables", variables); + + if (bindings != null) { + scriptBindings.putAll(bindings); + } + scriptEngine.eval(script, scriptBindings); + } catch (ScriptException e) { + logger.error("Error executing script", e); + e.printStackTrace(); + } } } - private void executePostRequestScript(ApiRequestDTO apiRequest, ApiResponse response) { - ScriptEngineManager manager = new ScriptEngineManager(); - ScriptEngine engine = manager.getEngineByName("nashorn"); - engine.put("console", new Console()); - engine.put("globals", globals); - engine.put("variables", variables); - engine.put("request", apiRequest); - engine.put("response", response); - try { - if (apiRequest.getPostRequestScript() != null){ - engine.eval(apiRequest.getPostRequestScript()); - } - } catch (ScriptException e) { - logger.error("Error executing post-request script", e); - e.printStackTrace(); + private void executePreRequestScript(ApiRequestDTO apiRequest) { + if (apiRequest.getPreRequestScript() == null) { + return; } + + Map bindings = new HashMap<>(); + bindings.put("request", apiRequest); + executeScript(bindings, apiRequest.getPreRequestScript()); + } + + private void executePostRequestScript(ApiRequestDTO apiRequest, ApiResponse response) { + if (apiRequest.getPostRequestScript() == null) { + return; + } + + Map bindings = new HashMap<>(); + bindings.put("request", apiRequest); + bindings.put("response", response); + executeScript(bindings, apiRequest.getPostRequestScript()); } private ApiRequestDTO replacePlaceHoldersWithVariables(ApiRequestDTO apiRequest) { @@ -260,7 +279,7 @@ public class VirtualUser implements Runnable { if (apiRequest.getLayout() != null && apiRequest.getLayout().getLayoutItems() != null) { apiRequest.getLayout().setLayoutItems(replaceInLayoutItems(apiRequest.getLayout().getLayoutItems(), placeholder, value)); } - if (apiRequest.getLayout() !=null && apiRequest.getLayout().getComputedLayoutItemRoot() !=null){ + if (apiRequest.getLayout() != null && apiRequest.getLayout().getComputedLayoutItemRoot() != null) { replaceLayoutItemTree(apiRequest.getLayout().getComputedLayoutItemRoot(), placeholder, value); } } diff --git a/src/main/java/com/eactive/testmaster/queue/controller/QueueHandlerMgmtController.java b/src/main/java/com/eactive/testmaster/queue/controller/QueueHandlerMgmtController.java new file mode 100644 index 0000000..4bed7f9 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/queue/controller/QueueHandlerMgmtController.java @@ -0,0 +1,149 @@ +package com.eactive.testmaster.queue.controller; + +import com.eactive.testmaster.queue.entity.QueueHandler; +import com.eactive.testmaster.queue.mapper.QueueHandlerMapper; +import com.eactive.testmaster.queue.service.DynamicQueueManager; +import com.eactive.testmaster.queue.service.QueueHandlerService; +import com.eactive.testmaster.queue.dto.QueueHandlerDTO; +import com.eactive.testmaster.queue.dto.QueueHandlerSearch; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.annotation.Secured; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Validator; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import javax.annotation.PostConstruct; +import java.util.List; + +@Controller +@RequestMapping("/mgmt/queues") +public class QueueHandlerMgmtController { + + private static final String QUEUE_HANDLER = "queueHandler"; + private static final String QUEUE_HANDLER_EDIT = "page/queues/queueHandlerEdit"; + private static final String QUEUE_HANDLER_LIST_VIEW = "redirect:/mgmt/queues/list_view.do"; + + private final QueueHandlerService queueHandlerService; + private final Validator validator; + private final QueueHandlerMapper queueHandlerMapper; + private final DynamicQueueManager dynamicQueueManager; + + public QueueHandlerMgmtController(QueueHandlerService queueHandlerService, Validator validator, QueueHandlerMapper queueHandlerMapper, DynamicQueueManager dynamicQueueManager) { + this.queueHandlerService = queueHandlerService; + this.validator = validator; + this.queueHandlerMapper = queueHandlerMapper; + this.dynamicQueueManager = dynamicQueueManager; + + } + + + @PostConstruct + public void init() { + queueHandlerService.initAllQueues(); + } + + @GetMapping("/list_view.do") + @Secured("ROLE_MANAGE_QUEUE") + public ModelAndView listView(@ModelAttribute("queueHandlerSearch") QueueHandlerSearch queueHandlerSearch, + Pageable pageable) { + ModelAndView mav = new ModelAndView("page/queues/queueHandlerList"); + Page page = queueHandlerService.findAll(queueHandlerSearch.buildSpecification(), pageable); + mav.addObject("page", page); + mav.addObject("pageable", pageable); + mav.addObject("modelSearch", queueHandlerSearch); + return mav; + } + + @GetMapping("/create_view.do") + @Secured("ROLE_MANAGE_QUEUE") + public ModelAndView createView() { + ModelAndView mav = new ModelAndView(QUEUE_HANDLER_EDIT); + mav.addObject(QUEUE_HANDLER, new QueueHandlerDTO()); + return mav; + } + + @GetMapping("/update_view.do") + @Secured("ROLE_MANAGE_QUEUE") + public ModelAndView updateView(@RequestParam("id") String id) { + ModelAndView mav = new ModelAndView(QUEUE_HANDLER_EDIT); + QueueHandler found = queueHandlerService.findById(id); + mav.addObject(QUEUE_HANDLER, found); // Assuming direct usage of entity, you might want to use a DTO mapper + return mav; + } + + @PostMapping("/register.do") + @Secured("ROLE_MANAGE_QUEUE") + public String register(@ModelAttribute("queueHandler") QueueHandlerDTO dto, + BindingResult bindingResult, + ModelMap model) { + + if (dto == null) { + return "redirect:/mgmt/queues/create_view.do"; + } + + validator.validate(dto, bindingResult); + + if (bindingResult.hasErrors()) { + model.addAttribute(QUEUE_HANDLER, dto); + return QUEUE_HANDLER_EDIT; + } + + QueueHandler queueHandler = queueHandlerMapper.map(dto); + queueHandlerService.create(queueHandler); + queueHandlerService.initAllQueues(); + return QUEUE_HANDLER_LIST_VIEW; + } + + @PostMapping("/update.do") + @Secured("ROLE_MANAGE_QUEUE") + public String update(@RequestParam("queueName") String queueName, + @ModelAttribute("queueHandler") QueueHandlerDTO dto, + BindingResult bindingResult, + ModelMap model) { + + if (dto.getQueueName() == null) { + return QUEUE_HANDLER_LIST_VIEW; + } + + validator.validate(dto, bindingResult); + + if (bindingResult.hasErrors()) { + model.addAttribute(QUEUE_HANDLER, dto); + return QUEUE_HANDLER_EDIT; + } + + QueueHandler queueHandler = queueHandlerMapper.map(dto); + queueHandlerService.update(dto.getQueueName(), queueHandler); + queueHandlerService.initAllQueues(); + return QUEUE_HANDLER_LIST_VIEW; + } + + @PostMapping("/delete.do") + @Secured("ROLE_MANAGE_QUEUE") + public String delete(@RequestParam("checkedIdForDel") List ids) { + for (String id : ids) { + queueHandlerService.deleteById(id); + } + queueHandlerService.initAllQueues(); + return QUEUE_HANDLER_LIST_VIEW; + } + + @PostMapping("/start.do") + @Secured("ROLE_MANAGE_QUEUE") + public String startQueue(@RequestParam("id") String id) { + QueueHandler queueHandler = queueHandlerService.findById(id); + dynamicQueueManager.startQueueListener(queueHandler); + return QUEUE_HANDLER_LIST_VIEW; + } + + @PostMapping("/stop.do") + @Secured("ROLE_MANAGE_QUEUE") + public String stopQueue(@RequestParam("id") String id) { + QueueHandler queueHandler = queueHandlerService.findById(id); + dynamicQueueManager.stopQueueListener(queueHandler.getQueueName()); + return QUEUE_HANDLER_LIST_VIEW; + } +} diff --git a/src/main/java/com/eactive/testmaster/queue/dto/QueueHandlerDTO.java b/src/main/java/com/eactive/testmaster/queue/dto/QueueHandlerDTO.java new file mode 100644 index 0000000..0e93db0 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/queue/dto/QueueHandlerDTO.java @@ -0,0 +1,24 @@ +package com.eactive.testmaster.queue.dto; + +public class QueueHandlerDTO { + + private String queueName; + + private String handlerScript; + + public String getQueueName() { + return queueName; + } + + public void setQueueName(String queueName) { + this.queueName = queueName; + } + + public String getHandlerScript() { + return handlerScript; + } + + public void setHandlerScript(String handlerScript) { + this.handlerScript = handlerScript; + } +} diff --git a/src/main/java/com/eactive/testmaster/queue/dto/QueueHandlerSearch.java b/src/main/java/com/eactive/testmaster/queue/dto/QueueHandlerSearch.java new file mode 100644 index 0000000..a5ee019 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/queue/dto/QueueHandlerSearch.java @@ -0,0 +1,30 @@ +package com.eactive.testmaster.queue.dto; + +import com.eactive.testmaster.common.search.BaseSearch; +import com.eactive.testmaster.common.search.ColumnSearchModel; +import com.eactive.testmaster.common.search.SearchCondition; +import com.eactive.testmaster.common.search.SearchModel; +import com.eactive.testmaster.queue.entity.QueueHandler; +import java.util.ArrayList; +import java.util.List; + +public class QueueHandlerSearch implements BaseSearch { + + private String queueName; + + @Override + public List buildSearchCondition() { + List models = new ArrayList<>(); + models.add(new ColumnSearchModel("queueName", SearchCondition.LIKE, queueName)); + return models; + } + + public String getQueueName() { + return queueName; + } + + public void setQueueName(String queueName) { + this.queueName = queueName; + } +} + diff --git a/src/main/java/com/eactive/testmaster/queue/entity/QueueHandler.java b/src/main/java/com/eactive/testmaster/queue/entity/QueueHandler.java new file mode 100644 index 0000000..d7c15f5 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/queue/entity/QueueHandler.java @@ -0,0 +1,31 @@ +package com.eactive.testmaster.queue.entity; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Lob; + +@Entity +public class QueueHandler { + + @Id + private String queueName; + + @Lob + private String handlerScript; + + public String getQueueName() { + return queueName; + } + + public void setQueueName(String queueName) { + this.queueName = queueName; + } + + public String getHandlerScript() { + return handlerScript; + } + + public void setHandlerScript(String handlerScript) { + this.handlerScript = handlerScript; + } +} diff --git a/src/main/java/com/eactive/testmaster/queue/mapper/QueueHandlerMapper.java b/src/main/java/com/eactive/testmaster/queue/mapper/QueueHandlerMapper.java new file mode 100644 index 0000000..92593d9 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/queue/mapper/QueueHandlerMapper.java @@ -0,0 +1,20 @@ +package com.eactive.testmaster.queue.mapper; + +import com.eactive.testmaster.common.mapper.CommonMapper; +import com.eactive.testmaster.queue.dto.QueueHandlerDTO; +import com.eactive.testmaster.queue.entity.QueueHandler; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.springframework.stereotype.Component; + +@Mapper(componentModel = "spring", + uses = {CommonMapper.class}, + unmappedTargetPolicy = ReportingPolicy.IGNORE) +@Component +public abstract class QueueHandlerMapper { + + public abstract QueueHandler map(QueueHandlerDTO queue); + + public abstract QueueHandlerDTO map(QueueHandler queue); + +} diff --git a/src/main/java/com/eactive/testmaster/queue/repository/QueueHandlerRepository.java b/src/main/java/com/eactive/testmaster/queue/repository/QueueHandlerRepository.java new file mode 100644 index 0000000..2eb9f9d --- /dev/null +++ b/src/main/java/com/eactive/testmaster/queue/repository/QueueHandlerRepository.java @@ -0,0 +1,9 @@ +package com.eactive.testmaster.queue.repository; + +import com.eactive.testmaster.queue.entity.QueueHandler; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface QueueHandlerRepository extends JpaRepository, JpaSpecificationExecutor { + +} diff --git a/src/main/java/com/eactive/testmaster/queue/service/DynamicQueueManager.java b/src/main/java/com/eactive/testmaster/queue/service/DynamicQueueManager.java new file mode 100644 index 0000000..ff29696 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/queue/service/DynamicQueueManager.java @@ -0,0 +1,73 @@ +package com.eactive.testmaster.queue.service; + +import com.eactive.testmaster.config.HazelcastJSBridge; +import com.eactive.testmaster.queue.entity.QueueHandler; +import com.hazelcast.core.HazelcastInstance; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DynamicQueueManager { + + private static final Logger logger = LoggerFactory.getLogger(DynamicQueueManager.class); + + private final HazelcastInstance hazelcastInstance; + private final Map queueListeners; + private final HazelcastJSBridge hazelcastJSBridge; + + @Autowired + public DynamicQueueManager(HazelcastInstance hazelcastInstance, HazelcastJSBridge hazelcastJSBridge) { + this.hazelcastInstance = hazelcastInstance; + this.hazelcastJSBridge = hazelcastJSBridge; + this.queueListeners = new ConcurrentHashMap<>(); + } + + /** + * Start a listener for the specified queue + */ +// public void startQueueListener(QueueHandler queueHandler) { +// if (!queueListeners.containsKey(queueHandler.getQueueName())) { +// QueueListenerService listener = new QueueListenerService(hazelcastInstance, hazelcastJSBridge); +// listener.setQueueHandler(queueHandler); +// queueListeners.put(queueHandler.getQueueName(), listener); +// listener.startListening(); +// logger.info("Started listener for queue: {}", queueHandler.getQueueName()); +// } else { +// logger.warn("Listener already exists for queue: {}", queueHandler.getQueueName()); +// } +// } + public void startQueueListener(QueueHandler queueHandler) { + QueueListenerService existingListener = queueListeners.get(queueHandler.getQueueName()); + if (existingListener != null) { + // If exists but not running, remove it so we can start fresh + if (!existingListener.isRunning()) { + queueListeners.remove(queueHandler.getQueueName()); + } else { + logger.warn("Listener already exists and running for queue: {}", queueHandler.getQueueName()); + return; + } + } + + QueueListenerService listener = new QueueListenerService(hazelcastInstance, hazelcastJSBridge); + listener.setQueueHandler(queueHandler); + queueListeners.put(queueHandler.getQueueName(), listener); + listener.startListening(); + logger.info("Started listener for queue: {}", queueHandler.getQueueName()); + } + + /** + * Stop a queue listener + */ + public void stopQueueListener(String queueName) { + QueueListenerService listener = queueListeners.remove(queueName); + if (listener != null) { + listener.stopListening(); + logger.info("Stopped listener for queue: {}", queueName); + } + } + +} diff --git a/src/main/java/com/eactive/testmaster/queue/service/QueueHandlerService.java b/src/main/java/com/eactive/testmaster/queue/service/QueueHandlerService.java new file mode 100644 index 0000000..d4f9ccd --- /dev/null +++ b/src/main/java/com/eactive/testmaster/queue/service/QueueHandlerService.java @@ -0,0 +1,51 @@ +package com.eactive.testmaster.queue.service; + +import com.eactive.testmaster.queue.entity.QueueHandler; +import com.eactive.testmaster.queue.repository.QueueHandlerRepository; +import javax.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; + +@Service +@Transactional +public class QueueHandlerService { + + @Autowired + private QueueHandlerRepository queueHandlerRepository; + + @Autowired + private DynamicQueueManager dynamicQueueManager; + + public void initAllQueues() { + queueHandlerRepository.findAll().forEach(queueHandler -> { + dynamicQueueManager.startQueueListener(queueHandler); + }); + } + + + public Page findAll(Specification specification, Pageable pageable) { + return queueHandlerRepository.findAll(specification, pageable); + } + + public QueueHandler findById(String id) { + return queueHandlerRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Queue Handler not found with id: " + id)); + } + + public QueueHandler create(QueueHandler queueHandler) { + return queueHandlerRepository.save(queueHandler); + } + + public QueueHandler update(String queueName, QueueHandler queueHandler) { + QueueHandler handler = queueHandlerRepository.findById(queueName).orElseThrow(() -> new IllegalArgumentException("Queue Handler not found with name: " + queueName)); + handler.setHandlerScript(queueHandler.getHandlerScript()); + return queueHandlerRepository.save(handler); + } + + public void deleteById(String id) { + queueHandlerRepository.deleteById(id); + } +} diff --git a/src/main/java/com/eactive/testmaster/queue/service/QueueListenerService.java b/src/main/java/com/eactive/testmaster/queue/service/QueueListenerService.java new file mode 100644 index 0000000..634bc02 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/queue/service/QueueListenerService.java @@ -0,0 +1,162 @@ +package com.eactive.testmaster.queue.service; + +import com.eactive.testmaster.config.HazelcastJSBridge; +import com.eactive.testmaster.loadtest.service.Console; +import com.eactive.testmaster.queue.entity.QueueHandler; +import com.hazelcast.collection.IQueue; +import com.hazelcast.core.HazelcastInstance; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import javax.script.SimpleBindings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class QueueListenerService { + + private static final Logger logger = LoggerFactory.getLogger(QueueListenerService.class); + + private IQueue taskQueue; + private final ExecutorService executorService; + private final AtomicBoolean running; + private final HazelcastInstance hazelcastInstance; + private final HazelcastJSBridge hazelcastJSBridge; + private QueueHandler queueHandler; + private final ScriptEngine scriptEngine; // Add ScriptEngine as a field + private final Console console; + + public void setQueueHandler(QueueHandler queueHandler) { + this.queueHandler = queueHandler; + } + + @Autowired + public QueueListenerService(HazelcastInstance hazelcastInstance, HazelcastJSBridge hazelcastJSBridge) { + this.hazelcastInstance = hazelcastInstance; + this.hazelcastJSBridge = hazelcastJSBridge; + + ScriptEngineManager manager = new ScriptEngineManager(); + this.scriptEngine = manager.getEngineByName("nashorn"); + this.hazelcastJSBridge.enhanceEngine(this.scriptEngine); + this.executorService = Executors.newSingleThreadExecutor(); + this.running = new AtomicBoolean(false); + this.console = new Console(); + } + +// @PostConstruct +// public void startListening() { +// if (queueHandler != null) { +// taskQueue = hazelcastInstance.getQueue(queueHandler.getQueueName()); +// executorService.submit(this::processQueueItems); +// logger.info("Queue listener started"); +// this.running.set(true); +// } +// } + + // private void processQueueItems() { +// while (running.get()) { +// try { +// Object item = taskQueue.poll(); +// if (item != null) { +// processItem(item); +// } +// } catch (Exception e) { +// logger.error("Error processing queue item", e); +// } +// } +// } + + @PostConstruct + public void startListening() { + if (queueHandler != null) { + // Set running flag BEFORE starting the processing + this.running.set(true); + taskQueue = hazelcastInstance.getQueue(queueHandler.getQueueName()); + executorService.submit(this::processQueueItems); + logger.info("Queue listener started for queue: {}", queueHandler.getQueueName()); + } + } + + private void processQueueItems() { + while (running.get()) { + try { + // Use take() instead of poll() to block until an item is available + Object item = taskQueue.take(); + processItem(item); + } catch (InterruptedException e) { + // Handle interruption (likely from shutdown) + logger.info("Queue listener interrupted, ending processing"); + Thread.currentThread().interrupt(); + break; + } catch (Exception e) { + logger.error("Error processing queue item", e); + } + } + logger.info("Queue listener stopped processing items"); + } + + + private void processItem(Object message) { + + String script = queueHandler.getHandlerScript(); + if (script != null && !script.isEmpty()) { + Bindings scriptBindings = new SimpleBindings(); + scriptBindings.put("console", this.console); + scriptBindings.put("message", message); + + try { + scriptEngine.eval(script, scriptBindings); + } catch (ScriptException e) { + logger.error("Error executing request script", e); + e.printStackTrace(); + } + } + } + + public boolean isRunning() { + return running.get(); + } + +// @PreDestroy +// public void stopListening() { +// logger.info("Stopping queue listener..."); +// running.set(false); +// executorService.shutdown(); +// try { +// if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) { +// executorService.shutdownNow(); +// } +// } catch (InterruptedException e) { +// Thread.currentThread().interrupt(); +// executorService.shutdownNow(); +// } +// logger.info("Queue listener stopped"); +// +// } + + @PreDestroy + public void stopListening() { + logger.info("Stopping queue listener..."); + running.set(false); + // Interrupt the blocking take() call + executorService.shutdownNow(); + try { + if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) { + logger.warn("Queue listener did not terminate in time"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + logger.info("Queue listener stopped"); + } + +} diff --git a/src/main/java/com/eactive/testmaster/user/entity/User.java b/src/main/java/com/eactive/testmaster/user/entity/User.java index 0f0a959..b7b8bce 100644 --- a/src/main/java/com/eactive/testmaster/user/entity/User.java +++ b/src/main/java/com/eactive/testmaster/user/entity/User.java @@ -133,6 +133,7 @@ public abstract class User extends Auditable implements Serializable, UserDetail authorities.add(new SimpleGrantedAuthority((userSecurity == null || userSecurity.isEmpty())? "ROLE_USER" : userSecurity)); if (this.userSecurity.equals("ROLE_ADMIN")) { + authorities.add(new SimpleGrantedAuthority("ROLE_MANAGE_QUEUE")); authorities.add(new SimpleGrantedAuthority("ROLE_MANAGE_ROUTE")); authorities.add(new SimpleGrantedAuthority("ROLE_MANAGE_SERVER")); authorities.add(new SimpleGrantedAuthority("ROLE_MANAGE_USER")); diff --git a/src/main/resources/templates/fragment/header.html b/src/main/resources/templates/fragment/header.html index 8f5a3fb..7235725 100644 --- a/src/main/resources/templates/fragment/header.html +++ b/src/main/resources/templates/fragment/header.html @@ -22,6 +22,9 @@ + diff --git a/src/main/resources/templates/page/queues/queueHandlerEdit.html b/src/main/resources/templates/page/queues/queueHandlerEdit.html new file mode 100644 index 0000000..7527019 --- /dev/null +++ b/src/main/resources/templates/page/queues/queueHandlerEdit.html @@ -0,0 +1,110 @@ + + + + +
+ + +
+ +
+
+
+

Queue Handler 등록

+

Queue Handler 수정

+
+
+ + + +
+ + + +
+
+
+ + +
+
Handler Script
+
+
+ +
+
+ +
+ +
+
+
+
+ + + + + + diff --git a/src/main/resources/templates/page/queues/queueHandlerList.html b/src/main/resources/templates/page/queues/queueHandlerList.html new file mode 100644 index 0000000..ec6829b --- /dev/null +++ b/src/main/resources/templates/page/queues/queueHandlerList.html @@ -0,0 +1,215 @@ + + + + + +
+
+
+
+
Queue Handler List
+
+
+
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
No.
+
+ +
+
Queue Name
Actions
+
+ + +
+
+
+ +
+
+
+ + + +
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/src/main/resources/templates/page/routes/mockRouteEdit.html b/src/main/resources/templates/page/routes/mockRouteEdit.html index 2cd85ab..e9b4b28 100644 --- a/src/main/resources/templates/page/routes/mockRouteEdit.html +++ b/src/main/resources/templates/page/routes/mockRouteEdit.html @@ -78,6 +78,15 @@
+ +
+
요청 스크립트
+
+
+ +
+
+
@@ -240,12 +249,14 @@ name: '', method: '', pathPattern: '', - responses: [] + requestScript: '', + responses: [], }; let view = { currentIdx: 0, responseEditors : [], + requestScriptEditor: null, // Add reference for request script editor init: function () { $('.btn_add_case').on('click', function () { controller.addResponse(); @@ -310,7 +321,23 @@ response.headers.push(header); }); }); + $('#responses').empty(); + + require(['vs/editor/editor.main'], function () { + view.requestScriptEditor = monaco.editor.create(document.getElementById('request_script_editor'), { + value: $('textarea[name="requestScript"]').val() || '', + theme: 'vs-light', + language: 'javascript', + automaticLayout: true + }); + + view.requestScriptEditor.onDidChangeModelContent(function (e) { + model.requestScript = view.requestScriptEditor.getValue(); + $('textarea[name="requestScript"]').val(view.requestScriptEditor.getValue()); + }); + }); + this.render(); }, renderCondition: function(i) {