-
Notifications
You must be signed in to change notification settings - Fork 0
feature: add feature/load engine #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,26 +1,14 @@ | ||
| package com.volta; | ||
|
|
||
| import java.net.http.HttpResponse; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import com.volta.engine.LoadEngine; | ||
|
|
||
| public class Main { | ||
| private static final Logger log = LoggerFactory.getLogger(Main.class); | ||
| private static final String TARGET_URL = "https://jsonplaceholder.typicode.com/posts/1"; | ||
| private static final int TARGET_RPS = 5; | ||
| private static final int DURATION_SECONDS = 10; | ||
|
|
||
| public static void main(String[] args) { | ||
| try (HttpSender sender = new HttpSender()) { | ||
| while (true) { | ||
| try { | ||
| Thread.sleep(1000); | ||
| HttpResponse<String> response = sender.send(TARGET_URL); | ||
| log.info("Status: {}, Body: {}", response.statusCode(), response.body()); | ||
| } catch (Exception e) { | ||
| log.error("Request failed", e); | ||
| } | ||
| } | ||
| } catch (Exception e) { | ||
| log.error("Sender closed or failed to initialize", e); | ||
| } | ||
| LoadEngine engine = new LoadEngine(TARGET_URL, TARGET_RPS, DURATION_SECONDS); | ||
| engine.start(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| package com.volta.engine; | ||
|
|
||
| import com.volta.http.HttpSender; | ||
| import java.net.http.HttpResponse; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| public class LoadEngine { | ||
| private static final Logger log = LoggerFactory.getLogger(LoadEngine.class); | ||
|
|
||
| private final String url; | ||
| private final int targetRps; | ||
| private final int durationSeconds; | ||
| private volatile boolean running = false; | ||
|
|
||
| public LoadEngine(String URL, int targetRPS, int durationSeconds) { | ||
| if (URL == null || URL.isBlank()) { | ||
| throw new IllegalArgumentException("URL must not be empty"); | ||
| } | ||
| if (targetRPS <= 0) { | ||
| throw new IllegalArgumentException("RPS must be positive"); | ||
| } | ||
| if (durationSeconds <= 0) { | ||
| throw new IllegalArgumentException("Duration must be positive"); | ||
| } | ||
|
|
||
| this.url = URL; | ||
| this.targetRps = targetRPS; | ||
| this.durationSeconds = durationSeconds; | ||
| } | ||
|
|
||
| public void start() { | ||
| running = true; | ||
| long intervalNanos = 1_000_000_000L / targetRps; | ||
| long endTime = System.nanoTime() + (long) durationSeconds * 1_000_000_000L; | ||
| long sendNextTime = System.nanoTime(); | ||
|
|
||
| try (HttpSender sender = new HttpSender()) { | ||
| while (running && System.nanoTime() < endTime) { | ||
|
|
||
| while (System.nanoTime() < sendNextTime) { | ||
| // busy-wait | ||
| } | ||
|
Comment on lines
+41
to
+43
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thread.sleep?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At high RPS sleep precision can become a problem — we can overshoot the interval. Should I combine both approaches (sleep for most of the wait, then busy-wait for the last ~1ms), or is Thread.sleep() acceptable for now?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thread.sleep is a simple way, but there are other delay options |
||
|
|
||
| try { | ||
| HttpResponse<String> response = sender.send(url); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use asynchronous sending (or a thread pool). Otherwise, the calls may block and the required RPC performance cannot be achieved
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a limitation of the current implementation. The next issue (#10 ) introduces Virtual Threads — each request will be submitted to a Virtual Thread pool so the main loop is not blocked. |
||
| log.info("Status: {}, Body: {}", response.statusCode(), response.body()); | ||
| } catch (Exception e) { | ||
| log.error("Request failed", e); | ||
| } | ||
| sendNextTime += intervalNanos; | ||
| } | ||
| } catch (Exception e) { | ||
| log.error("Sender closed or failed to initialize", e); | ||
| } | ||
|
|
||
| log.info("Test finished"); | ||
| } | ||
|
|
||
| public void stop() { | ||
| running = false; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.volta.http; | ||
|
|
||
| import java.io.IOException; | ||
| import java.net.URI; | ||
| import java.net.http.HttpClient; | ||
| import java.net.http.HttpRequest; | ||
| import java.net.http.HttpResponse; | ||
|
|
||
| public class HttpSender implements AutoCloseable { | ||
| private final HttpClient client = HttpClient.newHttpClient(); | ||
|
|
||
| public HttpResponse<String> send(String url) throws IOException, InterruptedException { | ||
| HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build(); | ||
|
|
||
| return client.send(request, HttpResponse.BodyHandlers.ofString()); | ||
| } | ||
|
|
||
| @Override | ||
| public void close() throws Exception { | ||
| client.close(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| package com.volta.engine; | ||
|
|
||
| import static com.github.tomakehurst.wiremock.client.WireMock.*; | ||
| import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; | ||
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| import com.github.tomakehurst.wiremock.WireMockServer; | ||
| import com.volta.http.HttpSender; | ||
| import org.junit.jupiter.api.AfterEach; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class LoadEngineTest { | ||
|
|
||
| private WireMockServer wireMock; | ||
| private String baseUrl; | ||
| private HttpSender sender; | ||
|
|
||
| @BeforeEach | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add AfterEach for closing sender |
||
| void setUp() { | ||
| wireMock = new WireMockServer(wireMockConfig().dynamicPort()); | ||
| wireMock.start(); | ||
| wireMock.stubFor(get("/test").willReturn(ok("OK"))); | ||
| baseUrl = "http://localhost:" + wireMock.port(); | ||
|
|
||
| try (HttpSender sender = new HttpSender()) { | ||
| sender.send(baseUrl + "/test"); | ||
|
Comment on lines
+26
to
+27
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this needed?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HttpClient warmup. The first request takes 2-3 seconds due to internal initialization. Without this RPS accuracy tests fail because most of the test duration is spent on the cold start.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, it should be in the source code, not in the test code |
||
| } catch (Exception ignored) { | ||
| } | ||
| } | ||
|
|
||
| @AfterEach | ||
| void tearDown() throws Exception { | ||
| if (sender != null) { | ||
| sender.close(); | ||
| } | ||
| wireMock.stop(); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldSendRequestsAtTargetRps() { | ||
| int targetRps = 10; | ||
| int durationSeconds = 5; | ||
| int expectedRequests = targetRps * durationSeconds; | ||
|
|
||
| LoadEngine engine = new LoadEngine(baseUrl + "/test", targetRps, durationSeconds); | ||
| engine.start(); | ||
|
|
||
| int actualRequests = | ||
| wireMock.countRequestsMatching(getRequestedFor(urlEqualTo("/test")).build()).getCount(); | ||
|
|
||
| int minExpected = (int) (expectedRequests * 0.8); | ||
| int maxExpected = (int) (expectedRequests * 1.2); | ||
|
|
||
| assertTrue( | ||
| actualRequests >= minExpected && actualRequests <= maxExpected, | ||
| String.format( | ||
| "Expected %d requests (±20%%), but got %d", expectedRequests, actualRequests)); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldStopAutomaticallyAfterDuration() { | ||
| int durationSeconds = 2; | ||
| LoadEngine engine = new LoadEngine(baseUrl + "/test", 10, durationSeconds); | ||
|
|
||
| long startTime = System.currentTimeMillis(); | ||
| engine.start(); | ||
| long elapsed = System.currentTimeMillis() - startTime; | ||
|
|
||
| assertTrue( | ||
| elapsed >= 2000 && elapsed < 3000, | ||
| String.format("Expected ~2000ms, but took %dms", elapsed)); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldStopManually() throws InterruptedException { | ||
| int durationSeconds = 60; | ||
| LoadEngine engine = new LoadEngine(baseUrl + "/test", 10, durationSeconds); | ||
|
|
||
| Thread engineThread = new Thread(engine::start); | ||
| engineThread.start(); | ||
|
|
||
| Thread.sleep(1000); | ||
| engine.stop(); | ||
| engineThread.join(2000); | ||
|
|
||
| assertFalse(engineThread.isAlive(), "Engine should have stopped"); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldHandleServerErrors() { | ||
| wireMock.stubFor(get("/error").willReturn(serverError())); | ||
| LoadEngine engine = new LoadEngine(baseUrl + "/error", 10, 2); | ||
|
|
||
| assertDoesNotThrow(() -> engine.start()); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldHandleInvalidUrl() { | ||
| LoadEngine engine = new LoadEngine("http://invalid-host-that-does-not-exist:9999/test", 5, 2); | ||
|
|
||
| assertDoesNotThrow(() -> engine.start()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.volta.http; | ||
|
|
||
| import static com.github.tomakehurst.wiremock.client.WireMock.*; | ||
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| import com.github.tomakehurst.wiremock.junit5.WireMockTest; | ||
| import java.net.http.HttpResponse; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| @WireMockTest(httpPort = 8089) | ||
| class HttpSenderTest { | ||
|
|
||
| private final HttpSender sender = new HttpSender(); | ||
|
|
||
| @Test | ||
| void sendReturns200ForValidUrl() throws Exception { | ||
| stubFor(get("/test").willReturn(ok("Hello from mock!"))); | ||
|
|
||
| HttpResponse<String> response = sender.send("http://localhost:8089/test"); | ||
| assertEquals(200, response.statusCode()); | ||
| } | ||
|
|
||
| @Test | ||
| void responseBodyIsNotEmpty() throws Exception { | ||
| stubFor(get("/test").willReturn(ok("Hello from mock!"))); | ||
|
|
||
| HttpResponse<String> response = sender.send("http://localhost:8089/test"); | ||
| assertFalse(response.body().isEmpty()); | ||
| } | ||
|
|
||
| @Test | ||
| void sendThrowsOnInvalidUrl() { | ||
| assertThrows(Exception.class, () -> sender.send("http://localhost:1")); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validate values