rainbows 0.5.0 → 0.6.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/FAQ +39 -0
- data/GIT-VERSION-GEN +1 -1
- data/GNUmakefile +1 -1
- data/Rakefile +39 -2
- data/lib/rainbows.rb +14 -2
- data/lib/rainbows/base.rb +8 -30
- data/lib/rainbows/const.rb +1 -1
- data/lib/rainbows/ev_core.rb +6 -2
- data/lib/rainbows/ev_thread_core.rb +80 -0
- data/lib/rainbows/event_machine.rb +10 -14
- data/lib/rainbows/http_response.rb +1 -1
- data/lib/rainbows/http_server.rb +10 -4
- data/lib/rainbows/rev.rb +4 -158
- data/lib/rainbows/rev/client.rb +73 -0
- data/lib/rainbows/rev/core.rb +42 -0
- data/lib/rainbows/rev/deferred_response.rb +74 -0
- data/lib/rainbows/rev/heartbeat.rb +1 -10
- data/lib/rainbows/rev_thread_spawn.rb +94 -0
- data/lib/rainbows/revactor.rb +23 -27
- data/lib/rainbows/revactor/tee_input.rb +13 -5
- data/lib/rainbows/thread_pool.rb +5 -6
- data/lib/rainbows/thread_spawn.rb +3 -6
- data/local.mk.sample +1 -1
- data/rainbows.gemspec +3 -2
- data/t/GNUmakefile +1 -0
- data/t/bin/sha1sum.rb +23 -0
- data/t/heartbeat-timeout.ru +1 -3
- data/t/sha1-random-size.ru +19 -0
- data/t/sha1.ru +6 -5
- data/t/simple-http_RevThreadSpawn.ru +9 -0
- data/t/t0003-reopen-logs.sh +11 -1
- data/t/t0004-heartbeat-timeout.sh +2 -2
- data/t/t0007-worker-follows-master-to-death.sh +50 -0
- data/t/t0008-ensure-usable-after-limit.sh +181 -0
- data/t/t0100-rack-input-hammer.sh +6 -1
- data/t/t0102-rack-input-short.sh +32 -0
- data/t/t9000-rack-app-pool.sh +1 -1
- data/t/test-lib.sh +12 -2
- data/t/worker-follows-master-to-death.ru +17 -0
- metadata +21 -4
data/lib/rainbows/rev.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
|
-
require 'rainbows/rev/
|
3
|
-
require 'rainbows/
|
2
|
+
require 'rainbows/rev/core'
|
3
|
+
require 'rainbows/rev/client'
|
4
|
+
require 'rainbows/rev/deferred_response'
|
4
5
|
|
5
6
|
module Rainbows
|
6
7
|
|
@@ -23,161 +24,6 @@ module Rainbows
|
|
23
24
|
# temporary file before the application is entered.
|
24
25
|
|
25
26
|
module Rev
|
26
|
-
|
27
|
-
include Base
|
28
|
-
|
29
|
-
class Client < ::Rev::IO
|
30
|
-
include Rainbows::EvCore
|
31
|
-
G = Rainbows::G
|
32
|
-
|
33
|
-
def initialize(io)
|
34
|
-
G.cur += 1
|
35
|
-
super(io)
|
36
|
-
post_init
|
37
|
-
@deferred_bodies = [] # for (fast) regular files only
|
38
|
-
end
|
39
|
-
|
40
|
-
# queued, optional response bodies, it should only be unpollable "fast"
|
41
|
-
# devices where read(2) is uninterruptable. Unfortunately, NFS and ilk
|
42
|
-
# are also part of this. We'll also stick DeferredResponse bodies in
|
43
|
-
# here to prevent connections from being closed on us.
|
44
|
-
def defer_body(io)
|
45
|
-
@deferred_bodies << io
|
46
|
-
on_write_complete unless @hp.headers? # triggers a write
|
47
|
-
end
|
48
|
-
|
49
|
-
def app_call
|
50
|
-
begin
|
51
|
-
(@env[RACK_INPUT] = @input).rewind
|
52
|
-
alive = @hp.keepalive?
|
53
|
-
@env[REMOTE_ADDR] = @remote_addr
|
54
|
-
response = G.app.call(@env.update(RACK_DEFAULTS))
|
55
|
-
alive &&= G.alive
|
56
|
-
out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if @hp.headers?
|
57
|
-
|
58
|
-
DeferredResponse.write(self, response, out)
|
59
|
-
if alive
|
60
|
-
@env.clear
|
61
|
-
@hp.reset
|
62
|
-
@state = :headers
|
63
|
-
# keepalive requests are always body-less, so @input is unchanged
|
64
|
-
@hp.headers(@env, @buf) and next
|
65
|
-
else
|
66
|
-
quit
|
67
|
-
end
|
68
|
-
return
|
69
|
-
end while true
|
70
|
-
end
|
71
|
-
|
72
|
-
def on_write_complete
|
73
|
-
if body = @deferred_bodies.first
|
74
|
-
return if DeferredResponse === body
|
75
|
-
begin
|
76
|
-
begin
|
77
|
-
write(body.sysread(CHUNK_SIZE))
|
78
|
-
rescue EOFError # expected at file EOF
|
79
|
-
@deferred_bodies.shift
|
80
|
-
body.close
|
81
|
-
close if :close == @state && @deferred_bodies.empty?
|
82
|
-
end
|
83
|
-
rescue Object => e
|
84
|
-
handle_error(e)
|
85
|
-
end
|
86
|
-
else
|
87
|
-
close if :close == @state
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def on_close
|
92
|
-
G.cur -= 1
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
class Server < ::Rev::IO
|
97
|
-
G = Rainbows::G
|
98
|
-
|
99
|
-
def on_readable
|
100
|
-
return if G.cur >= G.max
|
101
|
-
begin
|
102
|
-
Client.new(@_io.accept_nonblock).attach(::Rev::Loop.default)
|
103
|
-
rescue Errno::EAGAIN, Errno::ECONNABORTED
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
end
|
108
|
-
|
109
|
-
class DeferredResponse < ::Rev::IO
|
110
|
-
include Unicorn
|
111
|
-
include Rainbows::Const
|
112
|
-
G = Rainbows::G
|
113
|
-
|
114
|
-
def self.defer!(client, response, out)
|
115
|
-
body = response.last
|
116
|
-
headers = Rack::Utils::HeaderHash.new(response[1])
|
117
|
-
|
118
|
-
# to_io is not part of the Rack spec, but make an exception
|
119
|
-
# here since we can't get here without checking to_path first
|
120
|
-
io = body.to_io if body.respond_to?(:to_io)
|
121
|
-
io ||= ::IO.new($1.to_i) if body.to_path =~ %r{\A/dev/fd/(\d+)\z}
|
122
|
-
io ||= File.open(body.to_path, 'rb')
|
123
|
-
st = io.stat
|
124
|
-
|
125
|
-
if st.socket? || st.pipe?
|
126
|
-
do_chunk = !!(headers['Transfer-Encoding'] =~ %r{\Achunked\z}i)
|
127
|
-
do_chunk = false if headers.delete('X-Rainbows-Autochunk') == 'no'
|
128
|
-
# too tricky to support keepalive/pipelining when a response can
|
129
|
-
# take an indeterminate amount of time here.
|
130
|
-
if out.nil?
|
131
|
-
do_chunk = false
|
132
|
-
else
|
133
|
-
out[0] = CONN_CLOSE
|
134
|
-
end
|
135
|
-
|
136
|
-
io = new(io, client, do_chunk, body).attach(::Rev::Loop.default)
|
137
|
-
elsif st.file?
|
138
|
-
headers.delete('Transfer-Encoding')
|
139
|
-
headers['Content-Length'] ||= st.size.to_s
|
140
|
-
else # char/block device, directory, whatever... nobody cares
|
141
|
-
return response
|
142
|
-
end
|
143
|
-
client.defer_body(io)
|
144
|
-
[ response.first, headers.to_hash, [] ]
|
145
|
-
end
|
146
|
-
|
147
|
-
def self.write(client, response, out)
|
148
|
-
response.last.respond_to?(:to_path) and
|
149
|
-
response = defer!(client, response, out)
|
150
|
-
HttpResponse.write(client, response, out)
|
151
|
-
end
|
152
|
-
|
153
|
-
def initialize(io, client, do_chunk, body)
|
154
|
-
super(io)
|
155
|
-
@client, @do_chunk, @body = client, do_chunk, body
|
156
|
-
end
|
157
|
-
|
158
|
-
def on_read(data)
|
159
|
-
@do_chunk and @client.write(sprintf("%x\r\n", data.size))
|
160
|
-
@client.write(data)
|
161
|
-
@do_chunk and @client.write("\r\n")
|
162
|
-
end
|
163
|
-
|
164
|
-
def on_close
|
165
|
-
@do_chunk and @client.write("0\r\n\r\n")
|
166
|
-
@client.quit
|
167
|
-
@body.respond_to?(:close) and @body.close
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
# runs inside each forked worker, this sits around and waits
|
172
|
-
# for connections and doesn't die until the parent dies (or is
|
173
|
-
# given a INT, QUIT, or TERM signal)
|
174
|
-
def worker_loop(worker)
|
175
|
-
init_worker_process(worker)
|
176
|
-
rloop = ::Rev::Loop.default
|
177
|
-
Heartbeat.new(worker.tmp).attach(rloop)
|
178
|
-
LISTENERS.map! { |s| Server.new(s).attach(rloop) }
|
179
|
-
rloop.run
|
180
|
-
end
|
181
|
-
|
27
|
+
include Core
|
182
28
|
end
|
183
29
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require 'rainbows/ev_core'
|
3
|
+
module Rainbows
|
4
|
+
module Rev
|
5
|
+
|
6
|
+
class Client < ::Rev::IO
|
7
|
+
include Rainbows::EvCore
|
8
|
+
G = Rainbows::G
|
9
|
+
|
10
|
+
def initialize(io)
|
11
|
+
G.cur += 1
|
12
|
+
super(io)
|
13
|
+
post_init
|
14
|
+
@deferred_bodies = [] # for (fast) regular files only
|
15
|
+
end
|
16
|
+
|
17
|
+
# queued, optional response bodies, it should only be unpollable "fast"
|
18
|
+
# devices where read(2) is uninterruptable. Unfortunately, NFS and ilk
|
19
|
+
# are also part of this. We'll also stick DeferredResponse bodies in
|
20
|
+
# here to prevent connections from being closed on us.
|
21
|
+
def defer_body(io, out_headers)
|
22
|
+
@deferred_bodies << io
|
23
|
+
schedule_write unless out_headers # triggers a write
|
24
|
+
end
|
25
|
+
|
26
|
+
def app_call
|
27
|
+
begin
|
28
|
+
(@env[RACK_INPUT] = @input).rewind
|
29
|
+
@env[REMOTE_ADDR] = @remote_addr
|
30
|
+
response = APP.call(@env.update(RACK_DEFAULTS))
|
31
|
+
alive = @hp.keepalive? && G.alive
|
32
|
+
out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if @hp.headers?
|
33
|
+
|
34
|
+
DeferredResponse.write(self, response, out)
|
35
|
+
if alive
|
36
|
+
@env.clear
|
37
|
+
@hp.reset
|
38
|
+
@state = :headers
|
39
|
+
# keepalive requests are always body-less, so @input is unchanged
|
40
|
+
@hp.headers(@env, @buf) and next
|
41
|
+
else
|
42
|
+
quit
|
43
|
+
end
|
44
|
+
return
|
45
|
+
end while true
|
46
|
+
end
|
47
|
+
|
48
|
+
def on_write_complete
|
49
|
+
if body = @deferred_bodies.first
|
50
|
+
return if DeferredResponse === body
|
51
|
+
begin
|
52
|
+
begin
|
53
|
+
write(body.sysread(CHUNK_SIZE))
|
54
|
+
rescue EOFError # expected at file EOF
|
55
|
+
@deferred_bodies.shift
|
56
|
+
body.close
|
57
|
+
close if :close == @state && @deferred_bodies.empty?
|
58
|
+
end
|
59
|
+
rescue => e
|
60
|
+
handle_error(e)
|
61
|
+
end
|
62
|
+
else
|
63
|
+
close if :close == @state
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def on_close
|
68
|
+
G.cur -= 1
|
69
|
+
end
|
70
|
+
|
71
|
+
end # module Client
|
72
|
+
end # module Rev
|
73
|
+
end # module Rainbows
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require 'rev'
|
3
|
+
Rev::VERSION >= '0.3.0' or abort 'rev >= 0.3.0 is required'
|
4
|
+
require 'rainbows/rev/heartbeat'
|
5
|
+
|
6
|
+
module Rainbows
|
7
|
+
module Rev
|
8
|
+
class Server < ::Rev::IO
|
9
|
+
G = Rainbows::G
|
10
|
+
LOOP = ::Rev::Loop.default
|
11
|
+
# CL and MAX will be defined in the corresponding worker loop
|
12
|
+
|
13
|
+
def on_readable
|
14
|
+
return if G.cur >= MAX
|
15
|
+
begin
|
16
|
+
CL.new(@_io.accept_nonblock).attach(LOOP)
|
17
|
+
rescue Errno::EAGAIN, Errno::ECONNABORTED
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end # class Server
|
21
|
+
|
22
|
+
module Core
|
23
|
+
include Base
|
24
|
+
|
25
|
+
# runs inside each forked worker, this sits around and waits
|
26
|
+
# for connections and doesn't die until the parent dies (or is
|
27
|
+
# given a INT, QUIT, or TERM signal)
|
28
|
+
def worker_loop(worker)
|
29
|
+
init_worker_process(worker)
|
30
|
+
mod = self.class.const_get(@use)
|
31
|
+
Server.const_set(:MAX, @worker_connections)
|
32
|
+
Server.const_set(:CL, mod.const_get(:Client))
|
33
|
+
EvCore.setup(EvCore)
|
34
|
+
rloop = ::Rev::Loop.default
|
35
|
+
Heartbeat.new(1, true).attach(rloop)
|
36
|
+
LISTENERS.map! { |s| Server.new(s).attach(rloop) }
|
37
|
+
rloop.run
|
38
|
+
end
|
39
|
+
|
40
|
+
end # module Core
|
41
|
+
end # module Rev
|
42
|
+
end # module Rainbows
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
module Rainbows
|
3
|
+
module Rev
|
4
|
+
|
5
|
+
# this is class is specific to Rev for writing large static files
|
6
|
+
# or proxying IO-derived objects
|
7
|
+
class DeferredResponse < ::Rev::IO
|
8
|
+
include Unicorn
|
9
|
+
include Rainbows::Const
|
10
|
+
G = Rainbows::G
|
11
|
+
HH = Rack::Utils::HeaderHash
|
12
|
+
|
13
|
+
# we only want to attach to the Rev::Loop belonging to the
|
14
|
+
# main thread in Ruby 1.9
|
15
|
+
LOOP = ::Rev::Loop.default
|
16
|
+
|
17
|
+
def self.defer!(client, response, out)
|
18
|
+
body = response.last
|
19
|
+
headers = HH.new(response[1])
|
20
|
+
|
21
|
+
# to_io is not part of the Rack spec, but make an exception
|
22
|
+
# here since we can't get here without checking to_path first
|
23
|
+
io = body.to_io if body.respond_to?(:to_io)
|
24
|
+
io ||= ::IO.new($1.to_i) if body.to_path =~ %r{\A/dev/fd/(\d+)\z}
|
25
|
+
io ||= File.open(body.to_path, 'rb')
|
26
|
+
st = io.stat
|
27
|
+
|
28
|
+
if st.socket? || st.pipe?
|
29
|
+
do_chunk = !!(headers['Transfer-Encoding'] =~ %r{\Achunked\z}i)
|
30
|
+
do_chunk = false if headers.delete('X-Rainbows-Autochunk') == 'no'
|
31
|
+
# too tricky to support keepalive/pipelining when a response can
|
32
|
+
# take an indeterminate amount of time here.
|
33
|
+
if out.nil?
|
34
|
+
do_chunk = false
|
35
|
+
else
|
36
|
+
out[0] = CONN_CLOSE
|
37
|
+
end
|
38
|
+
|
39
|
+
io = new(io, client, do_chunk, body).attach(LOOP)
|
40
|
+
elsif st.file?
|
41
|
+
headers.delete('Transfer-Encoding')
|
42
|
+
headers['Content-Length'] ||= st.size.to_s
|
43
|
+
else # char/block device, directory, whatever... nobody cares
|
44
|
+
return response
|
45
|
+
end
|
46
|
+
client.defer_body(io, out)
|
47
|
+
[ response.first, headers.to_hash, [] ]
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.write(client, response, out)
|
51
|
+
response.last.respond_to?(:to_path) and
|
52
|
+
response = defer!(client, response, out)
|
53
|
+
HttpResponse.write(client, response, out)
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize(io, client, do_chunk, body)
|
57
|
+
super(io)
|
58
|
+
@client, @do_chunk, @body = client, do_chunk, body
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_read(data)
|
62
|
+
@do_chunk and @client.write(sprintf("%x\r\n", data.size))
|
63
|
+
@client.write(data)
|
64
|
+
@do_chunk and @client.write("\r\n")
|
65
|
+
end
|
66
|
+
|
67
|
+
def on_close
|
68
|
+
@do_chunk and @client.write("0\r\n\r\n")
|
69
|
+
@client.quit
|
70
|
+
@body.respond_to?(:close) and @body.close
|
71
|
+
end
|
72
|
+
end # class DeferredResponse
|
73
|
+
end # module Rev
|
74
|
+
end # module Rainbows
|
@@ -1,7 +1,4 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
|
-
require 'rev'
|
3
|
-
Rev::VERSION >= '0.3.0' or abort 'rev >= 0.3.0 is required'
|
4
|
-
|
5
2
|
module Rainbows
|
6
3
|
module Rev
|
7
4
|
|
@@ -11,15 +8,9 @@ module Rainbows
|
|
11
8
|
# will also detect and execute the graceful exit if triggered
|
12
9
|
# by SIGQUIT
|
13
10
|
class Heartbeat < ::Rev::TimerWatcher
|
14
|
-
# +tmp+ must be a +File+ that responds to +chmod+
|
15
|
-
def initialize(tmp)
|
16
|
-
@m, @tmp = 0, tmp
|
17
|
-
super(1, true)
|
18
|
-
end
|
19
11
|
|
20
12
|
def on_timer
|
21
|
-
|
22
|
-
exit if (! G.alive && G.cur <= 0)
|
13
|
+
exit if (! G.tick && G.cur <= 0)
|
23
14
|
end
|
24
15
|
|
25
16
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require 'rainbows/rev'
|
3
|
+
require 'rainbows/ev_thread_core'
|
4
|
+
|
5
|
+
warn "Rainbows::RevThreadSpawn is extremely experimental"
|
6
|
+
|
7
|
+
module Rainbows
|
8
|
+
|
9
|
+
# This concurrency model is EXTREMELY experimental and does
|
10
|
+
# not perform very well.
|
11
|
+
#
|
12
|
+
# A combination of the Rev and ThreadSpawn models. This allows Ruby
|
13
|
+
# 1.8 and 1.9 to effectively serve more than ~1024 concurrent clients
|
14
|
+
# on systems that support kqueue or epoll while still using
|
15
|
+
# Thread-based concurrency for application processing. It exposes
|
16
|
+
# Unicorn::TeeInput for a streamable "rack.input" for upload
|
17
|
+
# processing within the app. Threads are spawned immediately after
|
18
|
+
# header processing is done for calling the application. Rack
|
19
|
+
# applications running under this mode should be thread-safe.
|
20
|
+
# DevFdResponse should be used with this class to proxy asynchronous
|
21
|
+
# responses. All network I/O between the client and server are
|
22
|
+
# handled by the main thread (even when streaming "rack.input").
|
23
|
+
#
|
24
|
+
# Caveats:
|
25
|
+
#
|
26
|
+
# * TeeInput performance under Ruby 1.8 is terrible unless you
|
27
|
+
# match the length argument of your env["rack.input"]#read
|
28
|
+
# calls so that it is greater than or equal to Rev::IO::INPUT_SIZE.
|
29
|
+
# Most applications depending on Rack to do multipart POST
|
30
|
+
# processing should be alright as the current Rev::IO::INPUT_SIZE
|
31
|
+
# of 16384 bytes matches the read size used by
|
32
|
+
# Rack::Utils::Multipart::parse_multipart.
|
33
|
+
|
34
|
+
module RevThreadSpawn
|
35
|
+
class Client < Rainbows::Rev::Client
|
36
|
+
include EvThreadCore
|
37
|
+
LOOP = ::Rev::Loop.default
|
38
|
+
DR = Rainbows::Rev::DeferredResponse
|
39
|
+
TEE_RESUMER = ::Rev::AsyncWatcher.new
|
40
|
+
|
41
|
+
def pause
|
42
|
+
@lock.synchronize { disable if enabled? }
|
43
|
+
end
|
44
|
+
|
45
|
+
def resume
|
46
|
+
@lock.synchronize { enable unless enabled? }
|
47
|
+
TEE_RESUMER.signal
|
48
|
+
end
|
49
|
+
|
50
|
+
def write(data)
|
51
|
+
if Thread.current != @thread && @lock.locked?
|
52
|
+
# we're being called inside on_writable
|
53
|
+
super
|
54
|
+
else
|
55
|
+
@lock.synchronize { super }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def defer_body(io, out_headers)
|
60
|
+
@lock.synchronize { super }
|
61
|
+
end
|
62
|
+
|
63
|
+
def response_write(response, out)
|
64
|
+
DR.write(self, response, out)
|
65
|
+
(out && CONN_ALIVE == out.first) or
|
66
|
+
@lock.synchronize {
|
67
|
+
quit
|
68
|
+
schedule_write
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def on_writable
|
73
|
+
# don't ever want to block in the main loop with lots of clients,
|
74
|
+
# libev is level-triggered so we'll always get another chance later
|
75
|
+
if @lock.try_lock
|
76
|
+
begin
|
77
|
+
super
|
78
|
+
ensure
|
79
|
+
@lock.unlock
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
include Rainbows::Rev::Core
|
87
|
+
|
88
|
+
def init_worker_process(worker)
|
89
|
+
super
|
90
|
+
Client::TEE_RESUMER.attach(::Rev::Loop.default)
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|