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.
- data/.document +6 -5
- data/DEPLOY +13 -0
- data/GIT-VERSION-GEN +1 -1
- data/GNUmakefile +1 -1
- data/README +32 -6
- data/SIGNALS +11 -7
- data/TODO +2 -3
- data/lib/rainbows.rb +10 -3
- data/lib/rainbows/app_pool.rb +90 -0
- data/lib/rainbows/base.rb +41 -4
- data/lib/rainbows/const.rb +1 -6
- data/lib/rainbows/http_server.rb +1 -1
- data/lib/rainbows/rev.rb +174 -0
- data/lib/rainbows/revactor.rb +40 -37
- data/lib/rainbows/thread_pool.rb +31 -57
- data/lib/rainbows/thread_spawn.rb +32 -45
- data/local.mk.sample +4 -3
- data/t/.gitignore +1 -2
- data/t/GNUmakefile +21 -7
- data/t/README +42 -0
- data/t/bin/content-md5-put +36 -0
- data/t/bin/unused_listen +1 -1
- data/t/content-md5.ru +23 -0
- data/t/sleep.ru +11 -0
- data/t/t0000-basic.sh +29 -3
- data/t/t1000-thread-pool-basic.sh +5 -6
- data/t/t1000.ru +5 -1
- data/t/t1002-thread-pool-graceful.sh +37 -0
- data/t/t2000-thread-spawn-basic.sh +4 -6
- data/t/t2000.ru +5 -1
- data/t/t2002-thread-spawn-graceful.sh +37 -0
- data/t/t3000-revactor-basic.sh +4 -6
- data/t/t3000.ru +5 -1
- data/t/t3001-revactor-pipeline.sh +46 -0
- data/t/t3002-revactor-graceful.sh +38 -0
- data/t/t3003-revactor-reopen-logs.sh +54 -0
- data/t/t3100-revactor-tee-input.sh +8 -13
- data/t/t4000-rev-basic.sh +51 -0
- data/t/t4000.ru +9 -0
- data/t/t4002-rev-graceful.sh +52 -0
- data/t/t4003-rev-parser-error.sh +34 -0
- data/t/t4100-rev-rack-input.sh +44 -0
- data/t/t4101-rev-rack-input-trailer.sh +51 -0
- data/t/t9000-rack-app-pool.sh +37 -0
- data/t/t9000.ru +14 -0
- data/t/test-lib.sh +29 -2
- data/vs_Unicorn +50 -1
- metadata +28 -6
data/lib/rainbows/revactor.rb
CHANGED
@@ -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
|
-
#
|
19
|
-
#
|
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
|
-
|
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
|
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
|
-
|
90
|
-
|
91
|
-
|
87
|
+
revactorize_listeners!
|
88
|
+
clients = {}
|
89
|
+
alive = true
|
92
90
|
|
93
|
-
listeners.map
|
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
|
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
|
-
|
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(
|
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
|
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
|
156
|
+
end
|
157
|
+
LISTENERS.compact!
|
155
158
|
end
|
156
159
|
|
157
160
|
end
|
data/lib/rainbows/thread_pool.rb
CHANGED
@@ -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
|
7
|
-
# client connection is
|
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
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
thr.join(timeout
|
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
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
27
|
+
m = 0
|
21
28
|
limit = worker_connections
|
22
29
|
|
23
|
-
|
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(
|
32
|
-
IO.select(LISTENERS, nil, nil, timeout
|
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
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
44
|
-
l.accept_nonblock
|
45
|
-
rescue Errno::
|
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
|
-
|
51
|
-
|
52
|
-
|
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.
|
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
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
|
-
|
25
|
-
|
26
|
-
|
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 = ( ( (
|
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
|
-
|
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
|
79
|
+
$(RM) -r trash/*.log trash/*.code test-bin-$(RUBY_VERSION)
|
66
80
|
|
67
81
|
.PHONY: $(T) clean
|