From e8e579644c79042bb039ad852c122f81157b7b89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 08:28:40 +0200 Subject: [PATCH 1/8] build(deps): bump junit-jupiter from 5.13.0 to 5.13.1 (#210) Bumps `junit-jupiter` from 5.13.0 to 5.13.1. Updates `org.junit.jupiter:junit-jupiter-api` from 5.13.0 to 5.13.1 - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.13.0...r5.13.1) Updates `org.junit.jupiter:junit-jupiter-engine` from 5.13.0 to 5.13.1 - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.13.0...r5.13.1) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter-api dependency-version: 5.13.1 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.junit.jupiter:junit-jupiter-engine dependency-version: 5.13.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f90b8ce..ec26074 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ pmd = "7.14.0" jacoco = "0.8.13" # Testing mockito = "5.18.0" -junit-jupiter = "5.13.0" +junit-jupiter = "5.13.1" junit-platform = "1.13.0" [libraries] From f6c512a5805f5733c74121cc447d2884942c76c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 08:29:28 +0200 Subject: [PATCH 2/8] build(deps): bump org.junit.platform:junit-platform-launcher (#211) Bumps [org.junit.platform:junit-platform-launcher](https://github.com/junit-team/junit5) from 1.13.0 to 1.13.1. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/commits) --- updated-dependencies: - dependency-name: org.junit.platform:junit-platform-launcher dependency-version: 1.13.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Patrick Boos --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec26074..f1d13c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ jacoco = "0.8.13" # Testing mockito = "5.18.0" junit-jupiter = "5.13.1" -junit-platform = "1.13.0" +junit-platform = "1.13.1" [libraries] openapi-tools-jacksonDatabindNullable = { group = "org.openapitools", name = "jackson-databind-nullable", version.ref = "openapi-tools" } From 13861377b4a13ef0c9727aa3e5234efbc20a50b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 08:29:52 +0200 Subject: [PATCH 3/8] build(deps): bump com.atlassian.oai:swagger-request-validator-core (#213) Bumps [com.atlassian.oai:swagger-request-validator-core](https://bitbucket.org/atlassian/swagger-request-validator) from 2.44.8 to 2.44.9. - [Changelog](https://bitbucket.org/atlassian/swagger-request-validator/src/master/RELEASE-NOTES.md) - [Commits](https://bitbucket.org/atlassian/swagger-request-validator/branches/compare/swagger-request-validator-2.44.9..swagger-request-validator-2.44.8) --- updated-dependencies: - dependency-name: com.atlassian.oai:swagger-request-validator-core dependency-version: 2.44.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f1d13c9..097fb0e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ spring-dependency-management = "1.1.7" openapi-generator = "7.13.0" openapi-tools = "0.2.6" swagger = "2.2.32" -swagger-request-validator = "2.44.8" +swagger-request-validator = "2.44.9" jakarta-validation = "3.1.1" lombok = "1.18.38" commons-codec = "1.18.0" From 82a5be25d5c2b248700f10f4d0225878e82d795c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Co=C8=9Bofan=C4=83?= Date: Wed, 25 Jun 2025 08:30:42 +0200 Subject: [PATCH 4/8] [build] Define other workflow permissions (#212) --- .github/workflows/check.yml | 3 +++ .github/workflows/publish.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index fb00a7b..b730264 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -6,6 +6,9 @@ on: - '**' workflow_call: +permissions: + contents: write + concurrency: group: ${{ github.workflow }}${{ github.ref_name != github.event.repository.default_branch && github.ref || github.run_id }} cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2e01b77..1d5da54 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,6 +5,9 @@ on: tags: - 'v*' +permissions: + contents: write + jobs: check: uses: ./.github/workflows/check.yml From 312e4e3c52986a830400af06cadf667c752b34cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 08:31:04 +0200 Subject: [PATCH 5/8] build(deps): bump org.springframework.boot from 3.5.0 to 3.5.3 (#220) Bumps [org.springframework.boot](https://github.com/spring-projects/spring-boot) from 3.5.0 to 3.5.3. - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.0...v3.5.3) --- updated-dependencies: - dependency-name: org.springframework.boot dependency-version: 3.5.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 097fb0e..f9565f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] java = "21" -spring-boot = "3.5.0" +spring-boot = "3.5.3" spring-dependency-management = "1.1.7" openapi-generator = "7.13.0" openapi-tools = "0.2.6" From f40aea0d64aab310dbfc66b74f1e455bef9dfaaa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 08:37:19 +0200 Subject: [PATCH 6/8] build(deps): bump io.swagger.core.v3:swagger-annotations (#221) Bumps io.swagger.core.v3:swagger-annotations from 2.2.32 to 2.2.34. --- updated-dependencies: - dependency-name: io.swagger.core.v3:swagger-annotations dependency-version: 2.2.34 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f9565f2..5858d56 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ spring-boot = "3.5.3" spring-dependency-management = "1.1.7" openapi-generator = "7.13.0" openapi-tools = "0.2.6" -swagger = "2.2.32" +swagger = "2.2.34" swagger-request-validator = "2.44.9" jakarta-validation = "3.1.1" lombok = "1.18.38" From f8a5bd2bcd4b465a7e9f431bceb5ff205c267d81 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Fri, 27 Jun 2025 15:42:28 +0200 Subject: [PATCH 7/8] Support virtual threads (#215) --- README.md | 3 ++ .../core/OpenApiRequestValidator.java | 10 ++-- .../VirtualThreadLimitedExecutor.java | 54 +++++++++++++++++++ .../core/OpenApiRequestValidatorTest.java | 10 ++-- ...penApiValidationApplicationProperties.java | 5 ++ .../LibraryAutoConfiguration.java | 27 +++++++--- ...piValidationApplicationPropertiesTest.java | 3 +- 7 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/executor/VirtualThreadLimitedExecutor.java diff --git a/README.md b/README.md index 60cb14e..170c60e 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,9 @@ openapi.validation.validation-report-metric-additional-tags=service=example,team # Fail requests on request/response violations. Defaults to false. openapi.validation.should-fail-on-request-violation=true openapi.validation.should-fail-on-response-violation=true + +# Enable virtual threads for async validation. Defaults to false. +openapi.validation.enable-virtual-threads=true ``` ### DataDog metrics diff --git a/openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidator.java b/openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidator.java index 1991fda..1d3e922 100644 --- a/openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidator.java +++ b/openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidator.java @@ -14,26 +14,26 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ThreadPoolExecutor; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.utils.URLEncodedUtils; @Slf4j public class OpenApiRequestValidator { - private final ThreadPoolExecutor threadPoolExecutor; + private final Executor executor; private final OpenApiInteractionValidatorWrapper validator; private final ValidationReportToOpenApiViolationsMapper mapper; public OpenApiRequestValidator( - ThreadPoolExecutor threadPoolExecutor, + Executor executor, MetricsReporter metricsReporter, OpenApiInteractionValidatorWrapper validator, ValidationReportToOpenApiViolationsMapper mapper, OpenApiRequestValidationConfiguration configuration ) { - this.threadPoolExecutor = threadPoolExecutor; + this.executor = executor; this.validator = validator; this.mapper = mapper; @@ -74,7 +74,7 @@ public void validateResponseObjectAsync( private void executeAsync(Runnable command) { try { - threadPoolExecutor.execute(command); + executor.execute(command); } catch (RejectedExecutionException ignored) { // ignored } diff --git a/openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/executor/VirtualThreadLimitedExecutor.java b/openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/executor/VirtualThreadLimitedExecutor.java new file mode 100644 index 0000000..9aab3a9 --- /dev/null +++ b/openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/executor/VirtualThreadLimitedExecutor.java @@ -0,0 +1,54 @@ +package com.getyourguide.openapi.validation.core.executor; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +public class VirtualThreadLimitedExecutor implements Executor { + private static final int DEFAULT_MAX_CONCURRENT = 2; + private final int maxConcurrent; + private final AtomicInteger runningCount = new AtomicInteger(0); + + public VirtualThreadLimitedExecutor() { + this(DEFAULT_MAX_CONCURRENT); + } + + public VirtualThreadLimitedExecutor(int maxConcurrent) { + checkVirtualThreadSupport(); + this.maxConcurrent = maxConcurrent; + } + + public static boolean isSupported() { + try { + checkVirtualThreadSupport(); + return true; + } catch (UnsupportedOperationException | NoSuchMethodError e) { + return false; + } + } + + private static void checkVirtualThreadSupport() { + // This will throw NoSuchMethodError on Java < 21 + //noinspection ResultOfMethodCallIgnored + Thread.ofVirtual(); + } + + @Override + public void execute(Runnable command) { + if (runningCount.get() >= maxConcurrent) { + return; + } + + if (runningCount.incrementAndGet() > maxConcurrent) { + runningCount.decrementAndGet(); + return; + } + + Thread.ofVirtual().start(() -> { + try { + command.run(); + } finally { + runningCount.decrementAndGet(); + } + }); + } +} diff --git a/openapi-validation-core/src/test/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidatorTest.java b/openapi-validation-core/src/test/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidatorTest.java index 3ba4fbf..9cb2c38 100644 --- a/openapi-validation-core/src/test/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidatorTest.java +++ b/openapi-validation-core/src/test/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidatorTest.java @@ -14,8 +14,8 @@ import java.net.URI; import java.util.HashMap; import java.util.List; +import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ThreadPoolExecutor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -23,21 +23,21 @@ public class OpenApiRequestValidatorTest { - private ThreadPoolExecutor threadPoolExecutor; + private Executor executor; private OpenApiInteractionValidatorWrapper validator; private OpenApiRequestValidator openApiRequestValidator; @BeforeEach public void setup() { - threadPoolExecutor = mock(); + executor = mock(); validator = mock(); MetricsReporter metricsReporter = mock(); var mapper = mock(ValidationReportToOpenApiViolationsMapper.class); when(mapper.map(any(), any(), any(), any(), any())).thenReturn(List.of()); openApiRequestValidator = new OpenApiRequestValidator( - threadPoolExecutor, + executor, metricsReporter, validator, mapper, @@ -47,7 +47,7 @@ public void setup() { @Test public void testWhenThreadPoolExecutorRejectsExecutionThenItShouldNotThrow() { - Mockito.doThrow(new RejectedExecutionException()).when(threadPoolExecutor).execute(any()); + Mockito.doThrow(new RejectedExecutionException()).when(executor).execute(any()); openApiRequestValidator.validateRequestObjectAsync(mock(), null, null, mock()); } diff --git a/spring-boot-starter/spring-boot-starter-core/src/main/java/com/getyourguide/openapi/validation/OpenApiValidationApplicationProperties.java b/spring-boot-starter/spring-boot-starter-core/src/main/java/com/getyourguide/openapi/validation/OpenApiValidationApplicationProperties.java index 6e0f891..6448612 100644 --- a/spring-boot-starter/spring-boot-starter-core/src/main/java/com/getyourguide/openapi/validation/OpenApiValidationApplicationProperties.java +++ b/spring-boot-starter/spring-boot-starter-core/src/main/java/com/getyourguide/openapi/validation/OpenApiValidationApplicationProperties.java @@ -38,6 +38,7 @@ public class OpenApiValidationApplicationProperties { private List excludedHeaders; private Boolean shouldFailOnRequestViolation; private Boolean shouldFailOnResponseViolation; + private Boolean enableVirtualThreads; public double getSampleRate() { return sampleRate != null ? sampleRate : SAMPLE_RATE_DEFAULT; @@ -84,6 +85,10 @@ public List getExcludedHeaders() { .toList(); } + public boolean isEnableVirtualThreads() { + return enableVirtualThreads != null ? enableVirtualThreads : false; + } + public OpenApiRequestValidationConfiguration toOpenApiRequestValidationConfiguration() { return OpenApiRequestValidationConfiguration.builder() .sampleRate(getSampleRate()) diff --git a/spring-boot-starter/spring-boot-starter-core/src/main/java/com/getyourguide/openapi/validation/autoconfigure/LibraryAutoConfiguration.java b/spring-boot-starter/spring-boot-starter-core/src/main/java/com/getyourguide/openapi/validation/autoconfigure/LibraryAutoConfiguration.java index 29d8aa1..130d8b9 100644 --- a/spring-boot-starter/spring-boot-starter-core/src/main/java/com/getyourguide/openapi/validation/autoconfigure/LibraryAutoConfiguration.java +++ b/spring-boot-starter/spring-boot-starter-core/src/main/java/com/getyourguide/openapi/validation/autoconfigure/LibraryAutoConfiguration.java @@ -19,12 +19,14 @@ import com.getyourguide.openapi.validation.core.OpenApiInteractionValidatorFactory; import com.getyourguide.openapi.validation.core.OpenApiRequestValidator; import com.getyourguide.openapi.validation.core.exclusions.InternalViolationExclusions; +import com.getyourguide.openapi.validation.core.executor.VirtualThreadLimitedExecutor; import com.getyourguide.openapi.validation.core.log.DefaultOpenApiViolationHandler; import com.getyourguide.openapi.validation.core.log.ExclusionsOpenApiViolationHandler; import com.getyourguide.openapi.validation.core.log.ThrottlingOpenApiViolationHandler; import com.getyourguide.openapi.validation.core.mapper.ValidationReportToOpenApiViolationsMapper; import com.getyourguide.openapi.validation.core.metrics.DefaultMetricsReporter; import java.util.Optional; +import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -104,14 +106,7 @@ public OpenApiRequestValidator openApiRequestValidator( MetricsReporter metricsReporter, ValidatorConfiguration validatorConfiguration ) { - var threadPoolExecutor = new ThreadPoolExecutor( - 2, - 2, - 1000L, - TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(10), - new ThreadPoolExecutor.DiscardPolicy() - ); + var threadPoolExecutor = createThreadPoolExecutor(); return new OpenApiRequestValidator( threadPoolExecutor, @@ -122,4 +117,20 @@ public OpenApiRequestValidator openApiRequestValidator( properties.toOpenApiRequestValidationConfiguration() ); } + + private Executor createThreadPoolExecutor() { + if (properties.isEnableVirtualThreads() && VirtualThreadLimitedExecutor.isSupported()) { + return new VirtualThreadLimitedExecutor(); + } + + // Fallback to ThreadPoolExecutor with regular threads + return new ThreadPoolExecutor( + 2, + 2, + 1000L, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(10), + new ThreadPoolExecutor.DiscardPolicy() + ); + } } diff --git a/spring-boot-starter/spring-boot-starter-core/src/test/java/com/getyourguide/openapi/validation/OpenApiValidationApplicationPropertiesTest.java b/spring-boot-starter/spring-boot-starter-core/src/test/java/com/getyourguide/openapi/validation/OpenApiValidationApplicationPropertiesTest.java index f5f95c1..0f0a00f 100644 --- a/spring-boot-starter/spring-boot-starter-core/src/test/java/com/getyourguide/openapi/validation/OpenApiValidationApplicationPropertiesTest.java +++ b/spring-boot-starter/spring-boot-starter-core/src/test/java/com/getyourguide/openapi/validation/OpenApiValidationApplicationPropertiesTest.java @@ -34,7 +34,8 @@ void getters() { EXCLUDED_PATHS, EXCLUDED_HEADERS, true, - false + false, + true ); assertEquals(SAMPLE_RATE, loggingConfiguration.getSampleRate()); From 6571c26197555bb08e6a0d1c872240b22c999844 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Fri, 27 Jun 2025 15:51:46 +0200 Subject: [PATCH 8/8] v3.2.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 81c7f89..e4288e5 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ apply from: "${rootDir}/gradle/publish-root.gradle" allprojects { group = 'com.getyourguide.openapi.validation' description = 'OpenAPI Validation library' - version = '3.2.1' + version = '3.2.2' java { toolchain {