[PERF] Jackson reflection-free serialization/deserialization (#42946)

* [PERF] Jackson reflection-free serialization/deserialization

Closes #42945

Signed-off-by: Martin Bartoš <mabartos@redhat.com>

* Update docs/guides/server/configuration-production.adoc

Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net>
Signed-off-by: Martin Bartoš <mabartos@redhat.com>

* Docs improvements

Signed-off-by: Martin Bartoš <mabartos@redhat.com>

* Update docs/guides/server/configuration-production.adoc

Co-authored-by: Václav Muzikář <vaclav@muzikari.cz>
Signed-off-by: Martin Bartoš <mabartos@redhat.com>

* Polish the features template macros

Signed-off-by: Martin Bartoš <mabartos@redhat.com>

---------

Signed-off-by: Martin Bartoš <mabartos@redhat.com>
Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net>
Co-authored-by: Václav Muzikář <vaclav@muzikari.cz>
This commit is contained in:
Martin Bartoš
2025-10-17 20:24:47 +02:00
committed by GitHub
parent b807a45091
commit 37bea126c7
12 changed files with 111 additions and 22 deletions

View File

@@ -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

View File

@@ -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 <a href="https://github.com/keycloak/keycloak/issues/37967">Deprecate for removal the Instagram social broker</a>.
*/

View File

@@ -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]

View File

@@ -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.

View File

@@ -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]
====

View File

@@ -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"/>
</@tmpl.guide>

View File

@@ -8,3 +8,26 @@
</#list>
|===
</#macro>
<#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 </#if>`--features=${profileFeature.name}`
====
</#macro>

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -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()
);
}

View File

@@ -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<T> {
return new PropertyMapper.Builder<>(opt);
}
/**
* Create a property mapper from a feature.
* The mapper maps to external properties the state of the feature.
* <p>
* If the feature is enabled, it returns {@code true}. Otherwise {@code null}.
*/
public static PropertyMapper.Builder<Boolean> 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);

View File

@@ -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");
}
}