yahns Ruby server user/dev discussion
 help / color / Atom feed
From: Eric Wong <e@80x24.org>
To: yahns-public@yhbt.net
Subject: [RFC] exec_cgi: add timeout parameter
Date: Sat, 5 Jan 2019 20:51:20 +0000
Message-ID: <20190105205120.GA11253@dcvr> (raw)

This may be useful for longer responses which are CPU-intensive,
which makes RLIMIT_CPU inappropriate.
---
 I hate adding new parameters/APIs, and I don't think this API
 is sufficient, since I can imagine different timeouts for:

 1) initial header
 2) each body read
 3) overall response time
 ...

 Anyways, I doubt anybody else cares for this module, so it
 won't be in the next release.  But I've been taking time to
 improve cgit <https://80x24.org/cgit.git> (upstream:
 <https://git.zx2c4.com/cgit> and using ExecCGI to launch it.

 extras/exec_cgi.rb           | 31 ++++++++++++++++++++++---------
 test/helper.rb               |  3 ++-
 test/test_extras_exec_cgi.rb | 24 ++++++++++++++++++++++++
 3 files changed, 48 insertions(+), 10 deletions(-)

diff --git a/extras/exec_cgi.rb b/extras/exec_cgi.rb
index 8a1939d..848975e 100644
--- a/extras/exec_cgi.rb
+++ b/extras/exec_cgi.rb
@@ -23,12 +23,13 @@
 #
 class ExecCgi
   class MyIO
-    attr_writer :my_pid
+    attr_accessor :my_pid
     attr_writer :body_tip
     attr_reader :rd
 
-    def initialize(rd)
+    def initialize(rd, timeout)
       @rd = rd
+      @timeout = timeout
     end
 
     def each
@@ -37,7 +38,10 @@ def each
 
       case tmp = @rd.read_nonblock(8192, buf, exception: false)
       when :wait_readable
-        @rd.wait_readable
+        unless @rd.wait_readable(@timeout[1])
+          Process.kill(@timeout[0], @my_pid)
+          break
+        end
       when nil
         break
       else # String
@@ -93,6 +97,7 @@ def initialize(*args)
     File.executable?(args[0]) or
       raise ArgumentError, "#{args[0]} is not executable"
     @opts = Hash === args[-1] ? args.pop : {}
+    @timeout = @opts.delete(:timeout) || []
   end
 
   # Calls the app
@@ -103,19 +108,27 @@ def call(env)
     env.each { |key,val| cgi_env[key] = val if key =~ /\AHTTP_/ }
 
     rd, wr = IO.pipe
-    io = MyIO.new(rd)
+    io = MyIO.new(rd, @timeout)
     errbody = io
     errbody.my_pid = spawn(cgi_env.merge!(@env), *@args,
                            @opts.merge(out: wr, close_others: true))
     wr.close
 
     begin
-      head = rd.readpartial(8192)
-      until head =~ /\r?\n\r?\n/
-        tmp = rd.readpartial(8192)
-        head << tmp
+      head = ''.b
+      tmp = ''.b
+      case rd.read_nonblock(8192, tmp, exception: false)
+      when :wait_readable
+        unless rd.wait_readable(@timeout[1])
+          Process.kill(@timeout[0], errbody.my_pid)
+        end
+      when nil
         tmp.clear
-      end
+        raise EOFError, 'timed out or EOF reached', []
+        break
+      else
+        head << tmp
+      end until head =~ /\r?\n\r?\n/
       head, body = head.split(/\r?\n\r?\n/, 2)
       io.body_tip = body
 
diff --git a/test/helper.rb b/test/helper.rb
index 550a0f1..edca30f 100644
--- a/test/helper.rb
+++ b/test/helper.rb
@@ -130,7 +130,8 @@ def cloexec_pipe
 
 def require_exec(cmd)
   ENV["PATH"].split(/:/).each do |path|
-    return true if File.executable?("#{path}/#{cmd}")
+    bin = "#{path}/#{cmd}"
+    return bin if File.executable?(bin)
   end
   skip "#{cmd} not found in PATH"
   false
diff --git a/test/test_extras_exec_cgi.rb b/test/test_extras_exec_cgi.rb
index 426409d..f4c022c 100644
--- a/test/test_extras_exec_cgi.rb
+++ b/test/test_extras_exec_cgi.rb
@@ -202,4 +202,28 @@ def test_rlimit_options
   ensure
     quit_wait(pid)
   end
+
+  def test_timeout
+    err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
+    tout = 1
+    opts = { timeout: [:TERM, 0.5 ] }
+    bin = require_exec('sleep')
+    cmd = [ bin, '10', opts ]
+    pid = mkserver(cfg) do
+      require './extras/exec_cgi'
+      cfg.instance_eval do
+        stack = Rack::ContentLength.new(Rack::Chunked.new(ExecCgi.new(*cmd)))
+        app(:rack, stack) { listen "#{host}:#{port}" }
+        stderr_path err.path
+        worker_processes 1
+      end
+    end
+    c = get_tcp_client(host, port)
+    c.write "GET / HTTP/1.0\r\n\r\n"
+    assert_same c, c.wait(tout + 1)
+    assert_match %r{ 500 Internal Server Error\b}, c.readpartial(4096)
+    c.close
+  ensure
+    quit_wait(pid)
+  end
 end
-- 
EW


                 reply index

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publically to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://yhbt.net/yahns/README

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20190105205120.GA11253@dcvr \
    --to=e@80x24.org \
    --cc=yahns-public@yhbt.net \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

yahns Ruby server user/dev discussion

Archives are clonable:
	git clone --mirror https://yhbt.net/yahns-public
	git clone --mirror http://ou63pmih66umazou.onion/yahns-public

Newsgroups are available over NNTP:
	nntp://news.public-inbox.org/inbox.comp.lang.ruby.yahns
	nntp://ou63pmih66umazou.onion/inbox.comp.lang.ruby.yahns

 note: .onion URLs require Tor: https://www.torproject.org/

AGPL code for this site: git clone https://public-inbox.org/ public-inbox