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
[Identity] [core-http] Concept PR for the token refresher update #10085
[Identity] [core-http] Concept PR for the token refresher update #10085
Conversation
Ended up getting pulled onto house maintenance instead of coding today. What you have looks good. cc @schaabs to double check we're going in the right direction. If it looks good, I can work with you to take it to the finish line. |
@@ -63,6 +66,12 @@ export class BearerTokenAuthenticationPolicy extends BaseRequestPolicy { | |||
private tokenCache: AccessTokenCache | |||
) { | |||
super(nextPolicy, options); | |||
const requiredMillisecondsBeforeNewRefresh = 30000; |
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.
Commenting so we don't forget it - probably don't want to hardcode the delay like this. At the very least, we should pick the buffer, make it a const, and document it.
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.
we should pick the buffer, make it a const, and document it
What do you mean by picking the buffer?
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.
You're hardcoding 30000 here. What does it mean? Why 30000?
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.
What do you think of this as a property of the BearerTokenAuthenticationPolicy class? Like:
export class BearerTokenAuthenticationPolicy extends BaseRequestPolicy {
// The automated token refresh will only start to happen at the
// expiration date minus the value of millisecondsBeforeNewRefresh,
// which is by default 30 seconds.
public millisecondsBeforeNewRefresh: number = 30000;
[refreshCred1, 2], | ||
[refreshCred2, 2], | ||
[refreshCred1, 1], | ||
[refreshCred2, 1], |
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.
What's the difference between 1 and 2 here?
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.
number of refreshments have changed because of this PR, since we know use the existing tokens first, and in parallel attempt to request (?) Please let me know if I'm missing something
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.
Ah okay, got it.
97d5469
to
f973ea5
Compare
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.
Refresh logic LGTM please get approval from a JS team member.
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.
Overall looks very good, but there are a few things I'd like to have fix before this gets merged.
* Returns null if the required time between each call hasn't been reached. | ||
* @param options getToken options | ||
*/ | ||
public refresh(options: GetTokenOptions): Promise<AccessToken | undefined> | null { |
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.
I don't think we should return Promise | null
- that's a confusing and hard to use contract with async/await. In the null
case we should return Promise.resolve()
or a cached version of that.
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.
Here's a much better approach with some small tweaks! How come I didn't think of this earlier? 😓 well, it's here now! 😁 16c88f5
* Forces the requests of a new token if we're not currently waiting for a new token. | ||
* @param options getToken options | ||
*/ | ||
public forcedRefresh(options: GetTokenOptions): Promise<AccessToken | undefined> | null { |
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.
I think the return type is wrong here, since how can it be null? If the promise doesn't exist it gets created.
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.
Here's a much better approach with some small tweaks! How come I didn't think of this earlier? 😓 well, it's here now! 😁 16c88f5
* expiration date minus the value of timeBetweenRefreshAttemptsInMs, | ||
* which is by default 30 seconds. | ||
*/ | ||
private timeBetweenRefreshAttemptsInMs: number = 30000; |
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.
nit: no need to annotate the type as number
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.
also why make this a class member? It seems to be constant, in which case it could be pulled out into a module const
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.
I moved it to a module const here: 16c88f5
// only if the cache is unable to retrieve the access token, | ||
// which means that it has expired, or it has never been set. | ||
const refreshPromise = this.tokenRefresher.forcedRefresh(options); | ||
if (refreshPromise !== null) { |
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.
yeah having to do this check is ugly
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.
Here's a much better approach with some small tweaks! How come I didn't think of this earlier? 😓 well, it's here now! 😁 16c88f5
sdk/core/core-http/src/policies/bearerTokenAuthenticationPolicy.ts
Outdated
Show resolved
Hide resolved
// If the tokenRefresher returned null, some other refresh is happening already. | ||
// if this is a new refresh, we set it up to update the cachedToken once it finishes. | ||
if (refreshPromise !== null) { | ||
refreshPromise.then((accessToken: AccessToken | undefined) => { |
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 fork the promise chain instead of using await?
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.
the idea is to not wait for this to happen, that's the main point of this feature: this should happen in the background.
@@ -73,6 +74,19 @@ describe("BearerTokenAuthenticationPolicy", function() { | |||
} | |||
}); | |||
|
|||
it("tests that AccessTokenRefresher is working", async function() { | |||
this.timeout(35000); |
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.
no, please don't do this. There is no reason to make a test take 30+ seconds just because of a default timer. We should mock the clock with sinon if you want to test that, but not sit there and waste CPU cycles
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.
alright, I'll follow your advice here
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.
I believe I did it here: 7f3192e thank you again for the feedback!
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.
I think this doesn't really fit in with what we're already doing for token refreshing. If the idea is that you want to make sure that there's only a single refresh happening at any given point based on a pre-expiration delta, you could do it using a single Promise field inside of the existing BearerTokenAuthenticationPolicy
class. We already have logic for calculating the pre-expiration delta so I don't think we need to add another way to calculate that.
const request = createRequest(); | ||
const policy = createBearerTokenPolicy("testscope", credentialToTest); | ||
await policy.sendRequest(request); | ||
await delay(30000); |
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.
You should stub this out like you did in the Identity tests so that we don't have to wait 30 seconds on a test run
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.
thank you! I'll do what you say
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.
I believe I did it here: 7f3192e thank you again for the feedback!
* Forces the requests of a new token if we're not currently waiting for a new token. | ||
* @param options getToken options | ||
*/ | ||
public forcedRefresh(options: GetTokenOptions): Promise<AccessToken | undefined> | null { |
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.
Could this just be an optional parameter on refresh
? Ideally it would be, otherwise I'd call this method forceRefresh
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.
I thought on intentionally not doing the forced parameter since it becomes sort of meaningless once passed in, like refresh(true)
. I wonder if extending GetTokenOptions to an interface like RefreshOptions
with an extra property would be better. What do you think?
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.
Here's a much better approach with some small tweaks! How come I didn't think of this earlier? 😓 well, it's here now! 😁 16c88f5
} else { | ||
// If we still have a cached access token, | ||
// then attempt to refresh without waiting. | ||
const refreshPromise = this.tokenRefresher.refresh(options); |
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 refresh if the token cache's refresh time hasn't been reached? We use a delta to determine the time before the real expiration time that we should start a refresh. The previous logic case (accessToken === undefined
) should handle that, in theory.
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.
This has changed, but the gist is that we will not attempt to refresh based on a conditional. We can make the conditional better too! if that makes sense.
/** | ||
* Helps the core-http token authentication policies with requesting a new token if we're not currently waiting for a new token. | ||
*/ | ||
export class AccessTokenRefresher { |
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.
After looking over the code I wonder if we even need a separate class for this behavior. You could just do the promise management inside of BearerTokenAuthenticationPolicy
itself.
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.
I think that @schaabs could help me here. The idea is to be able to have something plug-able, to use somewhere else, as far as I understand.
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.
I spoke with @schaabs on Teams, he clarified things for me.
*/ | ||
export class AccessTokenRefresher { | ||
private promise: Promise<AccessToken | undefined> | undefined; | ||
private lastCalled: number | undefined; |
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.
instead of undefined
, a minimum number like 0 or beginning of the time can be used here instead?
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.
I'll assign 0
We are a little late in the game to take in changes for the core packages in the current release cycle. I would strongly recommend pushing this for the next release cycle. |
@schaabs - is it possible to move this fix out of GA, or do we need to slip the GA for Identity/core-http to ensure it's in? |
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.
Thanks for making the improvements! This looks good to me.
Just got word back from @schaabs - this won't make the GA release. We'll put it in the first 1.2 prerelease next month instead. |
de8f86b
to
b58608d
Compare
@xirzec a new review is appreciated! |
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.
I like this a lot more! Left a few cleanup/style suggestions and thoughts.
private lastCalled = 0; | ||
|
||
constructor( | ||
private credential: TokenCredential, |
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.
personal style nit: I find this way of initializing private variables to be confusing for some folks and always explicitly initialize them in the constructor body instead.
@bterlson you have any pref here?
sdk/core/core-http/src/policies/bearerTokenAuthenticationPolicy.ts
Outdated
Show resolved
Hide resolved
sdk/core/core-http/src/policies/bearerTokenAuthenticationPolicy.ts
Outdated
Show resolved
Hide resolved
Co-authored-by: Jeff Fisher <xirzec@xirzec.com>
this.tokenCache.setCachedToken(accessToken); | ||
// Waiting for the next refresh only if the cache is unable to retrieve the access token, | ||
// which means that it has expired, or it has never been set. | ||
accessToken = await this.tokenRefresher.refresh(options); |
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.
Oops! I forgot to this.tokenCache.setCachedToken(accessToken);
here!
Thank you, @HarshaNalluru for helping me see this! 🌞
Here's a PR: #10692
@@ -61,7 +62,7 @@ describe("BearerTokenAuthenticationPolicy", function() { | |||
const credentialsToTest: [MockRefreshAzureCredential, number][] = [ | |||
[refreshCred1, 2], | |||
[refreshCred2, 2], | |||
[notRefreshCred1, 1] | |||
[notRefreshCred1, 2] |
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.
Well, this was actually important: https://github.com/Azure/azure-sdk-for-js/pull/10692/files#r472566282
Jonathan will take over this today, but in any case, here's my concept PR to:
Fix #10084
Tests pass, at least 🌞
Also fixes #9777 (since the feature crew decided to not make it configurable).