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

Writable is destroyed before calling callback in rare case #40377

Closed
szmarczak opened this issue Oct 8, 2021 · 15 comments
Closed

Writable is destroyed before calling callback in rare case #40377

szmarczak opened this issue Oct 8, 2021 · 15 comments
Labels
confirmed-bug Issues with confirmed bugs. doc Issues and PRs related to the documentations. good first issue Issues that are suitable for first-time contributors. linux Issues and PRs related to the Linux platform. stream Issues and PRs related to the stream subsystem.

Comments

@szmarczak
Copy link
Member

szmarczak commented Oct 8, 2021

Version

v16.10.0

Platform

Linux solus 5.14.7-198.current #1 SMP PREEMPT Wed Sep 22 16:02:46 UTC 2021 x86_64 GNU/Linux

Subsystem

stream

What steps will reproduce the bug?

const {Writable} = require('stream');

class X extends Writable {
    async _destroy(error, callback) {
        (async () => {
            await new Promise(resolve => setTimeout(resolve, 10));
            console.log(w._writableState.closed);

            callback(error);
        })();
    }
}

const w = new X();

w.once('error', error => {
    console.log(error);
});

w.destroy(new Error('oh no!'));

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

Always.

What is the expected behavior?

false
Error: oh no!

What do you see instead?

true

Additional information

Some undocumented behavior here:

try {
const result = self._destroy(err || null, onDestroy);
if (result != null) {
const then = result.then;
if (typeof then === 'function') {
then.call(
result,
function() {
process.nextTick(onDestroy, null);
},
function(err) {
process.nextTick(onDestroy, err);
});
}
}
} catch (err) {
onDestroy(err);
}

@szmarczak szmarczak changed the title Writable is destroyed before calling callback Writable is destroyed before calling callback in rare case Oct 8, 2021
@iam-frankqiu iam-frankqiu added stream Issues and PRs related to the stream subsystem. linux Issues and PRs related to the Linux platform. labels Oct 8, 2021
@targos
Copy link
Member

targos commented Oct 9, 2021

@nodejs/streams

@ronag
Copy link
Member

ronag commented Oct 9, 2021

Don’t mix thenable with callback.

@mcollina
Copy link
Member

mcollina commented Oct 9, 2021

The behavior is correct for me. After .destroy() is called, the stream is closed synchronously and no more read or write operation can happen. However the destroy cycle is asynchronous because it might take a while to completely clean up a native resource.

@mcollina mcollina closed this as completed Oct 9, 2021
@szmarczak
Copy link
Member Author

szmarczak commented Oct 9, 2021

@mcollina I forgot to include Error: oh no! in expected behavior. The error is not emitted. The issue is that if _destroy returns a thenable, there's a race condition - either the callback gets called first or the promise resolves. If it waits for the thenable, it should omit the callback argument (or throw when it gets called). Also the documentation doesn't mention anything about _destroy being thenable. Therefore I consider this a bug.

@mcollina mcollina reopened this Oct 9, 2021
@mcollina
Copy link
Member

mcollina commented Oct 9, 2021

Uh? Using a thenable there should not be supported.

@ronag
Copy link
Member

ronag commented Oct 9, 2021

I think this is a case of insufficient documentation.

@mcollina
Copy link
Member

I have a feeling I missed something when we added thenable support to destroy.

I'll need to dig into this and check out what's the problem.

@mcollina
Copy link
Member

This was added in 744a284 without documentation.

The error is not printed because it is not rethrown by the _destroy function. Moreover, the callback should not be mixed with async/await (nor is needed).

@ronag
Copy link
Member

ronag commented Oct 10, 2021

I think we could maybe emit a warning if function returns a thenable when the function.length has the value for the callback signature.

@szmarczak
Copy link
Member Author

szmarczak commented Oct 10, 2021

Wouldn't it be better to throw after the promise resolves and the callback gets called?

@mcollina
Copy link
Member

Wouldn't it be better to throw after the promise resolves and the callback gets called?

if you implement _destroy(), you need to rethrow or call the callback with the error passed as an argument.

@szmarczak
Copy link
Member Author

szmarczak commented Oct 10, 2021

Again there's this situation where callback may be called after the promise resolves. Currently calling the callback doesn't do anything because at that point the stream is destroyed already because the promise resolved.

One can expect callback not to throw, so indeed a warning would be sufficient I think.

@mcollina mcollina added doc Issues and PRs related to the documentations. confirmed-bug Issues with confirmed bugs. labels Oct 11, 2021
@mcollina
Copy link
Member

The documentation must be updated to better specify all of this. I would recommend to add a warning as well when using a promise with a callback.

@Mesteery Mesteery added the good first issue Issues that are suitable for first-time contributors. label Oct 11, 2021
@Mesteery
Copy link
Member

I think this is a good first issue.

TiagodePAlves added a commit to TiagodePAlves/node that referenced this issue Dec 14, 2021
Update _destroy to only call onDestroy in case of errors, waiting for the user implementation to call onDestroy on the success path of async functions as well.

Fixes: nodejs#40377
TiagodePAlves added a commit to TiagodePAlves/node that referenced this issue Dec 14, 2021
Update _destroy to only call onDestroy in case of errors,
waiting for the user implementation to call onDestroy on
the success path of async functions as well.

Fixes: nodejs#40377
@RafaelGSS
Copy link
Member

@mcollina it can be closed #41040 was merged.

@ronag ronag closed this as completed Dec 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed-bug Issues with confirmed bugs. doc Issues and PRs related to the documentations. good first issue Issues that are suitable for first-time contributors. linux Issues and PRs related to the Linux platform. stream Issues and PRs related to the stream subsystem.
Projects
None yet
7 participants