Skip to content

Commit 8733708

Browse files
authored
feat: sync testResult bm-controller and bm-agent, client (#58)
* dev: change static css/styles.css path in html * dev: temporary add Randomized TestResult SSE event * dev: css textarea style add * dev: json mapper method add * dev: CommonTestResult class name change * dev: CommonTestResult class name change * dev: invalid json string errorcode * dev: TemplateInfo getting service * dev: util for verify accessable template * dev: add all throws JsonParsing exception * dev: add prepareScript & header field * dev: jsonMapper pre-defining * dev: pre-check invalid json body * dev: headers & prepareScript entity field add * dev: remove unused test and add exception * dev: JsonProcessingException add * dev: remove random method and add vuser, maxRequest, maxDuration field * dev: TestResult class change * dev: pre-build empty template * dev: add front design and functional method * dev: http load sender with multi threads * dev: dns resolver apple silicon dependency * dev: change dto field * dev: additional method and comments * dev: apply changed field * chore: apply changed field * dev: health check endpoint * dev: RequestSpec setter * dev: child thread with parent thread dependency setting * dev: child thread managing method * dev: TPS, MTTFB percentile consts * dev: TestResult scheduled & async-nonblocking http sender & emitter update * test: add mockServer test * fix: java syntax error * dev: split service & controller * dev: pr comment merge & test remove * dev: test exclude setting * dev: add jacoco perfTest within gradle * dev: sse mocking response method ! * dev: test code with sse mock response & message event * dev: schedulerManager duplication/shutdown test * dev: requestHeadersSpec test * dev: HealthCheck controller test * dev: agent information dto * dev: agent status manager setup * dev: agent status manager testing * dev: agent system scheduler * dev: test with cpu, memory, url, etc. * dev: system scheduler constant * dev: encapsulate method * chore: remove system print * dev: maxRequest maxDuration validator * chore: const test exclude * test: sendRequest wrong url, duration null * test: AgentInfo endpoint
1 parent e9a7c7c commit 8733708

File tree

53 files changed

+1599
-264
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1599
-264
lines changed

bm-agent/build.gradle

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def excludeJacocoTestCoverageReport = [
1313
// bmagent
1414
'org/benchmarker/bmagent/BmAgentApplication**',
1515
'org/benchmarker/bmagent/sse/**',
16+
'org/benchmarker/bmagent/consts/SystemSchedulerConst',
1617
// 'org/benchmarker/bmagent/**',
1718
]
1819

@@ -34,16 +35,21 @@ repositories {
3435
mavenCentral()
3536
}
3637

38+
3739
dependencies {
3840
implementation 'org.springframework.boot:spring-boot-starter-web'
39-
compileOnly 'org.projectlombok:lombok'
41+
implementation 'org.projectlombok:lombok'
4042
annotationProcessor 'org.projectlombok:lombok'
43+
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
4144
// add webflux
4245
implementation 'org.springframework.boot:spring-boot-starter-webflux'
4346
testImplementation 'org.springframework.boot:spring-boot-starter-test'
4447
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
4548
testImplementation 'org.testcontainers:junit-jupiter'
4649
testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.1'
50+
if (isAppleSilicon()) {
51+
runtimeOnly("io.netty:netty-resolver-dns-native-macos:4.1.94.Final:osx-aarch_64")
52+
}
4753

4854
implementation project(':bm-common')
4955
}
@@ -89,4 +95,8 @@ jacocoTestCoverageVerification {
8995
}
9096
}
9197
}
98+
}
99+
100+
boolean isAppleSilicon() {
101+
return System.getProperty("os.name") == "Mac OS X" && System.getProperty("os.arch") == "aarch64"
92102
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.benchmarker.bmagent.consts;
2+
3+
import java.util.Arrays;
4+
import java.util.List;
5+
6+
public interface PreftestConsts {
7+
List<Double> percentiles = Arrays.asList(50D, 90D, 95D,99D,99.9D);
8+
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.benchmarker.bmagent.consts;
2+
3+
public interface SystemSchedulerConst {
4+
5+
/**
6+
* System scheduler will run in this ID
7+
*/
8+
Long systemSchedulerId = -100L;
9+
String systemUsageSchedulerName = "cpu-memory-usage-update";
10+
11+
}

bm-agent/src/main/java/org/benchmarker/bmagent/controller/AgentApiController.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package org.benchmarker.bmagent.controller;
22

33

4+
import jakarta.servlet.http.HttpServletRequest;
45
import java.util.Map;
56
import lombok.RequiredArgsConstructor;
67
import lombok.extern.slf4j.Slf4j;
8+
import org.benchmarker.bmagent.AgentInfo;
9+
import org.benchmarker.bmagent.AgentStatus;
710
import org.benchmarker.bmagent.schedule.SchedulerStatus;
811
import org.benchmarker.bmagent.service.IScheduledTaskService;
912
import org.benchmarker.bmagent.service.ISseManageService;
13+
import org.benchmarker.bmagent.status.AgentStatusManager;
1014
import org.benchmarker.bmcommon.dto.TemplateInfo;
1115
import org.springframework.http.ResponseEntity;
1216
import org.springframework.web.bind.annotation.GetMapping;
@@ -16,6 +20,8 @@
1620
import org.springframework.web.bind.annotation.RequestMapping;
1721
import org.springframework.web.bind.annotation.RequestParam;
1822
import org.springframework.web.bind.annotation.RestController;
23+
import org.springframework.web.context.request.RequestContextHolder;
24+
import org.springframework.web.context.request.ServletRequestAttributes;
1925
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
2026

2127

@@ -27,6 +33,7 @@ public class AgentApiController {
2733

2834
private final ISseManageService sseManageService;
2935
private final IScheduledTaskService scheduledTaskService;
36+
private final AgentStatusManager agentStatusManager;
3037

3138
/**
3239
* support sse for the given id
@@ -37,7 +44,10 @@ public class AgentApiController {
3744
*/
3845
@PostMapping("/templates/{template_id}")
3946
public SseEmitter manageSSE(@PathVariable("template_id") Long templateId,
40-
@RequestParam("action") String action, @RequestBody(required = false) TemplateInfo templateInfo) {
47+
@RequestParam("action") String action, @RequestBody TemplateInfo templateInfo) {
48+
log.info(templateInfo.toString());
49+
agentStatusManager.getAndUpdateStatusIfReady(
50+
AgentStatus.TESTING).orElseThrow(() -> new RuntimeException("agent is not ready"));
4151

4252
if (action.equals("start")) {
4353
return sseManageService.start(templateId, templateInfo);
@@ -52,9 +62,27 @@ public SseEmitter manageSSE(@PathVariable("template_id") Long templateId,
5262
*
5363
* @return Map of scheduler id, status
5464
*/
55-
@GetMapping("/status")
65+
@GetMapping("/scheduler/status")
5666
public ResponseEntity<Map<Long, SchedulerStatus>> getSchedulersStatus() {
5767
return ResponseEntity.ok(scheduledTaskService.getStatus());
5868
}
69+
70+
@GetMapping("/status")
71+
public AgentInfo getStatus() {
72+
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
73+
String scheme = request.getScheme(); // http or https
74+
String serverName = request.getServerName();
75+
int serverPort = request.getServerPort();
76+
77+
String agentServerUrl = scheme + "://" + serverName + ":" + serverPort;
78+
79+
return AgentInfo.builder()
80+
.cpuUsage(agentStatusManager.getCpuUsage())
81+
.memoryUsage(agentStatusManager.getMemoryUsage())
82+
.startedAt(agentStatusManager.getStartedAt())
83+
.serverUrl(agentServerUrl)
84+
.status(agentStatusManager.getStatus().get())
85+
.build();
86+
}
5987
}
6088

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.benchmarker.bmagent.initializer;
2+
3+
import java.util.concurrent.TimeUnit;
4+
import lombok.RequiredArgsConstructor;
5+
import org.benchmarker.bmagent.consts.SystemSchedulerConst;
6+
import org.benchmarker.bmagent.schedule.ScheduledTaskService;
7+
import org.benchmarker.bmagent.status.AgentStatusManager;
8+
import org.springframework.boot.CommandLineRunner;
9+
import org.springframework.stereotype.Component;
10+
11+
@Component
12+
@RequiredArgsConstructor
13+
public class Initializer implements CommandLineRunner {
14+
15+
private final ScheduledTaskService scheduledTaskService;
16+
private final AgentStatusManager agentStatusManager;
17+
18+
@Override
19+
public void run(String... args) throws Exception {
20+
// cpu, memory usage checker
21+
scheduledTaskService.startChild(SystemSchedulerConst.systemSchedulerId,
22+
SystemSchedulerConst.systemUsageSchedulerName, agentStatusManager::updateStats, 0, 1,
23+
TimeUnit.SECONDS);
24+
}
25+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package org.benchmarker.bmagent.pref;
2+
3+
import static org.benchmarker.bmcommon.util.NoOp.noOp;
4+
5+
import java.net.MalformedURLException;
6+
import java.net.URL;
7+
import java.time.Duration;
8+
import java.time.LocalDateTime;
9+
import java.time.temporal.ChronoUnit;
10+
import java.util.Comparator;
11+
import java.util.HashMap;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.concurrent.CompletableFuture;
15+
import java.util.concurrent.ConcurrentHashMap;
16+
import java.util.concurrent.atomic.AtomicInteger;
17+
import java.util.stream.IntStream;
18+
import lombok.Getter;
19+
import lombok.extern.slf4j.Slf4j;
20+
import org.benchmarker.bmagent.service.IScheduledTaskService;
21+
import org.benchmarker.bmagent.util.WebClientSupport;
22+
import org.benchmarker.bmcommon.dto.TemplateInfo;
23+
import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec;
24+
import reactor.core.publisher.Mono;
25+
26+
/**
27+
* High load HTTP sender
28+
*
29+
* @author Gyumin Hwangbo
30+
*/
31+
@Slf4j
32+
@Getter
33+
public class HttpSender {
34+
35+
private final ResultManagerService resultManagerService;
36+
private final IScheduledTaskService scheduledTaskService;
37+
38+
39+
public HttpSender(ResultManagerService resultManagerService,
40+
IScheduledTaskService scheduledTaskService) {
41+
this.resultManagerService = resultManagerService;
42+
this.scheduledTaskService = scheduledTaskService;
43+
}
44+
45+
private Integer defaultMaxRequestsPerUser = 100000000;
46+
private Integer defaultMaxDuration = 5; // 5 hours
47+
private AtomicInteger totalRequests = new AtomicInteger(0);
48+
private AtomicInteger totalSuccess = new AtomicInteger(0);
49+
private AtomicInteger totalErrors = new AtomicInteger(0);
50+
// Response time, TPS
51+
private AtomicInteger tps = new AtomicInteger(0);
52+
private ConcurrentHashMap<LocalDateTime, Double> tpsMap = new ConcurrentHashMap<>();
53+
private ConcurrentHashMap<LocalDateTime, Long> mttfbMap = new ConcurrentHashMap<>();
54+
private ConcurrentHashMap<String, Integer> statusCodeCount = new ConcurrentHashMap<>();
55+
private List<CompletableFuture<Void>> futures;
56+
private Boolean isRunning = true;
57+
58+
/**
59+
* <strong>Major implementation sending multiple requests to target server</strong>
60+
*
61+
* <p>Whenever totalRequests hit maxRequests, test stop even if duration is still remained.</p>
62+
* <p>But, if you has null value of maxRequests and valid maxDuration, it will continue to run
63+
* test until current time is reach to maxDuration </p>
64+
*
65+
* <p>This method run child scheduler with {@link TemplateInfo#id} key</p>
66+
*
67+
* @param templateInfo {@link TemplateInfo}
68+
*/
69+
public void sendRequests(TemplateInfo templateInfo) throws MalformedURLException {
70+
URL url = new URL(templateInfo.getUrl());
71+
RequestHeadersSpec<?> req = WebClientSupport.create(templateInfo.getMethod(),
72+
templateInfo.getUrl(),
73+
templateInfo.getBody(),
74+
templateInfo.getHeaders());
75+
76+
// if both duration & requests is not valid, immediately return
77+
if (templateInfo.getMaxDuration() == null && templateInfo.getMaxRequest() == null) {
78+
return;
79+
} else if (templateInfo.getMaxRequest() != null && templateInfo.getMaxDuration() == null) {
80+
templateInfo.setMaxDuration(Duration.ofHours(defaultMaxDuration));
81+
} else if (templateInfo.getMaxRequest() == null && templateInfo.getMaxDuration() != null) {
82+
templateInfo.setMaxRequest(defaultMaxRequestsPerUser);
83+
} else {
84+
noOp();
85+
}
86+
87+
Duration duration = templateInfo.getMaxDuration();
88+
89+
// Future setup
90+
futures = IntStream.range(0, templateInfo.getVuser())
91+
.mapToObj(i -> CompletableFuture.runAsync(() -> {
92+
long startTime = System.currentTimeMillis(); // 시작 시간 기록
93+
long endTime = startTime + duration.toMillis();
94+
for (int j = 0; j < templateInfo.getMaxRequest(); j++) {
95+
// 만약 running 이 아니거나 시간이 끝났다면,
96+
if (!isRunning || System.currentTimeMillis() > endTime) {
97+
return;
98+
}
99+
long requestStartTime = System.currentTimeMillis(); // 요청 시작 시간 기록
100+
req.exchangeToMono(resp -> {
101+
String statusCode = resp.statusCode().toString();
102+
statusCodeCount.merge(statusCode, 1, Integer::sum);
103+
if (resp.statusCode().is2xxSuccessful()) {
104+
totalSuccess.incrementAndGet();
105+
} else {
106+
totalErrors.incrementAndGet();
107+
}
108+
return Mono.empty();
109+
}).block();
110+
111+
long requestEndTime = System.currentTimeMillis();
112+
long elapsedTime = requestEndTime - requestStartTime;
113+
LocalDateTime currentTime = LocalDateTime.now()
114+
.truncatedTo(ChronoUnit.SECONDS);
115+
mttfbMap.merge(currentTime, elapsedTime,
116+
(oldValue, newValue) -> (oldValue + newValue) / 2);
117+
tps.incrementAndGet();
118+
totalRequests.incrementAndGet();
119+
}
120+
121+
}))
122+
.toList();
123+
124+
// Need to calculate & save TPS and MTTFB in every 1 second.
125+
scheduledTaskService.startChild(Long.valueOf(templateInfo.getId()), "recorder", () -> {
126+
// save current tps & reset
127+
tpsMap.put(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS),
128+
Double.valueOf(tps.get()));
129+
tps.set(0); // initial
130+
}, 0, 1, java.util.concurrent.TimeUnit.SECONDS);
131+
132+
// CompletableFuture 종료까지 대기
133+
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
134+
}
135+
136+
/**
137+
* Calculate percentile for TPS (Transactions Per Second)
138+
*
139+
* @param percentile The percentile to calculate (e.g., 90 for 90th percentile)
140+
* @return The calculated percentile value
141+
*/
142+
public Map<Double, Double> calculateTpsPercentile(List<Double> percentile) {
143+
Map<Double, Double> result = new HashMap<>();
144+
145+
List<Double> tpsList = this.sortedTpsMap();
146+
int size = tpsList.size();
147+
for (Double p : percentile) {
148+
int index = (int) Math.ceil((p / 100) * size) - 1;
149+
if (index < 0) {
150+
return Map.of(0D, 0D); // 예외처리: 인덱스가 음수인 경우
151+
}
152+
result.put(p, tpsList.get(index));
153+
}
154+
155+
return result;
156+
}
157+
158+
public List<Double> sortedTpsMap() {
159+
Map<LocalDateTime, Double> tpsSnapshot = new ConcurrentHashMap<>(tpsMap);
160+
return tpsSnapshot.values().stream().sorted(Comparator.reverseOrder())
161+
.toList(); // <-- Here STOP!
162+
}
163+
164+
public List<Long> sortedMttfbMap() {
165+
Map<LocalDateTime, Long> tpsSnapshot = new ConcurrentHashMap<>(mttfbMap);
166+
return tpsSnapshot.values().stream().sorted().toList(); // <-- Here STOP!
167+
}
168+
169+
/**
170+
* Calculate percentile for MTTFB (Mean Time To First Byte)
171+
*
172+
* @param percentile The percentile to calculate (e.g., 90 for 90th percentile)
173+
* @return The calculated percentile value
174+
*/
175+
public Map<Double, Double> calculateMttfbPercentile(List<Double> percentile) {
176+
Map<Double, Double> result = new HashMap<>();
177+
178+
List<Long> mttfbList = this.sortedMttfbMap();
179+
int size = mttfbList.size();
180+
for (Double p : percentile) {
181+
int index = (int) Math.ceil((p / 100) * size) - 1;
182+
if (index < 0) {
183+
return Map.of(0D, 0D); // 예외처리: 인덱스가 음수인 경우
184+
}
185+
result.put(p, Double.valueOf(mttfbList.get(index)));
186+
}
187+
188+
return result;
189+
}
190+
191+
/**
192+
* downstream 에서만 제거 가능. 외부에서는 cancel 불가능...
193+
*/
194+
public void cancelRequests() {
195+
isRunning = false;
196+
197+
}
198+
199+
}

0 commit comments

Comments
 (0)