-
Notifications
You must be signed in to change notification settings - Fork 3k
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
Block SequentialQueue
on reauthentication. Reorganize Network / Reauthentication code.
#8742
Conversation
…cationInSequentialQueue
@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...
One thing that is still bugging me about this (but will be fixed shortly) is that I've got a |
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 I think this looks really clean and organized. I like how the middleware pattern keeps logic very self-contained and purposeful.
} | ||
|
||
// We are already authenticating | ||
if (NetworkStore.getIsAuthenticating()) { |
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 about moving this to Authenticate.isAuthenticating()
?
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 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).
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.
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
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, 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. |
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 I agree with this solution but I'm not sure what that might imply.
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, 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.
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.
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.
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, 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); |
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.
If this request is replayed, wouldn't it just fail to authenticate again?
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.
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.
App/src/libs/Authentication.js
Lines 128 to 136 in dcaea12
// 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); |
SequentialQueue
on reauthenticationSequentialQueue
on reauthentication
Ok ready for review now. |
SequentialQueue
on reauthenticationSequentialQueue
on reauthentication. Reorganize Network / Reauthentication code.
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 love the unit tests! Just some suggestions on names that might help make their purpose more clear.
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); |
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.
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.
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.
yep, great suggestion
// 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). |
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.
It might be helpful to add some comments about the order they are used in and why that order matters.
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.
Added some more context below.
Updated thanks for the notes @tgolen ! 🙇 |
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 won't pretend to be an expert in this code, but I did read all of it. Only had a couple comments.
throw new Error('Missing credentials required for authentication'); | ||
} | ||
|
||
MainQueue.replay(request); |
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 get get what this does. If we failed for lack of credentials, we try again? Will this automatically get credentials somewhere?
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, 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.
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.
Looked into this a bit. The changes required here are a bit involved but I think worth doing...
We have some logic here:
App/src/libs/Network/MainQueue.js
Lines 23 to 27 in 58a139a
// 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.
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.
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.
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.
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.
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.
Created an issue here: https://github.com/Expensify/Expensify/issues/208303
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.
👍
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 not familiar with network code but the changes look great! Just a couple questions
Gonna merge since we have a couple of approvals. @tylerkaraszewski let me know if you have any thoughts about my response here. |
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
🚀 Cherry-picked to staging by @sketchydroide in version: 1.1.57-8 🚀
@Expensify/applauseleads please QA this PR and check it off on the deploy checklist if it passes. |
🚀 Deployed to production by @chiragsalian in version: 1.1.57-17 🚀
|
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'); | ||
} |
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.
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.
After: The reauthentication logic has been reorganized and calls to Authenticate will block the queue
Fixed Issues
https://github.com/Expensify/Expensify/issues/207534
Tests (Build staging version of app)
Authenticate
to complete before moving onto the next request in the queue.PR Review Checklist
PR Reviewer Checklist
### Fixed Issues
section aboveTests
sectionQA steps
sectiontoggleReport
and notonIconClick
).src/languages/*
filesSTYLE.md
) were followed/** comment above it */
displayName
propertythis
properly so there are no scoping issues (i.e. foronClick={this.submit}
the methodthis.submit
should be bound tothis
in the constructor)this
are necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);
ifthis.submit
is never passed to a component event handler likeonClick
)StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG
)Avatar
is modified, I verified thatAvatar
is working as expected in all cases)QA Steps
Screenshots
Web
Mobile Web
Desktop
iOS
Android