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

[Identity] [core-http] Concept PR for the token refresher update #10085

Merged
merged 18 commits into from Aug 14, 2020

Conversation

sadasant
Copy link
Contributor

@sadasant sadasant commented Jul 15, 2020

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).

@sadasant sadasant requested a review from sophiajt July 15, 2020 22:12
@sadasant sadasant self-assigned this Jul 15, 2020
@ghost ghost added the Azure.Core label Jul 15, 2020
@sophiajt
Copy link
Contributor

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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?

Copy link
Contributor Author

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],
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, got it.

@sadasant sadasant force-pushed the identity/refresh-offset-token-cache branch from 97d5469 to f973ea5 Compare August 3, 2020 23:02
@sadasant sadasant marked this pull request as ready for review August 4, 2020 22:40
Copy link
Contributor

@schaabs schaabs left a 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.

Copy link
Member

@xirzec xirzec left a 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.

sdk/core/core-http/src/credentials/accessTokenRefresher.ts Outdated Show resolved Hide resolved
* 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 {
Copy link
Member

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.

Copy link
Contributor Author

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 {
Copy link
Member

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.

Copy link
Contributor Author

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;
Copy link
Member

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

Copy link
Member

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

Copy link
Contributor Author

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) {
Copy link
Member

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

Copy link
Contributor Author

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

// 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) => {
Copy link
Member

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?

Copy link
Contributor Author

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);
Copy link
Member

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

Copy link
Contributor Author

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

Copy link
Contributor Author

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!

Copy link
Contributor

@daviwil daviwil left a 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);
Copy link
Contributor

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

Copy link
Contributor Author

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

Copy link
Contributor Author

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 {
Copy link
Contributor

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

Copy link
Contributor Author

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?

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll assign 0

@ramya-rao-a
Copy link
Contributor

@sadasant, @jonathandturner,

We are a little late in the game to take in changes for the core packages in the current release cycle.
Code complete for core packages for current release cycle was last Friday and this week is when we release the core packages.

I would strongly recommend pushing this for the next release cycle.

@sophiajt
Copy link
Contributor

sophiajt commented Aug 5, 2020

@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?

Copy link
Contributor

@daviwil daviwil left a 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.

@sophiajt
Copy link
Contributor

sophiajt commented Aug 5, 2020

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.

@sadasant sadasant changed the title [Identity] Concept PR for the token refresher update [DO NOT MERGE] [Identity] Concept PR for the token refresher update Aug 5, 2020
@sadasant sadasant changed the title [DO NOT MERGE] [Identity] Concept PR for the token refresher update [DO NOT MERGE] [Identity] [core-http] Concept PR for the token refresher update Aug 6, 2020
@sadasant sadasant requested a review from xirzec August 6, 2020 17:16
@sadasant sadasant changed the title [DO NOT MERGE] [Identity] [core-http] Concept PR for the token refresher update [Identity] [core-http] Concept PR for the token refresher update Aug 13, 2020
@sadasant sadasant force-pushed the identity/refresh-offset-token-cache branch from de8f86b to b58608d Compare August 13, 2020 22:07
@sadasant sadasant requested a review from daviwil August 13, 2020 22:08
@sadasant
Copy link
Contributor Author

@xirzec a new review is appreciated!

Copy link
Member

@xirzec xirzec left a 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.

sdk/core/core-http/src/credentials/accessTokenRefresher.ts Outdated Show resolved Hide resolved
private lastCalled = 0;

constructor(
private credential: TokenCredential,
Copy link
Member

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/credentials/accessTokenRefresher.ts Outdated Show resolved Hide resolved
sdk/core/core-http/src/credentials/accessTokenRefresher.ts Outdated Show resolved Hide resolved
@sadasant sadasant merged commit 03db02d into Azure:master Aug 14, 2020
@sadasant sadasant deleted the identity/refresh-offset-token-cache branch August 14, 2020 21:40
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);
Copy link
Contributor Author

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]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants