Unsubscribe deadlocks AsyncGenerator #654
Replies: 2 comments 2 replies
-
While indeed inconvenient, this is not a design flaw with the AsyncGenerator. This actually mirrors how synchronous generators work - you can't forcibly terminate a generator that's in the middle of executing code. The async version extends this principle to async operations. That is an important safety mechanism! Let's go over an example where abrutply killing/ending a generator on async function* subscribeToEvents() {
const eventSource = new EventSource('/events');
try {
while (true) {
// in a forever loop to get all events until cancelled
const event = await waitForNextEvent(eventSource);
yield event;
}
} finally {
// very important cleanup, must remove listener to avoid memory leaks
eventSource.close();
}
} If this design decision did not exist and our approach was naive: calling a The solution to this issue is to have the Keeping the example in mind, the same principle applies to next/return, if P.S. I personally recommend using RepeaterJS. |
Beta Was this translation helpful? Give feedback.
-
Yeah, I get why it needs to work this way. My particular beef with the design of AsyncGenerators is that none of the native language support lets you do this without opening yourself up to deadlocks. For anything beyond trivial examples, you have to roll your implementation. The AsyncGenerators being this way are not graphql-ws's fault, obviously. But, all the examples I found in the docs use the Debugging it took about 30 minutes so it wasn't too bad, but it's not an obvious trap until you've already fallen into it once. IMO, the graphql-ws recipes should have at least an example or two of the real-world version most folks would need to use and ideally a word of caution about using any of the toy examples in a real app. Would save others time. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Screenshot
none
Expected Behaviour
When a client wants to cancel a subscription, it sends a "complete" message:
This is intended to close the AsyncGenerator returned by the resolver's
subscribe()
function. graphql-ws does this by using thereturn()
method on the AsyncGenerator. In all situations, the AsyncGenerator should actually stop.Actual Behaviour
If the AsyncGenerator's
next()
function is being awaited by the subscription when thatreturn()
call happens, the generator will not return until thenext()
call resolves. For all intents and purposes, this is a deadlock that will not resolve unless whatever is blockingnext()
returns. This means that any implementation that needs to clean up subscriber resources will not do so.This is a well-known problem with AsyncGenerators in Javascript, even seen before with graphql. There is no fixing this deficiency with AsyncGenerators, as they are operating as (badly) designed. And there is a workaround for it: make your own AsyncIterator implementation that implements the
return
call directly rather than use the native language feature. It's a pain, but you can see from this example that it works: https://codesandbox.io/p/sandbox/async-generator-vs-iterator-returns-s4shtIMO, this gotcha should at the very least be documented clearly. Might have missed something, but I didn't see this anywhere in the graphql-ws docs. It might also be a good idea to expose an
onUnsubscribe
method onuseServer()
to allow implementers to signal to their AsyncGenerator to reject the awaited promise.Debug Information
none
Further Information
none
Beta Was this translation helpful? Give feedback.
All reactions