ruby-changes:38829
From: normal <ko1@a...>
Date: Tue, 16 Jun 2015 04:39:18 +0900 (JST)
Subject: [ruby-changes:38829] normal:r50910 (trunk): socket: allow exception-free nonblocking sendmsg/recvmsg
normal 2015-06-16 04:38:49 +0900 (Tue, 16 Jun 2015) New Revision: 50910 http://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=revision&revision=50910 Log: socket: allow exception-free nonblocking sendmsg/recvmsg As documented before, exceptions are expensive and IO::Wait*able are too common in socket applications to be the exceptional case. Datagram sockets deserve the same API which stream sockets are allowed with read_nonblock and write_nonblock. Note: this does not offer a performance advantage under optimal conditions when both ends are equally matched in speed, but it it does make debug output cleaner by avoiding exceptions whenever the receiver slows down. * ext/socket/ancdata.c (bsock_sendmsg_internal, bsock_recvmsg_internal): support "exception: false" kwarg * ext/socket/init.c (rsock_s_recvfrom_nonblock): ditto * ext/socket/init.c (rsock_s_recvfrom_nonblock): use rsock_opt_false_p * ext/socket/socket.c (sock_connect_nonblock): ditto * ext/socket/rubysocket.h (rsock_opt_false_p): new function * ext/socket/basicsocket.c (bsock_recv_nonblock): update rdoc * ext/socket/udpsocket.c (udp_recvfrom_nonblock): ditto * test/socket/test_nonblock.rb: new tests [ruby-core:69542] [Feature #11229] Modified files: trunk/ChangeLog trunk/ext/socket/ancdata.c trunk/ext/socket/basicsocket.c trunk/ext/socket/init.c trunk/ext/socket/rubysocket.h trunk/ext/socket/socket.c trunk/ext/socket/udpsocket.c trunk/test/socket/test_nonblock.rb Index: ChangeLog =================================================================== --- ChangeLog (revision 50909) +++ ChangeLog (revision 50910) @@ -1,3 +1,18 @@ https://github.com/ruby/ruby/blob/trunk/ChangeLog#L1 +Tue Jun 16 04:38:02 2015 Eric Wong <e@8...> + + * ext/socket/ancdata.c (bsock_sendmsg_internal, + bsock_recvmsg_internal): + support "exception: false" kwarg + * ext/socket/init.c (rsock_s_recvfrom_nonblock): + ditto + * ext/socket/init.c (rsock_s_recvfrom_nonblock): use rsock_opt_false_p + * ext/socket/socket.c (sock_connect_nonblock): ditto + * ext/socket/rubysocket.h (rsock_opt_false_p): new function + * ext/socket/basicsocket.c (bsock_recv_nonblock): update rdoc + * ext/socket/udpsocket.c (udp_recvfrom_nonblock): ditto + * test/socket/test_nonblock.rb: new tests + [ruby-core:69542] [Feature #11229] + Mon Jun 15 14:33:02 2015 Akinori MUSHA <knu@i...> * lib/set.rb: Make Set#each and SortedSet#each generate a sized Index: ext/socket/rubysocket.h =================================================================== --- ext/socket/rubysocket.h (revision 50909) +++ ext/socket/rubysocket.h (revision 50910) @@ -423,4 +423,12 @@ static inline void rsock_maybe_wait_fd(i https://github.com/ruby/ruby/blob/trunk/ext/socket/rubysocket.h#L423 # define MSG_DONTWAIT_RELIABLE 0 #endif +static inline int +rsock_opt_false_p(VALUE opt, VALUE sym) +{ + if (!NIL_P(opt) && Qfalse == rb_hash_lookup2(opt, sym, Qundef)) + return 1; + return 0; +} + #endif Index: ext/socket/udpsocket.c =================================================================== --- ext/socket/udpsocket.c (revision 50909) +++ ext/socket/udpsocket.c (revision 50910) @@ -194,8 +194,7 @@ udp_send(int argc, VALUE *argv, VALUE so https://github.com/ruby/ruby/blob/trunk/ext/socket/udpsocket.c#L194 /* * call-seq: - * udpsocket.recvfrom_nonblock(maxlen) => [mesg, sender_inet_addr] - * udpsocket.recvfrom_nonblock(maxlen, flags) => [mesg, sender_inet_addr] + * udpsocket.recvfrom_nonblock(maxlen [, flags [, options]) => [mesg, sender_inet_addr] * * Receives up to _maxlen_ bytes from +udpsocket+ using recvfrom(2) after * O_NONBLOCK is set for the underlying file descriptor. @@ -211,6 +210,7 @@ udp_send(int argc, VALUE *argv, VALUE so https://github.com/ruby/ruby/blob/trunk/ext/socket/udpsocket.c#L210 * === Parameters * * +maxlen+ - the number of bytes to receive from the socket * * +flags+ - zero or more of the +MSG_+ options + * * +options+ - keyword hash, supporting `exception: false` * * === Example * require 'socket' @@ -238,6 +238,10 @@ udp_send(int argc, VALUE *argv, VALUE so https://github.com/ruby/ruby/blob/trunk/ext/socket/udpsocket.c#L238 * it is extended by IO::WaitReadable. * So IO::WaitReadable can be used to rescue the exceptions for retrying recvfrom_nonblock. * + * By specifying `exception: false`, the options hash allows you to indicate + * that recvmsg_nonblock should not raise an IO::WaitWritable exception, but + * return the symbol :wait_writable instead. + * * === See * * Socket#recvfrom */ Index: ext/socket/init.c =================================================================== --- ext/socket/init.c (revision 50909) +++ ext/socket/init.c (revision 50910) @@ -188,9 +188,10 @@ rsock_s_recvfrom_nonblock(VALUE sock, in https://github.com/ruby/ruby/blob/trunk/ext/socket/init.c#L188 long slen; int fd, flags; VALUE addr = Qnil; + VALUE opts = Qnil; socklen_t len0; - rb_scan_args(argc, argv, "11", &len, &flg); + rb_scan_args(argc, argv, "11:", &len, &flg, &opts); if (flg == Qnil) flags = 0; else flags = NUM2INT(flg); @@ -226,6 +227,8 @@ rsock_s_recvfrom_nonblock(VALUE sock, in https://github.com/ruby/ruby/blob/trunk/ext/socket/init.c#L227 #if defined(EWOULDBLOCK) && EWOULDBLOCK != EAGAIN case EWOULDBLOCK: #endif + if (rsock_opt_false_p(opts, sym_exception)) + return sym_wait_readable; rb_readwrite_sys_fail(RB_IO_WAIT_READABLE, "recvfrom(2) would block"); } rb_sys_fail("recvfrom(2)"); @@ -528,14 +531,10 @@ rsock_s_accept_nonblock(int argc, VALUE https://github.com/ruby/ruby/blob/trunk/ext/socket/init.c#L531 struct sockaddr *sockaddr, socklen_t *len) { int fd2; - int ex = 1; VALUE opts = Qnil; rb_scan_args(argc, argv, "0:", &opts); - if (!NIL_P(opts) && Qfalse == rb_hash_lookup2(opts, sym_exception, Qundef)) - ex = 0; - rb_secure(3); rb_io_set_nonblock(fptr); fd2 = cloexec_accept(fptr->fd, (struct sockaddr*)sockaddr, len, 1); @@ -549,7 +548,7 @@ rsock_s_accept_nonblock(int argc, VALUE https://github.com/ruby/ruby/blob/trunk/ext/socket/init.c#L548 #if defined EPROTO case EPROTO: #endif - if (!ex) + if (rsock_opt_false_p(opts, sym_exception)) return sym_wait_readable; rb_readwrite_sys_fail(RB_IO_WAIT_READABLE, "accept(2) would block"); } Index: ext/socket/socket.c =================================================================== --- ext/socket/socket.c (revision 50909) +++ ext/socket/socket.c (revision 50910) @@ -503,15 +503,13 @@ sock_connect_nonblock(int argc, VALUE *a https://github.com/ruby/ruby/blob/trunk/ext/socket/socket.c#L503 n = connect(fptr->fd, (struct sockaddr*)RSTRING_PTR(addr), RSTRING_SOCKLEN(addr)); if (n < 0) { if (errno == EINPROGRESS) { - if (!NIL_P(opts) && - Qfalse == rb_hash_lookup2(opts, sym_exception, Qundef)) { + if (rsock_opt_false_p(opts, sym_exception)) { return sym_wait_writable; } rb_readwrite_sys_fail(RB_IO_WAIT_WRITABLE, "connect(2) would block"); } if (errno == EISCONN) { - if (!NIL_P(opts) && - Qfalse == rb_hash_lookup2(opts, sym_exception, Qundef)) { + if (rsock_opt_false_p(opts, sym_exception)) { return INT2FIX(0); } } Index: ext/socket/basicsocket.c =================================================================== --- ext/socket/basicsocket.c (revision 50909) +++ ext/socket/basicsocket.c (revision 50910) @@ -640,8 +640,7 @@ bsock_recv(int argc, VALUE *argv, VALUE https://github.com/ruby/ruby/blob/trunk/ext/socket/basicsocket.c#L640 /* * call-seq: - * basicsocket.recv_nonblock(maxlen) => mesg - * basicsocket.recv_nonblock(maxlen, flags) => mesg + * basicsocket.recv_nonblock(maxlen [, flags [, options ]) => mesg * * Receives up to _maxlen_ bytes from +socket+ using recvfrom(2) after * O_NONBLOCK is set for the underlying file descriptor. @@ -655,6 +654,7 @@ bsock_recv(int argc, VALUE *argv, VALUE https://github.com/ruby/ruby/blob/trunk/ext/socket/basicsocket.c#L654 * === Parameters * * +maxlen+ - the number of bytes to receive from the socket * * +flags+ - zero or more of the +MSG_+ options + * * +options+ - keyword hash, supporting `exception: false` * * === Example * serv = TCPServer.new("127.0.0.1", 0) @@ -679,6 +679,10 @@ bsock_recv(int argc, VALUE *argv, VALUE https://github.com/ruby/ruby/blob/trunk/ext/socket/basicsocket.c#L679 * it is extended by IO::WaitReadable. * So IO::WaitReadable can be used to rescue the exceptions for retrying recv_nonblock. * + * By specifying `exception: false`, the options hash allows you to indicate + * that recv_nonblock should not raise an IO::WaitWritable exception, but + * return the symbol :wait_writable instead. + * * === See * * Socket#recvfrom */ Index: ext/socket/ancdata.c =================================================================== --- ext/socket/ancdata.c (revision 50909) +++ ext/socket/ancdata.c (revision 50910) @@ -3,6 +3,7 @@ https://github.com/ruby/ruby/blob/trunk/ext/socket/ancdata.c#L3 #include <time.h> int rsock_cmsg_cloexec_state = -1; /* <0: unknown, 0: ignored, >0: working */ +static VALUE sym_exception, sym_wait_readable, sym_wait_writable; #if defined(HAVE_STRUCT_MSGHDR_MSG_CONTROL) static VALUE rb_cAncillaryData; @@ -1133,6 +1134,7 @@ bsock_sendmsg_internal(int argc, VALUE * https://github.com/ruby/ruby/blob/trunk/ext/socket/ancdata.c#L1134 VALUE data, vflags, dest_sockaddr; struct msghdr mh; struct iovec iov; + VALUE opts = Qnil; VALUE controls = Qnil; int controls_num; #if defined(HAVE_STRUCT_MSGHDR_MSG_CONTROL) @@ -1152,7 +1154,8 @@ bsock_sendmsg_internal(int argc, VALUE * https://github.com/ruby/ruby/blob/trunk/ext/socket/ancdata.c#L1154 if (argc == 0) rb_raise(rb_eArgError, "mesg argument required"); - rb_scan_args(argc, argv, "12*", &data, &vflags, &dest_sockaddr, &controls); + rb_scan_args(argc, argv, "12*:", &data, &vflags, &dest_sockaddr, &controls, + &opts); StringValue(data); controls_num = RARRAY_LENINT(controls); @@ -1281,8 +1284,13 @@ bsock_sendmsg_internal(int argc, VALUE * https://github.com/ruby/ruby/blob/trunk/ext/socket/ancdata.c#L1284 rb_io_check_closed(fptr); goto retry; } - if (nonblock && (errno == EWOULDBLOCK || errno == EAGAIN)) - rb_readwrite_sys_fail(RB_IO_WAIT_WRITABLE, "sendmsg(2) would block"); + if (nonblock && (errno == EWOULDBLOCK || errno == EAGAIN)) { + if (rsock_opt_false_p(opts, sym_exception)) { + return sym_wait_writable; + } + rb_readwrite_sys_fail(RB_IO_WAIT_WRITABLE, + "sendmsg(2) would block"); + } rb_sys_fail("sendmsg(2)"); } #if defined(HAVE_STRUCT_MSGHDR_MSG_CONTROL) @@ -1336,7 +1344,7 @@ rsock_bsock_sendmsg(int argc, VALUE *arg https://github.com/ruby/ruby/blob/trunk/ext/socket/ancdata.c#L1344 #if defined(HAVE_SENDMSG) /* * call-seq: - * basicsocket.sendmsg_nonblock(mesg, flags=0, dest_sockaddr=nil, *controls) => numbytes_sent + * basicsocket.sendmsg_nonblock(mesg, flags=0, dest_sockaddr=nil, *controls, opts={}) => numbytes_sent * * sendmsg_nonblock sends a message using sendmsg(2) system call in non-blocking manner. * @@ -1344,6 +1352,9 @@ rsock_bsock_sendmsg(int argc, VALUE *arg https://github.com/ruby/ruby/blob/trunk/ext/socket/ancdata.c#L1352 * but the non-blocking flag is set before the system call * and it doesn't retry the system call. * + * By specifying `exception: false`, the _opts_ hash allows you to indicate + * that sendmsg_nonblock should not raise an IO::WaitWritable exception, but + * return the symbol :wait_writable instead. */ VALUE rsock_bsock_sendmsg_nonblock(int argc, VALUE *argv, VALUE sock) @@ -1602,8 +1613,12 @@ bsock_recvmsg_internal(int argc, VALUE * https://github.com/ruby/ruby/blob/trunk/ext/socket/ancdata.c#L1613 rb_io_check_closed(fptr); goto retry; } - if (nonblock && (errno == EWOULDBLOCK || errno == EAGAIN)) + if (nonblock && (errno == EWOULDBLOCK || errno == EAGAIN)) { + if (rsock_opt_false_p(vopts, sym_exception)) { + return sym_wait_readable; + } rb_readwrite_sys_fail(RB_IO_WAIT_READABLE, "recvmsg(2) would block"); + } #if defined(HAVE_STRUCT_MSGHDR_MSG_CONTROL) if (!gc_done && (errno == EMFILE || errno == EMSGSIZE)) { /* @@ -1788,6 +1803,9 @@ rsock_bsock_recvmsg(int argc, VALUE *arg https://github.com/ruby/ruby/blob/trunk/ext/socket/ancdata.c#L1803 * but non-blocking flag is set before the system call * and it doesn't retry the system call. * + * By specifying `exception: false`, the _opts_ hash allows you to indicate + * that recvmsg_nonblock should not raise an IO::WaitWritable exception, but + * return the symbol :wait_writable instead. */ VALUE rsock_bsock_recvmsg_nonblock(int argc, VALUE *argv, VALUE sock) @@ -1833,4 +1851,8 @@ rsock_init_ancdata(void) https://github.com/ruby/ruby/blob/trunk/ext/socket/ancdata.c#L1851 rb_define_method(rb_cAncillaryData, "ipv6_pktinfo_addr", ancillary_ipv6_pktinfo_addr, 0); rb_define_method(rb_cAncillaryData, "ipv6_pktinfo_ifindex", ancillary_ipv6_pktinfo_ifindex, 0); #endif +#undef rb_intern + sym_exception = ID2SYM(rb_intern("exception")); + sym_wait_readable = ID2SYM(rb_intern("wait_readable")); + sym_wait_writable = ID2SYM(rb_intern("wait_writable")); } Index: test/socket/test_nonblock.rb =================================================================== --- test/socket/test_nonblock.rb (revision 50909) +++ test/socket/test_nonblock.rb (revision 50910) @@ -1,6 +1,7 @@ https://github.com/ruby/ruby/blob/trunk/test/socket/test_nonblock.rb#L1 begin require "socket" require "io/nonblock" + require "io/wait" rescue LoadError end @@ -275,6 +276,17 @@ class TestSocketNonblock < Test::Unit::T https://github.com/ruby/ruby/blob/trunk/test/socket/test_nonblock.rb#L276 } end + def test_recvfrom_nonblock_no_exception + udp_pair do |s1, s2| + assert_equal :wait_readable, s1.recvfrom_nonblock(100, exception: false) + s2.send("aaa", 0, s1.getsockname) + assert s1.wait_readable + mesg, inet_addr = s1.recvfrom_nonblock(100, exception: false) + assert_equal(4, inet_addr.length) + assert_equal("aaa", mesg) + end + end + if defined?(UNIXSocket) && defined?(Socket::SOCK_SEQPACKET) def test_sendmsg_nonblock_seqpacket buf = '*' * 8192 @@ -286,6 +298,27 @@ class TestSocketNonblock < Test::Unit::T https://github.com/ruby/ruby/blob/trunk/test/socket/test_nonblock.rb#L298 rescue NotImplementedError, Errno::ENOSYS, Errno::EPROTONOSUPPORT skip "UNIXSocket.pair(:SEQPACKET) not implemented on this platform: #{$!}" end + + def test_sendmsg_nonblock_no_exception + buf = '*' * 128 + UNIXSocket.pair(:SEQPACKET) do |s1, s2| + n = 0 + Timeout.timeout(60) do + case rv = s1.sendmsg_nonblock(buf, exception: false) + when Integer + n += rv + when :wait_writable + break + else + flunk "unexpected return value: #{rv.inspect}" + end while true + assert_equal :wait_writable, rv + assert_operator n, :>, 0 + end + end + rescue NotImplementedError, Errno::ENOSYS, Errno::EPROTONOSUPPORT + skip "UNIXSocket.pair(:SEQPACKET) not implemented on this platform: #{$!}" + end end def test_recvmsg_nonblock_error @@ -297,6 +330,7 @@ class TestSocketNonblock < Test::Unit::T https://github.com/ruby/ruby/blob/trunk/test/socket/test_nonblock.rb#L330 rescue Errno::EWOULDBLOCK assert_kind_of(IO::WaitReadable, $!) end + assert_equal :wait_readable, s1.recvmsg_nonblock(11, exception: false) } end @@ -310,6 +344,16 @@ class TestSocketNonblock < Test::Unit::T https://github.com/ruby/ruby/blob/trunk/test/socket/test_nonblock.rb#L344 } end + def test_recv_nonblock_no_exception + tcp_pair {|c, s| + assert_equal :wait_readable, c.recv_nonblock(11, exception: false) + s.write('HI') + assert c.wait_readable + assert_equal 'HI', c.recv_nonblock(11, exception: false) + assert_equal :wait_readable, c.recv_nonblock(11, exception: false) + } + end + def test_connect_nonblock_error serv = TCPServer.new("127.0.0.1", 0) _, port, _, _ = serv.addr -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/