Skip to content
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

Perform background refresh of credentials during preempt expiry period #3541

Open
wants to merge 2 commits into
base: v4-development
Choose a base branch
from
Open
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
132 changes: 94 additions & 38 deletions sdk/src/Core/Amazon.Runtime/Credentials/RefreshingAWSCredentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@ public abstract class RefreshingAWSCredentials : AWSCredentials, IDisposable
/// </summary>
public class CredentialsRefreshState
{
public ImmutableCredentials Credentials
{
get;
set;
}
public ImmutableCredentials Credentials { get; set; }
public DateTime Expiration { get; set; }

public CredentialsRefreshState()
Expand All @@ -55,12 +51,18 @@ public CredentialsRefreshState(ImmutableCredentials credentials, DateTime expira

internal bool IsExpiredWithin(TimeSpan preemptExpiryTime)
{
#pragma warning disable CS0612,CS0618 // Type or member is obsolete
var now = AWSSDKUtils.CorrectedUtcNow;
#pragma warning restore CS0612,CS0618 // Type or member is obsolete
var exp = Expiration.ToUniversalTime();
return (now > exp - preemptExpiryTime);
}

internal TimeSpan GetTimeToLive(TimeSpan preemptExpiryTime)
{
var now = AWSSDKUtils.CorrectedUtcNow;
var exp = Expiration.ToUniversalTime();

return exp - now + preemptExpiryTime;
}
}

/// <summary>
Expand Down Expand Up @@ -112,46 +114,106 @@ public TimeSpan PreemptExpiryTime
/// <returns></returns>
public override ImmutableCredentials GetCredentials()
{
_updateGeneratedCredentialsSemaphore.Wait();
try
// We save the currentState as it might be modified or cleared.
var tempState = currentState;

var ttl = tempState?.GetTimeToLive(PreemptExpiryTime);

if (ttl > TimeSpan.Zero)
{
// We save the currentState as it might be modified or cleared.
var tempState = currentState;
// If credentials are expired or we don't have any state yet, update
if (ShouldUpdateState(tempState, PreemptExpiryTime))
if (ttl < PreemptExpiryTime)
{
tempState = GenerateNewCredentials();
UpdateToGeneratedCredentials(tempState, PreemptExpiryTime);
currentState = tempState;
// background refresh (fire & forget)
if (_updateGeneratedCredentialsSemaphore.Wait(0))
{
_ = System.Threading.Tasks.Task.Run(GenerateCredentialsAndUpdateState);
}
}
return tempState.Credentials.Copy();
}
finally
else
{
// If credentials are expired, update
_updateGeneratedCredentialsSemaphore.Wait();
tempState = GenerateCredentialsAndUpdateState();
}

return tempState.Credentials.Copy();

CredentialsRefreshState GenerateCredentialsAndUpdateState()
{
_updateGeneratedCredentialsSemaphore.Release();
System.Diagnostics.Debug.Assert(_updateGeneratedCredentialsSemaphore.CurrentCount == 0);

try
{
var tempState = currentState;
// double-check that the credentials still need updating
// as it's possible that multiple requests were queued acquiring the semaphore
if (ShouldUpdateState(tempState, PreemptExpiryTime))
{
tempState = GenerateNewCredentials();
UpdateToGeneratedCredentials(tempState, PreemptExpiryTime);
currentState = tempState;
}

return tempState;
}
finally
{
_updateGeneratedCredentialsSemaphore.Release();
}
}
}

#if AWS_ASYNC_API
public override async System.Threading.Tasks.Task<ImmutableCredentials> GetCredentialsAsync()
{
await _updateGeneratedCredentialsSemaphore.WaitAsync().ConfigureAwait(false);
try
// We save the currentState as it might be modified or cleared.
var tempState = currentState;

var ttl = tempState?.GetTimeToLive(PreemptExpiryTime);

if (ttl > TimeSpan.Zero)
{
// We save the currentState as it might be modified or cleared.
var tempState = currentState;
// If credentials are expired, update
if (ShouldUpdateState(tempState, PreemptExpiryTime))
if (ttl < PreemptExpiryTime)
{
tempState = await GenerateNewCredentialsAsync().ConfigureAwait(false);
UpdateToGeneratedCredentials(tempState, PreemptExpiryTime);
currentState = tempState;
// background refresh (fire & forget)
if (_updateGeneratedCredentialsSemaphore.Wait(0))
{
_ = GenerateCredentialsAndUpdateStateAsync();
}
}
return tempState.Credentials.Copy();
}
finally
else
{
_updateGeneratedCredentialsSemaphore.Release();
// If credentials are expired, update
await _updateGeneratedCredentialsSemaphore.WaitAsync().ConfigureAwait(false);
tempState = await GenerateCredentialsAndUpdateStateAsync().ConfigureAwait(false);
}

return tempState.Credentials.Copy();

async System.Threading.Tasks.Task<CredentialsRefreshState> GenerateCredentialsAndUpdateStateAsync()
{
System.Diagnostics.Debug.Assert(_updateGeneratedCredentialsSemaphore.CurrentCount == 0);

try
{
var tempState = currentState;
// double-check that the credentials still need updating
// as it's possible that multiple requests were queued acquiring the semaphore
if (ShouldUpdateState(tempState, PreemptExpiryTime))
{
tempState = await GenerateNewCredentialsAsync().ConfigureAwait(false);
UpdateToGeneratedCredentials(tempState, PreemptExpiryTime);
currentState = tempState;
}

return tempState;
}
finally
{
_updateGeneratedCredentialsSemaphore.Release();
}
}
}
#endif
Expand All @@ -174,9 +236,7 @@ private static void UpdateToGeneratedCredentials(CredentialsRefreshState state,
{
errorMessage = string.Format(CultureInfo.InvariantCulture,
"The retrieved credentials have already expired: Now = {0}, Credentials expiration = {1}",
#pragma warning disable CS0612,CS0618 // Type or member is obsolete
AWSSDKUtils.CorrectedUtcNow.ToLocalTime(), state.Expiration);
#pragma warning restore CS0612,CS0618 // Type or member is obsolete
}

throw new AmazonClientException(errorMessage);
Expand All @@ -193,9 +253,7 @@ private static void UpdateToGeneratedCredentials(CredentialsRefreshState state,
var logger = Logger.GetLogger(typeof(RefreshingAWSCredentials));
logger.InfoFormat(
"The preempt expiry time is set too high: Current time = {0}, Credentials expiry time = {1}, Preempt expiry time = {2}.",
#pragma warning disable CS0612,CS0618 // Type or member is obsolete
AWSSDKUtils.CorrectedUtcNow.ToLocalTime(),
#pragma warning restore CS0612,CS0618 // Type or member is obsolete
state.Expiration, preemptExpiryTime);
}
}
Expand Down Expand Up @@ -232,12 +290,10 @@ private static bool ShouldUpdateState(CredentialsRefreshState state, TimeSpan pr
var isExpired = state?.IsExpiredWithin(TimeSpan.Zero);
if (isExpired == true)
{
#pragma warning disable CS0612,CS0618 // Type or member is obsolete
var logger = Logger.GetLogger(typeof(RefreshingAWSCredentials));
logger.InfoFormat("Determined refreshing credentials should update. Expiration time: {0}, Current time: {1}",
state.Expiration.Add(preemptExpiryTime).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffK", CultureInfo.InvariantCulture),
AWSSDKUtils.CorrectedUtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK", CultureInfo.InvariantCulture));
#pragma warning restore CS0612,CS0618 // Type or member is obsolete
}

return isExpired ?? true;
Expand All @@ -254,7 +310,7 @@ protected virtual CredentialsRefreshState GenerateNewCredentials()
throw new NotImplementedException();
}
#if AWS_ASYNC_API
/// <summary>
/// <summary>
/// When overridden in a derived class, generates new credentials and new expiration date.
///
/// Called on first credentials request and when expiration date is in the past.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ This project file should not be used as part of a release pipeline.
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" >
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Loading