rainbows 0.6.0 → 0.7.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 (55) hide show
  1. data/.document +1 -0
  2. data/Documentation/GNUmakefile +4 -1
  3. data/Documentation/comparison.css +6 -0
  4. data/Documentation/comparison.haml +297 -0
  5. data/GIT-VERSION-GEN +1 -1
  6. data/GNUmakefile +24 -17
  7. data/README +32 -28
  8. data/Summary +7 -0
  9. data/TODO +4 -6
  10. data/bin/rainbows +2 -2
  11. data/lib/rainbows.rb +33 -3
  12. data/lib/rainbows/actor_spawn.rb +29 -0
  13. data/lib/rainbows/app_pool.rb +17 -6
  14. data/lib/rainbows/base.rb +10 -13
  15. data/lib/rainbows/const.rb +1 -1
  16. data/lib/rainbows/dev_fd_response.rb +6 -0
  17. data/lib/rainbows/error.rb +34 -0
  18. data/lib/rainbows/ev_core.rb +3 -12
  19. data/lib/rainbows/event_machine.rb +7 -9
  20. data/lib/rainbows/fiber.rb +15 -0
  21. data/lib/rainbows/fiber/base.rb +112 -0
  22. data/lib/rainbows/fiber/io.rb +65 -0
  23. data/lib/rainbows/fiber/queue.rb +35 -0
  24. data/lib/rainbows/fiber_pool.rb +44 -0
  25. data/lib/rainbows/fiber_spawn.rb +34 -0
  26. data/lib/rainbows/http_server.rb +14 -1
  27. data/lib/rainbows/never_block.rb +69 -0
  28. data/lib/rainbows/rev.rb +7 -0
  29. data/lib/rainbows/rev/client.rb +9 -3
  30. data/lib/rainbows/rev/core.rb +2 -5
  31. data/lib/rainbows/rev/heartbeat.rb +5 -1
  32. data/lib/rainbows/rev_thread_spawn.rb +62 -60
  33. data/lib/rainbows/revactor.rb +22 -23
  34. data/lib/rainbows/thread_pool.rb +28 -26
  35. data/lib/rainbows/thread_spawn.rb +33 -33
  36. data/local.mk.sample +9 -7
  37. data/rainbows.gemspec +8 -2
  38. data/t/GNUmakefile +14 -7
  39. data/t/fork-sleep.ru +10 -0
  40. data/t/simple-http_FiberPool.ru +9 -0
  41. data/t/simple-http_FiberSpawn.ru +9 -0
  42. data/t/simple-http_NeverBlock.ru +11 -0
  43. data/t/sleep.ru +2 -0
  44. data/t/t0000-simple-http.sh +12 -1
  45. data/t/t0001-unix-http.sh +12 -1
  46. data/t/t0009-broken-app.sh +56 -0
  47. data/t/t0009.ru +13 -0
  48. data/t/t0010-keepalive-timeout-effective.sh +42 -0
  49. data/t/t0011-close-on-exec-set.sh +54 -0
  50. data/t/t0300-async_sinatra.sh +1 -1
  51. data/t/t9000-rack-app-pool.sh +1 -1
  52. data/t/t9000.ru +8 -5
  53. data/t/test-lib.sh +14 -4
  54. metadata +33 -5
  55. data/lib/rainbows/ev_thread_core.rb +0 -80
