rainbows 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.document +6 -5
  2. data/DEPLOY +13 -0
  3. data/GIT-VERSION-GEN +1 -1
  4. data/GNUmakefile +1 -1
  5. data/README +32 -6
  6. data/SIGNALS +11 -7
  7. data/TODO +2 -3
  8. data/lib/rainbows.rb +10 -3
  9. data/lib/rainbows/app_pool.rb +90 -0
  10. data/lib/rainbows/base.rb +41 -4
  11. data/lib/rainbows/const.rb +1 -6
  12. data/lib/rainbows/http_server.rb +1 -1
  13. data/lib/rainbows/rev.rb +174 -0
  14. data/lib/rainbows/revactor.rb +40 -37
  15. data/lib/rainbows/thread_pool.rb +31 -57
  16. data/lib/rainbows/thread_spawn.rb +32 -45
  17. data/local.mk.sample +4 -3
  18. data/t/.gitignore +1 -2
  19. data/t/GNUmakefile +21 -7
  20. data/t/README +42 -0
  21. data/t/bin/content-md5-put +36 -0
  22. data/t/bin/unused_listen +1 -1
  23. data/t/content-md5.ru +23 -0
  24. data/t/sleep.ru +11 -0
  25. data/t/t0000-basic.sh +29 -3
  26. data/t/t1000-thread-pool-basic.sh +5 -6
  27. data/t/t1000.ru +5 -1
  28. data/t/t1002-thread-pool-graceful.sh +37 -0
  29. data/t/t2000-thread-spawn-basic.sh +4 -6
  30. data/t/t2000.ru +5 -1
  31. data/t/t2002-thread-spawn-graceful.sh +37 -0
  32. data/t/t3000-revactor-basic.sh +4 -6
  33. data/t/t3000.ru +5 -1
  34. data/t/t3001-revactor-pipeline.sh +46 -0
  35. data/t/t3002-revactor-graceful.sh +38 -0
  36. data/t/t3003-revactor-reopen-logs.sh +54 -0
  37. data/t/t3100-revactor-tee-input.sh +8 -13
  38. data/t/t4000-rev-basic.sh +51 -0
  39. data/t/t4000.ru +9 -0
  40. data/t/t4002-rev-graceful.sh +52 -0
  41. data/t/t4003-rev-parser-error.sh +34 -0
  42. data/t/t4100-rev-rack-input.sh +44 -0
  43. data/t/t4101-rev-rack-input-trailer.sh +51 -0
  44. data/t/t9000-rack-app-pool.sh +37 -0
  45. data/t/t9000.ru +14 -0
  46. data/t/test-lib.sh +29 -2
  47. data/vs_Unicorn +50 -1
  48. metadata +28 -6
@@ -14,12 +14,14 @@ module Rainbows
14
14
  # +worker_connections+ will limit the number of client Actors we have
15
15
  # running at any one time.
16
16
  #
17
- # Applications using this model are required to be reentrant, but
18
- # generally do not have to worry about race conditions. Multiple
19
- # instances of the same app may run in the same address space
17
+ # Applications using this model are required to be reentrant, but do
18
+ # not have to worry about race conditions unless they use threads
19
+ # internally. \Rainbows! does not spawn threads under this model.
20
+ # Multiple instances of the same app may run in the same address space
20
21
  # sequentially (but at interleaved points). Any network dependencies
21
22
  # in the application using this model should be implemented using the
22
- # \Revactor library as well.
23
+ # \Revactor library as well, to take advantage of the networking
24
+ # concurrency features this model provides.
23
25
 
24
26
  module Revactor
25
27
  require 'rainbows/revactor/tee_input'
@@ -32,6 +34,7 @@ module Rainbows
32
34
  buf = client.read or return # this probably does not happen...
33
35
  hp = HttpParser.new
34
36
  env = {}
37
+ alive = true
35
38
  remote_addr = ::Revactor::TCP::Socket === client ?
36
39
  client.remote_addr : LOCALHOST
37
40
 
@@ -52,9 +55,10 @@ module Rainbows
52
55
  response = app.call(env)
53
56
  end
54
57
 
55
- out = [ hp.keepalive? ? CONN_ALIVE : CONN_CLOSE ] if hp.headers?
58
+ alive = hp.keepalive? && ! Actor.current[:quit]
59
+ out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if hp.headers?
56
60
  HttpResponse.write(client, response, out)
57
- end while hp.keepalive? and hp.reset.nil? and env.clear
61
+ end while alive and hp.reset.nil? and env.clear
58
62
  client.close
