Skip to content

Commit f1fd88e

Browse files
authored
Reduced resource consumption of async credential providers. (#3275)
* Reduced resource consumption of async credential providers. 1. Share thread pools across async credential providers (anything using CachedSupplier's NonBlocking prefetch strategy). 2. Log a warning if an extreme number of concurrent refreshes are happening, to help users detect when they're not closing their credential providers. Even though this is an increase in resource sharing, it should not cause increased availability risks. Because these threads are only used for background refreshes, if one particular type of credential provider has availability problems (e.g. SSO or STS high latency), it only disables background refreshes, not prefetches or synchronous fetches.
1 parent f71e9d9 commit f1fd88e

21 files changed

+546
-172
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Share background refresh threads across async credential providers to reduce base SDK resource consumption."
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Log a warning when an extreme number of async credential providers are running in parallel, because it could indicate that the user is not closing their clients or credential providers when they are done using them."
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Jitter credential provider cache refresh times."
6+
}

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,11 @@
2828
public interface HttpCredentialsProvider extends AwsCredentialsProvider, SdkAutoCloseable {
2929
interface Builder<TypeToBuildT extends HttpCredentialsProvider, BuilderT extends Builder<?, ?>> {
3030
/**
31-
* Configure whether this provider should fetch credentials asynchronously in the background. If this is true, threads are
32-
* less likely to block when {@link #resolveCredentials()} is called, but additional resources are used to maintain the
33-
* provider.
31+
* Configure whether the provider should fetch credentials asynchronously in the background. If this is true,
32+
* threads are less likely to block when credentials are loaded, but additional resources are used to maintain
33+
* the provider.
3434
*
35-
* <p>
36-
* By default, this is disabled.
35+
* <p>By default, this is disabled.</p>
3736
*/
3837
BuilderT asyncCredentialUpdateEnabled(Boolean asyncCredentialUpdateEnabled);
3938

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

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
package software.amazon.awssdk.auth.credentials;
1717

1818
import static java.time.temporal.ChronoUnit.MINUTES;
19-
import static java.time.temporal.ChronoUnit.SECONDS;
20-
import static software.amazon.awssdk.utils.ComparableUtils.minimum;
19+
import static software.amazon.awssdk.utils.ComparableUtils.maximum;
2120

2221
import java.io.IOException;
2322
import java.net.URI;
2423
import java.time.Clock;
24+
import java.time.Duration;
2525
import java.time.Instant;
2626
import java.util.Collections;
2727
import java.util.Map;
@@ -151,7 +151,7 @@ private RefreshResult<AwsCredentials> refreshCredentials() {
151151
// Choose whether to report this failure at the debug or warn level based on how much time is left on the
152152
// credentials before expiration.
153153
Supplier<String> errorMessage = () -> "Failure encountered when attempting to refresh credentials from IMDS.";
154-
Instant fifteenMinutesFromNow = Instant.now().plus(15, MINUTES);
154+
Instant fifteenMinutesFromNow = clock.instant().plus(15, MINUTES);
155155
if (expiration.isBefore(fifteenMinutesFromNow)) {
156156
log.warn(errorMessage, e);
157157
} else {
@@ -164,7 +164,7 @@ private RefreshResult<AwsCredentials> refreshCredentials() {
164164
}
165165

166166
return RefreshResult.builder(credentials.getAwsCredentials())
167-
.staleTime(null) // Allow use of expired credentials - they may still work
167+
.staleTime(Instant.MAX) // Allow use of expired credentials - they may still work
168168
.prefetchTime(prefetchTime(credentials.getExpiration().orElse(null)))
169169
.build();
170170
}
@@ -180,35 +180,18 @@ private boolean isLocalCredentialLoadingDisabled() {
180180
private Instant prefetchTime(Instant expiration) {
181181
Instant now = clock.instant();
182182

183-
// If expiration time doesn't exist, refresh in 60 minutes
184183
if (expiration == null) {
185184
return now.plus(60, MINUTES);
186185
}
187186

188-
// If expiration time is 60+ minutes from now, refresh in 30 minutes.
189-
Instant sixtyMinutesBeforeExpiration = expiration.minus(60, MINUTES);
190-
if (now.isBefore(sixtyMinutesBeforeExpiration)) {
191-
return now.plus(30, MINUTES);
187+
Duration timeUntilExpiration = Duration.between(now, expiration);
188+
if (timeUntilExpiration.isNegative()) {
189+
log.warn(() -> "IMDS credential expiration has been extended due to an IMDS availability outage. A refresh "
190+
+ "of these credentials will be attempted again in ~5 minutes.");
191+
return now.plus(5, MINUTES);
192192
}
193193

194-
// If expiration time is 15 minutes or more from now, refresh in 10 minutes.
195-
Instant fifteenMinutesBeforeExpiration = expiration.minus(15, MINUTES);
196-
if (now.isBefore(fifteenMinutesBeforeExpiration)) {
197-
return now.plus(10, MINUTES);
198-
}
199-
200-
// If expiration time is 0.25-15 minutes from now, refresh in 5 minutes, or 15 seconds before expiration, whichever is
201-
// sooner.
202-
Instant fifteenSecondsBeforeExpiration = expiration.minus(15, SECONDS);
203-
if (now.isBefore(fifteenSecondsBeforeExpiration)) {
204-
return minimum(now.plus(5, MINUTES), fifteenSecondsBeforeExpiration);
205-
}
206-
207-
// These credentials are expired. Try refreshing again in 5 minutes. We can't be more aggressive than that, because we
208-
// don't want to overload the IMDS endpoint.
209-
log.warn(() -> "IMDS credential expiration has been extended due to an IMDS availability outage. A refresh "
210-
+ "of these credentials will be attempted again in 5 minutes.");
211-
return now.plus(5, MINUTES);
194+
return now.plus(maximum(timeUntilExpiration.abs().dividedBy(2), Duration.ofMinutes(5)));
212195
}
213196

214197
@Override

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,9 @@ private Builder(ProcessCredentialsProvider provider) {
249249
}
250250

251251
/**
252-
* Configure whether the provider should fetch credentials asynchronously in the background. If this is true, threads are
253-
* less likely to block when credentials are loaded, but additional resources are used to maintain the provider.
252+
* Configure whether the provider should fetch credentials asynchronously in the background. If this is true,
253+
* threads are less likely to block when credentials are loaded, but additional resources are used to maintain
254+
* the provider.
254255
*
255256
* <p>By default, this is disabled.</p>
256257
*/

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

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import java.time.Instant;
4242
import java.time.ZoneId;
4343
import java.time.ZoneOffset;
44+
import java.time.temporal.ChronoUnit;
4445
import org.junit.AfterClass;
4546
import org.junit.Before;
4647
import org.junit.Rule;
@@ -361,22 +362,58 @@ public void resolveCredentials_callsImdsIfCredentialsWithin5MinutesOfExpiration(
361362
stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse1));
362363
AwsCredentials credentials24HoursAgo = credentialsProvider.resolveCredentials();
363364

364-
// Set the time to 3 minutes before expiration, and fail to call IMDS
365-
clock.time = now.minus(3, MINUTES);
365+
// Set the time to 10 minutes before expiration, and fail to call IMDS
366+
clock.time = now.minus(10, MINUTES);
366367
stubCredentialsResponse(aResponse().withStatus(500));
367-
AwsCredentials credentials3MinutesAgo = credentialsProvider.resolveCredentials();
368+
AwsCredentials credentials10MinutesAgo = credentialsProvider.resolveCredentials();
368369

369370
// Set the time to 10 seconds before expiration, and verify that we still call IMDS to try to get credentials in at the
370371
// last moment before expiration
371372
clock.time = now.minus(10, SECONDS);
372373
stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse2));
373374
AwsCredentials credentials10SecondsAgo = credentialsProvider.resolveCredentials();
374375

375-
assertThat(credentials24HoursAgo).isEqualTo(credentials3MinutesAgo);
376+
assertThat(credentials24HoursAgo).isEqualTo(credentials10MinutesAgo);
376377
assertThat(credentials24HoursAgo.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY");
377378
assertThat(credentials10SecondsAgo.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY2");
378379
}
379380

381+
@Test
382+
public void imdsCallFrequencyIsLimited() {
383+
// Requires running the test multiple times to account for refresh jitter
384+
for (int i = 0; i < 10; i++) {
385+
AdjustableClock clock = new AdjustableClock();
386+
AwsCredentialsProvider credentialsProvider = credentialsProviderWithClock(clock);
387+
Instant now = Instant.now();
388+
String successfulCredentialsResponse1 =
389+
"{"
390+
+ "\"AccessKeyId\":\"ACCESS_KEY_ID\","
391+
+ "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\","
392+
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(now) + '"'
393+
+ "}";
394+
395+
String successfulCredentialsResponse2 =
396+
"{"
397+
+ "\"AccessKeyId\":\"ACCESS_KEY_ID2\","
398+
+ "\"SecretAccessKey\":\"SECRET_ACCESS_KEY2\","
399+
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(now.plus(6, HOURS)) + '"'
400+
+ "}";
401+
402+
// Set the time to 5 minutes before expiration and call IMDS
403+
clock.time = now.minus(5, MINUTES);
404+
stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse1));
405+
AwsCredentials credentials5MinutesAgo = credentialsProvider.resolveCredentials();
406+
407+
// Set the time to 1 second before expiration, and verify that do not call IMDS because it hasn't been 5 minutes yet
408+
clock.time = now.minus(1, SECONDS);
409+
stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse2));
410+
AwsCredentials credentials1SecondsAgo = credentialsProvider.resolveCredentials();
411+
412+
assertThat(credentials5MinutesAgo).isEqualTo(credentials1SecondsAgo);
413+
assertThat(credentials5MinutesAgo.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY");
414+
}
415+
}
416+
380417
private AwsCredentialsProvider credentialsProviderWithClock(Clock clock) {
381418
InstanceProfileCredentialsProvider.BuilderImpl builder =
382419
(InstanceProfileCredentialsProvider.BuilderImpl) InstanceProfileCredentialsProvider.builder();

services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.time.Duration;
2121
import java.time.Instant;
2222
import java.util.Optional;
23-
import java.util.function.Consumer;
2423
import java.util.function.Supplier;
2524
import software.amazon.awssdk.annotations.SdkPublicApi;
2625
import software.amazon.awssdk.auth.credentials.AwsCredentials;
@@ -38,23 +37,16 @@
3837
import software.amazon.awssdk.utils.cache.RefreshResult;
3938

4039
/**
41-
* <p>
42-
* An implementation of {@link AwsCredentialsProvider} that is extended within this package to provide support for
43-
* periodically updating session credentials. This credential provider maintains a {@link Supplier<GetRoleCredentialsRequest>}
44-
* for a {@link SsoClient#getRoleCredentials(Consumer)} call to retrieve the credentials needed.
45-
* </p>
40+
* An implementation of {@link AwsCredentialsProvider} that periodically sends a {@link GetRoleCredentialsRequest} to the AWS
41+
* Single Sign-On Service to maintain short-lived sessions to use for authentication. These sessions are updated using a single
42+
* calling thread (by default) or asynchronously (if {@link Builder#asyncCredentialUpdateEnabled(Boolean)} is set).
4643
*
47-
* <p>
48-
* While creating the {@link GetRoleCredentialsRequest}, an access token is needed to be resolved from a token file.
49-
* In default, the token is assumed unexpired, and if it's expired then an {@link ExpiredTokenException} will be thrown.
50-
* If the users want to change the behavior of this, please implement your own token resolving logic and override the
51-
* {@link Builder#refreshRequest).
52-
* </p>
44+
* If the credentials are not successfully updated before expiration, calls to {@link #resolveCredentials()} will block until
45+
* they are updated successfully.
5346
*
54-
* <p>
55-
* When credentials get close to expiration, this class will attempt to update them asynchronously. If the credentials
56-
* end up expiring, this class will block all calls to {@link #resolveCredentials()} until the credentials can be updated.
57-
* </p>
47+
* Users of this provider must {@link #close()} it when they are finished using it.
48+
*
49+
* This is created using {@link SsoCredentialsProvider#builder()}.
5850
*/
5951
@SdkPublicApi
6052
public final class SsoCredentialsProvider implements AwsCredentialsProvider, SdkAutoCloseable,
@@ -186,7 +178,10 @@ public interface Builder extends CopyableBuilder<Builder, SsoCredentialsProvider
186178

187179
/**
188180
* Configure the amount of time, relative to SSO session token expiration, that the cached credentials are considered
189-
* close to stale and should be updated. See {@link #asyncCredentialUpdateEnabled}.
181+
* close to stale and should be updated.
182+
*
183+
* Prefetch updates will occur between the specified time and the stale time of the provider. Prefetch updates may be
184+
* asynchronous. See {@link #asyncCredentialUpdateEnabled}.
190185
*
191186
* <p>By default, this is 5 minutes.</p>
192187
*/

services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleCredentialsProvider.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@
3030

3131
/**
3232
* An implementation of {@link AwsCredentialsProvider} that periodically sends an {@link AssumeRoleRequest} to the AWS
33-
* Security Token Service to maintain short-lived sessions to use for authentication. These sessions are updated asynchronously
34-
* in the background as they get close to expiring. If the credentials are not successfully updated asynchronously in the
35-
* background, calls to {@link #resolveCredentials()} will begin to block in an attempt to update the credentials synchronously.
33+
* Security Token Service to maintain short-lived sessions to use for authentication. These sessions are updated using a single
34+
* calling thread (by default) or asynchronously (if {@link Builder#asyncCredentialUpdateEnabled(Boolean)} is set).
3635
*
37-
* This provider creates a thread in the background to periodically update credentials. If this provider is no longer needed,
38-
* the background thread can be shut down using {@link #close()}.
36+
* If the credentials are not successfully updated before expiration, calls to {@link #resolveCredentials()} will block until
37+
* they are updated successfully.
38+
*
39+
* Users of this provider must {@link #close()} it when they are finished using it.
3940
*
4041
* This is created using {@link StsAssumeRoleCredentialsProvider#builder()}.
4142
*/

services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithSamlCredentialsProvider.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@
2929
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
3030

3131
/**
32-
* An implementation of {@link AwsCredentialsProvider} that periodically sends a {@link AssumeRoleWithSamlRequest}
33-
* to the AWS Security Token Service to maintain short-lived sessions to use for authentication. These sessions are updated
34-
* asynchronously in the background as they get close to expiring. If the credentials are not successfully updated asynchronously
35-
* in the background, calls to {@link #resolveCredentials()} will begin to block in an attempt to update the credentials
36-
* synchronously.
32+
* An implementation of {@link AwsCredentialsProvider} that periodically sends an {@link AssumeRoleWithSamlRequest} to the AWS
33+
* Security Token Service to maintain short-lived sessions to use for authentication. These sessions are updated using a single
34+
* calling thread (by default) or asynchronously (if {@link Builder#asyncCredentialUpdateEnabled(Boolean)} is set).
3735
*
38-
* This provider creates a thread in the background to periodically update credentials. If this provider is no longer needed,
39-
* the background thread can be shut down using {@link #close()}.
36+
* If the credentials are not successfully updated before expiration, calls to {@link #resolveCredentials()} will block until
37+
* they are updated successfully.
38+
*
39+
* Users of this provider must {@link #close()} it when they are finished using it.
4040
*
4141
* This is created using {@link StsAssumeRoleWithSamlCredentialsProvider#builder()}.
4242
*/

services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithWebIdentityCredentialsProvider.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,16 @@
3030
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
3131

3232
/**
33-
* An implementation of {@link AwsCredentialsProvider} that periodically sends a {@link AssumeRoleWithWebIdentityRequest}
34-
* to the AWS Security Token Service to maintain short-lived sessions to use for authentication. These sessions are updated
35-
* asynchronously in the background as they get close to expiring. If the credentials are not successfully updated asynchronously
36-
* in the background, calls to {@link #resolveCredentials()} will begin to block in an attempt to update the credentials
37-
* synchronously.
33+
* An implementation of {@link AwsCredentialsProvider} that periodically sends an {@link AssumeRoleWithWebIdentityRequest} to the
34+
* AWS Security Token Service to maintain short-lived sessions to use for authentication. These sessions are updated using a
35+
* single calling thread (by default) or asynchronously (if {@link Builder#asyncCredentialUpdateEnabled(Boolean)} is set).
3836
*
39-
* This provider creates a thread in the background to periodically update credentials. If this provider is no longer needed,
40-
* the background thread can be shut down using {@link #close()}.
37+
* If the credentials are not successfully updated before expiration, calls to {@link #resolveCredentials()} will block until
38+
* they are updated successfully.
4139
*
42-
* This is created using {@link StsAssumeRoleWithWebIdentityCredentialsProvider#builder()}.
40+
* Users of this provider must {@link #close()} it when they are finished using it.
41+
*
42+
* This is created using {@link #builder()}.
4343
*/
4444
@SdkPublicApi
4545
@ThreadSafe

0 commit comments

Comments
 (0)