Skip to content

Commit 6a44581

Browse files
committed
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
1 parent 7881691 commit 6a44581

File tree

12 files changed

+314
-40
lines changed

12 files changed

+314
-40
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"type": "bugfix",
4+
"description": "Defer all errors raised when creating `ProfileCredentialsProvider` to the `resolveCredentials()` call."
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"type": "feature",
4+
"description": "Never initialize the default credentials provider chain if credentials are always specified in the client builder."
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"type": "feature",
4+
"description": "Never initialie the default region provider chain if the region is always specified in the client builder."
5+
}

core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package software.amazon.awssdk.auth.credentials;
1717

1818
import software.amazon.awssdk.annotations.SdkPublicApi;
19+
import software.amazon.awssdk.auth.credentials.internal.LazyAwsCredentialsProvider;
1920
import software.amazon.awssdk.utils.SdkAutoCloseable;
2021
import software.amazon.awssdk.utils.ToString;
2122

@@ -41,7 +42,7 @@ public final class DefaultCredentialsProvider implements AwsCredentialsProvider,
4142

4243
private static final DefaultCredentialsProvider DEFAULT_CREDENTIALS_PROVIDER = new DefaultCredentialsProvider(builder());
4344

44-
private final AwsCredentialsProviderChain providerChain;
45+
private final LazyAwsCredentialsProvider providerChain;
4546

4647
/**
4748
* @see #builder()
@@ -61,23 +62,28 @@ public static DefaultCredentialsProvider create() {
6162
/**
6263
* Create the default credential chain using the configuration in the provided builder.
6364
*/
64-
private static AwsCredentialsProviderChain createChain(Builder builder) {
65-
AwsCredentialsProvider[] credentialsProviders = new AwsCredentialsProvider[] {
66-
SystemPropertyCredentialsProvider.create(),
67-
EnvironmentVariableCredentialsProvider.create(),
68-
ProfileCredentialsProvider.create(),
69-
ContainerCredentialsProvider.builder()
70-
.asyncCredentialUpdateEnabled(builder.asyncCredentialUpdateEnabled)
71-
.build(),
72-
InstanceProfileCredentialsProvider.builder()
73-
.asyncCredentialUpdateEnabled(builder.asyncCredentialUpdateEnabled)
74-
.build()
75-
};
76-
77-
return AwsCredentialsProviderChain.builder()
78-
.reuseLastProviderEnabled(builder.reuseLastProviderEnabled)
79-
.credentialsProviders(credentialsProviders)
80-
.build();
65+
private static LazyAwsCredentialsProvider createChain(Builder builder) {
66+
boolean asyncCredentialUpdateEnabled = builder.asyncCredentialUpdateEnabled;
67+
boolean reuseLastProviderEnabled = builder.reuseLastProviderEnabled;
68+
69+
return LazyAwsCredentialsProvider.create(() -> {
70+
AwsCredentialsProvider[] credentialsProviders = new AwsCredentialsProvider[] {
71+
SystemPropertyCredentialsProvider.create(),
72+
EnvironmentVariableCredentialsProvider.create(),
73+
ProfileCredentialsProvider.create(),
74+
ContainerCredentialsProvider.builder()
75+
.asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled)
76+
.build(),
77+
InstanceProfileCredentialsProvider.builder()
78+
.asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled)
79+
.build()
80+
};
81+
82+
return AwsCredentialsProviderChain.builder()
83+
.reuseLastProviderEnabled(reuseLastProviderEnabled)
84+
.credentialsProviders(credentialsProviders)
85+
.build();
86+
});
8187
}
8288

8389
/**

core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,47 @@ public final class ProfileCredentialsProvider implements AwsCredentialsProvider,
5252
* @see #builder()
5353
*/
5454
private ProfileCredentialsProvider(BuilderImpl builder) {
55-
this.profileName = builder.profileName != null ? builder.profileName
56-
: ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow();
57-
58-
// Load the profiles file
59-
this.profileFile = Optional.ofNullable(builder.profileFile)
60-
.orElseGet(builder.defaultProfileFileLoader);
61-
62-
// Load the profile and credentials provider
63-
this.credentialsProvider = profileFile.profile(profileName)
64-
.flatMap(p -> new ProfileCredentialsUtils(p, profileFile::profile)
65-
.credentialsProvider())
66-
.orElse(null);
67-
68-
// If we couldn't load the credentials provider for some reason, save an exception describing why. This exception will
69-
// only be raised on calls to getCredentials. We don't want to raise an exception here because it may be expected (eg. in
70-
// the default credential chain).
71-
if (credentialsProvider == null) {
72-
String loadError = String.format("Profile file contained no credentials for profile '%s': %s",
73-
profileName, profileFile);
74-
this.loadException = SdkClientException.builder().message(loadError).build();
55+
AwsCredentialsProvider credentialsProvider = null;
56+
RuntimeException loadException = null;
57+
ProfileFile profileFile = null;
58+
String profileName = null;
59+
60+
try {
61+
profileName = builder.profileName != null ? builder.profileName
62+
: ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow();
63+
64+
// Load the profiles file
65+
profileFile = Optional.ofNullable(builder.profileFile)
66+
.orElseGet(builder.defaultProfileFileLoader);
67+
68+
// Load the profile and credentials provider
69+
String finalProfileName = profileName;
70+
ProfileFile finalProfileFile = profileFile;
71+
credentialsProvider =
72+
profileFile.profile(profileName)
73+
.flatMap(p -> new ProfileCredentialsUtils(p, finalProfileFile::profile).credentialsProvider())
74+
.orElseThrow(() -> {
75+
String errorMessage = String.format("Profile file contained no credentials for " +
76+
"profile '%s': %s", finalProfileName, finalProfileFile);
77+
return SdkClientException.builder().message(errorMessage).build();
78+
});
79+
} catch (RuntimeException e) {
80+
// If we couldn't load the credentials provider for some reason, save an exception describing why. This exception
81+
// will only be raised on calls to getCredentials. We don't want to raise an exception here because it may be
82+
// expected (eg. in the default credential chain).
83+
loadException = e;
84+
}
85+
86+
if (loadException != null) {
87+
this.loadException = loadException;
88+
this.credentialsProvider = null;
89+
this.profileFile = null;
90+
this.profileName = null;
7591
} else {
7692
this.loadException = null;
93+
this.credentialsProvider = credentialsProvider;
94+
this.profileFile = profileFile;
95+
this.profileName = profileName;
7796
}
7897
}
7998

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.auth.credentials.internal;
17+
18+
import java.util.function.Supplier;
19+
import software.amazon.awssdk.annotations.SdkInternalApi;
20+
import software.amazon.awssdk.auth.credentials.AwsCredentials;
21+
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
22+
import software.amazon.awssdk.utils.IoUtils;
23+
import software.amazon.awssdk.utils.SdkAutoCloseable;
24+
import software.amazon.awssdk.utils.ToString;
25+
26+
/**
27+
* A wrapper for {@link AwsCredentialsProvider} that defers creation of the underlying provider until the first time the
28+
* {@link AwsCredentialsProvider#resolveCredentials()} method is invoked.
29+
*/
30+
@SdkInternalApi
31+
public class LazyAwsCredentialsProvider implements AwsCredentialsProvider, SdkAutoCloseable {
32+
private final Supplier<AwsCredentialsProvider> delegateConstructor;
33+
private volatile AwsCredentialsProvider delegate;
34+
35+
private LazyAwsCredentialsProvider(Supplier<AwsCredentialsProvider> delegateConstructor) {
36+
this.delegateConstructor = delegateConstructor;
37+
}
38+
39+
public static LazyAwsCredentialsProvider create(Supplier<AwsCredentialsProvider> delegateConstructor) {
40+
return new LazyAwsCredentialsProvider(delegateConstructor);
41+
}
42+
43+
@Override
44+
public AwsCredentials resolveCredentials() {
45+
if (delegate == null) {
46+
synchronized (this) {
47+
if (delegate == null) {
48+
delegate = delegateConstructor.get();
49+
}
50+
}
51+
}
52+
return delegate.resolveCredentials();
53+
}
54+
55+
@Override
56+
public void close() {
57+
IoUtils.closeIfCloseable(delegate, null);
58+
}
59+
60+
@Override
61+
public String toString() {
62+
return ToString.builder("LazyAwsCredentialsProvider")
63+
.add("delegateConstructor", delegateConstructor)
64+
.add("delegate", delegate)
65+
.build();
66+
}
67+
}

core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@
2727
* Verify functionality of {@link ProfileCredentialsProvider}.
2828
*/
2929
public class ProfileCredentialsProviderTest {
30+
@Test
31+
public void missingCredentialsFileThrowsExceptionInGetCredentials() {
32+
ProfileCredentialsProvider provider =
33+
new ProfileCredentialsProvider.BuilderImpl()
34+
.defaultProfileFileLoader(() -> { throw new IllegalStateException(); })
35+
.build();
36+
37+
assertThatThrownBy(provider::resolveCredentials).isInstanceOf(IllegalStateException.class);
38+
}
39+
3040
@Test
3141
public void missingProfileFileThrowsExceptionInGetCredentials() {
3242
ProfileCredentialsProvider provider =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.auth.credentials.internal;
17+
18+
import java.util.function.Supplier;
19+
import org.junit.Before;
20+
import org.junit.Test;
21+
import org.mockito.Mockito;
22+
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
23+
24+
public class LazyAwsCredentialsProviderTest {
25+
@SuppressWarnings("unchecked")
26+
private Supplier<AwsCredentialsProvider> credentialsConstructor = Mockito.mock(Supplier.class);
27+
28+
private AwsCredentialsProvider credentials = Mockito.mock(AwsCredentialsProvider.class);
29+
30+
@Before
31+
public void reset() {
32+
Mockito.reset(credentials, credentialsConstructor);
33+
Mockito.when(credentialsConstructor.get()).thenReturn(credentials);
34+
}
35+
36+
@Test
37+
public void creationDoesntInvokeSupplier() {
38+
LazyAwsCredentialsProvider.create(credentialsConstructor);
39+
Mockito.verifyZeroInteractions(credentialsConstructor);
40+
}
41+
42+
@Test
43+
public void resolveCredentialsInvokesSupplierExactlyOnce() {
44+
LazyAwsCredentialsProvider credentialsProvider = LazyAwsCredentialsProvider.create(credentialsConstructor);
45+
credentialsProvider.resolveCredentials();
46+
credentialsProvider.resolveCredentials();
47+
48+
Mockito.verify(credentialsConstructor, Mockito.times(1)).get();
49+
Mockito.verify(credentials, Mockito.times(2)).resolveCredentials();
50+
}
51+
}

core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import software.amazon.awssdk.regions.ServiceMetadata;
3636
import software.amazon.awssdk.regions.providers.AwsRegionProvider;
3737
import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
38+
import software.amazon.awssdk.regions.providers.LazyAwsRegionProvider;
3839
import software.amazon.awssdk.utils.AttributeMap;
3940

4041
/**
@@ -60,7 +61,8 @@ public abstract class AwsDefaultClientBuilder<BuilderT extends AwsClientBuilder<
6061
extends SdkDefaultClientBuilder<BuilderT, ClientT>
6162
implements AwsClientBuilder<BuilderT, ClientT> {
6263
private static final String DEFAULT_ENDPOINT_PROTOCOL = "https";
63-
private static final AwsRegionProvider DEFAULT_REGION_PROVIDER = new DefaultAwsRegionProviderChain();
64+
private static final AwsRegionProvider DEFAULT_REGION_PROVIDER =
65+
new LazyAwsRegionProvider(DefaultAwsRegionProviderChain::new);
6466

6567
protected AwsDefaultClientBuilder() {
6668
super();
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.regions.providers;
17+
18+
import java.util.function.Supplier;
19+
import software.amazon.awssdk.annotations.SdkProtectedApi;
20+
import software.amazon.awssdk.regions.Region;
21+
import software.amazon.awssdk.utils.ToString;
22+
23+
/**
24+
* A wrapper for {@link AwsRegionProvider} that defers creation of the underlying provider until the first time the
25+
* {@link AwsRegionProvider#getRegion()} method is invoked.
26+
*/
27+
@SdkProtectedApi
28+
public class LazyAwsRegionProvider implements AwsRegionProvider {
29+
private final Supplier<AwsRegionProvider> delegateConstructor;
30+
private volatile AwsRegionProvider delegate;
31+
32+
public LazyAwsRegionProvider(Supplier<AwsRegionProvider> delegateConstructor) {
33+
this.delegateConstructor = delegateConstructor;
34+
}
35+
36+
@Override
37+
public Region getRegion() {
38+
if (delegate == null) {
39+
synchronized (this) {
40+
if (delegate == null) {
41+
delegate = delegateConstructor.get();
42+
}
43+
}
44+
}
45+
return delegate.getRegion();
46+
}
47+
48+
@Override
49+
public String toString() {
50+
return ToString.builder("LazyAwsRegionProvider")
51+
.add("delegateConstructor", delegateConstructor)
52+
.add("delegate", delegate)
53+
.build();
54+
}
55+
}

0 commit comments

Comments
 (0)