Skip to content

Reduced resource consumption of async credential providers. #3275

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

Merged
merged 10 commits into from
Jul 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-36b8c47.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "AWS SDK for Java v2",
"contributor": "",
"description": "Share background refresh threads across async credential providers to reduce base SDK resource consumption."
}
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-8f6d13b.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "AWS SDK for Java v2",
"contributor": "",
"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 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-d28e3ad.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "AWS SDK for Java v2",
"contributor": "",
"description": "Jitter credential provider cache refresh times."
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@
public interface HttpCredentialsProvider extends AwsCredentialsProvider, SdkAutoCloseable {
interface Builder<TypeToBuildT extends HttpCredentialsProvider, BuilderT extends Builder<?, ?>> {
/**
* Configure whether this provider should fetch credentials asynchronously in the background. If this is true, threads are
* less likely to block when {@link #resolveCredentials()} is called, but additional resources are used to maintain the
* provider.
* Configure whether the provider should fetch credentials asynchronously in the background. If this is true,
* threads are less likely to block when credentials are loaded, but additional resources are used to maintain
* the provider.
*
* <p>
* By default, this is disabled.
* <p>By default, this is disabled.</p>
*/
BuilderT asyncCredentialUpdateEnabled(Boolean asyncCredentialUpdateEnabled);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
package software.amazon.awssdk.auth.credentials;

import static java.time.temporal.ChronoUnit.MINUTES;
import static java.time.temporal.ChronoUnit.SECONDS;
import static software.amazon.awssdk.utils.ComparableUtils.minimum;
import static software.amazon.awssdk.utils.ComparableUtils.maximum;

import java.io.IOException;
import java.net.URI;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.Map;
Expand Down Expand Up @@ -151,7 +151,7 @@ private RefreshResult<AwsCredentials> refreshCredentials() {
// Choose whether to report this failure at the debug or warn level based on how much time is left on the
// credentials before expiration.
Supplier<String> errorMessage = () -> "Failure encountered when attempting to refresh credentials from IMDS.";
Instant fifteenMinutesFromNow = Instant.now().plus(15, MINUTES);
Instant fifteenMinutesFromNow = clock.instant().plus(15, MINUTES);
if (expiration.isBefore(fifteenMinutesFromNow)) {
log.warn(errorMessage, e);
} else {
Expand All @@ -164,7 +164,7 @@ private RefreshResult<AwsCredentials> refreshCredentials() {
}

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

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

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

// If expiration time is 15 minutes or more from now, refresh in 10 minutes.
Instant fifteenMinutesBeforeExpiration = expiration.minus(15, MINUTES);
if (now.isBefore(fifteenMinutesBeforeExpiration)) {
return now.plus(10, MINUTES);
}

// If expiration time is 0.25-15 minutes from now, refresh in 5 minutes, or 15 seconds before expiration, whichever is
// sooner.
Instant fifteenSecondsBeforeExpiration = expiration.minus(15, SECONDS);
if (now.isBefore(fifteenSecondsBeforeExpiration)) {
return minimum(now.plus(5, MINUTES), fifteenSecondsBeforeExpiration);
}

// These credentials are expired. Try refreshing again in 5 minutes. We can't be more aggressive than that, because we
// don't want to overload the IMDS endpoint.
log.warn(() -> "IMDS credential expiration has been extended due to an IMDS availability outage. A refresh "
+ "of these credentials will be attempted again in 5 minutes.");
return now.plus(5, MINUTES);
return now.plus(maximum(timeUntilExpiration.abs().dividedBy(2), Duration.ofMinutes(5)));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,9 @@ private Builder(ProcessCredentialsProvider provider) {
}

/**
* Configure whether the provider should fetch credentials asynchronously in the background. If this is true, threads are
* less likely to block when credentials are loaded, but additional resources are used to maintain the provider.
* Configure whether the provider should fetch credentials asynchronously in the background. If this is true,
* threads are less likely to block when credentials are loaded, but additional resources are used to maintain
* the provider.
*
* <p>By default, this is disabled.</p>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
Expand Down Expand Up @@ -361,22 +362,58 @@ public void resolveCredentials_callsImdsIfCredentialsWithin5MinutesOfExpiration(
stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse1));
AwsCredentials credentials24HoursAgo = credentialsProvider.resolveCredentials();

// Set the time to 3 minutes before expiration, and fail to call IMDS
clock.time = now.minus(3, MINUTES);
// Set the time to 10 minutes before expiration, and fail to call IMDS
clock.time = now.minus(10, MINUTES);
stubCredentialsResponse(aResponse().withStatus(500));
AwsCredentials credentials3MinutesAgo = credentialsProvider.resolveCredentials();
AwsCredentials credentials10MinutesAgo = credentialsProvider.resolveCredentials();

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

assertThat(credentials24HoursAgo).isEqualTo(credentials3MinutesAgo);
assertThat(credentials24HoursAgo).isEqualTo(credentials10MinutesAgo);
assertThat(credentials24HoursAgo.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY");
assertThat(credentials10SecondsAgo.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY2");
}

@Test
public void imdsCallFrequencyIsLimited() {
// Requires running the test multiple times to account for refresh jitter
for (int i = 0; i < 10; i++) {
AdjustableClock clock = new AdjustableClock();
AwsCredentialsProvider credentialsProvider = credentialsProviderWithClock(clock);
Instant now = Instant.now();
String successfulCredentialsResponse1 =
"{"
+ "\"AccessKeyId\":\"ACCESS_KEY_ID\","
+ "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\","
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(now) + '"'
+ "}";

String successfulCredentialsResponse2 =
"{"
+ "\"AccessKeyId\":\"ACCESS_KEY_ID2\","
+ "\"SecretAccessKey\":\"SECRET_ACCESS_KEY2\","
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(now.plus(6, HOURS)) + '"'
+ "}";

// Set the time to 5 minutes before expiration and call IMDS
clock.time = now.minus(5, MINUTES);
stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse1));
AwsCredentials credentials5MinutesAgo = credentialsProvider.resolveCredentials();

// Set the time to 1 second before expiration, and verify that do not call IMDS because it hasn't been 5 minutes yet
clock.time = now.minus(1, SECONDS);
stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse2));
AwsCredentials credentials1SecondsAgo = credentialsProvider.resolveCredentials();

assertThat(credentials5MinutesAgo).isEqualTo(credentials1SecondsAgo);
assertThat(credentials5MinutesAgo.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY");
}
}

private AwsCredentialsProvider credentialsProviderWithClock(Clock clock) {
InstanceProfileCredentialsProvider.BuilderImpl builder =
(InstanceProfileCredentialsProvider.BuilderImpl) InstanceProfileCredentialsProvider.builder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
Expand All @@ -38,23 +37,16 @@
import software.amazon.awssdk.utils.cache.RefreshResult;

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

/**
* Configure the amount of time, relative to SSO session token expiration, that the cached credentials are considered
* close to stale and should be updated. See {@link #asyncCredentialUpdateEnabled}.
* close to stale and should be updated.
*
* Prefetch updates will occur between the specified time and the stale time of the provider. Prefetch updates may be
* asynchronous. See {@link #asyncCredentialUpdateEnabled}.
*
* <p>By default, this is 5 minutes.</p>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@

/**
* An implementation of {@link AwsCredentialsProvider} that periodically sends an {@link AssumeRoleRequest} to the AWS
* Security Token Service to maintain short-lived sessions to use for authentication. These sessions are updated asynchronously
* in the background as they get close to expiring. If the credentials are not successfully updated asynchronously in the
* background, calls to {@link #resolveCredentials()} will begin to block in an attempt to update the credentials synchronously.
* Security Token Service to maintain short-lived sessions to use for authentication. These sessions are updated using a single
* calling thread (by default) or asynchronously (if {@link Builder#asyncCredentialUpdateEnabled(Boolean)} is set).
*
* This provider creates a thread in the background to periodically update credentials. If this provider is no longer needed,
* the background thread can be shut down using {@link #close()}.
* If the credentials are not successfully updated before expiration, calls to {@link #resolveCredentials()} will block until
* they are updated successfully.
*
* Users of this provider must {@link #close()} it when they are finished using it.
*
* This is created using {@link StsAssumeRoleCredentialsProvider#builder()}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;

/**
* An implementation of {@link AwsCredentialsProvider} that periodically sends a {@link AssumeRoleWithSamlRequest}
* to the AWS Security Token Service to maintain short-lived sessions to use for authentication. These sessions are updated
* asynchronously in the background as they get close to expiring. If the credentials are not successfully updated asynchronously
* in the background, calls to {@link #resolveCredentials()} will begin to block in an attempt to update the credentials
* synchronously.
* An implementation of {@link AwsCredentialsProvider} that periodically sends an {@link AssumeRoleWithSamlRequest} to the AWS
* Security Token Service to maintain short-lived sessions to use for authentication. These sessions are updated using a single
* calling thread (by default) or asynchronously (if {@link Builder#asyncCredentialUpdateEnabled(Boolean)} is set).
*
* This provider creates a thread in the background to periodically update credentials. If this provider is no longer needed,
* the background thread can be shut down using {@link #close()}.
* If the credentials are not successfully updated before expiration, calls to {@link #resolveCredentials()} will block until
* they are updated successfully.
*
* Users of this provider must {@link #close()} it when they are finished using it.
*
* This is created using {@link StsAssumeRoleWithSamlCredentialsProvider#builder()}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;

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