load tester beta
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package com.eactive.testmaster.client.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ApiRequestDTO {
|
||||
public class ApiRequestDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String id;
|
||||
|
||||
@@ -28,6 +31,8 @@ public class ApiRequestDTO {
|
||||
|
||||
private String postRequestScript;
|
||||
|
||||
private long sentBytes; //estimated size of the request
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -115,4 +120,12 @@ public class ApiRequestDTO {
|
||||
public void setPostRequestScript(String postRequestScript) {
|
||||
this.postRequestScript = postRequestScript;
|
||||
}
|
||||
|
||||
public long getSentBytes() {
|
||||
return sentBytes;
|
||||
}
|
||||
|
||||
public void setSentBytes(long sentBytes) {
|
||||
this.sentBytes = sentBytes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
package com.eactive.testmaster.client.dto;
|
||||
|
||||
public class ApiRequestKeyValueDTO {
|
||||
import java.io.Serializable;
|
||||
|
||||
public class ApiRequestKeyValueDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private boolean enabled;
|
||||
|
||||
private String key;
|
||||
private String value;
|
||||
|
||||
|
||||
public ApiRequestKeyValueDTO(boolean enabled, String key, String value) {
|
||||
this.enabled = enabled;
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.eactive.testmaster.client.dto.internal;
|
||||
|
||||
public class ScenarioConnection {
|
||||
|
||||
private String node;
|
||||
private String input;
|
||||
private String output;
|
||||
|
||||
public String getNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
public void setNode(String node) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
public String getInput() {
|
||||
return input;
|
||||
}
|
||||
|
||||
public void setInput(String input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
public String getOutput() {
|
||||
return output;
|
||||
}
|
||||
|
||||
public void setOutput(String output) {
|
||||
this.output = output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.eactive.testmaster.client.dto.internal;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ScenarioConnections {
|
||||
|
||||
private List<ScenarioConnection> connections;
|
||||
|
||||
public List<ScenarioConnection> getConnections() {
|
||||
return connections;
|
||||
}
|
||||
|
||||
public void setConnections(List<ScenarioConnection> connections) {
|
||||
this.connections = connections;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.eactive.testmaster.client.dto.internal;
|
||||
|
||||
import com.eactive.testmaster.client.dto.ApiRequestDTO;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ScenarioNode {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private Map<String, String> data;
|
||||
|
||||
@JsonProperty("class")
|
||||
private String className;
|
||||
|
||||
private String html;
|
||||
private Map<String, ScenarioConnections> inputs;
|
||||
private Map<String, ScenarioConnections> outputs;
|
||||
|
||||
private List<ScenarioNode> connections;
|
||||
|
||||
private ApiRequestDTO apiRequest;
|
||||
|
||||
public ScenarioNode() {
|
||||
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Map<String, String> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(Map<String, String> data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public String getClassName() {
|
||||
return className;
|
||||
}
|
||||
|
||||
public void setClassName(String className) {
|
||||
this.className = className;
|
||||
}
|
||||
|
||||
public String getHtml() {
|
||||
return html;
|
||||
}
|
||||
|
||||
public void setHtml(String html) {
|
||||
this.html = html;
|
||||
}
|
||||
|
||||
public Map<String, ScenarioConnections> getInputs() {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
public void setInputs(Map<String, ScenarioConnections> inputs) {
|
||||
this.inputs = inputs;
|
||||
}
|
||||
|
||||
public Map<String, ScenarioConnections> getOutputs() {
|
||||
return outputs;
|
||||
}
|
||||
|
||||
public void setOutputs(Map<String, ScenarioConnections> outputs) {
|
||||
this.outputs = outputs;
|
||||
}
|
||||
|
||||
public List<ScenarioNode> getConnections() {
|
||||
return connections;
|
||||
}
|
||||
|
||||
public void setConnections(List<ScenarioNode> connections) {
|
||||
this.connections = connections;
|
||||
}
|
||||
|
||||
public void addNode(ScenarioNode node) {
|
||||
if (this.connections == null)
|
||||
this.connections = new ArrayList<>();
|
||||
|
||||
this.connections.add(node);
|
||||
}
|
||||
|
||||
public ApiRequestDTO getApiRequest() {
|
||||
return apiRequest;
|
||||
}
|
||||
|
||||
public void setApiRequest(ApiRequestDTO apiRequest) {
|
||||
this.apiRequest = apiRequest;
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import com.eactive.testmaster.client.dto.ApiCollectionDTO;
|
||||
import com.eactive.testmaster.client.dto.ApiRequestDTO;
|
||||
import com.eactive.testmaster.client.entity.ApiCollection;
|
||||
import com.eactive.testmaster.client.entity.ApiRequestInfo;
|
||||
import com.eactive.testmaster.client.repository.ApiRequestRepository;
|
||||
import com.eactive.testmaster.common.mapper.CommonMapper;
|
||||
import com.eactive.testmaster.server.mapper.ServerMapper;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
@@ -17,17 +19,25 @@ import java.util.List;
|
||||
uses = {CommonMapper.class, ServerMapper.class},
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
@Component
|
||||
public interface ApiRequestMapper {
|
||||
ApiCollection map(ApiCollectionDTO dto);
|
||||
public abstract class ApiRequestMapper {
|
||||
|
||||
ApiRequestInfo map(ApiRequestDTO dto);
|
||||
@Autowired
|
||||
private ApiRequestRepository apiRequestRepository;
|
||||
|
||||
public abstract ApiCollection map(ApiCollectionDTO dto);
|
||||
|
||||
ApiRequestDTO map(ApiRequestInfo entity);
|
||||
public abstract ApiRequestInfo map(ApiRequestDTO dto);
|
||||
|
||||
public abstract ApiRequestDTO map(ApiRequestInfo entity);
|
||||
|
||||
public ApiRequestInfo map(String id) {
|
||||
if(id == null) {
|
||||
return null;
|
||||
}
|
||||
return apiRequestRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("No ApiRequest found for ID: " + id));
|
||||
}
|
||||
|
||||
List<ApiRequestInfo> mapApis(List<ApiRequestDTO> apis);
|
||||
public abstract List<ApiRequestInfo> mapApis(List<ApiRequestDTO> apis);
|
||||
|
||||
List<ApiCollectionDTO> mapCollections(List<ApiCollection> apis);
|
||||
public abstract List<ApiCollectionDTO> mapCollections(List<ApiCollection> apis);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.eactive.testmaster.client.mapper;
|
||||
|
||||
import com.eactive.testmaster.client.entity.ApiScenario;
|
||||
import com.eactive.testmaster.client.repository.ApiScenarioRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ApiScenarioHelper {
|
||||
|
||||
private final ApiScenarioRepository apiScenarioRepository;
|
||||
|
||||
public ApiScenarioHelper(ApiScenarioRepository apiScenarioRepository) {
|
||||
this.apiScenarioRepository = apiScenarioRepository;
|
||||
}
|
||||
|
||||
public ApiScenario getApiScenarioById(String id) {
|
||||
// Assuming the repository has a method findById that returns an Optional<ApiScenario>
|
||||
return apiScenarioRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("No ApiScenario found for ID: " + id));
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,39 @@ package com.eactive.testmaster.client.mapper;
|
||||
|
||||
import com.eactive.testmaster.client.dto.ApiScenarioDTO;
|
||||
import com.eactive.testmaster.client.entity.ApiScenario;
|
||||
import com.eactive.testmaster.client.repository.ApiScenarioRepository;
|
||||
import org.mapstruct.Context;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper(componentModel = "spring",
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
@Component
|
||||
public interface ApiScenarioMapper {
|
||||
public abstract class ApiScenarioMapper {
|
||||
|
||||
ApiScenario map(ApiScenarioDTO dto);
|
||||
@Autowired
|
||||
private ApiScenarioRepository apiScenarioRepository;
|
||||
|
||||
ApiScenarioDTO map(ApiScenario scenario);
|
||||
public abstract ApiScenario map(ApiScenarioDTO dto);
|
||||
|
||||
List<ApiScenarioDTO> toDtoList(List<ApiScenario> scenarios);
|
||||
public abstract ApiScenarioDTO map(ApiScenario scenario);
|
||||
|
||||
public ApiScenario map(String id) {
|
||||
if(id == null) {
|
||||
return null;
|
||||
}
|
||||
return apiScenarioRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("No ApiScenario found for ID: " + id));
|
||||
}
|
||||
|
||||
public String mapToId(ApiScenario scenario) {
|
||||
if(scenario == null) {
|
||||
return null;
|
||||
}
|
||||
return scenario.getId();
|
||||
}
|
||||
|
||||
public abstract List<ApiScenarioDTO> toDtoList(List<ApiScenario> scenarios);
|
||||
}
|
||||
|
||||
@@ -52,9 +52,7 @@ public class HttpHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
|
||||
public List<ApiRequestKeyValueDTO> convertHttpHeadersToList(HttpHeaders headers) {
|
||||
List<ApiRequestKeyValueDTO> headerList = new ArrayList<>();
|
||||
for (Map.Entry<String, String> entry : headers.entries()) {
|
||||
ApiRequestKeyValueDTO header = new ApiRequestKeyValueDTO();
|
||||
header.setKey(entry.getKey());
|
||||
header.setValue(entry.getValue());
|
||||
ApiRequestKeyValueDTO header = new ApiRequestKeyValueDTO(true, entry.getKey(), entry.getValue());
|
||||
headerList.add(header);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import io.netty.handler.ssl.SslContextBuilder;
|
||||
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
||||
import io.netty.util.CharsetUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -29,12 +31,12 @@ import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Service
|
||||
public class NettyApiClient {
|
||||
private static final Logger logger = LoggerFactory.getLogger(NettyApiClient.class);
|
||||
|
||||
@Autowired
|
||||
ServerRepository serverRepository;
|
||||
|
||||
public CompletableFuture<ApiResponse> handleRequest(ApiRequestDTO originalRequest) {
|
||||
ApiRequestDTO apiRequest = originalRequest;
|
||||
public CompletableFuture<ApiResponse> handleRequest(ApiRequestDTO apiRequest) {
|
||||
Server server = serverRepository.findById(apiRequest.getServer()).orElseThrow(() -> new ServerNotFoundException("Server not found"));
|
||||
String host = server.getHostname();
|
||||
int port = server.getPort();
|
||||
@@ -92,6 +94,8 @@ public class NettyApiClient {
|
||||
request.headers().add(customHeaders);
|
||||
}
|
||||
|
||||
apiRequest.setSentBytes(request.toString().length());
|
||||
|
||||
ch.writeAndFlush(request);
|
||||
ch.closeFuture().sync();
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Mapper(componentModel = "spring",
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
public class CommonMapper {
|
||||
|
||||
public String map(boolean value) {
|
||||
@@ -22,17 +22,23 @@ public class CommonMapper {
|
||||
}
|
||||
|
||||
public boolean map(String value) {
|
||||
if (value == null) return false;
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
return value.equals("Y");
|
||||
}
|
||||
|
||||
public String map(List<String> values) {
|
||||
if (values == null) return "";
|
||||
if (values == null) {
|
||||
return "";
|
||||
}
|
||||
return String.join(",", values);
|
||||
}
|
||||
|
||||
public List<String> toList(String value) {
|
||||
if (StringUtils.hasLength(value)) return new ArrayList<>();
|
||||
if (StringUtils.hasLength(value)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return Arrays.asList(value.split(","));
|
||||
}
|
||||
@@ -44,7 +50,9 @@ public class CommonMapper {
|
||||
}
|
||||
|
||||
public String mapKeyValue(List<ApiRequestKeyValueDTO> list) {
|
||||
if (list == null) return "";
|
||||
if (list == null) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (ApiRequestKeyValueDTO dto : list) {
|
||||
result.append(dto.toString()).append("||");
|
||||
@@ -58,22 +66,19 @@ public class CommonMapper {
|
||||
public List<ApiRequestKeyValueDTO> keyValueToList(String keyValue) {
|
||||
List<ApiRequestKeyValueDTO> list = new ArrayList<>();
|
||||
|
||||
if (!StringUtils.hasLength(keyValue)) return list;
|
||||
if (!StringUtils.hasLength(keyValue)) {
|
||||
return list;
|
||||
}
|
||||
|
||||
String[] pairs = keyValue.split("\\|\\|");
|
||||
for (String pair : pairs) {
|
||||
ApiRequestKeyValueDTO dto = new ApiRequestKeyValueDTO();
|
||||
if (pair.startsWith("//")) {
|
||||
dto.setEnabled(false);
|
||||
boolean isEnabled = pair.startsWith("//");
|
||||
if (isEnabled) {
|
||||
pair = pair.substring(2);
|
||||
} else {
|
||||
dto.setEnabled(true);
|
||||
}
|
||||
String[] keyValueArray = pair.split("::");
|
||||
if (keyValueArray.length == 2) {
|
||||
dto.setKey(keyValueArray[0]);
|
||||
dto.setValue(keyValueArray[1]);
|
||||
list.add(dto);
|
||||
list.add(new ApiRequestKeyValueDTO(isEnabled, keyValueArray[0], keyValueArray[1]));
|
||||
}
|
||||
}
|
||||
return list;
|
||||
|
||||
@@ -9,20 +9,21 @@ import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class ServletConfig {
|
||||
@Value("${server.port.http}")
|
||||
private int serverPortHttp;
|
||||
|
||||
@Bean
|
||||
public ServletWebServerFactory serverFactory() {
|
||||
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
|
||||
tomcat.addAdditionalTomcatConnectors(createStandardConnector());
|
||||
return tomcat;
|
||||
}
|
||||
@Value("${server.port.http}")
|
||||
private int serverPortHttp;
|
||||
|
||||
private Connector createStandardConnector() {
|
||||
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
|
||||
connector.setPort(serverPortHttp);
|
||||
return connector;
|
||||
}
|
||||
@Bean
|
||||
public ServletWebServerFactory serverFactory() {
|
||||
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
|
||||
tomcat.addAdditionalTomcatConnectors(createStandardConnector());
|
||||
return tomcat;
|
||||
}
|
||||
|
||||
private Connector createStandardConnector() {
|
||||
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
|
||||
connector.setPort(serverPortHttp);
|
||||
return connector;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.eactive.testmaster.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.ChannelRegistration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/load-test-result").withSockJS();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker("/topic");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
}
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
registration.interceptors(new ChannelInterceptor() {
|
||||
@Override
|
||||
public void postSend(org.springframework.messaging.Message<?> message, org.springframework.messaging.MessageChannel channel, boolean sent) {
|
||||
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
|
||||
switch (accessor.getCommand()) {
|
||||
case CONNECT:
|
||||
// Log or handle session connect
|
||||
System.out.println("STOMP Connect [sessionId: " + accessor.getSessionId() + "]");
|
||||
break;
|
||||
case DISCONNECT:
|
||||
// Log or handle session disconnect
|
||||
System.out.println("STOMP Disconnect [sessionId: " + accessor.getSessionId() + "]");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.eactive.testmaster.loadtest.controller;
|
||||
|
||||
import com.eactive.testmaster.loadtest.dto.LoadTestDTO;
|
||||
import com.eactive.testmaster.loadtest.service.LoadTestService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@Scope("session")
|
||||
public class LoadTestController {
|
||||
|
||||
@Autowired
|
||||
private LoadTestService loadTestService;
|
||||
|
||||
@PostMapping("/startLoadTest")
|
||||
public ResponseEntity<String> startLoadTest(@RequestBody LoadTestDTO loadTestDTO) {
|
||||
loadTestService.startLoadTest(loadTestDTO);
|
||||
return ResponseEntity.ok("Load Test Started");
|
||||
}
|
||||
|
||||
//stop
|
||||
@PostMapping("/stopLoadTest")
|
||||
public ResponseEntity<String> stopLoadTest(@RequestBody LoadTestDTO loadTestDTO) {
|
||||
loadTestService.stopLoadTest(loadTestDTO.getId());
|
||||
return ResponseEntity.ok("Load Test Stopped");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.eactive.testmaster.loadtest.controller;
|
||||
|
||||
import com.eactive.testmaster.common.util.SecurityUtil;
|
||||
import com.eactive.testmaster.loadtest.dto.LoadTestDTO;
|
||||
import com.eactive.testmaster.loadtest.entity.LoadTest;
|
||||
import com.eactive.testmaster.loadtest.mapper.LoadTestMapper;
|
||||
import com.eactive.testmaster.loadtest.service.LoadTestMgmtService;
|
||||
import com.eactive.testmaster.user.entity.StaffUser;
|
||||
import com.eactive.testmaster.user.service.UserService;
|
||||
import java.util.List;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/mgmt/load_test")
|
||||
public class LoadTestMgmtController {
|
||||
|
||||
private final LoadTestMgmtService loadTestMgmtService;
|
||||
|
||||
private final LoadTestMapper loadTestMapper;
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
public LoadTestMgmtController(LoadTestMgmtService loadTestMgmtService, LoadTestMapper loadTestMapper, UserService userService) {
|
||||
this.loadTestMgmtService = loadTestMgmtService;
|
||||
this.loadTestMapper = loadTestMapper;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/list.do")
|
||||
public ResponseEntity<List<LoadTestDTO>> list() {
|
||||
List<LoadTest> list = loadTestMgmtService.findAll();
|
||||
return ResponseEntity.ok(loadTestMapper.toDtoList(list));
|
||||
}
|
||||
|
||||
@PostMapping("/create.do")
|
||||
public ResponseEntity<LoadTestDTO> createScenario(@RequestBody LoadTestDTO loadTestDto) {
|
||||
LoadTest loadTest = loadTestMapper.map(loadTestDto);
|
||||
StaffUser user = userService.findByEsntlId(SecurityUtil.getCurrentUserEsntlId());
|
||||
loadTest.setOwner(user);
|
||||
LoadTest createdScenario = loadTestMgmtService.createLoadTest(loadTest);
|
||||
return new ResponseEntity<>(loadTestMapper.map(createdScenario), HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@PostMapping("/update.do")
|
||||
public ResponseEntity<LoadTestDTO> updateScenario(@RequestBody LoadTestDTO loadTestDTO) {
|
||||
LoadTest updatedLoadTest = loadTestMapper.map(loadTestDTO);
|
||||
LoadTest updatedScenario = loadTestMgmtService.updateLoadTest(loadTestDTO.getId(), updatedLoadTest);
|
||||
return ResponseEntity.ok(loadTestMapper.map(updatedScenario));
|
||||
}
|
||||
|
||||
@PostMapping("/delete.do")
|
||||
public ResponseEntity<Void> deleteScenario(@RequestBody LoadTestDTO loadTestDto) {
|
||||
loadTestMgmtService.deleteLoadTest(loadTestDto.getId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.eactive.testmaster.loadtest.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
|
||||
@Controller
|
||||
public class LoadTestPageController {
|
||||
|
||||
@GetMapping("/mgmt/load_tester.do")
|
||||
public String testView(ModelMap model) {
|
||||
return "page/tester/loadTester";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.eactive.testmaster.loadtest.dto;
|
||||
|
||||
public class ApiRequestEvent {
|
||||
private String testId;
|
||||
private LoadTestRequestAndResponse requestAndResponse;
|
||||
|
||||
public String getTestId() {
|
||||
return testId;
|
||||
}
|
||||
|
||||
public void setTestId(String testId) {
|
||||
this.testId = testId;
|
||||
}
|
||||
|
||||
public LoadTestRequestAndResponse getRequestAndResponse() {
|
||||
return requestAndResponse;
|
||||
}
|
||||
|
||||
public void setRequestAndResponse(LoadTestRequestAndResponse requestAndResponse) {
|
||||
this.requestAndResponse = requestAndResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.eactive.testmaster.loadtest.dto;
|
||||
|
||||
|
||||
import com.eactive.testmaster.client.entity.ApiScenario;
|
||||
import com.eactive.testmaster.user.entity.StaffUser;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.ManyToOne;
|
||||
|
||||
public class LoadTestDTO {
|
||||
|
||||
private String id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
private String scenarioId;
|
||||
|
||||
private int threadCount; //min 1
|
||||
|
||||
private int loopCount; //0 means infinite
|
||||
|
||||
private int rampUpTime; //in milliseconds
|
||||
|
||||
private int duration; //in minutes
|
||||
|
||||
private String status; //Running, Completed, Stopped, Waiting
|
||||
|
||||
private String testData; //csv format, first row is header
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getScenarioId() {
|
||||
return scenarioId;
|
||||
}
|
||||
|
||||
public void setScenarioId(String scenarioId) {
|
||||
this.scenarioId = scenarioId;
|
||||
}
|
||||
|
||||
public int getThreadCount() {
|
||||
return threadCount;
|
||||
}
|
||||
|
||||
public void setThreadCount(int threadCount) {
|
||||
this.threadCount = threadCount;
|
||||
}
|
||||
|
||||
public int getLoopCount() {
|
||||
return loopCount;
|
||||
}
|
||||
|
||||
public void setLoopCount(int loopCount) {
|
||||
this.loopCount = loopCount;
|
||||
}
|
||||
|
||||
public int getRampUpTime() {
|
||||
return rampUpTime;
|
||||
}
|
||||
|
||||
public void setRampUpTime(int rampUpTime) {
|
||||
this.rampUpTime = rampUpTime;
|
||||
}
|
||||
|
||||
public int getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(int duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getTestData() {
|
||||
return testData;
|
||||
}
|
||||
|
||||
public void setTestData(String testData) {
|
||||
this.testData = testData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.eactive.testmaster.loadtest.dto;
|
||||
|
||||
import com.eactive.testmaster.client.dto.ApiRequestDTO;
|
||||
import com.eactive.testmaster.client.dto.ApiResponse;
|
||||
|
||||
public class LoadTestRequestAndResponse {
|
||||
|
||||
private ApiRequestDTO request;
|
||||
|
||||
private ApiResponse response;
|
||||
|
||||
private long responseTime;
|
||||
|
||||
|
||||
public LoadTestRequestAndResponse(ApiRequestDTO request, ApiResponse response, long responseTime) {
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
this.responseTime = responseTime;
|
||||
}
|
||||
|
||||
public ApiRequestDTO getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
public void setRequest(ApiRequestDTO request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public ApiResponse getResponse() {
|
||||
return response;
|
||||
}
|
||||
|
||||
public void setResponse(ApiResponse response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public long getResponseTime() {
|
||||
return responseTime;
|
||||
}
|
||||
|
||||
public void setResponseTime(long responseTime) {
|
||||
this.responseTime = responseTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.eactive.testmaster.loadtest.entity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class APILoadTestResult {
|
||||
|
||||
private String testName;
|
||||
private long timestamp;
|
||||
private double averageResponseTime;
|
||||
private double maxResponseTime;
|
||||
private double minResponseTime;
|
||||
private int totalRequests;
|
||||
private double throughput; // requests per second
|
||||
private double errorRate; // percentage of requests that resulted in errors
|
||||
private int concurrentUsers;
|
||||
private double cpuUsagePercentage;
|
||||
private double memoryUsagePercentage;
|
||||
private Map<Integer, Integer> statusCodeDistribution; // HTTP status code -> count
|
||||
private Map<String, Double> percentileResponseTimes; // e.g., "P95" -> 95th percentile response time
|
||||
private double networkIO; // Network I/O usage
|
||||
private double diskIO; // Disk I/O usage
|
||||
private List<String> errors; // List of error messages or types encountered during the test
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.eactive.testmaster.loadtest.entity;
|
||||
|
||||
import com.eactive.testmaster.client.entity.ApiScenario;
|
||||
import com.eactive.testmaster.user.entity.StaffUser;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "LOAD_TEST")
|
||||
public class LoadTest {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "USER_ID")
|
||||
private StaffUser owner;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "API_SCENARIO_ID")
|
||||
private ApiScenario scenario;
|
||||
|
||||
private int threadCount; //min 1
|
||||
|
||||
private int loopCount; //0 means infinite
|
||||
|
||||
private int rampUpTime; //in milliseconds
|
||||
|
||||
private int duration; //in minutes
|
||||
|
||||
private String status; //Running, Completed, Stopped, Waiting
|
||||
|
||||
private String testData; //csv format, first row is header
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public StaffUser getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public void setOwner(StaffUser owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public ApiScenario getScenario() {
|
||||
return scenario;
|
||||
}
|
||||
|
||||
public void setScenario(ApiScenario scenario) {
|
||||
this.scenario = scenario;
|
||||
}
|
||||
|
||||
public int getThreadCount() {
|
||||
return threadCount;
|
||||
}
|
||||
|
||||
public void setThreadCount(int threadCount) {
|
||||
this.threadCount = threadCount;
|
||||
}
|
||||
|
||||
public int getLoopCount() {
|
||||
return loopCount;
|
||||
}
|
||||
|
||||
public void setLoopCount(int loopCount) {
|
||||
this.loopCount = loopCount;
|
||||
}
|
||||
|
||||
public int getRampUpTime() {
|
||||
return rampUpTime;
|
||||
}
|
||||
|
||||
public void setRampUpTime(int rampUpTime) {
|
||||
this.rampUpTime = rampUpTime;
|
||||
}
|
||||
|
||||
public int getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(int duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getTestData() {
|
||||
return testData;
|
||||
}
|
||||
|
||||
public void setTestData(String testData) {
|
||||
this.testData = testData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.eactive.testmaster.loadtest.entity;
|
||||
|
||||
import com.eactive.testmaster.client.entity.ApiScenario;
|
||||
import java.time.LocalDateTime;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "LOAD_TEST_RESULT")
|
||||
public class LoadTestResult {
|
||||
|
||||
@Id
|
||||
private int id;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "USER_ID")
|
||||
private ApiScenario scenario;
|
||||
|
||||
private LocalDateTime startDate;
|
||||
private LocalDateTime endDate;
|
||||
private String status; //Running, Completed, Stopped
|
||||
private String result; // api result in csv format.
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public ApiScenario getScenario() {
|
||||
return scenario;
|
||||
}
|
||||
|
||||
public void setScenario(ApiScenario scenario) {
|
||||
this.scenario = scenario;
|
||||
}
|
||||
|
||||
public LocalDateTime getStartDate() {
|
||||
return startDate;
|
||||
}
|
||||
|
||||
public void setStartDate(LocalDateTime startDate) {
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public LocalDateTime getEndDate() {
|
||||
return endDate;
|
||||
}
|
||||
|
||||
public void setEndDate(LocalDateTime endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setResult(String result) {
|
||||
this.result = result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.eactive.testmaster.loadtest.mapper;
|
||||
|
||||
import com.eactive.testmaster.client.mapper.ApiScenarioMapper;
|
||||
import com.eactive.testmaster.common.mapper.CommonMapper;
|
||||
import com.eactive.testmaster.loadtest.dto.LoadTestDTO;
|
||||
import com.eactive.testmaster.loadtest.entity.LoadTest;
|
||||
import com.eactive.testmaster.server.mapper.ServerMapper;
|
||||
import java.util.List;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Mapper(componentModel = "spring",
|
||||
uses = {CommonMapper.class, ServerMapper.class, ApiScenarioMapper.class},
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
@Component
|
||||
public interface LoadTestMapper {
|
||||
|
||||
@Mapping(target = "scenario", source = "scenarioId")
|
||||
LoadTest map(LoadTestDTO dto);
|
||||
|
||||
@Mapping(target = "scenarioId", source = "scenario")
|
||||
LoadTestDTO map(LoadTest entity);
|
||||
|
||||
List<LoadTestDTO> toDtoList(List<LoadTest> list);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.eactive.testmaster.loadtest.repository;
|
||||
|
||||
import com.eactive.testmaster.loadtest.entity.LoadTest;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
public interface LoadTestRepository extends JpaRepository<LoadTest, String>, JpaSpecificationExecutor<LoadTest> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.eactive.testmaster.loadtest.repository;
|
||||
|
||||
public class LoadTestResultRepository {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.eactive.testmaster.loadtest.service;
|
||||
|
||||
public class ApiPerformanceAggregation {
|
||||
|
||||
private String apiId;
|
||||
|
||||
private String apiName;
|
||||
|
||||
private long numberOfSamples;
|
||||
|
||||
private double average;
|
||||
|
||||
private double median;
|
||||
|
||||
private double line90;
|
||||
|
||||
private double line95;
|
||||
|
||||
private double line99;
|
||||
|
||||
private double min;
|
||||
|
||||
private double max;
|
||||
|
||||
private double errorRate;
|
||||
|
||||
private double throughput;
|
||||
|
||||
private double receivedKbPerSec;
|
||||
|
||||
private double sentKbPerSec;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.eactive.testmaster.loadtest.service;
|
||||
|
||||
public class ApiPerformanceSummary {
|
||||
|
||||
private String apiId;
|
||||
|
||||
private String apiName;
|
||||
|
||||
private long numberOfSamples;
|
||||
|
||||
private double average;
|
||||
|
||||
private double min;
|
||||
|
||||
private double max;
|
||||
|
||||
private double stdDev;
|
||||
|
||||
private double errorRate;
|
||||
|
||||
private double throughput;
|
||||
|
||||
private double receivedKbPerSec;
|
||||
|
||||
private double sentKbPerSec;
|
||||
|
||||
private double avgBytes;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.eactive.testmaster.loadtest.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.PreDestroy;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@Scope("session")
|
||||
public class CallableLoadTestService {
|
||||
|
||||
@Autowired
|
||||
private SimpMessagingTemplate template;
|
||||
|
||||
private ScheduledExecutorService scheduledExecutorService;
|
||||
private List<ScheduledFuture<?>> scheduledFutures;
|
||||
|
||||
public CallableLoadTestService() {
|
||||
this.scheduledFutures = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void sendLoadTestUpdate(String topic, Object update) {
|
||||
template.convertAndSend("/topic/" + topic, update);
|
||||
}
|
||||
|
||||
public void startLoadTest(int numberOfUsers, long rampUpTimeMillis, long taskDurationMillis) {
|
||||
if (scheduledExecutorService != null && !scheduledExecutorService.isShutdown()) {
|
||||
scheduledExecutorService.shutdownNow();
|
||||
}
|
||||
scheduledExecutorService = Executors.newScheduledThreadPool(numberOfUsers);
|
||||
|
||||
for (int i = 0; i < numberOfUsers; i++) {
|
||||
final CallableVirtualUser virtualUser = new CallableVirtualUser(this);
|
||||
|
||||
// Calculate delay for each user to simulate ramp-up
|
||||
long delay = i * rampUpTimeMillis;
|
||||
|
||||
// Schedule the task with the calculated delay
|
||||
ScheduledFuture<?> future = scheduledExecutorService.schedule(() -> {
|
||||
try {
|
||||
// Execute the user task and wait for its completion or interruption
|
||||
String result = virtualUser.call();
|
||||
System.out.println("Task completed with result: " + result);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, delay, TimeUnit.MILLISECONDS);
|
||||
|
||||
scheduledFutures.add(future);
|
||||
|
||||
// Schedule a stop action for each task after its duration expires
|
||||
scheduledExecutorService.schedule(() -> {
|
||||
if (!future.isDone()) {
|
||||
future.cancel(true); // Attempt to interrupt the task
|
||||
System.out.println("Task was cancelled after its duration limit.");
|
||||
}
|
||||
}, delay + taskDurationMillis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopLoadTest() {
|
||||
if (scheduledFutures != null) {
|
||||
for (ScheduledFuture<?> future : scheduledFutures) {
|
||||
if (!future.isDone()) {
|
||||
future.cancel(true); // Attempt to interrupt if running
|
||||
}
|
||||
}
|
||||
scheduledFutures.clear();
|
||||
}
|
||||
if (scheduledExecutorService != null) {
|
||||
scheduledExecutorService.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
stopLoadTest(); // Ensure all tasks are stopped and executor service is shut down
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.eactive.testmaster.loadtest.service;
|
||||
|
||||
import com.eactive.testmaster.loadtest.dto.LoadTestDTO;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class CallableVirtualUser implements Callable<String> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VirtualUser.class);
|
||||
|
||||
private final CallableLoadTestService loadTestService;
|
||||
|
||||
private LoadTestDTO loadTest;
|
||||
private Map<String, String> data;
|
||||
|
||||
public CallableVirtualUser(CallableLoadTestService loadTestService) {
|
||||
this.loadTestService = loadTestService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call() throws Exception {
|
||||
try {
|
||||
// Simulate user action
|
||||
Thread.sleep(1000); // Replace with actual user action
|
||||
loadTestService.sendLoadTestUpdate(loadTest.getId(), "Virtual user completed");
|
||||
return "Completed";
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt(); // Preserve interrupt status
|
||||
logger.debug("Task interrupted");
|
||||
return "Interrupted";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.eactive.testmaster.loadtest.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Console {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Console.class);
|
||||
|
||||
public void log(String message) {
|
||||
logger.info(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package com.eactive.testmaster.loadtest.service;
|
||||
|
||||
import com.eactive.testmaster.client.dto.internal.ScenarioNode;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class LoadTestGraphBuilder {
|
||||
|
||||
public static ScenarioNode getTopNode(String data) {
|
||||
try {
|
||||
Map<String, ScenarioNode> graphData = loadData(data);
|
||||
return getHead(extractGraphFromStart(graphData));
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Map<String, ScenarioNode> loadData(String data) throws JsonProcessingException {
|
||||
Map<String, ScenarioNode> graphData;
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
graphData = objectMapper.readValue(data, new TypeReference<>() {
|
||||
});
|
||||
return graphData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static ScenarioNode getHead(Map<String, ScenarioNode> graphData) {
|
||||
for (ScenarioNode node : graphData.values()) {
|
||||
if ("start".equals(node.getClassName())) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Map<String, ScenarioNode> extractGraphFromStart(Map<String, ScenarioNode> graphData) {
|
||||
Map<String, ScenarioNode> graph = new HashMap<>();
|
||||
|
||||
// Helper function to recursively build the graph
|
||||
graphData.forEach((nodeId, node) -> {
|
||||
if ("start".equals(node.getClassName())) {
|
||||
buildGraph(nodeId, graph, graphData);
|
||||
return; // Break the forEach loop assuming there's only one start node
|
||||
}
|
||||
});
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
private static void buildGraph(String nodeId, Map<String, ScenarioNode> graph, Map<String, ScenarioNode> graphData) {
|
||||
ScenarioNode node = graphData.get(nodeId);
|
||||
if (node == null || graph.containsKey(nodeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
graph.put(nodeId, node);
|
||||
|
||||
if (node.getOutputs() != null) {
|
||||
node.getOutputs().values().forEach(outputList -> {
|
||||
outputList.getConnections().forEach(output -> {
|
||||
ScenarioNode v = graphData.get(output.getNode());
|
||||
graph.get(nodeId).addNode(v);
|
||||
buildGraph(output.getNode(), graph, graphData); // Recurse
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
Map<String, ScenarioNode> graphData = loadData(testData);
|
||||
LoadTestGraphBuilder graphBuilder = new LoadTestGraphBuilder();
|
||||
Map<String, ScenarioNode> graph = graphBuilder.extractGraphFromStart(graphData);
|
||||
ScenarioNode start = graphBuilder.getHead(graph);
|
||||
|
||||
System.out.println(start);
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static String testData = "{\n"
|
||||
+ " \"1\": {\n"
|
||||
+ " \"id\": 1,\n"
|
||||
+ " \"name\": \"Start\",\n"
|
||||
+ " \"data\": {},\n"
|
||||
+ " \"class\": \"start\",\n"
|
||||
+ " \"html\": \"\\n <div>\\n <div><i class=\\\"fas fa-play\\\"></i> Start </div>\\n </div>\\n \",\n"
|
||||
+ " \"typenode\": false,\n"
|
||||
+ " \"inputs\": {},\n"
|
||||
+ " \"outputs\": {\n"
|
||||
+ " \"output_1\": {\n"
|
||||
+ " \"connections\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"node\": \"2\",\n"
|
||||
+ " \"output\": \"input_1\"\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " },\n"
|
||||
+ " \"pos_x\": 100,\n"
|
||||
+ " \"pos_y\": 100\n"
|
||||
+ " },\n"
|
||||
+ " \"2\": {\n"
|
||||
+ " \"id\": 2,\n"
|
||||
+ " \"name\": \"잔액조회\",\n"
|
||||
+ " \"data\": {\n"
|
||||
+ " \"collection\": \"b9748a16-bbb7-4dcc-8521-88e76a0b2ee8\",\n"
|
||||
+ " \"id\": \"51351280-a72f-4886-8426-70d2932a2f48\",\n"
|
||||
+ " \"index\": \"2\",\n"
|
||||
+ " \"nodeType\": \"api\",\n"
|
||||
+ " \"name\": \"잔액조회\"\n"
|
||||
+ " },\n"
|
||||
+ " \"class\": \"api\",\n"
|
||||
+ " \"html\": \"<div><div class=\\\"title-box\\\">API</div><div class=\\\"box\\\">잔액조회</div></div>\",\n"
|
||||
+ " \"typenode\": false,\n"
|
||||
+ " \"inputs\": {\n"
|
||||
+ " \"input_1\": {\n"
|
||||
+ " \"connections\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"node\": \"1\",\n"
|
||||
+ " \"input\": \"output_1\"\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " },\n"
|
||||
+ " \"outputs\": {\n"
|
||||
+ " \"output_1\": {\n"
|
||||
+ " \"connections\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"node\": \"3\",\n"
|
||||
+ " \"output\": \"input_1\"\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " },\n"
|
||||
+ " \"pos_x\": 328,\n"
|
||||
+ " \"pos_y\": 184\n"
|
||||
+ " },\n"
|
||||
+ " \"3\": {\n"
|
||||
+ " \"id\": 3,\n"
|
||||
+ " \"name\": \"거래내역조회\",\n"
|
||||
+ " \"data\": {\n"
|
||||
+ " \"collection\": \"b9748a16-bbb7-4dcc-8521-88e76a0b2ee8\",\n"
|
||||
+ " \"id\": \"3f156dc1-f71b-4a26-a778-64a2ddd2b54f\",\n"
|
||||
+ " \"index\": \"3\",\n"
|
||||
+ " \"nodeType\": \"api\",\n"
|
||||
+ " \"name\": \"거래내역조회\"\n"
|
||||
+ " },\n"
|
||||
+ " \"class\": \"api\",\n"
|
||||
+ " \"html\": \"<div><div class=\\\"title-box\\\">API</div><div class=\\\"box\\\">거래내역조회</div></div>\",\n"
|
||||
+ " \"typenode\": false,\n"
|
||||
+ " \"inputs\": {\n"
|
||||
+ " \"input_1\": {\n"
|
||||
+ " \"connections\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"node\": \"2\",\n"
|
||||
+ " \"input\": \"output_1\"\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " },\n"
|
||||
+ " \"outputs\": {\n"
|
||||
+ " \"output_1\": {\n"
|
||||
+ " \"connections\": []\n"
|
||||
+ " }\n"
|
||||
+ " },\n"
|
||||
+ " \"pos_x\": 654,\n"
|
||||
+ " \"pos_y\": 301\n"
|
||||
+ " }\n"
|
||||
+ "}";
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.eactive.testmaster.loadtest.service;
|
||||
|
||||
import com.eactive.testmaster.loadtest.entity.LoadTest;
|
||||
import com.eactive.testmaster.loadtest.repository.LoadTestRepository;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import javax.transaction.Transactional;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class LoadTestMgmtService {
|
||||
|
||||
@Autowired
|
||||
LoadTestRepository loadTestRepository;
|
||||
|
||||
public List<LoadTest> findAll() {
|
||||
return loadTestRepository.findAll();
|
||||
}
|
||||
|
||||
public LoadTest createLoadTest(LoadTest loadTest) {
|
||||
loadTest.setId(UUID.randomUUID().toString());
|
||||
return loadTestRepository.save(loadTest);
|
||||
}
|
||||
|
||||
public LoadTest updateLoadTest(String id, LoadTest updatedLoadTest) {
|
||||
return loadTestRepository.findById(id)
|
||||
.map(loadTest -> {
|
||||
loadTest.setName(updatedLoadTest.getName());
|
||||
loadTest.setDescription(updatedLoadTest.getDescription());
|
||||
loadTest.setScenario(updatedLoadTest.getScenario());
|
||||
loadTest.setThreadCount(updatedLoadTest.getThreadCount());
|
||||
loadTest.setLoopCount(updatedLoadTest.getLoopCount());
|
||||
loadTest.setRampUpTime(updatedLoadTest.getRampUpTime());
|
||||
loadTest.setDuration(updatedLoadTest.getDuration());
|
||||
loadTest.setTestData(updatedLoadTest.getTestData());
|
||||
|
||||
return loadTestRepository.save(loadTest);
|
||||
}).orElseGet(() -> {
|
||||
// If the LoadTest with the given id doesn't exist, create a new one
|
||||
updatedLoadTest.setId(id);
|
||||
return loadTestRepository.save(updatedLoadTest);
|
||||
});
|
||||
}
|
||||
|
||||
public void deleteLoadTest(String id) {
|
||||
loadTestRepository.deleteById(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.eactive.testmaster.loadtest.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import javax.annotation.PreDestroy;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class LoadTestMonitorMap {
|
||||
|
||||
private final Map<String, List<ScheduledFuture<?>>> futures = new ConcurrentHashMap<>();
|
||||
private final Map<String, List<VirtualUser>> virtualUsers = new ConcurrentHashMap<>();
|
||||
|
||||
private final Map<String, ThreadPoolTaskScheduler> schedulers = new ConcurrentHashMap<>();
|
||||
|
||||
public void put(String testId, List<ScheduledFuture<?>> futures, List<VirtualUser> users, ThreadPoolTaskScheduler scheduler) {
|
||||
this.futures.put(testId, futures);
|
||||
virtualUsers.put(testId, users);
|
||||
schedulers.put(testId, scheduler);
|
||||
}
|
||||
|
||||
public void remove(String testId) {
|
||||
futures.remove(testId);
|
||||
virtualUsers.remove(testId);
|
||||
schedulers.remove(testId);
|
||||
}
|
||||
|
||||
public ThreadPoolTaskScheduler getScheduler(String testId) {
|
||||
return schedulers.get(testId);
|
||||
}
|
||||
|
||||
public List<ScheduledFuture<?>> getFutures(String testId) {
|
||||
return futures.get(testId);
|
||||
}
|
||||
|
||||
public List<VirtualUser> getVirtualUsers(String testId) {
|
||||
return virtualUsers.get(testId);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
// Perform cleanup logic here
|
||||
//get all the keys and stop them all
|
||||
futures.keySet().forEach(key -> {
|
||||
List<ScheduledFuture<?>> value = futures.get(key);
|
||||
value.forEach(future -> {
|
||||
future.cancel(true);
|
||||
});
|
||||
});
|
||||
|
||||
virtualUsers.keySet().forEach(key -> {
|
||||
List<VirtualUser> value = virtualUsers.get(key);
|
||||
value.forEach(VirtualUser::stop);
|
||||
});
|
||||
|
||||
schedulers.keySet().forEach(key -> {
|
||||
ThreadPoolTaskScheduler value = schedulers.get(key);
|
||||
value.shutdown();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package com.eactive.testmaster.loadtest.service;
|
||||
|
||||
import com.eactive.testmaster.client.dto.ApiRequestDTO;
|
||||
import com.eactive.testmaster.client.dto.internal.ScenarioNode;
|
||||
import com.eactive.testmaster.client.entity.ApiRequestInfo;
|
||||
import com.eactive.testmaster.client.entity.ApiScenario;
|
||||
import com.eactive.testmaster.client.mapper.ApiRequestMapper;
|
||||
import com.eactive.testmaster.client.mapper.ApiScenarioMapper;
|
||||
import com.eactive.testmaster.client.service.NettyApiClient;
|
||||
import com.eactive.testmaster.loadtest.dto.ApiRequestEvent;
|
||||
import com.eactive.testmaster.loadtest.dto.LoadTestDTO;
|
||||
import com.eactive.testmaster.loadtest.dto.LoadTestRequestAndResponse;
|
||||
import com.lmax.disruptor.RingBuffer;
|
||||
import com.lmax.disruptor.dsl.Disruptor;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
@Service
|
||||
//@Scope("session")
|
||||
public class LoadTestService extends TextWebSocketHandler {
|
||||
|
||||
//logger
|
||||
private static final Logger logger = LoggerFactory.getLogger(LoadTestService.class);
|
||||
private static long MINUTE = 10 * 1000L;
|
||||
|
||||
private SimpMessagingTemplate template;
|
||||
|
||||
private LoadTestMonitorMap loadTestMonitorMap;
|
||||
|
||||
private NettyApiClient nettyApiClient;
|
||||
|
||||
private ApiScenarioMapper apiScenarioMapper;
|
||||
|
||||
private ApiRequestMapper apiRequestMapper;
|
||||
|
||||
private final RingBuffer<ApiRequestEvent> ringBuffer;
|
||||
|
||||
public LoadTestService(SimpMessagingTemplate template, LoadTestMonitorMap loadTestMonitorMap, NettyApiClient nettyApiClient, ApiScenarioMapper apiScenarioMapper, ApiRequestMapper apiRequestMapper) {
|
||||
this.template = template;
|
||||
this.loadTestMonitorMap = loadTestMonitorMap;
|
||||
this.nettyApiClient = nettyApiClient;
|
||||
this.apiScenarioMapper = apiScenarioMapper;
|
||||
this.apiRequestMapper = apiRequestMapper;
|
||||
|
||||
Disruptor<ApiRequestEvent> disruptor = new Disruptor<>(ApiRequestEvent::new, 1024, Executors.defaultThreadFactory());
|
||||
disruptor.handleEventsWith(this::processApiRequestEvent);
|
||||
ringBuffer = disruptor.start();
|
||||
}
|
||||
|
||||
private void processApiRequestEvent(ApiRequestEvent event, long sequence, boolean endOfBatch) {
|
||||
LoadTestRequestAndResponse requestAndResponse = event.getRequestAndResponse();
|
||||
String testId = event.getTestId();
|
||||
List<Long> responseTimes = new ArrayList<>();
|
||||
insertSorted(responseTimes, requestAndResponse.getResponseTime());
|
||||
|
||||
template.convertAndSend("/topic/" + testId, requestAndResponse);
|
||||
}
|
||||
|
||||
public static void insertSorted(List<Long> list, long value) {
|
||||
if (list.isEmpty() || value > list.get(list.size() - 1)) {
|
||||
list.add(value);
|
||||
return;
|
||||
}
|
||||
|
||||
int insertionPoint = findInsertionPoint(list, value);
|
||||
list.add(insertionPoint, value);
|
||||
}
|
||||
|
||||
private static int findInsertionPoint(List<Long> list, long value) {
|
||||
int low = 0;
|
||||
int high = list.size() - 1;
|
||||
|
||||
while (low <= high) {
|
||||
int mid = low + (high - low) / 2;
|
||||
if (value == list.get(mid)) {
|
||||
return mid; // Optional: Handle duplicate values as needed
|
||||
} else if (value > list.get(mid)) {
|
||||
low = mid + 1;
|
||||
} else {
|
||||
high = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return low;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
String clientMessage = message.getPayload();
|
||||
System.out.println("Received message: " + clientMessage);
|
||||
session.sendMessage(new TextMessage("Hello from server!"));
|
||||
}
|
||||
|
||||
|
||||
public void sendLoadTestUpdate(String topic, LoadTestRequestAndResponse requestAndResponse) {
|
||||
long sequence = ringBuffer.next();
|
||||
ApiRequestEvent event = ringBuffer.get(sequence);
|
||||
event.setTestId(topic);
|
||||
event.setRequestAndResponse(requestAndResponse);
|
||||
ringBuffer.publish(sequence);
|
||||
}
|
||||
|
||||
public void sendLoadTestError(String topic, String errorMessage) {
|
||||
template.convertAndSend("/topic/" + topic, errorMessage);
|
||||
|
||||
}
|
||||
|
||||
public void startLoadTest(LoadTestDTO loadTestDTO) {
|
||||
int numberOfUsers = loadTestDTO.getThreadCount();
|
||||
int maxCount = loadTestDTO.getLoopCount();
|
||||
long rampUpTimeMillis = loadTestDTO.getRampUpTime();
|
||||
long taskDurationMillis = loadTestDTO.getDuration() * MINUTE;
|
||||
CountDownLatch latch = new CountDownLatch(numberOfUsers);
|
||||
|
||||
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
|
||||
taskScheduler.setPoolSize(numberOfUsers + 1); // Add extra space for cancellation tasks
|
||||
taskScheduler.initialize();
|
||||
ApiScenario apiScenario = apiScenarioMapper.map(loadTestDTO.getScenarioId());
|
||||
ScenarioNode scenario = LoadTestGraphBuilder.getTopNode(apiScenario.getScenario());
|
||||
traverseDFS(scenario);
|
||||
|
||||
List<ScheduledFuture<?>> scheduledFutures = new ArrayList<>();
|
||||
List<VirtualUser> virtualUsers = new ArrayList<>();
|
||||
for (int i = 0; i < numberOfUsers; i++) {
|
||||
final VirtualUser virtualUser = new VirtualUser(loadTestDTO.getId(), maxCount, scenario, this, nettyApiClient, latch);
|
||||
virtualUsers.add(virtualUser);
|
||||
|
||||
Instant startTime = Instant.now().plusMillis(i * rampUpTimeMillis);
|
||||
ScheduledFuture<?> userTask = taskScheduler.schedule(virtualUser, Date.from(startTime));
|
||||
scheduledFutures.add(userTask);
|
||||
}
|
||||
|
||||
if (taskDurationMillis > 0) {
|
||||
taskScheduler.schedule(() -> {
|
||||
stopLoadTest(loadTestDTO.getId());
|
||||
}, new Date(Instant.now().toEpochMilli() + taskDurationMillis)); // Schedule cancellation task at startTime + taskDurationMillis
|
||||
}
|
||||
|
||||
loadTestMonitorMap.put(loadTestDTO.getId(), scheduledFutures, virtualUsers, taskScheduler);
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
logger.debug("All virtual users have completed");
|
||||
stopLoadTest(loadTestDTO.getId());
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public void traverseDFS(ScenarioNode node) {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.getClassName() != null && node.getClassName().equalsIgnoreCase("api")) {
|
||||
ApiRequestInfo apiRequestInfo = apiRequestMapper.map(node.getData().get("id"));
|
||||
ApiRequestDTO apiRequestDTO = apiRequestMapper.map(apiRequestInfo);
|
||||
node.setApiRequest(apiRequestDTO);
|
||||
}
|
||||
|
||||
// Visited set to avoid cycles
|
||||
Set<String> visited = new HashSet<>();
|
||||
visited.add(node.getId());
|
||||
|
||||
dfsHelper(node, visited);
|
||||
}
|
||||
|
||||
private void dfsHelper(ScenarioNode node, Set<String> visited) {
|
||||
if (node.getConnections() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ScenarioNode adjacentNode : node.getConnections()) {
|
||||
if (!visited.contains(adjacentNode.getId())) {
|
||||
// Process the adjacent node
|
||||
System.out.println(adjacentNode.getName());
|
||||
if (adjacentNode.getClassName() != null && adjacentNode.getClassName().equalsIgnoreCase("api")) {
|
||||
ApiRequestInfo apiRequestInfo = apiRequestMapper.map(adjacentNode.getData().get("id"));
|
||||
ApiRequestDTO apiRequestDTO = apiRequestMapper.map(apiRequestInfo);
|
||||
adjacentNode.setApiRequest(apiRequestDTO);
|
||||
}
|
||||
visited.add(adjacentNode.getId());
|
||||
|
||||
// Recurse into adjacent node
|
||||
dfsHelper(adjacentNode, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void stopLoadTest(String id) {
|
||||
logger.debug("Stopping load test with id: " + id);
|
||||
List<VirtualUser> virtualUsers = loadTestMonitorMap.getVirtualUsers(id);
|
||||
if (virtualUsers != null) {
|
||||
virtualUsers.forEach(VirtualUser::stop);
|
||||
}
|
||||
|
||||
List<ScheduledFuture<?>> futures = loadTestMonitorMap.getFutures(id);
|
||||
if (futures != null) {
|
||||
futures.forEach(future -> {
|
||||
future.cancel(true);
|
||||
});
|
||||
}
|
||||
|
||||
ThreadPoolTaskScheduler scheduler = loadTestMonitorMap.getScheduler(id);
|
||||
if (scheduler != null) {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
|
||||
loadTestMonitorMap.remove(id);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.eactive.testmaster.loadtest.service;
|
||||
|
||||
import com.eactive.testmaster.client.dto.ApiRequestDTO;
|
||||
import com.eactive.testmaster.client.dto.ApiRequestKeyValueDTO;
|
||||
import com.eactive.testmaster.client.dto.ApiResponse;
|
||||
import com.eactive.testmaster.client.dto.internal.ScenarioNode;
|
||||
import com.eactive.testmaster.client.service.NettyApiClient;
|
||||
import com.eactive.testmaster.loadtest.dto.LoadTestRequestAndResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineManager;
|
||||
import javax.script.ScriptException;
|
||||
import org.apache.commons.lang3.SerializationUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class VirtualUser implements Runnable {
|
||||
|
||||
//logger
|
||||
private static final Logger logger = LoggerFactory.getLogger(VirtualUser.class);
|
||||
private final Object pauseLock = new Object();
|
||||
private boolean paused = false;
|
||||
private volatile boolean running = true;
|
||||
|
||||
private int maxCount;
|
||||
|
||||
private Thread runningThread;
|
||||
|
||||
private final LoadTestService loadTestService;
|
||||
private final NettyApiClient nettyApiClient;
|
||||
|
||||
private static Map<String, String> globals = new ConcurrentHashMap<>();
|
||||
private Map<String, String> variables = new ConcurrentHashMap<>();
|
||||
|
||||
private String testId;
|
||||
|
||||
private ScenarioNode scenario;
|
||||
|
||||
private Set<ScenarioNode> visitedNodes = new HashSet<>();
|
||||
|
||||
private CountDownLatch latch;
|
||||
|
||||
public VirtualUser(String testId, int maxCount, ScenarioNode scenarioDTO, LoadTestService loadTestService, NettyApiClient nettyApiClient, CountDownLatch latch) {
|
||||
this.testId = testId;
|
||||
this.scenario = scenarioDTO;
|
||||
this.loadTestService = loadTestService;
|
||||
this.nettyApiClient = nettyApiClient;
|
||||
this.maxCount = maxCount;
|
||||
this.latch = latch;
|
||||
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
synchronized (pauseLock) {
|
||||
paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
synchronized (pauseLock) {
|
||||
paused = false;
|
||||
pauseLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running = false;
|
||||
latch.countDown();
|
||||
resume(); // To ensure the user exits if it's paused
|
||||
if (runningThread != null) {
|
||||
logger.debug("Virtual User Interrupting thread");
|
||||
runningThread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
|
||||
logger.debug("Virtual user started");
|
||||
runningThread = Thread.currentThread();
|
||||
int count = 0;
|
||||
while (maxCount == 0 || running && count < maxCount) {
|
||||
synchronized (pauseLock) {
|
||||
if (paused) {
|
||||
try {
|
||||
pauseLock.wait();
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt(); // Preserve interrupt status
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!running) {
|
||||
break;
|
||||
}
|
||||
//clear visited nodes
|
||||
visitedNodes.clear();
|
||||
performAction();
|
||||
logger.debug("Count: " + (count + 1) + " of " + maxCount);
|
||||
count++;
|
||||
}
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
private void performAction() {
|
||||
ScenarioNode currentNode = scenario;
|
||||
|
||||
while (running) {
|
||||
if (Thread.interrupted()) {
|
||||
break;
|
||||
}
|
||||
if (currentNode != null) {
|
||||
if (visitedNodes.contains(currentNode)) {
|
||||
logger.debug("Loop detected, stopping");
|
||||
break;
|
||||
}
|
||||
visitedNodes.add(currentNode);
|
||||
|
||||
if (currentNode.getClassName().equalsIgnoreCase("api")) {
|
||||
processApi(currentNode);
|
||||
}
|
||||
|
||||
if (currentNode.getConnections() != null && !currentNode.getConnections().isEmpty()) {
|
||||
currentNode = currentNode.getConnections().get(0); //현재는 1개의 connection만 지원
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug("Action performed or interrupted");
|
||||
}
|
||||
|
||||
private void processApi(ScenarioNode currentNode) {
|
||||
try {
|
||||
if (currentNode.getApiRequest() != null) {
|
||||
ApiRequestDTO cloned = SerializationUtils.clone(currentNode.getApiRequest());
|
||||
|
||||
executePreRequestScript(cloned);
|
||||
ApiRequestDTO replaced = replacePlaceHoldersWithVariables(cloned);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
ApiResponse response = nettyApiClient.handleRequest(replaced).get();
|
||||
long endTime = System.currentTimeMillis();
|
||||
long responseTime = endTime - startTime;
|
||||
|
||||
executePostRequestScript(replaced, response);
|
||||
LoadTestRequestAndResponse requestAndResponse = new LoadTestRequestAndResponse(replaced, response, responseTime);
|
||||
loadTestService.sendLoadTestUpdate(testId, requestAndResponse);
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
loadTestService.sendLoadTestError(testId, "An error occurred: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
engine.eval(apiRequest.getPreRequestScript());
|
||||
} catch (ScriptException e) {
|
||||
logger.error("Error executing pre-request 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 {
|
||||
engine.eval(apiRequest.getPostRequestScript());
|
||||
} catch (ScriptException e) {
|
||||
logger.error("Error executing post-request script", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private ApiRequestDTO replacePlaceHoldersWithVariables(ApiRequestDTO apiRequest) {
|
||||
Map<String, String> mergedVariables = new HashMap<>();
|
||||
|
||||
// Merge globals and variables
|
||||
if (globals != null) {
|
||||
mergedVariables.putAll(globals);
|
||||
}
|
||||
if (variables != null) {
|
||||
mergedVariables.putAll(variables);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, String> entry : mergedVariables.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
String placeholder = "\\{\\{" + key + "\\}\\}"; // Escape braces for regex
|
||||
|
||||
apiRequest.setPath(apiRequest.getPath().replaceAll(placeholder, value));
|
||||
|
||||
if (apiRequest.getRequestBody() != null) {
|
||||
apiRequest.setRequestBody(apiRequest.getRequestBody().replaceAll(placeholder, value));
|
||||
}
|
||||
|
||||
if (apiRequest.getHeaders() != null) {
|
||||
apiRequest.setHeaders(replaceInList(apiRequest.getHeaders(), placeholder, value));
|
||||
}
|
||||
|
||||
if (apiRequest.getQueryParams() != null) {
|
||||
apiRequest.setQueryParams(replaceInList(apiRequest.getQueryParams(), placeholder, value));
|
||||
}
|
||||
}
|
||||
|
||||
return apiRequest;
|
||||
}
|
||||
|
||||
private List<ApiRequestKeyValueDTO> replaceInList(List<ApiRequestKeyValueDTO> originalList, String placeholder, String value) {
|
||||
List<ApiRequestKeyValueDTO> replacedList = new ArrayList<>();
|
||||
Pattern pattern = Pattern.compile(placeholder);
|
||||
|
||||
originalList.forEach(kv -> {
|
||||
String replacedKey = pattern.matcher(kv.getKey()).replaceAll(value);
|
||||
String replacedValue = pattern.matcher(kv.getValue()).replaceAll(value);
|
||||
replacedList.add(new ApiRequestKeyValueDTO(kv.isEnabled(), replacedKey, replacedValue));
|
||||
});
|
||||
|
||||
return replacedList;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.eactive.testmaster.server.controller;
|
||||
|
||||
import com.eactive.testmaster.server.dto.ServerDTO;
|
||||
import com.eactive.testmaster.server.dto.ServerSearch;
|
||||
import com.eactive.testmaster.server.entity.Server;
|
||||
import com.eactive.testmaster.server.mapper.ServerMapper;
|
||||
import com.eactive.testmaster.server.service.ServerMgmtService;
|
||||
import java.util.List;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/mgmt/servers")
|
||||
public class ServerRestController {
|
||||
|
||||
private final ServerMgmtService serverMgmtService;
|
||||
|
||||
private final ServerMapper serverMapper;
|
||||
|
||||
public ServerRestController(ServerMgmtService serverMgmtService, ServerMapper serverMapper) {
|
||||
this.serverMgmtService = serverMgmtService;
|
||||
this.serverMapper = serverMapper;
|
||||
}
|
||||
|
||||
@GetMapping("/list.do")
|
||||
public ResponseEntity<List<ServerDTO>> listView(@ModelAttribute("serverSearch") ServerSearch serverSearch, ModelMap model, Pageable pageable) {
|
||||
Page<Server> page = serverMgmtService.findAll(serverSearch.buildSpecification(), pageable);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("X-Total-Count", String.valueOf(page.getTotalElements()));
|
||||
headers.add("X-Total-Pages", String.valueOf(page.getTotalPages()));
|
||||
headers.add("X-Current-Page", String.valueOf(page.getNumber()));
|
||||
headers.add("X-Page-Size", String.valueOf(page.getSize()));
|
||||
return new ResponseEntity<>(serverMapper.map(page.getContent()), headers, HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,15 @@ import com.eactive.testmaster.common.exception.NotFoundException;
|
||||
import com.eactive.testmaster.server.dto.ServerDTO;
|
||||
import com.eactive.testmaster.server.entity.Server;
|
||||
import com.eactive.testmaster.server.entity.ServerRepository;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Mapper(componentModel = "spring",
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
@Component
|
||||
public class ServerMapper {
|
||||
|
||||
@@ -19,17 +21,23 @@ public class ServerMapper {
|
||||
ServerRepository serverRepository;
|
||||
|
||||
public Server mapById(Long id) {
|
||||
if (id == null || id == 0) return null;
|
||||
if (id == null || id == 0) {
|
||||
return null;
|
||||
}
|
||||
return serverRepository.findById(id).orElseThrow(() -> new NotFoundException("Server[" + id + "] not found"));
|
||||
}
|
||||
|
||||
public Long mapById(Server server) {
|
||||
if (server == null) return null;
|
||||
if (server == null) {
|
||||
return null;
|
||||
}
|
||||
return server.getId();
|
||||
}
|
||||
|
||||
public ServerDTO map(Server server) {
|
||||
if (server == null) return null;
|
||||
if (server == null) {
|
||||
return null;
|
||||
}
|
||||
ServerDTO dto = new ServerDTO();
|
||||
|
||||
dto.setId(server.getId());
|
||||
@@ -45,7 +53,9 @@ public class ServerMapper {
|
||||
|
||||
|
||||
public Server map(ServerDTO dto) {
|
||||
if (dto == null) return null;
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Server server = new Server();
|
||||
|
||||
@@ -59,4 +69,8 @@ public class ServerMapper {
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
public List<ServerDTO> map(List<Server> servers) {
|
||||
return servers.stream().map(this::map).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user