rainbows 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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