8000 Implement load shedding for RESTEasy reactive by ahus1 · Pull Request #23343 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,11 @@ public enum ClientAuth {
.description("Enables or disables the HTTP/s and Socket serving.")
.defaultValue(Boolean.TRUE)
.build();

public static final Option<Integer> HTTP_MAX_CONCURRENT_REQUESTS = new OptionBuilder<>("http-max-concurrent-requests", Integer.class)
Comment thread
vmuzikar marked this conversation as resolved.
Resolved
.category(OptionCategory.HTTP)
.description("Experimental: Maximum concurrent number of HTTP requests to handle. " +
"Use this to shed load in an overload situation. Excess requests will return a \"503 Server not Available\" response.")
.build();

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.runtime.configuration.ProfileManager;
import io.quarkus.vertx.http.deployment.FilterBuildItem;
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.smallrye.config.ConfigValue;
import org.hibernate.cfg.AvailableSettings;
Expand Down Expand Up @@ -185,6 +187,10 @@ class KeycloakProcessor {
ClasspathThemeResourceProviderFactory.class,
JarThemeProviderFactory.class,
JpaMapStorageProviderFactory.class);
public static final String QUARKUS_HEALTH_ROOT_PROPERTY = "quarkus.smallrye-health.root-path";
public static final String QUARKUS_METRICS_PATH_PROPERTY = "quarkus.micrometer.export.prometheus.path";
public static final String QUARKUS_DEFAULT_HEALTH_PATH = "health";
public static final String QUARKUS_DEFAULT_METRICS_PATH = "metrics";

static {
DEPLOYEABLE_SCRIPT_PROVIDERS.put(AUTHENTICATORS, KeycloakProcessor::registerScriptAuthenticator);
Expand Down Expand Up @@ -593,6 +599,27 @@ void indexLegacyJpaStore(BuildProducer<IndexDependencyBuildItem> indexDependency
indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem("org.keycloak", "keycloak-model-jpa"));
}

