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

Allow configuring Netty compression content size threshold #5303

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
8 changes: 6 additions & 2 deletions vertx-core/src/main/asciidoc/http.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -756,9 +756,9 @@ Whenever the response needs to be sent without compression you can set the heade

Be aware that compression may be able to reduce network traffic but is more CPU-intensive.

To address this latter issue Vert.x allows you to tune the 'compression level' parameter that is native of the gzip/deflate compression algorithms.
To address this latter issue Vert.x allows you to tune the 'compression level' parameter that is native of the gzip/deflate compression algorithms and also set the minimum response content size threshold for compression.

Compression level allows to configure gizp/deflate algorithms in terms of the compression ratio of the resulting data and the computational cost of the compress/decompress operation.
Compression level allows to configure gzip/deflate algorithms in terms of the compression ratio of the resulting data and the computational cost of the compress/decompress operation.

The compression level is an integer value ranged from '1' to '9', where '1' means lower compression ratio but fastest algorithm and '9' means maximum compression ratio available but a slower algorithm.

Expand All @@ -771,6 +771,10 @@ the more the level increases.
By default - if compression is enabled via {@link io.vertx.core.http.HttpServerOptions#setCompressionSupported} - Vert.x will use '6' as compression level,
but the parameter can be configured to address any case with {@link io.vertx.core.http.HttpServerOptions#setCompressionLevel}.

It may not make sense to compress responses under certain size thresholds where the trade-off between CPU and saved network bytes is not beneficial.
The minimum response content size threshold for compression can be configured via {@link io.vertx.core.http.HttpServerOptions#setCompressionContentSizeThreshold}.
For example, if set to '100', responses under 100 bytes will not be compressed. By default, it is '0' which means all content can be compressed.

=== HTTP compression algorithms

Vert.x supports out of the box deflate and gzip.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, HttpSer
obj.setCompressionLevel(((Number)member.getValue()).intValue());
}
break;
case "compressionContentSizeThreshold":
if (member.getValue() instanceof Number) {
obj.setCompressionContentSizeThreshold(((Number)member.getValue()).intValue());
}
break;
case "acceptUnmaskedFrames":
if (member.getValue() instanceof Boolean) {
obj.setAcceptUnmaskedFrames((Boolean)member.getValue());
Expand Down Expand Up @@ -189,6 +194,7 @@ static void toJson(HttpServerOptions obj, JsonObject json) {
static void toJson(HttpServerOptions obj, java.util.Map<String, Object> json) {
json.put("compressionSupported", obj.isCompressionSupported());
json.put("compressionLevel", obj.getCompressionLevel());
json.put("compressionContentSizeThreshold", obj.getCompressionContentSizeThreshold());
json.put("acceptUnmaskedFrames", obj.isAcceptUnmaskedFrames());
json.put("maxWebSocketFrameSize", obj.getMaxWebSocketFrameSize());
json.put("maxWebSocketMessageSize", obj.getMaxWebSocketMessageSize());
Expand Down
28 changes: 28 additions & 0 deletions vertx-core/src/main/java/io/vertx/core/http/HttpServerOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ public class HttpServerOptions extends NetServerOptions {
*/
public static final int DEFAULT_COMPRESSION_LEVEL = 6;

/**
* Default content size threshold for compression = 0 (Netty default)
*/
public static final int DEFAULT_COMPRESSION_CONTENT_SIZE_THRESHOLD = 0;

/**
* Default max WebSocket frame size = 65536
*/
Expand Down Expand Up @@ -198,6 +203,7 @@ public class HttpServerOptions extends NetServerOptions {

private boolean compressionSupported;
private int compressionLevel;
private int compressionContentSizeThreshold;
private List<CompressionOptions> compressors;
private int maxWebSocketFrameSize;
private int maxWebSocketMessageSize;
Expand Down Expand Up @@ -246,6 +252,7 @@ public HttpServerOptions(HttpServerOptions other) {
super(other);
this.compressionSupported = other.isCompressionSupported();
this.compressionLevel = other.getCompressionLevel();
this.compressionContentSizeThreshold = other.getCompressionContentSizeThreshold();
this.compressors = other.compressors != null ? new ArrayList<>(other.compressors) : null;
this.maxWebSocketFrameSize = other.maxWebSocketFrameSize;
this.maxWebSocketMessageSize = other.maxWebSocketMessageSize;
Expand Down Expand Up @@ -303,6 +310,7 @@ public JsonObject toJson() {
private void init() {
compressionSupported = DEFAULT_COMPRESSION_SUPPORTED;
compressionLevel = DEFAULT_COMPRESSION_LEVEL;
compressionContentSizeThreshold = DEFAULT_COMPRESSION_CONTENT_SIZE_THRESHOLD;
maxWebSocketFrameSize = DEFAULT_MAX_WEBSOCKET_FRAME_SIZE;
maxWebSocketMessageSize = DEFAULT_MAX_WEBSOCKET_MESSAGE_SIZE;
handle100ContinueAutomatically = DEFAULT_HANDLE_100_CONTINE_AUTOMATICALLY;
Expand Down Expand Up @@ -588,6 +596,26 @@ public HttpServerOptions setCompressionLevel(int compressionLevel) {
return this;
}

/**
* @return the compression content size threshold
*/
public int getCompressionContentSizeThreshold() {
wvuong marked this conversation as resolved.
Show resolved Hide resolved
return compressionContentSizeThreshold;
}

/**
* Set the compression content size threshold if compression is enabled. This is only applicable for HTTP/1.x response bodies.
* If the response content size in bytes is greater than this threshold, then the response is compressed. Otherwise, it is not compressed.
*
* @param compressionContentSizeThreshold integer greater than or equal to 0.
* @return a reference to this, so the API can be used fluently
*/
public HttpServerOptions setCompressionContentSizeThreshold(int compressionContentSizeThreshold) {
wvuong marked this conversation as resolved.
Show resolved Hide resolved
Arguments.require(compressionContentSizeThreshold >= 0, "compressionContentSizeThreshold must be >= 0");
this.compressionContentSizeThreshold = compressionContentSizeThreshold;
return this;
}

/**
* @return the list of compressor to use
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
*/
final class HttpChunkContentCompressor extends HttpContentCompressor {

public HttpChunkContentCompressor(CompressionOptions... compressionOptions) {
super(0, compressionOptions);
public HttpChunkContentCompressor(int contentSizeThreshold, CompressionOptions... compressionOptions) {
super(contentSizeThreshold, compressionOptions);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class HttpServerConnectionInitializer {
private final Handler<Throwable> exceptionHandler;
private final Object metric;
private final CompressionOptions[] compressionOptions;
private final int compressionContentSizeThreshold;
private final Function<String, String> encodingDetector;

HttpServerConnectionInitializer(ContextInternal context,
Expand Down Expand Up @@ -87,7 +88,8 @@ class HttpServerConnectionInitializer {
this.exceptionHandler = exceptionHandler;
this.metric = metric;
this.compressionOptions = compressionOptions;
this.encodingDetector = compressionOptions != null ? new EncodingDetector(compressionOptions)::determineEncoding : null;
this.compressionContentSizeThreshold = options.getCompressionContentSizeThreshold();
this.encodingDetector = compressionOptions != null ? new EncodingDetector(options.getCompressionContentSizeThreshold(), compressionOptions)::determineEncoding : null;
}

void configurePipeline(Channel ch, SslChannelProvider sslChannelProvider, SslContextManager sslContextManager) {
Expand Down Expand Up @@ -236,7 +238,7 @@ private void configureHttp1Pipeline(ChannelPipeline pipeline, SslChannelProvider
pipeline.addBefore(name, "inflater", new HttpContentDecompressor(false));
}
if (options.isCompressionSupported()) {
pipeline.addBefore(name, "deflater", new HttpChunkContentCompressor(compressionOptions));
pipeline.addBefore(name, "deflater", new HttpChunkContentCompressor(compressionContentSizeThreshold, compressionOptions));
}
}

Expand Down Expand Up @@ -265,8 +267,8 @@ void configureHttp1Handler(ChannelPipeline pipeline, SslChannelProvider sslChann

private static class EncodingDetector extends HttpContentCompressor {

private EncodingDetector(CompressionOptions[] compressionOptions) {
super(compressionOptions);
private EncodingDetector(int contentSizeThreshold, CompressionOptions[] compressionOptions) {
super(contentSizeThreshold, compressionOptions);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package io.vertx.tests.http.compression;

import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.compression.JdkZlibEncoder;
import io.netty.handler.codec.compression.ZlibWrapper;
import io.netty.util.CharsetUtil;
import io.netty.util.internal.StringUtil;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.*;
import org.junit.Test;

public class Http1xCompressionThresholdTest extends HttpCompressionTest {

@Override
protected String encoding() {
return "gzip";
}

@Override
protected MessageToByteEncoder<ByteBuf> encoder() {
return new JdkZlibEncoder(ZlibWrapper.GZIP);
}

@Override
protected HttpServerOptions createBaseServerOptions() {
return new HttpServerOptions().setPort(DEFAULT_HTTP_PORT).setHost(DEFAULT_HTTP_HOST);
}

@Override
protected HttpClientOptions createBaseClientOptions() {
return new HttpClientOptions().setDefaultPort(DEFAULT_HTTP_PORT).setDefaultHost(DEFAULT_HTTP_HOST);
}

@Override
protected void configureServerCompression(HttpServerOptions options) {
options.setCompressionSupported(true);
}

@Test
public void testServerCompressionBelowThreshold() throws Exception {
// set compression threshold to be greater than the content string size so it WILL NOT be compressed
HttpServerOptions httpServerOptions = createBaseServerOptions();
configureServerCompression(httpServerOptions);
httpServerOptions.setCompressionContentSizeThreshold(COMPRESS_TEST_STRING.length() * 2);

doTest(httpServerOptions, onSuccess(resp -> {
// check content encoding header is not set
assertNull(resp.getHeader(HttpHeaders.CONTENT_ENCODING));

resp.body().onComplete(onSuccess(responseBuffer -> {
// check that the response body bytes is itself
String responseBody = responseBuffer.toString(CharsetUtil.UTF_8);
assertEquals(COMPRESS_TEST_STRING, responseBody);
testComplete();
}));
}));
}

@Test
public void testServerCompressionAboveThreshold() throws Exception {
// set compression threshold to be less than the content string size so it WILL be compressed
HttpServerOptions httpServerOptions = createBaseServerOptions();
configureServerCompression(httpServerOptions);
httpServerOptions.setCompressionContentSizeThreshold(COMPRESS_TEST_STRING.length() / 2);

doTest(httpServerOptions, onSuccess(resp -> {
// check content encoding header is set
assertEquals(encoding(), resp.getHeader(HttpHeaders.CONTENT_ENCODING));

resp.body().onComplete(onSuccess(responseBuffer -> {
// check that response body bytes is compressed
assertEquals(StringUtil.toHexString(compressedTestString.getBytes()), StringUtil.toHexString(responseBuffer.getBytes()));
testComplete();
}));
}));
}

private void doTest(HttpServerOptions httpServerOptions, Handler<AsyncResult<HttpClientResponse>> handler) throws Exception {
server.close();
server = vertx.createHttpServer(httpServerOptions);
server.requestHandler(req -> {
assertNotNull(req.headers().get(HttpHeaders.ACCEPT_ENCODING));
req.response()
.end(Buffer.buffer(COMPRESS_TEST_STRING).toString(CharsetUtil.UTF_8));
});
startServer();
client.request(new RequestOptions())
.onComplete(onSuccess(req -> {
req.putHeader(HttpHeaders.ACCEPT_ENCODING, encoding());
req.send().onComplete(handler);
}));
await();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

public abstract class HttpCompressionTest extends HttpTestBase {

private static final String COMPRESS_TEST_STRING = "/*\n" +
protected static final String COMPRESS_TEST_STRING = "/*\n" +
" * Copyright (c) 2011-2016 The original author or authors\n" +
" * ------------------------------------------------------\n" +
" * All rights reserved. This program and the accompanying materials\n" +
Expand All @@ -51,7 +51,7 @@ public abstract class HttpCompressionTest extends HttpTestBase {
" * You may elect to redistribute this code under either of these licenses.\n" +
" */";

private Buffer compressedTestString;
protected Buffer compressedTestString;

public HttpCompressionTest() {
}
Expand Down