Posts multipart image uploads to the {@link pl.zzpj.ai_service.ClassificationController} + * under a ramped injection profile (~10 → 50 → 100 req/s). + * + *
How to run (requires a running ai-service instance): + *
+ * ./gradlew :ai-service:gatlingRun + *+ * Override the target and auth token via system properties, e.g. + *
+ * ./gradlew :ai-service:gatlingRun \ + * -DbaseUrl=http://localhost:8080 -Dtoken=<valid-jwt> + *+ * + *
Correlate the results with the service's Spring Boot Actuator metrics, e.g. + * {@code http.server.requests} (latency / throughput / status codes) and JVM metrics + * exposed at {@code /actuator/metrics} and {@code /actuator/prometheus}. + */ +public class ClassifySimulation extends Simulation { + + private static final String BASE_URL = + System.getProperty("baseUrl", "http://localhost:8080"); + private static final String TOKEN = + System.getProperty("token", "replace-with-a-valid-jwt"); + + private final HttpProtocolBuilder httpProtocol = http + .baseUrl(BASE_URL) + .acceptHeader("application/json") + .header("Authorization", "Bearer " + TOKEN) + .userAgentHeader("gatling-ai-service-loadtest"); + + private final ScenarioBuilder classify = scenario("Classify image") + .exec( + http("POST /api/classify") + .post("/api/classify") + .bodyPart( + io.gatling.javaapi.http.HttpDsl.RawFileBodyPart("file", "test-dog.jpg") + .fileName("test-dog.jpg") + .contentType("image/jpeg") + ).asMultipartForm() + .check(io.gatling.javaapi.http.HttpDsl.status().in(200, 401)) + ); + + { + setUp( + classify.injectOpen( + // ~10 req/s baseline + constantUsersPerSec(10).during(15), + // ramp 10 → 50 req/s + rampUsersPerSec(10).to(50).during(30), + // ramp 50 → 100 req/s peak + rampUsersPerSec(50).to(100).during(30), + // hold 100 req/s + constantUsersPerSec(100).during(30) + ) + ).protocols(httpProtocol); + } +} diff --git a/ai-service/src/gatling/resources/test-dog.jpg b/ai-service/src/gatling/resources/test-dog.jpg new file mode 100644 index 0000000..d0cec4a Binary files /dev/null and b/ai-service/src/gatling/resources/test-dog.jpg differ diff --git a/ai-service/src/main/java/pl/zzpj/ai_service/AiServiceApplication.java b/ai-service/src/main/java/pl/zzpj/ai_service/AiServiceApplication.java new file mode 100644 index 0000000..6f3ac96 --- /dev/null +++ b/ai-service/src/main/java/pl/zzpj/ai_service/AiServiceApplication.java @@ -0,0 +1,16 @@ +package pl.zzpj.ai_service; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; + +@SpringBootApplication +@EnableDiscoveryClient +@EnableFeignClients +public class AiServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(AiServiceApplication.class, args); + } +} diff --git a/ai-service/src/main/java/pl/zzpj/ai_service/CategoryMapper.java b/ai-service/src/main/java/pl/zzpj/ai_service/CategoryMapper.java new file mode 100644 index 0000000..8d9ac5c --- /dev/null +++ b/ai-service/src/main/java/pl/zzpj/ai_service/CategoryMapper.java @@ -0,0 +1,382 @@ +package pl.zzpj.ai_service; + +import java.util.List; +import java.util.Map; + +/** + * Maps a raw ImageNet class label to one of a fixed set of broad categories. Matching is done by + * checking whether any keyword for a given category appears as a substring of the lower-cased + * label. Returns {@code "other"} when no keyword matches. + * + *
Categories are evaluated in the order declared below — the first category with a matching
+ * keyword wins. Order matters only if a label could match keywords from multiple categories; today
+ * none do, but the ordered list keeps future additions deterministic.
+ */
+public final class CategoryMapper {
+
+ private CategoryMapper() {}
+
+ private static final List