Skip to content

unix: add uv_flush_sync#876

Closed
Fishrock123 wants to merge 1 commit into
libuv:v1.xfrom
Fishrock123:uv_flush_sync
Closed

unix: add uv_flush_sync#876
Fishrock123 wants to merge 1 commit into
libuv:v1.xfrom
Fishrock123:uv_flush_sync

Conversation

@Fishrock123

Copy link
Copy Markdown
Contributor

Required for nodejs/node#6773

@saghul Assuming this has to also be implemented on windows (although node doesn't need it on windows..) what's the windows version of uv__write? Do I need to switch over the handle type and call multiple methods?

I'm also not sure what kind of docs note should go here?

@Fishrock123

Copy link
Copy Markdown
Contributor Author

also cc @cjihrig & @bnoordhuis

@saghul

saghul commented May 17, 2016

Copy link
Copy Markdown
Member

I really don't think this is a solution. The entire libuv model is built around async operations, and you want libuv to add a convenience function because Node breaks that assumption, just for stdout and stderr.

I won't fight it to death, but I'm -1.

@Fishrock123

Copy link
Copy Markdown
Contributor Author

@saghul How can we do this from the node side? Do you have suggestions? Should we just float this?

Should we be doing an async flush and calling exit when that is done?

@saghul

saghul commented May 17, 2016

Copy link
Copy Markdown
Member

@Fishrock123 floating is not a solution. What node can do is use uv_walk, close all handles which are not stdio nor Node internal handles and keep the loop going until the tty handles have flushed. Then close them.

@Fishrock123

Copy link
Copy Markdown
Contributor Author

@saghul I suspect may people will object to that. :(

@saghul

saghul commented May 17, 2016

Copy link
Copy Markdown
Member

If the previous behavior was to have them block, we can always fallback to that with some strategically placed setBlocking while we come up with a clean solution, right?

@Fishrock123

Copy link
Copy Markdown
Contributor Author

@saghul For TTYs, yes, but this problem still persists for pipes.

Comment thread src/unix/stream.c
* Returns 0 on success, non-zero on failure.
*/
int uv_flush_sync(uv_stream_t* stream) {
int rc = uv_stream_set_blocking(stream, 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not reset blocking to previous value? why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but it sounds like that won't help get this anywhere.

This was an example patch cooked up that Node.js may need in some form.

@kzc

kzc commented May 25, 2016

Copy link
Copy Markdown

What node can do is use uv_walk, close all handles which are not stdio nor Node internal handles and keep the loop going until the tty handles have flushed. Then close them.

There is no loop at this point prior to exit(). But let's say there is a loop - can the process described be done without invoking callbacks back into node that would prolong (or prevent) the actual exit()?

@saghul

saghul commented May 25, 2016

Copy link
Copy Markdown
Member

There is no loop at this point prior to exit(). But let's say there is a loop - can the process described be done without invoking callbacks back into node that would prolong (or prevent) the actual exit()?

There is a loop, it's just not running. Otherwise handles would be dangling. The process described can be done. The trick is to close all handles except those used for stdio before running the loop again.

@kzc

kzc commented May 25, 2016

Copy link
Copy Markdown

There is a loop, it's just not running.

:-) Yes, that's the current dilemma.

The trick is to close all handles except those used for stdio before running the loop again.

And all the non-fd-related tasks would have to be unregistered - timer, idle, thread, (dns?) etc. Would also have to make the libuv callbacks no-ops at this point. It all could be done, it's just not trivial.

@saghul

saghul commented May 25, 2016

Copy link
Copy Markdown
Member

It all could be done, it's just not trivial.

I don't disagree :-S

I wonder if it wouldn't make sense for Node to use the C stdio family of functions for std{out,err}, and our uv_tty_t just for stdin.

This way, at the end of the program Node would just fflush the out and error streams and be doen with it.

I'll try to put together an addon, if I can find the time.

@kzc

kzc commented May 25, 2016

Copy link
Copy Markdown

I wonder if it wouldn't make sense for Node to use the C stdio family of functions for std{out,err}, and our uv_tty_t just for stdin.

libc has its own buffer(s) and once full they block. Plus it would make the code path to output to stdout different from sockets, pipes and files.

A libuv flush function may not be philosophically in tune with libuv but it's certainly easier to implement than the alternatives.

@saghul

saghul commented May 25, 2016

Copy link
Copy Markdown
Member

It's just an idea.

There seem to be 2 problems intertwined here: people want all stdout/err to be shown when Node ends, and people want stdout/err not to interleave.

Flushing only seems to solve one of them. The other would be solved by doing synchronous writes, which some seem to like / want.

@bnoordhuis

bnoordhuis commented May 25, 2016

Copy link
Copy Markdown
Member

A libuv flush function may not be philosophically in tune with libuv but it's certainly easier to implement than the alternatives.

That's not an argument I personally am very sympathetic to. I agree with @saghul: it's essentially a libuv user problem (i.e., node), not libuv itself.

So, one potential solution I see is this:

  1. Only write to stdout and stderr using uv_try_write().
  2. If the write fails with UV_EAGAIN, queue the data and queue a zero-sized write with uv_write().
  3. When the write callback is invoked, try uv_try_write() again.
  4. On exit, switch stdout and stderr to blocking mode and uv_try_write() the remaining pending data.

Order of stdout and stderr can be preserved without undue effort. It wouldn't work reliably when the process is terminated by a signal but that's no different from when we push everything through fwrite().

@saghul

saghul commented May 25, 2016

Copy link
Copy Markdown
Member

Good idea @bnoordhuis! One possible caveat is that uv_try_write is not supported for pipes on Windows.

@kzc

kzc commented May 25, 2016

Copy link
Copy Markdown

Another solution would be to create a libuv function to retrieve the pending write bytes for a given uv stream and then clear those pending writes from libuv - essentially a transfer of ownership of the data. At that point node user code would block stdout and flush as required.

@bnoordhuis

Copy link
Copy Markdown
Member

Cancelling outstanding writes is fraught with issues on Windows, I'm afraid.

I have a PR that adds uv_try_write() support for pipes on Windows (hopefully): #888

@kzc

kzc commented May 25, 2016

Copy link
Copy Markdown

As for interleave, such fine grained behavior can only be guaranteed to be deterministic when the all file descriptors involved (stdout and stderr) are blocking.

@kzc

kzc commented May 25, 2016

Copy link
Copy Markdown

Cancelling outstanding writes is fraught with issues on Windows, I'm afraid.

Could you offer some detail? If libuv is single threaded for all writes and the data hasn't yet been passed to a write function why would this be an issue?

@bnoordhuis

Copy link
Copy Markdown
Member

As for interleave, such fine grained behavior can only be guaranteed to be deterministic when the all file descriptors involved (stdout and stderr) are blocking.

What do you mean? That's merely a matter of maintaining total ordering.

Could you offer some detail? If libuv is single threaded for all writes and the data hasn't yet been passed to a write function why would this be an issue?

I'm not sure what you mean by 'write function' but we use IOCP on Windows, which is a (kind of, sort of) thread pool.

@saghul

saghul commented May 25, 2016

Copy link
Copy Markdown
Member

When pipes are non-overlapped (which happens when passing them for stdio IIRC) reads and writes happen in a thread pool (sssh, don't tell anyone!), and the only way to cancel all i/o would be to use CancelIOEx, which only works in Windows >= Vista, but libuv v1.x still supports XP.

@kzc

kzc commented May 25, 2016

Copy link
Copy Markdown

What do you mean? That's merely a matter of maintaining total ordering.

Ordering was never historically maintained for node 4.x, 5.x and 6.x on Linux once the 64KB OS-level write buffer was exceeded for non-blocking tty fds. Mac only buffers 1024 bytes for tty writes to non-blocking fds. I don't know the OS-level write buffer sizes for FreeBSD and Solaris for ttys. Ordering writes only worked on Mac historically in node 4.x/5.x because it was inadvertently blocking stdio writes on that platform.

When pipes are non-overlapped (which happens when passing them for stdio IIRC) reads and writes happen in a thread pool (sssh, don't tell anyone!)

That explains it.

Since we're discussing write order and given the use of IOCP and a thread pool on Windows, is write order between stdout and stderr presently preserved on Windows?

@saghul

saghul commented May 25, 2016

Copy link
Copy Markdown
Member

Since we're discussing write order and given the use of IOCP and a thread pool on Windows, is write order between stdout and stderr presently preserved on Windows?

For non-overlapped pipes I don't think so. Though this was never guaranteed.

@kzc

kzc commented May 25, 2016

Copy link
Copy Markdown

Though this was never guaranteed.

If that's the case then the only platform that truly preserved write order between stdout and stderr was Mac on node 4.x and 5.x. It wasn't a universal thing.

@saghul

saghul commented May 25, 2016

Copy link
Copy Markdown
Member

If that's the case then the only platform that truly preserved write order between stdout and stderr was Mac on node 4.x and 5.x. It wasn't a universal thing.

Correct. That was kind of an "accident".

@kzc

kzc commented May 25, 2016

Copy link
Copy Markdown

Correct. That was kind of an "accident".

That is why I believe that interleave should not be a concern of libuv, nor node for that matter. If such write fidelity between the two streams is desired then stdout and stderr can be made blocking at program start.

But the flushing of process.stdout and process.stderr upon process.exit() matter is a different issue. The fds are not guaranteed to be blocking, nor are they guaranteed to be ttys.

@Fishrock123

Copy link
Copy Markdown
Contributor Author

Flushing only seems to solve one of them. The other would be solved by doing synchronous writes, which some seem to like / want.

The blocking problem we appear to be able to handle separately, this only needs to be about flushing on exit.

@Fishrock123

Copy link
Copy Markdown
Contributor Author

That is why I believe that interleave should not be a concern of libuv, nor node for that matter. If such write fidelity between the two streams is desired then stdout and stderr can be made blocking at program start.

Perhaps on libuv's end, but not on node's end, as described in this comment by @piscisaureus on historical context: nodejs/node#6816 (comment)

@kzc

kzc commented May 25, 2016

Copy link
Copy Markdown

@Fishrock123 I'm fine with node making process.stdout and process.stderr ttys blocking at start up on all platforms.

@saghul

saghul commented Sep 15, 2016

Copy link
Copy Markdown
Member

I'm closing this since there are no plans to land this in libuv and Node already implemented a "workaround" by doing blocking writes. We can refer this issue in the future if we need to revisit.

Thanks everyone for the discussion!

@saghul saghul closed this Sep 15, 2016
@kzc

kzc commented Sep 15, 2016

Copy link
Copy Markdown

Was the piped tty truncation bug in node upon process.exit() also fixed?

@saghul

saghul commented Sep 15, 2016

Copy link
Copy Markdown
Member

I think it was, since all writes are blocking now.

On Sep 15, 2016 13:44, "kzc" notifications@github.com wrote:

Was the piped tty truncation bug in node upon process.exit() also fixed?


You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub
#876 (comment), or mute
the thread
https://github.com/notifications/unsubscribe-auth/AATYGL6ubDErIRoboCMluWyKCkdCzbOKks5qqS-PgaJpZM4IgcRT
.

@kzc

kzc commented Sep 15, 2016

Copy link
Copy Markdown

Checking... the piped tty truncation bug still exists on UNIX:

It still has a known issue test:

https://github.com/nodejs/node/blob/master/test/known_issues/test-stdout-buffer-flush-on-exit.js

@saghul

saghul commented Sep 15, 2016

Copy link
Copy Markdown
Member

AFAICT the resolution at this point is that this is a problem Node needs to
solve, not libuv. (I might have missed something since it's been a while...)

On Sep 15, 2016 14:15, "kzc" notifications@github.com wrote:

Checking... the piped tty truncation bug still exists on UNIX:

It still has a known issue test:

https://github.com/nodejs/node/blob/master/test/known_
issues/test-stdout-buffer-flush-on-exit.js


You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub
#876 (comment), or mute
the thread
https://github.com/notifications/unsubscribe-auth/AATYGNrognSkmGzYPmnE2N_KX-yHR-5gks5qqTbMgaJpZM4IgcRT
.

@kzc

kzc commented Sep 15, 2016

Copy link
Copy Markdown

I don't think the bug can be resolved without libuv support since the data is in-flight.

No matter. The Node.JS maintainers are apparently indifferent to tty pipe truncation upon process.exit() or uncaught exception.

@saghul

saghul commented Sep 15, 2016

Copy link
Copy Markdown
Member

I don't think the bug can be resolved without libuv support since the data is in-flight.

If the data is in-flight that would mean the write request has not completed yet. Waiting for the tty handles to properly close should be sufficient, I think.

@kzc

kzc commented Sep 15, 2016

Copy link
Copy Markdown

By in-flight I meant data was passed to libuv, but not yet written, and not accessible from libuv users like node. The exact case uv_flush_sync handled. Because UNIX writes are all handled in a single thread there's no issue with threads or race conditions.

This has all been hashed over before in countless threads, and we're not covering new ground. I was just interested in solving a real world node bug affecting peoples' applications (including my own) - reliably flushing piped tty data upon process.exit() when you cannot return control to the event loop. But the fix was deemed to be "not in the spirit of libuv" and the node bug will remain unfixed.

@saghul

saghul commented Sep 15, 2016

Copy link
Copy Markdown
Member

This has all been hashed over before in countless threads, and we're not covering new ground. I was just interested in solving a real world node bug affecting peoples' applications (including my own) - reliably flushing piped tty data upon process.exit() when you cannot return control to the event loop. But the fix was deemed to be "not in the spirit of libuv" and the node bug will remain unfixed.

Sure, I know you have good intentions. Alas we haven't yet come across a suitable solution on the libuv side, which is consistent with the library :-S

@kzc

kzc commented Sep 15, 2016

Copy link
Copy Markdown

In light of the fact there is no user land workaround for the issue, prioritizing perceived implementation design purity over a pragmatic 10 line patch that fixes such a fundamental I/O bug affecting node users is puzzling to me.

Oh well, I tried.

@saghul

saghul commented Sep 15, 2016

Copy link
Copy Markdown
Member

@kzc I can reopen the discussion, but this patch was already deemed not appropriate. There are ways to solve that, but those imply having to run the loop some more, which is a problem for Node.

@kzc

kzc commented Sep 15, 2016

Copy link
Copy Markdown

Every other potential solution is an order of magnitude more complicated to resolve an issue that only effects the flushing of a couple of file descriptors immediately prior to libc exit().

@saghul

saghul commented Sep 16, 2016

Copy link
Copy Markdown
Member

I don't disagree, but I don't see why we should compromise.

The way I see it, this is similar to libc exit vs _exit. If you do exit() then all fds are closed, the streams flushed and everyone is happy. Properly closing the event loop would be the equivalent. This means closing all handles, then waiting for the close callbacks, and finally cleaning up the loop itself. If you exit your program with _exit(), no streams are flushed, which would be equivalent to using uv_stop and breaking from the loop.

AFAIK trying to solve the problem this way hasn't been attempted yet, so I guess it's possible it's not an order of magnitude more complicated. Adding a hack will always be quicker, but we don't want to open that door.

@kzc

kzc commented Sep 16, 2016

Copy link
Copy Markdown

The term "hack" is arbitrary. One person's "hack" is another's engineering solution. No one would claim that any code base of this size is perfect in every way. Node.JS and libuv are not ends unto themselves - they exist to solve users' problems. There's a fundamental I/O bug in node that is easily fixed with a trivial patch and Node.JS and libuv are choosing not to do so. No one is stepping up to fix this problem in a way that suits some people's aesthetic sensibilities so a bug affecting real node users will not be fixed.

Even if the patch were floated over node's copy of libuv it'd be better than the status quo.

I'd be curious to get @rvagg and @Fishrock123's take on this.

@addaleax

Copy link
Copy Markdown
Contributor

@bnoordhuis Regarding your proposed solution in #876 (comment): That doesn’t work because zero-sized writes may succeed in cases where writes carrying data don’t.

@saghul Any chance this could get reopened? I see where your opposition is coming from but, like @kzc said, I think the flexibility it offers to users of libuv outweighs the downside of having an edge case API that doesn’t 100 % match libuv’s paradigm, especially in light of the fact that currently this forces users to abandon the advantages that libuv provides.

@bnoordhuis

Copy link
Copy Markdown
Member

Regarding your proposed solution in #876 (comment): That doesn’t work because zero-sized writes may succeed in cases where writes carrying data don’t.

The zero-sized write is just to get a writability notification event. It's basically implementing readiness-based I/O on top of libuv's completion-based I/O model.

@addaleax

addaleax commented May 27, 2017

Copy link
Copy Markdown
Contributor

@bnoordhuis Sorry, I wasn’t clear: The zero-sized write would not block/succeed immediately, even if any other write would block.

I actually tried that out in addaleax/node@55ebf86, addaleax/node@23bd72c if you want to take a look.

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

Successfully merging this pull request may close these issues.

7 participants