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

Fixes source token expiration by token refresh #5198

Merged
merged 10 commits into from Nov 4, 2022

Conversation

blidd-google
Copy link
Contributor

@blidd-google blidd-google commented Oct 31, 2022

Developers have reported a variety of issues while attempting large function deployments (see #4266). Specifically, developers were encountering an Invalid source token error message after successfully deploying a subset of the functions, indicating that at least part of the issue stemmed from the source token re-use hack (used to expedite deployments by re-using the same container for each function).

On particularly large deployments (~100+), the process can take >30 minutes. The source token acquired after the first function deployment expires at 30 minutes, resulting in the failure of subsequent function deployments that included the expired source token.

This change tracks the expiration of acquired source tokens, and after about 25 minutes, discards the old token and instructs the next function deployment to request a new source token. The remaining functions wait until the new source token is acquired before starting deployment. A 5 minute buffer is provided to basically guarantee that no subsequent function deployments will include the expired token. After about 25 minutes, the second token also expires and the next function requests a new token, etc., etc.

@blidd-google blidd-google changed the title Refreshes source token upon expiration Fixes source token expiration by token refresh Oct 31, 2022
@blidd-google blidd-google self-assigned this Nov 1, 2022
Comment on lines 3 to 7
enum TokenState {
"NONE",
"VALID",
"INVALID",
}
Copy link
Contributor

Choose a reason for hiding this comment

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

tangent: enums are controversial in TS for some reason.

https://blog.logrocket.com/why-typescript-enums-suck/

Internally, I think we prefer string literals:

type TokenState = "none" | "valid" | "invalid"

}

calculateTokenExpiry(): bigint {
const now = process.hrtime.bigint();
Copy link
Contributor

Choose a reason for hiding this comment

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

Personally, I think using hrtime with its bigint silliness is unnecessarily precise, especially if the time range we care about is in MINUTES not MICROSECONDS.

I'd suggest we stick to Date.now() and forget about big ints.

if (this.tokenState === TokenState.NONE) {
this.tokenState = TokenState.INVALID;
return undefined;
} else if (this.tokenState === TokenState.INVALID) {
Copy link
Contributor

@taeold taeold Nov 1, 2022

Choose a reason for hiding this comment

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

I found that token states as described here bit confusing. tokenState == INVALID makes me think that token is invalid, but token doesn't actually exist at this point.

What about another name that closely maps to the state of the scraper, like tokenFetchState instead of tokenState and FETCHING instead of INVALID?

return this.promise;
}

checkTokenExpired(): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit* I usually name my boolean function with "is" like isTokenExpired


checkTokenExpired(): boolean {
if (this.expiration === undefined) {
throw new Error();
Copy link
Contributor

Choose a reason for hiding this comment

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

This error is user visible - we should try our best to describe the situation and also call out what users should do when they see the issue.

@@ -210,9 +210,11 @@ export class Fabricator {
if (apiFunction.httpsTrigger) {
apiFunction.httpsTrigger.securityLevel = "SECURE_ALWAYS";
}
apiFunction.sourceToken = await scraper.tokenPromise();
// apiFunction.sourceToken = await scraper.tokenPromise();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit - stray comment?

return Promise.resolve(undefined);
isTokenExpired(): boolean {
if (this.expiry === undefined) {
throw new FirebaseError("failed to check expiry: no token exists");
Copy link
Contributor

Choose a reason for hiding this comment

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

This error message makes sense to you and me but won't to almost all users trying to deploy their function.

I think this is a good example of a user facing error message - describe the situation in coarsely and suggest next steps:

throw new FirebaseError(
"Cloud Runtime Config is currently experiencing issues, " +
"which is preventing your functions from being deployed. " +
"Please wait a few minutes and then try to deploy your functions again." +
"\nRun `firebase deploy --except functions` if you want to continue deploying the rest of your project."

In our case, hitting this if statement should never happen and would be considered a bug. Maybe we can suggest that they file a issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah gotcha. working on a more descriptive message.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

throw new FirebaseError(
        "Your deployment is checking the expiration of a source token that has not yet been polled. " +
          "Hitting this case should never happen and should be considered a bug. " +
          "Please file an issue at https://github.com/firebase/firebase-tools/issues " + 
          "and try deploying your functions again."
      );

What about something like this?

src/deploy/functions/release/sourceTokenScraper.ts Outdated Show resolved Hide resolved
src/deploy/functions/release/sourceTokenScraper.ts Outdated Show resolved Hide resolved
src/deploy/functions/release/timer.ts Outdated Show resolved Hide resolved
@codecov-commenter
Copy link

codecov-commenter commented Nov 3, 2022

Codecov Report

❗ No coverage uploaded for pull request base (master@ea2a9ec). Click here to learn what that means.
Patch coverage: 83.33% of modified lines in pull request are covered.

Additional details and impacted files
@@            Coverage Diff            @@
##             master    #5198   +/-   ##
=========================================
  Coverage          ?   56.26%           
=========================================
  Files             ?      308           
  Lines             ?    20814           
  Branches          ?     4225           
=========================================
  Hits              ?    11710           
  Misses            ?     8092           
  Partials          ?     1012           
Impacted Files Coverage Δ
src/deploy/functions/release/sourceTokenScraper.ts 83.33% <81.81%> (ø)
src/deploy/functions/release/fabricator.ts 83.59% <100.00%> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

@taeold taeold marked this pull request as ready for review November 3, 2022 18:00
Copy link
Contributor

@taeold taeold left a comment

Choose a reason for hiding this comment

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

Lgtm

this.promise = new Promise((resolve) => (this.resolve = resolve));
return undefined;
}
return this.promise;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we throw an error here instead of silently succeeding? Can see argument for either.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By throwing an error, do you mean throwing an error and then catching it in an enclosing try { await getToken(); ... } catch (expiredErr) {...} block, and then retrying?

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean that in theory we should never reach this codepath - fetchState can only be in one of three states, and we have if condition for all of them. So we could throw (which could happen if we misunderstood this code for somee reason) or just return the promise (which might leave the CLI in an undefined state). I have slight preference for the former.

await expect(scraper.getToken()).to.eventually.equal("magic token #2");
});

it("concurrent requests for source token", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice test!!

@blidd-google blidd-google enabled auto-merge (squash) November 4, 2022 22:09
@blidd-google blidd-google merged commit 98e23ed into master Nov 4, 2022
return Promise.resolve(undefined);
isTokenExpired(): boolean {
if (this.expiry === undefined) {
throw new FirebaseError(
Copy link
Member

Choose a reason for hiding this comment

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

Can we update this to throw with code: 2? We track this in Google Analytics for internal assertion errors.

const timeout = (duration: number): Promise<void> => {
return new Promise<void>((resolve) => setTimeout(resolve, duration));
};
await timeout(50);
Copy link
Member

Choose a reason for hiding this comment

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

In the future, try to instrument code without sleeping in your test. You can inject a clock for example.

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

Successfully merging this pull request may close these issues.

None yet

4 participants