59
63
  # if we get any error, try to write something back to the client
60
64
  # assuming we haven't closed the socket, but don't get hung up
@@ -74,59 +78,57 @@ module Rainbows
74
78
  # for connections and doesn't die until the parent dies (or is
75
79
  # given a INT, QUIT, or TERM signal)
76
80
  def worker_loop(worker)
77
- ppid = master_pid
78
81
  init_worker_process(worker)
79
- alive = worker.tmp # tmp is our lifeline to the master process
80
-
81
- trap(:USR1) { reopen_worker_logs(worker.nr) }
82
- trap(:QUIT) { alive = false; LISTENERS.each { |s| s.close rescue nil } }
83
- [:TERM, :INT].each { |sig| trap(sig) { exit!(0) } } # instant shutdown
84
82
 
85
83
  root = Actor.current
86
84
  root.trap_exit = true
87
85
 
88
86
  limit = worker_connections
89
- listeners = revactorize_listeners
90
- logger.info "worker=#{worker.nr} ready with Revactor"
91
- clients = 0
87
+ revactorize_listeners!
88
+ clients = {}
89
+ alive = true
92
90
 
93
- listeners.map! do |s|
91
+ listeners = LISTENERS.map do |s|
94
92
  Actor.spawn(s) do |l|
95
93
  begin
96
- while clients >= limit
97
- logger.info "busy: clients=#{clients} >= limit=#{limit}"
94
+ while clients.size >= limit
95
+ logger.info "busy: clients=#{clients.size} >= limit=#{limit}"
98
96
  Actor.receive { |filter| filter.when(:resume) {} }
99
97
  end
100
98
  actor = Actor.spawn(l.accept) { |c| process_client(c) }
101
- clients += 1
99
+ clients[actor.object_id] = actor
102
100
  root.link(actor)
103
101
  rescue Errno::EAGAIN, Errno::ECONNABORTED
102
+ rescue Errno::EBADF
103
+ break
104
104
  rescue Object => e
105
- if alive
106
- logger.error "Unhandled listen loop exception #{e.inspect}."
107
- logger.error e.backtrace.join("\n")
108
- end
105
+ listen_loop_error(e) if alive
109
106
  end while alive
110
107
  end
111
108
  end
112
109
 
113
- nr = 0
110
+ m = 0
111
+ check_quit = lambda do
112
+ worker.tmp.chmod(m = 0 == m ? 1 : 0)
113
+ if listeners.any? { |l| l.dead? } ||
114
+ master_pid != Process.ppid ||
115
+ LISTENERS.first.nil?
116
+ alive = false
117
+ clients.each_value { |a| a[:quit] = true }
118
+ end
119
+ end
120
+
114
121
  begin
115
122
  Actor.receive do |filter|
116
- filter.after(1) do
117
- if alive
118
- alive.chmod(nr = 0 == nr ? 1 : 0)
119
- listeners.each { |l| alive = false if l.dead? }
120
- ppid == Process.ppid or alive = false
121
- end
122
- end
123
+ filter.after(timeout, &check_quit)
123
124
  filter.when(Case[:exit, Actor, Object]) do |_,actor,_|
124
- orig = clients
125
- clients -= 1
125
+ orig = clients.size
126
+ clients.delete(actor.object_id)
126
127
  orig >= limit and listeners.each { |l| l << :resume }
128
+ check_quit.call
127
129
  end
128
130
  end
129
- end while alive || clients > 0
131
+ end while alive || clients.size > 0
130
132
  end
131
133
 
132
134
  private
@@ -141,8 +143,8 @@ module Rainbows
141
143
  client.close rescue nil
142
144
  end
143
145
 
144
- def revactorize_listeners
145
- LISTENERS.map do |s|
146
+ def revactorize_listeners!
147
+ LISTENERS.map! do |s|
146
148
  if TCPServer === s
147
149
  ::Revactor::TCP.listen(s, nil)
148
150
  elsif defined?(::Revactor::UNIX) && UNIXServer === s
@@ -151,7 +153,8 @@ module Rainbows
151
153
  logger.error "your version of Revactor can't handle #{s.inspect}"
152
154
  nil
153
155
  end
154
- end.compact
156
+ end
157
+ LISTENERS.compact!
155
158
  end
156
159
 
157
160
  end
@@ -3,8 +3,14 @@
3
3
  module Rainbows
4
4
 
5
5
  # Implements a worker thread pool model. This is suited for platforms
6
- # where the cost of dynamically spawning a new thread for every new
7
- # client connection is too high.
6
+ # like Ruby 1.9, where the cost of dynamically spawning a new thread
7
+ # for every new client connection is higher than with the ThreadSpawn
8
+ # model.
9
+ #
10
+ # This model should provide a high level of compatibility with all
11
+ # Ruby implementations, and most libraries and applications.
12
+ # Applications running under this model should be thread-safe
13
+ # but not necessarily reentrant.
8
14
  #
9
15
  # Applications using this model are required to be thread-safe.
10
16
  # Threads are never spawned dynamically under this model. If you're
@@ -12,80 +18,48 @@ module Rainbows
12
18
  # consider using the "resolv-replace" library which replaces parts of
13
19
  # the core Socket package with concurrent DNS lookup capabilities.
14
20
  #
15
- # This model is less suited for many slow clients than the others and
16
- # thus a lower +worker_connections+ setting is recommended.
21
+ # This model probably less suited for many slow clients than the
22
+ # others and thus a lower +worker_connections+ setting is recommended.
23
+
17
24
  module ThreadPool
18
25
 
19
26
  include Base
20
27
 
21
28
  def worker_loop(worker)
22
29
  init_worker_process(worker)
23
- threads = ThreadGroup.new
24
- alive = worker.tmp
25
- nr = 0
26
-
27
- # closing anything we IO.select on will raise EBADF
28
- trap(:USR1) { reopen_worker_logs(worker.nr) rescue nil }
29
- trap(:QUIT) { LISTENERS.map! { |s| s.close rescue nil } }
30
- [:TERM, :INT].each { |sig| trap(sig) { exit(0) } } # instant shutdown
31
- logger.info "worker=#{worker.nr} ready with ThreadPool"
30
+ RACK_DEFAULTS["rack.multithread"] = true
31
+ pool = (1..worker_connections).map { new_worker_thread }
32
+ m = 0
32
33
 
33
34
  while LISTENERS.first && master_pid == Process.ppid
34
- maintain_thread_count(threads)
35
- threads.list.each do |thr|
36
- alive.chmod(nr += 1)
37
- thr.join(timeout / 2.0) and break
35
+ pool.each do |thr|
36
+ worker.tmp.chmod(m = 0 == m ? 1 : 0)
37
+ # if any worker dies, something is serious wrong, bail
38
+ thr.join(timeout) and break
38
39
  end
39
40
  end
40
- join_worker_threads(threads)
41
- end
42
-
43
- def join_worker_threads(threads)
44
- logger.info "Joining worker threads..."
45
- t0 = Time.now
46
- timeleft = timeout
47
- threads.list.each { |thr|
48
- thr.join(timeleft)
49
- timeleft -= (Time.now - t0)
50
- }
51
- logger.info "Done joining worker threads."
52
- end
53
-
54
- def maintain_thread_count(threads)
55
- threads.list.each do |thr|
56
- next if (Time.now - (thr[:t] || next)) < timeout
57
- thr.kill
58
- logger.error "killed #{thr.inspect} for being too old"
59
- end
60
-
61
- while threads.list.size < worker_connections
62
- threads.add(new_worker_thread)
63
- end
41
+ join_threads(pool, worker)
64
42
  end
65
43
 
66
44
  def new_worker_thread
67
45
  Thread.new {
68
46
  begin
69
- ret = begin
70
- Thread.current[:t] = Time.now
71
- IO.select(LISTENERS, nil, nil, timeout/2.0) or next
47
+ begin
48
+ ret = IO.select(LISTENERS, nil, nil, timeout) or next
49
+ ret.first.each do |sock|
50
+ begin
51
+ process_client(sock.accept_nonblock)
52
+ rescue Errno::EAGAIN, Errno::ECONNABORTED
53
+ end
54
+ end
72
55
  rescue Errno::EINTR
73
- retry
74
- rescue Errno::EBADF
56
+ next
57
+ rescue Errno::EBADF, TypeError
75
58
  return
76
59
  end
77
- ret.first.each do |sock|
78
- begin
79
- process_client(sock.accept_nonblock)
80
- rescue Errno::EAGAIN, Errno::ECONNABORTED
81
- end
82
- end
83
60
  rescue Object => e
84
- if LISTENERS.first
85
- logger.error "Unhandled listen loop exception #{e.inspect}."
86
- logger.error e.backtrace.join("\n")
87
- end
88
- end while LISTENERS.first
61
+ listen_loop_error(e) if LISTENERS.first
62
+ end while ! Thread.current[:quit] && LISTENERS.first
89
63
  }
90
64
  end
91
65
 
@@ -2,75 +2,62 @@
2
2
  module Rainbows
3
3
 
4
4
  # Spawns a new thread for every client connection we accept(). This
5
- # model is recommended for platforms where spawning threads is
6
- # inexpensive.
5
+ # model is recommended for platforms like Ruby 1.8 where spawning new
6
+ # threads is inexpensive.
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.
7
12
  #
8
13
  # If you're connecting to external services and need to perform DNS
9
14
  # lookups, consider using the "resolv-replace" library which replaces
10
15
  # parts of the core Socket package with concurrent DNS lookup
11
16
  # capabilities
17
+
12
18
  module ThreadSpawn
13
19
 
14
20
  include Base
15
21
 
16
22
  def worker_loop(worker)
17
23
  init_worker_process(worker)
24
+ RACK_DEFAULTS["rack.multithread"] = true
18
25
  threads = ThreadGroup.new
19
26
  alive = worker.tmp
20
- nr = 0
27
+ m = 0
21
28
  limit = worker_connections
22
29
 
23
- # closing anything we IO.select on will raise EBADF
24
- trap(:USR1) { reopen_worker_logs(worker.nr) rescue nil }
25
- trap(:QUIT) { LISTENERS.map! { |s| s.close rescue nil } }
26
- [:TERM, :INT].each { |sig| trap(sig) { exit(0) } } # instant shutdown
27
- logger.info "worker=#{worker.nr} ready with ThreadSpawn"
28
-
29
- while alive && master_pid == Process.ppid
30
+ begin
30
31
  ret = begin
31
- alive.chmod(nr += 1)
32
- IO.select(LISTENERS, nil, nil, timeout/2.0) or next
32
+ alive.chmod(m = 0 == m ? 1 : 0)
33
+ IO.select(LISTENERS, nil, nil, timeout) or next
33
34
  rescue Errno::EINTR
34
35
  retry
35
- rescue Errno::EBADF
36
- alive = false
36
+ rescue Errno::EBADF, TypeError
37
+ break
37
38
  end
39
+ alive.chmod(m = 0 == m ? 1 : 0)
38
40
 
39
41
  ret.first.each do |l|
40
- while threads.list.size >= limit
41
- nuke_old_thread(threads)
42
+ # Sleep if we're busy, another less busy worker process may
43
+ # take it for us if we sleep. This is gross but other options
44
+ # still suck because they require expensive/complicated
45
+ # synchronization primitives for _every_ case, not just this
46
+ # unlikely one. Since this case is (or should be) uncommon,
47
+ # just busy wait when we have to.
48
+ while threads.list.size > limit # unlikely
49
+ sleep(0.1) # hope another process took it
50
+ break # back to IO.select
42
51
  end
43
- c = begin
44
- l.accept_nonblock
45
- rescue Errno::EINTR, Errno::ECONNABORTED
46
- next
52
+ begin
53
+ threads.add(Thread.new(l.accept_nonblock) {|c| process_client(c) })
54
+ rescue Errno::EAGAIN, Errno::ECONNABORTED
47
55
  end
48
- threads.add(Thread.new(c) { |c| process_client(c) })
49
56
  end
50
- end
51
- join_spawned_threads(threads)
52
- end
53
-
54
- def nuke_old_thread(threads)
55
- threads.list.each do |thr|
56
- next if (Time.now - (thr[:t] || next)) < timeout
57
- thr.kill
58
- logger.error "killed #{thr.inspect} for being too old"
59
- return
60
- end
61
- # nothing to kill, yield to another thread
62
- Thread.pass
63
- end
64
-
65
- def join_spawned_threads(threads)
66
- logger.info "Joining spawned threads..."
67
- t0 = Time.now
68
- timeleft = timeout
69
- threads.list.each { |thr|
70
- thr.join(timeleft)
71
- timeleft -= (Time.now - t0)
72
- }
73
- logger.info "Done joining spawned threads."
57
+ rescue Object => e
58
+ listen_loop_error(e) if LISTENERS.first
59
+ end while LISTENERS.first && master_pid == Process.ppid
60
+ join_threads(threads.list, worker)
74
61
  end
75
62
 
76
63
  end
data/local.mk.sample CHANGED
@@ -5,7 +5,7 @@
5
5
  # This is depends on a bunch of GNU-isms from bash, sed, touch.
6
6
 
7
7
  DLEXT := so
8
- gems := rev-0.3.0 rack-1.0.0 iobuffer-0.1.1
8
+ gems := rev-0.3.1 rack-1.0.0 iobuffer-0.1.1
9
9
 
10
10
  # Avoid loading rubygems to speed up tests because gmake is
11
11
  # fork+exec heavy with Ruby.
@@ -25,8 +25,9 @@ ifdef gem_paths
25
25
  RUBYLIB := $(subst $(sp),:,$(addsuffix /lib,$(gem_paths)))
26
26
  endif
27
27
 
28
- # pipefail is THE reason to use bash (v3+)
29
- SHELL := /bin/bash -e -o pipefail
28
+ # pipefail is THE reason to use bash (v3+) or never revisions of ksh93
29
+ # SHELL := /bin/bash -e -o pipefail
30
+ SHELL := /bin/ksh93 -e -o pipefail
30
31
 
31
32
  full-test: test-18 test-19
32
33
  test-18:
data/t/.gitignore CHANGED
@@ -1,4 +1,3 @@
1
1
  /test-results-*
2
2
  /test-bin-*
3
- /*.code
4
- /*.log
3
+ /random_blob
data/t/GNUmakefile CHANGED
@@ -21,12 +21,15 @@ T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)
21
21
  all:: $(T)
22
22
 
23
23
  # can't rely on "set -o pipefail" since we don't require bash or ksh93 :<
24
- t_code = $@-$(RUBY_VERSION).code
25
- t_log = $@-$(RUBY_VERSION).log
26
- t_run = $(TRACER) $(SHELL) $(TEST_OPTS) $@
24
+ t_pfx = trash/$@-$(RUBY_VERSION)
25
+ t_code = $(t_pfx).code
26
+ t_log = $(t_pfx).log
27
+ # TRACER = strace -f -o $(t_pfx).strace -s 100000
28
+ # TRACER = /usr/bin/time -o $(t_pfx).time
29
+ t_run = $(TRACER) $(SHELL) $(SH_TEST_OPTS) $@
27
30
 
28
31
  # prefix stdout messages with ':', and stderr messages with '!'
29
- t_wrap = ( ( ( $(RM) $(t_code); \
32
+ t_wrap = ( ( ( echo 42 > $(t_code); \
30
33
  $(t_run); \
31
34
  echo $$? > $(t_code) ) \
32
35
  | sed 's/^/$(pfx):/' 1>&3 ) 2>&1 \
@@ -38,14 +41,13 @@ ifndef V
38
41
  pfx =
39
42
  else
40
43
  ifeq ($(V),2)
41
- TEST_OPTS += -x
44
+ SH_TEST_OPTS += -x
42
45
  endif
43
46
  quiet_pre = @echo '* $@';
44
47
  quiet_post = 2>&1 | tee $(t_log); exit $$(cat $(t_code))
45
48
  pfx = $@
46
49
  endif
47
50
 
48
- # TRACER='strace -f -o $@.strace -s 100000'
49
51
  run_test = $(quiet_pre) ( $(t_wrap) ) $(quiet_post)
50
52
 
51
53
  test-bin-$(RUBY_VERSION)/rainbows: ruby_bin = $(shell which $(ruby))
@@ -56,12 +58,24 @@ test-bin-$(RUBY_VERSION)/rainbows: ../bin/rainbows
56
58
  cmp $@+ $@ 2>/dev/null || mv $@+ $@
57
59
  $(RM) $@+
58
60
 
61
+ req_random_blob := t3100-revactor-tee-input
62
+ random_blob:
63
+ dd if=/dev/urandom bs=1M count=10 of=$@+
64
+ mv $@+ $@
65
+
66
+ $(addsuffix .sh,$(req_random_blob)): random_blob
67
+
68
+ $(T): trash/.gitignore
59
69
  $(T): export ruby := $(ruby)
60
70
  $(T): export PATH := $(CURDIR)/test-bin-$(RUBY_VERSION):$(PATH)
61
71
  $(T): test-bin-$(RUBY_VERSION)/rainbows
62
72
  $(run_test)
63
73
 
74
+ trash/.gitignore:
75
+ mkdir -p $(@D)
76
+ echo '*' > $@
77
+
64
78
  clean:
65
- $(RM) -r *.log *.code test-bin-$(RUBY_VERSION)
79
+ $(RM) -r trash/*.log trash/*.code test-bin-$(RUBY_VERSION)
66
80
 
67
81
  .PHONY: $(T) clean