Laszlo Ersek
2023-Aug-31 09:12 UTC
[Libguestfs] [nbdkit PATCH] sh: Allow pwrite to not consume all data
On 8/31/23 10:02, Richard W.M. Jones wrote:> > On Wed, Aug 30, 2023 at 05:21:19PM -0500, Eric Blake wrote: >> I hit another transient failure in libnbd CI when a poorly-written >> eval script did not consume all of stdin during .pwrite. As behaving >> as a data sink can be a somewhat reasonable feature of a >> quickly-written sh or eval plugin, we should not be so insistent as >> treating an EPIPE failure as an immediate return of EIO to the client. > > I was thinking about this over night, and came to the conclusion that > it's always fine to ignore EPIPE errors.Interesting; I formed the opposite impression!> For example a script might > be processing the input data gradually and then encounter an error and > want to exit immediately. We also have plenty of plugins that discard > some or all of the written data.But that would be associated with a nonzero exit status, right? And that way the nbd client would see the pwrite operation as failed. Laszlo> > So my counter-proposal (coming soon) is going to simply turn the EPIPE > error into a debug message and discard the rest of the write buffer. > > Rich. > > >> Signed-off-by: Eric Blake <eblake at redhat.com> >> --- >> >> I probably need to add unit test coverage of this before committing >> (although proving that I win the data race on a client process exiting >> faster than the parent can write enough data to hit EPIPE is hard). >> >> plugins/sh/nbdkit-sh-plugin.pod | 8 +++++++ >> plugins/sh/call.c | 38 ++++++++++++++++++++++----------- >> 2 files changed, 34 insertions(+), 12 deletions(-) >> >> diff --git a/plugins/sh/nbdkit-sh-plugin.pod b/plugins/sh/nbdkit-sh-plugin.pod >> index b2c946a0..8b83a5b3 100644 >> --- a/plugins/sh/nbdkit-sh-plugin.pod >> +++ b/plugins/sh/nbdkit-sh-plugin.pod >> @@ -505,6 +505,14 @@ Unlike in other languages, if you provide a C<pwrite> method you >> B<must> also provide a C<can_write> method which exits with code C<0> >> (true). >> >> +With nbdkit E<ge> 1.36, this method may return C<0> without consuming >> +any data from stdin, and without producing any output, in order to >> +behave as an intentional data sink. But in older versions, nbdkit >> +would treat any C<EPIPE> failure in writing to your script as an error >> +condition even if your script returns success; to avoid unintended >> +failures, you may want to include C<"cat >/dev/null"> in a script >> +intending to ignore the client's write requests. >> + >> =item C<flush> >> >> /path/to/script flush <handle> >> diff --git a/plugins/sh/call.c b/plugins/sh/call.c >> index 888c6459..79c67a04 100644 >> --- a/plugins/sh/call.c >> +++ b/plugins/sh/call.c >> @@ -34,6 +34,7 @@ >> >> #include <assert.h> >> #include <fcntl.h> >> +#include <stdbool.h> >> #include <stdio.h> >> #include <stdlib.h> >> #include <inttypes.h> >> @@ -130,6 +131,7 @@ debug_call (const char **argv) >> */ >> static int >> call3 (const char *wbuf, size_t wbuflen, /* sent to stdin (can be NULL) */ >> + bool *pipe_full, /* set if wbuf not fully written */ >> string *rbuf, /* read from stdout */ >> string *ebuf, /* read from stderr */ >> const char **argv) /* script + parameters */ >> @@ -275,15 +277,8 @@ call3 (const char *wbuf, size_t wbuflen, /* sent to stdin (can be NULL) */ >> r = write (pfds[0].fd, wbuf, wbuflen); >> if (r == -1) { >> if (errno == EPIPE) { >> - /* We tried to write to the script but it didn't consume >> - * the data. Probably the script exited without reading >> - * from stdin. This is an error in the script. >> - */ >> - nbdkit_error ("%s: write to script failed because of a broken pipe: " >> - "this can happen if the script exits without " >> - "consuming stdin, which usually indicates a bug " >> - "in the script", >> - argv0); >> + *pipe_full = true; >> + r = wbuflen; >> } >> else >> nbdkit_error ("%s: write: %m", argv0); >> @@ -555,7 +550,7 @@ call (const char **argv) >> CLEANUP_FREE_STRING string rbuf = empty_vector; >> CLEANUP_FREE_STRING string ebuf = empty_vector; >> >> - r = call3 (NULL, 0, &rbuf, &ebuf, argv); >> + r = call3 (NULL, 0, NULL, &rbuf, &ebuf, argv); >> return handle_script_error (argv[0], &ebuf, r); >> } >> >> @@ -568,7 +563,7 @@ call_read (string *rbuf, const char **argv) >> int r; >> CLEANUP_FREE_STRING string ebuf = empty_vector; >> >> - r = call3 (NULL, 0, rbuf, &ebuf, argv); >> + r = call3 (NULL, 0, NULL, rbuf, &ebuf, argv); >> r = handle_script_error (argv[0], &ebuf, r); >> if (r == ERROR) >> string_reset (rbuf); >> @@ -584,7 +579,26 @@ call_write (const char *wbuf, size_t wbuflen, const char **argv) >> int r; >> CLEANUP_FREE_STRING string rbuf = empty_vector; >> CLEANUP_FREE_STRING string ebuf = empty_vector; >> + bool pipe_full = false; >> >> - r = call3 (wbuf, wbuflen, &rbuf, &ebuf, argv); >> + r = call3 (wbuf, wbuflen, &pipe_full, &rbuf, &ebuf, argv); >> + if (pipe_full && r == OK) { >> + /* We allow scripts to intentionally ignore data, but they must >> + * have no output when doing so. >> + */ >> + if (rbuf.len > 0 || ebuf.len > 0) { >> + nbdkit_error ("%s: write to script failed because of a broken pipe: " >> + "this can happen if the script exits without " >> + "consuming stdin, which usually indicates a bug " >> + "in the script", >> + argv[0]); >> + r = ERROR; >> + } >> + else >> + nbdkit_debug ("%s: write to script failed because of a broken pipe; " >> + "assuming this was an intentional data sink, although it " >> + "may indicate a bug in the script", >> + argv[0]); >> + } >> return handle_script_error (argv[0], &ebuf, r); >> } >> -- >> 2.41.0 >> >> _______________________________________________ >> Libguestfs mailing list >> Libguestfs at redhat.com >> https://listman.redhat.com/mailman/listinfo/libguestfs >
Richard W.M. Jones
2023-Aug-31 09:47 UTC
[Libguestfs] [nbdkit PATCH] sh: Allow pwrite to not consume all data
On Thu, Aug 31, 2023 at 11:12:59AM +0200, Laszlo Ersek wrote:> On 8/31/23 10:02, Richard W.M. Jones wrote: > > > > On Wed, Aug 30, 2023 at 05:21:19PM -0500, Eric Blake wrote: > >> I hit another transient failure in libnbd CI when a poorly-written > >> eval script did not consume all of stdin during .pwrite. As behaving > >> as a data sink can be a somewhat reasonable feature of a > >> quickly-written sh or eval plugin, we should not be so insistent as > >> treating an EPIPE failure as an immediate return of EIO to the client. > > > > I was thinking about this over night, and came to the conclusion that > > it's always fine to ignore EPIPE errors. > > Interesting; I formed the opposite impression! > > > For example a script might > > be processing the input data gradually and then encounter an error and > > want to exit immediately. We also have plenty of plugins that discard > > some or all of the written data. > > But that would be associated with a nonzero exit status, right?In the error case, yes it would exit with a non-zero exit status. In the "don't care about data" case it would exit with 0. As a concrete example the code currently has to look like this: case "$1") can_write) echo 0 ;; pwrite) ... if [ there is an error ]; then cat >/dev/null # discard stdin echo 'EIO I/O error' >&2 exit 1 else cat >/dev/null # discard stdin exit 0 fi where we're saying that the 'cat >/dev/null' commands are unnecessary complication. There have been cases where we have forgotten to discard stdin on every exit path and this has caused intermittent test failures in CI: https://gitlab.com/nbdkit/libnbd/-/commit/4df70870420d1be9348ac45f4aa467501eca5089 https://gitlab.com/nbdkit/libnbd/-/commit/c713529e9fd0641b2d73f764517b5f9c21a767fd Rich.> And that way the nbd client would see the pwrite operation as failed. > > Laszlo > > > > > So my counter-proposal (coming soon) is going to simply turn the EPIPE > > error into a debug message and discard the rest of the write buffer. > > > > Rich. > > > > > >> Signed-off-by: Eric Blake <eblake at redhat.com> > >> --- > >> > >> I probably need to add unit test coverage of this before committing > >> (although proving that I win the data race on a client process exiting > >> faster than the parent can write enough data to hit EPIPE is hard). > >> > >> plugins/sh/nbdkit-sh-plugin.pod | 8 +++++++ > >> plugins/sh/call.c | 38 ++++++++++++++++++++++----------- > >> 2 files changed, 34 insertions(+), 12 deletions(-) > >> > >> diff --git a/plugins/sh/nbdkit-sh-plugin.pod b/plugins/sh/nbdkit-sh-plugin.pod > >> index b2c946a0..8b83a5b3 100644 > >> --- a/plugins/sh/nbdkit-sh-plugin.pod > >> +++ b/plugins/sh/nbdkit-sh-plugin.pod > >> @@ -505,6 +505,14 @@ Unlike in other languages, if you provide a C<pwrite> method you > >> B<must> also provide a C<can_write> method which exits with code C<0> > >> (true). > >> > >> +With nbdkit E<ge> 1.36, this method may return C<0> without consuming > >> +any data from stdin, and without producing any output, in order to > >> +behave as an intentional data sink. But in older versions, nbdkit > >> +would treat any C<EPIPE> failure in writing to your script as an error > >> +condition even if your script returns success; to avoid unintended > >> +failures, you may want to include C<"cat >/dev/null"> in a script > >> +intending to ignore the client's write requests. > >> + > >> =item C<flush> > >> > >> /path/to/script flush <handle> > >> diff --git a/plugins/sh/call.c b/plugins/sh/call.c > >> index 888c6459..79c67a04 100644 > >> --- a/plugins/sh/call.c > >> +++ b/plugins/sh/call.c > >> @@ -34,6 +34,7 @@ > >> > >> #include <assert.h> > >> #include <fcntl.h> > >> +#include <stdbool.h> > >> #include <stdio.h> > >> #include <stdlib.h> > >> #include <inttypes.h> > >> @@ -130,6 +131,7 @@ debug_call (const char **argv) > >> */ > >> static int > >> call3 (const char *wbuf, size_t wbuflen, /* sent to stdin (can be NULL) */ > >> + bool *pipe_full, /* set if wbuf not fully written */ > >> string *rbuf, /* read from stdout */ > >> string *ebuf, /* read from stderr */ > >> const char **argv) /* script + parameters */ > >> @@ -275,15 +277,8 @@ call3 (const char *wbuf, size_t wbuflen, /* sent to stdin (can be NULL) */ > >> r = write (pfds[0].fd, wbuf, wbuflen); > >> if (r == -1) { > >> if (errno == EPIPE) { > >> - /* We tried to write to the script but it didn't consume > >> - * the data. Probably the script exited without reading > >> - * from stdin. This is an error in the script. > >> - */ > >> - nbdkit_error ("%s: write to script failed because of a broken pipe: " > >> - "this can happen if the script exits without " > >> - "consuming stdin, which usually indicates a bug " > >> - "in the script", > >> - argv0); > >> + *pipe_full = true; > >> + r = wbuflen; > >> } > >> else > >> nbdkit_error ("%s: write: %m", argv0); > >> @@ -555,7 +550,7 @@ call (const char **argv) > >> CLEANUP_FREE_STRING string rbuf = empty_vector; > >> CLEANUP_FREE_STRING string ebuf = empty_vector; > >> > >> - r = call3 (NULL, 0, &rbuf, &ebuf, argv); > >> + r = call3 (NULL, 0, NULL, &rbuf, &ebuf, argv); > >> return handle_script_error (argv[0], &ebuf, r); > >> } > >> > >> @@ -568,7 +563,7 @@ call_read (string *rbuf, const char **argv) > >> int r; > >> CLEANUP_FREE_STRING string ebuf = empty_vector; > >> > >> - r = call3 (NULL, 0, rbuf, &ebuf, argv); > >> + r = call3 (NULL, 0, NULL, rbuf, &ebuf, argv); > >> r = handle_script_error (argv[0], &ebuf, r); > >> if (r == ERROR) > >> string_reset (rbuf); > >> @@ -584,7 +579,26 @@ call_write (const char *wbuf, size_t wbuflen, const char **argv) > >> int r; > >> CLEANUP_FREE_STRING string rbuf = empty_vector; > >> CLEANUP_FREE_STRING string ebuf = empty_vector; > >> + bool pipe_full = false; > >> > >> - r = call3 (wbuf, wbuflen, &rbuf, &ebuf, argv); > >> + r = call3 (wbuf, wbuflen, &pipe_full, &rbuf, &ebuf, argv); > >> + if (pipe_full && r == OK) { > >> + /* We allow scripts to intentionally ignore data, but they must > >> + * have no output when doing so. > >> + */ > >> + if (rbuf.len > 0 || ebuf.len > 0) { > >> + nbdkit_error ("%s: write to script failed because of a broken pipe: " > >> + "this can happen if the script exits without " > >> + "consuming stdin, which usually indicates a bug " > >> + "in the script", > >> + argv[0]); > >> + r = ERROR; > >> + } > >> + else > >> + nbdkit_debug ("%s: write to script failed because of a broken pipe; " > >> + "assuming this was an intentional data sink, although it " > >> + "may indicate a bug in the script", > >> + argv[0]); > >> + } > >> return handle_script_error (argv[0], &ebuf, r); > >> } > >> -- > >> 2.41.0 > >> > >> _______________________________________________ > >> Libguestfs mailing list > >> Libguestfs at redhat.com > >> https://listman.redhat.com/mailman/listinfo/libguestfs > >-- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones Read my programming and virtualization blog: http://rwmj.wordpress.com virt-p2v converts physical machines to virtual machines. Boot with a live CD or over the network (PXE) and turn machines into KVM guests. http://libguestfs.org/virt-v2v
Eric Blake
2023-Aug-31 13:06 UTC
[Libguestfs] [nbdkit PATCH] sh: Allow pwrite to not consume all data
On Thu, Aug 31, 2023 at 11:12:59AM +0200, Laszlo Ersek wrote:> On 8/31/23 10:02, Richard W.M. Jones wrote: > > > > On Wed, Aug 30, 2023 at 05:21:19PM -0500, Eric Blake wrote: > >> I hit another transient failure in libnbd CI when a poorly-written > >> eval script did not consume all of stdin during .pwrite. As behaving > >> as a data sink can be a somewhat reasonable feature of a > >> quickly-written sh or eval plugin, we should not be so insistent as > >> treating an EPIPE failure as an immediate return of EIO to the client. > > > > I was thinking about this over night, and came to the conclusion that > > it's always fine to ignore EPIPE errors. > > Interesting; I formed the opposite impression!It took me a couple of tries to realize the subtle distinction. If the child process (aka the plugin script) dies with SIGPIPE (and we intentionally do signal(SIGPIPE, SIG_DFL) before exec'ing the child process, as it is notoriously hard to undo an inherited ignored SIGPIPE in shell), the parent process will see WIFSIGNALED and treat that as an EIO error to the NBD client. If the child process chooses to ignore SIGPIPE, but then sees its own EPIPE failure and exits with non-zero status as a result, the parent process will see the non-zero exit status and report an appropriate error to the NBD client (if it can parse an error name out of the plugin's stderr output, it uses that; otherwise it uses EIO). But if the parent process sees EPIPE, that merely means that the plugin script doesn't care to finish consuming stdin. It is indeterminate at that point whether the child has a reason for ignoring the pipe, so it is inappropriate to blindly treat failure to write to the child as a reason to claim the child would have failed with EIO, without giving the child a chance to exit() (or die by signal) first.> > > > > So my counter-proposal (coming soon) is going to simply turn the EPIPE > > error into a debug message and discard the rest of the write buffer.Yes, I liked your counter-proposal better than my attempt. -- Eric Blake, Principal Software Engineer Red Hat, Inc. Virtualization: qemu.org | libguestfs.org
Possibly Parallel Threads
- [nbdkit PATCH] sh: Allow pwrite to not consume all data
- [nbdkit PATCH] sh: Allow pwrite to not consume all data
- [nbdkit PATCH] sh: Allow pwrite to not consume all data
- [nbdkit PATCH] sh: Allow pwrite to not consume all data
- [nbdkit PATCH] sh: Allow pwrite to not consume all data