From 37bea126c723068b35d1f46bab5367c18db156ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Barto=C5=A1?= Date: Fri, 17 Oct 2025 20:24:47 +0200 Subject: [PATCH] [PERF] Jackson reflection-free serialization/deserialization (#42946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [PERF] Jackson reflection-free serialization/deserialization Closes #42945 Signed-off-by: Martin Bartoš * Update docs/guides/server/configuration-production.adoc Co-authored-by: Alexander Schwartz Signed-off-by: Martin Bartoš * Docs improvements Signed-off-by: Martin Bartoš * Update docs/guides/server/configuration-production.adoc Co-authored-by: Václav Muzikář Signed-off-by: Martin Bartoš * Polish the features template macros Signed-off-by: Martin Bartoš --------- Signed-off-by: Martin Bartoš Co-authored-by: Alexander Schwartz Co-authored-by: Václav Muzikář --- .github/workflows/ci.yml | 3 ++- .../java/org/keycloak/common/Profile.java | 2 ++ docs/documentation/release_notes/index.adoc | 3 +++ .../release_notes/topics/26_5_0.adoc | 19 ++++++++++++--- docs/guides/securing-apps/token-exchange.adoc | 18 ++------------- .../server/configuration-production.adoc | 22 ++++++++++++++++++ docs/guides/templates/features.adoc | 23 +++++++++++++++++++ .../org/keycloak/guides/maven/Features.java | 7 +++++- .../java/org/keycloak/config/HttpOptions.java | 1 - .../mappers/HttpPropertyMappers.java | 5 ++++ .../configuration/mappers/PropertyMapper.java | 18 +++++++++++++++ .../quarkus/runtime/cli/PicocliTest.java | 12 ++++++++++ 12 files changed, 111 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9c322957c0..9c584064dd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -140,10 +140,11 @@ jobs: uses: ./.github/actions/integration-test-setup - name: Run base tests + # enable the http-optimized-serializers feature for the old testsuite to verify it works as expected run: | TESTS=`testsuite/integration-arquillian/tests/base/testsuites/base-suite.sh ${{ matrix.group }}` echo "Tests: $TESTS" - ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dauth.server.feature=http-optimized-serializers -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh - uses: ./.github/actions/upload-flaky-tests name: Upload flaky tests diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 56f5b186a0c..48e56d0bc24 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -148,6 +148,8 @@ public class Profile { DB_TIDB("TiDB database type", Type.EXPERIMENTAL), + HTTP_OPTIMIZED_SERIALIZERS("Optimized JSON serializers for better performance of the HTTP layer", Type.PREVIEW), + /** * @see Deprecate for removal the Instagram social broker. */ diff --git a/docs/documentation/release_notes/index.adoc b/docs/documentation/release_notes/index.adoc index 7c5a58ebddf..95f34390fff 100644 --- a/docs/documentation/release_notes/index.adoc +++ b/docs/documentation/release_notes/index.adoc @@ -13,6 +13,9 @@ include::topics/templates/document-attributes.adoc[] :release_header_latest_link: {releasenotes_link_latest} include::topics/templates/release-header.adoc[] +== {project_name_full} 26.5.0 +include::topics/26_5_0.adoc[leveloffset=2] + == {project_name_full} 26.4.0 include::topics/26_4_0.adoc[leveloffset=2] diff --git a/docs/documentation/release_notes/topics/26_5_0.adoc b/docs/documentation/release_notes/topics/26_5_0.adoc index 6e6013a4db0..085ddcafaee 100644 --- a/docs/documentation/release_notes/topics/26_5_0.adoc +++ b/docs/documentation/release_notes/topics/26_5_0.adoc @@ -1,9 +1,22 @@ // Release notes should contain only headline-worthy new features, // assuming that people who migrate will read the upgrading guide anyway. -// -== Breaking Fix for Windows in Loopback Hostname Verification + += Preview of enhanced HTTP performance + +You can now enable a more efficient way to handle JSON data in the HTTP layer. +This change increases throughput by ~5%, stabilizes response times, and reduces system resource usage. + +In order to apply it, you need to explicitly enable the feature `http-optimized-serializers`. + +NOTE: This feature is *preview*. +ifeval::[{project_community}==true] +We gather more feedback about potential issues in https://github.com/keycloak/keycloak/discussions/43484[this discussion]. We appreciate any feedback. +endif::[] + +For more details, see the https://www.keycloak.org/server/configuration-production[Configuring Keycloak for production] guide. + += Breaking Fix for Windows in Loopback Hostname Verification This release introduces a breaking change for Windows users: setups that previously relied on custom machine names or non-standard hostnames for loopback (e.g., `127.0.0.1` resolving to a custom name) may require updates to their trusted domain configuration. Only `localhost` and `*.localhost` are now recognized for loopback verification. Keycloak now consistently normalizes loopback addresses to `localhost` for domain verification across all platforms. This change ensures predictable behavior for trusted domain checks, regardless of the underlying OS. - diff --git a/docs/guides/securing-apps/token-exchange.adoc b/docs/guides/securing-apps/token-exchange.adoc index 8f31018ad82..44cfd71ff42 100644 --- a/docs/guides/securing-apps/token-exchange.adoc +++ b/docs/guides/securing-apps/token-exchange.adoc @@ -1,5 +1,6 @@ <#import "/templates/guide.adoc" as tmpl> <#import "/templates/links.adoc" as links> +<#import "/templates/features.adoc" as features> <@tmpl.guide title="Configuring and using token exchange" @@ -327,22 +328,7 @@ s|Subject impersonation (including direct naked impersonation) | Not implement [[_legacy-token-exchange]] == Legacy token exchange -:tech_feature_name: Token Exchange -:tech_feature_id: token-exchange - -[NOTE] -==== -{tech_feature_name} is -*Preview* -and is not fully supported. This feature is disabled by default. - -To enable start the server with `--features=preview` -ifdef::tech_feature_id[] -or `--features={tech_feature_id}` -endif::[] - -{tech_feature_name} is *Technology Preview* and is not fully supported. -==== +<@features.techpreview feature="token-exchange"/> [NOTE] ==== diff --git a/docs/guides/server/configuration-production.adoc b/docs/guides/server/configuration-production.adoc index f752181d281..166ba6e546e 100644 --- a/docs/guides/server/configuration-production.adoc +++ b/docs/guides/server/configuration-production.adoc @@ -1,6 +1,7 @@ <#import "/templates/guide.adoc" as tmpl> <#import "/templates/kc.adoc" as kc> <#import "/templates/links.adoc" as links> +<#import "/templates/features.adoc" as features> <@tmpl.guide title="Configuring {project_name} for production" @@ -87,4 +88,25 @@ export JAVA_OPTS_APPEND="-Djava.net.preferIPv4Stack=false -Djava.net.preferIPv6A See <@links.server id="caching" anchor="network-bind-address"/> for more details. +== Preview of enhanced HTTP performance +<@features.techpreview feature="http-optimized-serializers" additionalCommunityText="We gather more feedback on this feature to promote it to supported. Please, share your feedback about any issue in https://github.com/keycloak/keycloak/discussions/43484[this discussion]."/> + +In production environments, the performance of the HTTP layer is critical. +Every request passes through it, making it a key factor in overall system responsiveness, scalability, and user experience. + +This feature improves how {project_name} handles JSON data in HTTP requests and responses. +The result is a more efficient runtime with measurable benefits: + +- ~5% increase in throughput +- More stable response times +- Reduced system resource usage + +These improvements help ensure smoother, more predictable performance at scale while also lowering the operational cost of running production systems. + +The only known tradeoff is that build time increases by ~6% as certain actions were moved to the build time instead of runtime. + +You can enable this feature as follows: + +<@kc.start parameters="--features=http-optimized-serializers"/> + diff --git a/docs/guides/templates/features.adoc b/docs/guides/templates/features.adoc index 4090f84a2ee..2713753ea9c 100644 --- a/docs/guides/templates/features.adoc +++ b/docs/guides/templates/features.adoc @@ -8,3 +8,26 @@ |=== + +<#macro techpreview feature additionalCommunityText=""> +<#assign profileFeature = ctx.features.getFeature(feature)> + +[NOTE] +==== +${profileFeature.description} is +ifeval::[{project_product}==true] +*Technology Preview* +endif::[] +ifeval::[{project_community}==true] +*Preview* +endif::[] +and is not fully supported. This feature is disabled by default. + +ifeval::[{project_community}==true] +${additionalCommunityText!""} +endif::[] + +To enable start the server with <#if profileFeature.type != "PREVIEW_DISABLED_BY_DEFAULT">`--features=preview` or `--features=${profileFeature.name}` + +==== + diff --git a/docs/maven-plugin/src/main/java/org/keycloak/guides/maven/Features.java b/docs/maven-plugin/src/main/java/org/keycloak/guides/maven/Features.java index b34e31a19c8..160d3addc98 100644 --- a/docs/maven-plugin/src/main/java/org/keycloak/guides/maven/Features.java +++ b/docs/maven-plugin/src/main/java/org/keycloak/guides/maven/Features.java @@ -43,6 +43,11 @@ public class Features { return features.stream().filter(f -> f.profileFeature.getUpdatePolicy() == Profile.FeatureUpdatePolicy.ROLLING_NO_UPGRADE).collect(Collectors.toList()); } + public Feature getFeature(String featureId) { + return features.stream().filter(f -> f.getName().equals(featureId)).findAny() + .orElseThrow(() -> new IllegalArgumentException("Cannot find the '%s' feature for guides".formatted(featureId))); + } + public static class Feature { private final Profile.Feature profileFeature; @@ -67,7 +72,7 @@ public class Features { return profileFeature.getUpdatePolicy().toString(); } - private Profile.Feature.Type getType() { + public Profile.Feature.Type getType() { return profileFeature.getType(); } } diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/HttpOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/HttpOptions.java index 474b69ac5e3..45504c2b2f0 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/HttpOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/HttpOptions.java @@ -150,5 +150,4 @@ public class HttpOptions { .description("Service level objectives for HTTP server requests. Use this instead of the default histogram, or use it in combination to add additional buckets. " + "Specify a list of comma-separated values defined in milliseconds. Example with buckets from 5ms to 10s: 5,10,25,50,250,500,1000,2500,5000,10000") .build(); - } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java index 3558d12d575..8bc0e7d55c0 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java @@ -4,6 +4,7 @@ import io.quarkus.runtime.util.ClassPathUtils; import io.quarkus.vertx.http.runtime.options.TlsUtils; import io.smallrye.config.ConfigSourceInterceptorContext; +import org.keycloak.common.Profile; import org.keycloak.common.crypto.FipsMode; import org.keycloak.config.HttpOptions; import org.keycloak.config.SecurityOptions; @@ -23,6 +24,7 @@ import java.util.Optional; import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue; import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalValue; +import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromFeature; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; public final class HttpPropertyMappers implements PropertyMapperGrouping { @@ -154,6 +156,9 @@ public final class HttpPropertyMappers implements PropertyMapperGrouping { fromOption(HttpOptions.HTTP_METRICS_SLOS) .isEnabled(MetricsPropertyMappers::metricsEnabled, MetricsPropertyMappers.METRICS_ENABLED_MSG) .paramLabel("list of buckets") + .build(), + fromFeature(Profile.Feature.HTTP_OPTIMIZED_SERIALIZERS) + .to("quarkus.rest.jackson.optimization.enable-reflection-free-serializers") .build() ); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java index b561aad2bfc..50c5653b77e 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java @@ -33,8 +33,10 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; +import org.keycloak.common.Profile; import org.keycloak.config.DeprecatedMetadata; import org.keycloak.config.Option; +import org.keycloak.config.OptionBuilder; import org.keycloak.config.OptionCategory; import org.keycloak.quarkus.runtime.cli.PropertyException; import org.keycloak.quarkus.runtime.cli.ShortErrorMessageHandler; @@ -547,6 +549,22 @@ public class PropertyMapper { return new PropertyMapper.Builder<>(opt); } + /** + * Create a property mapper from a feature. + * The mapper maps to external properties the state of the feature. + *

+ * If the feature is enabled, it returns {@code true}. Otherwise {@code null}. + */ + public static PropertyMapper.Builder fromFeature(Profile.Feature feature) { + final var option = new OptionBuilder<>(feature.getKey() + "-hidden-mapper", Boolean.class) + .buildTime(true) + .hidden() + .build(); + return new Builder<>(option) + .isEnabled(() -> Profile.isFeatureEnabled(feature)) + .transformer((v, ctx) -> Boolean.TRUE.toString()); // we know the feature is enabled due to .isEnabled() + } + public void validate(ConfigValue value) { if (validator != null) { validator.accept(this, value); diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java index ee62b76f839..5a2e32f5eb8 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java @@ -966,4 +966,16 @@ public class PicocliTest extends AbstractConfigurationTest { assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode); assertThat(nonRunningPicocli.getErrString(), containsString("Available only when health is enabled")); } + + @Test + public void httpOptimizedSerializers() { + var nonRunningPicocli = pseudoLaunch("start-dev"); + assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); + assertExternalConfigNull("quarkus.rest.jackson.optimization.enable-reflection-free-serializers"); + onAfter(); + + nonRunningPicocli = pseudoLaunch("start-dev", "--features=http-optimized-serializers"); + assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); + assertExternalConfig("quarkus.rest.jackson.optimization.enable-reflection-free-serializers", "true"); + } }