rainbows 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/FAQ
CHANGED
@@ -48,3 +48,42 @@ It depends on the size and amount of static files you're serving. If
|
|
48
48
|
you're serving a lot of static files (especially large ones), then by
|
49
49
|
all means use nginx. If not, then \Rainbows! is likely a "good enough"
|
50
50
|
solution even if nginx will always outperform it in raw throughput.
|
51
|
+
|
52
|
+
|
53
|
+
=== How do I support SSL?
|
54
|
+
|
55
|
+
If you need a streaming "rack.input" to do upload processing within your
|
56
|
+
Rack application, then {stunnel}[http://stunnel.org/] is required.
|
57
|
+
Otherwise, nginx is a perfectly good reverse proxy.
|
58
|
+
|
59
|
+
Refer to the {Unicorn FAQ}[http://unicorn.bogomips.org/FAQ.html] on how
|
60
|
+
to ensure redirects go to "https://" URLs.
|
61
|
+
|
62
|
+
|
63
|
+
=== Is there a "rainbows_rails" command like there is "unicorn_rails"?
|
64
|
+
|
65
|
+
Only if you write one and plan to support it.
|
66
|
+
|
67
|
+
"unicorn_rails" was written primarily to support older versions of
|
68
|
+
Rails. Since \Rainbows! is designed for newer Rails, it can just use
|
69
|
+
a "config.ru" file like other Rack frameworks and applications.
|
70
|
+
|
71
|
+
For Rails 2.3.x and later, the following config.ru will work for you:
|
72
|
+
|
73
|
+
ENV["RAILS_ENV"] ||= ENV["RACK_ENV"]
|
74
|
+
require "config/environment"
|
75
|
+
use Rails::Rack::Static
|
76
|
+
run ActionController::Dispatcher.new
|
77
|
+
|
78
|
+
For older versions of Rails, the following config.ru will work:
|
79
|
+
|
80
|
+
ENV["RAILS_ENV"] ||= ENV["RACK_ENV"]
|
81
|
+
require 'config/boot'
|
82
|
+
require 'config/environment'
|
83
|
+
require 'unicorn/app/old_rails'
|
84
|
+
require 'unicorn/app/old_rails/static' # not needed with Unicorn 0.95+
|
85
|
+
use Unicorn::App::OldRails::Static
|
86
|
+
run Unicorn::App::OldRails.new
|
87
|
+
|
88
|
+
One thing to watch out for is that RAILS_ENV will not be set in the
|
89
|
+
environment for you, thus we set it to match RACK_ENV.
|
data/GIT-VERSION-GEN
CHANGED
data/GNUmakefile
CHANGED
@@ -64,7 +64,7 @@ NEWS: GIT-VERSION-FILE
|
|
64
64
|
$(rake) -s news_rdoc > $@+
|
65
65
|
mv $@+ $@
|
66
66
|
|
67
|
-
SINCE = 0.
|
67
|
+
SINCE = 0.5.0
|
68
68
|
ChangeLog: log_range = $(shell test -n "$(SINCE)" && echo v$(SINCE)..)
|
69
69
|
ChangeLog: GIT-VERSION-FILE
|
70
70
|
@echo "ChangeLog from $(GIT_URL) ($(SINCE)..$(GIT_VERSION))" > $@+
|
data/Rakefile
CHANGED
@@ -24,6 +24,7 @@ def tags
|
|
24
24
|
end
|
25
25
|
|
26
26
|
cgit_url = "http://git.bogomips.org/cgit/rainbows.git"
|
27
|
+
git_url = ENV['GIT_URL'] || 'git://git.bogomips.org/rainbows.git'
|
27
28
|
|
28
29
|
desc 'prints news as an Atom feed'
|
29
30
|
task :news_atom do
|
@@ -88,8 +89,6 @@ desc "print release notes for Rubyforge"
|
|
88
89
|
task :release_notes do
|
89
90
|
require 'rubygems'
|
90
91
|
|
91
|
-
git_url = ENV['GIT_URL'] || 'git://git.bogomips.org/rainbows.git'
|
92
|
-
|
93
92
|
spec = Gem::Specification.load('rainbows.gemspec')
|
94
93
|
puts spec.description.strip
|
95
94
|
puts ""
|
@@ -117,3 +116,41 @@ task :publish_news do
|
|
117
116
|
rf.login
|
118
117
|
rf.post_news('rainbows', subject, body)
|
119
118
|
end
|
119
|
+
|
120
|
+
desc "post to RAA"
|
121
|
+
task :raa_update do
|
122
|
+
require 'rubygems'
|
123
|
+
require 'net/http'
|
124
|
+
require 'net/netrc'
|
125
|
+
rc = Net::Netrc.locate('rainbows-raa') or abort "~/.netrc not found"
|
126
|
+
password = rc.password
|
127
|
+
|
128
|
+
s = Gem::Specification.load('rainbows.gemspec')
|
129
|
+
desc = [ s.description.strip ]
|
130
|
+
desc << ""
|
131
|
+
desc << "* #{s.email}"
|
132
|
+
desc << "* #{git_url}"
|
133
|
+
desc << "* #{cgit_url}"
|
134
|
+
desc = desc.join("\n")
|
135
|
+
uri = URI.parse('http://raa.ruby-lang.org/regist.rhtml')
|
136
|
+
form = {
|
137
|
+
:name => s.name,
|
138
|
+
:short_description => s.summary,
|
139
|
+
:version => s.version.to_s,
|
140
|
+
:status => 'experimental',
|
141
|
+
:owner => s.authors.first,
|
142
|
+
:email => s.email,
|
143
|
+
:category_major => 'Library',
|
144
|
+
:category_minor => 'Web',
|
145
|
+
:url => s.homepage,
|
146
|
+
:download => "http://rubyforge.org/frs/?group_id=8977",
|
147
|
+
:license => "Ruby's",
|
148
|
+
:description_style => 'Plain',
|
149
|
+
:description => desc,
|
150
|
+
:pass => password,
|
151
|
+
:submit => "Update",
|
152
|
+
}
|
153
|
+
res = Net::HTTP.post_form(uri, form)
|
154
|
+
p res
|
155
|
+
puts res.body
|
156
|
+
end
|
data/lib/rainbows.rb
CHANGED
@@ -5,9 +5,20 @@ module Rainbows
|
|
5
5
|
|
6
6
|
# global vars because class/instance variables are confusing me :<
|
7
7
|
# this struct is only accessed inside workers and thus private to each
|
8
|
-
G = Struct.new(:cur, :max, :logger, :alive, :app).new
|
9
8
|
# G.cur may not be used the network concurrency model
|
10
|
-
|
9
|
+
class State < Struct.new(:alive,:m,:cur,:server,:tmp)
|
10
|
+
def tick
|
11
|
+
tmp.chmod(self.m = m == 0 ? 1 : 0)
|
12
|
+
alive && server.master_pid == Process.ppid or quit!
|
13
|
+
end
|
14
|
+
|
15
|
+
def quit!
|
16
|
+
self.alive = false
|
17
|
+
server.class.const_get(:LISTENERS).map! { |s| s.close rescue nil }
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
G = State.new(true, 0, 0)
|
11
22
|
|
12
23
|
require 'rainbows/const'
|
13
24
|
require 'rainbows/http_server'
|
@@ -56,6 +67,7 @@ module Rainbows
|
|
56
67
|
:ThreadSpawn => 30,
|
57
68
|
:ThreadPool => 10,
|
58
69
|
:Rev => 50,
|
70
|
+
:RevThreadSpawn => 50,
|
59
71
|
:EventMachine => 50,
|
60
72
|
}.each do |model, _|
|
61
73
|
u = model.to_s.gsub(/([a-z0-9])([A-Z0-9])/) { "#{$1}_#{$2.downcase!}" }
|
data/lib/rainbows/base.rb
CHANGED
@@ -10,14 +10,6 @@ module Rainbows
|
|
10
10
|
include Rainbows::Const
|
11
11
|
G = Rainbows::G
|
12
12
|
|
13
|
-
# write a response without caring if it went out or not for error
|
14
|
-
# messages.
|
15
|
-
# TODO: merge into Unicorn::HttpServer
|
16
|
-
def emergency_response(client, response_str)
|
17
|
-
client.write_nonblock(response_str) rescue nil
|
18
|
-
client.close rescue nil
|
19
|
-
end
|
20
|
-
|
21
13
|
def listen_loop_error(e)
|
22
14
|
G.alive or return
|
23
15
|
logger.error "Unhandled listen loop exception #{e.inspect}."
|
@@ -26,20 +18,13 @@ module Rainbows
|
|
26
18
|
|
27
19
|
def init_worker_process(worker)
|
28
20
|
super(worker)
|
29
|
-
G.
|
30
|
-
G.max = worker_connections
|
31
|
-
G.logger = logger
|
32
|
-
G.app = app
|
21
|
+
G.tmp = worker.tmp
|
33
22
|
|
34
23
|
# we're don't use the self-pipe mechanism in the Rainbows! worker
|
35
24
|
# since we don't defer reopening logs
|
36
25
|
HttpServer::SELF_PIPE.each { |x| x.close }.clear
|
37
|
-
trap(:USR1) { reopen_worker_logs(worker.nr)
|
38
|
-
trap(:QUIT)
|
39
|
-
G.alive = false
|
40
|
-
# closing anything we IO.select on will raise EBADF
|
41
|
-
HttpServer::LISTENERS.map! { |s| s.close rescue nil }
|
42
|
-
end
|
26
|
+
trap(:USR1) { reopen_worker_logs(worker.nr) }
|
27
|
+
trap(:QUIT) { G.quit! }
|
43
28
|
[:TERM, :INT].each { |sig| trap(sig) { exit!(0) } } # instant shutdown
|
44
29
|
logger.info "Rainbows! #@use worker_connections=#@worker_connections"
|
45
30
|
end
|
@@ -79,23 +64,16 @@ module Rainbows
|
|
79
64
|
# assuming we haven't closed the socket, but don't get hung up
|
80
65
|
# if the socket is already closed or broken. We'll always ensure
|
81
66
|
# the socket is closed at the end of this function
|
82
|
-
rescue
|
83
|
-
|
84
|
-
rescue HttpParserError # try to tell the client they're bad
|
85
|
-
buf.empty? or emergency_response(client, ERROR_400_RESPONSE)
|
86
|
-
rescue Object => e
|
87
|
-
emergency_response(client, ERROR_500_RESPONSE)
|
88
|
-
logger.error "Read error: #{e.inspect}"
|
89
|
-
logger.error e.backtrace.join("\n")
|
67
|
+
rescue => e
|
68
|
+
handle_error(client, e)
|
90
69
|
end
|
91
70
|
|
92
|
-
def join_threads(threads
|
93
|
-
|
71
|
+
def join_threads(threads)
|
72
|
+
G.quit!
|
94
73
|
expire = Time.now + (timeout * 2.0)
|
95
|
-
m = 0
|
96
74
|
until (threads.delete_if { |thr| ! thr.alive? }).empty?
|
97
75
|
threads.each { |thr|
|
98
|
-
|
76
|
+
G.tick
|
99
77
|
thr.join(1)
|
100
78
|
break if Time.now >= expire
|
101
79
|
}
|
data/lib/rainbows/const.rb
CHANGED
data/lib/rainbows/ev_core.rb
CHANGED
@@ -8,6 +8,10 @@ module Rainbows
|
|
8
8
|
include Rainbows::Const
|
9
9
|
G = Rainbows::G
|
10
10
|
|
11
|
+
def self.setup(klass)
|
12
|
+
klass.const_set(:APP, G.server.app)
|
13
|
+
end
|
14
|
+
|
11
15
|
def post_init
|
12
16
|
@remote_addr = ::TCPSocket === @_io ? @_io.peeraddr.last : LOCALHOST
|
13
17
|
@env = {}
|
@@ -28,8 +32,8 @@ module Rainbows
|
|
28
32
|
when HttpParserError # try to tell the client they're bad
|
29
33
|
ERROR_400_RESPONSE
|
30
34
|
else
|
31
|
-
G.logger.error "Read error: #{e.inspect}"
|
32
|
-
G.logger.error e.backtrace.join("\n")
|
35
|
+
G.server.logger.error "Read error: #{e.inspect}"
|
36
|
+
G.server.logger.error e.backtrace.join("\n")
|
33
37
|
ERROR_500_RESPONSE
|
34
38
|
end
|
35
39
|
write(msg)
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require 'thread' # for Queue
|
3
|
+
require 'rainbows/ev_core'
|
4
|
+
|
5
|
+
module Rainbows
|
6
|
+
|
7
|
+
# base module for mixed Thread + evented models like RevThreadSpawn
|
8
|
+
module EvThreadCore
|
9
|
+
include EvCore
|
10
|
+
|
11
|
+
def post_init
|
12
|
+
super
|
13
|
+
@lock = Mutex.new
|
14
|
+
@thread = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# we pass ourselves off as a Socket to Unicorn::TeeInput and this
|
18
|
+
# is the only method Unicorn::TeeInput requires from the socket
|
19
|
+
def readpartial(length, buf = "")
|
20
|
+
# we must modify the original buffer if there was one
|
21
|
+
length == 0 and return buf.replace("")
|
22
|
+
|
23
|
+
# wait on the main loop to feed us
|
24
|
+
while @tbuf.size == 0
|
25
|
+
@tbuf.write(@state.pop)
|
26
|
+
resume
|
27
|
+
end
|
28
|
+
buf.replace(@tbuf.read(length))
|
29
|
+
end
|
30
|
+
|
31
|
+
def app_spawn(input)
|
32
|
+
begin
|
33
|
+
@thread.nil? or @thread.join # only one thread per connection
|
34
|
+
env = @env.dup
|
35
|
+
alive, headers = @hp.keepalive?, @hp.headers?
|
36
|
+
@thread = Thread.new(self) do |client|
|
37
|
+
begin
|
38
|
+
env[REMOTE_ADDR] = @remote_addr
|
39
|
+
env[RACK_INPUT] = input || TeeInput.new(client, env, @hp, @buf)
|
40
|
+
response = APP.call(env.update(RACK_DEFAULTS))
|
41
|
+
if 100 == response.first.to_i
|
42
|
+
write(EXPECT_100_RESPONSE)
|
43
|
+
env.delete(HTTP_EXPECT)
|
44
|
+
response = APP.call(env)
|
45
|
+
end
|
46
|
+
|
47
|
+
alive &&= G.alive
|
48
|
+
out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if headers
|
49
|
+
response_write(response, out)
|
50
|
+
rescue => e
|
51
|
+
handle_error(e) rescue nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
if alive # in case we pipeline
|
55
|
+
@hp.reset
|
56
|
+
redo if @hp.headers(@env.clear, @buf)
|
57
|
+
end
|
58
|
+
end while false
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_read(data)
|
62
|
+
case @state
|
63
|
+
when :headers
|
64
|
+
@hp.headers(@env, @buf << data) or return
|
65
|
+
if 0 == @hp.content_length
|
66
|
+
app_spawn(HttpRequest::NULL_IO) # common case
|
67
|
+
else # nil or len > 0
|
68
|
+
@state, @tbuf = Queue.new, ::IO::Buffer.new
|
69
|
+
app_spawn(nil)
|
70
|
+
end
|
71
|
+
when Queue
|
72
|
+
pause
|
73
|
+
@state << data
|
74
|
+
end
|
75
|
+
rescue => e
|
76
|
+
handle_error(e)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -53,18 +53,17 @@ module Rainbows
|
|
53
53
|
def app_call
|
54
54
|
begin
|
55
55
|
(@env[RACK_INPUT] = @input).rewind
|
56
|
-
alive = @hp.keepalive?
|
57
56
|
@env[REMOTE_ADDR] = @remote_addr
|
58
57
|
@env[ASYNC_CALLBACK] = method(:response_write)
|
59
58
|
|
60
|
-
response = catch(:async) {
|
59
|
+
response = catch(:async) { APP.call(@env.update(RACK_DEFAULTS)) }
|
61
60
|
|
62
61
|
# too tricky to support pipelining with :async since the
|
63
62
|
# second (pipelined) request could be a stuck behind a
|
64
63
|
# long-running async response
|
65
64
|
(response.nil? || -1 == response.first) and return @state = :close
|
66
65
|
|
67
|
-
alive
|
66
|
+
alive = @hp.keepalive? && G.alive
|
68
67
|
out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if @hp.headers?
|
69
68
|
response_write(response, out, alive)
|
70
69
|
|
@@ -166,22 +165,17 @@ module Rainbows
|
|
166
165
|
|
167
166
|
module Server
|
168
167
|
|
169
|
-
def initialize(conns)
|
170
|
-
@limit = Rainbows::G.max + HttpServer::LISTENERS.size
|
171
|
-
@em_conns = conns
|
172
|
-
end
|
173
|
-
|
174
168
|
def close
|
175
169
|
detach
|
176
170
|
@io.close
|
177
171
|
end
|
178
172
|
|
179
173
|
def notify_readable
|
180
|
-
return if
|
174
|
+
return if CUR.size >= MAX
|
181
175
|
begin
|
182
176
|
io = @io.accept_nonblock
|
183
177
|
sig = EM.attach_fd(io.fileno, false)
|
184
|
-
|
178
|
+
CUR[sig] = Client.new(sig, io)
|
185
179
|
rescue Errno::EAGAIN, Errno::ECONNABORTED
|
186
180
|
end
|
187
181
|
end
|
@@ -192,24 +186,26 @@ module Rainbows
|
|
192
186
|
# given a INT, QUIT, or TERM signal)
|
193
187
|
def worker_loop(worker)
|
194
188
|
init_worker_process(worker)
|
195
|
-
m = 0
|
196
189
|
|
197
190
|
# enable them both, should be non-fatal if not supported
|
198
191
|
EM.epoll
|
199
192
|
EM.kqueue
|
200
193
|
logger.info "EventMachine: epoll=#{EM.epoll?} kqueue=#{EM.kqueue?}"
|
194
|
+
Server.const_set(:MAX, G.server.worker_connections +
|
195
|
+
HttpServer::LISTENERS.size)
|
196
|
+
EvCore.setup(Client)
|
201
197
|
EM.run {
|
202
198
|
conns = EM.instance_variable_get(:@conns) or
|
203
199
|
raise RuntimeError, "EM @conns instance variable not accessible!"
|
200
|
+
Server.const_set(:CUR, conns)
|
204
201
|
EM.add_periodic_timer(1) do
|
205
|
-
|
206
|
-
unless G.alive
|
202
|
+
unless G.tick
|
207
203
|
conns.each_value { |client| Client === client and client.quit }
|
208
204
|
EM.stop if conns.empty? && EM.reactor_running?
|
209
205
|
end
|
210
206
|
end
|
211
207
|
LISTENERS.map! do |s|
|
212
|
-
EM.watch(s, Server
|
208
|
+
EM.watch(s, Server) { |c| c.notify_readable = true }
|
213
209
|
end
|
214
210
|
}
|
215
211
|
end
|
data/lib/rainbows/http_server.rb
CHANGED
@@ -5,21 +5,27 @@ module Rainbows
|
|
5
5
|
class HttpServer < ::Unicorn::HttpServer
|
6
6
|
include Rainbows
|
7
7
|
|
8
|
-
@@instance = nil
|
9
|
-
|
10
8
|
class << self
|
11
9
|
def setup(block)
|
12
|
-
|
10
|
+
G.server.instance_eval(&block)
|
13
11
|
end
|
14
12
|
end
|
15
13
|
|
16
14
|
def initialize(app, options)
|
17
|
-
|
15
|
+
G.server = self
|
18
16
|
rv = super(app, options)
|
19
17
|
defined?(@use) or use(:Base)
|
20
18
|
@worker_connections ||= MODEL_WORKER_CONNECTIONS[@use]
|
21
19
|
end
|
22
20
|
|
21
|
+
def reopen_worker_logs(worker_nr)
|
22
|
+
logger.info "worker=#{worker_nr} reopening logs..."
|
23
|
+
Unicorn::Util.reopen_logs
|
24
|
+
logger.info "worker=#{worker_nr} done reopening logs"
|
25
|
+
rescue
|
26
|
+
G.quit! # let the master reopen and refork us
|
27
|
+
end
|
28
|
+
|
23
29
|
#:stopdoc:
|
24
30
|
#
|
25
31
|
# Add one second to the timeout since our fchmod heartbeat is less
|