From 28851b4f7296cd65149ef81171d1e7db1b0a256b Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 8 Feb 2014 02:42:22 -0500 Subject: queue_quitter_pipe: do not prematurely close reader This will lead to ENOENT when placing writer into kqueue changelist via kevent(2) --- lib/yahns/queue_quitter_pipe.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/yahns/queue_quitter_pipe.rb b/lib/yahns/queue_quitter_pipe.rb index 1aa0643..e18e249 100644 --- a/lib/yahns/queue_quitter_pipe.rb +++ b/lib/yahns/queue_quitter_pipe.rb @@ -5,9 +5,8 @@ class Yahns::QueueQuitter # :nodoc: attr_reader :to_io def initialize - reader, @to_io = IO.pipe + @reader, @to_io = IO.pipe @to_io.close_on_exec = true - reader.close end def yahns_step @@ -19,6 +18,7 @@ class Yahns::QueueQuitter # :nodoc: end def close + @reader.close @to_io.close end end -- cgit v1.2.3-24-ge0c7 From d084e6caa23b585a62065b9c89503deb69be72e3 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 8 Feb 2014 03:07:37 -0500 Subject: sigevent_pipe: kgio_wait_writable takes variadic args We may pass a timeout to kgio_wait_writable during shutdown. --- lib/yahns/sigevent_pipe.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/yahns/sigevent_pipe.rb b/lib/yahns/sigevent_pipe.rb index 8bd083f..ee0f14e 100644 --- a/lib/yahns/sigevent_pipe.rb +++ b/lib/yahns/sigevent_pipe.rb @@ -8,8 +8,8 @@ class Yahns::Sigevent # :nodoc: @to_io.close_on_exec = @wr.close_on_exec = true end - def kgio_wait_readable - @to_io.kgio_wait_readable + def kgio_wait_readable(*args) + @to_io.kgio_wait_readable(*args) end def sev_signal -- cgit v1.2.3-24-ge0c7 From dda19354de0bc72c1ab7ea49537a0ae2b019e837 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 8 Feb 2014 03:08:27 -0500 Subject: wbuf_common: avoid trysendfile on empty sf_count We may unnecessarily drop persistent connections from this bug, and we had an incorrect assertion in our unit test, even. --- lib/yahns/wbuf_common.rb | 3 ++- test/test_wbuf.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/yahns/wbuf_common.rb b/lib/yahns/wbuf_common.rb index b3a4502..a3e737b 100644 --- a/lib/yahns/wbuf_common.rb +++ b/lib/yahns/wbuf_common.rb @@ -25,7 +25,8 @@ module Yahns::WbufCommon # :nodoc: raise "BUG: rv=#{rv.inspect} " \ "on tmpio=#{@tmpio.inspect} " \ "sf_offset=#@sf_offset sf_count=#@sf_count" - end while true + end while @sf_count > 0 + wbuf_close(client) end def wbuf_close_common(client) diff --git a/test/test_wbuf.rb b/test/test_wbuf.rb index 25cdeba..644c76b 100644 --- a/test/test_wbuf.rb +++ b/test/test_wbuf.rb @@ -81,8 +81,8 @@ class TestWbuf < Testcase assert_equal b, IO.select([b], nil, nil, 5)[0][0] b.read(nr - 2) if nr > 2 assert_equal b, IO.select([b], nil, nil, 5)[0][0] - assert_equal "HI", b.read(2) - assert_equal false, wbuf.wbuf_flush(a) + assert_equal "HI", b.read(2), "read the end of the response" + assert_equal true, wbuf.wbuf_flush(a) ensure a.close b.close -- cgit v1.2.3-24-ge0c7 From e86802a1f3f7ac34d32483f9d5de7e627e984db7 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 8 Feb 2014 03:18:26 -0500 Subject: test_client_expire: add delays for non-Linux OSes Not all TCP stacks behave the same with timing-sensitive tests. Fortunately these timing differences aren't very critical over real networks. --- test/test_client_expire.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/test_client_expire.rb b/test/test_client_expire.rb index bdf0cb4..4f20803 100644 --- a/test/test_client_expire.rb +++ b/test/test_client_expire.rb @@ -149,9 +149,17 @@ class TestClientExpire < Testcase Process.waitpid2(_pid) end end + + # this seems to be needed in Debian GNU/kFreeBSD + linux = !!(RUBY_PLATFORM =~ /linux/) + sleep(1) unless linux + [ f, s ].each do |io| assert_raises(Errno::EPIPE,Errno::ECONNRESET) do - req.each_byte { |b| io.write(b.chr) } + req.each_byte do |b| + io.write(b.chr) + sleep(0.01) unless linux + end end io.close end -- cgit v1.2.3-24-ge0c7 From 2f7660ed8124bef150c48ce14f9df759e467e44c Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 8 Feb 2014 03:20:56 -0500 Subject: test_server: check_client_connection uses Unix sockets TCP socket timing is too varied over different OSes, Unix sockets seem to test more reliably than TCP ones on my Debian GNU/kFreeBSD system. --- test/test_server.rb | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/test/test_server.rb b/test/test_server.rb index f4c66ae..96fc704 100644 --- a/test/test_server.rb +++ b/test/test_server.rb @@ -178,11 +178,14 @@ class TestServer < Testcase end def test_check_client_connection + tmpdir = Dir.mktmpdir + sock = "#{tmpdir}/sock" + unix_srv = UNIXServer.new(sock) + unix_srv.close_on_exec = true msgs = %w(ZZ zz) err = @err cfg = Yahns::Config.new bpipe = cloexec_pipe - host, port = @srv.addr[3], @srv.addr[1] cfg.instance_eval do ru = lambda { |e| case e['PATH_INFO'] @@ -205,7 +208,7 @@ class TestServer < Testcase } GTL.synchronize { app(:rack, ru) { - listen "#{host}:#{port}" + listen sock check_client_connection true # needed to avoid concurrency with check_client_connection queue { worker_threads 1 } @@ -223,12 +226,13 @@ class TestServer < Testcase pid = fork do bpipe[1].close - ENV["YAHNS_FD"] = @srv.fileno.to_s + ENV["YAHNS_FD"] = unix_srv.fileno.to_s srv.start.join end bpipe[0].close - a = get_tcp_client(host, port) - b = get_tcp_client(host, port) + a = UNIXSocket.new(sock) + b = UNIXSocket.new(sock) + b.close_on_exec = a.close_on_exec = true a.write("GET /sleep HTTP/1.0\r\n\r\n") r = IO.select([a], nil, nil, 4) assert r, "nothing ready" @@ -250,10 +254,19 @@ class TestServer < Testcase assert_equal msgs.join, buf.split(/\r\n\r\n/)[1] # do things still work? - run_client(host, port) { |res| assert_equal "HI", res.body } + c = UNIXSocket.new(sock) + c.write "GET /\r\n\r\n" + assert_equal "HI", c.read + c.close a.close + rescue => e + warn e.class + warn e.message + warn e.backtrace.join("\n") ensure + unix_srv.close quit_wait(pid) + FileUtils.rm_rf(tmpdir) end def test_mp -- cgit v1.2.3-24-ge0c7 From 922e7141e30fc0ef5fc973716fab20a272531300 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 8 Feb 2014 03:28:49 -0500 Subject: test: log skipped tests on non-Linux systems --- test/test_buffer_tmpdir.rb | 10 ++++++++-- test/test_mt_accept.rb | 4 +++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/test/test_buffer_tmpdir.rb b/test/test_buffer_tmpdir.rb index c7665f6..8461346 100644 --- a/test/test_buffer_tmpdir.rb +++ b/test/test_buffer_tmpdir.rb @@ -9,12 +9,18 @@ class TestBufferTmpdir < Testcase attr_reader :ino, :tmpdir def setup - @ino = SleepyPenguin::Inotify.new(:CLOEXEC) + @ino = nil + begin + @ino = SleepyPenguin::Inotify.new(:CLOEXEC) + rescue + skip "test needs inotify" + end @tmpdir = Dir.mktmpdir server_helper_setup end def teardown + return unless @ino server_helper_teardown @ino.close FileUtils.rm_rf @tmpdir @@ -100,4 +106,4 @@ class TestBufferTmpdir < Testcase c.close if c quit_wait(pid) end -end if SleepyPenguin.const_defined?(:Inotify) +end diff --git a/test/test_mt_accept.rb b/test/test_mt_accept.rb index e006af8..37813d4 100644 --- a/test/test_mt_accept.rb +++ b/test/test_mt_accept.rb @@ -10,6 +10,8 @@ class TestMtAccept < Testcase alias teardown server_helper_teardown def test_mt_accept + skip "Linux kernel required" unless RUBY_PLATFORM =~ /linux/ + skip "/proc not mounted" unless File.directory?("/proc") err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1] opts = { threads: 1 } cfg.instance_eval do @@ -45,4 +47,4 @@ class TestMtAccept < Testcase ensure quit_wait(pid) end -end if RUBY_PLATFORM =~ /linux/ && File.directory?("/proc") +end -- cgit v1.2.3-24-ge0c7 From f46aea4aa26d3c3c5613d6800b2832cddb14b754 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 8 Feb 2014 03:30:28 -0500 Subject: implement kqueue and sendfile compatibility support Tested on Debian GNU/kFreeBSD (sid): make check SENDFILE_BROKEN=1 --- INSTALL | 7 ++++ lib/yahns.rb | 2 +- lib/yahns/queue_kqueue.rb | 82 ++++++++++++++++++++++++++++++++++++++++++++ lib/yahns/sendfile_compat.rb | 29 ++++++++++++++++ lib/yahns/wbuf_common.rb | 7 +++- 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 lib/yahns/queue_kqueue.rb create mode 100644 lib/yahns/sendfile_compat.rb diff --git a/INSTALL b/INSTALL index bd269f2..87acc53 100644 --- a/INSTALL +++ b/INSTALL @@ -6,3 +6,10 @@ No tarballs are currently provided. You may also install yahns from the git source, see the HACKING document for more details. + +Debian GNU/kFreeBSD: + +For now, you will need to define SENDFILE_BROKEN=1 in the env before +running yahns, and also install the "io-extra" RubyGem (tested on 1.2.7) + + gem install -v 1.2.7 io-extra diff --git a/lib/yahns.rb b/lib/yahns.rb index 5ffb845..5e5875a 100644 --- a/lib/yahns.rb +++ b/lib/yahns.rb @@ -57,7 +57,7 @@ end # FIXME: require lazily require_relative 'yahns/log' -require_relative 'yahns/queue_epoll' +require_relative 'yahns/queue' require_relative 'yahns/stream_input' require_relative 'yahns/tee_input' require_relative 'yahns/queue_egg' diff --git a/lib/yahns/queue_kqueue.rb b/lib/yahns/queue_kqueue.rb new file mode 100644 index 0000000..c502de0 --- /dev/null +++ b/lib/yahns/queue_kqueue.rb @@ -0,0 +1,82 @@ +# -*- encoding: binary -*- +# Copyright (C) 2014, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +# +# This is the dangerous, low-level kqueue interface for sleepy_penguin +# It is safe as long as you're aware of all potential concurrency +# issues given multithreading, GC, and kqueue itself. +class Yahns::Queue < SleepyPenguin::Kqueue::IO # :nodoc: + include SleepyPenguin + attr_accessor :fdmap # Yahns::Fdmap + + # public + QEV_QUIT = nil # Level Trigger for QueueQuitter + QEV_RD = EvFilt::READ + QEV_WR = EvFilt::WRITE + + ADD_ONESHOT = Ev::ADD | Ev::ONESHOT # private + + def self.new + rv = super + rv.close_on_exec = true + rv + end + + # for HTTP and HTTPS servers, we rely on the io writing to us, first + # flags: QEV_RD/QEV_WR (usually QEV_RD) + def queue_add(io, flags) + # order is very important here, this thread cannot do anything with + # io once we've issued epoll_ctl() because another thread may use it + @fdmap.add(io) + fflags = ADD_ONESHOT + if flags == QEV_QUIT + fflags = Ev::ADD + flags = QEV_WR + end + kevent(Kevent[io.fileno, flags, fflags, 0, 0, io]) + end + + def thr_init + Thread.current[:yahns_rbuf] = "" + Thread.current[:yahns_fdmap] = @fdmap + end + + def queue_del(io) + # do not bother with kevent EV_DELETE, it may be tricky to get right, + # we only did it in epoll since Eric knows the epoll internals well. + @fdmap.forget(io) + end + + # returns an array of infinitely running threads + def worker_thread(logger, max_events) + Thread.new do + thr_init + begin + kevent(nil, max_events) do |_,_,_,_,_,io| # don't care for flags for now + # Note: we absolutely must not do anything with io after + # we've called epoll_ctl on it, io is exclusive to this + # thread only until epoll_ctl is called on it. + case rv = io.yahns_step + when :wait_readable + kevent(Kevent[io.fileno, QEV_RD, ADD_ONESHOT, 0, 0, io]) + when :wait_writable + kevent(Kevent[io.fileno, QEV_WR, ADD_ONESHOT, 0, 0, io]) + when :ignore # only used by rack.hijack + # we cannot EV_DELETE after hijacking, the hijacker + # may have already closed it Likewise, io.fileno is not + # expected to work, so we had to erase it from fdmap before hijack + when nil, :close + # this must be the ONLY place where we call IO#close on + # things that got inside the queue + @fdmap.sync_close(io) + else + raise "BUG: #{io.inspect}#yahns_step returned: #{rv.inspect}" + end + end + rescue => e + break if closed? # can still happen due to shutdown_timeout + Yahns::Log.exception(logger, 'queue loop', e) + end while true + end + end +end diff --git a/lib/yahns/sendfile_compat.rb b/lib/yahns/sendfile_compat.rb new file mode 100644 index 0000000..e3f53d1 --- /dev/null +++ b/lib/yahns/sendfile_compat.rb @@ -0,0 +1,29 @@ +# -*- encoding: binary -*- +# Copyright (C) 2009-2014, Eric Wong et. al. +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require 'io/extra' # gem install io-extra + +module Yahns::SendfileCompat + def trysendfile(io, offset, count) + return 0 if count == 0 + count = 0x4000 if count > 0x4000 + str = IO.pread(io.fileno, count, offset) + if count > str.bytesize + raise EOFError, "end of file reached" + end + n = 0 + case rv = kgio_trywrite(str) + when String # partial write, keep trying + n += (str.bytesize - rv.bytesize) + str = rv + when :wait_writable, :wait_readable + return n > 0 ? n : rv + when nil + return n + str.bytesize # yay! + end while true + end +end + +class IO + include Yahns::SendfileCompat +end diff --git a/lib/yahns/wbuf_common.rb b/lib/yahns/wbuf_common.rb index a3e737b..01e20bb 100644 --- a/lib/yahns/wbuf_common.rb +++ b/lib/yahns/wbuf_common.rb @@ -1,7 +1,12 @@ # -*- encoding: binary -*- # Copyright (C) 2009-2013, Eric Wong et. al. # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) -require 'sendfile' +if ENV["SENDFILE_BROKEN"] + require_relative 'sendfile_compat' +else + require 'sendfile' +end + module Yahns::WbufCommon # :nodoc: # returns nil on success, :wait_*able when blocked # currently, we rely on each thread having exclusive access to the -- cgit v1.2.3-24-ge0c7