1 # -*- encoding: binary -*-
4 # Soft timeout middleware for thread-based concurrency models in \Rainbows!
5 # This timeout only includes application dispatch, and will not take into
6 # account the (rare) response bodies that are dynamically generated while
7 # they are being written out to the client.
9 # In your rackup config file (config.ru), the following line will
10 # cause execution to timeout in 1.5 seconds.
12 # use Rainbows::ThreadTimeout, :timeout => 1.5
13 # run MyApplication.new
15 # You may also specify a threshold, so the timeout does not take
16 # effect until there are enough active clients. It does not make
17 # sense to set a +:threshold+ higher or equal to the
18 # +worker_connections+ \Rainbows! configuration parameter.
19 # You may specify a negative threshold to be an absolute
20 # value relative to the +worker_connections+ parameter, thus
21 # if you specify a threshold of -1, and have 100 worker_connections,
22 # ThreadTimeout will only activate when there are 99 active requests.
24 # use Rainbows::ThreadTimeout, :timeout => 1.5, :threshold => -1
25 # run MyApplication.new
27 # This middleware only affects elements below it in the stack, so
28 # it can be configured to ignore certain endpoints or middlewares.
30 # Timed-out requests will cause this middleware to return with a
31 # "408 Request Timeout" response.
33 class Rainbows::ThreadTimeout
36 class ExecutionExpired < Exception
39 def initialize(app, opts)
40 @timeout = opts[:timeout]
41 Numeric === @timeout or
42 raise TypeError, "timeout=#{@timeout.inspect} is not numeric"
44 if @threshold = opts[:threshold]
45 Integer === @threshold or
46 raise TypeError, "threshold=#{@threshold.inspect} is not an integer"
48 raise ArgumentError, "threshold=0 does not make sense"
50 @threshold += Rainbows::G.server.worker_connections
59 start_watchdog unless @watchdog
60 @active[Thread.current] = Time.now + @timeout
65 @lock.synchronize { @active.delete(Thread.current) }
67 rescue ExecutionExpired
68 [ 408, { 'Content-Type' => 'text/plain', 'Content-Length' => '0' }, [] ]
72 @watchdog = Thread.new do
74 if next_wake = @lock.synchronize { @active.values }.min
77 # because of the lack of GVL-releasing syscalls in this branch
78 # of the thread loop, we need Thread.pass to ensure other threads
79 # get scheduled appropriately under 1.9. This is likely a threading
80 # bug in 1.9 that warrants further investigation when we're in a
82 next_wake > 0 ? sleep(next_wake) : Thread.pass
87 # "active.size" is atomic in MRI 1.8 and 1.9
88 next if @threshold && @active.size < @threshold
92 @active.delete_if do |thread, time|
93 now >= time and thread.raise(ExecutionExpired).nil?