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

PubSub iterator promise never resolves if return() called too soon #201

Open
arendjr opened this issue May 10, 2019 · 7 comments
Open

PubSub iterator promise never resolves if return() called too soon #201

arendjr opened this issue May 10, 2019 · 7 comments
Assignees

Comments

@arendjr
Copy link

arendjr commented May 10, 2019

The following snippet will result in an iterator promise returned by next() that never resolves:

const iterator = new PubSub().asyncIterator();
const promise = iterator.next();
iterator.return();
await promise; // will wait forever

The trick is that return() gets called synchronously while the first call to next() is still setting up its subscriptions.

This appears to be a regression since 1.0 since I have a test suite that was green and started failing on this issue when I upgraded to 1.1.

I have tried various solutions, including monkey-patching emptyQueue() like this:

PubSubAsyncIterator.prototype.emptyQueue = async function() {
    if (this.running) {
        this.running = false;
        this.pullQueue.forEach(resolve => resolve({ value: undefined, done: true }));
        this.pullQueue.length = 0;
        this.pushQueue.length = 0;
        const subscriptionIds = await this.allSubscribed;
        if (subscriptionIds) {
            this.unsubscribeAll(subscriptionIds);
        }
        // This one should make sure new next() calls while we were awaiting and resolved:
        this.pullQueue.forEach(resolve => resolve({ value: undefined, done: true }));
    }
};

But interestingly, doing so made other tests in my suite fail for reasons I'm afraid I don't understand right now... Ignore this last part, that failure seemed to be caused by me mis-using Jest's .resolves method (forgetting to await it).

@grantwwu grantwwu self-assigned this May 13, 2019
@arendjr
Copy link
Author

arendjr commented May 13, 2019

Okay, I think I have gotten to the bottom of it. I can get all my tests green again as long as I call iterator.next() before doing anything else in my tests. The fact that things that got published previously (despite no one waiting for it yet), which don't get published anymore unless iterator.next() has been called (due to the lazy initialization of the subscriptions) wreaked quite some havoc in my tests. But I can solve it now without needing any further patching of the graphql-subscriptions library.

Still, given this experience, I believe it would have been more appropriate to push this change with a major version bump rather than a minor one, because despite the API technically not changing, the semantics were certainly changed by the introduction of this lazy initialization.

I'm also not entirely sure whether it's safe to close this issue now, because I find it hard to reason whether a situation like the snippet I originally described could be triggered in production usage. To be on the safe side, I'd say the PubSub implementation should still guard against it.

@grantwwu
Copy link
Contributor

I'll take a look at this. I feel like the behavior in the original snippet you posted should be that promise should resolve immediately with no values.

@arendjr
Copy link
Author

arendjr commented May 14, 2019

Thanks!

@TimSusa
Copy link

TimSusa commented May 30, 2019

@grantwwu
Copy link
Contributor

@TimSusa I don't know what "JFI" means here or how that issue is relevant.

@arendjr I've been really busy at work, but at first glance, I think the issue is that next() doesn't do any checking to see if the iterator has been terminated. I'll try to get a PR out later today.

@TimSusa
Copy link

TimSusa commented Jun 13, 2019

@grantwwu Sry for being quiet. I just wanted to point out and tag, that our issue could maybe have something to do with the issue meant here. If this is not the case in your opinion, just drop a comment. Thanks for help and Greets from Berlin!

@JeffML
Copy link

JeffML commented Jun 17, 2020

Update:
It seems setImmediate() isn't as immediate as the name suggests. By executing publish() through setImmediate, the publish event is delayed long enough for the subscriber to receive it via sub.next(). Otherwise, the publish sends the event before sub.next() is called.


This might be related:

I'm writing a Jest test and I have to call setImmediate to invoke pubsub.publish() before I call the async iterator from subscribe, otherwise next() never returns:

import { graphql, validateSchema, subscribe, parse, ExecutionResult } from 'graphql'
import { schema, client, pubsub } from './schema'   // pubsub => graphql-subscription instance

  test('subscriptions', async () => {
    const document = parse(`
      subscription {
        create 
      }
    `)

    const sub = <AsyncIterator<ExecutionResult>>await subscribe(schema, document);
    expect(sub.next).toBeDefined()

    /** why is setImmediate needed here? **/
    setImmediate(() => pubsub.publish('CREATE_ONE', { create: "FLUM!" }))

    /** if setImmediate isn't used, the following promise is never resolved and test will timeout
    const { value: { errors, data } } = await sub.next()

    expect(errors).toBeUndefined()
    expect(data).toBeDefined()
    expect(data.create).toBe('FLUM!')
  })

The need for setImmediate() is confusing, non-obvious and seems to rely on a side-effect. Awaiting form the publish() promise doesn't work, either.

More details here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants