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

Block SequentialQueue on reauthentication. Reorganize Network / Reauthentication code. #8742

Merged
merged 28 commits into from
Apr 29, 2022

Conversation

marcaaron
Copy link
Contributor

@marcaaron marcaaron commented Apr 21, 2022

Details

Makes the SequentialQueue work better when we are reauthenticating.

Before: When a request in the SQ needed reauthentication we would call Authenticate and then send the original request into the non-sequential queue (where it would be processed after the SQ empties). That is bad because requests happen out of order.

Request 1 (Fail)
Authenticate
Request 2
Request 3
Request 1

After: The reauthentication logic has been reorganized and calls to Authenticate will block the queue

Request 1 (Fail)
Authenticate
Request 1 
Request 2
Request 3

Fixed Issues

https://github.com/Expensify/Expensify/issues/207534

Tests (Build staging version of app)

  1. Go offline via Chrome Dev Tools Network tab
    2022-04-25_10-16-52
  2. Leave 10 comments on a report
  3. Invalidate the authToken via Preferences
    2022-04-25_10-15-59
  4. Go back online
  5. Verify by inspecting the Network requests tab in Chrome Dev Tools that each request in the queue is made sequentially and that the first request requiring reauthentication will wait for Authenticate to complete before moving onto the next request in the queue.
  • Verify that no errors appear in the JS console

PR Review Checklist

PR Reviewer Checklist

  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I verified tests pass on all platforms & I tested again on:
    • iOS / native
    • Android / native
    • iOS / Safari
    • Android / Chrome
    • MacOS / Chrome
    • MacOS / Desktop
  • I verified there are no console errors (if there’s a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained “why” the code was doing something instead of only explaining “what” the code was doing.
    • I verified any copy / text shown in the product was added in all src/languages/* files
    • I verified any copy / text that was added to the app is correct English and approved by marketing by tagging the marketing team on the original GH to get the correct copy.
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named “index.js”. All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.js or at the top of the file that uses the constant) are defined as such
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • Any functional components have the displayName property
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose and it is broken down into smaller components in order to separate concerns and functions
  • If a new CSS style is added I verified that:
    • A similar style doesn’t already exist
    • The style can’t be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.

QA Steps

  • Verify that no errors appear in the JS console

Screenshots

Web

Mobile Web

Desktop

iOS

Android

@marcaaron marcaaron self-assigned this Apr 21, 2022
@marcaaron
Copy link
Contributor Author

@tgolen This PR got a little out of hand - but I think in a good way.

So many things that were bugging me about the Network code feel pretty nice now with the middleware idea applied.

Here's the basic idea...

  • Network.post() passes requests to the queues

  • The queues call Request.process() which calls HttpUtils.xhr() with middlewares applied

  • Each middleware returns a promise and passes the response to the next middleware

  • The order is important and we have:

    • Logging - Logs the request details (this cleaned up all circular dependencies with Log)
    • Recheck connection - Not sure how much we need the stuff in this file, but figured I'd leave it for now.
    • Reauthentication - Handles reauthentication
    • Retry - Handles request retry logic

One thing that is still bugging me about this (but will be fixed shortly) is that I've got a isFromSequentialQueue argument getting passed to the middleware. This is necessary because I haven't yet forced all network requests that can be persisted directly into the sequential queue. Once we do that we can just look at the request object and see if it's "persistable", but for now, isFromSequentialQueue is the only way to tell if it was called from the sequential queue or not.

Copy link
Contributor

@tgolen tgolen left a comment

Choose a reason for hiding this comment

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

Overall I think this looks really clean and organized. I like how the middleware pattern keeps logic very self-contained and purposeful.

src/libs/Middleware/Reauthentication.js Show resolved Hide resolved
}

// We are already authenticating
if (NetworkStore.getIsAuthenticating()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about moving this to Authenticate.isAuthenticating()?

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 the only problem with that is Authentication has a dependency on Network (it calls post()) and we also use this in the main queue logic to see if we should make a request (which would create a circular dependency and is the main reason why I created the NetworkStore idea to keep track of these things for us).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One idea I am having is to create an AuthenticationUtils.

I've tried a couple different things and didn't land on how to fix the dependency cycle (I need more practice fixing those puzzles 😄 ), but it maybe is reasonable to have authenticate and reauthenticate in Authentication and put the credentials and isAuthenticating stuff in an AuthenticationUtils

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, circular dependencies can be a huge pain like that. I think I'd rather leave them on NetworkStore rather than having a new AuthenticationUtils.

It's at least better that Network doesn't have any specialized knowledge now, but it is a little hard trying to reason about the difference between Network and NetworkStore.

// This can happen if we are already in the process of authenticating via the main queue and then
// the sequential queue gets flushed (e.g. because we went offline while we were authenticating, queued some requests
// and then came back online which triggers the sequential queue to flush).
// I think the solution is to maybe only ever have one call to Authenticate happening at any time no matter who calls it.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I agree with this solution but I'm not sure what that might imply.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I haven't quite worked it out, but I think it'd be something like...

  • We only ever have one call to Authenticate happening at a time and we have a reference to that promise in the middleware
  • If a request is made and that promise hasn't resolved yet we wait until it does by hooking into it and using it's response

I will caveat that I also haven't observed the case I'm describing happen, but did write a failing test that seems to imply it could happen like this:

  • Request made while online returns 407 which triggers a call to Authenticate to get queued, but not run
  • We go offline the same time the response is received but before we actually call Authenticate (not sure if this is possible)
  • We make some requests while offline
  • We get "stuck" isAuthenticating (again, not sure if it's possible)
  • The Sequential Queue runs and the first request gets a 407
  • We can't continue because we are "already authenticating"

This is why I had the instinct to throw an error - which would reveal if it's possible or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, the situation I am describing can be prevented by just checking to see if we are offline when handling a 407 and throwing an error if we are. That seems like a simpler solution so gonna try that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, cool. In the end, if we still really want to log for this case, then I think we can just leave a comment that says it's there for a bit to ensure that nothing ever happens (because if it did happen, we would never know it, and it would lead to bugs that would be very hard to debug) and maybe we can go in later and remove the error.

throw new Error('Unable to reauthenticate sequential queue request because we failed to reauthenticate');
}

Network.replayRequest(request);
Copy link
Contributor

Choose a reason for hiding this comment

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

If this request is replayed, wouldn't it just fail to authenticate again?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A comment could help here (or maybe there's some way to improve this).

We have to dig into the extra logic in reauthenticate(), but it is likely to succeed as it most likely failed because of a networking issue. If we fail because the credentials are bad then we just get logged out.

// When a fetch() fails and the "API is offline" error is thrown we won't log the user out. Most likely they
// have a spotty connection and will need to try to reauthenticate when they come back online. We will
// re-throw this error so it can be handled by callers of reauthenticate().
if (error.message === CONST.ERROR.API_OFFLINE) {
throw error;
}
// If we experience something other than a network error then redirect the user to sign in
redirectToSignIn(error.message);

@marcaaron marcaaron changed the title [WIP] Block SequentialQueue on reauthentication Block SequentialQueue on reauthentication Apr 26, 2022
@marcaaron marcaaron marked this pull request as ready for review April 26, 2022 20:20
@marcaaron marcaaron requested a review from a team as a code owner April 26, 2022 20:20
@melvin-bot melvin-bot bot requested review from tylerkaraszewski and removed request for a team April 26, 2022 20:20
@marcaaron marcaaron requested a review from a team April 26, 2022 20:33
@melvin-bot melvin-bot bot requested review from thienlnam and removed request for a team April 26, 2022 20:33
@marcaaron
Copy link
Contributor Author

Ok ready for review now.
Gonna add another reviewer here since the changes are pretty extensive (though a lot of this is just reorganizing).
I am open to splitting this stuff up into several PRs, but would have to think about how to do it.

@marcaaron marcaaron changed the title Block SequentialQueue on reauthentication Block SequentialQueue on reauthentication. Reorganize Network / Reauthentication code. Apr 26, 2022
Copy link
Contributor

@tgolen tgolen left a comment

Choose a reason for hiding this comment

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

I love the unit tests! Just some suggestions on names that might help make their purpose more clear.

src/libs/API.js Outdated Show resolved Hide resolved
src/libs/API.js Outdated Show resolved Hide resolved
src/libs/API.js Outdated
function Authenticate(parameters) {
const commandName = 'Authenticate';
// Recheck - Sets a timeout timer for a request that will "recheck" if we are connected to the internet if time runs out. Also triggers the connection recheck when we encounter any error.
Request.use(Middleware.Recheck);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: rename to Middleware.RecheckConnection

This just gives it a little more context. When I first saw this, I wasn't sure what "recheck" meant at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yep, great suggestion

src/libs/Authentication.js Show resolved Hide resolved
src/libs/Authentication.js Outdated Show resolved Hide resolved
src/libs/Middleware/Recheck.js Outdated Show resolved Hide resolved
src/libs/Request.js Outdated Show resolved Hide resolved
src/libs/Request.js Outdated Show resolved Hide resolved
// All other jsonCode besides 407 are treated as a successful response and must be handled in the .then() of the API method
queuedRequest.resolve(response);
});
// Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next).
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be helpful to add some comments about the order they are used in and why that order matters.

Copy link
Contributor Author

@marcaaron marcaaron Apr 26, 2022

Choose a reason for hiding this comment

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

Added some more context below.

@marcaaron
Copy link
Contributor Author

Updated thanks for the notes @tgolen ! 🙇

Copy link
Contributor

@tylerkaraszewski tylerkaraszewski left a comment

Choose a reason for hiding this comment

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

I won't pretend to be an expert in this code, but I did read all of it. Only had a couple comments.

src/libs/Authentication.js Show resolved Hide resolved
throw new Error('Missing credentials required for authentication');
}

MainQueue.replay(request);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't get get what this does. If we failed for lack of credentials, we try again? Will this automatically get credentials somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this is a really great find and I think points to a deeper problem.

The comment suggests that "Credentials haven't been initialized". So we are putting this request back into the queue because we are "waiting for the credentials". Which begs these questions:

  • Why we are waiting for them at all ?
  • Why we are not waiting for them in the sequential queue ?

Gonna think about this for a second. My gut says no requests should happen at all until we can guarantee the credentials either exist or don't exist. Then we can just throw here and the user should probably also get signed out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looked into this a bit. The changes required here are a bit involved but I think worth doing...

We have some logic here:

// We must attempt to read authToken and credentials from storage before allowing any requests to happen so that any requests that
// require authToken or trigger reauthentication will succeed.
if (!NetworkStore.hasReadRequiredDataFromStorage()) {
return false;
}

and it needs a better design since both the "main queue" and "sequential queue" should only make requests when we are "ready" to do so.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Going to create a follow up issue for this as it's slightly tricky to untangle and I don't think it needs to be a blocker.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TL;DR - no, credentials are not automatically regenerated and this code appears related to a race condition. the queue we have for these requests now isn't "waiting for the credentials" so it's not clear if this is a problem or not, but should be cleaned up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

👍

thienlnam
thienlnam previously approved these changes Apr 27, 2022
Copy link
Contributor

@thienlnam thienlnam left a comment

Choose a reason for hiding this comment

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

Also not familiar with network code but the changes look great! Just a couple questions

src/libs/Authentication.js Show resolved Hide resolved
src/libs/Authentication.js Show resolved Hide resolved
@marcaaron marcaaron requested a review from tgolen April 28, 2022 23:52
@marcaaron
Copy link
Contributor Author

Gonna merge since we have a couple of approvals.

@tylerkaraszewski let me know if you have any thoughts about my response here.

@marcaaron marcaaron merged commit 69e3e28 into main Apr 29, 2022
@marcaaron marcaaron deleted the marcaaron-handleReauthenticationInSequentialQueue branch April 29, 2022 16:07
@OSBotify
Copy link
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify
Copy link
Contributor

OSBotify commented May 9, 2022

🚀 Cherry-picked to staging by @sketchydroide in version: 1.1.57-8 🚀

platform result
🤖 android 🤖 success ✅
🖥 desktop 🖥 success ✅
🍎 iOS 🍎 success ✅
🕸 web 🕸 success ✅

@Expensify/applauseleads please QA this PR and check it off on the deploy checklist if it passes.

@OSBotify
Copy link
Contributor

🚀 Deployed to production by @chiragsalian in version: 1.1.57-17 🚀

platform result
🤖 android 🤖 success ✅
🖥 desktop 🖥 success ✅
🍎 iOS 🍎 success ✅
🕸 web 🕸 success ✅

Comment on lines +21 to +24
if (NetworkStore.isOffline()) {
// If we are offline and somehow handling this response we do not want to reauthenticate
throw new Error('Unable to reauthenticate because we are offline');
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This PR causes a regression here more details on the PR

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.

6 participants