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/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
|