@@ -0,0 +1,65 @@
1
+ # -*- encoding: binary -*-
2
+ module Rainbows
3
+ module Fiber
4
+
5
+ # A partially complete IO wrapper, this exports an IO.select()-able
6
+ # #to_io method and gives users the illusion of a synchronous
7
+ # interface that yields away from the current Fiber whenever
8
+ # the underlying IO object cannot read or write
9
+ class IO < Struct.new(:to_io, :f)
10
+
11
+ # for wrapping output response bodies
12
+ def each(&block)
13
+ begin
14
+ yield readpartial(16384)
15
+ rescue EOFError
16
+ break
17
+ end while true
18
+ self
19
+ end
20
+
21
+ def close
22
+ to_io.close
23
+ end
24
+
25
+ def write(buf)
26
+ begin
27
+ (w = to_io.write_nonblock(buf)) == buf.size and return
28
+ buf = buf[w..-1]
29
+ rescue Errno::EAGAIN
30
+ WR[self] = false
31
+ ::Fiber.yield
32
+ WR.delete(self)
33
+ retry
34
+ end while true
35
+ end
36
+
37
+ # used for reading headers (respecting keepalive_timeout)
38
+ def read_timeout
39
+ expire = nil
40
+ begin
41
+ to_io.read_nonblock(16384)
42
+ rescue Errno::EAGAIN
43
+ return if expire && expire < Time.now
44
+ RD[self] = false
45
+ expire ||= Time.now + G.kato
46
+ ::Fiber.yield
47
+ RD.delete(self)
48
+ retry
49
+ end
50
+ end
51
+
52
+ def readpartial(length, buf = "")
53
+ begin
54
+ to_io.read_nonblock(length, buf)
55
+ rescue Errno::EAGAIN
56
+ RD[self] = false
57
+ ::Fiber.yield
58
+ RD.delete(self)
59
+ retry
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,35 @@
1
+ # -*- encoding: binary -*-
2
+ module Rainbows
3
+ module Fiber
4
+
5
+ # a self-sufficient Queue implementation for Fiber-based concurrency
6
+ # models. This requires no external scheduler, so it may be used with
7
+ # Revactor as well as FiberSpawn and FiberPool.
8
+ class Queue < Struct.new(:queue, :waiters)
9
+
10
+ def initialize(queue = [], waiters = [])
11
+ # move elements of the Queue into an Array
12
+ if queue.class.name == "Queue"
13
+ queue = queue.length.times.map { queue.pop }
14
+ end
15
+ super queue, waiters
16
+ end
17
+
18
+ def shift
19
+ # ah the joys of not having to deal with race conditions
20
+ if queue.empty?
21
+ waiters << ::Fiber.current
22
+ ::Fiber.yield
23
+ end
24
+ queue.shift
25
+ end
26
+
27
+ def <<(obj)
28
+ queue << obj
29
+ blocked = waiters.shift and blocked.resume
30
+ queue # not quite 100% compatible but no-one's looking :>
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,44 @@
1
+ # -*- encoding: binary -*-
2
+ require 'rainbows/fiber'
3
+
4
+ module Rainbows
5
+
6
+ # A Fiber-based concurrency model for Ruby 1.9. This uses a pool of
7
+ # Fibers to handle client IO to run the application and the root Fiber
8
+ # for scheduling and connection acceptance. The pool size is equal to
9
+ # the number of +worker_connections+. Compared to the ThreadPool
10
+ # model, Fibers are very cheap in terms of memory usage so you can
11
+ # have more active connections. This model supports a streaming
12
+ # "rack.input" with lightweight concurrency. Applications are
13
+ # strongly advised to wrap all slow IO objects (sockets, pipes) using
14
+ # the Rainbows::Fiber::IO class whenever possible.
15
+
16
+ module FiberPool
17
+ include Fiber::Base
18
+
19
+ def worker_loop(worker)
20
+ init_worker_process(worker)
21
+ pool = []
22
+ worker_connections.times {
23
+ ::Fiber.new {
24
+ process_client(::Fiber.yield) while pool << ::Fiber.current
25
+ }.resume # resume to hit ::Fiber.yield so it waits on a client
26
+ }
27
+ Fiber::Base.const_set(:APP, app)
28
+
29
+ begin
30
+ schedule do |l|
31
+ fib = pool.shift or break # let another worker process take it
32
+ if io = Rainbows.accept(l)
33
+ fib.resume(Fiber::IO.new(io, fib))
34
+ else
35
+ pool << fib
36
+ end
37
+ end
38
+ rescue => e
39
+ Error.listen_loop(e)
40
+ end while G.alive || G.cur > 0
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,34 @@
1
+ # -*- encoding: binary -*-
2
+ require 'rainbows/fiber'
3
+
4
+ module Rainbows
5
+
6
+ # Simple Fiber-based concurrency model for 1.9. This spawns a new
7
+ # Fiber for every incoming client connection and the root Fiber for
8
+ # scheduling and connection acceptance. This exports a streaming
9
+ # "rack.input" with lightweight concurrency. Applications are
10
+ # strongly advised to wrap all slow IO objects (sockets, pipes) using
11
+ # the Rainbows::Fiber::IO class whenever possible.
12
+
13
+ module FiberSpawn
14
+ include Fiber::Base
15
+
16
+ def worker_loop(worker)
17
+ init_worker_process(worker)
18
+ Fiber::Base.const_set(:APP, app)
19
+ limit = worker_connections
20
+ fio = Rainbows::Fiber::IO
21
+
22
+ begin
23
+ schedule do |l|
24
+ break if G.cur >= limit
25
+ io = Rainbows.accept(l) or next
26
+ ::Fiber.new { process_client(fio.new(io, ::Fiber.current)) }.resume
27
+ end
28
+ rescue => e
29
+ Error.listen_loop(e)
30
+ end while G.alive || G.cur > 0
31
+ end
32
+
33
+ end
34
+ end
@@ -52,10 +52,18 @@ module Rainbows
52
52
  Module === mod or
