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/.document
CHANGED
data/DEPLOY
CHANGED
@@ -27,3 +27,16 @@ processing of the request body as it is being uploaded.
|
|
27
27
|
|
28
28
|
In this case, haproxy or any similar (non-request-body-buffering) load
|
29
29
|
balancer should be used to balance requests between different machines.
|
30
|
+
|
31
|
+
== Denial-of-Service Concerns
|
32
|
+
|
33
|
+
Since \Rainbows! is designed to talk to slow clients with long-held
|
34
|
+
connections, it may be subject to brute force denial-of-service attacks.
|
35
|
+
In Unicorn and Mongrel, we've already enabled the "httpready" accept
|
36
|
+
filter for FreeBSD and the TCP_DEFER_ACCEPT option in Linux; but it is
|
37
|
+
still possible to build clients that work around and fool these
|
38
|
+
mechanisms.
|
39
|
+
|
40
|
+
\Rainbows! itself does not feature any explicit protection against brute
|
41
|
+
force denial-of-service attacks. We believe this is best handled by
|
42
|
+
dedicated firewalls provided by the operating system.
|
data/GIT-VERSION-GEN
CHANGED
data/GNUmakefile
CHANGED
@@ -58,7 +58,7 @@ NEWS: GIT-VERSION-FILE
|
|
58
58
|
$(rake) -s news_rdoc > $@+
|
59
59
|
mv $@+ $@
|
60
60
|
|
61
|
-
SINCE =
|
61
|
+
SINCE = 0.1.1
|
62
62
|
ChangeLog: log_range = $(shell test -n "$(SINCE)" && echo v$(SINCE)..)
|
63
63
|
ChangeLog: GIT-VERSION-FILE
|
64
64
|
@echo "ChangeLog from $(GIT_URL) ($(SINCE)..$(GIT_VERSION))" > $@+
|
data/README
CHANGED
@@ -16,11 +16,21 @@ For network concurrency, models we currently support are:
|
|
16
16
|
* {:ThreadSpawn}[link:Rainbows/ThreadSpawn.html]
|
17
17
|
* {:ThreadPool}[link:Rainbows/ThreadPool.html]
|
18
18
|
* {:Revactor}[link:Rainbows/Revactor.html]
|
19
|
+
* {:Rev}[link:Rainbows/Rev.html]*
|
19
20
|
|
20
21
|
We have {more on the way}[link:TODO.html] for handling network concurrency.
|
21
22
|
Additionally, we also use multiple processes (managed by Unicorn) for
|
22
23
|
CPU/memory/disk concurrency.
|
23
24
|
|
25
|
+
\* our \Rev concurrency model is only recommended for slow clients, not
|
26
|
+
sleepy apps. Additionally it does not support streaming "rack.input" to
|
27
|
+
the application.
|
28
|
+
|
29
|
+
For application concurrency, we have the Rainbows::AppPool Rack
|
30
|
+
middleware that allows us to limit application concurrency independently
|
31
|
+
of network concurrency. Rack::Lock as distributed by Rack may also be
|
32
|
+
used to limit application concurrency to one (per-worker).
|
33
|
+
|
24
34
|
== Features
|
25
35
|
|
26
36
|
* Designed for {Rack}[http://rack.rubyforge.org/], the standard for
|
@@ -54,6 +64,9 @@ CPU/memory/disk concurrency.
|
|
54
64
|
* Long polling
|
55
65
|
* Reverse Ajax
|
56
66
|
|
67
|
+
\Rainbows may also be used to service slow clients even with fast
|
68
|
+
applications using the \Rev concurrency model.
|
69
|
+
|
57
70
|
== License
|
58
71
|
|
59
72
|
\Rainbows! is copyright 2009 by all contributors (see logs in git).
|
@@ -99,13 +112,25 @@ config file:
|
|
99
112
|
|
100
113
|
Rainbows! do
|
101
114
|
use :Revactor
|
102
|
-
worker_connections
|
115
|
+
worker_connections 400
|
103
116
|
end
|
104
117
|
|
118
|
+
See the {Rainbows! configuration documentation}[link:Rainbows.html#M000001]
|
119
|
+
for more details.
|
120
|
+
|
105
121
|
== Development
|
106
122
|
|
107
|
-
|
108
|
-
|
123
|
+
You can get the latest source via git from the following locations
|
124
|
+
(these versions may not be stable):
|
125
|
+
|
126
|
+
git://git.bogomips.org/rainbows.git
|
127
|
+
git://rubyforge.org/rainbows.git (mirror)
|
128
|
+
|
129
|
+
You may browse the code from the web and download the latest snapshot
|
130
|
+
tarballs here:
|
131
|
+
|
132
|
+
* http://git.bogomips.org/cgit/rainbows.git (cgit)
|
133
|
+
* http://rainbows.rubyforge.org/git?p=rainbows.git (gitweb)
|
109
134
|
|
110
135
|
Inline patches (from "git format-patch") to the mailing list are
|
111
136
|
preferred because they allow code review and comments in the reply to
|
@@ -126,9 +151,10 @@ and we'll try our best to fix it.
|
|
126
151
|
All feedback (bug reports, user/development dicussion, patches, pull
|
127
152
|
requests) go to the mailing list/newsgroup. Patches must be sent inline
|
128
153
|
(git format-patch -M + git send-email). No subscription is necessary
|
129
|
-
to post on the mailing list. No top posting. Address replies +To:+
|
130
|
-
|
154
|
+
to post on the mailing list. No top posting. Address replies +To:+
|
155
|
+
the mailing list.
|
131
156
|
|
132
157
|
* email: mailto:rainbows-talk@rubyforge.org
|
133
|
-
*
|
158
|
+
* nntp: nntp://news.gmane.org/gmane.comp.lang.ruby.rainbows.general
|
134
159
|
* subscribe: http://rubyforge.org/mailman/listinfo/rainbows-talk
|
160
|
+
* archives: http://rubyforge.org/pipermail/rainbows-talk
|
data/SIGNALS
CHANGED
@@ -2,7 +2,10 @@
|
|
2
2
|
|
3
3
|
In general, signals need only be sent to the master process. However,
|
4
4
|
the signals Rainbows! uses internally to communicate with the worker
|
5
|
-
processes are documented here as well.
|
5
|
+
processes are documented here as well. With the exception of TTIN/TTOU,
|
6
|
+
signal handling matches the behavior of and {nginx}[http://nginx.net/]
|
7
|
+
so it should be possible to easily share process management scripts
|
8
|
+
between \Rainbows!, Unicorn and nginx.
|
6
9
|
|
7
10
|
=== Master Process
|
8
11
|
|
@@ -43,16 +46,17 @@ automatically respawned.
|
|
43
46
|
|
44
47
|
* USR1 - Reopen all logs owned by the worker process.
|
45
48
|
See Unicorn::Util.reopen_logs for what is considered a log.
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
+
Unlike Unicorn, log files are reopened immediately in \Rainbows!
|
50
|
+
since worker processes are likely to be serving multiple clients
|
51
|
+
simutaneously, we can't wait for all of them to finish.
|
49
52
|
|
50
53
|
=== Procedure to replace a running rainbows executable
|
51
54
|
|
52
|
-
You may replace a running instance of
|
55
|
+
You may replace a running instance of rainbows with a new one without
|
53
56
|
losing any incoming connections. Doing so will reload all of your
|
54
|
-
application code, Unicorn config, Ruby executable, and all
|
55
|
-
The only things that will not change (due to OS limitations)
|
57
|
+
application code, Unicorn/Rainbows! config, Ruby executable, and all
|
58
|
+
libraries. The only things that will not change (due to OS limitations)
|
59
|
+
are:
|
56
60
|
|
57
61
|
1. The path to the rainbows executable script. If you want to change to
|
58
62
|
a different installation of Ruby, you can modify the shebang
|
data/TODO
CHANGED
@@ -3,9 +3,8 @@
|
|
3
3
|
We're lazy and pick the easy items to do first, then the ones people
|
4
4
|
care about.
|
5
5
|
|
6
|
-
* Rev
|
7
|
-
|
8
|
-
is probably going to get ugly, though...
|
6
|
+
* Rev + Thread - current Rev model with threading, which will give
|
7
|
+
us a streaming (but rewindable) "rack.input".
|
9
8
|
|
10
9
|
* EventMachine - much like Rev, but we haven't looked at this one much
|
11
10
|
(our benevolent dictator doesn't like C++). If we can figure out how
|
data/lib/rainbows.rb
CHANGED
@@ -7,6 +7,7 @@ module Rainbows
|
|
7
7
|
require 'rainbows/http_server'
|
8
8
|
require 'rainbows/http_response'
|
9
9
|
require 'rainbows/base'
|
10
|
+
autoload :AppPool, 'rainbows/app_pool'
|
10
11
|
|
11
12
|
class << self
|
12
13
|
|
@@ -17,18 +18,23 @@ module Rainbows
|
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
20
|
-
# configures Rainbows! with a given concurrency model to +use+ and
|
21
|
+
# configures \Rainbows! with a given concurrency model to +use+ and
|
21
22
|
# a +worker_connections+ upper-bound. This method may be called
|
22
23
|
# inside a Unicorn/Rainbows configuration file:
|
23
24
|
#
|
24
25
|
# Rainbows! do
|
25
26
|
# use :Revactor # this may also be :ThreadSpawn or :ThreadPool
|
26
|
-
# worker_connections
|
27
|
+
# worker_connections 400
|
27
28
|
# end
|
28
29
|
#
|
30
|
+
# # the rest of the Unicorn configuration
|
31
|
+
# worker_processes 8
|
32
|
+
#
|
29
33
|
# See the documentation for the respective Revactor, ThreadSpawn,
|
30
34
|
# and ThreadPool classes for descriptions and recommendations for
|
31
|
-
# each of them.
|
35
|
+
# each of them. The total number of clients we're able to serve is
|
36
|
+
# +worker_processes+ * +worker_connections+, so in the above example
|
37
|
+
# we can serve 8 * 400 = 3200 clients concurrently.
|
32
38
|
def Rainbows!(&block)
|
33
39
|
block_given? or raise ArgumentError, "Rainbows! requires a block"
|
34
40
|
HttpServer.setup(block)
|
@@ -42,6 +48,7 @@ module Rainbows
|
|
42
48
|
:Revactor => 50,
|
43
49
|
:ThreadSpawn => 30,
|
44
50
|
:ThreadPool => 10,
|
51
|
+
:Rev => 50,
|
45
52
|
}.each do |model, _|
|
46
53
|
u = model.to_s.gsub(/([a-z0-9])([A-Z0-9])/) { "#{$1}_#{$2.downcase!}" }
|
47
54
|
autoload model, "rainbows/#{u.downcase!}"
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
module Rainbows
|
6
|
+
|
7
|
+
# Rack middleware to limit application-level concurrency independently
|
8
|
+
# of network conncurrency in \Rainbows! Since the +worker_connections+
|
9
|
+
# option in \Rainbows! is only intended to limit the number of
|
10
|
+
# simultaneous clients, this middleware may be used to limit the
|
11
|
+
# number of concurrent application dispatches independently of
|
12
|
+
# concurrent clients.
|
13
|
+
#
|
14
|
+
# Instead of using M:N concurrency in \Rainbows!, this middleware
|
15
|
+
# allows M:N:P concurrency where +P+ is the AppPool +:size+ while
|
16
|
+
# +M+ remains the number of +worker_processes+ and +N+ remains the
|
17
|
+
# number of +worker_connections+.
|
18
|
+
#
|
19
|
+
# rainbows master
|
20
|
+
# \_ rainbows worker[0]
|
21
|
+
# | \_ client[0,0]------\ ___app[0]
|
22
|
+
# | \_ client[0,1]-------\ /___app[1]
|
23
|
+
# | \_ client[0,2]-------->--< ...
|
24
|
+
# | ... __/ `---app[P]
|
25
|
+
# | \_ client[0,N]----/
|
26
|
+
# \_ rainbows worker[1]
|
27
|
+
# | \_ client[1,0]------\ ___app[0]
|
28
|
+
# | \_ client[1,1]-------\ /___app[1]
|
29
|
+
# | \_ client[1,2]-------->--< ...
|
30
|
+
# | ... __/ `---app[P]
|
31
|
+
# | \_ client[1,N]----/
|
32
|
+
# \_ rainbows worker[M]
|
33
|
+
# \_ client[M,0]------\ ___app[0]
|
34
|
+
# \_ client[M,1]-------\ /___app[1]
|
35
|
+
# \_ client[M,2]-------->--< ...
|
36
|
+
# ... __/ `---app[P]
|
37
|
+
# \_ client[M,N]----/
|
38
|
+
#
|
39
|
+
# AppPool should be used if you want to enforce a lower value of +P+
|
40
|
+
# than +N+.
|
41
|
+
#
|
42
|
+
# AppPool has no effect on the Rainbows::Rev concurrency model as that is
|
43
|
+
# single-threaded/single-instance as far as application concurrency goes.
|
44
|
+
# In other words, +P+ is always +one+ when using \Rev (but not
|
45
|
+
# \Revactor) regardless of (or even if) this middleware is loaded.
|
46
|
+
#
|
47
|
+
# Since this is Rack middleware, you may load this in your Rack
|
48
|
+
# config.ru file and even use it in servers other than \Rainbows!
|
49
|
+
#
|
50
|
+
# use Rainbows::AppPool, :size => 30
|
51
|
+
# map "/lobster" do
|
52
|
+
# run Rack::Lobster.new
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# You may to load this earlier or later in your middleware chain
|
56
|
+
# depending on the concurrency/copy-friendliness of your middleware(s).
|
57
|
+
|
58
|
+
class AppPool < Struct.new(:pool)
|
59
|
+
|
60
|
+
# +opt+ is a hash, +:size+ is the size of the pool (default: 6)
|
61
|
+
# meaning you can have up to 6 concurrent instances of +app+
|
62
|
+
# within one \Rainbows! worker process. We support various
|
63
|
+
# methods of the +:copy+ option: +dup+, +clone+, +deep+ and +none+.
|
64
|
+
# Depending on your +app+, one of these options should be set.
|
65
|
+
# The default +:copy+ is +:dup+ as is commonly seen in existing
|
66
|
+
# Rack middleware.
|
67
|
+
def initialize(app, opt = {})
|
68
|
+
self.pool = Queue.new
|
69
|
+
(1...(opt[:size] || 6)).each do
|
70
|
+
pool << case (opt[:copy] || :dup)
|
71
|
+
when :none then app
|
72
|
+
when :dup then app.dup
|
73
|
+
when :clone then app.clone
|
74
|
+
when :deep then Marshal.load(Marshal.dump(app)) # unlikely...
|
75
|
+
else
|
76
|
+
raise ArgumentError, "unsupported copy method: #{opt[:copy].inspect}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
pool << app # the original
|
80
|
+
end
|
81
|
+
|
82
|
+
# Rack application endpoint, +env+ is the Rack environment
|
83
|
+
def call(env)
|
84
|
+
app = pool.shift
|
85
|
+
app.call(env)
|
86
|
+
ensure
|
87
|
+
pool << app
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/rainbows/base.rb
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
module Rainbows
|
4
4
|
|
5
|
-
# base class for Rainbows concurrency models
|
5
|
+
# base class for Rainbows concurrency models, this is currently
|
6
|
+
# used by ThreadSpawn and ThreadPool models
|
6
7
|
module Base
|
7
8
|
|
8
9
|
include Unicorn
|
@@ -16,16 +17,35 @@ module Rainbows
|
|
16
17
|
client.close rescue nil
|
17
18
|
end
|
18
19
|
|
20
|
+
# TODO: migrate into Unicorn::HttpServer
|
21
|
+
def listen_loop_error(e)
|
22
|
+
logger.error "Unhandled listen loop exception #{e.inspect}."
|
23
|
+
logger.error e.backtrace.join("\n")
|
24
|
+
end
|
25
|
+
|
26
|
+
def init_worker_process(worker)
|
27
|
+
super(worker)
|
28
|
+
|
29
|
+
# we're don't use the self-pipe mechanism in the Rainbows! worker
|
30
|
+
# since we don't defer reopening logs
|
31
|
+
HttpServer::SELF_PIPE.each { |x| x.close }.clear
|
32
|
+
trap(:USR1) { reopen_worker_logs(worker.nr) rescue nil }
|
33
|
+
# closing anything we IO.select on will raise EBADF
|
34
|
+
trap(:QUIT) { HttpServer::LISTENERS.map! { |s| s.close rescue nil } }
|
35
|
+
[:TERM, :INT].each { |sig| trap(sig) { exit!(0) } } # instant shutdown
|
36
|
+
logger.info "Rainbows! #@use worker_connections=#@worker_connections"
|
37
|
+
end
|
38
|
+
|
19
39
|
# once a client is accepted, it is processed in its entirety here
|
20
40
|
# in 3 easy steps: read request, call app, write app response
|
21
41
|
def process_client(client)
|
22
42
|
buf = client.readpartial(CHUNK_SIZE)
|
23
43
|
hp = HttpParser.new
|
24
44
|
env = {}
|
45
|
+
alive = true
|
25
46
|
remote_addr = TCPSocket === client ? client.peeraddr.last : LOCALHOST
|
26
47
|
|
27
48
|
begin # loop
|
28
|
-
Thread.current[:t] = Time.now
|
29
49
|
while ! hp.headers(env, buf)
|
30
50
|
buf << client.readpartial(CHUNK_SIZE)
|
31
51
|
end
|
@@ -42,9 +62,10 @@ module Rainbows
|
|
42
62
|
response = app.call(env)
|
43
63
|
end
|
44
64
|
|
45
|
-
|
65
|
+
alive = hp.keepalive? && ! Thread.current[:quit]
|
66
|
+
out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if hp.headers?
|
46
67
|
HttpResponse.write(client, response, out)
|
47
|
-
end while
|
68
|
+
end while alive and hp.reset.nil? and env.clear
|
48
69
|
client.close
|
49
70
|
# if we get any error, try to write something back to the client
|
50
71
|
# assuming we haven't closed the socket, but don't get hung up
|
@@ -60,6 +81,22 @@ module Rainbows
|
|
60
81
|
logger.error e.backtrace.join("\n")
|
61
82
|
end
|
62
83
|
|
84
|
+
def join_threads(threads, worker)
|
85
|
+
logger.info "Joining threads..."
|
86
|
+
threads.each { |thr| thr[:quit] = true }
|
87
|
+
t0 = Time.now
|
88
|
+
timeleft = timeout * 2.0
|
89
|
+
m = 0
|
90
|
+
while (nr = threads.count { |thr| thr.alive? }) > 0 && timeleft > 0
|
91
|
+
threads.each { |thr|
|
92
|
+
worker.tmp.chmod(m = 0 == m ? 1 : 0)
|
93
|
+
thr.join(1)
|
94
|
+
break if (timeleft -= (Time.now - t0)) < 0
|
95
|
+
}
|
96
|
+
end
|
97
|
+
logger.info "Done joining threads. #{nr} left running"
|
98
|
+
end
|
99
|
+
|
63
100
|
def self.included(klass)
|
64
101
|
klass.const_set :LISTENERS, HttpServer::LISTENERS
|
65
102
|
end
|
data/lib/rainbows/const.rb
CHANGED
@@ -3,16 +3,11 @@
|
|
3
3
|
module Rainbows
|
4
4
|
|
5
5
|
module Const
|
6
|
-
RAINBOWS_VERSION = '0.
|
6
|
+
RAINBOWS_VERSION = '0.2.0'
|
7
7
|
|
8
8
|
include Unicorn::Const
|
9
9
|
|
10
10
|
RACK_DEFAULTS = ::Unicorn::HttpRequest::DEFAULTS.merge({
|
11
|
-
|
12
|
-
# we need to observe many of the rules for thread-safety even
|
13
|
-
# with Revactor or Rev, so we're considered multithread-ed even
|
14
|
-
# when we're not technically...
|
15
|
-
"rack.multithread" => true,
|
16
11
|
"SERVER_SOFTWARE" => "Rainbows! #{RAINBOWS_VERSION}",
|
17
12
|
})
|
18
13
|
|
data/lib/rainbows/http_server.rb
CHANGED
data/lib/rainbows/rev.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require 'rev'
|
3
|
+
|
4
|
+
# workaround revactor 0.1.4 still using the old Rev::Buffer
|
5
|
+
# ref: http://rubyforge.org/pipermail/revactor-talk/2009-October/000034.html
|
6
|
+
defined?(Rev::Buffer) or Rev::Buffer = IO::Buffer
|
7
|
+
|
8
|
+
module Rainbows
|
9
|
+
|
10
|
+
# Implements a basic single-threaded event model with
|
11
|
+
# {Rev}[http://rev.rubyforge.org/]. It is capable of handling
|
12
|
+
# thousands of simultaneous client connections, but with only a
|
13
|
+
# single-threaded app dispatch. It is suited for slow clients and
|
14
|
+
# fast applications (applications that do not have slow network
|
15
|
+
# dependencies). It does not require your Rack application to
|
16
|
+
# be reentrant or thread-safe.
|
17
|
+
#
|
18
|
+
# Compatibility: Whatever \Rev itself supports, currently Ruby
|
19
|
+
# 1.8/1.9.
|
20
|
+
#
|
21
|
+
# This model does not implement as streaming "rack.input" which
|
22
|
+
# allows the Rack application to process data as it arrives. This
|
23
|
+
# means "rack.input" will be fully buffered in memory or to a
|
24
|
+
# temporary file before the application is entered.
|
25
|
+
#
|
26
|
+
# Caveats: this model can buffer all output for slow clients in
|
27
|
+
# memory. This can be a problem if your application generates large
|
28
|
+
# responses (including static files served with Rack) as it will cause
|
29
|
+
# the memory footprint of your process to explode. If your workers
|
30
|
+
# seem to be eating a lot of memory from this, consider the
|
31
|
+
# {mall}[http://bogomips.org/mall/] library which allows access to the
|
32
|
+
# mallopt(3) function from Ruby.
|
33
|
+
|
34
|
+
module Rev
|
35
|
+
|
36
|
+
# global vars because class/instance variables are confusing me :<
|
37
|
+
# this struct is only accessed inside workers and thus private to each
|
38
|
+
G = Struct.new(:nr, :max, :logger, :alive, :app).new
|
39
|
+
|
40
|
+
include Base
|
41
|
+
|
42
|
+
class Client < ::Rev::IO
|
43
|
+
include Unicorn
|
44
|
+
include Rainbows::Const
|
45
|
+
G = Rainbows::Rev::G
|
46
|
+
|
47
|
+
def initialize(io)
|
48
|
+
G.nr += 1
|
49
|
+
super(io)
|
50
|
+
@remote_addr = ::TCPSocket === io ? io.peeraddr.last : LOCALHOST
|
51
|
+
@env = {}
|
52
|
+
@hp = HttpParser.new
|
53
|
+
@state = :headers # [ :body [ :trailers ] ] :app_call :close
|
54
|
+
@buf = ""
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_error(e)
|
58
|
+
msg = case e
|
59
|
+
when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
|
60
|
+
ERROR_500_RESPONSE
|
61
|
+
when HttpParserError # try to tell the client they're bad
|
62
|
+
ERROR_400_RESPONSE
|
63
|
+
else
|
64
|
+
G.logger.error "Read error: #{e.inspect}"
|
65
|
+
G.logger.error e.backtrace.join("\n")
|
66
|
+
ERROR_500_RESPONSE
|
67
|
+
end
|
68
|
+
write(msg)
|
69
|
+
ensure
|
70
|
+
@state = :close
|
71
|
+
end
|
72
|
+
|
73
|
+
def app_call
|
74
|
+
@input.rewind
|
75
|
+
@env[RACK_INPUT] = @input
|
76
|
+
@env[REMOTE_ADDR] = @remote_addr
|
77
|
+
response = G.app.call(@env.update(RACK_DEFAULTS))
|
78
|
+
alive = @hp.keepalive? && G.alive
|
79
|
+
out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if @hp.headers?
|
80
|
+
HttpResponse.write(self, response, out)
|
81
|
+
if alive
|
82
|
+
@env.clear
|
83
|
+
@hp.reset
|
84
|
+
@state = :headers
|
85
|
+
else
|
86
|
+
@state = :close
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def on_write_complete
|
91
|
+
:close == @state and close
|
92
|
+
end
|
93
|
+
|
94
|
+
def on_close
|
95
|
+
G.nr -= 1
|
96
|
+
end
|
97
|
+
|
98
|
+
def tmpio
|
99
|
+
io = Util.tmpio
|
100
|
+
def io.size
|
101
|
+
# already sync=true at creation, so no need to flush before stat
|
102
|
+
stat.size
|
103
|
+
end
|
104
|
+
io
|
105
|
+
end
|
106
|
+
|
107
|
+
# TeeInput doesn't map too well to this right now...
|
108
|
+
def on_read(data)
|
109
|
+
case @state
|
110
|
+
when :headers
|
111
|
+
@hp.headers(@env, @buf << data) or return
|
112
|
+
@state = :body
|
113
|
+
len = @hp.content_length
|
114
|
+
if len == 0
|
115
|
+
@input = HttpRequest::NULL_IO
|
116
|
+
app_call # common case
|
117
|
+
else # nil or len > 0
|
118
|
+
# since we don't do streaming input, we have no choice but
|
119
|
+
# to take over 100-continue handling from the Rack application
|
120
|
+
if @env[HTTP_EXPECT] =~ /\A100-continue\z/i
|
121
|
+
write(EXPECT_100_RESPONSE)
|
122
|
+
@env.delete(HTTP_EXPECT)
|
123
|
+
end
|
124
|
+
@input = len && len <= MAX_BODY ? StringIO.new("") : tmpio
|
125
|
+
@hp.filter_body(@buf2 = @buf.dup, @buf)
|
126
|
+
@input << @buf2
|
127
|
+
on_read("")
|
128
|
+
end
|
129
|
+
when :body
|
130
|
+
if @hp.body_eof?
|
131
|
+
@state = :trailers
|
132
|
+
on_read(data)
|
133
|
+
elsif data.size > 0
|
134
|
+
@hp.filter_body(@buf2, @buf << data)
|
135
|
+
@input << @buf2
|
136
|
+
on_read("")
|
137
|
+
end
|
138
|
+
when :trailers
|
139
|
+
@hp.trailers(@env, @buf << data) and app_call
|
140
|
+
end
|
141
|
+
rescue Object => e
|
142
|
+
handle_error(e)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class Server < ::Rev::IO
|
147
|
+
G = Rainbows::Rev::G
|
148
|
+
|
149
|
+
def on_readable
|
150
|
+
return if G.nr >= G.max
|
151
|
+
begin
|
152
|
+
Client.new(@_io.accept_nonblock).attach(::Rev::Loop.default)
|
153
|
+
rescue Errno::EAGAIN, Errno::ECONNBORTED
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
# runs inside each forked worker, this sits around and waits
|
160
|
+
# for connections and doesn't die until the parent dies (or is
|
161
|
+
# given a INT, QUIT, or TERM signal)
|
162
|
+
def worker_loop(worker)
|
163
|
+
init_worker_process(worker)
|
164
|
+
G.nr = 0
|
165
|
+
G.max = worker_connections
|
166
|
+
G.alive = true
|
167
|
+
G.logger = logger
|
168
|
+
G.app = app
|
169
|
+
LISTENERS.map! { |s| Server.new(s).attach(::Rev::Loop.default) }
|
170
|
+
::Rev::Loop.default.run
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|