Repository navigation
tee: replace the pipe a failed output used - #15132
Conversation
| splice_or_detach!( | ||
| pipe2_read, | ||
| pipe2_write, | ||
| pipe::<true>(), |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
No, pipe::<true>() is a macro argument and only runs inside the drain_pipe error branch, so a new pipe is made once when an output fails, and that output is removed right after. The success path doesn't change.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Done in ff0fcb9. The macro now makes the new pipe itself, enlarged only if the 2nd pipe was enlarged at startup.
| .is_err(); | ||
| $writer.name.clear(); //mark as exited | ||
| // the failed write can leave bytes in the pipe: replace it with an empty one. | ||
| // Free it first, so that the new one does not need more file descriptors. |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Done in 6997600. At the open file limit tee now can't make the new pipe and continues with read/write, which test_tee_failed_output_at_open_file_limit still covers.
There was a problem hiding this comment.
I missed your edit, sorry. The drops are back in ff0fcb9.
There was a problem hiding this comment.
One of an annoying fact is lack of kernel API to make pipe empty. splice to /dev/null is theorically faster if /dev/null is cached, but it assumes /dev is mounted.
There was a problem hiding this comment.
Agreed, that's why it makes a new pipe instead.
There was a problem hiding this comment.
Previously, cat, tee, etc... was fallbacking from all splice error (instead of 1st splice only) at cat, tee by reading everything of pipe to userspace RAM. So the bug was not existing previously. It has simpler code base. However, we cannot recover the behaviour because GnuTests wants to catch EIO of 2nd splice...
I am still not sure if we should close & open pipes, or read everything to RAM...
There was a problem hiding this comment.
I think recreating pipes is better for other utils becuase we can safely fallback from fast-path when EMFILE happened, but about tee, it might not.
There was a problem hiding this comment.
I'd keep recreating, as you suggested for the pipe cache in #15086. the old pipe is closed first, so the fd limit doesn't block it (test_tee_failed_output_at_open_file_limit runs with 10 fds). if pipe() still fails, tee reports it and stops; no test reaches that.
There was a problem hiding this comment.
I think we can still cause EMFILE by system's limit. This looks race. But this PR is OK at a monent.
| return Ok(()); | ||
| } | ||
| let Some((last, others)) = self.writers.split_last_mut() else { | ||
| let Some(last) = self.writers.len().checked_sub(1) else { |
There was a problem hiding this comment.
Please revert this. Loop by index is difficult to understand.
There was a problem hiding this comment.
Reverted to split_last_mut in 6997600. The fallback still needs the index for drain(..=i), so it's others.iter_mut().enumerate().
There was a problem hiding this comment.
Outputs up to i already have this chunk, and write_flush writes to all of self.writers. So they're taken out while the rest gets the leftover input, then put back in front to keep the order. I can add a write_flush that starts at an index instead, if you prefer.
There was a problem hiding this comment.
Failed writers can be marked by empty names. Is it not enough? Previous code did not have Vec allocation.
There was a problem hiding this comment.
Yes, done in 6e1c349: the fallback writes to the outputs after the failed one in place and marks failures by clearing the name, so the done Vec is gone.
There was a problem hiding this comment.
Removed the fallback: if no new pipe can be made, tee now fails with the error.
There was a problem hiding this comment.
Yes. drop minimizes risk of failure (e.g. EMFILE) and we ahready know that pipe(2) is supported on the system at here. So catching error of pipe(2) should be fine.
| let tee_res = uucore::pipes::tee(&pipe_read, &pipe2_write, s); | ||
| assert_eq!(tee_res, Ok(s), "2nd pipe should have enough spare"); | ||
| splice_or_detach!(&pipe2_read, other, s); | ||
| splice_or_detach!(pipe2_read, pipe2_write, *other, s, { |
There was a problem hiding this comment.
since the drops are back, no test reaches this fallback anymore, no?
could we keep it simpler or please add a test for it
This comment was marked as low quality.
This comment was marked as low quality.
Sorry, something went wrong.
There was a problem hiding this comment.
Right, with the drops back no test reaches it: once the old pipe is freed, pipe() there only fails for reasons a test can't set up reliably (system-wide file or memory limits, or the per-user pipe buffer limit refusing the resize). I made the fallback smaller in 6e1c349. If you'd rather not keep it untested, the other option is removing the drops again, which lets the open-file-limit test reach it. (This is about the block that runs when no new pipe can be made, not the assert_eq!.)
There was a problem hiding this comment.
it's simpler now: if no new pipe can be made, tee reports the error and stops. still no test reaches it.
| } | ||
| // last one consumes input | ||
| splice_or_detach!(&pipe_read, last, s); | ||
| splice_or_detach!(pipe_read, pipe_write, *last, s, { |
There was a problem hiding this comment.
this is the same retain + aborted check as just below, could be dedup?
There was a problem hiding this comment.
Done in 6e1c349: both fallbacks now break out of the loop, and the retain and aborted check is one remove_exited(), called in the loop and after it.
This comment was marked as duplicate.
This comment was marked as duplicate.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
done when the fallback went, failed writers are removed in one place again.
| let _ = fcntl_setpipe_size(&pipe_read, MAX_ROOTLESS_PIPE_SIZE); | ||
| let _ = fcntl_setpipe_size(&self.writers[0], MAX_ROOTLESS_PIPE_SIZE); // stdout | ||
| } | ||
| macro_rules! splice_or_detach { |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Moved it back and squashed.
fa83021 to
95228ac
Compare
|
Seems that it makes coverage sad: |
tee duplicates its input through two pipes on Linux. When splicing from one of them to an output failed part-way, for example with ENOSPC or EFBIG, the bytes the output did not take stayed in the pipe, and the remaining outputs got them in front of their next chunk. Replace the pipe with an empty one after a failed output. The old pipe is closed first, so that this also works at the open file limit. A new pipe gets the larger size only if the 2nd pipe got it at the start, so that the 2nd is never smaller than the 1st. If no new pipe can be made, tee fails with the error.
95228ac to
b2c51e5
Compare
|
thanks, that's the coverage build writing its profile under the same size limit. both tests now check exit code 1 and use |
| drop($pipe_read); | ||
| drop($pipe_write); | ||
| // same size as the 2nd pipe got, so the 2nd is never smaller than the 1st | ||
| match if $sized { pipe::<true>() } else { io::pipe() } { |
There was a problem hiding this comment.
Oops, it is const generics... this PR is OK as now.
This comment was marked as resolved.
This comment was marked as resolved.
|
Thanks for your PR |
On Linux, tee copies its input to the outputs through two pipes. When splicing to one output failed part-way (EFBIG, ENOSPC), the bytes it didn't take stayed in the pipe. The other outputs would get them in front of their next chunk, and with the 2nd pipe left part-full tee panicked on its
2nd pipe should have enough spareassertion.After a failed output, the old pipe is now closed and replaced with an empty one. A new pipe gets the larger size only if the 2nd pipe got it at startup, so the 2nd is never smaller than the 1st. The old pipe is closed first, so this also works at the open file limit. If no new pipe can be made, tee fails with the error.
test_tee_failed_output_does_not_spill_into_othersandtest_tee_failed_output_at_open_file_limitfail on main and pass here.Split out of #15086.
Closes #15119