rainbows 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. data/.document +1 -0
  2. data/COPYING +617 -282
  3. data/Documentation/comparison.haml +81 -24
  4. data/FAQ +3 -0
  5. data/GIT-VERSION-GEN +1 -1
  6. data/LICENSE +14 -5
  7. data/README +10 -9
  8. data/Sandbox +25 -0
  9. data/TODO +2 -22
  10. data/lib/rainbows.rb +50 -49
  11. data/lib/rainbows/client.rb +6 -5
  12. data/lib/rainbows/configurator.rb +191 -37
  13. data/lib/rainbows/const.rb +1 -1
  14. data/lib/rainbows/coolio.rb +4 -1
  15. data/lib/rainbows/coolio/client.rb +2 -2
  16. data/lib/rainbows/coolio/heartbeat.rb +2 -1
  17. data/lib/rainbows/coolio_fiber_spawn.rb +12 -7
  18. data/lib/rainbows/coolio_thread_pool.rb +19 -10
  19. data/lib/rainbows/coolio_thread_spawn.rb +3 -0
  20. data/lib/rainbows/epoll.rb +27 -5
  21. data/lib/rainbows/epoll/client.rb +3 -3
  22. data/lib/rainbows/ev_core.rb +2 -1
  23. data/lib/rainbows/event_machine.rb +4 -0
  24. data/lib/rainbows/event_machine/client.rb +2 -1
  25. data/lib/rainbows/fiber.rb +5 -0
  26. data/lib/rainbows/fiber/base.rb +1 -0
  27. data/lib/rainbows/fiber/coolio/methods.rb +0 -1
  28. data/lib/rainbows/fiber/io.rb +10 -6
  29. data/lib/rainbows/fiber/io/pipe.rb +6 -1
  30. data/lib/rainbows/fiber/io/socket.rb +6 -1
  31. data/lib/rainbows/fiber_pool.rb +12 -7
  32. data/lib/rainbows/fiber_spawn.rb +11 -6
  33. data/lib/rainbows/http_server.rb +55 -59
  34. data/lib/rainbows/join_threads.rb +4 -0
  35. data/lib/rainbows/max_body.rb +29 -10
  36. data/lib/rainbows/never_block.rb +7 -10
  37. data/lib/rainbows/pool_size.rb +14 -0
  38. data/lib/rainbows/process_client.rb +23 -1
  39. data/lib/rainbows/queue_pool.rb +8 -6
  40. data/lib/rainbows/response.rb +12 -11
  41. data/lib/rainbows/revactor.rb +14 -7
  42. data/lib/rainbows/revactor/client.rb +2 -2
  43. data/lib/rainbows/stream_file.rb +11 -4
  44. data/lib/rainbows/thread_pool.rb +12 -28
  45. data/lib/rainbows/thread_spawn.rb +14 -13
  46. data/lib/rainbows/thread_timeout.rb +118 -30
  47. data/lib/rainbows/writer_thread_pool/client.rb +1 -1
  48. data/lib/rainbows/writer_thread_spawn/client.rb +2 -2
  49. data/lib/rainbows/xepoll.rb +13 -5
  50. data/lib/rainbows/xepoll/client.rb +19 -17
  51. data/lib/rainbows/xepoll_thread_pool.rb +82 -0
  52. data/lib/rainbows/xepoll_thread_pool/client.rb +129 -0
  53. data/lib/rainbows/xepoll_thread_spawn.rb +58 -0
  54. data/lib/rainbows/xepoll_thread_spawn/client.rb +121 -0
  55. data/pkg.mk +4 -0
  56. data/rainbows.gemspec +4 -1
  57. data/t/GNUmakefile +5 -1
  58. data/t/client_header_buffer_size.ru +5 -0
  59. data/t/simple-http_XEpollThreadPool.ru +10 -0
  60. data/t/simple-http_XEpollThreadSpawn.ru +10 -0
  61. data/t/t0022-copy_stream-byte-range.sh +1 -15
  62. data/t/t0026-splice-copy_stream-byte-range.sh +25 -0
  63. data/t/t0027-nil-copy_stream.sh +60 -0
  64. data/t/t0041-optional-pool-size.sh +2 -2
  65. data/t/t0042-client_header_buffer_size.sh +65 -0
  66. data/t/t9100-thread-timeout.sh +1 -6
  67. data/t/t9101-thread-timeout-threshold.sh +1 -6
  68. data/t/test-lib.sh +58 -0
  69. data/t/test_isolate.rb +9 -3
  70. metadata +47 -16
@@ -6,8 +6,11 @@
6
6
  # a streaming "rack.input" but is compatible with everything else
7
7
  # EventMachine supports.
8
8
  #
9
+ # === :pool_size vs worker_connections
10
+ #
9
11
  # In your Rainbows! config block, you may specify a Fiber pool size
10
12
  # to limit your application concurrency (without using Rainbows::AppPool)
13
+ # independently of worker_connections.
11
14
  #
12
15
  # Rainbows! do
13
16
  # use :NeverBlock, :pool_size => 50
@@ -15,20 +18,14 @@
15
18
  # end
16
19
  #
17
20
  module Rainbows::NeverBlock
18
-
19
21
  # :stopdoc:
20
- DEFAULTS = {
21
- :pool_size => 20, # same default size used by NB
22
- :backend => :EventMachine, # NeverBlock doesn't support Rev yet
23
- }
22
+ extend Rainbows::PoolSize
24
23
 
25
24
  # same pool size NB core itself uses
26
25
  def self.setup # :nodoc:
27
- o = Rainbows::O
28
- DEFAULTS.each { |k,v| o[k] ||= v }
29
- Integer === o[:pool_size] && o[:pool_size] > 0 or
30
- raise ArgumentError, "pool_size must a be an Integer > 0"
31
- mod = Rainbows.const_get(o[:backend])
26
+ super
27
+ Rainbows::O[:backend] ||= :EventMachine # no Cool.io support, yet
28
+ Rainbows.const_get(Rainbows::O[:backend])
32
29
  require "never_block" # require EM first since we need a higher version
33
30
  end
34
31
 
@@ -0,0 +1,14 @@
1
+ # -*- encoding: binary -*-
2
+ # :stopdoc:
3
+ module Rainbows::PoolSize
4
+ DEFAULTS = {
5
+ :pool_size => 50, # same as the default worker_connections
6
+ }
7
+
8
+ def setup
9
+ o = Rainbows::O
10
+ DEFAULTS.each { |k,v| o[k] ||= v }
11
+ Integer === o[:pool_size] && o[:pool_size] > 0 or
12
+ raise ArgumentError, "pool_size must a be an Integer > 0"
13
+ end
14
+ end
@@ -7,10 +7,11 @@ module Rainbows::ProcessClient
7
7
  NULL_IO = Unicorn::HttpRequest::NULL_IO
8
8
  RACK_INPUT = Unicorn::HttpRequest::RACK_INPUT
9
9
  IC = Unicorn::HttpRequest.input_class
10
+ Rainbows.config!(self, :client_header_buffer_size)
10
11
 
11
12
  def process_loop
12
13
  @hp = hp = Rainbows::HttpParser.new
13
- kgio_read!(16384, buf = hp.buf) or return
14
+ kgio_read!(CLIENT_HEADER_BUFFER_SIZE, buf = hp.buf) or return
14
15
 
15
16
  begin # loop
16
17
  until env = hp.parse
@@ -46,4 +47,25 @@ module Rainbows::ProcessClient
46
47
  def set_input(env, hp)
47
48
  env[RACK_INPUT] = 0 == hp.content_length ? NULL_IO : IC.new(self, hp)
48
49
  end
50
+
51
+ def process_pipeline(env, hp)
52
+ begin
53
+ set_input(env, hp)
54
+ env[REMOTE_ADDR] = kgio_addr
55
+ status, headers, body = APP.call(env.merge!(RACK_DEFAULTS))
56
+ if 100 == status.to_i
57
+ write(EXPECT_100_RESPONSE)
58
+ env.delete(HTTP_EXPECT)
59
+ status, headers, body = APP.call(env)
60
+ end
61
+ write_response(status, headers, body, alive = hp.next?)
62
+ end while alive && pipeline_ready(hp)
63
+ alive or close
64
+ rescue => e
65
+ handle_error(e)
66
+ end
67
+
68
+ # override this in subclass/module
69
+ def pipeline_ready(hp)
70
+ end
49
71
  end
