Repository navigation
Make streams default pause mode apply for non data events (such as 'error') #39722
Description
Is your feature request related to a problem? Please describe.
I am trying build a pipeline of streams in conjunction with promises/async code in the midst of the orchestration of the pipeline:
const pipeline = util.promisify(stream.pipeline);
async function queryToFile(querySql, path) {
// promise that resolves on first result from the DB or rejects on SQL error
const queryResults = await dbClient.query(querySql).promiseFirstResult();
const dbStream = queryResults.stream();
const formattingStream = new stream.Transform(...);
return pipeline(dbStream, formattingStream, fs.createWriteStream(path));
}This should work without a problem.
However, one might decide to split this function into multiple ones:
async function queryStream(querySql) {
const queryResults = await dbClient.query(querySql).promiseFirstResult();
return queryResults.stream();
}
async function writeStream(inputStream) {
const formattingStream = new stream.Transform(...);
return pipeline(inputStream, formattingStream, fs.createWriteStream(path));
}
async function queryToFile(querySql, path) {
const dbStream = queryStream(querySql);
return writeStream(dbStream);
}Now, though, the code isn't safe.
If I understand correctly, it could be that in between the time that the queryStream function produces a stream and the time that the writeStream function subscribes to said stream's events (via pipeline), there might be events emitted to the stream, since these functions do not run synchronically and so they make room for other functions -- such as IO -- to run in between.
This is fine for incoming data, as the stream begins in paused mode, buffering data until turning to flowing mode when the stream is piped by pipeline. In contrast, other events such as error or close are not buffered, and are emitted immediately as they are received, which means that the subscribing stream in writeStream will not be aware of them, which is a problem.
Describe the solution you'd like
I would love for the default paused mode -- i.e, buffering events until first "subscriber" -- to apply for non data events (at least the builtin ones). Should it be the default behavior or not is a separate question, but having an ability to provide an option to the stream at construction could prove extremely useful.
As a first step though, it might be helpful to add this scenario to the documentation on streams.
Describe alternatives you've considered
I tried a workaround where before returning a queue from an async function, I added listeners to these events to buffer them, and then overridden each of the event-registration methods (on, once, etc) to push events to any new subscriber of these events. I encountered two problems:
- Using
pipeline, which in turn usespipe, makes it so that if the stream has event listeners prior to the function, it will not propagate these evens further on (I guess it assumes that there's already a handler in place). - The order of event registration by the
pipeline/pipemethod is not necessarily (and is often not) the same as the order in which the events were emitted; for instance,erroris usually emitted beforeclose, while the registration to these events is in the opposite order.
Activity
targos commentedon Aug 9, 2021
@nodejs/streams
eyalroth commentedon Aug 10, 2021
Adding a test case to replicate:
Trying to replace the last
forkthat does nothing withfinished:Results in the process exiting without printing anything (not even the
successmessage).benjamingr commentedon Aug 10, 2021
Thanks for the report.
I'm really not sure I see the case where this is a problem?
Is the scenario:
If the stream emits
errorsynchronously you won't catch it in either case - only if they emit it "in a microtask" you're precisely able to catch it in the first case and not the second. (I vaguely recall discussions about the timing oferrorevents on streams).I am not sure how that's different from the following scanerio:
Can you elaborate on when you ran into this?
(As a note promises don't wait for I/O and pending promises don't block Node.js from exiting)
(I am in favour of adding documentation to always adding error handlers synchronously regardless of
pipeline/compose)eyalroth commentedon Aug 10, 2021
@benjamingr Thanks for the reply!
I'm not sure I follow this scenario, as I don't see how this would happen with promises. For instance, here:
I would expect to catch an error even if the
responsePromisewas rejected during the (async) execution oflongProcessing. It's worth mentioning that my expectation is based mostly on experience with Java/Scala and not so much with NodeJS to which I'm relatively new.According to this code it seems that my expectation is met:
which prints:
However, it does come with a deprecation warning "Unhandled promise rejections are deprecated" and so on (I'm using node 14.16.1), which seems very unfortunate to me.
In short, when trying to stream a result set from a mysql database to S3, while formatting and zipping the results in between the two.
I am using node-mysql2 as a client with their promise-wrapping API (
mysql/promises), while implementing a custom workaround -- described here -- to produce a code similar to my initial (and a bit contrived) example in the original post (dbClient.query(querySql).promiseFirstResult()).mcollina commentedon Aug 10, 2021
The "big" example could be simplified to the following:
The ask is for pipeline to destroy the other streams if the first one is destroyed.
(Note, the proper way to error a stream is to call
stream.destroy(err)- there is quite a bit of machinery behind it and emitting error will not be equivalent).mcollina commentedon Aug 10, 2021
wdyt @ronag?
ronag commentedon Aug 10, 2021
I think this is already fixed on master?
mcollina commentedon Aug 10, 2021
Indeed it is! Which PR did you fix this in? Are we planning to backport?
ronag commentedon Aug 10, 2021
#39235 semver-major
eyalroth commentedon Aug 11, 2021
@mcollina
Is this part of the nightly build? I just tried the both "tests" withv17.0.0-nightly202108113914354cd7and they both fail.My bad. Running from IntelliJ doesn't seem to use the right version (I used
npmfrom the shell instead).With the nightly build it seems that the "short" test case passes and prints:
(not sure if this is intended)
However, the "long" test case fails on the assertion of the data:
Removing this assertion and leaving only the error assertions passes the test.