From d7bcfb1aed85dbd2ec96ba37e0c66f47b38300d6 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 2 Jan 2017 03:59:07 +0000 Subject: new API for splice and tee Since we're breaking away from the old io_splice gem, we have liberty to change the API when moving to a new namespace. This might allow us to simplify the common case args and reduce the amount of C code we maintain. --- ext/sleepy_penguin/splice.c | 207 ++++++------------------------------------- lib/sleepy_penguin.rb | 4 + lib/sleepy_penguin/splice.rb | 121 +++++++++++++++++++++++++ test/test_splice.rb | 57 ++++++------ test/test_splice_eintr.rb | 2 +- 5 files changed, 179 insertions(+), 212 deletions(-) create mode 100644 lib/sleepy_penguin/splice.rb diff --git a/ext/sleepy_penguin/splice.c b/ext/sleepy_penguin/splice.c index 4ce2557..59ddac7 100644 --- a/ext/sleepy_penguin/splice.c +++ b/ext/sleepy_penguin/splice.c @@ -35,117 +35,34 @@ static void *nogvl_splice(void *ptr) a->len, a->flags); } -static ssize_t do_splice(int argc, VALUE *argv, unsigned dflags) +/* :nodoc: */ +static VALUE my_splice(VALUE mod, VALUE io_in, VALUE off_in, + VALUE io_out, VALUE off_out, + VALUE len, VALUE flags) { off_t i = 0, o = 0; - VALUE io_in, off_in, io_out, off_out, len, flags; struct copy_args a; ssize_t bytes; - ssize_t total = 0; - - rb_scan_args(argc, argv, "51", - &io_in, &off_in, &io_out, &off_out, &len, &flags); a.off_in = NIL_P(off_in) ? NULL : (i = NUM2OFFT(off_in), &i); a.off_out = NIL_P(off_out) ? NULL : (o = NUM2OFFT(off_out), &o); a.len = NUM2SIZET(len); - a.flags = NIL_P(flags) ? dflags : NUM2UINT(flags) | dflags; + a.flags = NUM2UINT(flags); for (;;) { a.fd_in = check_fileno(io_in); a.fd_out = check_fileno(io_out); bytes = (ssize_t)IO_RUN(nogvl_splice, &a); + if (bytes == 0) return Qnil; if (bytes < 0) { - if (errno == EINTR) - continue; - if (total > 0) - return total; - return bytes; - } else if (bytes == 0) { - break; - } else { - return bytes; + switch (errno) { + case EINTR: continue; + case EAGAIN: return sym_EAGAIN; + default: rb_sys_fail("splice"); + } } + return SSIZET2NUM(bytes); } - - return total; -} - -/* - * call-seq: - * SleepyPenguin.splice(io_in, off_in, io_out, off_out, len) => integer - * SleepyPenguin.splice(io_in, off_in, io_out, off_out, len, flags) => integer - * - * Splice +len+ bytes from/to a pipe. Either +io_in+ or +io_out+ - * MUST be a pipe. +io_in+ and +io_out+ may BOTH be pipes as of - * Linux 2.6.31 or later. - * - * +off_in+ and +off_out+ if non-nil may be used to - * specify an offset for the non-pipe file descriptor. - * - * +flags+ defaults to zero if unspecified. - * +flags+ may be a bitmask of the following flags: - * - * * SleepyPenguin::F_MOVE - * * SleepyPenguin::F_NONBLOCK - * * SleepyPenguin::F_MORE - * - * Returns the number of bytes spliced. - * Raises EOFError when +io_in+ has reached end of file. - * Raises Errno::EAGAIN if the SleepyPenguin::F_NONBLOCK flag is set - * and the pipe has no data to read from or space to write to. May - * also raise Errno::EAGAIN if the non-pipe descriptor has no data - * to read from or space to write to. - * - * As splice never exposes buffers to userspace, it will not take - * into account userspace buffering done by Ruby or stdio. It is - * also not subject to encoding/decoding filters under Ruby 1.9. - * - * Consider using SleepyPenguin.trysplice if +io_out+ is a pipe or if you are using - * non-blocking I/O on both descriptors as it avoids the cost of raising - * common Errno::EAGAIN exceptions. - * - * See manpage for full documentation: - * http://kernel.org/doc/man-pages/online/pages/man2/splice.2.html - */ -static VALUE my_splice(int argc, VALUE *argv, VALUE self) -{ - ssize_t n = do_splice(argc, argv, 0); - - if (n == 0) - rb_eof_error(); - if (n < 0) - rb_sys_fail("splice"); - return SSIZET2NUM(n); -} - -/* - * call-seq: - * SleepyPenguin.trysplice(io_in, off_in, io_out, off_out, len) => integer - * SleepyPenguin.trysplice(io_in, off_in, io_out, off_out, len, flags) => integer - * - * Exactly like SleepyPenguin.splice, except +:EAGAIN+ is returned when either - * the read or write end would block instead of raising Errno::EAGAIN. - * - * SleepyPenguin::F_NONBLOCK is always passed for the pipe descriptor, - * but this can still block if the non-pipe descriptor is blocking. - * - * See SleepyPenguin.splice documentation for more details. - * - * This method is recommended whenever +io_out+ is a pipe. - */ -static VALUE trysplice(int argc, VALUE *argv, VALUE self) -{ - ssize_t n = do_splice(argc, argv, SPLICE_F_NONBLOCK); - - if (n == 0) - return Qnil; - if (n < 0) { - if (errno == EAGAIN) - return sym_EAGAIN; - rb_sys_fail("splice"); - } - return SSIZET2NUM(n); } struct tee_args { @@ -163,111 +80,37 @@ static void *nogvl_tee(void *ptr) return (void *)tee(a->fd_in, a->fd_out, a->len, a->flags); } -static ssize_t do_tee(int argc, VALUE *argv, unsigned dflags) +/* :nodoc: */ +static VALUE my_tee(VALUE mod, VALUE io_in, VALUE io_out, + VALUE len, VALUE flags) { - VALUE io_in, io_out, len, flags; struct tee_args a; ssize_t bytes; - ssize_t total = 0; - rb_scan_args(argc, argv, "31", &io_in, &io_out, &len, &flags); a.len = (size_t)NUM2SIZET(len); - a.flags = NIL_P(flags) ? dflags : NUM2UINT(flags) | dflags; + a.flags = NUM2UINT(flags); for (;;) { a.fd_in = check_fileno(io_in); a.fd_out = check_fileno(io_out); bytes = (ssize_t)IO_RUN(nogvl_tee, &a); + if (bytes == 0) return Qnil; if (bytes < 0) { - if (errno == EINTR) - continue; - if (total > 0) - return total; - return bytes; - } else if (bytes == 0) { - break; - } else { - return bytes; + switch (errno) { + case EINTR: continue; + case EAGAIN: return sym_EAGAIN; + default: rb_sys_fail("tee"); + } } + return SSIZET2NUM(bytes); } - - return total; -} - -/* - * call-seq: - * SleepyPenguin.tee(io_in, io_out, len) => integer - * SleepyPenguin.tee(io_in, io_out, len, flags) => integer - * - * Copies up to +len+ bytes of data from +io_in+ to +io_out+. +io_in+ - * and +io_out+ must both refer to pipe descriptors. +io_in+ and +io_out+ - * may not be endpoints of the same pipe. - * - * +flags+ may be zero (the default) or a combination of: - * * SleepyPenguin::F_NONBLOCK - * - * Other splice-related flags are currently unimplemented in the - * kernel and have no effect. - * - * Returns the number of bytes duplicated if successful. - * Raises EOFError when +io_in+ is closed and emptied. - * Raises Errno::EAGAIN when +io_in+ is empty and/or +io_out+ is full - * and +flags+ contains SleepyPenguin::F_NONBLOCK - * - * Consider using SleepyPenguin.trytee if you are using - * SleepyPenguin::F_NONBLOCK as it avoids the cost of raising - * common Errno::EAGAIN exceptions. - * - * See manpage for full documentation: - * http://kernel.org/doc/man-pages/online/pages/man2/tee.2.html - */ -static VALUE my_tee(int argc, VALUE *argv, VALUE self) -{ - ssize_t n = do_tee(argc, argv, 0); - - if (n == 0) - rb_eof_error(); - if (n < 0) - rb_sys_fail("tee"); - - return SSIZET2NUM(n); -} - -/* - * call-seq: - * SleepyPenguin.trytee(io_in, io_out, len) => integer - * SleepyPenguin.trytee(io_in, io_out, len, flags) => integer - * - * Exactly like SleepyPenguin.tee, except +:EAGAIN+ is returned when either - * the read or write end would block instead of raising Errno::EAGAIN. - * - * SleepyPenguin::F_NONBLOCK is always passed for the pipe descriptor, - * but this can still block if the non-pipe descriptor is blocking. - * - * See SleepyPenguin.tee documentation for more details. - */ -static VALUE trytee(int argc, VALUE *argv, VALUE self) -{ - ssize_t n = do_tee(argc, argv, SPLICE_F_NONBLOCK); - - if (n == 0) - return Qnil; - if (n < 0) { - if (errno == EAGAIN) - return sym_EAGAIN; - rb_sys_fail("tee"); - } - - return SSIZET2NUM(n); } void sleepy_penguin_init_splice(void) { VALUE mod = rb_define_module("SleepyPenguin"); - rb_define_singleton_method(mod, "splice", my_splice, -1); - rb_define_singleton_method(mod, "trysplice", trysplice, -1); - rb_define_singleton_method(mod, "tee", my_tee, -1); - rb_define_singleton_method(mod, "trytee", trytee, -1); + rb_define_singleton_method(mod, "__splice", my_splice, 6); + rb_define_singleton_method(mod, "__tee", my_tee, 4); /* * Attempt to move pages instead of copying. This is only a hint diff --git a/lib/sleepy_penguin.rb b/lib/sleepy_penguin.rb index 808c94a..63f293d 100644 --- a/lib/sleepy_penguin.rb +++ b/lib/sleepy_penguin.rb @@ -15,3 +15,7 @@ if defined?(SleepyPenguin::Inotify) && # :startdoc end end + +if SleepyPenguin.respond_to?(:__splice) || SleepyPenguin.respond_to?(:__tee) + require_relative 'sleepy_penguin/splice' +end diff --git a/lib/sleepy_penguin/splice.rb b/lib/sleepy_penguin/splice.rb new file mode 100644 index 0000000..5358ed5 --- /dev/null +++ b/lib/sleepy_penguin/splice.rb @@ -0,0 +1,121 @@ +# -*- encoding: binary -*- + +module SleepyPenguin + # call-seq: + # SleepyPenguin.splice(io_in, io_out, len[, flags [, keywords]) => Integer + # + # Splice +len+ bytes from/to a pipe. Either +io_in+ or +io_out+ + # MUST be a pipe. +io_in+ and +io_out+ may BOTH be pipes as of + # Linux 2.6.31 or later. + # + # +flags+ defaults to zero if unspecified. + # It may be an Integer bitmask, a Symbol, or Array of Symbols + # + # The integer bitmask may any combination of: + # + # * SleepyPenguin::F_MOVE - attempt to move pages instead of copying + # + # * SleepyPenguin::F_NONBLOCK - do not block on pipe I/O (only) + # + # * SleepyPenguin::F_MORE - indicates more data will be sent soon + # + # Symbols may be used as well to specify a single flag: + # + # * :move - corresponds to F_MOVE + # * :nonblock - corresponds to F_NONBLOCK + # * :more - corresponds to F_MORE + # + # Or, an array of any combination of the above symbols. + # + # Keywords: + # + # :off_in and :off_out if non-nil may be used to + # specify an offset for the respective non-pipe file descriptor. + # + # :exception defaults to +true+. Setting it to +false+ + # will return :EAGAIN symbol instead of raising Errno::EAGAIN. + # This will also return +nil+ instead of raising EOFError + # when +io_in+ is at the end. + # + # Raises EOFError when +io_in+ has reached end of file. + # Raises Errno::EAGAIN if the SleepyPenguin::F_NONBLOCK flag is set + # and the pipe has no data to read from or space to write to. May + # also raise Errno::EAGAIN if the non-pipe descriptor has no data + # to read from or space to write to. + # + # As splice never exposes buffers to userspace, it will not take + # into account userspace buffering done by Ruby or stdio. It is + # also not subject to encoding/decoding filters under Ruby 1.9+. + # + # Consider using `exception: false` if +io_out+ is a pipe or if you + # are using non-blocking I/O on both descriptors as it avoids the + # cost of raising common Errno::EAGAIN exceptions. + # + # See manpage for full documentation: + # http://man7.org/linux/man-pages/man2/splice.2.html + def self.splice(io_in, io_out, len, flags = 0, + off_in: nil, off_out: nil, exception: true) + flags = __map_splice_flags(flags) + ret = __splice(io_in, off_in, io_out, off_out, len, flags) + exception ? __map_exc(ret) : ret + end + + # call-seq: + # SleepyPenguin.tee(io_in, io_out, len[, flags[, keywords]) => Integer + # + # Copies up to +len+ bytes of data from +io_in+ to +io_out+. +io_in+ + # and +io_out+ must both refer to pipe descriptors. +io_in+ and +io_out+ + # may not be endpoints of the same pipe. + # + # +flags+ may be zero (the default) or a combination of: + # * SleepyPenguin::F_NONBLOCK + # + # As a shortcut, the `:nonblock` symbol may be used instead. + # + # Other splice-related flags are currently unimplemented in the + # kernel and have no effect. + # + # Returns the number of bytes duplicated if successful. + # Raises EOFError when +io_in+ is closed and emptied. + # Raises Errno::EAGAIN when +io_in+ is empty and/or +io_out+ is full + # and +flags+ specifies non-blocking operation + # + # Keywords: + # + # :exception defaults to +true+. Setting it to +false+ + # will return :EAGAIN symbol instead of raising Errno::EAGAIN. + # This will also return +nil+ instead of raising EOFError + # when +io_in+ is at the end. + # + # Consider using `exception: false` if +io_out+ is a pipe or if you + # are using non-blocking I/O on both descriptors as it avoids the + # cost of raising common Errno::EAGAIN exceptions. + # + # See manpage for full documentation: + # http://man7.org/linux/man-pages/man2/tee.2.html + def self.tee(io_in, io_out, len, flags = 0, exception: true) + flags = __map_splice_flags(flags) + ret = __tee(io_in, io_out, len, flags) + exception ? __map_exc(ret) : ret + end if respond_to?(:__tee) + + @__splice_f_map = { # :nodoc: + :nonblock => F_NONBLOCK, + :more => F_MORE, + :move => F_MOVE + } + + def self.__map_splice_flags(flags) # :nodoc: + onef = @__splice_f_map[flags] and return onef + flags.respond_to?(:inject) ? + flags.inject(0) { |fl, sym| fl |= @__splice_f_map[sym] } : flags + end + + def self.__map_exc(ret) # :nodoc: + case ret + when :EAGAIN then raise Errno::EAGAIN, 'Resource temporarily unavailable' + when nil then raise EOFError, 'end of file reached' + end + ret + end +end diff --git a/test/test_splice.rb b/test/test_splice.rb index 475ba8b..71e0117 100644 --- a/test/test_splice.rb +++ b/test/test_splice.rb @@ -18,7 +18,7 @@ class TestSplice < Test::Unit::TestCase assert_equal 5, tmp.syswrite(str) tmp.sysseek(0) - nr = SleepyPenguin.splice(tmp.fileno, nil, wr.fileno, nil, size, 0) + nr = SleepyPenguin.splice(tmp.fileno, wr.fileno, size) assert_equal size, nr assert_equal str, rd.sysread(size) end @@ -32,7 +32,7 @@ class TestSplice < Test::Unit::TestCase assert_equal 5, tmp.syswrite(str) tmp.sysseek(0) - nr = SleepyPenguin.splice(tmp, nil, wr, nil, size, 0) + nr = SleepyPenguin.splice(tmp, wr, size) assert_equal size, nr assert_equal str, rd.sysread(size) end @@ -46,7 +46,7 @@ class TestSplice < Test::Unit::TestCase assert_equal 5, tmp.syswrite(str) tmp.sysseek(0) - nr = SleepyPenguin.splice(tmp, nil, wr, nil, size) + nr = SleepyPenguin.splice(tmp, wr, size) assert_equal size, nr assert_equal str, rd.sysread(size) end @@ -60,7 +60,7 @@ class TestSplice < Test::Unit::TestCase assert_equal 5, tmp.syswrite(str) tmp.sysseek(0) - nr = SleepyPenguin.trysplice(tmp, nil, wr, nil, size) + nr = SleepyPenguin.splice(tmp, wr, size, :nonblock, exception: false) assert_equal size, nr assert_equal str, rd.sysread(size) end @@ -78,7 +78,7 @@ class TestSplice < Test::Unit::TestCase assert_equal 5, tmp.syswrite(str) tmp.sysseek(0) - nr = SleepyPenguin.splice(io_ish, nil, wr, nil, size, 0) + nr = SleepyPenguin.splice(io_ish, wr, size) assert_equal size, nr assert_equal str, rd.sysread(size) end @@ -93,7 +93,7 @@ class TestSplice < Test::Unit::TestCase assert_equal 5, tmp.syswrite(str) tmp.sysseek(0) - nr = SleepyPenguin.splice(tmp.fileno, off, wr.fileno, nil, len, 0) + nr = SleepyPenguin.splice(tmp.fileno, wr.fileno, len, off_in: off) assert_equal len, nr assert_equal 'de', rd.sysread(len) end @@ -104,7 +104,7 @@ class TestSplice < Test::Unit::TestCase tmp = Tempfile.new('ruby_splice') assert_equal 5, wr.syswrite(str) - nr = SleepyPenguin.splice(rd.fileno, nil, tmp.fileno, 3, str.size, 0) + nr = SleepyPenguin.splice(rd.fileno, tmp.fileno, str.size, off_out: 3) assert_equal 5, nr tmp.sysseek(0) assert_equal "\0\0\0abcde", tmp.sysread(9) @@ -115,25 +115,25 @@ class TestSplice < Test::Unit::TestCase tmp = Tempfile.new('ruby_splice') assert_raises(Errno::EAGAIN) { - SleepyPenguin.splice(rd.fileno, nil, tmp.fileno, 0, 5, - SleepyPenguin::F_NONBLOCK) + SleepyPenguin.splice(rd.fileno, tmp.fileno, 5, :nonblock, off_out: 0) } end def test_trysplice_nonblock rd, wr = IO.pipe tmp = Tempfile.new('ruby_splice') - assert_equal :EAGAIN, - SleepyPenguin.trysplice(rd, nil, tmp, 0, 5, - SleepyPenguin::F_NONBLOCK) + assert_equal :EAGAIN, SleepyPenguin.splice(rd, tmp, 5, :nonblock, + off_out: 0, exception: false) end def test_trysplice_nonblock_noargs rd, wr = IO.pipe tmp = Tempfile.new('ruby_splice') - assert_equal :EAGAIN, SleepyPenguin.trysplice(rd, nil, tmp, 0, 5) - assert_equal :EAGAIN, SleepyPenguin.trysplice(rd, nil, tmp, 0, 5, - SleepyPenguin::F_MORE) + assert_equal :EAGAIN, SleepyPenguin.splice(rd, tmp, 5, :nonblock, + off_out: 0, exception: false) + assert_equal :EAGAIN, SleepyPenguin.splice(rd, tmp, 5, [:more,:nonblock], + off_out: 0, + exception: false) end def test_splice_eof @@ -142,12 +142,10 @@ class TestSplice < Test::Unit::TestCase wr.syswrite 'abc' wr.close - nr = SleepyPenguin.splice(rd.fileno, nil, tmp.fileno, 0, 5, - SleepyPenguin::F_NONBLOCK) + nr = SleepyPenguin.splice(rd.fileno, tmp.fileno, 5, :nonblock, off_out: 0) assert_equal 3, nr assert_raises(EOFError) { - SleepyPenguin.splice(rd.fileno, nil, tmp.fileno, 0, 5, - SleepyPenguin::F_NONBLOCK) + SleepyPenguin.splice(rd.fileno, tmp.fileno, 5, :nonblock, off_out: 0) } end @@ -157,10 +155,10 @@ class TestSplice < Test::Unit::TestCase wr.syswrite 'abc' wr.close - nr = SleepyPenguin.trysplice(rd, nil, tmp, 0, 5, SleepyPenguin::F_NONBLOCK) + nr = SleepyPenguin.splice(rd, tmp, 5, off_out: 0, exception: false) assert_equal 3, nr - assert_nil SleepyPenguin.trysplice(rd, nil, tmp, 0, 5, - SleepyPenguin::F_NONBLOCK) + assert_nil SleepyPenguin.splice(rd, tmp, 5, :nonblock, + off_out: 0, exception: false) end def test_splice_nonblock_socket @@ -170,7 +168,7 @@ class TestSplice < Test::Unit::TestCase rs = TCPSocket.new('127.0.0.1', port) rs.nonblock = true assert_raises(Errno::EAGAIN) { - SleepyPenguin.splice(rs, nil, wp, nil, 1024, 0) + SleepyPenguin.splice(rs, wp, 1024) } rs.close server.close @@ -183,7 +181,7 @@ class TestSplice < Test::Unit::TestCase rdb, wrb = IO.pipe assert_equal 5, wra.syswrite(str) - nr = SleepyPenguin.tee(rda.fileno, wrb.fileno, size, 0) + nr = SleepyPenguin.tee(rda.fileno, wrb.fileno, size) assert_equal 5, nr assert_equal str, rdb.sysread(5) assert_equal str, rda.sysread(5) @@ -196,7 +194,7 @@ class TestSplice < Test::Unit::TestCase rdb, wrb = IO.pipe assert_equal 5, wra.syswrite(str) - nr = SleepyPenguin.trytee(rda, wrb, size, 0) + nr = SleepyPenguin.tee(rda, wrb, size, :nonblock, exception: false) assert_equal 5, nr assert_equal str, rdb.sysread(5) assert_equal str, rda.sysread(5) @@ -207,7 +205,7 @@ class TestSplice < Test::Unit::TestCase rdb, wrb = IO.pipe wra.close assert_raises(EOFError) { - SleepyPenguin.tee(rda.fileno, wrb.fileno, 4096, 0) + SleepyPenguin.tee(rda.fileno, wrb.fileno, 4096) } end @@ -215,7 +213,7 @@ class TestSplice < Test::Unit::TestCase rda, wra = IO.pipe rdb, wrb = IO.pipe wra.close - assert_nil SleepyPenguin.trytee(rda, wrb, 4096) + assert_nil SleepyPenguin.tee(rda, wrb, 4096, :nonblock, exception: false) end def test_tee_nonblock @@ -229,7 +227,8 @@ class TestSplice < Test::Unit::TestCase def test_trytee_nonblock rda, wra = IO.pipe rdb, wrb = IO.pipe - assert_equal :EAGAIN, SleepyPenguin.trytee(rda, wrb, 4096) + assert_equal :EAGAIN, SleepyPenguin.tee(rda, wrb, 4096, :nonblock, + exception: false) end def test_tee_io @@ -239,7 +238,7 @@ class TestSplice < Test::Unit::TestCase rdb, wrb = IO.pipe assert_equal 5, wra.syswrite(str) - nr = SleepyPenguin.tee(rda, wrb, size, 0) + nr = SleepyPenguin.tee(rda, wrb, size) assert_equal 5, nr assert_equal str, rdb.sysread(5) assert_equal str, rda.sysread(5) diff --git a/test/test_splice_eintr.rb b/test/test_splice_eintr.rb index 3a5d96f..41b6dd0 100644 --- a/test/test_splice_eintr.rb +++ b/test/test_splice_eintr.rb @@ -27,7 +27,7 @@ class Test_Splice_EINTR < Test::Unit::TestCase sleep 0.01 wr.write "HI" end - nr = SleepyPenguin.splice rd, nil, tmp, nil, 666 + nr = SleepyPenguin.splice rd, tmp, 666 assert_equal 2, nr assert_equal 1, @usr1 end -- cgit v1.2.3-24-ge0c7