@@ -5,24 +5,26 @@ require 'thread'
5
5
  # Thread pool class based on pulling off a single Ruby Queue.
6
6
  # This is NOT used for the ThreadPool class, since that class does not
7
7
  # need a userspace Queue.
8
- class Rainbows::QueuePool < Struct.new(:queue, :threads)
8
+ class Rainbows::QueuePool
9
+ attr_reader :queue
10
+
9
11
  def initialize(size = 20)
10
12
  q = Queue.new
11
- self.threads = (1..size).map do
13
+ @threads = (1..size).map do
12
14
  Thread.new do
13
15
  while job = q.shift
14
16
  yield job
15
17
  end
16
18
  end
17
19
  end
18
- self.queue = q
20
+ @queue = q
19
21
  end
20
22
 
21
23
  def quit!
22
- threads.each { |_| queue << nil }
23
- threads.delete_if do |t|
24
+ @threads.each { |_| @queue << nil }
25
+ @threads.delete_if do |t|
24
26
  Rainbows.tick
25
27
  t.alive? ? t.join(0.01) : true
26
- end until threads.empty?
28
+ end until @threads.empty?
27
29
  end
28
30
  end
@@ -6,6 +6,7 @@ module Rainbows::Response
6
6
  KeepAlive = "keep-alive"
7
7
  Content_Length = "Content-Length".freeze
8
8
  Transfer_Encoding = "Transfer-Encoding".freeze
9
+ Rainbows.config!(self, :copy_stream)
9
10
 
10
11
  # private file class for IO objects opened by Rainbows! itself (and not
11
12
  # the app or middleware)
@@ -14,7 +15,7 @@ module Rainbows::Response
14
15
  # called after forking
15
16
  def self.setup(klass)
16
17
  Kgio.accept_class = Rainbows::Client
17
- 0 == Rainbows.keepalive_timeout and
18
+ 0 == Rainbows.server.keepalive_timeout and
18
19
  Rainbows::HttpParser.keepalive_requests = 0
19
20
  end
20
21
 
@@ -67,7 +68,7 @@ module Rainbows::Response
67
68
  end
68
69
 
69
70
  # generic response writer, used for most dynamically-generated responses
70
- # and also when IO.copy_stream and/or IO#trysendfile is unavailable
71
+ # and also when copy_stream and/or IO#trysendfile is unavailable
71
72
  def write_response(status, headers, body, alive)
72
73
  write_headers(status, headers, alive)
73
74
  write_body_each(body)
@@ -89,29 +90,29 @@ module Rainbows::Response
89
90
  include Sendfile
90
91
  end
91
92
 
92
- if IO.respond_to?(:copy_stream)
93
+ if COPY_STREAM
93
94
  unless IO.method_defined?(:trysendfile)
94
95
  module CopyStream
95
96
  def write_body_file(body, range)
96
- range ? IO.copy_stream(body, self, range[1], range[0]) :
97
- IO.copy_stream(body, self, nil, 0)
97
+ range ? COPY_STREAM.copy_stream(body, self, range[1], range[0]) :
98
+ COPY_STREAM.copy_stream(body, self, nil, 0)
98
99
  end
99
100
  end
100
101
  include CopyStream
101
102
  end
102
103
 
103
- # write_body_stream is an alias for write_body_each if IO.copy_stream
104
+ # write_body_stream is an alias for write_body_each if copy_stream
104
105
  # isn't used or available.
105
106
  def write_body_stream(body)
106
- IO.copy_stream(io = body_to_io(body), self)
107
+ COPY_STREAM.copy_stream(io = body_to_io(body), self)
107
108
  ensure
108
109
  close_if_private(io)
109
110
  end
110
- else # ! IO.respond_to?(:copy_stream)
111
+ else # ! COPY_STREAM
111
112
  alias write_body_stream write_body_each
112
- end # ! IO.respond_to?(:copy_stream)
113
+ end # ! COPY_STREAM
113
114
 
114
- if IO.method_defined?(:trysendfile) || IO.respond_to?(:copy_stream)
115
+ if IO.method_defined?(:trysendfile) || COPY_STREAM
115
116
  HTTP_RANGE = 'HTTP_RANGE'
116
117
  Content_Range = 'Content-Range'.freeze
117
118
 
@@ -181,5 +182,5 @@ module Rainbows::Response
181
182
  end
182
183
  end
183
184
  include ToPath
184
- end # IO.respond_to?(:copy_stream) || IO.method_defined?(:trysendfile)
185
+ end # COPY_STREAM || IO.method_defined?(:trysendfile)
185
186
  end
@@ -3,10 +3,14 @@ require 'revactor'
3
3
  require 'fcntl'
4
4
  Revactor::VERSION >= '0.1.5' or abort 'revactor 0.1.5 is required'
5
5
 
6
- # Enables use of the Actor model through
7
- # {Revactor}[http://revactor.org] under Ruby 1.9. It spawns one
8
- # long-lived Actor for every listen socket in the process and spawns a
9
- # new Actor for every client connection accept()-ed.
6
+ # Enables use of the Actor model through {Revactor}[http://revactor.org]
7
+ # under Ruby 1.9.
8
+ #
9
+ # \Revactor dormant upstream, so the use of this is NOT recommended for
10
+ # new applications.
11
+ #
12
+ # It spawns one long-lived Actor for every listen socket in the process
13
+ # and spawns a new Actor for every client connection accept()-ed.
10
14
  # +worker_connections+ will limit the number of client Actors we have
11
15
  # running at any one time.
12
16
  #
@@ -18,6 +22,9 @@ Revactor::VERSION >= '0.1.5' or abort 'revactor 0.1.5 is required'
18
22
  # in the application using this model should be implemented using the
19
23
  # \Revactor library as well, to take advantage of the networking
20
24
  # concurrency features this model provides.
25
+ #
26
+ # === RubyGem Requirements
27
+ # * revactor 0.1.5 or later
21
28
  module Rainbows::Revactor
22
29
  autoload :Client, 'rainbows/revactor/client'
23
30
  autoload :Proxy, 'rainbows/revactor/proxy'
@@ -34,8 +41,8 @@ module Rainbows::Revactor
34
41
  limit = worker_connections
35
42
  actor_exit = Case[:exit, Actor, Object]
36
43
 
37
- revactorize_listeners.each do |l, close, accept|
38
- Actor.spawn(l, close, accept) do |l, close, accept|
44
+ revactorize_listeners.each do |l,close,accept|
45
+ Actor.spawn do
39
46
  Actor.current.trap_exit = true
40
47
  l.controller = l.instance_variable_set(:@receiver, Actor.current)
41
48
  begin
@@ -73,7 +80,7 @@ module Rainbows::Revactor
73
80
  # ignore, let another worker process take it
74
81
  end
75
82
 
76
- def revactorize_listeners
83
+ def revactorize_listeners #:nodoc:
77
84
  LISTENERS.map do |s|
78
85
  case s
79
86
  when TCPServer
@@ -4,8 +4,8 @@ require 'fcntl'
4
4
  class Rainbows::Revactor::Client
5
5
  autoload :TeeSocket, 'rainbows/revactor/client/tee_socket'
6
6
  RD_ARGS = {}
7
- Rainbows.keepalive_timeout > 0 and
8
- RD_ARGS[:timeout] = Rainbows.keepalive_timeout
7
+ Rainbows.server.keepalive_timeout > 0 and
8
+ RD_ARGS[:timeout] = Rainbows.server.keepalive_timeout
9
9
  attr_reader :kgio_addr
10
10
 
11
11
  def initialize(client)
@@ -5,10 +5,17 @@
5
5
  # models. We always maintain our own file offsets in userspace because
6
6
  # because sendfile() implementations offer pread()-like idempotency for
7
7
  # concurrency (multiple clients can read the same underlying file handle).
8
- class Rainbows::StreamFile < Struct.new(:offset, :count, :to_io, :body)
8
+ class Rainbows::StreamFile
9
+ attr_reader :to_io
10
+ attr_accessor :offset, :count
11
+
12
+ def initialize(offset, count, io, body)
13
+ @offset, @count, @to_io, @body = offset, count, io, body
14
+ end
15
+
9
16
  def close
10
- body.close if body.respond_to?(:close)
11
- to_io.close unless to_io.closed?
12
- self.to_io = nil
17
+ @body.close if @body.respond_to?(:close)
18
+ @to_io.close unless @to_io.closed?
19
+ @to_io = nil
13
20
  end
14
21
  end
@@ -1,24 +1,21 @@
1
1
  # -*- encoding: binary -*-
2
2
 
3
3
  # Implements a worker thread pool model. This is suited for platforms
4
- # like Ruby 1.9, where the cost of dynamically spawning a new thread
5
- # for every new client connection is higher than with the ThreadSpawn
6
- # model.
4
+ # like Ruby 1.9, where the cost of dynamically spawning a new thread for
5
+ # every new client connection is higher than with the ThreadSpawn model,
6
+ # but the cost of an idle thread is low (e.g. NPTL under Linux).
7
7
  #
8
- # This model should provide a high level of compatibility with all
9
- # Ruby implementations, and most libraries and applications.
10
- # Applications running under this model should be thread-safe
11
- # but not necessarily reentrant.
8
+ # This model should provide a high level of compatibility with all Ruby
9
+ # implementations, and most libraries and applications. Applications
10
+ # running under this model should be thread-safe but not necessarily
11
+ # reentrant.
12
12
  #
13
- # Applications using this model are required to be thread-safe.
14
- # Threads are never spawned dynamically under this model. If you're
15
- # connecting to external services and need to perform DNS lookups,
16
- # consider using the "resolv-replace" library which replaces parts of
17
- # the core Socket package with concurrent DNS lookup capabilities.
13
+ # Applications using this model are required to be thread-safe. Threads
14
+ # are never spawned dynamically under this model.
18
15
  #
19
- # This model probably less suited for many slow clients than the
20
- # others and thus a lower +worker_connections+ setting is recommended.
21
-
16
+ # If you're using green threads (MRI 1.8) and need to perform DNS lookups,
17
+ # consider using the "resolv-replace" library which replaces parts of the
18
+ # core Socket package with concurrent DNS lookup capabilities.
22
19
  module Rainbows::ThreadPool
23
20
  include Rainbows::Base
24
21
 
@@ -62,17 +59,4 @@ module Rainbows::ThreadPool
62
59
  Rainbows::Error.listen_loop(e)
63
60
  end while Rainbows.alive
64
61
  end
65
-
66
- def join_threads(threads) # :nodoc:
67
- Rainbows.quit!
68
- threads.delete_if do |thr|
69
- Rainbows.tick
70
- begin
71
- thr.run
72
- thr.join(0.01)
73
- rescue
74
- true
75
- end
76
- end until threads.empty?
77
- end
78
62
  end
@@ -2,18 +2,19 @@
2
2
  require 'thread'
3
3
 
4
4
  # Spawns a new thread for every client connection we accept(). This
5
- # model is recommended for platforms like Ruby 1.8 where spawning new
6
- # threads is inexpensive.
5
+ # model is recommended for platforms like Ruby (MRI) 1.8 where spawning
6
+ # new threads is inexpensive, but still seems to work well enough with
7
+ # good native threading implementations such as NPTL under Linux on
8
+ # Ruby (MRI/YARV) 1.9
7
9
  #
8
- # This model should provide a high level of compatibility with all
9
- # Ruby implementations, and most libraries and applications.
10
- # Applications running under this model should be thread-safe
11
- # but not necessarily reentrant.
10
+ # This model should provide a high level of compatibility with all Ruby
11
+ # implementations, and most libraries and applications. Applications
12
+ # running under this model should be thread-safe but not necessarily
13
+ # reentrant.
12
14
  #
13
- # If you're connecting to external services and need to perform DNS
14
- # lookups, consider using the "resolv-replace" library which replaces
15
- # parts of the core Socket package with concurrent DNS lookup
16
- # capabilities
15
+ # If you're using green threads (MRI 1.8) and need to perform DNS lookups,
16
+ # consider using the "resolv-replace" library which replaces parts of the
17
+ # core Socket package with concurrent DNS lookup capabilities.
17
18
 
18
19
  module Rainbows::ThreadSpawn
19
20
  include Rainbows::Base
@@ -24,12 +25,12 @@ module Rainbows::ThreadSpawn
24
25
  limit = worker_connections
