librelist archives

« back to archive

[PATCH] epoll: cleanup GVL-release code for Ruby 2.0.0

[PATCH] epoll: cleanup GVL-release code for Ruby 2.0.0

From:
Eric Wong
Date:
2013-04-13 @ 00:22
rb_thread_blocking_region is deprecated in Ruby 2.0, but
rb_thread_io_blocking region is not (and superior for I/O).  So we will
favor rb_thread_io_blocking_region for now.

While we're at it, reimplement green-thread-safe (Ruby 1.8) epoll_wait
in Ruby instead of C.  The extra #ifdefs for 1.8.7 were more prone to
bitrot and Ruby code should be easier to follow for Rubyists who care
about 1.8.
---
 ext/sleepy_penguin/epoll.c          | 13 +++--
 ext/sleepy_penguin/epoll_green.h    | 95 -------------------------------------
 ext/sleepy_penguin/extconf.rb       |  1 +
 ext/sleepy_penguin/inotify.c        |  2 +-
 ext/sleepy_penguin/sleepy_penguin.h | 45 ++++++++++++++----
 ext/sleepy_penguin/util.c           | 14 ------
 lib/sleepy_penguin/epoll/io.rb      | 27 +++++++++++
 7 files changed, 71 insertions(+), 126 deletions(-)
 delete mode 100644 ext/sleepy_penguin/epoll_green.h
 create mode 100644 lib/sleepy_penguin/epoll/io.rb

diff --git a/ext/sleepy_penguin/epoll.c b/ext/sleepy_penguin/epoll.c
index 7ce5dcb..46183ca 100644
--- a/ext/sleepy_penguin/epoll.c
+++ b/ext/sleepy_penguin/epoll.c
@@ -176,7 +176,6 @@ static int epoll_resume_p(uint64_t expire_at, struct 
ep_per_thread *ept)
 	return 1;
 }
 
-#if defined(HAVE_RB_THREAD_BLOCKING_REGION)
 static VALUE nogvl_wait(void *args)
 {
 	struct ep_per_thread *ept = args;
@@ -187,18 +186,15 @@ static VALUE nogvl_wait(void *args)
 
 static VALUE real_epwait(struct ep_per_thread *ept)
 {
-	int n;
+	long n;
 	uint64_t expire_at = ept->timeout > 0 ? now_ms() + ept->timeout : 0;
 
 	do {
-		n = (int)rb_sp_fd_region(nogvl_wait, ept, ept->fd);
+		n = (long)rb_sp_fd_region(nogvl_wait, ept, ept->fd);
 	} while (n == -1 && epoll_resume_p(expire_at, ept));
 
-	return epwait_result(ept, n);
+	return epwait_result(ept, (int)n);
 }
-#else /* 1.8 Green thread compatible code */
-#  include "epoll_green.h"
-#endif /* 1.8 Green thread compatibility code */
 
 /*
  * call-seq:
@@ -342,4 +338,7 @@ void sleepy_penguin_init_epoll(void)
 	rb_define_const(cEpoll, "ONESHOT", UINT2NUM(EPOLLONESHOT));
 
 	id_for_fd = rb_intern("for_fd");
+
+	if (RB_SP_GREEN_THREAD)
+		rb_require("sleepy_penguin/epoll/io");
 }
diff --git a/ext/sleepy_penguin/epoll_green.h b/ext/sleepy_penguin/epoll_green.h
deleted file mode 100644
index e3414eb..0000000
--- a/ext/sleepy_penguin/epoll_green.h
+++ /dev/null
@@ -1,95 +0,0 @@
-/* this file is only used by Matz Ruby 1.8 which used green threads */
-
-/*
- * we have to worry about green threads and always pass zero
- * as the timeout for epoll_wait :(
- */
-#include <rubysig.h>
-#include <sys/time.h>
-
-/* in case _BSD_SOURCE doesn't give us this macro */
-#ifndef timersub
-#  define timersub(a, b, result) \
-do { \
-	(result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \
-	(result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \
-	if ((result)->tv_usec < 0) { \
-		--(result)->tv_sec; \
-		(result)->tv_usec += 1000000; \
-	} \
-} while (0)
-#endif
-
-static int safe_epoll_wait(struct ep_per_thread *ept)
-{
-	int n;
-
-	do {
-		TRAP_BEG;
-		n = epoll_wait(ept->fd, ept->events, ept->maxevents, 0);
-		TRAP_END;
-	} while (n == -1 && ep_fd_check(ept) && errno == EINTR);
-
-	return n;
-}
-
-static int epwait_forever(struct ep_per_thread *ept)
-{
-	int n;
-
-	do {
-		(void)rb_io_wait_readable(ept->fd);
-		n = safe_epoll_wait(ept);
-	} while (n == 0);
-
-	return n;
-}
-
-static int epwait_timed(struct ep_per_thread *ept)
-{
-	struct timeval tv;
-
-	tv.tv_sec = ept->timeout / 1000;
-	tv.tv_usec = (ept->timeout % 1000) * 1000;
-
-	for (;;) {
-		struct timeval t0, now, diff;
-		int n;
-		int fd = ept->fd;
-		fd_set rfds;
-
-		FD_ZERO(&rfds);
-		FD_SET(fd, &rfds);
-
-		gettimeofday(&t0, NULL);
-		(void)rb_thread_select(fd + 1, &rfds, NULL, NULL, &tv);
-		n = safe_epoll_wait(ept);
-		if (n != 0)
-			return n;
-
-		/* XXX use CLOCK_MONOTONIC if people care about 1.8... */
-		gettimeofday(&now, NULL);
-		timersub(&now, &t0, &diff);
-		timersub(&tv, &diff, &tv);
-
-		if (tv.tv_usec < 0 || tv.tv_sec < 0)
-			return (n == -1) ? 0 : n;
-	}
-
-	assert("should never get here (epwait_timed)");
-	return -1;
-}
-
-static VALUE real_epwait(struct ep_per_thread *ept)
-{
-	int n;
-
-	if (ept->timeout == -1)
-		n = epwait_forever(ept);
-	else if (ept->timeout == 0)
-		n = safe_epoll_wait(ept);
-	else
-		n = epwait_timed(ept);
-
-	return epwait_result(ept, n);
-}
diff --git a/ext/sleepy_penguin/extconf.rb b/ext/sleepy_penguin/extconf.rb
index fe8e1ac..5e2c223 100644
--- a/ext/sleepy_penguin/extconf.rb
+++ b/ext/sleepy_penguin/extconf.rb
@@ -11,6 +11,7 @@
 have_header('sys/inotify.h')
 have_header('ruby/io.h') and have_struct_member('rb_io_t', 'fd', 'ruby/io.h')
 have_func('epoll_create1', %w(sys/epoll.h))
+have_func('rb_thread_call_without_gvl')
 have_func('rb_thread_blocking_region')
 have_func('rb_thread_io_blocking_region')
 have_func('rb_thread_fd_close')
diff --git a/ext/sleepy_penguin/inotify.c b/ext/sleepy_penguin/inotify.c
index 344145c..01862ae 100644
--- a/ext/sleepy_penguin/inotify.c
+++ b/ext/sleepy_penguin/inotify.c
@@ -215,7 +215,7 @@ static VALUE take(int argc, VALUE *argv, VALUE self)
 	else
 		blocking_io_prepare(args.fd);
 	do {
-		r = rb_sp_fd_region(inread, &args, args.fd);
+		r = (ssize_t)rb_sp_fd_region(inread, &args, args.fd);
 		if (r == 0 /* Linux < 2.6.21 */
 		    ||
 		    (r < 0 && errno == EINVAL) /* Linux >= 2.6.21 */
diff --git a/ext/sleepy_penguin/sleepy_penguin.h 
b/ext/sleepy_penguin/sleepy_penguin.h
index 599b319..86c6e5f 100644
--- a/ext/sleepy_penguin/sleepy_penguin.h
+++ b/ext/sleepy_penguin/sleepy_penguin.h
@@ -19,24 +19,51 @@ int rb_sp_io_closed(VALUE io);
 int rb_sp_fileno(VALUE io);
 void rb_sp_set_nonblock(int fd);
 
-#ifdef HAVE_RB_THREAD_BLOCKING_REGION
-static inline VALUE rb_sp_io_region(rb_blocking_function_t *func, void *data)
-{
-	return rb_thread_blocking_region(func, data, RUBY_UBF_IO, 0);
-}
+#if defined(HAVE_RB_THREAD_BLOCKING_REGION) || \
+    defined(HAVE_RB_THREAD_IO_BLOCKING_REGION) || \
+    defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
+#  define RB_SP_GREEN_THREAD 0
 #  define blocking_io_prepare(fd) ((void)(fd))
 #else
-typedef VALUE rb_blocking_function_t(void *);
-VALUE rb_sp_io_region(rb_blocking_function_t *func, void *data);
+#  define RB_SP_GREEN_THREAD 1
 #  define blocking_io_prepare(fd) rb_sp_set_nonblock((fd))
 #endif
 
 #ifdef HAVE_RB_THREAD_IO_BLOCKING_REGION
+/* Ruby 1.9.3 and 2.0.0 */
 VALUE rb_thread_io_blocking_region(rb_blocking_function_t *, void *, int);
 #  define rb_sp_fd_region(fn,data,fd) \
-          rb_thread_io_blocking_region((fn),(data),(fd))
+	rb_thread_io_blocking_region((fn),(data),(fd))
+#elif defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL) && \
+	defined(HAVE_RUBY_THREAD_H) && HAVE_RUBY_THREAD_H
+/* in case Ruby 2.0+ ever drops rb_thread_io_blocking_region: */
+#  include <ruby/thread.h>
+#  define COMPAT_FN (void *(*)(void *))
+#  define rb_sp_fd_region(fn,data,fd) \
+	rb_thread_call_without_gvl(COMPAT_FN(fn),(data),RUBY_UBF_IO,NULL)
+#elif defined(HAVE_RB_THREAD_BLOCKING_REGION)
+/* Ruby 1.9.2 */
+#  define rb_sp_fd_region(fn,data,fd) \
+	rb_thread_blocking_region((fn),(data),RUBY_UBF_IO,NULL)
 #else
-#  define rb_sp_fd_region(fn,data,fd) rb_sp_io_region(fn,data)
+/*
+ * Ruby 1.8 does not have a GVL, we'll just enable signal interrupts
+ * here in case we make interruptible syscalls.
+ *
+ * Note: epoll_wait with timeout=0 was interruptible until Linux 2.6.39
+ */
+#  include <rubysig.h>
+static inline VALUE fake_blocking_region(VALUE (*fn)(void *), void *data)
+{
+	VALUE rv;
+
+	TRAP_BEG;
+	rv = fn(data);
+	TRAP_END;
+
+	return rv;
+}
+#  define rb_sp_fd_region(fn,data,fd) fake_blocking_region((fn),(data))
 #endif
 
 #define NODOC_CONST(klass,name,value) \
diff --git a/ext/sleepy_penguin/util.c b/ext/sleepy_penguin/util.c
index 5cf84bf..4ae702d 100644
--- a/ext/sleepy_penguin/util.c
+++ b/ext/sleepy_penguin/util.c
@@ -135,20 +135,6 @@ void rb_sp_set_nonblock(int fd)
 		rb_sys_fail("fcntl(F_SETFL)");
 }
 
-#ifndef HAVE_RB_THREAD_BLOCKING_REGION
-#include <rubysig.h>
-VALUE rb_sp_io_region(rb_blocking_function_t *func, void *data)
-{
-	VALUE rv;
-
-	TRAP_BEG;
-	rv = func(data);
-	TRAP_END;
-
-	return rv;
-}
-#endif
-
 int rb_sp_wait(rb_sp_waitfn waiter, VALUE obj, int *fd)
 {
 	/*
diff --git a/lib/sleepy_penguin/epoll/io.rb b/lib/sleepy_penguin/epoll/io.rb
new file mode 100644
index 0000000..caeb376
--- /dev/null
+++ b/lib/sleepy_penguin/epoll/io.rb
@@ -0,0 +1,27 @@
+# :stopdoc:
+class SleepyPenguin::Epoll::IO
+  alias __epoll_wait epoll_wait
+  undef_method :epoll_wait
+  def epoll_wait(maxevents = 64, timeout = nil)
+    begin
+      if timeout == nil || timeout < 0 # wait forever
+        begin
+          IO.select([self])
+          n = __epoll_wait(maxevents, 0) { |events,io| yield(events, io) }
+        end while n == 0
+      elsif timeout == 0
+        return __epoll_wait(maxevents, 0) { |events,io| yield(events, io) }
+      else
+        done = Time.now + (timeout / 1000.0)
+        begin
+          tout = done - Time.now
+          IO.select([self], nil, nil, tout) if tout > 0
+          n = __epoll_wait(maxevents, 0) { |events,io| yield(events, io) }
+        end while n == 0 && tout > 0
+      end
+      n
+    rescue Errno::EINTR
+      retry
+    end
+  end
+end
-- 
Eric Wong