From 6a44581e8283a6d738e8a36c97dde2e0488c8afa Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 7 Feb 2019 14:28:31 -0800 Subject: [PATCH] Make default region and credential loading lazy. The default credentials provider chain and region provider chains will no longer be loaded until they are first used. Further, the profile credentials provider will never raise an exception when it is created. The exception won't be raised until it is first used. Fixes #1030, #1014, #749 --- .../bugfix-AWSSDKforJavav2-d33383e.json | 5 ++ .../feature-AWSSDKforJavav2-63c04be.json | 5 ++ .../feature-AWSSDKforJavav2-6c062f1.json | 5 ++ .../DefaultCredentialsProvider.java | 42 +++++++----- .../ProfileCredentialsProvider.java | 59 ++++++++++------ .../internal/LazyAwsCredentialsProvider.java | 67 +++++++++++++++++++ .../ProfileCredentialsProviderTest.java | 10 +++ .../LazyAwsCredentialsProviderTest.java | 51 ++++++++++++++ .../builder/AwsDefaultClientBuilder.java | 4 +- .../providers/LazyAwsRegionProvider.java | 55 +++++++++++++++ .../providers/LazyAwsRegionProviderTest.java | 50 ++++++++++++++ .../client/config/SdkClientConfiguration.java | 1 - 12 files changed, 314 insertions(+), 40 deletions(-) create mode 100644 .changes/next-release/bugfix-AWSSDKforJavav2-d33383e.json create mode 100644 .changes/next-release/feature-AWSSDKforJavav2-63c04be.json create mode 100644 .changes/next-release/feature-AWSSDKforJavav2-6c062f1.json create mode 100644 core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java create mode 100644 core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProviderTest.java create mode 100644 core/regions/src/main/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProvider.java create mode 100644 core/regions/src/test/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProviderTest.java diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-d33383e.json b/.changes/next-release/bugfix-AWSSDKforJavav2-d33383e.json new file mode 100644 index 000000000000..7dc7df981a64 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-d33383e.json @@ -0,0 +1,5 @@ +{ + "category": "AWS SDK for Java v2", + "type": "bugfix", + "description": "Defer all errors raised when creating `ProfileCredentialsProvider` to the `resolveCredentials()` call." +} diff --git a/.changes/next-release/feature-AWSSDKforJavav2-63c04be.json b/.changes/next-release/feature-AWSSDKforJavav2-63c04be.json new file mode 100644 index 000000000000..c63668511add --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-63c04be.json @@ -0,0 +1,5 @@ +{ + "category": "AWS SDK for Java v2", + "type": "feature", + "description": "Never initialize the default credentials provider chain if credentials are always specified in the client builder." +} diff --git a/.changes/next-release/feature-AWSSDKforJavav2-6c062f1.json b/.changes/next-release/feature-AWSSDKforJavav2-6c062f1.json new file mode 100644 index 000000000000..c7cc0825cbe0 --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-6c062f1.json @@ -0,0 +1,5 @@ +{ + "category": "AWS SDK for Java v2", + "type": "feature", + "description": "Never initialie the default region provider chain if the region is always specified in the client builder." +} diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java index 91f428bdba9b..a8cbf236fbbb 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.auth.credentials; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.auth.credentials.internal.LazyAwsCredentialsProvider; import software.amazon.awssdk.utils.SdkAutoCloseable; import software.amazon.awssdk.utils.ToString; @@ -41,7 +42,7 @@ public final class DefaultCredentialsProvider implements AwsCredentialsProvider, private static final DefaultCredentialsProvider DEFAULT_CREDENTIALS_PROVIDER = new DefaultCredentialsProvider(builder()); - private final AwsCredentialsProviderChain providerChain; + private final LazyAwsCredentialsProvider providerChain; /** * @see #builder() @@ -61,23 +62,28 @@ public static DefaultCredentialsProvider create() { /** * Create the default credential chain using the configuration in the provided builder. */ - private static AwsCredentialsProviderChain createChain(Builder builder) { - AwsCredentialsProvider[] credentialsProviders = new AwsCredentialsProvider[] { - SystemPropertyCredentialsProvider.create(), - EnvironmentVariableCredentialsProvider.create(), - ProfileCredentialsProvider.create(), - ContainerCredentialsProvider.builder() - .asyncCredentialUpdateEnabled(builder.asyncCredentialUpdateEnabled) - .build(), - InstanceProfileCredentialsProvider.builder() - .asyncCredentialUpdateEnabled(builder.asyncCredentialUpdateEnabled) - .build() - }; - - return AwsCredentialsProviderChain.builder() - .reuseLastProviderEnabled(builder.reuseLastProviderEnabled) - .credentialsProviders(credentialsProviders) - .build(); + private static LazyAwsCredentialsProvider createChain(Builder builder) { + boolean asyncCredentialUpdateEnabled = builder.asyncCredentialUpdateEnabled; + boolean reuseLastProviderEnabled = builder.reuseLastProviderEnabled; + + return LazyAwsCredentialsProvider.create(() -> { + AwsCredentialsProvider[] credentialsProviders = new AwsCredentialsProvider[] { + SystemPropertyCredentialsProvider.create(), + EnvironmentVariableCredentialsProvider.create(), + ProfileCredentialsProvider.create(), + ContainerCredentialsProvider.builder() + .asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled) + .build(), + InstanceProfileCredentialsProvider.builder() + .asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled) + .build() + }; + + return AwsCredentialsProviderChain.builder() + .reuseLastProviderEnabled(reuseLastProviderEnabled) + .credentialsProviders(credentialsProviders) + .build(); + }); } /** diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java index ae5a919bdd13..fe13174f2c31 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java @@ -52,28 +52,47 @@ public final class ProfileCredentialsProvider implements AwsCredentialsProvider, * @see #builder() */ private ProfileCredentialsProvider(BuilderImpl builder) { - this.profileName = builder.profileName != null ? builder.profileName - : ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow(); - - // Load the profiles file - this.profileFile = Optional.ofNullable(builder.profileFile) - .orElseGet(builder.defaultProfileFileLoader); - - // Load the profile and credentials provider - this.credentialsProvider = profileFile.profile(profileName) - .flatMap(p -> new ProfileCredentialsUtils(p, profileFile::profile) - .credentialsProvider()) - .orElse(null); - - // If we couldn't load the credentials provider for some reason, save an exception describing why. This exception will - // only be raised on calls to getCredentials. We don't want to raise an exception here because it may be expected (eg. in - // the default credential chain). - if (credentialsProvider == null) { - String loadError = String.format("Profile file contained no credentials for profile '%s': %s", - profileName, profileFile); - this.loadException = SdkClientException.builder().message(loadError).build(); + AwsCredentialsProvider credentialsProvider = null; + RuntimeException loadException = null; + ProfileFile profileFile = null; + String profileName = null; + + try { + profileName = builder.profileName != null ? builder.profileName + : ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow(); + + // Load the profiles file + profileFile = Optional.ofNullable(builder.profileFile) + .orElseGet(builder.defaultProfileFileLoader); + + // Load the profile and credentials provider + String finalProfileName = profileName; + ProfileFile finalProfileFile = profileFile; + credentialsProvider = + profileFile.profile(profileName) + .flatMap(p -> new ProfileCredentialsUtils(p, finalProfileFile::profile).credentialsProvider()) + .orElseThrow(() -> { + String errorMessage = String.format("Profile file contained no credentials for " + + "profile '%s': %s", finalProfileName, finalProfileFile); + return SdkClientException.builder().message(errorMessage).build(); + }); + } catch (RuntimeException e) { + // If we couldn't load the credentials provider for some reason, save an exception describing why. This exception + // will only be raised on calls to getCredentials. We don't want to raise an exception here because it may be + // expected (eg. in the default credential chain). + loadException = e; + } + + if (loadException != null) { + this.loadException = loadException; + this.credentialsProvider = null; + this.profileFile = null; + this.profileName = null; } else { this.loadException = null; + this.credentialsProvider = credentialsProvider; + this.profileFile = profileFile; + this.profileName = profileName; } } diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java new file mode 100644 index 000000000000..54260eca2e8a --- /dev/null +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright 2010-2019 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.auth.credentials.internal; + +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.utils.IoUtils; +import software.amazon.awssdk.utils.SdkAutoCloseable; +import software.amazon.awssdk.utils.ToString; + +/** + * A wrapper for {@link AwsCredentialsProvider} that defers creation of the underlying provider until the first time the + * {@link AwsCredentialsProvider#resolveCredentials()} method is invoked. + */ +@SdkInternalApi +public class LazyAwsCredentialsProvider implements AwsCredentialsProvider, SdkAutoCloseable { + private final Supplier delegateConstructor; + private volatile AwsCredentialsProvider delegate; + + private LazyAwsCredentialsProvider(Supplier delegateConstructor) { + this.delegateConstructor = delegateConstructor; + } + + public static LazyAwsCredentialsProvider create(Supplier delegateConstructor) { + return new LazyAwsCredentialsProvider(delegateConstructor); + } + + @Override + public AwsCredentials resolveCredentials() { + if (delegate == null) { + synchronized (this) { + if (delegate == null) { + delegate = delegateConstructor.get(); + } + } + } + return delegate.resolveCredentials(); + } + + @Override + public void close() { + IoUtils.closeIfCloseable(delegate, null); + } + + @Override + public String toString() { + return ToString.builder("LazyAwsCredentialsProvider") + .add("delegateConstructor", delegateConstructor) + .add("delegate", delegate) + .build(); + } +} diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java index b8dbd8c86338..a1f36c7ae0cb 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java @@ -27,6 +27,16 @@ * Verify functionality of {@link ProfileCredentialsProvider}. */ public class ProfileCredentialsProviderTest { + @Test + public void missingCredentialsFileThrowsExceptionInGetCredentials() { + ProfileCredentialsProvider provider = + new ProfileCredentialsProvider.BuilderImpl() + .defaultProfileFileLoader(() -> { throw new IllegalStateException(); }) + .build(); + + assertThatThrownBy(provider::resolveCredentials).isInstanceOf(IllegalStateException.class); + } + @Test public void missingProfileFileThrowsExceptionInGetCredentials() { ProfileCredentialsProvider provider = diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProviderTest.java new file mode 100644 index 000000000000..f669402b1fac --- /dev/null +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProviderTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2010-2019 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.auth.credentials.internal; + +import java.util.function.Supplier; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; + +public class LazyAwsCredentialsProviderTest { + @SuppressWarnings("unchecked") + private Supplier credentialsConstructor = Mockito.mock(Supplier.class); + + private AwsCredentialsProvider credentials = Mockito.mock(AwsCredentialsProvider.class); + + @Before + public void reset() { + Mockito.reset(credentials, credentialsConstructor); + Mockito.when(credentialsConstructor.get()).thenReturn(credentials); + } + + @Test + public void creationDoesntInvokeSupplier() { + LazyAwsCredentialsProvider.create(credentialsConstructor); + Mockito.verifyZeroInteractions(credentialsConstructor); + } + + @Test + public void resolveCredentialsInvokesSupplierExactlyOnce() { + LazyAwsCredentialsProvider credentialsProvider = LazyAwsCredentialsProvider.create(credentialsConstructor); + credentialsProvider.resolveCredentials(); + credentialsProvider.resolveCredentials(); + + Mockito.verify(credentialsConstructor, Mockito.times(1)).get(); + Mockito.verify(credentials, Mockito.times(2)).resolveCredentials(); + } +} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java index 466ddad904a0..98b1ae31d582 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java @@ -35,6 +35,7 @@ import software.amazon.awssdk.regions.ServiceMetadata; import software.amazon.awssdk.regions.providers.AwsRegionProvider; import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; +import software.amazon.awssdk.regions.providers.LazyAwsRegionProvider; import software.amazon.awssdk.utils.AttributeMap; /** @@ -60,7 +61,8 @@ public abstract class AwsDefaultClientBuilder implements AwsClientBuilder { private static final String DEFAULT_ENDPOINT_PROTOCOL = "https"; - private static final AwsRegionProvider DEFAULT_REGION_PROVIDER = new DefaultAwsRegionProviderChain(); + private static final AwsRegionProvider DEFAULT_REGION_PROVIDER = + new LazyAwsRegionProvider(DefaultAwsRegionProviderChain::new); protected AwsDefaultClientBuilder() { super(); diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProvider.java b/core/regions/src/main/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProvider.java new file mode 100644 index 000000000000..85c239d559f9 --- /dev/null +++ b/core/regions/src/main/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProvider.java @@ -0,0 +1,55 @@ +/* + * Copyright 2010-2019 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.regions.providers; + +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.utils.ToString; + +/** + * A wrapper for {@link AwsRegionProvider} that defers creation of the underlying provider until the first time the + * {@link AwsRegionProvider#getRegion()} method is invoked. + */ +@SdkProtectedApi +public class LazyAwsRegionProvider implements AwsRegionProvider { + private final Supplier delegateConstructor; + private volatile AwsRegionProvider delegate; + + public LazyAwsRegionProvider(Supplier delegateConstructor) { + this.delegateConstructor = delegateConstructor; + } + + @Override + public Region getRegion() { + if (delegate == null) { + synchronized (this) { + if (delegate == null) { + delegate = delegateConstructor.get(); + } + } + } + return delegate.getRegion(); + } + + @Override + public String toString() { + return ToString.builder("LazyAwsRegionProvider") + .add("delegateConstructor", delegateConstructor) + .add("delegate", delegate) + .build(); + } +} diff --git a/core/regions/src/test/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProviderTest.java b/core/regions/src/test/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProviderTest.java new file mode 100644 index 000000000000..e2ff7246878f --- /dev/null +++ b/core/regions/src/test/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProviderTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2019 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.regions.providers; + +import java.util.function.Supplier; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class LazyAwsRegionProviderTest { + @SuppressWarnings("unchecked") + private Supplier regionProviderConstructor = Mockito.mock(Supplier.class); + + private AwsRegionProvider regionProvider = Mockito.mock(AwsRegionProvider.class); + + @Before + public void reset() { + Mockito.reset(regionProvider, regionProviderConstructor); + Mockito.when(regionProviderConstructor.get()).thenReturn(regionProvider); + } + + @Test + public void creationDoesntInvokeSupplier() { + new LazyAwsRegionProvider(regionProviderConstructor); + Mockito.verifyZeroInteractions(regionProviderConstructor); + } + + @Test + public void getRegionInvokesSupplierExactlyOnce() { + LazyAwsRegionProvider lazyRegionProvider = new LazyAwsRegionProvider(regionProviderConstructor); + lazyRegionProvider.getRegion(); + lazyRegionProvider.getRegion(); + + Mockito.verify(regionProviderConstructor, Mockito.times(1)).get(); + Mockito.verify(regionProvider, Mockito.times(2)).getRegion(); + } +} \ No newline at end of file diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java index 2ed45017c6d8..3372007d98aa 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java @@ -16,7 +16,6 @@ package software.amazon.awssdk.core.client.config; import java.util.function.Consumer; - import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.SdkAutoCloseable;