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
@@ -23,21 +23,30 @@ module Rainbows
23
23
  module Revactor
24
24
  require 'rainbows/revactor/tee_input'
25
25
 
26
+ RD_ARGS = {}
27
+
26
28
  include Base
27
29
 
28
30
  # once a client is accepted, it is processed in its entirety here
29
31
  # in 3 easy steps: read request, call app, write app response
30
32
  def process_client(client)
31
- buf = client.read or return # this probably does not happen...
33
+ defined?(Fcntl::FD_CLOEXEC) and
34
+ client.instance_eval { @_io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
35
+ rd_args = [ nil ]
36
+ remote_addr = if ::Revactor::TCP::Socket === client
37
+ rd_args << RD_ARGS
38
+ client.remote_addr
39
+ else
40
+ LOCALHOST
41
+ end
42
+ buf = client.read(*rd_args)
32
43
  hp = HttpParser.new
33
44
  env = {}
34
45
  alive = true
35
- remote_addr = ::Revactor::TCP::Socket === client ?
36
- client.remote_addr : LOCALHOST
37
46
 
38
47
  begin
39
48
  while ! hp.headers(env, buf)
40
- buf << client.read
49
+ buf << client.read(*rd_args)
41
50
  end
42
51
 
43
52
  env[Const::RACK_INPUT] = 0 == hp.content_length ?
@@ -56,9 +65,11 @@ module Rainbows
56
65
  out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if hp.headers?
57
66
  HttpResponse.write(client, response, out)
58
67
  end while alive and hp.reset.nil? and env.clear
59
- client.close
68
+ rescue ::Revactor::TCP::ReadError
60
69
  rescue => e
61
70
  handle_error(client, e)
71
+ ensure
72
+ client.close
62
73
  end
63
74
 
64
75
  # runs inside each forked worker, this sits around and waits
@@ -66,6 +77,7 @@ module Rainbows
66
77
  # given a INT, QUIT, or TERM signal)
67
78
  def worker_loop(worker)
68
79
  init_worker_process(worker)
80
+ RD_ARGS[:timeout] = G.kato if G.kato > 0
69
81
 
70
82
  root = Actor.current
71
83
  root.trap_exit = true
@@ -85,8 +97,8 @@ module Rainbows
85
97
  clients[actor.object_id] = actor
86
98
  root.link(actor)
87
99
  rescue Errno::EAGAIN, Errno::ECONNABORTED
88
- rescue Object => e
89
- listen_loop_error(e)
100
+ rescue => e
101
+ Error.listen_loop(e)
90
102
  end while G.alive
91
103
  end
92
104
  end
@@ -109,23 +121,10 @@ module Rainbows
109
121
  # if the socket is already closed or broken. We'll always ensure
110
122
  # the socket is closed at the end of this function
111
123
  def handle_error(client, e)
112
- msg = case e
113
- when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
114
- Const::ERROR_500_RESPONSE
115
- when HttpParserError # try to tell the client they're bad
116
- Const::ERROR_400_RESPONSE
117
- else
118
- logger.error "Read error: #{e.inspect}"
119
- logger.error e.backtrace.join("\n")
120
- Const::ERROR_500_RESPONSE
121
- end
122
- client.instance_eval do
123
- # this is Revactor implementation dependent
124
- @_io.write_nonblock(msg)
125
- close
126
- end
124
+ # this is Revactor implementation dependent
125
+ msg = Error.response(e) and
126
+ client.instance_eval { @_io.write_nonblock(msg) }
127
127
  rescue
128
- nil
129
128
  end
130
129
 
131
130
  def revactorize_listeners!
@@ -27,42 +27,44 @@ module Rainbows
27
27
 
28
28
  def worker_loop(worker)
29
29
  init_worker_process(worker)
30
- pool = (1..worker_connections).map { new_worker_thread }
30
+ pool = (1..worker_connections).map do
31
+ Thread.new { LISTENERS.size == 1 ? sync_worker : async_worker }
32
+ end
31
33
 
32
34
  while G.alive
33
35
  # if any worker dies, something is serious wrong, bail
34
36
  pool.each do |thr|
35
- G.tick
37
+ G.tick or break
36
38
  thr.join(1) and G.quit!
37
39
  end
38
40
  end
39
41
  join_threads(pool)
40
42
  end
41
43
 
42
- def new_worker_thread
43
- Thread.new {
44
- begin
45
- begin
46
- # TODO: check if select() or accept() is a problem on large
47
- # SMP systems under Ruby 1.9. Hundreds of native threads
48
- # all working off the same socket could be a thundering herd
49
- # problem. On the other hand, a thundering herd may not
50
- # even incur as much overhead as an extra Mutex#synchronize
51
- ret = IO.select(LISTENERS, nil, nil, 1) and
52
- ret.first.each do |sock|
53
- begin
54
- process_client(sock.accept_nonblock)
55
- rescue Errno::EAGAIN, Errno::ECONNABORTED
56
- end
57
- end
58
- rescue Errno::EINTR
59
- rescue Errno::EBADF, TypeError
60
- break
61
- end
62
- rescue Object => e
63
- listen_loop_error(e)
64
- end while G.alive
65
- }
44
+ def sync_worker
45
+ s = LISTENERS.first
46
+ begin
47
+ process_client(s.accept)
48
+ rescue Errno::EINTR, Errno::ECONNABORTED
49
+ rescue => e
50
+ Error.listen_loop(e)
51
+ end while G.alive
52
+ end
53
+
54
+ def async_worker
55
+ begin
56
+ # TODO: check if select() or accept() is a problem on large
57
+ # SMP systems under Ruby 1.9. Hundreds of native threads
58
+ # all working off the same socket could be a thundering herd
59
+ # problem. On the other hand, a thundering herd may not
60
+ # even incur as much overhead as an extra Mutex#synchronize
61
+ ret = IO.select(LISTENERS, nil, nil, 1) and ret.first.each do |s|
62
+ s = Rainbows.accept(s) and process_client(s)
63
+ end
64
+ rescue Errno::EINTR
65
+ rescue => e
66
+ Error.listen_loop(e)
67
+ end while G.alive
66
68
  end
67
69
 
68
70
  end
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
+ require 'thread'
2
3
  module Rainbows
3
4
 
4
5
  # Spawns a new thread for every client connection we accept(). This
@@ -19,43 +20,42 @@ module Rainbows
19
20
 
20
21
  include Base
21
22
 
22
- def worker_loop(worker)
23
- init_worker_process(worker)
24
- threads = ThreadGroup.new
23
+ def accept_loop(klass)
24
+ lock = Mutex.new
25
25
  limit = worker_connections
26
-
27
- begin
28
- ret = begin
29
- G.tick or break
30
- IO.select(LISTENERS, nil, nil, 1) or next
31
- rescue Errno::EINTR
32
- retry
33
- rescue Errno::EBADF, TypeError
34
- break
35
- end
36
- G.tick
37
-
38
- ret.first.each do |l|
39
- # Sleep if we're busy, another less busy worker process may
40
- # take it for us if we sleep. This is gross but other options
41
- # still suck because they require expensive/complicated
42
- # synchronization primitives for _every_ case, not just this
43
- # unlikely one. Since this case is (or should be) uncommon,
44
- # just busy wait when we have to.
45
- while threads.list.size > limit # unlikely
46
- sleep(0.1) # hope another process took it
47
- break # back to IO.select
48
- end
26
+ LISTENERS.each do |l|
27
+ klass.new(l) do |l|
49
28
  begin
50
- threads.add(Thread.new(l.accept_nonblock) {|c| process_client(c) })
51
- rescue Errno::EAGAIN, Errno::ECONNABORTED
52
- end
29
+ if lock.synchronize { G.cur >= limit }
30
+ # Sleep if we're busy, another less busy worker process may
31
+ # take it for us if we sleep. This is gross but other options
32
+ # still suck because they require expensive/complicated
33
+ # synchronization primitives for _every_ case, not just this
34
+ # unlikely one. Since this case is (or should be) uncommon,
35
+ # just busy wait when we have to.
36
+ sleep(0.01)
37
+ else
38
+ klass.new(l.accept) do |c|
39
+ begin
40
+ lock.synchronize { G.cur += 1 }
41
+ process_client(c)
42
+ ensure
43
+ lock.synchronize { G.cur -= 1 }
44
+ end
45
+ end
46
+ end
47
+ rescue Errno::EINTR, Errno::ECONNABORTED
48
+ rescue => e
49
+ Error.listen_loop(e)
50
+ end while G.alive
53
51
  end
54
- rescue Object => e
55
- listen_loop_error(e)
56
- end while true
57
- join_threads(threads.list)
52
+ end
53
+ sleep 1 while G.tick || lock.synchronize { G.cur > 0 }
58
54
  end
59
55
 
56
+ def worker_loop(worker)
57
+ init_worker_process(worker)
58
+ accept_loop(Thread)
59
+ end
60
60
  end
61
61
  end
data/local.mk.sample CHANGED
@@ -4,12 +4,15 @@
4
4
  #
5
5
  # This is depends on a bunch of GNU-isms from bash, sed, touch.
6
6
 
7
+ RSYNC = rsync
7
8
  DLEXT := so
8
9
  gems := rack-1.0.1
9
- # gems += unicorn-0.95.0 # installed via setup.rb
10
- gems += rev-0.3.1 iobuffer-0.1.1
10
+ # gems += unicorn-0.95.1 # installed via setup.rb
11
+ gems += rev-0.3.1 # up to 0.3.2 once it's out for 1.8
12
+ gems += iobuffer-0.1.3
11
13
  gems += eventmachine-0.12.10
12
14
  gems += async_sinatra-0.1.5 sinatra-0.9.4
15
+ gems += espace-neverblock-0.1.6.1
13
16
 
14
17
  # Avoid loading rubygems to speed up tests because gmake is
15
18
  # fork+exec heavy with Ruby.
@@ -51,20 +54,19 @@ latest: NEWS
51
54
  # publishes docs to http://rainbows.rubyforge.org
52
55
  publish_doc:
53
56
  -git set-file-times
54
- $(RM) -r doc
55
- $(MAKE) doc
57
+ $(RM) -r doc ChangeLog NEWS
58
+ $(MAKE) doc LOG_VERSION=$(shell git tag -l | tail -1)
56
59
  $(MAKE) -s latest > doc/LATEST
57
60
  find doc/images doc/js -type f | \
58
61
  TZ=UTC xargs touch -d '1970-01-01 00:00:00' doc/rdoc.css
59
62
  $(MAKE) doc_gz
60
63
  chmod 644 $$(find doc -type f)
61
- rsync -av --delete doc/ \
62
- rubyforge.org:/var/www/gforge-projects/rainbows/
64
+ $(RSYNC) -av doc/ rubyforge.org:/var/www/gforge-projects/rainbows/
65
+ $(RSYNC) -av doc/ dcvr:/srv/rainbows/
63
66
  git ls-files | xargs touch
64
67
 
65
68
  # Create gzip variants of the same timestamp as the original so nginx
66
69
  # "gzip_static on" can serve the gzipped versions directly.
67
- doc_gz: suf := html js css
68
70
  doc_gz: docs = $(shell find doc -type f ! -regex '^.*\.\(gif\|jpg\|png\|gz\)$$')
69
71
  doc_gz:
70
72
  touch doc/NEWS.atom.xml -d "$$(awk 'NR==1{print $$4,$$5,$$6}' NEWS)"
data/rainbows.gemspec CHANGED
@@ -54,10 +54,16 @@ Gem::Specification.new do |s|
54
54
  # s.add_dependency(%q<revactor>, [">= 0.1.5"])
55
55
  #
56
56
  # Revactor depends on Rev, too, 0.3.0 got the ability to attach IOs
57
- # s.add_dependency(%q<rev>, [">= 0.3.0"])
57
+ # s.add_dependency(%q<rev>, [">= 0.3.1"])
58
+ #
59
+ # Rev depends on IOBuffer, which got faster in 0.1.3
60
+ # s.add_dependency(%q<iobuffer>, [">= 0.1.3"])
58
61
  #
59
62
  # We use the new EM::attach/watch API in 0.12.10
60
63
  # s.add_dependency(%q<eventmachine>, ["~> 0.12.10"])
64
+ #
65
+ # NeverBlock, currently only available on http://gems.github.com/
66
+ # s.add_dependency(%q<espace-neverblock>, ["~> 0.1.6.1"])
61
67
 
62
- # s.licenses = %w(GPLv2 Ruby) # accessor not compatible with older Rubygems
68
+ # s.licenses = %w(GPLv2 Ruby) # accessor not compatible with older RubyGems
63
69
  end
data/t/GNUmakefile CHANGED
@@ -4,13 +4,17 @@ all::
4
4
 
5
5
  pid := $(shell echo $$PPID)
6
6
 
7
- RUBY = $(ruby)
7
+ RUBY = ruby
8
8
  rainbows_lib := $(shell cd ../lib && pwd)
9
9
  -include ../local.mk
10
10
  ifeq ($(RUBY_VERSION),)
11
11
  RUBY_VERSION := $(shell $(RUBY) -e 'puts RUBY_VERSION')
12
12
  endif
13
13
 
14
+ ifeq ($(RUBY_VERSION),)
15
+ $(error unable to detect RUBY_VERSION)
16
+ endif
17
+
14
18
  ifeq ($(RUBYLIB),)
15
19
  RUBYLIB := $(rainbows_lib)
16
20
  else
@@ -18,10 +22,16 @@ else
18
22
  endif
19
23
  export RUBYLIB RUBY_VERSION
20
24
 
21
- models := ThreadPool ThreadSpawn Rev EventMachine
22
- models += RevThreadSpawn
23
- ifeq ($(RUBY_VERSION),1.9.1) # 1.9.2-preview1 was broken
25
+ models = ThreadPool ThreadSpawn Rev EventMachine NeverBlock
26
+ rp := )
27
+ ONENINE := $(shell case $(RUBY_VERSION) in 1.9.*$(rp) echo true;;esac)
28
+ ifeq ($(ONENINE),true)
24
29
  models += Revactor
