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

aborting stream using the signal does not close the stream when the from generator never finish #47133

Open
rluvaton opened this issue Mar 17, 2023 · 12 comments
Labels
stream Issues and PRs related to the stream subsystem.

Comments

@rluvaton
Copy link
Member

rluvaton commented Mar 17, 2023

Version

v20.0.0-pre (master)

Platform

Darwin MBP 22.3.0 Darwin Kernel Version 22.3.0: Mon Jan 30 20:38:37 PST 2023; root:xnu-8792.81.3~2/RELEASE_ARM64_T6000 arm64

Subsystem

No response

What steps will reproduce the bug?

This fails as the catch is not called

{
  let reached = false;

  const ac = new AbortController();
  let resolve;
  const promise = new Promise((res) => resolve = res);

  const stream = from((async function *() {
    yield 1;
    await promise;
    reached = true;
  })());

  setTimeout(() => ac.abort(), 1000);
  addAbortSignal(ac.signal, stream);
  stream
    .toArray({ signal: ac.signal })
    .then(common.mustNotCall())
    .catch(common.mustCall((e) => {
      strictEqual(e.name, 'AbortError')
      strictEqual(reached, false);
    }));
}

How often does it reproduce? Is there a required condition?

always

What is the expected behavior? Why is that the expected behavior?

to close the stream

What do you see instead?

error:

Mismatched <anonymous> function calls. Expected exactly 1, actual 0.
    at Proxy.mustCall (/open-source/node/node-fork/test/common/index.js:407:10)
    at Object.<anonymous> (/open-source/node/node-fork/test/parallel/test-stream-drop-take.js:30:19)
    at Module._compile (node:internal/modules/cjs/loader:1287:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1341:10)
    at Module.load (node:internal/modules/cjs/loader:1145:32)
    at Module._load (node:internal/modules/cjs/loader:984:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:83:12)
    at node:internal/main/run_main_module:23:47

Additional information

If I omit the ``addAbortSignal(ac.signal, stream);, the stream.destroy` function is not even called

But when I add the abort signal (addAbortSignal(ac.signal, stream);) to the original stream it does get called but still, the test fails.

this is because we get to this line:

const { value, done } = await iterator.throw(error);

but not to this:

await value;

@rluvaton rluvaton changed the title map and forEach operators need to wait for another item before aborting aborting stream using the signal does not close the stream Mar 17, 2023
@rluvaton rluvaton changed the title aborting stream using the signal does not close the stream aborting stream using the signal does not close the stream when the from generator never finish Mar 17, 2023
@rluvaton
Copy link
Member Author

rluvaton commented Mar 17, 2023

I see that this happens with async generator/iterator when next but not awaited and then iterator.throw (which is basically what happens in the stream core)

// Keep process open
setInterval(() => {
}, 10000);

let resolve;
const promise = new Promise(res => resolve = res);
async function* neverEnding() {
  yield 1;
  await promise;

  yield 2;
  yield 3;
}

async function run() {
  const iterator = neverEnding();

  console.log(await iterator.next());
  const res = iterator.next();
  console.log(await iterator.throw(new Error('throw error')));

  console.log('nothing here')
}

run()
  .then(result => console.log('success', result))
  .catch(error => console.log('failure', error))

what get logged is:

{ value: 1, done: false }

is there something we can do to fix this? or this is JavaScript engine (V8) internal bug?

@rluvaton
Copy link
Member Author

rluvaton commented Mar 17, 2023

Looking at the Generator GeneratorResumeAbrupt specs (that was called from Generator.prototype.throw spec) it's not counting for executing [[GeneratorState]] which is the current generator state when we call iterator.throw

In the debugger the [[GeneratorState]] is actually suspended which is weird because it's not one of the possible values which are undefined, suspendedStartsuspendedYieldexecuting, or completed

@debadree25 debadree25 added the stream Issues and PRs related to the stream subsystem. label Mar 18, 2023
@ljharb
Copy link
Member

ljharb commented Mar 19, 2023

https://tc39.es/ecma262/#sec-properties-of-asyncgenerator-intances are the possible values, and GeneratorState is a slot on sync generators, not async generators.

@rluvaton
Copy link
Member Author

Thanks @ljharb, so the example of the async generator not throwing is the expected behavior?

@ljharb
Copy link
Member

ljharb commented Mar 19, 2023

I'm not sure what the expected behavior is here tbh.

However, by aborting after 10ms, you're basically making a race condition - I think there's no guarantee that stream.toArray({ signal: ac.signal }) will settle before the 10ms have elapsed.

@rluvaton
Copy link
Member Author

rluvaton commented Mar 19, 2023

However, by aborting after 10ms, you're basically making a race condition - I think there's no guarantee that stream.toArray({ signal: ac.signal }) will settle before the 10ms have elapsed.

the 10ms is just an example it can be 1s as well and the same result, toArray will never finish

I'm not sure what the expected behavior is here tbh.

Lets say I have this use case:
Taking the first 10 chunks the user wrote, if not written 10 chunks in 10 seconds aborting

code:

const ac = new AbortController();

setTimeout(() => {
  console.log('10s passed')
  ac.abort();
}, 10000);

process.stdin
  .map(item => {
    console.log('item', item)
    return item.toString();
  }, {signal: ac.signal})
  .take(10, {signal: ac.signal})
  .toArray({signal: ac.signal})
  .then(arr => {
    console.log('success', arr);
  })
  .catch((e) => {
    console.error('Aborted', e);
  });

In the current behavior, if you don't type anything and 10s pass, the Aborted log won't be logged until you type something

I don't wanna wait for user input when I close the stream, I expect that when I call ac.abort() it will abort immediately (can be in the next tick of course)

@ljharb
Copy link
Member

ljharb commented Mar 20, 2023

Your then and catch, though, don't take a signal because Promises aren't cancellable.

@rluvaton
Copy link
Member Author

This is why I don't pass them a signal

@jagadeeshmeesala
Copy link

can I work on this issue?

@rluvaton
Copy link
Member Author

rluvaton commented Apr 5, 2023

The problem is if this is even possible, After talking to @giltayar it seems like this can't be fixed as this is how async generator work

WDYT @benjamingr

@benjamingr
Copy link
Member

I was OOO for a while and I'll be abroad until after Passover so my capacity is limited (hence why I didn't weigh in on the transform issue even though I still think the fix is unintuitive).

This just looks like a bug to me where the error is not being propagated correctly, when the stream is destroyed catch should be called with an AbortError even if the generator is being thrown into.

@benjamingr
Copy link
Member

If you remove addAbortSignal the behavior is correct because the stream in fact never finishes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stream Issues and PRs related to the stream subsystem.
Projects
None yet
Development

No branches or pull requests

5 participants