diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-5e4425a.json b/.changes/next-release/bugfix-AWSSDKforJavav2-5e4425a.json new file mode 100644 index 000000000000..b3bd0c0bb365 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-5e4425a.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Improve Sigv4 signing performance, particularly for RPC protocols" +} diff --git a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/V4CanonicalRequest.java b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/V4CanonicalRequest.java index ea14f356889e..ea9794da5470 100644 --- a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/V4CanonicalRequest.java +++ b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/V4CanonicalRequest.java @@ -202,21 +202,16 @@ public static String getCanonicalHeadersString(List>> * Get the string representing which headers are part of the signing process. Header names are separated by a semicolon. */ public static String getSignedHeadersString(List>> canonicalHeaders) { - String signedHeadersString; StringBuilder headersString = new StringBuilder(512); for (Pair> header : canonicalHeaders) { - headersString.append(header.left()).append(";"); + headersString.append(header.left()).append(';'); } - // get rid of trailing semicolon - signedHeadersString = headersString.toString(); - boolean trimTrailingSemicolon = signedHeadersString.length() > 1 && - signedHeadersString.endsWith(";"); - - if (trimTrailingSemicolon) { - signedHeadersString = signedHeadersString.substring(0, signedHeadersString.length() - 1); + if (headersString.length() > 0) { + headersString.setLength(headersString.length() - 1); // remove last semicolon } - return signedHeadersString; + + return headersString.toString(); } /** @@ -296,13 +291,17 @@ private static void addAndTrim(StringBuilder result, String value) { * If the path is empty, a single-forward slash ('/') is returned. */ private static String getCanonicalUri(SdkHttpRequest request, Options options) { - String path = options.normalizePath ? request.getUri().normalize().getRawPath() - : request.encodedPath(); - - if (StringUtils.isEmpty(path)) { + String path = request.encodedPath(); + if (StringUtils.isEmpty(path) || "/".equals(path)) { return "/"; } + if (options.normalizePath) { + path = request.getUri().normalize().getRawPath(); + } + + + if (options.doubleUrlEncode) { path = SdkHttpUtils.urlEncodeIgnoreSlashes(path); } @@ -329,6 +328,10 @@ private static String getCanonicalUri(SdkHttpRequest request, Options options) { * Get the sorted map of query parameters that are to be signed. */ private static SortedMap> getCanonicalQueryParams(SdkHttpRequest request) { + if (request.numRawQueryParameters() == 0) { + return Collections.emptySortedMap(); + } + SortedMap> sorted = new TreeMap<>(); // Signing protocol expects the param values also to be sorted after url diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/signer/Sigv4SignerBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/signer/Sigv4SignerBenchmark.java new file mode 100644 index 000000000000..3f949bc45d48 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/signer/Sigv4SignerBenchmark.java @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.benchmark.signer; + +import java.net.URI; +import java.time.Clock; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.auth.aws.internal.signer.CredentialScope; +import software.amazon.awssdk.http.auth.aws.internal.signer.V4Properties; +import software.amazon.awssdk.http.auth.aws.internal.signer.V4RequestSigner; +import software.amazon.awssdk.http.auth.aws.internal.signer.V4RequestSigningResult; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; + + +@State(Scope.Thread) +@Warmup(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 3, time = 10, timeUnit = TimeUnit.SECONDS) +@Fork(2) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class Sigv4SignerBenchmark { + SdkHttpRequest.Builder request; + V4RequestSigner siger; + + // Setup runs once per trial or iteration (not measured) + @Setup(Level.Iteration) + public void setup() { + URI target = URI.create("https://test.com/"); + request = SdkHttpRequest.builder() + .method(SdkHttpMethod.GET) + .uri(target) + .encodedPath(target.getPath()) + .putHeader("x-amz-content-sha256", "checksum") + .putHeader("x-amz-archive-description", "test test"); + AwsCredentialsIdentity creds = + AwsCredentialsIdentity.create("access", "secret"); + Clock clock = Clock.systemUTC(); + V4Properties properties = V4Properties.builder() + .credentials(creds) + .credentialScope(new CredentialScope("us-east-1", "demo", clock.instant())) + .signingClock(clock) + .doubleUrlEncode(true) + .normalizePath(true) + .build(); + + siger = V4RequestSigner.create(properties, "abc123"); + } + + @Benchmark + public void benchmarkSign(Blackhole blackhole) { + V4RequestSigningResult signingResult = siger.sign(request); + blackhole.consume(signingResult); + } +} \ No newline at end of file