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
@@ -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" && {