-
Notifications
You must be signed in to change notification settings - Fork 21
Add support for request compression #968
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package software.amazon.smithy.java.client.http.compression; | ||
|
|
||
| import software.amazon.smithy.java.io.datastream.DataStream; | ||
|
|
||
| /** | ||
| * Represents a compression algorithm that can be used to compress request | ||
| * bodies. | ||
| */ | ||
| public interface CompressionAlgorithm { | ||
| /** | ||
| * The ID of the checksum algorithm. This is matched against the algorithm | ||
| * names used in the trait e.g. "gzip" | ||
| */ | ||
| String algorithmId(); | ||
|
|
||
| /** | ||
| * Compresses content of fixed length | ||
| */ | ||
| DataStream compress(DataStream data); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package software.amazon.smithy.java.client.http.compression; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.UncheckedIOException; | ||
| import java.util.zip.GZIPOutputStream; | ||
| import software.amazon.smithy.java.io.ByteBufferOutputStream; | ||
| import software.amazon.smithy.java.io.datastream.DataStream; | ||
| import software.amazon.smithy.java.io.datastream.GzipInputStream; | ||
|
|
||
| public class Gzip implements CompressionAlgorithm { | ||
| @Override | ||
| public String algorithmId() { | ||
| return "gzip"; | ||
| } | ||
|
|
||
| @Override | ||
| public DataStream compress(DataStream data) { | ||
| if (!data.hasKnownLength()) { // Using streaming | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not always stream?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two different handling between streaming and non-streaming in request compression SEP, will discuss with Michael on this. |
||
| try { | ||
| return DataStream.ofInputStream( | ||
| new GzipInputStream(data.asInputStream()), | ||
| data.contentType(), | ||
| -1); | ||
| } catch (IOException e) { | ||
| throw new UncheckedIOException(e); | ||
| } | ||
| } | ||
|
|
||
| try (var bos = new ByteBufferOutputStream(); | ||
| var gzip = new GZIPOutputStream(bos); | ||
| var in = data.asInputStream()) { | ||
| in.transferTo(gzip); | ||
| gzip.close(); | ||
| return DataStream.ofBytes(bos.toByteBuffer().array()); | ||
| } catch (IOException e) { | ||
| throw new UncheckedIOException(e); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package software.amazon.smithy.java.client.http.plugins; | ||
|
|
||
| import java.util.List; | ||
| import software.amazon.smithy.java.client.core.AutoClientPlugin; | ||
| import software.amazon.smithy.java.client.core.ClientConfig; | ||
| import software.amazon.smithy.java.client.core.interceptors.ClientInterceptor; | ||
| import software.amazon.smithy.java.client.core.interceptors.RequestHook; | ||
| import software.amazon.smithy.java.client.http.HttpContext; | ||
| import software.amazon.smithy.java.client.http.HttpMessageExchange; | ||
| import software.amazon.smithy.java.client.http.compression.CompressionAlgorithm; | ||
| import software.amazon.smithy.java.client.http.compression.Gzip; | ||
| import software.amazon.smithy.java.context.Context; | ||
| import software.amazon.smithy.java.core.schema.TraitKey; | ||
| import software.amazon.smithy.java.http.api.HttpRequest; | ||
| import software.amazon.smithy.java.io.datastream.DataStream; | ||
| import software.amazon.smithy.model.traits.RequestCompressionTrait; | ||
| import software.amazon.smithy.utils.ListUtils; | ||
| import software.amazon.smithy.utils.SmithyInternalApi; | ||
|
|
||
| /** | ||
| * Compress the request body using provided compression algorithm if @requestCompression trait is applied. | ||
| */ | ||
| @SmithyInternalApi | ||
| public final class RequestCompressionPlugin implements AutoClientPlugin { | ||
|
|
||
| @Override | ||
| public void configureClient(ClientConfig.Builder config) { | ||
| if (config.isUsingMessageExchange(HttpMessageExchange.INSTANCE)) { | ||
| config.addInterceptor(RequestCompressionInterceptor.INSTANCE); | ||
| config.putConfigIfAbsent(HttpContext.DISABLE_REQUEST_COMPRESSION, false); | ||
| } | ||
| } | ||
|
|
||
| static final class RequestCompressionInterceptor implements ClientInterceptor { | ||
|
|
||
| private static final int DEFAULT_MIN_COMPRESSION_SIZE_BYTES = 10240; | ||
| private static final int COMPRESSION_SIZE_CAP = 10485760; | ||
| private static final String CONTENT_ENCODING_HEADER = "Content-Encoding"; | ||
| private static final ClientInterceptor INSTANCE = new RequestCompressionInterceptor(); | ||
| private static final TraitKey<RequestCompressionTrait> REQUEST_COMPRESSION_TRAIT_KEY = | ||
| TraitKey.get(RequestCompressionTrait.class); | ||
| // Currently only Gzip is supported in Smithy model: https://smithy.io/2.0/spec/behavior-traits.html#requestcompression-trait | ||
| private static final List<CompressionAlgorithm> supportedAlgorithms = ListUtils.of(new Gzip()); | ||
|
|
||
| @Override | ||
| public <RequestT> RequestT modifyBeforeTransmit(RequestHook<?, ?, RequestT> hook) { | ||
| return hook.mapRequest(HttpRequest.class, RequestCompressionInterceptor::processRequest); | ||
| } | ||
|
|
||
| private static HttpRequest processRequest(RequestHook<?, ?, HttpRequest> hook) { | ||
| if (shouldCompress(hook)) { | ||
| RequestCompressionTrait compressionTrait = | ||
| hook.operation().schema().getTrait(REQUEST_COMPRESSION_TRAIT_KEY); | ||
| var request = hook.request(); | ||
| // Will pick the first supported algorithm to compress the body. | ||
| for (String algorithmId : compressionTrait.getEncodings()) { | ||
| for (CompressionAlgorithm algorithm : supportedAlgorithms) { | ||
| if (algorithmId.equals(algorithm.algorithmId())) { | ||
| var compressed = algorithm.compress(request.body()); | ||
| return request.toBuilder() | ||
| .body(compressed) | ||
| .withAddedHeader(CONTENT_ENCODING_HEADER, algorithmId) | ||
| .build(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return hook.request(); | ||
| } | ||
|
|
||
| private static boolean shouldCompress(RequestHook<?, ?, HttpRequest> hook) { | ||
| var context = hook.context(); | ||
| var operation = hook.operation(); | ||
| if (!operation.schema().hasTrait(REQUEST_COMPRESSION_TRAIT_KEY) | ||
| || context.getOrDefault(HttpContext.DISABLE_REQUEST_COMPRESSION, false)) { | ||
| return false; | ||
| } | ||
| var requestBody = hook.request().body(); | ||
| // Streaming should not have known length | ||
| if (operation.inputStreamMember() != null && !requestBody.hasKnownLength()) { | ||
| return true; | ||
| } | ||
| return isBodySizeValid(requestBody, context); | ||
| } | ||
|
|
||
| private static boolean isBodySizeValid(DataStream requestBody, Context context) { | ||
| var minCompressionSize = context.getOrDefault(HttpContext.REQUEST_MIN_COMPRESSION_SIZE_BYTES, | ||
| DEFAULT_MIN_COMPRESSION_SIZE_BYTES); | ||
| validateCompressionSize(minCompressionSize); | ||
| return requestBody.contentLength() >= minCompressionSize; | ||
| } | ||
|
|
||
| private static void validateCompressionSize(int minCompressionSize) { | ||
| if (minCompressionSize < 0 || minCompressionSize > COMPRESSION_SIZE_CAP) { | ||
| throw new IllegalArgumentException("Min compression size must be between 0 and 10485760"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the upper limit?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The request compression SEP required the minimum length should be between 0 and 10485760 bytes. |
||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| software.amazon.smithy.java.client.http.plugins.UserAgentPlugin | ||
| software.amazon.smithy.java.client.http.plugins.ApplyHttpRetryInfoPlugin | ||
| software.amazon.smithy.java.client.http.plugins.RequestCompressionPlugin | ||
| software.amazon.smithy.java.client.http.plugins.HttpChecksumPlugin |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package software.amazon.smithy.java.client.http.compression; | ||
|
|
||
| import static org.hamcrest.MatcherAssert.assertThat; | ||
| import static org.hamcrest.Matchers.equalTo; | ||
| import static org.hamcrest.Matchers.lessThan; | ||
|
|
||
| import java.io.ByteArrayInputStream; | ||
| import java.io.ByteArrayOutputStream; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.zip.GZIPInputStream; | ||
| import org.junit.jupiter.api.Test; | ||
| import software.amazon.smithy.java.io.datastream.DataStream; | ||
|
|
||
| public class GzipTest { | ||
|
|
||
| private final Gzip gzip = new Gzip(); | ||
|
|
||
| @Test | ||
| public void algorithmIdReturnsGzip() { | ||
| assertThat(gzip.algorithmId(), equalTo("gzip")); | ||
| } | ||
|
|
||
| @Test | ||
| public void compressesKnownLengthData() throws Exception { | ||
| // Use larger, repetitive data that compresses well | ||
| String original = "Hello World! ".repeat(10); | ||
| DataStream input = DataStream.ofString(original); | ||
|
|
||
| DataStream compressed = gzip.compress(input); | ||
|
|
||
| // Verify compressed data is smaller | ||
| assertThat(compressed.contentLength(), lessThan((long) original.length())); | ||
|
|
||
| // Verify decompression produces original | ||
| String decompressed = decompress(compressed.asByteBuffer().array()); | ||
| assertThat(decompressed, equalTo(original)); | ||
| } | ||
|
|
||
| @Test | ||
| public void compressesLargeStreamInChunks() throws Exception { | ||
| // Create 100KB of data | ||
| StringBuilder sb = new StringBuilder(); | ||
| for (int i = 0; i < 10000; i++) { | ||
| sb.append("0123456789"); | ||
| } | ||
| String original = sb.toString(); | ||
| byte[] bytes = original.getBytes(StandardCharsets.UTF_8); | ||
| DataStream input = DataStream.ofInputStream(new ByteArrayInputStream(bytes)); | ||
|
|
||
| DataStream compressed = gzip.compress(input); | ||
|
|
||
| // Verify decompression produces original | ||
| byte[] compressedBytes = compressed.asInputStream().readAllBytes(); | ||
| String decompressed = decompress(compressedBytes); | ||
| assertThat(decompressed, equalTo(original)); | ||
| } | ||
|
|
||
| @Test | ||
| public void compressesEmptyData() throws Exception { | ||
| DataStream input = DataStream.ofString(""); | ||
|
|
||
| DataStream compressed = gzip.compress(input); | ||
|
|
||
| String decompressed = decompress(compressed.asByteBuffer().array()); | ||
| assertThat(decompressed, equalTo("")); | ||
| } | ||
|
|
||
| private String decompress(byte[] compressed) throws Exception { | ||
| try (GZIPInputStream gzipIn = new GZIPInputStream(new ByteArrayInputStream(compressed)); | ||
| ByteArrayOutputStream out = new ByteArrayOutputStream()) { | ||
| byte[] buffer = new byte[1024]; | ||
| int len; | ||
| while ((len = gzipIn.read(buffer)) > 0) { | ||
| out.write(buffer, 0, len); | ||
| } | ||
| return out.toString(StandardCharsets.UTF_8); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't mind adding a new SPI for this