rainbows 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|