@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
void initializeFilter(BuildProducer<FilterBuildItem> filters, KeycloakRecorder recorder, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {

List<String> ignoredPaths = new ArrayList<>();

if (isHealthEnabled()) {
ignoredPaths.add(nonApplicationRootPathBuildItem.
resolvePath(getOptionalValue(QUARKUS_HEALTH_ROOT_PROPERTY)
.orElse(QUARKUS_DEFAULT_HEALTH_PATH)));
}

if (isMetricsEnabled()) {
ignoredPaths.add(nonApplicationRootPathBuildItem.
resolvePath(getOptionalValue(QUARKUS_METRICS_PATH_PROPERTY)
.orElse(QUARKUS_DEFAULT_METRICS_PATH)));
}

filters.produce(new FilterBuildItem(recorder.createRequestFilter(ignoredPaths),FilterBuildItem.AUTHORIZATION - 10));
}

@BuildStep
void disableMetricsEndpoint(BuildProducer<RouteBuildItem> routes) {
if (!isMetricsEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory;
import org.keycloak.quarkus.runtime.integration.web.LoadSheddingHandler;
import org.keycloak.quarkus.runtime.storage.database.liquibase.FastServiceLocator;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
Expand Down Expand Up @@ -140,6 +141,26 @@ public void contributeRuntimeProperties(BiConsumer<String, Object> propertyColle
};
}

public LoadSheddingHandler createRequestFilter(List<String> ignoredPaths) {
return new LoadSheddingHandler(createIgnoredHttpPathsPredicate(ignoredPaths));
}

private Predicate<RoutingContext> createIgnoredHttpPathsPredicate(List<String> ignoredPaths) {
if (ignoredPaths == null || ignoredPaths.isEmpty()) {
return null;
}

return context -> {
for (String ignoredPath : ignoredPaths) {
if (context.request().uri().startsWith(ignoredPath)) {
return true;
}
}

return false;
};
}

public void setCryptoProvider(FipsMode fipsMode) {
String cryptoProvider = fipsMode.getProviderClassName();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ public static PropertyMapper[] getHttpPropertyMappers() {
.mapFrom(SecurityOptions.FIPS_MODE.getKey())
.transformer(HttpPropertyMappers::resolveKeyStoreType)
.paramLabel("type")
.build(),
fromOption(HttpOptions.HTTP_MAX_CONCURRENT_REQUESTS)
.paramLabel("limit")
.build()
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.keycloak.quarkus.runtime.integration.web;

import java.util.Optional;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Predicate;

import org.apache.http.HttpStatus;
import org.jboss.logging.Logger;

import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;
import org.keycloak.quarkus.runtime.configuration.Configuration;

/**
* Limit the number of concurrent requests (both blocking and non-blocking) to a configured upper limit.
* Special paths (like for health probes and metrics) can be excluded, so their requests will not be dropped.
*
* @author Alexander Schwartz
*/
public class LoadSheddingHandler implements Handler<RoutingContext> {

private final Logger logger = Logger.getLogger(LoadSheddingHandler.class);

private final Predicate<RoutingContext> contextFilter;

private final Semaphore available;

public LoadSheddingHandler() {
this(null);
}

public LoadSheddingHandler(Predicate<RoutingContext> contextFilter) {
Optional<String> maxConcurrentRequests = Configuration.getRuntimeProperty("kc.http-max-concurrent-requests");
if (maxConcurrentRequests.isPresent()) {
available = new Semaphore(Integer.parseInt(maxConcurrentRequests.get()), false);
} else {
available = null;
}
this.contextFilter = contextFilter;
}

private final LongAdder rejectedRequests = new LongAdder();
private volatile boolean loadSheddingActive;

@Override
public void handle(RoutingContext context) {
if (available == null || ignoreContext(context)) {
context.next();
return;
}
if (available.tryAcquire()) {
context.addEndHandler(event -> {
available.release();
});
if (loadSheddingActive) {
Long req = null;
synchronized (rejectedRequests) {
if (loadSheddingActive) {
loadSheddingActive = false;
// rejectedRequests.sumThenReset() is approximative when concurrent increments are active, still it should be accurate enough for this log message
req = rejectedRequests.sumThenReset();
}
}
if (req != null) {
// this avoids logging in a synchronized block
logger.warnf("Executor thread pool no longer exhausted, request processing continued after %s discarded request(s)", req);
}
}
context.next();
} else {
boolean logExhausted = false;
if (!loadSheddingActive) {
synchronized (rejectedRequests) {
if (!loadSheddingActive) {
loadSheddingActive = true;
logExhausted = true;
}
}
}
if (logExhausted) {
// this avoids logging in a synchronized block
logger.warn("Executor thread pool exhausted, starting to reject requests");
}
rejectedRequests.increment();
// if the thread pool has been configured with a maximum queue size, it might reject the request
context.fail(HttpStatus.SC_SERVICE_UNAVAILABLE);
}
}

private boolean ignoreContext(RoutingContext context) {
return contextFilter != null && contextFilter.test(context);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ HTTP/TLS:
--http-enabled <true|false>
Enables the HTTP listener. Default: false.
--http-host <host> The used HTTP Host. Default: 0.0.0.0.
--http-max-concurrent-requests <limit>
Experimental: Maximum concurrent number of HTTP requests to handle. Use this
to shed load in an overload situation. Excess requests will return a "503
Server not Available" response.
--http-port <port> The used HTTP port. Default: 8080.
--http-relative-path <path>
Set the path relative to '/' for serving resources. The path must start with a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ HTTP/TLS:
--http-enabled <true|false>
Enables the HTTP listener. Default: false.
--http-host <host> The used HTTP Host. Default: 0.0.0.0.
--http-max-concurrent-requests <limit>
Experimental: Maximum concurrent number of HTTP requests to handle. Use this
to shed load in an overload situation. Excess requests will return a "503
Server not Available" response.
--http-port <port> The used HTTP port. Default: 8080.
--http-relative-path <path>
Set the path relative to '/' for serving resources. The path must start with a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ HTTP/TLS:
--http-enabled <true|false>
Enables the HTTP listener. Default: false.
--http-host <host> The used HTTP Host. Default: 0.0.0.0.
--http-max-concurrent-requests <limit>
Experimental: Maximum concurrent number of HTTP requests to handle. Use this
to shed load in an overload situation. Excess requests will return a "503
Server not Available" response.
--http-port <port> The used HTTP port. Default: 8080.
--http-relative-path <path>
Set the path relative to '/' for serving resources. The path must start with a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ HTTP/TLS:
--http-enabled <true|false>
Enables the HTTP listener. Default: false.
--http-host <host> The used HTTP Host. Default: 0.0.0.0.
--http-max-concurrent-requests <limit>
Experimental: Maximum concurrent number of HTTP requests to handle. Use this
to shed load in an overload situation. Excess requests will return a "503
Server not Available" response.
--http-port <port> The used HTTP port. Default: 8080.
--http-relative-path <path>
Set the path relative to '/' for serving resources. The path must start with a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ HTTP/TLS:
--http-enabled <true|false>
Enables the HTTP listener. Default: false.
--http-host <host> The used HTTP Host. Default: 0.0.0.0.
--http-max-concurrent-requests <limit>
Experimental: Maximum concurrent number of HTTP requests to handle. Use this
to shed load in an overload situation. Excess requests will return a "503
Server not Available" response.
--http-port <port> The used HTTP port. Default: 8080.
--http-relative-path <path>
Set the path relative to '/' for serving resources. The path must start with a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ HTTP/TLS:
--http-enabled <true|false>
Enables the HTTP listener. Default: false.
--http-host <host> The used HTTP Host. Default: 0.0.0.0.
--http-max-concurrent-requests <limit>
Experimental: Maximum concurrent number of HTTP requests to handle. Use this
to shed load in an overload situation. Excess requests will return a "503
Server not Available" response.
--http-port <port> The used HTTP port. Default: 8080.
--http-relative-path <path>
Set the path relative to '/' for serving resources. The path must start with a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ HTTP/TLS:
--http-enabled <true|false>
Enables the HTTP listener. Default: false.
--http-host <host> The used HTTP Host. Default: 0.0.0.0.
--http-max-concurrent-requests <limit>
Experimental: Maximum concurrent number of HTTP requests to handle. Use this
to shed load in an overload situation. Excess requests will return a "503
Server not Available" response.
--http-port <port> The used HTTP port. Default: 8080.
--http-relative-path <path>
Set the path relative to '/' for serving resources. The path must start with a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ HTTP/TLS:
--http-enabled <true|false>
Enables the HTTP listener. Default: false.
--http-host <host> The used HTTP Host. Default: 0.0.0.0.
--http-max-concurrent-requests <limit>
Experimental: Maximum concurrent number of HTTP requests to handle. Use this
to shed load in an overload situation. Excess requests will return a "503
Server not Available" response.
--http-port <port> The used HTTP port. Default: 8080.
--http-relative-path <path>
Set the path relative to '/' for serving resources. The path must start with a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ HTTP/TLS:
--http-enabled <true|false>
Enables the HTTP listener. Default: false.
--http-host <host> The used HTTP Host. Default: 0.0.0.0.
--http-max-concurrent-requests <limit>
Experimental: Maximum concurrent number of HTTP requests to handle. Use this
to shed load in an overload situation. Excess requests will return a "503
Server not Available" response.
--http-port <port> The used HTTP port. Default: 8080.
--https-certificate-file <file>
The file path to a server certificate or certificate chain in PEM format.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ HTTP/TLS:
--http-enabled <true|false>
Enables the HTTP listener. Default: false.
--http-host <host> The used HTTP Host. Default: 0.0.0.0.
--http-max-concurrent-requests <limit>
Experimental: Maximum concurrent number of HTTP requests to handle. Use this
to shed load in an overload situation. Excess requests will return a "503
Server not Available" response.
--http-port <port> The used HTTP port. Default: 8080.
--https-certificate-file <file>
The file path to a server certificate or certificate chain in PEM format.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ HTTP/TLS:
--http-enabled <true|false>
Enables the HTTP listener. Default: false.
--http-host <host> The used HTTP Host. Default: 0.0.0.0.
--http-max-concurrent-requests <limit>
Experimental: Maximum concurrent number of HTTP requests to handle. Use this
to shed load in an overload situation. Excess requests will return a "503
Server not Available" response.
--http-port <port> The used HTTP port. Default: 8080.
--https-certificate-file <file>
The file path to a server certificate or certificate chain in PEM format.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ HTTP/TLS:
--http-enabled <true|false>
Enables the HTTP listener. Default: false.
--http-host <host> The used HTTP Host. Default: 0.0.0.0.
--http-max-concurrent-requests <limit>
Experimental: Maximum concurrent number of HTTP requests to handle. Use this
to shed load in an overload situation. Excess requests will return a "503
Server not Available" response.
--http-port <port> The used HTTP port. Default: 8080.
--https-certificate-file <file>
The file path to a server certificate or certificate chain in PEM format.
Expand Down
0