rainbows 0.1.1 → 0.2.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 (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