30
+ models += FiberSpawn
31
+ models += FiberPool
32
+
33
+ # technically this works under 1.8, but wait until rev 0.3.2
34
+ models += RevThreadSpawn
25
35
  endif
26
36
  all_models := $(models) Base
27
37
 
@@ -37,9 +47,6 @@ t0002-graceful.sh: MODELS = $(all_models)
37
47
  t0002-parser-error.sh: MODELS = $(all_models)
38
48
  t0003-reopen-logs.sh: MODELS = $(all_models)
39
49
 
40
- # this test is not compatible with non-Thread models yet
41
- t9000-rack-app-pool.sh: MODELS = ThreadPool ThreadSpawn
42
-
43
50
  # recursively run per-model tests
44
51
  # haven't figured out a good way to make make non-recursive here, yet...
45
52
  $(T):
data/t/fork-sleep.ru ADDED
@@ -0,0 +1,10 @@
1
+ #\-E none
2
+ # we do not want Rack::Lint or anything to protect us
3
+ use Rack::ContentLength
4
+ use Rack::ContentType, "text/plain"
5
+ trap(:CHLD) { $stderr.puts Process.waitpid2(-1).inspect }
6
+ map "/" do
7
+ time = ENV["nr"] || '15'
8
+ pid = fork { exec('sleep', time) }
9
+ run lambda { |env| [ 200, {}, [ "#{pid}\n" ] ] }
10
+ end
@@ -0,0 +1,9 @@
1
+ use Rack::ContentLength
2
+ use Rack::ContentType
3
+ run lambda { |env|
4
+ if env['rack.multithread'] == false && env['rainbows.model'] == :FiberPool
5
+ [ 200, {}, [ Thread.current.inspect << "\n" ] ]
6
+ else
7
+ raise "rack.multithread is not true"
8
+ end
9
+ }
@@ -0,0 +1,9 @@
1
+ use Rack::ContentLength
2
+ use Rack::ContentType
3
+ run lambda { |env|
4
+ if env['rack.multithread'] == false && env['rainbows.model'] == :FiberSpawn
5
+ [ 200, {}, [ Thread.current.inspect << "\n" ] ]
6
+ else
7
+ raise "rack.multithread is not true"
8
+ end
9
+ }
@@ -0,0 +1,11 @@
1
+ use Rack::ContentLength
2
+ use Rack::ContentType
3
+ run lambda { |env|
4
+ if env['rack.multithread'] == false &&
5
+ EM.reactor_running? &&
6
+ env['rainbows.model'] == :NeverBlock
7
+ [ 200, {}, [ Thread.current.inspect << "\n" ] ]
8
+ else
9
+ raise env.inspect
10
+ end
11
+ }
data/t/sleep.ru CHANGED
@@ -8,6 +8,8 @@ run lambda { |env|
8
8
  env["PATH_INFO"] =~ %r{/([\d\.]+)\z} and nr = $1.to_f
9
9
 
10
10
  (case env['rainbows.model']
11
+ when :FiberPool, :FiberSpawn
12
+ Rainbows::Fiber
11
13
  when :Revactor
12
14
  Actor
13
15
  else
@@ -1,6 +1,6 @@
1
1
  #!/bin/sh
2
2
  . ./test-lib.sh
3
- t_plan 24 "simple HTTP connection keepalive/pipelining tests for $model"
3
+ t_plan 25 "simple HTTP connection keepalive/pipelining tests for $model"
4
4
 
5
5
  t_begin "checking for config.ru for $model" && {
6
6
  tbase=simple-http_$model.ru
@@ -21,6 +21,17 @@ t_begin "single request" && {
21
21
  curl -sSfv http://$listen/
22
22
  }
23
23
 
24
+ t_begin "handles client EOF gracefully" && {
25
+ printf 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n' | \
26
+ socat - TCP4:$listen > $tmp
27
+ dbgcat tmp
28
+ if grep 'HTTP.* 500' $tmp
29
+ then
30
+ die "500 error returned on client shutdown(SHUT_WR)"
31
+ fi
32
+ check_stderr
33
+ }
34
+
24
35
  dbgcat r_err
25
36
 
26
37
  t_begin "two requests with keepalive" && {
data/t/t0001-unix-http.sh CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/bin/sh
2
2
  . ./test-lib.sh
3
- t_plan 18 "simple HTTP connection keepalive/pipelining tests for $model"
3
+ t_plan 19 "simple HTTP connection keepalive/pipelining tests for $model"
4
4
 
5
5
  t_begin "checking for config.ru for $model" && {
6
6
  tbase=simple-http_$model.ru
@@ -23,6 +23,17 @@ t_begin "single TCP request" && {
23
23
  curl -sSfv http://$listen/
24
24
  }
25
25
 
26
+ t_begin "handles client EOF gracefully" && {
27
+ printf 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n' | \
28
+ socat - UNIX:$unix_socket > $tmp
29
+ dbgcat tmp
30
+ if grep 'HTTP.* 500' $tmp
31
+ then
32
+ die "500 error returned on client shutdown(SHUT_WR)"
33
+ fi
34
+ check_stderr
35
+ }
36
+
26
37
  dbgcat r_err
27
38
 
28
39
  t_begin "pipelining partial requests" && {