25
26
  nr = 0
26
27
  LISTENERS.each do |l|
27
- klass.new(l) do |l|
28
+ klass.new do
28
29
  begin
29
30
  if lock.synchronize { nr >= limit }
30
31
  worker_yield
31
- elsif c = l.kgio_accept
32
- klass.new(c) do |c|
32
+ elsif client = l.kgio_accept
33
+ klass.new(client) do |c|
33
34
  begin
34
35
  lock.synchronize { nr += 1 }
35
36
  c.process_loop
@@ -29,14 +29,35 @@ require 'thread'
29
29
  #
30
30
  # Timed-out requests will cause this middleware to return with a
31
31
  # "408 Request Timeout" response.
32
-
32
+ #
33
+ # == Caveats
34
+ #
35
+ # Badly-written C extensions may not be timed out. Audit and fix
36
+ # (or remove) those extensions before relying on this module.
37
+ #
38
+ # Do NOT, under any circumstances nest and load this in
39
+ # the same middleware stack. You may load this in parallel in the
40
+ # same process completely independent middleware stacks, but DO NOT
41
+ # load this twice so it nests. Things will break!
42
+ #
43
+ # This will behave badly if system time is changed since Ruby
44
+ # does not expose a monotonic clock for users, so don't change
45
+ # the system time while this is running. All servers should be
46
+ # running ntpd anyways.
33
47
  class Rainbows::ThreadTimeout
34
48
 
35
49
  # :stopdoc:
36
- class ExecutionExpired < Exception
37
- end
50
+ #
51
+ # we subclass Exception to get rid of normal StandardError rescues
52
+ # in app-level code. timeout.rb does something similar
53
+ ExecutionExpired = Class.new(Exception)
54
+
55
+ # The MRI 1.8 won't be usable in January 2038, we'll raise this
56
+ # when we eventually drop support for 1.8 (before 2038, hopefully)
57
+ NEVER = Time.at(0x7fffffff)
38
58
 
39
59
  def initialize(app, opts)
60
+ # @timeout must be Numeric since we add this to Time
40
61
  @timeout = opts[:timeout]
41
62
  Numeric === @timeout or
42
63
  raise TypeError, "timeout=#{@timeout.inspect} is not numeric"
@@ -44,56 +65,123 @@ class Rainbows::ThreadTimeout
44
65
  if @threshold = opts[:threshold]
45
66
  Integer === @threshold or
46
67
  raise TypeError, "threshold=#{@threshold.inspect} is not an integer"
47
- @threshold == 0 and
48
- raise ArgumentError, "threshold=0 does not make sense"
49
- @threshold < 0 and
50
- @threshold += Rainbows.server.worker_connections
68
+ @threshold == 0 and raise ArgumentError, "threshold=0 does not make sense"
69
+ @threshold < 0 and @threshold += Rainbows.server.worker_connections
51
70
  end
52
71
  @app = app
72
+
73
+ # This is the main datastructure for communicating Threads eligible
74
+ # for expiration to the watchdog thread. If the eligible thread
75
+ # completes its job before its expiration time, it will delete itself
76
+ # @active. If the watchdog thread notices the thread is timed out,
77
+ # the watchdog thread will delete the thread from this hash as it
78
+ # raises the exception.
79
+ #
80
+ # key: Thread to be timed out
81
+ # value: Time of expiration
53
82
  @active = {}
83
+
84
+ # Protects all access to @active. It is important since it also limits
85
+ # safe points for asynchronously raising exceptions.
54
86
  @lock = Mutex.new
87
+
88
+ # There is one long-running watchdog thread that watches @active and
89
+ # kills threads that have been running too long
90
+ # see start_watchdog
91
+ @watchdog = nil
55
92
  end
56
93
 
94
+ # entry point for Rack middleware
57
95
  def call(env)