53
53
  raise ArgumentError, "concurrency model #{model.inspect} not supported"
54
54
  extend(mod)
55
+ args.each do |opt|
56
+ case opt
57
+ when Hash; O.update(opt)
58
+ when Symbol; O[opt] = true
59
+ else; raise ArgumentError, "can't handle option: #{opt.inspect}"
60
+ end
61
+ end
62
+ mod.setup if mod.respond_to?(:setup)
55
63
  Const::RACK_DEFAULTS['rainbows.model'] = @use = model.to_sym
56
64
  Const::RACK_DEFAULTS['rack.multithread'] = !!(/Thread/ =~ model.to_s)
57
65
  case @use
58
- when :Rev, :EventMachine
66
+ when :Rev, :EventMachine, :NeverBlock
59
67
  Const::RACK_DEFAULTS['rainbows.autochunk'] = true
60
68
  end
61
69
  end
@@ -68,6 +76,11 @@ module Rainbows
68
76
  @worker_connections = nr
69
77
  end
70
78
 
79
+ def keepalive_timeout(nr)
80
+ (Integer === nr && nr >= 0) or
81
+ raise ArgumentError, "keepalive must be a non-negative Integer"
82
+ G.kato = nr
83
+ end
71
84
  end
72
85
 
73
86
  end
@@ -0,0 +1,69 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ module Rainbows
4
+
5
+ # {NeverBlock}[www.espace.com.eg/neverblock/] library that combines
6
+ # the EventMachine library with Ruby Fibers. This includes use of
7
+ # Thread-based Fibers under Ruby 1.8. It currently does NOT support
8
+ # a streaming "rack.input" but is compatible with everything else
9
+ # EventMachine supports.
10
+ #
11
+ # In your Rainbows! config block, you may specify a Fiber pool size
12
+ # to limit your application concurrency (without using Rainbows::AppPool)
13
+ #
14
+ # Rainbows! do
15
+ # use :NeverBlock, :pool_size => 50
16
+ # worker_connections 100
17
+ # end
18
+ #
19
+ module NeverBlock
20
+
21
+ DEFAULTS = {
22
+ :pool_size => 20, # same default size used by NB
23
+ :backend => :EventMachine, # NeverBlock doesn't support Rev yet
24
+ }
25
+
26
+ # same pool size NB core itself uses
27
+ def self.setup
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])
32
+ require "never_block" # require EM first since we need a higher version
33
+ G.server.extend(mod)
34
+ G.server.extend(Core)
35
+ end
36
+
37
+ module Client
38
+
39
+ def self.setup
40
+ const_set(:POOL, ::NB::Pool::FiberPool.new(O[:pool_size]))
41
+ Rainbows.const_get(O[:backend]).const_get(:Client).module_eval do
42
+ include Rainbows::NeverBlock::Client
43
+ alias _app_call app_call
44
+ undef_method :app_call
45
+ alias app_call nb_app_call
46
+ end
47
+ end
48
+
49
+ def nb_app_call
50
+ POOL.spawn do
51
+ begin
52
+ _app_call
53
+ rescue => e
54
+ handle_error(e)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ module Core
61
+ def init_worker_process(worker)
62
+ super
63
+ Client.setup
64
+ logger.info "NeverBlock/#{O[:backend]} pool_size=#{O[:pool_size]}"
65
+ end
66
+ end
67
+
68
+ end
69
+ end
data/lib/rainbows/rev.rb CHANGED
@@ -24,6 +24,13 @@ module Rainbows
24
24
  # temporary file before the application is entered.
25
25
 
26
26
  module Rev
27
+
28
+ # keep-alive timeout scoreboard
29
+ KATO = {}
30
+
31
+ # all connected clients
32
+ CONN = {}
33
+
27
34
  include Core
28
35
  end
29
36
  end
