-
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
AsapScheduler race condition w/ nested schedules (Cannot read properties of undefined (reading 'execute')) #6672
Comments
I've had a quick read through this and have one comment ATM. Regarding this - in point 3:
The Also, my understanding is that actions scheduled within other actions should not be synchronously executed with their parent action - that is, they should be executed via a separate I'll have a closer look at this later. It's unfortunate that the implementation is so complicated. |
Also, I have a question: IIRC, the others who have raised this issue have seen the problem in Angular applications. Is your application using Angular? |
Thanks for pointing out the Yes, our application uses Angular. |
@cartant, I haven't tested this, but just a thought, maybe something like this would work:
In summary, we create a new async task for C, but C slipping into B's position in the queue "tricks" the guard logic in the |
@matthewjh What you've mentioned in the above comment sounds reasonable. I can imagine that unsubscriptions from within executed actions could be the culprit. I'll have a closer look/think about this later. I'm curious though, is the action unsubscription scenario that you've mentioned something that could occur in your app? |
I've had a bit of a look at this. If you run the following: import { asapScheduler, Subscription } from "rxjs";
let a: Subscription;
let b: Subscription;
let c: Subscription;
const originalFlush: any = asapScheduler.flush;
asapScheduler.flush = function (...args: any[]) {
console.log(`flush; actions.length = ${asapScheduler.actions.length}`);
return originalFlush.apply(asapScheduler, args);
};
a = asapScheduler.schedule(() => {
c = asapScheduler.schedule(() => {
console.log("executed c");
});
console.log("scheduled c");
b.unsubscribe();
console.log("unsubscribed b");
console.log("executed a");
});
console.log("scheduled a");
b = asapScheduler.schedule(() => {
console.log("executed b");
});
console.log("scheduled b"); You'll see that
And if the
However, the error is not effected because when rxjs/src/internal/scheduler/AsyncAction.ts Line 108 in ecbc6c1
And that effects a rxjs/src/internal/scheduler/AsapAction.ts Lines 33 to 36 in ecbc6c1
So ... 🤔 |
@cartant how about this? I've added logging to our code, and this appears to be what is happening.
If so, the problem is that each task keeps track of its own "flush task id" and can clear the task in recycleAsyncId, but the check required to do so reads a data structure ( |
Yeah, I've verified that this reproduces the problem: a = asapScheduler.schedule(() => {
c = asapScheduler.schedule(() => {
console.log("executed c");
});
console.log("scheduled c");
c.unsubscribe();
console.log("unsubscribed c");
a.unsubscribe();
console.log("unsubscribed a");
console.log("executed a");
});
console.log("scheduled a");
b = asapScheduler.schedule(() => {
console.log("executed b");
});
console.log("scheduled b"); But I think the solution should involve ensuring that actions scheduled within the Thanks so much for your help on this. |
Fantastic, thank you @cartant. Interested to see your fix when it's ready. |
…bing during flushes * chore: use sinon sandbox consistently * test: add failing flush tests * fix: don't execute actions scheduled within flush * test: add failing tests * fix: avoid calling flush with empty actions queue Closes ReactiveX#6672 * chore: remove accidental auto-import * test: call all the dones
…bing during flushes * chore: use sinon sandbox consistently * test: add failing flush tests * fix: don't execute actions scheduled within flush * test: add failing tests * fix: avoid calling flush with empty actions queue Closes #6672 * chore: remove accidental auto-import * test: call all the dones
Bug Report
Current Behavior
On our non-trivial, non-public UI codebase that heavily uses rx.js, we sporadically see the following error arising from code that uses the
AsapScheduler
:TypeError: Cannot read properties of undefined (reading 'execute')
at AsapScheduler.flush (node_modules/rxjs/src/internal/scheduler/AsapScheduler.ts:19:8
This is thrown from
rxjs/src/internal/scheduler/AsapScheduler.ts
Line 16 in 1f7b9c5
I have debugged this in-situ and have arrived at what is, I think, a working understanding how the exception arises. I believe it to be a problem with nested
AsapTask
schedules.Note there have been reports of similar issues in #4690 and #2697, which were closed without resolution. I think it likely all these share this cause.
AsapAction
, A, is scheduled. The scheduler's actions are[A]
. When A is executed, it will schedule anotherAsapAction
, B.this._scheduled = undefined;
https://github.com/ReactiveX/rxjs/blob/master/src/internal/scheduler/AsapScheduler.ts#L5
action.execute()
on A. Itshifts
A off so the actions queue is now[]
.(Here,
index
is initially -1 and count is the length of the queue on enteringflush
, which is 1.)AsapAction
B (as mentioned). The action queue is now[B]
. AsasapScheduler._scheduled
was set toundefined
on enteringAsapScheduler#flush
, the following code will create a new async task that will flush the scheduler:https://github.com/ReactiveX/rxjs/blob/master/src/internal/scheduler/AsapAction.ts#L21
action.execute
on A, the while expression will be evaluated. Bear in mind that we mutatedactions
in (4). The first part is true:(++index < count) === (++(-1) < 1) === (0 < 1) === true
The second part,
action = actions.shift()
will pop B from the actions queue and set the current action to B.action.execute()
on B.AsapActions
were scheduled after B, thisflush
task is now redundant as B has already been executed and popped from the actions queue.[]
,action = action || actions.shift()!;
isundefined
.action.execute(action.state, action.delay)
.One interesting footnote is that in our codebase, it appears to be quite common for an
AsapAction
to schedule anotherAsapAction
. But - this exception is rare. The question is why. I suspect the following code serves to mask the issue in most instances, because it happens to remove the async task if the action queue is empty:https://github.com/ReactiveX/rxjs/blob/master/src/internal/scheduler/AsapAction.ts#L23
However, as far as I can tell (not familiar with this code), if this happens to clean up dangling async tasks created by this scenario, it would be coincidence - they are logically distinct code paths.
Expected behavior
No exception
Reproduction
** Please provide repro code does not involves framework, or other setups to address this bug is specific to rxjs core **
I don't have one at the moment but will try to make a stackblitz or failing test case on an rxjs fork.
Environment
Possible Solution
The simplest fix is to fix the flawed assumption in
AsapScheduler
flush that there will always be actions in the queue. As the reports of this exception demonstrate, this is not always true. However, it would be even better in this case to not schedule the flush at all.I'm assuming nested AsapAction schedules should indeed be flushed in the same turn, similar to microtasks. In that case, I think there is an argument that
asapScheduler._scheduled
should not be set toundefined
until after all the actions are executed in the flush loop.asapScheduler._scheduled
is what drives the "do I need to schedule another flush?" condition (https://github.com/ReactiveX/rxjs/blob/master/src/internal/scheduler/AsapAction.ts#L21). IfasapScheduler._scheduled?
answers the question "is there already a time in the future in which my new action will be executed?", then the answer needs to be "yes" inside the action execution loop. It is logically inconsistent to set this toundefined
at the start offlush
, answering "no" to the question, when that is not quite true: actions scheduled inside the action execution loop will be executed in the current flush.That solution would look like this:
However, I'm not sure whether there are reasons NOT to do this.
From looking at one of the linked issues, as well as the code, it looks like other schedulers may suffer from the same loophole (e.g. the RAF scheduler).
Additional context/Screenshots
Add any other context about the problem here. If applicable, add screenshots to help explain.
@cartant @notnarb @decafdennis
The text was updated successfully, but these errors were encountered: