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

Fix useAbortableAsync race condition #207365

Merged
merged 3 commits into from
Jan 21, 2025

Conversation

flash1293
Copy link
Contributor

@flash1293 flash1293 commented Jan 21, 2025

useAbortableAsync can easily get confused about the current state - e.g. when a previous invocation gets aborted and a new one is started at the same time, the loading state gets set to false after the next invocation got started, so it's false for the time it's running:

old

You can see that while typing, the old slow request is aborted properly, but the loading state gets lost and the abort error from the last invocation is still set even though a new request is running already.

This is not the only possible issue that could happen here - e.g. if the promise chain throws too late, an unrelated error could be set in the error handling logic, which is not related to the currently running fn.

This is hard to fix because as the hook does not control the fn, it does not know at which point it resolves, even after a new invocation was started already. The abort signal asks the fn nicely to throw with an abort error, but it can't be controlled when that happens.

This PR introduces a notion of the current "generation" and only accepts state updates from the most recent one.

With this, the new invocation correctly sets the loading state after the abort - what happens to the old promise chain after the abort can't affect the state anymore:
new

I'm not sure whether this is the best way to resolve this issue, but I couldn't come up with a better way. Happy to adjust, but I think we need a solution that doesn't assume any special behavior of the passed in fn, otherwise this helper will always be super brittle.

@flash1293 flash1293 added release_note:skip Skip the PR/issue when compiling release notes v9.0.0 backport:version Backport to applied version labels v8.18.0 labels Jan 21, 2025
@flash1293 flash1293 requested a review from a team as a code owner January 21, 2025 14:18
@flash1293 flash1293 enabled auto-merge (squash) January 21, 2025 15:44
@flash1293 flash1293 merged commit 8ff18e2 into elastic:main Jan 21, 2025
8 checks passed
@kibanamachine
Copy link
Contributor

Starting backport for target branches: 8.x

https://github.com/elastic/kibana/actions/runs/12892755015

kibanamachine pushed a commit to kibanamachine/kibana that referenced this pull request Jan 21, 2025
`useAbortableAsync` can easily get confused about the current state -
e.g. when a previous invocation gets aborted and a new one is started at
the same time, the `loading` state gets set to false _after_ the next
invocation got started, so it's false for the time it's running:

![old](https://github.com/user-attachments/assets/6a784b1a-58b2-4951-8d25-9f109bce39c5)

You can see that while typing, the old slow request is aborted properly,
but the `loading` state gets lost and the abort error from the last
invocation is still set even though a new request is running already.

This is not the only possible issue that could happen here - e.g. if the
promise chain throws too late, an unrelated error could be set in the
error handling logic, which is not related to the currently running
`fn`.

This is hard to fix because as the hook does not control the `fn`, it
does not know at which point it resolves, even after a new invocation
was started already. The abort signal asks the `fn` nicely to throw with
an abort error, but it can't be controlled when that happens.

This PR introduces a notion of the current "generation" and only accepts
state updates from the most recent one.

With this, the new invocation correctly sets the loading state after the
abort - what happens to the old promise chain after the abort can't
affect the state anymore:

![new](https://github.com/user-attachments/assets/b39dd725-6bf1-4ef1-9eb6-d1463e1ec146)

I'm not sure whether this is the best way to resolve this issue, but I
couldn't come up with a better way. Happy to adjust, but I think we need
a solution that doesn't assume any special behavior of the passed in
`fn`, otherwise this helper will always be super brittle.

(cherry picked from commit 8ff18e2)
@kibanamachine
Copy link
Contributor

💚 All backports created successfully

Status Branch Result
8.x

Note: Successful backport PRs will be merged automatically after passing CI.

Questions ?

Please refer to the Backport tool documentation

kibanamachine added a commit that referenced this pull request Jan 21, 2025
# Backport

This will backport the following commits from `main` to `8.x`:
- [Fix useAbortableAsync race condition
(#207365)](#207365)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Joe
Reuter","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-01-21T17:45:43Z","message":"Fix
useAbortableAsync race condition (#207365)\n\n`useAbortableAsync` can
easily get confused about the current state -\ne.g. when a previous
invocation gets aborted and a new one is started at\nthe same time, the
`loading` state gets set to false _after_ the next\ninvocation got
started, so it's false for the time it's
running:\n\n\n![old](https://github.com/user-attachments/assets/6a784b1a-58b2-4951-8d25-9f109bce39c5)\n\nYou
can see that while typing, the old slow request is aborted
properly,\nbut the `loading` state gets lost and the abort error from
the last\ninvocation is still set even though a new request is running
already.\n\nThis is not the only possible issue that could happen here -
e.g. if the\npromise chain throws too late, an unrelated error could be
set in the\nerror handling logic, which is not related to the currently
running\n`fn`.\n\nThis is hard to fix because as the hook does not
control the `fn`, it\ndoes not know at which point it resolves, even
after a new invocation\nwas started already. The abort signal asks the
`fn` nicely to throw with\nan abort error, but it can't be controlled
when that happens.\n\nThis PR introduces a notion of the current
\"generation\" and only accepts\nstate updates from the most recent
one.\n\nWith this, the new invocation correctly sets the loading state
after the\nabort - what happens to the old promise chain after the abort
can't\naffect the state
anymore:\n\n![new](https://github.com/user-attachments/assets/b39dd725-6bf1-4ef1-9eb6-d1463e1ec146)\n\nI'm
not sure whether this is the best way to resolve this issue, but
I\ncouldn't come up with a better way. Happy to adjust, but I think we
need\na solution that doesn't assume any special behavior of the passed
in\n`fn`, otherwise this helper will always be super
brittle.","sha":"8ff18e25758a05d4deff76ffa4b3407d98722a3c","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:version","v8.18.0"],"title":"Fix
useAbortableAsync race
condition","number":207365,"url":"https://github.com/elastic/kibana/pull/207365","mergeCommit":{"message":"Fix
useAbortableAsync race condition (#207365)\n\n`useAbortableAsync` can
easily get confused about the current state -\ne.g. when a previous
invocation gets aborted and a new one is started at\nthe same time, the
`loading` state gets set to false _after_ the next\ninvocation got
started, so it's false for the time it's
running:\n\n\n![old](https://github.com/user-attachments/assets/6a784b1a-58b2-4951-8d25-9f109bce39c5)\n\nYou
can see that while typing, the old slow request is aborted
properly,\nbut the `loading` state gets lost and the abort error from
the last\ninvocation is still set even though a new request is running
already.\n\nThis is not the only possible issue that could happen here -
e.g. if the\npromise chain throws too late, an unrelated error could be
set in the\nerror handling logic, which is not related to the currently
running\n`fn`.\n\nThis is hard to fix because as the hook does not
control the `fn`, it\ndoes not know at which point it resolves, even
after a new invocation\nwas started already. The abort signal asks the
`fn` nicely to throw with\nan abort error, but it can't be controlled
when that happens.\n\nThis PR introduces a notion of the current
\"generation\" and only accepts\nstate updates from the most recent
one.\n\nWith this, the new invocation correctly sets the loading state
after the\nabort - what happens to the old promise chain after the abort
can't\naffect the state
anymore:\n\n![new](https://github.com/user-attachments/assets/b39dd725-6bf1-4ef1-9eb6-d1463e1ec146)\n\nI'm
not sure whether this is the best way to resolve this issue, but
I\ncouldn't come up with a better way. Happy to adjust, but I think we
need\na solution that doesn't assume any special behavior of the passed
in\n`fn`, otherwise this helper will always be super
brittle.","sha":"8ff18e25758a05d4deff76ffa4b3407d98722a3c"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/207365","number":207365,"mergeCommit":{"message":"Fix
useAbortableAsync race condition (#207365)\n\n`useAbortableAsync` can
easily get confused about the current state -\ne.g. when a previous
invocation gets aborted and a new one is started at\nthe same time, the
`loading` state gets set to false _after_ the next\ninvocation got
started, so it's false for the time it's
running:\n\n\n![old](https://github.com/user-attachments/assets/6a784b1a-58b2-4951-8d25-9f109bce39c5)\n\nYou
can see that while typing, the old slow request is aborted
properly,\nbut the `loading` state gets lost and the abort error from
the last\ninvocation is still set even though a new request is running
already.\n\nThis is not the only possible issue that could happen here -
e.g. if the\npromise chain throws too late, an unrelated error could be
set in the\nerror handling logic, which is not related to the currently
running\n`fn`.\n\nThis is hard to fix because as the hook does not
control the `fn`, it\ndoes not know at which point it resolves, even
after a new invocation\nwas started already. The abort signal asks the
`fn` nicely to throw with\nan abort error, but it can't be controlled
when that happens.\n\nThis PR introduces a notion of the current
\"generation\" and only accepts\nstate updates from the most recent
one.\n\nWith this, the new invocation correctly sets the loading state
after the\nabort - what happens to the old promise chain after the abort
can't\naffect the state
anymore:\n\n![new](https://github.com/user-attachments/assets/b39dd725-6bf1-4ef1-9eb6-d1463e1ec146)\n\nI'm
not sure whether this is the best way to resolve this issue, but
I\ncouldn't come up with a better way. Happy to adjust, but I think we
need\na solution that doesn't assume any special behavior of the passed
in\n`fn`, otherwise this helper will always be super
brittle.","sha":"8ff18e25758a05d4deff76ffa4b3407d98722a3c"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Joe Reuter <[email protected]>
viduni94 pushed a commit to viduni94/kibana that referenced this pull request Jan 23, 2025
`useAbortableAsync` can easily get confused about the current state -
e.g. when a previous invocation gets aborted and a new one is started at
the same time, the `loading` state gets set to false _after_ the next
invocation got started, so it's false for the time it's running:


![old](https://github.com/user-attachments/assets/6a784b1a-58b2-4951-8d25-9f109bce39c5)

You can see that while typing, the old slow request is aborted properly,
but the `loading` state gets lost and the abort error from the last
invocation is still set even though a new request is running already.

This is not the only possible issue that could happen here - e.g. if the
promise chain throws too late, an unrelated error could be set in the
error handling logic, which is not related to the currently running
`fn`.

This is hard to fix because as the hook does not control the `fn`, it
does not know at which point it resolves, even after a new invocation
was started already. The abort signal asks the `fn` nicely to throw with
an abort error, but it can't be controlled when that happens.

This PR introduces a notion of the current "generation" and only accepts
state updates from the most recent one.

With this, the new invocation correctly sets the loading state after the
abort - what happens to the old promise chain after the abort can't
affect the state anymore:

![new](https://github.com/user-attachments/assets/b39dd725-6bf1-4ef1-9eb6-d1463e1ec146)

I'm not sure whether this is the best way to resolve this issue, but I
couldn't come up with a better way. Happy to adjust, but I think we need
a solution that doesn't assume any special behavior of the passed in
`fn`, otherwise this helper will always be super brittle.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:version Backport to applied version labels release_note:skip Skip the PR/issue when compiling release notes v8.18.0 v9.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants