about summary refs log tree commit homepage
path: root/ext/unicorn_http
diff options
context:
space:
mode:
Diffstat (limited to 'ext/unicorn_http')
-rw-r--r--ext/unicorn_http/epollexclusive.h56
-rw-r--r--ext/unicorn_http/extconf.rb5
-rw-r--r--ext/unicorn_http/httpdate.c20
-rw-r--r--ext/unicorn_http/unicorn_http.rl21
-rw-r--r--ext/unicorn_http/unicorn_http_common.rl2
5 files changed, 69 insertions, 35 deletions
diff --git a/ext/unicorn_http/epollexclusive.h b/ext/unicorn_http/epollexclusive.h
index 677e1fe..c74a779 100644
--- a/ext/unicorn_http/epollexclusive.h
+++ b/ext/unicorn_http/epollexclusive.h
@@ -22,6 +22,18 @@
 #endif
 
 #if USE_EPOLL
+#if defined(HAVE_RB_IO_DESCRIPTOR) /* Ruby 3.1+ */
+#        define my_fileno(io) rb_io_descriptor(io)
+#else /* Ruby <3.1 */
+static int my_fileno(VALUE io)
+{
+        rb_io_t *fptr;
+        GetOpenFile(io, fptr);
+        rb_io_check_closed(fptr);
+        return fptr->fd;
+}
+#endif /* Ruby <3.1 */
+
 /*
  * :nodoc:
  * returns IO object if EPOLLEXCLUSIVE works and arms readers
@@ -38,9 +50,8 @@ static VALUE prep_readers(VALUE cls, VALUE readers)
 
         Check_Type(readers, T_ARRAY);
         for (i = 0; i < RARRAY_LEN(readers); i++) {
-                int rc;
+                int rc, fd;
                 struct epoll_event e;
-                rb_io_t *fptr;
                 VALUE io = rb_ary_entry(readers, i);
 
                 e.data.u64 = i; /* the reason readers shouldn't change */
@@ -53,9 +64,8 @@ static VALUE prep_readers(VALUE cls, VALUE readers)
                  * cycles on maintaining level-triggering.
                  */
                 e.events = EPOLLEXCLUSIVE | EPOLLIN;
-                io = rb_io_get_io(io);
-                GetOpenFile(io, fptr);
-                rc = epoll_ctl(epfd, EPOLL_CTL_ADD, fptr->fd, &e);
+                fd = my_fileno(rb_io_get_io(io));
+                rc = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &e);
                 if (rc < 0) rb_sys_fail("epoll_ctl");
         }
         return epio;
@@ -64,18 +74,22 @@ static VALUE prep_readers(VALUE cls, VALUE readers)
 
 #if USE_EPOLL
 struct ep_wait {
-        struct epoll_event *events;
-        rb_io_t *fptr;
-        int maxevents;
+        struct epoll_event event;
+        int epfd;
         int timeout_msec;
 };
 
 static void *do_wait(void *ptr) /* runs w/o GVL */
 {
         struct ep_wait *epw = ptr;
-
-        return (void *)(long)epoll_wait(epw->fptr->fd, epw->events,
-                                epw->maxevents, epw->timeout_msec);
+        /*
+         * Linux delivers epoll events in the order received, and using
+         * maxevents=1 ensures we pluck one item off ep->rdllist
+         * at-a-time (c.f. fs/eventpoll.c in linux.git, it's quite
+         * easy-to-understand for anybody familiar with Ruby C).
+         */
+        return (void *)(long)epoll_wait(epw->epfd, &epw->event, 1,
+                                        epw->timeout_msec);
 }
 
 /* :nodoc: */
@@ -84,32 +98,22 @@ static VALUE
 get_readers(VALUE epio, VALUE ready, VALUE readers, VALUE timeout_msec)
 {
         struct ep_wait epw;
-        long i, n;
-        VALUE buf;
+        long n;
 
         Check_Type(ready, T_ARRAY);
         Check_Type(readers, T_ARRAY);
-        epw.maxevents = RARRAY_LENINT(readers);
-        buf = rb_str_buf_new(sizeof(struct epoll_event) * epw.maxevents);
-        epw.events = (struct epoll_event *)RSTRING_PTR(buf);
-        epio = rb_io_get_io(epio);
-        GetOpenFile(epio, epw.fptr);
 
+        epw.epfd = my_fileno(epio);
         epw.timeout_msec = NUM2INT(timeout_msec);
         n = (long)rb_thread_call_without_gvl(do_wait, &epw, RUBY_UBF_IO, NULL);
         if (n < 0) {
                 if (errno != EINTR) rb_sys_fail("epoll_wait");
-                n = 0;
-        }
-        /* Linux delivers events in order received */
-        for (i = 0; i < n; i++) {
-                struct epoll_event *ev = &epw.events[i];
-                VALUE obj = rb_ary_entry(readers, ev->data.u64);
+        } else if (n > 0) { /* maxevents is hardcoded to 1 */
+                VALUE obj = rb_ary_entry(readers, epw.event.data.u64);
 
                 if (RTEST(obj))
                         rb_ary_push(ready, obj);
-        }
-        rb_str_resize(buf, 0);
+        } /* n == 0 : timeout */
         return Qfalse;
 }
 #endif /* USE_EPOLL */
diff --git a/ext/unicorn_http/extconf.rb b/ext/unicorn_http/extconf.rb
index 80d00e5..de896fe 100644
--- a/ext/unicorn_http/extconf.rb
+++ b/ext/unicorn_http/extconf.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 require 'mkmf'
 
 have_func("rb_hash_clear", "ruby.h") or abort 'Ruby 2.0+ required'
@@ -33,5 +34,7 @@ else
   message("no, needs Ruby 2.6+\n")
 end
 
-have_func('epoll_create1', %w(sys/epoll.h))
+if have_func('epoll_create1', %w(sys/epoll.h))
+  have_func('rb_io_descriptor') # Ruby 3.1+
+end
 create_makefile("unicorn_http")
diff --git a/ext/unicorn_http/httpdate.c b/ext/unicorn_http/httpdate.c
index 3f512dd..0faf5da 100644
--- a/ext/unicorn_http/httpdate.c
+++ b/ext/unicorn_http/httpdate.c
@@ -1,5 +1,6 @@
 #include <ruby.h>
 #include <time.h>
+#include <sys/time.h>
 #include <stdio.h>
 
 static const size_t buf_capa = sizeof("Thu, 01 Jan 1970 00:00:00 GMT");
@@ -43,13 +44,24 @@ static struct tm * my_gmtime_r(time_t *now, struct tm *tm)
 static VALUE httpdate(VALUE self)
 {
         static time_t last;
-        time_t now = time(NULL); /* not a syscall on modern 64-bit systems */
+        struct timeval now;
         struct tm tm;
 
-        if (last == now)
+        /*
+         * Favor gettimeofday(2) over time(2), as the latter can return the
+         * wrong value in the first 1 .. 2.5 ms of every second(!)
+         *
+         * https://lore.kernel.org/git/20230320230507.3932018-1-gitster@pobox.com/
+         * https://inbox.sourceware.org/libc-alpha/20230306160321.2942372-1-adhemerval.zanella@linaro.org/T/
+         * https://sourceware.org/bugzilla/show_bug.cgi?id=30200
+         */
+        if (gettimeofday(&now, NULL))
+                rb_sys_fail("gettimeofday");
+
+        if (last == now.tv_sec)
                 return buf;
-        last = now;
-        gmtime_r(&now, &tm);
+        last = now.tv_sec;
+        gmtime_r(&now.tv_sec, &tm);
 
         /* we can make this thread-safe later if our Ruby loses the GVL */
         snprintf(buf_ptr, buf_capa,
diff --git a/ext/unicorn_http/unicorn_http.rl b/ext/unicorn_http/unicorn_http.rl
index ba23438..fb5dcde 100644
--- a/ext/unicorn_http/unicorn_http.rl
+++ b/ext/unicorn_http/unicorn_http.rl
@@ -28,10 +28,15 @@ void init_unicorn_httpdate(void);
 #define UH_FL_TO_CLEAR 0x200
 #define UH_FL_RESSTART 0x400 /* for check_client_connection */
 #define UH_FL_HIJACK 0x800
+#define UH_FL_RES_CHUNK_VER (1U << 12)
+#define UH_FL_RES_CHUNK_METHOD (1U << 13)
 
 /* all of these flags need to be set for keepalive to be supported */
 #define UH_FL_KEEPALIVE (UH_FL_KAVERSION | UH_FL_REQEOF | UH_FL_HASHEADER)
 
+/* we can only chunk responses for non-HEAD HTTP/1.1 requests */
+#define UH_FL_RES_CHUNKABLE (UH_FL_RES_CHUNK_VER | UH_FL_RES_CHUNK_METHOD)
+
 static unsigned int MAX_HEADER_LEN = 1024 * (80 + 32); /* same as Mongrel */
 
 /* this is only intended for use with Rainbows! */
@@ -145,6 +150,9 @@ request_method(struct http_parser *hp, const char *ptr, size_t len)
 {
   VALUE v = rb_str_new(ptr, len);
 
+  if (len != 4 || memcmp(ptr, "HEAD", 4))
+    HP_FL_SET(hp, RES_CHUNK_METHOD);
+
   rb_hash_aset(hp->env, g_request_method, v);
 }
 
@@ -158,6 +166,7 @@ http_version(struct http_parser *hp, const char *ptr, size_t len)
   if (CONST_MEM_EQ("HTTP/1.1", ptr, len)) {
     /* HTTP/1.1 implies keepalive unless "Connection: close" is set */
     HP_FL_SET(hp, KAVERSION);
+    HP_FL_SET(hp, RES_CHUNK_VER);
     v = g_http_11;
   } else if (CONST_MEM_EQ("HTTP/1.0", ptr, len)) {
     v = g_http_10;
@@ -801,6 +810,14 @@ static VALUE HttpParser_keepalive(VALUE self)
   return HP_FL_ALL(hp, KEEPALIVE) ? Qtrue : Qfalse;
 }
 
+/* :nodoc: */
+static VALUE chunkable_response_p(VALUE self)
+{
+  const struct http_parser *hp = data_get(self);
+
+  return HP_FL_ALL(hp, RES_CHUNKABLE) ? Qtrue : Qfalse;
+}
+
 /**
  * call-seq:
  *    parser.next? => true or false
@@ -981,6 +998,7 @@ void Init_unicorn_http(void)
   rb_define_method(cHttpParser, "content_length", HttpParser_content_length, 0);
   rb_define_method(cHttpParser, "body_eof?", HttpParser_body_eof, 0);
   rb_define_method(cHttpParser, "keepalive?", HttpParser_keepalive, 0);
+  rb_define_method(cHttpParser, "chunkable_response?", chunkable_response_p, 0);
   rb_define_method(cHttpParser, "headers?", HttpParser_has_headers, 0);
   rb_define_method(cHttpParser, "next?", HttpParser_next, 0);
   rb_define_method(cHttpParser, "buf", HttpParser_buf, 0);
@@ -1015,9 +1033,6 @@ void Init_unicorn_http(void)
   id_set_backtrace = rb_intern("set_backtrace");
   init_unicorn_httpdate();
 
-#ifndef HAVE_RB_HASH_CLEAR
-  id_clear = rb_intern("clear");
-#endif
   id_is_chunked_p = rb_intern("is_chunked?");
 
   init_epollexclusive(mUnicorn);
diff --git a/ext/unicorn_http/unicorn_http_common.rl b/ext/unicorn_http/unicorn_http_common.rl
index 0988b54..507e570 100644
--- a/ext/unicorn_http/unicorn_http_common.rl
+++ b/ext/unicorn_http/unicorn_http_common.rl
@@ -4,7 +4,7 @@
 
 #### HTTP PROTOCOL GRAMMAR
 # line endings
-  CRLF = ("\r\n" | "\n");
+  CRLF = ("\r\n" | "\n");
 
 # character types
   CTL = (cntrl | 127);