@@ -8,7 +8,7 @@ module Rainbows
8
8
  G = Rainbows::G
9
9
 
10
10
  def initialize(io)
11
- G.cur += 1
11
+ CONN[self] = false
12
12
  super(io)
13
13
  post_init
14
14
  @deferred_bodies = [] # for (fast) regular files only
@@ -23,9 +23,14 @@ module Rainbows
23
23
  schedule_write unless out_headers # triggers a write
24
24
  end
25
25
 
26
+ def timeout?
27
+ @_write_buffer.empty? && @deferred_bodies.empty? and close.nil?
28
+ end
29
+
26
30
  def app_call
27
31
  begin
28
- (@env[RACK_INPUT] = @input).rewind
32
+ KATO.delete(self)
33
+ @env[RACK_INPUT] = @input
29
34
  @env[REMOTE_ADDR] = @remote_addr
30
35
  response = APP.call(@env.update(RACK_DEFAULTS))
31
36
  alive = @hp.keepalive? && G.alive
@@ -38,6 +43,7 @@ module Rainbows
38
43
  @state = :headers
39
44
  # keepalive requests are always body-less, so @input is unchanged
40
45
  @hp.headers(@env, @buf) and next
46
+ KATO[self] = Time.now
41
47
  else
42
48
  quit
43
49
  end
@@ -65,7 +71,7 @@ module Rainbows
65
71
  end
66
72
 
67
73
  def on_close
68
- G.cur -= 1
74
+ CONN.delete(self)
69
75
  end
70
76
 
71
77
  end # module Client
@@ -11,11 +11,8 @@ module Rainbows
11
11
  # CL and MAX will be defined in the corresponding worker loop
12
12
 
13
13
  def on_readable
14
- return if G.cur >= MAX
15
- begin
16
- CL.new(@_io.accept_nonblock).attach(LOOP)
17
- rescue Errno::EAGAIN, Errno::ECONNABORTED
18
- end
14
+ return if CONN.size >= MAX
15
+ io = Rainbows.accept(@_io) and CL.new(io).attach(LOOP)
19
16
  end
20
17
  end # class Server
21
18
 
@@ -10,7 +10,11 @@ module Rainbows
10
10
  class Heartbeat < ::Rev::TimerWatcher
11
11
 
12
12
  def on_timer
13
- exit if (! G.tick && G.cur <= 0)
13
+ if (ot = G.kato) > 0
14
+ ot = Time.now - ot
15
+ KATO.delete_if { |client, time| time < ot and client.timeout? }
16
+ end
17
+ exit if (! G.tick && CONN.size <= 0)
14
18
  end
15
19
 
16
20
  end
@@ -1,93 +1,95 @@
1
1
  # -*- encoding: binary -*-
2
2
  require 'rainbows/rev'
3
- require 'rainbows/ev_thread_core'
4
3
 
5
- warn "Rainbows::RevThreadSpawn is extremely experimental"
4
+ RUBY_VERSION =~ %r{\A1\.8} && ::Rev::VERSION < "0.3.2" and
5
+ warn "Rainbows::RevThreadSpawn + Rev (< 0.3.2)" \
6
+ " does not work well under Ruby 1.8"
6
7
 
7
8
  module Rainbows
8
9
 
9
- # This concurrency model is EXTREMELY experimental and does
10
- # not perform very well.
11
- #
12
10
  # A combination of the Rev and ThreadSpawn models. This allows Ruby
13
- # 1.8 and 1.9 to effectively serve more than ~1024 concurrent clients
14
- # on systems that support kqueue or epoll while still using
15
- # Thread-based concurrency for application processing. It exposes
16
- # Unicorn::TeeInput for a streamable "rack.input" for upload
17
- # processing within the app. Threads are spawned immediately after
18
- # header processing is done for calling the application. Rack
19
- # applications running under this mode should be thread-safe.
20
- # DevFdResponse should be used with this class to proxy asynchronous
21
- # responses. All network I/O between the client and server are
22
- # handled by the main thread (even when streaming "rack.input").
23
- #
24
- # Caveats:
11
+ # Thread-based concurrency for application processing. It DOES NOT
12
+ # expose a streamable "rack.input" for upload processing within the
13
+ # app. DevFdResponse should be used with this class to proxy
14
+ # asynchronous responses. All network I/O between the client and
15
+ # server are handled by the main thread and outside of the core
16
+ # application dispatch.
25
17
  #
26
- # * TeeInput performance under Ruby 1.8 is terrible unless you
27
- # match the length argument of your env["rack.input"]#read
28
- # calls so that it is greater than or equal to Rev::IO::INPUT_SIZE.
29
- # Most applications depending on Rack to do multipart POST
30
- # processing should be alright as the current Rev::IO::INPUT_SIZE
31
- # of 16384 bytes matches the read size used by
32
- # Rack::Utils::Multipart::parse_multipart.
18
+ # WARNING: this model does not currently perform well under 1.8. See the
19
+ # {rev-talk mailing list}[http://rubyforge.org/mailman/listinfo/rev-talk]
20
+ # for ongoing performance work that will hopefully make it into the
21
+ # next release of {Rev}[http://rev.rubyforge.org/].
33
22
 
34
23
  module RevThreadSpawn
35
- class Client < Rainbows::Rev::Client
36
- include EvThreadCore
37
- LOOP = ::Rev::Loop.default
38
- DR = Rainbows::Rev::DeferredResponse
39
- TEE_RESUMER = ::Rev::AsyncWatcher.new
40
24
 
41
- def pause
42
- @lock.synchronize { disable if enabled? }
43
- end
25
+ class Master < ::Rev::AsyncWatcher
44
26
 
45
- def resume
46
- @lock.synchronize { enable unless enabled? }
47
- TEE_RESUMER.signal
27
+ def initialize
28
+ super
29
+ @queue = Queue.new
48
30
  end
49
31
 
50
- def write(data)
51
- if Thread.current != @thread && @lock.locked?
52
- # we're being called inside on_writable
53
- super
54
- else
55
- @lock.synchronize { super }
56
- end
32
+ def <<(output)
33
+ @queue << output
34
+ signal
57
35
  end
58
36
 
59
- def defer_body(io, out_headers)
60
- @lock.synchronize { super }
37
+ def on_signal
38
+ client, response = @queue.pop
39
+ client.response_write(response)
61
40
  end
41
+ end
62
42
 
63
- def response_write(response, out)
43
+ class Client < Rainbows::Rev::Client
44
+ DR = Rainbows::Rev::DeferredResponse
45
+ KATO = Rainbows::Rev::KATO
46
+
47
+ def response_write(response)
48
+ enable
49
+ alive = @hp.keepalive? && G.alive
50
+ out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if @hp.headers?
64
51
  DR.write(self, response, out)
65
- (out && CONN_ALIVE == out.first) or
66
- @lock.synchronize {
67
- quit
68
- schedule_write
69
- }
52
+ return quit unless alive && G.alive
53
+
54
+ @env.clear
55
+ @hp.reset
56
+ @state = :headers
57
+ # keepalive requests are always body-less, so @input is unchanged
58
+ if @hp.headers(@env, @buf)
59
+ @input = HttpRequest::NULL_IO
60
+ app_call
61
+ else
62
+ KATO[self] = Time.now
63
+ end
70
64
  end
71
65
 
72
- def on_writable
73
- # don't ever want to block in the main loop with lots of clients,
74
- # libev is level-triggered so we'll always get another chance later
75
- if @lock.try_lock
76
- begin
77
- super
78
- ensure
79
- @lock.unlock
80
- end
66
+ # fails-safe application dispatch, we absolutely cannot
67
+ # afford to fail or raise an exception (killing the thread)
68
+ # here because that could cause a deadlock and we'd leak FDs
69
+ def app_response
70
+ begin
71
+ @env[REMOTE_ADDR] = @remote_addr
72
+ APP.call(@env.update(RACK_DEFAULTS))
73
+ rescue => e
74
+ Error.app(e) # we guarantee this does not raise
75
+ [ 500, {}, [] ]
81
76
  end
82
77
  end
83
78
 
79
+ def app_call
80
+ KATO.delete(client = self)
81
+ disable
82
+ @env[RACK_INPUT] = @input
83
+ @input = nil # not sure why, @input seems to get closed otherwise...
84
+ Thread.new { MASTER << [ client, app_response ] }
85
+ end
84
86
  end
85
87
 
86
88
  include Rainbows::Rev::Core
87
89
 
88
90
  def init_worker_process(worker)
89
91
  super
90
- Client::TEE_RESUMER.attach(::Rev::Loop.default)
92
+ Client.const_set(:MASTER, Master.new.attach(::Rev::Loop.default))
91
93
  end
92
94
 
93
95
  end