about summary refs log tree commit homepage
path: root/ext/kgio/autopush.c
diff options
context:
space:
mode:
authorEric Wong <e@80x24.org>2016-12-15 22:53:48 +0000
committerEric Wong <e@80x24.org>2016-12-15 23:51:08 +0000
commit333347c3ae54c8d605c673fcd11ff8dcb2ea4c38 (patch)
tree0663faa01cb9f84457a1b55ab52867ed986dd64c /ext/kgio/autopush.c
parent64dc570f4b99f68b5ed792b36e7e8abc3df74927 (diff)
downloadkgio-333347c3ae54c8d605c673fcd11ff8dcb2ea4c38.tar.gz
The regression for existing users was unnacceptable and
completely poor judgement on my part.  This change brings
us back to potentially not-future-compatible code which
will impose maintenance burdens on us in the face of
future Ruby changes.

But TODAY, it is the most performant option for folks who
need to use autopush.

Revert "resurrect Kgio.autopush support in pure Ruby"
and "remove autopush support and make it a no-op"

This reverts commits 64dc570f4b99f68b5ed792b36e7e8abc3df74927
and 4347980fa66115425fa8b765353c8b1bfe5dec24.
Diffstat (limited to 'ext/kgio/autopush.c')
-rw-r--r--ext/kgio/autopush.c252
1 files changed, 252 insertions, 0 deletions
diff --git a/ext/kgio/autopush.c b/ext/kgio/autopush.c
new file mode 100644
index 0000000..f9b9ef2
--- /dev/null
+++ b/ext/kgio/autopush.c
@@ -0,0 +1,252 @@
+/*
+ * We use a very basic strategy to use TCP_CORK semantics optimally
+ * in most TCP servers:  On corked sockets, we will uncork on recv()
+ * if there was a previous send().  Otherwise we do not fiddle
+ * with TCP_CORK at all.
+ *
+ * Under Linux, we can rely on TCP_CORK being inherited in an
+ * accept()-ed client socket so we can avoid syscalls for each
+ * accept()-ed client if we know the accept() socket corks.
+ *
+ * This module does NOTHING for client TCP sockets, we only deal
+ * with accept()-ed sockets right now.
+ */
+
+#include "kgio.h"
+#include "my_fileno.h"
+#include <netinet/tcp.h>
+
+/*
+ * As of FreeBSD 4.5, TCP_NOPUSH == TCP_CORK
+ * ref: http://dotat.at/writing/nopush.html
+ * We won't care for older FreeBSD since nobody runs Ruby on them...
+ */
+#ifdef TCP_CORK
+#  define KGIO_NOPUSH TCP_CORK
+#elif defined(TCP_NOPUSH)
+#  define KGIO_NOPUSH TCP_NOPUSH
+#endif
+
+#ifdef KGIO_NOPUSH
+static ID id_autopush_state;
+static int enabled = 1;
+
+enum autopush_state {
+        AUTOPUSH_STATE_ACCEPTOR_IGNORE = -1,
+        AUTOPUSH_STATE_IGNORE = 0,
+        AUTOPUSH_STATE_WRITER = 1,
+        AUTOPUSH_STATE_WRITTEN = 2,
+        AUTOPUSH_STATE_ACCEPTOR = 3
+};
+
+#if defined(R_CAST) && \
+    defined(HAVE_TYPE_STRUCT_RFILE) && \
+    defined(HAVE_TYPE_STRUCT_ROBJECT) && \
+    ((SIZEOF_STRUCT_RFILE + SIZEOF_INT) <= (SIZEOF_STRUCT_ROBJECT))
+
+struct AutopushSocket {
+        struct RFile rfile;
+        enum autopush_state autopush_state;
+};
+
+static enum autopush_state state_get(VALUE io)
+{
+        return ((struct AutopushSocket *)(io))->autopush_state;
+}
+
+static void state_set(VALUE io, enum autopush_state state)
+{
+        ((struct AutopushSocket *)(io))->autopush_state = state;
+}
+#else
+static enum autopush_state state_get(VALUE io)
+{
+        VALUE val;
+
+        if (rb_ivar_defined(io, id_autopush_state) == Qfalse)
+                return AUTOPUSH_STATE_IGNORE;
+        val = rb_ivar_get(io, id_autopush_state);
+
+        return (enum autopush_state)NUM2INT(val);
+}
+
+static void state_set(VALUE io, enum autopush_state state)
+{
+        rb_ivar_set(io, id_autopush_state, INT2NUM(state));
+}
+#endif /* IVAR fallback */
+
+static enum autopush_state detect_acceptor_state(VALUE io);
+static void push_pending_data(VALUE io);
+
+/*
+ * call-seq:
+ *        Kgio.autopush? -> true or false
+ *
+ * Returns whether or not autopush is enabled.
+ *
+ * Only available on systems with TCP_CORK (Linux) or
+ * TCP_NOPUSH (FreeBSD, and maybe other *BSDs).
+ */
+static VALUE s_get_autopush(VALUE self)
+{
+        return enabled ? Qtrue : Qfalse;
+}
+
+/*
+ * call-seq:
+ *        Kgio.autopush = true
+ *        Kgio.autopush = false
+ *
+ * Enables or disables autopush for sockets created with kgio_accept
+ * and kgio_tryaccept methods.  Autopush relies on TCP_CORK/TCP_NOPUSH
+ * being enabled on the listen socket.
+ *
+ * Only available on systems with TCP_CORK (Linux) or
+ * TCP_NOPUSH (FreeBSD, and maybe other *BSDs).
+ *
+ * Please do not use this (or kgio at all) in new code.  Under Linux,
+ * use MSG_MORE, instead, as it requires fewer syscalls.  Users of
+ * other systems are encouraged to add MSG_MORE support to their
+ * favorite OS.
+ */
+static VALUE s_set_autopush(VALUE self, VALUE val)
+{
+        enabled = RTEST(val);
+
+        return val;
+}
+
+/*
+ * call-seq:
+ *
+ *        io.kgio_autopush?  -> true or false
+ *
+ * Returns the current autopush state of the Kgio::SocketMethods-enabled
+ * socket.
+ *
+ * Only available on systems with TCP_CORK (Linux) or
+ * TCP_NOPUSH (FreeBSD, and maybe other *BSDs).
+ */
+static VALUE autopush_get(VALUE io)
+{
+        return state_get(io) <= 0 ? Qfalse : Qtrue;
+}
+
+/*
+ * call-seq:
+ *
+ *        io.kgio_autopush = true
+ *        io.kgio_autopush = false
+ *
+ * Enables or disables autopush on any given Kgio::SocketMethods-capable
+ * IO object.  This does NOT enable or disable TCP_NOPUSH/TCP_CORK right
+ * away, that must be done with IO.setsockopt
+ *
+ * Only available on systems with TCP_CORK (Linux) or
+ * TCP_NOPUSH (FreeBSD, and maybe other *BSDs).
+ */
+static VALUE autopush_set(VALUE io, VALUE vbool)
+{
+        if (RTEST(vbool))
+                state_set(io, AUTOPUSH_STATE_WRITER);
+        else
+                state_set(io, AUTOPUSH_STATE_IGNORE);
+        return vbool;
+}
+
+void init_kgio_autopush(void)
+{
+        VALUE mKgio = rb_define_module("Kgio");
+        VALUE tmp;
+
+        rb_define_singleton_method(mKgio, "autopush?", s_get_autopush, 0);
+        rb_define_singleton_method(mKgio, "autopush=", s_set_autopush, 1);
+
+        tmp = rb_define_module_under(mKgio, "SocketMethods");
+        rb_define_method(tmp, "kgio_autopush=", autopush_set, 1);
+        rb_define_method(tmp, "kgio_autopush?", autopush_get, 0);
+
+        id_autopush_state = rb_intern("@kgio_autopush_state");
+}
+
+/*
+ * called after a successful write, just mark that we've put something
+ * in the skb and will need to uncork on the next write.
+ */
+void kgio_autopush_send(VALUE io)
+{
+        if (state_get(io) == AUTOPUSH_STATE_WRITER)
+                state_set(io, AUTOPUSH_STATE_WRITTEN);
+}
+
+/* called on successful accept() */
+void kgio_autopush_accept(VALUE accept_io, VALUE client_io)
+{
+        enum autopush_state acceptor_state;
+
+        if (!enabled)
+                return;
+        acceptor_state = state_get(accept_io);
+        if (acceptor_state == AUTOPUSH_STATE_IGNORE)
+                acceptor_state = detect_acceptor_state(accept_io);
+        if (acceptor_state == AUTOPUSH_STATE_ACCEPTOR)
+                state_set(client_io, AUTOPUSH_STATE_WRITER);
+        else
+                state_set(client_io, AUTOPUSH_STATE_IGNORE);
+}
+
+void kgio_autopush_recv(VALUE io)
+{
+        if (enabled && (state_get(io) == AUTOPUSH_STATE_WRITTEN)) {
+                push_pending_data(io);
+                state_set(io, AUTOPUSH_STATE_WRITER);
+        }
+}
+
+static enum autopush_state detect_acceptor_state(VALUE io)
+{
+        int corked = 0;
+        int fd = my_fileno(io);
+        socklen_t optlen = sizeof(int);
+        enum autopush_state state;
+
+        if (getsockopt(fd, IPPROTO_TCP, KGIO_NOPUSH, &corked, &optlen) != 0) {
+                if (errno != EOPNOTSUPP)
+                        rb_sys_fail("getsockopt(TCP_CORK/TCP_NOPUSH)");
+                errno = 0;
+                state = AUTOPUSH_STATE_ACCEPTOR_IGNORE;
+        } else if (corked) {
+                state = AUTOPUSH_STATE_ACCEPTOR;
+        } else {
+                state = AUTOPUSH_STATE_ACCEPTOR_IGNORE;
+        }
+        state_set(io, state);
+
+        return state;
+}
+
+/*
+ * checks to see if we've written anything since the last recv()
+ * If we have, uncork the socket and immediately recork it.
+ */
+static void push_pending_data(VALUE io)
+{
+        int optval = 0;
+        const socklen_t optlen = sizeof(int);
+        const int fd = my_fileno(io);
+
+        if (setsockopt(fd, IPPROTO_TCP, KGIO_NOPUSH, &optval, optlen) != 0)
+                rb_sys_fail("setsockopt(TCP_CORK/TCP_NOPUSH, 0)");
+        /* immediately recork */
+        optval = 1;
+        if (setsockopt(fd, IPPROTO_TCP, KGIO_NOPUSH, &optval, optlen) != 0)
+                rb_sys_fail("setsockopt(TCP_CORK/TCP_NOPUSH, 1)");
+}
+#else /* !KGIO_NOPUSH */
+void kgio_autopush_recv(VALUE io){}
+void kgio_autopush_send(VALUE io){}
+void init_kgio_autopush(void)
+{
+}
+#endif /* ! KGIO_NOPUSH */