-
Notifications
You must be signed in to change notification settings - Fork 462
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
EdgeHub: Fix TokenUpdate logic #206
Changes from 5 commits
4c69750
5cd1632
f5a4251
177b7c8
7e47659
eccd866
13f74f8
00ddb6a
1436521
a909c71
3908d9e
f3d7658
27d4962
d6bed46
d2675c5
db7d8cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,7 @@ class CloudConnection : ICloudConnection | |
const int TokenTimeToLiveSeconds = 3600; // Unused - Token is generated by downstream clients | ||
const int TokenExpiryBufferPercentage = 8; // Assuming a standard token for 1 hr, we set expiry time to around 5 mins. | ||
const uint OperationTimeoutMilliseconds = 1 * 60 * 1000; // 1 min | ||
static readonly TimeSpan TokenRetryWaitTime = TimeSpan.FromSeconds(20); | ||
|
||
readonly Action<string, CloudConnectionStatus> connectionStatusChangedHandler; | ||
readonly ITransportSettings[] transportSettingsList; | ||
|
@@ -93,15 +94,15 @@ public async Task<ICloudProxy> CreateOrUpdateAsync(IClientCredentials newCredent | |
if (newCredentials is ITokenCredentials tokenAuth && this.tokenGetter.HasValue) | ||
{ | ||
if (IsTokenExpired(tokenAuth.Identity.IotHubHostName, tokenAuth.Token)) | ||
{ | ||
{ | ||
throw new InvalidOperationException($"Token for client {tokenAuth.Identity.Id} is expired"); | ||
} | ||
|
||
this.tokenGetter.ForEach(tg => | ||
{ | ||
tg.SetResult(tokenAuth.Token); | ||
// First reset the token getter and then set the result. | ||
this.tokenGetter = Option.None<TaskCompletionSource<string>>(); | ||
Events.NewTokenObtained(newCredentials.Identity.IotHubHostName, newCredentials.Identity.Id, tokenAuth.Token); | ||
tg.SetResult(tokenAuth.Token); | ||
}); | ||
return (cp, false); | ||
} | ||
|
@@ -177,7 +178,7 @@ async Task<IClient> CreateDeviceClient( | |
{ | ||
client.SetProductInfo(newCredentials.ProductInfo); | ||
} | ||
|
||
Events.CreateDeviceClientSuccess(transportSettings.GetTransportType(), OperationTimeoutMilliseconds, newCredentials.Identity); | ||
return client; | ||
} | ||
|
@@ -263,33 +264,55 @@ void InternalConnectionStatusChangesHandler(ConnectionStatus status, ConnectionS | |
async Task<string> GetNewToken(string iotHub, string id, string currentToken, IIdentity currentIdentity) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you please add some comments explaining the retry logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. |
||
{ | ||
Events.GetNewToken(id); | ||
// We have to catch UnauthorizedAccessException, because on IsTokenUsable, we call parse from | ||
// Device Client and it throws if the token is expired. | ||
if (IsTokenUsable(iotHub, currentToken)) | ||
{ | ||
Events.UsingExistingToken(id); | ||
return currentToken; | ||
} | ||
else | ||
bool retrying = false; | ||
string token = currentToken; | ||
while (true) | ||
{ | ||
Events.TokenExpired(id, currentToken); | ||
} | ||
// We have to catch UnauthorizedAccessException, because on IsTokenUsable, we call parse from | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where does this catch occur? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In GetTokenExpiry (and IsTokenUsable also catches all exceptions) |
||
// Device Client and it throws if the token is expired. | ||
if (IsTokenUsable(iotHub, token)) | ||
{ | ||
if (retrying) | ||
{ | ||
Events.NewTokenObtained(iotHub, id, token); | ||
} | ||
else | ||
{ | ||
Events.UsingExistingToken(id); | ||
} | ||
return token; | ||
} | ||
else | ||
{ | ||
Events.TokenNotUsable(iotHub, id, token); | ||
} | ||
|
||
// No need to lock here as the lock is being held by the refresher. | ||
TaskCompletionSource<string> tcs = this.tokenGetter | ||
.GetOrElse( | ||
() => | ||
bool newTokenGetterCreated = false; | ||
// No need to lock here as the lock is being held by the refresher. | ||
TaskCompletionSource<string> tcs = this.tokenGetter | ||
.GetOrElse( | ||
() => | ||
{ | ||
Events.SafeCreateNewToken(id); | ||
var taskCompletionSource = new TaskCompletionSource<string>(); | ||
this.tokenGetter = Option.Some(taskCompletionSource); | ||
newTokenGetterCreated = true; | ||
return taskCompletionSource; | ||
}); | ||
|
||
if (newTokenGetterCreated) | ||
{ | ||
if (retrying) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is not clear to me when and how many times 'newTokenGetterCreated' and 'retrying' would both be true. Perhaps some comments could help clarify when tokenGetters are set and cleared. |
||
{ | ||
Events.SafeCreateNewToken(id); | ||
var taskCompletionSource = new TaskCompletionSource<string>(); | ||
this.tokenGetter = Option.Some(taskCompletionSource); | ||
this.connectionStatusChangedHandler(currentIdentity.Id, CloudConnectionStatus.TokenNearExpiry); | ||
return taskCompletionSource; | ||
}); | ||
string newToken = await tcs.Task; | ||
return newToken; | ||
} | ||
await Task.Delay(TokenRetryWaitTime); | ||
} | ||
this.connectionStatusChangedHandler(currentIdentity.Id, CloudConnectionStatus.TokenNearExpiry); | ||
} | ||
|
||
retrying = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we iterating the loop again only to log a message? Could we log the NewTokenObtained event here and return the new token right away? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could, but wanted to avoid the duplicate code. This seemed cleaner :) |
||
token = await tcs.Task; | ||
} | ||
} | ||
|
||
internal static DateTime GetTokenExpiry(string hostName, string token) | ||
{ | ||
|
@@ -412,7 +435,6 @@ enum EventIds | |
CreateNewToken, | ||
UpdatedCloudConnection, | ||
ObtainedNewToken, | ||
TokenExpired, | ||
ErrorRenewingToken, | ||
ErrorCheckingTokenUsability | ||
} | ||
|
@@ -471,11 +493,6 @@ internal static void NewTokenObtained(string hostname, string id, string newToke | |
Log.LogInformation((int)EventIds.ObtainedNewToken, Invariant($"Obtained new token for client {id} that expires in {timeRemaining}")); | ||
} | ||
|
||
internal static void TokenExpired(string id, string currentToken) | ||
{ | ||
Log.LogDebug((int)EventIds.TokenExpired, Invariant($"Token Expired. Id:{id}, CurrentToken: {currentToken}.")); | ||
} | ||
|
||
internal static void ErrorRenewingToken(Exception ex) | ||
{ | ||
Log.LogDebug((int)EventIds.ErrorRenewingToken, ex, "Critical Error trying to renew Token."); | ||
|
@@ -485,6 +502,12 @@ public static void ErrorCheckingTokenUsable(Exception ex) | |
{ | ||
Log.LogDebug((int)EventIds.ErrorCheckingTokenUsability, ex, "Error checking if token is usable."); | ||
} | ||
|
||
public static void TokenNotUsable(string hostname, string id, string newToken) | ||
{ | ||
TimeSpan timeRemaining = GetTokenExpiryTimeRemaining(hostname, newToken); | ||
Log.LogDebug((int)EventIds.ObtainedNewToken, Invariant($"Token received for client {id} expires in {timeRemaining}, and so is not usable. Getting a fresh token...")); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this order matter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the code below expects that the tokenGetter will be reset when the value is set. And that also answers your other question, I suppose
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In principle, if order is important, then we should do these under a lock. Otherwise order shouldn't matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed offline, the order is important because the caller is waiting for SetResult to be called (not looping), and when it resumes, it expects this.tokenGetter to be reset.