-
-
Notifications
You must be signed in to change notification settings - Fork 8.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
withAsyncContext race condition #4050
Comments
My fault. The problem is that What happens in this bug report is:
As can be seen, the problem is deeper than just Fundamentally, we would like to run code synchronously, i.e. immediately, when some promises settle. I suppose the only solution is to insert those synchronous calls manually in the right places. The high-level concept is that we want this: const awaitable = ...;
const ctx = saveCtx(); // happens right before await
const result = await awaitable;
ctx.restore(); // must not happen in a Promise, must be right here in sync code
...
clearCtx(); // must not happen in a continuation of this promise, must be right here in sync code (I'm gonna continue with a revised API proposal) |
I'm sharing one failed idea, just in case it inspires someone: I thought I could subclass My idea was to set/remove context in And it does work in Given this, it seems there has to be some calls in user code to set/remove context at boundary points. |
While working on the new proposal I noticed another bug. If you use the current async function useRemoteSum() {
const data = await withAsyncContext(fetch("/data"));
return computed(() => data.reduce((a, b) => a + b, 0));
} Then the caller of this function (e.g. |
Here's a new API proposal. Far futureBasically what we want is an Until then we need a workaround. Quote from that proposal:
General idea
At the end of an async component setup, users must explicitely call Explicit Resource Management is a stage 2 proposal that could make this better. If it lands, About effectScope()
Basically, the APIclass AsyncContext {
#ctx = getCurrentInstance();
async<T>(awaitable: T | PromiseLike<T>) {
setCurrentInstance(null);
return Promise.resolve(awaitable).then(
value => ({ get result() { setCurrentInstance(this.#ctx); return value; } }),
error => ({ get result() { setCurrentInstance(this.#ctx); throw error; } })
);
}
end<T>(result?: T) {
setCurrentInstance(null);
return result;
}
// If one day Explicit Resource Management lands, we could add the following:
[Symbol.dispose]() { this.end() }
} Example usageThis is what is required in hand-written code. async setup() {
const ctx = new AsyncContext();
// ... do stuff
const { result: data } = await ctx.async(fetch("/data"));
// ... do more stuff with data
ctx.end();
}
// Async composable function
// 1. If no Vue function is called after await, then nothing special needs to be done.
// This is a tricky situation, though.
// You must be sure that neither your code, nor any transitive function you call, needs Vue context.
useAsync() {
// Vue API before async call is ok.
const result = computed(() => 42);
// No need for context preservation if context isn't used after the async call
await delay(10);
return result;
}
// 2. If Vue context is required after await, AsyncContext must be used.
useAsync() {
const ctx = new AsyncContext();
const a = computed(() => 42);
const { result: _ } = await ctx.async(delay(10));
// Do more Vue stuff
const b = computed(() => a + 1);
return ctx.end(b);
} Pitfalls and safeguards
|
Thanks for the research into this @jods4 - I think it's ok for the API to be a bit unwieldy as long as it is easy to automatically generate. It isn't that much a burden for userland composition functions to ensure proper placement of |
@yyx990803 03e2684 is not a complete, proper fix. I noticed:
let promise = fetchData();
let data = await promise; // context is already lost here That's why I chose captured the state once at the beginning of code. More thoughts on advanced patternsI realized that for code patterns that manipulate promises without using Here's some code that tries to fetch three things in parallel: const promises = [fetch(1), fetch(2), fetch(3)]
const [data1, data2, data3] = await promises; Using the API I proposed, it's possible to write the following code manually: // at beginning of method
const ctx = new VueContext();
const promises = [
ctx.async(fetch(1), /*stayInContext:*/ true),
ctx.async(fetch(2), true),
ctx.async(fetch(3), true),
];
const { result: [data1, data2, data3] } = await ctx.async(promises);
// at the end of method
ctx.end(); But that can't be a fully automated transformation, because of the Maybe the compiler should allow and recognize // Not required in basic case, but is picked up by compiler
const ctx = new VueContext();
// Additional, manual calls
const promises = [
ctx.async(fetch(1), /*stayInContext:*/ true),
ctx.async(fetch(2), true),
ctx.async(fetch(3), true),
];
// await is handled by compiler itself
const [data1, data2, data3] = await promises;
// end of setup is handled by compiler |
@yyx990803 As you said, this new "fixed" version (03e2684) makes it almost impossible to use for handwritten code. Although it's not going to be possible to have a simple promise wrapper, seeking for an intermediate readable syntax is not crazy imho. |
Version
3.1.3
Reproduction link
sfc.vuejs.org
Steps to reproduce
Note the rendered text.
What is expected?
It should say 'first - first / second - second'.
What is actually happening?
It says 'first - second / second - second'. The 'second' has jumped to the first component.
getCurrentInstance
is returning the second component in both cases.As
withAsyncContext
is new in 3.1.3 it isn't necessarily clear whether what I'm doing is allowed. However...This failure is quite subtle. In 3.1.2 it would have failed obviously and explosively, whereas in 3.1.3 it appears to work and mysteriously yields incorrect values in certain cases.
I haven't stepped through in the debugger but from looking at the code I believe the problem is that the compiled code using
withAsyncContext
is assuming that the microtask queue is only dealing with a single component at once. My example creates two promises that resolve at the same time, so their microtasks get weaved together in the queue and thecurrentInstance
gets set twice before thesetup
code resumes.The text was updated successfully, but these errors were encountered: