From 3c1b33600f38614b331dd39b8159d9596af84122 Mon Sep 17 00:00:00 2001 From: marr97 Date: Thu, 5 Mar 2026 19:30:21 +0300 Subject: [PATCH 1/2] feature: add basic load engine --- src/main/java/com/volta/Main.java | 22 +--- .../java/com/volta/engine/LoadEngine.java | 53 ++++++++++ src/main/java/com/volta/http/HttpSender.java | 22 ++++ .../java/com/volta/engine/LoadEngineTest.java | 100 ++++++++++++++++++ .../java/com/volta/http/HttpSenderTest.java | 35 ++++++ 5 files changed, 215 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/volta/engine/LoadEngine.java create mode 100644 src/main/java/com/volta/http/HttpSender.java create mode 100644 src/test/java/com/volta/engine/LoadEngineTest.java create mode 100644 src/test/java/com/volta/http/HttpSenderTest.java diff --git a/src/main/java/com/volta/Main.java b/src/main/java/com/volta/Main.java index f5a4827..a55d96d 100644 --- a/src/main/java/com/volta/Main.java +++ b/src/main/java/com/volta/Main.java @@ -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 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(); } } diff --git a/src/main/java/com/volta/engine/LoadEngine.java b/src/main/java/com/volta/engine/LoadEngine.java new file mode 100644 index 0000000..cc84efd --- /dev/null +++ b/src/main/java/com/volta/engine/LoadEngine.java @@ -0,0 +1,53 @@ +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) { + 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 + } + + try { + HttpResponse response = sender.send(url); + 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; + } +} diff --git a/src/main/java/com/volta/http/HttpSender.java b/src/main/java/com/volta/http/HttpSender.java new file mode 100644 index 0000000..330de6e --- /dev/null +++ b/src/main/java/com/volta/http/HttpSender.java @@ -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 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(); + } +} diff --git a/src/test/java/com/volta/engine/LoadEngineTest.java b/src/test/java/com/volta/engine/LoadEngineTest.java new file mode 100644 index 0000000..ef6a6a4 --- /dev/null +++ b/src/test/java/com/volta/engine/LoadEngineTest.java @@ -0,0 +1,100 @@ +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; + + @BeforeEach + 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"); + } catch (Exception ignored) { + } + } + + @AfterEach + void tearDown() { + 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()); + } +} diff --git a/src/test/java/com/volta/http/HttpSenderTest.java b/src/test/java/com/volta/http/HttpSenderTest.java new file mode 100644 index 0000000..1405cd2 --- /dev/null +++ b/src/test/java/com/volta/http/HttpSenderTest.java @@ -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 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 response = sender.send("http://localhost:8089/test"); + assertFalse(response.body().isEmpty()); + } + + @Test + void sendThrowsOnInvalidUrl() { + assertThrows(Exception.class, () -> sender.send("http://localhost:1")); + } +} From 110969e3658409e33eca5228d6621297f9d02c62 Mon Sep 17 00:00:00 2001 From: marr97 Date: Wed, 11 Mar 2026 01:24:14 +0300 Subject: [PATCH 2/2] fixed --- src/main/java/com/volta/engine/LoadEngine.java | 10 ++++++++++ src/test/java/com/volta/engine/LoadEngineTest.java | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/volta/engine/LoadEngine.java b/src/main/java/com/volta/engine/LoadEngine.java index cc84efd..12d4194 100644 --- a/src/main/java/com/volta/engine/LoadEngine.java +++ b/src/main/java/com/volta/engine/LoadEngine.java @@ -14,6 +14,16 @@ public class LoadEngine { 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; diff --git a/src/test/java/com/volta/engine/LoadEngineTest.java b/src/test/java/com/volta/engine/LoadEngineTest.java index ef6a6a4..30b1b61 100644 --- a/src/test/java/com/volta/engine/LoadEngineTest.java +++ b/src/test/java/com/volta/engine/LoadEngineTest.java @@ -14,6 +14,7 @@ class LoadEngineTest { private WireMockServer wireMock; private String baseUrl; + private HttpSender sender; @BeforeEach void setUp() { @@ -29,7 +30,10 @@ void setUp() { } @AfterEach - void tearDown() { + void tearDown() throws Exception { + if (sender != null) { + sender.close(); + } wireMock.stop(); }