58
- @lock.synchronize do
59
- start_watchdog unless @watchdog
60
- @active[Thread.current] = Time.now + @timeout
61
- end
96
+ # Once we have this lock, we ensure two things:
97
+ # 1) there is only one watchdog thread started
98
+ # 2) we can't be killed once we have this lock, it's unlikely
99
+ # to happen unless @timeout is really low and the machine
100
+ # is very slow.
101
+ @lock.lock
102
+
103
+ # we're dead if anything in the next two lines raises, but it's
104
+ # highly unlikely that they will, and anything such as NoMemoryError
105
+ # is hopeless and we might as well just die anyways.
106
+ # initialize guarantees @timeout will be Numeric
107
+ start_watchdog(env) unless @watchdog
108
+ @active[Thread.current] = Time.now + @timeout
109
+
62
110
  begin
111
+ # It is important to unlock inside this begin block
112
+ # Mutex#unlock really can't fail here since we did a successful
113
+ # Mutex#lock before
114
+ @lock.unlock
115
+
116
+ # Once the Mutex was unlocked, we're open to Thread#raise from
117
+ # the watchdog process. This is the main place we expect to receive
118
+ # Thread#raise. @app is of course the next layer of the Rack
119
+ # application stack
63
120
  @app.call(env)
64
121
  ensure
122
+ # I's still possible to receive a Thread#raise here from
123
+ # the watchdog, but that's alright, the "rescue ExecutionExpired"
124
+ # line will catch that.
65
125
  @lock.synchronize { @active.delete(Thread.current) }
126
+ # Thread#raise no longer possible here
66
127
  end
67
128
  rescue ExecutionExpired
129
+ # If we got here, it's because the watchdog thread raised an exception
130
+ # here to kill us. The watchdog uses @active.delete_if with a lock,
131
+ # so we guaranteed it's
68
132
  [ 408, { 'Content-Type' => 'text/plain', 'Content-Length' => '0' }, [] ]
69
133
  end
70
134
 
71
- def start_watchdog
72
- @watchdog = Thread.new do
135
+ # The watchdog thread is the one that does the job of killing threads
136
+ # that have expired.
137
+ def start_watchdog(env)
138
+ @watchdog = Thread.new(env["rack.logger"]) do |logger|
73
139
  begin
74
- if next_wake = @lock.synchronize { @active.values }.min
75
- next_wake -= Time.now
76
-
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
81
- # better mood.
82
- next_wake > 0 ? sleep(next_wake) : Thread.pass
83
- else
84
- sleep(@timeout)
140
+ if @threshold
141
+ # Hash#size is atomic in MRI 1.8 and 1.9 and we
142
+ # expect that from other implementations.
143
+ #
144
+ # Even without a memory barrier, sleep(@timeout) vs
145
+ # sleep(@timeout - time-for-SMP-to-synchronize-a-word)
146
+ # is too trivial to worry about here.
147
+ sleep(@timeout) while @active.size < @threshold
85
148
  end
86
149
 
87
- # "active.size" is atomic in MRI 1.8 and 1.9
88
- next if @threshold && @active.size < @threshold
150
+ next_expiry = NEVER
89
151
 
90
- now = Time.now
152
+ # We always lock access to @active, so we can't kill threads
153
+ # that are about to release themselves from the eye of the
154
+ # watchdog thread.
91
155
  @lock.synchronize do
92
- @active.delete_if do |thread, time|
93
- now >= time and thread.raise(ExecutionExpired).nil?
156
+ now = Time.now
157
+ @active.delete_if do |thread, expire_at|
158
+ # We also use this loop to get the maximum possible time to
159
+ # sleep for if we're not killing the thread.
160
+ if expire_at > now
161
+ next_expiry = expire_at if next_expiry > expire_at
162
+ false
163
+ else
164
+ # Terminate execution and delete this from the @active
165
+ thread.raise(ExecutionExpired)
166
+ true
167
+ end
94
168
  end
95
169
  end
96
- end while true
170
+
171
+ # We always try to sleep as long as possible to avoid consuming
172
+ # resources from the app. So that's the user-configured @timeout
173
+ # value.
174
+ if next_expiry == NEVER
175
+ sleep(@timeout)
176
+ else
177
+ # sleep until the next known thread is about to expire.
178
+ sec = next_expiry - Time.now
179
+ sec > 0.0 ? sleep(sec) : Thread.pass # give other threads a chance
180
+ end
181
+ rescue => e
182
+ # just in case
183
+ logger.error e
184
+ end while true # we run this forever
97
185
  end
98
186
  end
99
187
  # :startdoc: