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

QueryCursor asyncIterator may not work if the iteration body is asynchronous #9403

Closed
dbellavista opened this issue Sep 9, 2020 · 2 comments
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Milestone

Comments

@dbellavista
Copy link

Do you want to request a feature or report a bug?

bug

What is the current behavior?

Sample script: https://gist.github.com/dbellavista/f9523dc900a3e93a02d4027295897499

the QueryCursor inherit from stream.Readable the [Symbol.asyncIterable] method (see https://nodejs.org/api/stream.html#stream_readable_symbol_asynciterator), allowing it to be used inside the for await or as an async iterable.

// For await
for await (const el of cursor) {}
// Low level iterator
const iterable = cursor[Symbol.asyncIterator]();

Which basically invokes continuously the _read method until the internal buffer is filled. The problem is that when the body of the for await is asyncronous. The _read method closes the cursor and emits the close event

setTimeout(function() {
// on node >= 14 streams close automatically (gh-8834)
const isNotClosedAutomatically = !_this.destroyed;
if (isNotClosedAutomatically) {
_this.emit('close');
}
}, 0);

I think the Readable stream doesn't like this behaviour and stops the async iteration. By increasing the cursor setTimeout from 0 to 1000 the sample script executes correctly.

What happen is:

  1. iterable.next()
  2. Readable._read is invoked and the next value is served to the iteration body
  3. The iteration body executes but goes async
  4. The Readable._read is invoked in "background" until the buffer is full
  5. The Cursor has no more elements, it's closed and the emit event is forcefully invoked
  6. The iteration body continue to execute and invokes iterable.next() again
  7. The Readable stream has been closed so the iteration is over

What are the versions of Node.js, Mongoose and MongoDB you are using? Note that "latest" is not a version.

  • node 12.18.3
  • mongoose 5.10.3
  • mongodb 4.2.9
@vkarpov15 vkarpov15 added this to the 5.10.5 milestone Sep 10, 2020
@vkarpov15
Copy link
Collaborator

I believe this is almost a duplicate of #8280, although #8280 is for aggregation cursors rather than query cursors.

TLDR; the solution is to not use async iterators with Model.find().cursor(). You should use async iterators with Model.find() instead:

// Don't do this! Uses async iterator for streams under the hood
const cursor = Model.find().cursor();
for await (const v of cursor) {
  await wait();
  countForAwaitAsync += 1;
}
await cursor.close();

// Do this instead
for await (const v of Model.find()) {
  await wait();
  countForAwaitAsync += 1;
}

As you mentioned, the problem comes down to the fact that, for legacy reasons, QueryCursor is a Node.js stream, and so it inherits streams' built-in async iterator capability. Unfortunately, streams' built-in async iterator has some quirks with pull mode vs push mode.

We should make QueryCursors support async iteration in a future release to avoid this potential gotcha, or at least throw an error if the user tries to use a cursor as a stream

@dbellavista
Copy link
Author

I understand and I agree the best curse of action should be to throw and exception when trying to access to the asynIterator method, otherwise it may be difficult to spot the error

@vkarpov15 vkarpov15 added the confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. label Sep 13, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Projects
None yet
Development

No branches or pull requests

2 participants