about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2010-08-18 23:59:21 -0700
committerEric Wong <normalperson@yhbt.net>2010-08-19 00:27:15 -0700
commitbd3ed0b04f826b20cce83f9b77fc13c0eefd3902 (patch)
tree8b1065a10ae2e3e44bfe04edc868946e603adfc7
parentac05e7035e1946b78ce4679548db7680aa01734c (diff)
downloadrainbows-bd3ed0b04f826b20cce83f9b77fc13c0eefd3902.tar.gz
This allows for per-dispatch timeouts similar to (but not exactly)
the way Mongrel (1.1.x) implemented them with threads.
-rw-r--r--lib/rainbows.rb1
-rw-r--r--lib/rainbows/thread_timeout.rb94
-rwxr-xr-xt/t9100-thread-timeout.sh36
-rw-r--r--t/t9100.ru9
-rwxr-xr-xt/t9101-thread-timeout-threshold.sh62
-rw-r--r--t/t9101.ru9
6 files changed, 211 insertions, 0 deletions
diff --git a/lib/rainbows.rb b/lib/rainbows.rb
index 2faf3c8..52e7519 100644
--- a/lib/rainbows.rb
+++ b/lib/rainbows.rb
@@ -137,4 +137,5 @@ module Rainbows
   autoload :ByteSlice, 'rainbows/byte_slice'
   autoload :StreamFile, 'rainbows/stream_file'
   autoload :HttpResponse, 'rainbows/http_response' # deprecated
+  autoload :ThreadTimeout, 'rainbows/thread_timeout'
 end
diff --git a/lib/rainbows/thread_timeout.rb b/lib/rainbows/thread_timeout.rb
new file mode 100644
index 0000000..53c675b
--- /dev/null
+++ b/lib/rainbows/thread_timeout.rb
@@ -0,0 +1,94 @@
+# -*- encoding: binary -*-
+require 'thread'
+
+# Soft timeout middleware for thread-based concurrency models in \Rainbows!
+# This timeout only includes application dispatch, and will not take into
+# account the (rare) response bodies that are dynamically generated while
+# they are being written out to the client.
+#
+# In your rackup config file (config.ru), the following line will
+# cause execution to timeout in 1.5 seconds.
+#
+#    use Rainbows::ThreadTimeout, :timeout => 1.5
+#    run MyApplication.new
+#
+# You may also specify a threshold, so the timeout does not take
+# effect until there are enough active clients.  It does not make
+# sense to set a +:threshold+ higher or equal to the
+# +worker_connections+ \Rainbows! configuration parameter.
+# You may specify a negative threshold to be an absolute
+# value relative to the +worker_connections+ parameter, thus
+# if you specify a threshold of -1, and have 100 worker_connections,
+# ThreadTimeout will only activate when there are 99 active requests.
+#
+#    use Rainbows::ThreadTimeout, :timeout => 1.5, :threshold => -1
+#    run MyApplication.new
+#
+# This middleware only affects elements below it in the stack, so
+# it can be configured to ignore certain endpoints or middlewares.
+#
+# Timed-out requests will cause this middleware to return with a
+# "408 Request Timeout" response.
+
+class Rainbows::ThreadTimeout < Struct.new(:app, :timeout,
+                                           :threshold, :watchdog,
+                                           :active, :lock)
+
+  # :stopdoc:
+  class ExecutionExpired < ::Exception
+  end
+
+  def initialize(app, opts)
+    timeout = opts[:timeout]
+    Numeric === timeout or
+      raise TypeError, "timeout=#{timeout.inspect} is not numeric"
+
+    if threshold = opts[:threshold]
+      Integer === threshold or
+        raise TypeError, "threshold=#{threshold.inspect} is not an integer"
+      threshold == 0 and
+        raise ArgumentError, "threshold=0 does not make sense"
+      threshold < 0 and
+        threshold += Rainbows::G.server.worker_connections
+    end
+    super(app, timeout, threshold, nil, {}, Mutex.new)
+  end
+
+  def call(env)
+    lock.synchronize do
+      start_watchdog unless watchdog
+      active[Thread.current] = Time.now + timeout
+    end
+    begin
+      app.call(env)
+    ensure
+      lock.synchronize { active.delete(Thread.current) }
+    end
+    rescue ExecutionExpired
+      [ 408, { 'Content-Type' => 'text/plain', 'Content-Length' => '0' }, [] ]
+  end
+
+  def start_watchdog
+    self.watchdog = Thread.new do
+      begin
+        if next_wake = lock.synchronize { active.values }.min
+          next_wake -= Time.now
+          sleep(next_wake) if next_wake > 0
+        else
+          sleep(timeout)
+        end
+
+        # "active.size" is atomic in MRI 1.8 and 1.9
+        next if threshold && active.size < threshold
+
+        now = Time.now
+        lock.synchronize do
+          active.delete_if do |thread, time|
+            time >= now and thread.raise(ExecutionExpired).nil?
+          end
+        end
+      end while true
+    end
+  end
+  # :startdoc:
+end
diff --git a/t/t9100-thread-timeout.sh b/t/t9100-thread-timeout.sh
new file mode 100755
index 0000000..ab46e5a
--- /dev/null
+++ b/t/t9100-thread-timeout.sh
@@ -0,0 +1,36 @@
+#!/bin/sh
+. ./test-lib.sh
+case $model in
+ThreadSpawn|ThreadPool|RevThreadSpawn|RevThreadPool) ;;
+*) t_info "$0 is only compatible with Thread*"; exit 0 ;;
+esac
+
+t_plan 5 "ThreadTimeout Rack middleware test for $model"
+
+t_begin "configure and start" && {
+        rtmpfiles curl_err
+        rainbows_setup
+        rainbows -D t9100.ru -c $unicorn_config
+        rainbows_wait_start
+}
+
+t_begin "normal request should not timeout" && {
+        test x"HI" = x"$(curl -sSf http://$listen/ 2>> $curl_err)"
+}
+
+t_begin "sleepy request times out with 408" && {
+        rm -f $ok
+        curl -sSf http://$listen/2 2>> $curl_err || > $ok
+        test -e $ok
+        grep 408 $curl_err
+}
+
+t_begin "kill server" && {
+        kill $rainbows_pid
+}
+
+t_begin "no errors in Rainbows! stderr" && {
+        check_stderr
+}
+
+t_done
diff --git a/t/t9100.ru b/t/t9100.ru
new file mode 100644
index 0000000..ed9e1ee
--- /dev/null
+++ b/t/t9100.ru
@@ -0,0 +1,9 @@
+use Rack::ContentLength
+use Rack::ContentType, 'text/plain'
+use Rainbows::ThreadTimeout, :timeout => 1
+run lambda { |env|
+  if env["PATH_INFO"] =~ %r{/([\d\.]+)\z}
+    Rainbows.sleep($1.to_f)
+  end
+  [ 200, [], [ "HI\n" ] ]
+}
diff --git a/t/t9101-thread-timeout-threshold.sh b/t/t9101-thread-timeout-threshold.sh
new file mode 100755
index 0000000..1979dba
--- /dev/null
+++ b/t/t9101-thread-timeout-threshold.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+. ./test-lib.sh
+case $model in
+ThreadSpawn|ThreadPool|RevThreadSpawn|RevThreadPool) ;;
+*) t_info "$0 is only compatible with Thread*"; exit 0 ;;
+esac
+
+t_plan 6 "ThreadTimeout Rack middleware test for $model"
+
+t_begin "configure and start" && {
+        rtmpfiles curl_err curl_out
+        rainbows_setup $model 10
+        rainbows -D t9101.ru -c $unicorn_config
+        rainbows_wait_start
+}
+
+t_begin "normal request should not timeout" && {
+        test x"HI" = x"$(curl -sSf http://$listen/ 2>> $curl_err)"
+}
+
+t_begin "8 sleepy requests do not time out" && {
+        > $curl_err
+        for i in 1 2 3 4 5 6 7 8
+        do
+                curl --no-buffer -sSf http://$listen/3 \
+                  2>> $curl_err >> $curl_out &
+        done
+        wait
+        test 8 -eq "$(wc -l < $curl_out)"
+        test xHI = x"$(sort < $curl_out | uniq)"
+}
+
+t_begin "9 sleepy requests do time out" && {
+        > $curl_err
+        > $curl_out
+        for i in 1 2 3 4 5 6 7 8 9
+        do
+                rtmpfiles curl_err_$i
+                curl -sSf --no-buffer \
+                  http://$listen/3 2>> ${curl_err}_${i} >> $curl_out &
+        done
+        wait
+        if test -s $curl_out
+        then
+                dbgcat curl_out
+                die "$curl_out should be empty"
+        fi
+        for i in 1 2 3 4 5 6 7 8 9
+        do
+                grep 408 ${curl_err}_${i}
+        done
+}
+
+t_begin "kill server" && {
+        kill $rainbows_pid
+}
+
+t_begin "no errors in Rainbows! stderr" && {
+        check_stderr
+}
+
+t_done
diff --git a/t/t9101.ru b/t/t9101.ru
new file mode 100644
index 0000000..ee20085
--- /dev/null
+++ b/t/t9101.ru
@@ -0,0 +1,9 @@
+use Rack::ContentLength
+use Rack::ContentType, 'text/plain'
+use Rainbows::ThreadTimeout, :timeout => 1, :threshold => -1
+run lambda { |env|
+  if env["PATH_INFO"] =~ %r{/([\d\.]+)\z}
+    Rainbows.sleep($1.to_f)
+  end
+  [ 200, [], [ "HI\n" ] ]
+}