Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(#607): support for disabling keep alive header in configuration #614

Merged
Merged
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
48 changes: 42 additions & 6 deletions documentation/user/en/operate/configure.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ cache: # [see Cache configuration](#c

api: # [see API configuration](#api-configuration)
exposedOn: null
ioThreads: 4
ioThreads: null
idleTimeoutInMillis: 2K
requestTimeoutInMillis: 2K
parseTimeoutInMillis: 1K
keepAlive: true
maxEntitySizeInBytes: 2MB
accessLog: false
certificate: # [see TLS configuration](#tls-configuration)
generateAndUseSelfSigned: true
Expand Down Expand Up @@ -566,11 +571,6 @@ is resolved.
This section of the configuration allows you to selectively enable, disable, and tweak specific APIs.

<dl>
<dt>ioThreads</dt>
<dd>
<p>**Default:** `4`</p>
<p>Defines the number of IO threads that will be used by Undertow for accept and send HTTP payload.</p>
</dd>
<dt>exposedOn</dt>
<dd>
<p>When evitaDB is running in a Docker container and the ports are exposed on the host systems
Expand All @@ -579,6 +579,42 @@ This section of the configuration allows you to selectively enable, disable, and
the name (without port) of the host system host name that will be used by all API endpoints without
specific `exposedHost` configuration property to use that host name and appropriate port.</p>
</dd>
<dt>ioThreads</dt>
<dd>
<p>**Default:** `number of CPUs * 2`</p>
<p>Defines the number of IO threads that will be used by Undertow for accept and send HTTP payload.</p>
</dd>
<dt>idleTimeoutInMillis</dt>
<dd>
<p>**Default:** `2K`</p>
<p>The amount of time a connection can be idle for before it is timed out. An idle connection is a connection
that has had no data transfer in the idle timeout period. Note that this is a fairly coarse grained approach,
and small values will cause problems for requests with a long processing time.</p>
</dd>
<dt>requestTimeoutInMillis</dt>
<dd>
<p>**Default:** `2K`</p>
<p>The amount of time a connection can sit idle without processing a request, before it is closed by the server.</p>
</dd>
<dt>parseTimeoutInMillis</dt>
<dd>
<p>**Default:** `1K`</p>
<p>How long a request can spend in the parsing phase before it is timed out. This timer is started when the first
bytes of a request are read, and finishes once all the headers have been parsed.</p>
</dd>
<dt>keepAlive</dt>
<dd>
<p>**Default:** `true`</p>
<p>If this is true then a Connection: keep-alive header will be added to responses, even when it is not strictly
required by the specification.</p>
</dd>
<dt>maxEntitySizeInBytes</dt>
<dd>
<p>**Default:** `2MB`</p>
<p>The default maximum size of a request entity. If entity body is larger than this limit then a IOException
will be thrown at some point when reading the request (on the first read for fixed length requests, when too
much data has been read for chunked requests).</p>
</dd>
<dt>accessLog</dt>
<dd>
<p>**Default:** `false`</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,44 @@
/**
* This DTO record encapsulates common settings shared among all the API endpoints.
*
* @param exposedOn the name of the host the APIs will be exposed on when evitaDB is running inside a container
* @param ioThreads defines the number of IO thread will be used by Undertow for accept and send HTTP payload
* @param accessLog defines whether the access logs will be enabled or not
* @param endpoints contains specific configuration for all the API endpoints
* @param certificate defines the certificate settings that will be used to secure connections to the web servers providing APIs
* @param exposedOn the name of the host the APIs will be exposed on when evitaDB is running inside a container
* @param ioThreads defines the number of IO thread will be used by Undertow for accept and send HTTP payload
* @param idleTimeoutInMillis The amount of time a connection can be idle for before it is timed out. An idle connection is a
* connection that has had no data transfer in the idle timeout period. Note that this is a fairly coarse
* grained approach, and small values will cause problems for requests with a long processing time.
* @param parseTimeoutInMillis How long a request can spend in the parsing phase before it is timed out. This timer is started when
* the first bytes of a request are read, and finishes once all the headers have been parsed.
* @param requestTimeoutInMillis The amount of time a connection can sit idle without processing a request, before it is closed by
* the server.
* @param keepAlive If this is true then a Connection: keep-alive header will be added to responses, even when it is not strictly required by
* the specification.
* @param maxEntitySizeInBytes The default maximum size of a request entity. If entity body is larger than this limit then a
* java.io.IOException will be thrown at some point when reading the request (on the first read for fixed
* length requests, when too much data has been read for chunked requests).
* @param accessLog defines whether the access logs will be enabled or not
* @param endpoints contains specific configuration for all the API endpoints
* @param certificate defines the certificate settings that will be used to secure connections to the web servers providing APIs
* @author Jan Novotný (novotny@fg.cz), FG Forrest a.s. (c) 2022
*/
public record ApiOptions(
@Nonnull String exposedOn,
@Nullable Integer ioThreads,
int ioThreads,
int idleTimeoutInMillis,
int requestTimeoutInMillis,
int parseTimeoutInMillis,
boolean keepAlive,
long maxEntitySizeInBytes,
boolean accessLog,
@Nonnull CertificateSettings certificate,
@Nonnull Map<String, AbstractApiConfiguration> endpoints
) {
// double the value of available processors (recommended by Undertow configuration)
public static final int DEFAULT_IO_THREADS = Runtime.getRuntime().availableProcessors() << 1;
public static final int DEFAULT_IDLE_TIMEOUT = 20 * 1000;
public static final int DEFAULT_PARSE_TIMEOUT = 1000;
public static final int DEFAULT_REQUEST_TIMEOUT = 1000;
public static final boolean DEFAULT_KEEP_ALIVE = true;
public static final long DEFAULT_MAX_ENTITY_SIZE = 2_097_152L;

/**
* Builder for the api options. Recommended to use to avoid binary compatibility problems in the future.
Expand All @@ -64,8 +88,31 @@ public static ApiOptions.Builder builder() {
return new ApiOptions.Builder();
}

public ApiOptions(
@Nonnull String exposedOn,
int ioThreads, int idleTimeoutInMillis, int requestTimeoutInMillis, int parseTimeoutInMillis,
boolean keepAlive, long maxEntitySizeInBytes, boolean accessLog,
@Nonnull CertificateSettings certificate,
@Nonnull Map<String, AbstractApiConfiguration> endpoints
) {
this.exposedOn = exposedOn;
this.ioThreads = ioThreads <= 0 ? DEFAULT_IO_THREADS : ioThreads;
this.idleTimeoutInMillis = idleTimeoutInMillis <= 0 ? DEFAULT_IDLE_TIMEOUT : idleTimeoutInMillis;
this.requestTimeoutInMillis = requestTimeoutInMillis <= 0 ? DEFAULT_REQUEST_TIMEOUT : requestTimeoutInMillis;
this.parseTimeoutInMillis = parseTimeoutInMillis <= 0 ? DEFAULT_PARSE_TIMEOUT : parseTimeoutInMillis;
this.keepAlive = keepAlive;
this.maxEntitySizeInBytes = maxEntitySizeInBytes <= 0 ? DEFAULT_MAX_ENTITY_SIZE : maxEntitySizeInBytes;
this.accessLog = accessLog;
this.certificate = certificate;
this.endpoints = endpoints;
}

public ApiOptions() {
this(null, null, false, new CertificateSettings(), new HashMap<>(8));
this(
null, DEFAULT_IO_THREADS, DEFAULT_IDLE_TIMEOUT, DEFAULT_REQUEST_TIMEOUT, DEFAULT_PARSE_TIMEOUT,
DEFAULT_KEEP_ALIVE, DEFAULT_MAX_ENTITY_SIZE, false,
new CertificateSettings(), new HashMap<>(8)
);
}

/**
Expand All @@ -77,25 +124,21 @@ public <T extends AbstractApiConfiguration> T getEndpointConfiguration(@Nonnull
return (T) endpoints.get(endpointCode);
}

/**
* Returns set {@link #ioThreads} or returns a default value.
*/
public int ioThreadsAsInt() {
return ofNullable(ioThreads)
// double the value of available processors (recommended by Undertow configuration)
.orElseGet(() -> Runtime.getRuntime().availableProcessors() << 1);
}

/**
* Standard builder pattern implementation.
*/
@ToString
public static class Builder {
private final Map<String, Class<?>> apiProviders;
private final Map<String, AbstractApiConfiguration> enabledProviders;
private int ioThreads = DEFAULT_IO_THREADS;
private int idleTimeoutInMillis = DEFAULT_IDLE_TIMEOUT;
private int requestTimeoutInMillis = DEFAULT_REQUEST_TIMEOUT;
private int parseTimeoutInMillis = DEFAULT_PARSE_TIMEOUT;
private boolean keepAlive = DEFAULT_KEEP_ALIVE;
private long maxEntitySizeInBytes = DEFAULT_MAX_ENTITY_SIZE;
private CertificateSettings certificate;
@Nullable private String exposedOn;
@Nullable private Integer ioThreads;
private boolean accessLog;

Builder() {
Expand All @@ -119,8 +162,38 @@ public ApiOptions.Builder exposedOn(@Nonnull String exposedOn) {
}

@Nonnull
public ApiOptions.Builder ioThreads(int ioThreads) {
this.ioThreads = ioThreads;
public ApiOptions.Builder ioThreads(@Nullable Integer ioThreads) {
this.ioThreads = ofNullable(ioThreads).orElse(DEFAULT_IO_THREADS);
return this;
}

@Nonnull
public ApiOptions.Builder idleTimeoutInMillis(int idleTimeoutInMillis) {
this.idleTimeoutInMillis = idleTimeoutInMillis;
return this;
}

@Nonnull
public ApiOptions.Builder requestTimeoutInMillis(int requestTimeoutInMillis) {
this.requestTimeoutInMillis = requestTimeoutInMillis;
return this;
}

@Nonnull
public ApiOptions.Builder parseTimeoutInMillis(int parseTimeoutInMillis) {
this.parseTimeoutInMillis = parseTimeoutInMillis;
return this;
}

@Nonnull
public ApiOptions.Builder keepAlive(boolean keepAlive) {
this.keepAlive = keepAlive;
return this;
}

@Nonnull
public ApiOptions.Builder maxEntitySizeInBytes(long maxEntitySizeInBytes) {
this.maxEntitySizeInBytes = maxEntitySizeInBytes;
return this;
}

Expand Down Expand Up @@ -170,7 +243,8 @@ public <T extends AbstractApiConfiguration> ApiOptions.Builder enable(@Nonnull S
@Nonnull
public ApiOptions build() {
return new ApiOptions(
exposedOn, ioThreads, accessLog, certificate, enabledProviders
exposedOn, ioThreads, idleTimeoutInMillis, requestTimeoutInMillis, parseTimeoutInMillis,
keepAlive, maxEntitySizeInBytes, accessLog, certificate, enabledProviders
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,12 @@ public static CertificatePath initCertificate(
final CertificateType[] certificateTypes = apiOptions.endpoints()
.values()
.stream()
.flatMap(it -> Stream.of(
it.isTlsEnabled() ? CertificateType.SERVER : null,
it.isMtlsEnabled() ? CertificateType.CLIENT : null
)
.filter(Objects::nonNull))
.flatMap(
it -> Stream.of(
it.isEnabled() && it.isTlsEnabled() ? CertificateType.SERVER : null,
it.isEnabled() && it.isMtlsEnabled() ? CertificateType.CLIENT : null
).filter(Objects::nonNull)
)
.distinct()
.toArray(CertificateType[]::new);

Expand Down Expand Up @@ -362,7 +363,7 @@ public ExternalApiServer(
initCertificate(apiOptions, serverCertificateManager) :
(
certificateSettings.custom().certificate() != null && !certificateSettings.custom().certificate().isBlank() &&
certificateSettings.custom().privateKey() != null && !certificateSettings.custom().privateKey().isBlank() ?
certificateSettings.custom().privateKey() != null && !certificateSettings.custom().privateKey().isBlank() ?
new CertificatePath(
certificateSettings.custom().certificate(),
certificateSettings.custom().privateKey(),
Expand Down Expand Up @@ -452,7 +453,7 @@ private void configureUndertow(
/*
IO threads represent separate thread pool, this is enforced by Undertow.
*/
.setWorkerIoThreads(apiOptions.ioThreadsAsInt())
.setWorkerIoThreads(apiOptions.ioThreads())
.setWorkerName("Undertow XNIO worker.")
.build()
)
Expand All @@ -470,27 +471,32 @@ private void configureUndertow(
grained approach, and small values will cause problems for requests with a long processing time.
(milliseconds)
*/
.setServerOption(UndertowOptions.IDLE_TIMEOUT, 20 * 1000)
.setServerOption(UndertowOptions.IDLE_TIMEOUT, apiOptions.idleTimeoutInMillis())
/*
How long a request can spend in the parsing phase before it is timed out. This timer is started when
the first bytes of a request are read, and finishes once all the headers have been parsed.
(milliseconds)
*/
.setServerOption(UndertowOptions.REQUEST_PARSE_TIMEOUT, 1000)
.setServerOption(UndertowOptions.REQUEST_PARSE_TIMEOUT, apiOptions.parseTimeoutInMillis())
/*
The amount of time a connection can sit idle without processing a request, before it is closed by
the server.
(milliseconds)
*/
.setServerOption(UndertowOptions.NO_REQUEST_TIMEOUT, 1000)
.setServerOption(UndertowOptions.NO_REQUEST_TIMEOUT, apiOptions.requestTimeoutInMillis())
/*
If this is true then a Connection: keep-alive header will be added to responses, even when it is not strictly required by
the specification.
*/
.setServerOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE, apiOptions.keepAlive())
/*
The default maximum size of a request entity. If entity body is larger than this limit then a
java.io.IOException will be thrown at some point when reading the request (on the first read for fixed
length requests, when too much data has been read for chunked requests). This value is only the default
size, it is possible for a handler to override this for an individual request by calling
io.undertow.server.HttpServerExchange.setMaxEntitySize(long size). Defaults to unlimited.
*/
.setServerOption(UndertowOptions.MAX_ENTITY_SIZE, 2_097_152L);
.setServerOption(UndertowOptions.MAX_ENTITY_SIZE, apiOptions.maxEntitySizeInBytes());

final AccessLogReceiver accessLogReceiver = apiOptions.accessLog() ? new Slf4JAccessLogReceiver() : new NoopAccessLogReceiver();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import io.evitadb.externalApi.api.system.ProbesProvider.Readiness;
import io.evitadb.externalApi.api.system.ProbesProvider.ReadinessState;
import io.evitadb.externalApi.api.system.model.HealthProblem;
import io.evitadb.externalApi.configuration.AbstractApiConfiguration;
import io.evitadb.externalApi.configuration.ApiOptions;
import io.evitadb.externalApi.configuration.CertificatePath;
import io.evitadb.externalApi.configuration.CertificateSettings;
Expand Down Expand Up @@ -269,7 +268,7 @@ public ExternalApiProvider<SystemConfig> register(
final boolean atLeastOnEndpointRequiresTls = apiOptions.endpoints()
.values()
.stream()
.anyMatch(AbstractApiConfiguration::isTlsEnabled);
.anyMatch(it -> it.isEnabled() && it.isTlsEnabled());
final boolean atLeastOnEndpointRequiresMtls = apiOptions.endpoints()
.values()
.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* | __/\ V /| | || (_| | |_| | |_) |
* \___| \_/ |_|\__\__,_|____/|____/
*
* Copyright (c) 2023
* Copyright (c) 2023-2024
*
* Licensed under the Business Source License, Version 1.1 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -27,7 +27,6 @@
import io.evitadb.api.requestResponse.data.structure.EntityReference;
import io.evitadb.api.requestResponse.schema.SealedEntitySchema;
import io.evitadb.externalApi.configuration.ApiOptions;
import io.evitadb.externalApi.configuration.CertificateSettings;
import io.evitadb.externalApi.graphql.GraphQLProvider;
import io.evitadb.externalApi.graphql.GraphQLProviderRegistrar;
import io.evitadb.externalApi.graphql.configuration.GraphQLConfig;
Expand All @@ -39,7 +38,6 @@
import org.openjdk.jmh.annotations.TearDown;

import java.util.Collections;
import java.util.Map;

/**
* Base state class for {@link GraphQLArtificialEntitiesBenchmark} benchmark.
Expand Down Expand Up @@ -86,7 +84,9 @@ public void setUp() {
// start graphql server
server = new ExternalApiServer(
this.evita,
new ApiOptions(null, null, false, new CertificateSettings.Builder().build(), Map.of(GraphQLProvider.CODE, new GraphQLConfig())),
ApiOptions.builder()
.enable(GraphQLProvider.CODE, new GraphQLConfig())
.build(),
Collections.singleton(new GraphQLProviderRegistrar())
);
server.start();
Expand Down
Loading