-
-
Notifications
You must be signed in to change notification settings - Fork 643
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
Clarify startEventPolling, fix a race condition #3830
Conversation
Also, for the authEquivalent function, note that it returns false if auth1 or auth2 is missing. However, it's up to the caller to assign meaning, if desired, to whether or not these auths are present. There is probably a more responsible way to handle this, but I'm not quite sure what it is. The function must still return something if one or both of the auths are missing, but sometimes an undefined auth carries meaning (usually, that the account is not logged in), and sometimes, I suppose, it might be undefined by mistake — but authEquivalent can't tell the difference. |
Reflexive, ill-considered opinion: in this context, |
Mmm, I think I chose to put it in too general terms; I don’t mean to propose a new state, to be represented by null or something. Maybe the specific thing that puzzles me is that my design of authEquivalent works together with tryGetAuth’s specific behavior of returning undefined when the user is logged out, so that if a user has logged out, authEquivalent will return false and we can break the loop. So maybe my discomfort is that this is kind of fragile; not only does it depend on tryGetAuth’s current behavior, but that tryGetAuth will be used to produce the arguments in the first place. (And maybe this point is moot: I think false will always be a reasonable return value if exactly one of the auths is undefined, regardless of why it’s undefined. It’ll currently also return false if they’re both undefined, which, why would anyone do that?) Or maybe (and each “maybe” should put emphasis on all the preceding ones), it’s that the naming of authEquivalent kind of suggests to me that two Auth objects will definitely be passed. Not only have I broken such a contract, but I’ve assigned meaning to the breach: an undefined means the user is not logged in, as noted above. Another theory (and yes, this has apparently become an investigation into my own subconscious that you’re free to skim past, but you might spot habits of mind that can be improved) is this: The framing of this authEquivalent function as a symmetric, all-purpose equality check between any two Auth objects in the world, in support of which I’ve named the params auth1 and auth2, is disingenuous because the only values for auth1 and auth2 we’d ever need to pass are for “the active account” (obtained somehow) at two different points in time. All other uses are pointless. (I don’t need to confirm that the active account is identical to an inactive account, or that two inactive accounts are identical. Maybe check my reasoning here.) Since this function is destined only to check the login state of the active account across time, maybe we should write it that way — it could take previousAuth and currentAuth, or maybe just a single argument maybeStaleAuth, and do a tryGetAuth internally. |
(To be fair, this is also exactly the meaning you've inherited from
That suggests a different fix, involving bypassing the whole question: actually require two Auth objects (
(Conceptually, you could then generalize it to take an equivalence relation The rest of that last paragraph I choose to summarize and generalize as "If an abstraction raises questions whose answers are functionally irrelevant, it probably isn't the right abstraction." Which is, at the very least, a proposition I can't trivially come up with counterexamples for. |
I think of it basically exactly this way. In the type system we have, it's spelled One reason I worry when I see (In fact the best use case I can think of right now for something like I think this function's signature can treat its inputs as the moral equivalent of That also helps keep its description from having to get entangled with the details of other functions like |
Yeah, so thinking of this type as a way of writing It's no longer this function's job to worry about the specific concept of "the user is logged out", or the specific behavior of Then it's up to the callers, if they pass data from one to the other, to make sure the semantics line up appropriately. In this case I think each side doing the most natural thing does line up nicely -- which is what one hopes for from the natural thing. 😉 PS - for background, here's descriptions of |
This makes so much sense! Thanks for the explanations, both of you, and I'll read those docs this afternoon! 🙂 |
Hmm, so if auth1 and auth2 are both undefined, it should return true, right, and if callers don't want that behavior, they can adjust locally. So, the current
I wonder what's the best way to implement the natural way of "absence === absence" being true in JavaScript. Do you think there's a more clear and concise way than this?
|
4ed6526
to
7bd8fe0
Compare
If it's important to have tests for authEquivalent (none of the other functions in If there's a more concise way to handle the case where auth1 or auth2 is undefined, but not both, in authEquivalent, I'd like to know. The optional chaining operator would work for the apiKey, as in |
In startEventPolling, there's no use delaying the response to a loop-breaking BAD_EVENT_QUEUE_ID with backoffMachine.wait(). So, move that wait so it happens after DEAD_QUEUE is dispatched.
This is a central function to determine that two Auth objects refer to the same account, to be used in the next commit.
This follows a discussion at and around https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/Event.20polling.20optimization.3F/near/810784. The `queueId !== getState().session.eventQueueId` is not the most direct way to check that the same user has remained logged in. So, replace that check and the `auth` check above it with a call to the new helper function authEquivalent, to check the maybe-stale auth against the current auth. Also, the `queueId !== ...` check allows a race condition! We change `session.eventQueueId` at `REALM_INIT`, which only happens after a `/register` request *completes*. So a user could switch accounts, triggering a new `/register` request, but the old account's event poll could return before that new `/register` completes, and the `queueId !== ...` will pass, triggering a state update with the old account's events instead of the new account's. Checking auth information is what we want here. It's true that this auth check will not catch the case that a queue was killed following a period of inactivity (i.e., the BAD_EVENT_QUEUE_ID). However, this is not a flaw, because the loop already handles that case correctly without the `queueId !== ...` check. In particular, if a queue is killed, api.pollForEvents will reject, and that rejection is handled in the catch block. When modifying startEventPolling to accept an auth parameter, I noticed (and added a comment to explain) that some actions being dispatched in doInitialFetch will throw unhandled errors if a user logs out while the /register request is in progress. This will be of concern when we work on zulip#3706.
7bd8fe0
to
0abc2cb
Compare
Just fixed some merge conflicts and shortened some topic lines. |
if (!authEquivalent(auth, tryGetAuth(getState()))) { | ||
// The user logged out or switched accounts during progressiveTimeout | ||
// called from see catch block, below. (If tryGetAuth returns undefined, | ||
// the user is not logged 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.
This would make more sense immediately after the relevant await
, rather than up here as far removed from it as possible. That wouldn't quite be [nfc]
on its own, though, since it's entirely possible that this function is itself async-delayed (and whether or not it is is a bit fragile, anyway). You'd have to duplicate it to also occur before loop entry.
I wouldn't ordinarily suggest that... but given the duplicated parenthetical in the comment, perhaps this test should be pulled into a mini-closure anyway?
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.
... and also used on catch
entry. If a user has logged in to another account, you probably don't want to fire off the deadQueue()
logic – or, worse yet, logout()
.
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.
... and also used on
catch
entry.
Good catch! (No pun intended.)
And I agree these checks are best done immediately after the await
.
Then one other point which can help keep things reasonably simple when checking this at the top of the catch
: everything else in that try
block doesn't need to be there, and can be moved to after the try .. catch
. That error-handling code (including the backoff) only makes sense for an exception thrown inside the API call.
it's entirely possible that this function is itself async-delayed (and whether or not it is is a bit fragile, anyway). You'd have to duplicate it to also occur before loop entry.
I think the best fix for this is to move the tryGetAuth(getState())
from its caller down into the top of this function.
given the duplicated parenthetical in the comment
I think the parenthetical is best just deleted. Its content is in the summary line of the mentioned function's jsdoc anyway.
Closing my own PR, as it's gone stale and I'd like to approach this afresh with #4170 in mind. |
This PR follows a long discussion at and around https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/Event.20polling.20optimization.3F/near/810784.