piesync-puma 3.12.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/History.md +1429 -0
- data/LICENSE +26 -0
- data/README.md +280 -0
- data/bin/puma +10 -0
- data/bin/puma-wild +31 -0
- data/bin/pumactl +12 -0
- data/docs/architecture.md +36 -0
- data/docs/deployment.md +91 -0
- data/docs/images/puma-connection-flow-no-reactor.png +0 -0
- data/docs/images/puma-connection-flow.png +0 -0
- data/docs/images/puma-general-arch.png +0 -0
- data/docs/nginx.md +80 -0
- data/docs/plugins.md +28 -0
- data/docs/restart.md +39 -0
- data/docs/signals.md +96 -0
- data/docs/systemd.md +272 -0
- data/ext/puma_http11/PumaHttp11Service.java +17 -0
- data/ext/puma_http11/ext_help.h +15 -0
- data/ext/puma_http11/extconf.rb +15 -0
- data/ext/puma_http11/http11_parser.c +1071 -0
- data/ext/puma_http11/http11_parser.h +65 -0
- data/ext/puma_http11/http11_parser.java.rl +161 -0
- data/ext/puma_http11/http11_parser.rl +149 -0
- data/ext/puma_http11/http11_parser_common.rl +54 -0
- data/ext/puma_http11/io_buffer.c +155 -0
- data/ext/puma_http11/mini_ssl.c +494 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +234 -0
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +470 -0
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +352 -0
- data/ext/puma_http11/puma_http11.c +500 -0
- data/lib/puma.rb +23 -0
- data/lib/puma/accept_nonblock.rb +23 -0
- data/lib/puma/app/status.rb +74 -0
- data/lib/puma/binder.rb +413 -0
- data/lib/puma/cli.rb +235 -0
- data/lib/puma/client.rb +480 -0
- data/lib/puma/cluster.rb +531 -0
- data/lib/puma/commonlogger.rb +108 -0
- data/lib/puma/compat.rb +14 -0
- data/lib/puma/configuration.rb +361 -0
- data/lib/puma/const.rb +239 -0
- data/lib/puma/control_cli.rb +264 -0
- data/lib/puma/convenient.rb +25 -0
- data/lib/puma/daemon_ext.rb +33 -0
- data/lib/puma/delegation.rb +13 -0
- data/lib/puma/detect.rb +15 -0
- data/lib/puma/dsl.rb +518 -0
- data/lib/puma/events.rb +153 -0
- data/lib/puma/io_buffer.rb +9 -0
- data/lib/puma/java_io_buffer.rb +47 -0
- data/lib/puma/jruby_restart.rb +84 -0
- data/lib/puma/launcher.rb +433 -0
- data/lib/puma/minissl.rb +285 -0
- data/lib/puma/null_io.rb +44 -0
- data/lib/puma/plugin.rb +117 -0
- data/lib/puma/plugin/tmp_restart.rb +34 -0
- data/lib/puma/rack/backports/uri/common_193.rb +33 -0
- data/lib/puma/rack/builder.rb +299 -0
- data/lib/puma/rack/urlmap.rb +91 -0
- data/lib/puma/rack_default.rb +7 -0
- data/lib/puma/reactor.rb +347 -0
- data/lib/puma/runner.rb +184 -0
- data/lib/puma/server.rb +1072 -0
- data/lib/puma/single.rb +123 -0
- data/lib/puma/state_file.rb +31 -0
- data/lib/puma/tcp_logger.rb +41 -0
- data/lib/puma/thread_pool.rb +346 -0
- data/lib/puma/util.rb +129 -0
- data/lib/rack/handler/puma.rb +115 -0
- data/tools/jungle/README.md +19 -0
- data/tools/jungle/init.d/README.md +61 -0
- data/tools/jungle/init.d/puma +421 -0
- data/tools/jungle/init.d/run-puma +18 -0
- data/tools/jungle/rc.d/README.md +74 -0
- data/tools/jungle/rc.d/puma +61 -0
- data/tools/jungle/rc.d/puma.conf +10 -0
- data/tools/jungle/upstart/README.md +61 -0
- data/tools/jungle/upstart/puma-manager.conf +31 -0
- data/tools/jungle/upstart/puma.conf +69 -0
- data/tools/trickletest.rb +45 -0
- metadata +131 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
# :stopdoc:
|
2
|
+
|
3
|
+
require 'uri/common'
|
4
|
+
|
5
|
+
# Issue:
|
6
|
+
# http://bugs.ruby-lang.org/issues/5925
|
7
|
+
#
|
8
|
+
# Relevant commit:
|
9
|
+
# https://github.com/ruby/ruby/commit/edb7cdf1eabaff78dfa5ffedfbc2e91b29fa9ca1
|
10
|
+
|
11
|
+
|
12
|
+
module URI
|
13
|
+
begin
|
14
|
+
256.times do |i|
|
15
|
+
TBLENCWWWCOMP_[i.chr] = '%%%02X' % i
|
16
|
+
end
|
17
|
+
TBLENCWWWCOMP_[' '] = '+'
|
18
|
+
TBLENCWWWCOMP_.freeze
|
19
|
+
|
20
|
+
256.times do |i|
|
21
|
+
h, l = i>>4, i&15
|
22
|
+
TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr
|
23
|
+
TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr
|
24
|
+
TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr
|
25
|
+
TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr
|
26
|
+
end
|
27
|
+
TBLDECWWWCOMP_['+'] = ' '
|
28
|
+
TBLDECWWWCOMP_.freeze
|
29
|
+
rescue Exception
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# :startdoc:
|
@@ -0,0 +1,299 @@
|
|
1
|
+
module Puma
|
2
|
+
end
|
3
|
+
|
4
|
+
module Puma::Rack
|
5
|
+
class Options
|
6
|
+
def parse!(args)
|
7
|
+
options = {}
|
8
|
+
opt_parser = OptionParser.new("", 24, ' ') do |opts|
|
9
|
+
opts.banner = "Usage: rackup [ruby options] [rack options] [rackup config]"
|
10
|
+
|
11
|
+
opts.separator ""
|
12
|
+
opts.separator "Ruby options:"
|
13
|
+
|
14
|
+
lineno = 1
|
15
|
+
opts.on("-e", "--eval LINE", "evaluate a LINE of code") { |line|
|
16
|
+
eval line, TOPLEVEL_BINDING, "-e", lineno
|
17
|
+
lineno += 1
|
18
|
+
}
|
19
|
+
|
20
|
+
opts.on("-b", "--builder BUILDER_LINE", "evaluate a BUILDER_LINE of code as a builder script") { |line|
|
21
|
+
options[:builder] = line
|
22
|
+
}
|
23
|
+
|
24
|
+
opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") {
|
25
|
+
options[:debug] = true
|
26
|
+
}
|
27
|
+
opts.on("-w", "--warn", "turn warnings on for your script") {
|
28
|
+
options[:warn] = true
|
29
|
+
}
|
30
|
+
opts.on("-q", "--quiet", "turn off logging") {
|
31
|
+
options[:quiet] = true
|
32
|
+
}
|
33
|
+
|
34
|
+
opts.on("-I", "--include PATH",
|
35
|
+
"specify $LOAD_PATH (may be used more than once)") { |path|
|
36
|
+
(options[:include] ||= []).concat(path.split(":"))
|
37
|
+
}
|
38
|
+
|
39
|
+
opts.on("-r", "--require LIBRARY",
|
40
|
+
"require the library, before executing your script") { |library|
|
41
|
+
options[:require] = library
|
42
|
+
}
|
43
|
+
|
44
|
+
opts.separator ""
|
45
|
+
opts.separator "Rack options:"
|
46
|
+
opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick/mongrel)") { |s|
|
47
|
+
options[:server] = s
|
48
|
+
}
|
49
|
+
|
50
|
+
opts.on("-o", "--host HOST", "listen on HOST (default: localhost)") { |host|
|
51
|
+
options[:Host] = host
|
52
|
+
}
|
53
|
+
|
54
|
+
opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port|
|
55
|
+
options[:Port] = port
|
56
|
+
}
|
57
|
+
|
58
|
+
opts.on("-O", "--option NAME[=VALUE]", "pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '#{$0} -s SERVER -h' to get a list of options for SERVER") { |name|
|
59
|
+
name, value = name.split('=', 2)
|
60
|
+
value = true if value.nil?
|
61
|
+
options[name.to_sym] = value
|
62
|
+
}
|
63
|
+
|
64
|
+
opts.on("-E", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: development)") { |e|
|
65
|
+
options[:environment] = e
|
66
|
+
}
|
67
|
+
|
68
|
+
opts.on("-D", "--daemonize", "run daemonized in the background") { |d|
|
69
|
+
options[:daemonize] = d ? true : false
|
70
|
+
}
|
71
|
+
|
72
|
+
opts.on("-P", "--pid FILE", "file to store PID") { |f|
|
73
|
+
options[:pid] = ::File.expand_path(f)
|
74
|
+
}
|
75
|
+
|
76
|
+
opts.separator ""
|
77
|
+
opts.separator "Common options:"
|
78
|
+
|
79
|
+
opts.on_tail("-h", "-?", "--help", "Show this message") do
|
80
|
+
puts opts
|
81
|
+
puts handler_opts(options)
|
82
|
+
|
83
|
+
exit
|
84
|
+
end
|
85
|
+
|
86
|
+
opts.on_tail("--version", "Show version") do
|
87
|
+
puts "Rack #{Rack.version} (Release: #{Rack.release})"
|
88
|
+
exit
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
begin
|
93
|
+
opt_parser.parse! args
|
94
|
+
rescue OptionParser::InvalidOption => e
|
95
|
+
warn e.message
|
96
|
+
abort opt_parser.to_s
|
97
|
+
end
|
98
|
+
|
99
|
+
options[:config] = args.last if args.last
|
100
|
+
options
|
101
|
+
end
|
102
|
+
|
103
|
+
def handler_opts(options)
|
104
|
+
begin
|
105
|
+
info = []
|
106
|
+
server = Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
|
107
|
+
if server && server.respond_to?(:valid_options)
|
108
|
+
info << ""
|
109
|
+
info << "Server-specific options for #{server.name}:"
|
110
|
+
|
111
|
+
has_options = false
|
112
|
+
server.valid_options.each do |name, description|
|
113
|
+
next if name.to_s =~ /^(Host|Port)[^a-zA-Z]/ # ignore handler's host and port options, we do our own.
|
114
|
+
|
115
|
+
info << " -O %-21s %s" % [name, description]
|
116
|
+
has_options = true
|
117
|
+
end
|
118
|
+
return "" if !has_options
|
119
|
+
end
|
120
|
+
info.join("\n")
|
121
|
+
rescue NameError
|
122
|
+
return "Warning: Could not find handler specified (#{options[:server] || 'default'}) to determine handler-specific options"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Rack::Builder implements a small DSL to iteratively construct Rack
|
128
|
+
# applications.
|
129
|
+
#
|
130
|
+
# Example:
|
131
|
+
#
|
132
|
+
# require 'rack/lobster'
|
133
|
+
# app = Rack::Builder.new do
|
134
|
+
# use Rack::CommonLogger
|
135
|
+
# use Rack::ShowExceptions
|
136
|
+
# map "/lobster" do
|
137
|
+
# use Rack::Lint
|
138
|
+
# run Rack::Lobster.new
|
139
|
+
# end
|
140
|
+
# end
|
141
|
+
#
|
142
|
+
# run app
|
143
|
+
#
|
144
|
+
# Or
|
145
|
+
#
|
146
|
+
# app = Rack::Builder.app do
|
147
|
+
# use Rack::CommonLogger
|
148
|
+
# run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# run app
|
152
|
+
#
|
153
|
+
# +use+ adds middleware to the stack, +run+ dispatches to an application.
|
154
|
+
# You can use +map+ to construct a Rack::URLMap in a convenient way.
|
155
|
+
|
156
|
+
class Builder
|
157
|
+
def self.parse_file(config, opts = Options.new)
|
158
|
+
options = {}
|
159
|
+
if config =~ /\.ru$/
|
160
|
+
cfgfile = ::File.read(config)
|
161
|
+
if cfgfile[/^#\\(.*)/] && opts
|
162
|
+
options = opts.parse! $1.split(/\s+/)
|
163
|
+
end
|
164
|
+
cfgfile.sub!(/^__END__\n.*\Z/m, '')
|
165
|
+
app = new_from_string cfgfile, config
|
166
|
+
else
|
167
|
+
require config
|
168
|
+
app = Object.const_get(::File.basename(config, '.rb').capitalize)
|
169
|
+
end
|
170
|
+
return app, options
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.new_from_string(builder_script, file="(rackup)")
|
174
|
+
eval "Puma::Rack::Builder.new {\n" + builder_script + "\n}.to_app",
|
175
|
+
TOPLEVEL_BINDING, file, 0
|
176
|
+
end
|
177
|
+
|
178
|
+
def initialize(default_app = nil,&block)
|
179
|
+
@use, @map, @run, @warmup = [], nil, default_app, nil
|
180
|
+
|
181
|
+
# Conditionally load rack now, so that any rack middlewares,
|
182
|
+
# etc are available.
|
183
|
+
begin
|
184
|
+
require 'rack'
|
185
|
+
rescue LoadError
|
186
|
+
end
|
187
|
+
|
188
|
+
instance_eval(&block) if block_given?
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.app(default_app = nil, &block)
|
192
|
+
self.new(default_app, &block).to_app
|
193
|
+
end
|
194
|
+
|
195
|
+
# Specifies middleware to use in a stack.
|
196
|
+
#
|
197
|
+
# class Middleware
|
198
|
+
# def initialize(app)
|
199
|
+
# @app = app
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
# def call(env)
|
203
|
+
# env["rack.some_header"] = "setting an example"
|
204
|
+
# @app.call(env)
|
205
|
+
# end
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
# use Middleware
|
209
|
+
# run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
|
210
|
+
#
|
211
|
+
# All requests through to this application will first be processed by the middleware class.
|
212
|
+
# The +call+ method in this example sets an additional environment key which then can be
|
213
|
+
# referenced in the application if required.
|
214
|
+
def use(middleware, *args, &block)
|
215
|
+
if @map
|
216
|
+
mapping, @map = @map, nil
|
217
|
+
@use << proc { |app| generate_map app, mapping }
|
218
|
+
end
|
219
|
+
@use << proc { |app| middleware.new(app, *args, &block) }
|
220
|
+
end
|
221
|
+
|
222
|
+
# Takes an argument that is an object that responds to #call and returns a Rack response.
|
223
|
+
# The simplest form of this is a lambda object:
|
224
|
+
#
|
225
|
+
# run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
|
226
|
+
#
|
227
|
+
# However this could also be a class:
|
228
|
+
#
|
229
|
+
# class Heartbeat
|
230
|
+
# def self.call(env)
|
231
|
+
# [200, { "Content-Type" => "text/plain" }, ["OK"]]
|
232
|
+
# end
|
233
|
+
# end
|
234
|
+
#
|
235
|
+
# run Heartbeat
|
236
|
+
def run(app)
|
237
|
+
@run = app
|
238
|
+
end
|
239
|
+
|
240
|
+
# Takes a lambda or block that is used to warm-up the application.
|
241
|
+
#
|
242
|
+
# warmup do |app|
|
243
|
+
# client = Rack::MockRequest.new(app)
|
244
|
+
# client.get('/')
|
245
|
+
# end
|
246
|
+
#
|
247
|
+
# use SomeMiddleware
|
248
|
+
# run MyApp
|
249
|
+
def warmup(prc=nil, &block)
|
250
|
+
@warmup = prc || block
|
251
|
+
end
|
252
|
+
|
253
|
+
# Creates a route within the application.
|
254
|
+
#
|
255
|
+
# Rack::Builder.app do
|
256
|
+
# map '/' do
|
257
|
+
# run Heartbeat
|
258
|
+
# end
|
259
|
+
# end
|
260
|
+
#
|
261
|
+
# The +use+ method can also be used here to specify middleware to run under a specific path:
|
262
|
+
#
|
263
|
+
# Rack::Builder.app do
|
264
|
+
# map '/' do
|
265
|
+
# use Middleware
|
266
|
+
# run Heartbeat
|
267
|
+
# end
|
268
|
+
# end
|
269
|
+
#
|
270
|
+
# This example includes a piece of middleware which will run before requests hit +Heartbeat+.
|
271
|
+
#
|
272
|
+
def map(path, &block)
|
273
|
+
@map ||= {}
|
274
|
+
@map[path] = block
|
275
|
+
end
|
276
|
+
|
277
|
+
def to_app
|
278
|
+
app = @map ? generate_map(@run, @map) : @run
|
279
|
+
fail "missing run or map statement" unless app
|
280
|
+
app = @use.reverse.inject(app) { |a,e| e[a] }
|
281
|
+
@warmup.call(app) if @warmup
|
282
|
+
app
|
283
|
+
end
|
284
|
+
|
285
|
+
def call(env)
|
286
|
+
to_app.call(env)
|
287
|
+
end
|
288
|
+
|
289
|
+
private
|
290
|
+
|
291
|
+
def generate_map(default_app, mapping)
|
292
|
+
require 'puma/rack/urlmap'
|
293
|
+
|
294
|
+
mapped = default_app ? {'/' => default_app} : {}
|
295
|
+
mapping.each { |r,b| mapped[r] = self.class.new(default_app, &b).to_app }
|
296
|
+
URLMap.new(mapped)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Puma::Rack
|
2
|
+
# Rack::URLMap takes a hash mapping urls or paths to apps, and
|
3
|
+
# dispatches accordingly. Support for HTTP/1.1 host names exists if
|
4
|
+
# the URLs start with <tt>http://</tt> or <tt>https://</tt>.
|
5
|
+
#
|
6
|
+
# URLMap modifies the SCRIPT_NAME and PATH_INFO such that the part
|
7
|
+
# relevant for dispatch is in the SCRIPT_NAME, and the rest in the
|
8
|
+
# PATH_INFO. This should be taken care of when you need to
|
9
|
+
# reconstruct the URL in order to create links.
|
10
|
+
#
|
11
|
+
# URLMap dispatches in such a way that the longest paths are tried
|
12
|
+
# first, since they are most specific.
|
13
|
+
|
14
|
+
class URLMap
|
15
|
+
NEGATIVE_INFINITY = -1.0 / 0.0
|
16
|
+
INFINITY = 1.0 / 0.0
|
17
|
+
|
18
|
+
def initialize(map = {})
|
19
|
+
remap(map)
|
20
|
+
end
|
21
|
+
|
22
|
+
def remap(map)
|
23
|
+
@mapping = map.map { |location, app|
|
24
|
+
if location =~ %r{\Ahttps?://(.*?)(/.*)}
|
25
|
+
host, location = $1, $2
|
26
|
+
else
|
27
|
+
host = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
unless location[0] == ?/
|
31
|
+
raise ArgumentError, "paths need to start with /"
|
32
|
+
end
|
33
|
+
|
34
|
+
location = location.chomp('/')
|
35
|
+
match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
|
36
|
+
|
37
|
+
[host, location, match, app]
|
38
|
+
}.sort_by do |(host, location, _, _)|
|
39
|
+
[host ? -host.size : INFINITY, -location.size]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def call(env)
|
44
|
+
path = env['PATH_INFO']
|
45
|
+
script_name = env['SCRIPT_NAME']
|
46
|
+
http_host = env['HTTP_HOST']
|
47
|
+
server_name = env['SERVER_NAME']
|
48
|
+
server_port = env['SERVER_PORT']
|
49
|
+
|
50
|
+
is_same_server = casecmp?(http_host, server_name) ||
|
51
|
+
casecmp?(http_host, "#{server_name}:#{server_port}")
|
52
|
+
|
53
|
+
@mapping.each do |host, location, match, app|
|
54
|
+
unless casecmp?(http_host, host) \
|
55
|
+
|| casecmp?(server_name, host) \
|
56
|
+
|| (!host && is_same_server)
|
57
|
+
next
|
58
|
+
end
|
59
|
+
|
60
|
+
next unless m = match.match(path.to_s)
|
61
|
+
|
62
|
+
rest = m[1]
|
63
|
+
next unless !rest || rest.empty? || rest[0] == ?/
|
64
|
+
|
65
|
+
env['SCRIPT_NAME'] = (script_name + location)
|
66
|
+
env['PATH_INFO'] = rest
|
67
|
+
|
68
|
+
return app.call(env)
|
69
|
+
end
|
70
|
+
|
71
|
+
[404, {'Content-Type' => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]]
|
72
|
+
|
73
|
+
ensure
|
74
|
+
env['PATH_INFO'] = path
|
75
|
+
env['SCRIPT_NAME'] = script_name
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def casecmp?(v1, v2)
|
80
|
+
# if both nil, or they're the same string
|
81
|
+
return true if v1 == v2
|
82
|
+
|
83
|
+
# if either are nil... (but they're not the same)
|
84
|
+
return false if v1.nil?
|
85
|
+
return false if v2.nil?
|
86
|
+
|
87
|
+
# otherwise check they're not case-insensitive the same
|
88
|
+
v1.casecmp(v2).zero?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/puma/reactor.rb
ADDED
@@ -0,0 +1,347 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'puma/util'
|
4
|
+
require 'puma/minissl'
|
5
|
+
|
6
|
+
module Puma
|
7
|
+
# Internal Docs, Not a public interface.
|
8
|
+
#
|
9
|
+
# The Reactor object is responsible for ensuring that a request has been
|
10
|
+
# completely received before it starts to be processed. This may be known as read buffering.
|
11
|
+
# If read buffering is not done, and no other read buffering is performed (such as by an application server
|
12
|
+
# such as nginx) then the application would be subject to a slow client attack.
|
13
|
+
#
|
14
|
+
# Each Puma "worker" process has its own Reactor. For example if you start puma with `$ puma -w 5` then
|
15
|
+
# it will have 5 workers and each worker will have it's own reactor.
|
16
|
+
#
|
17
|
+
# For a graphical representation of how the reactor works see [architecture.md](https://github.com/puma/puma/blob/master/docs/architecture.md#connection-pipeline).
|
18
|
+
#
|
19
|
+
# ## Reactor Flow
|
20
|
+
#
|
21
|
+
# A request comes into a `Puma::Server` instance, it is then passed to a `Puma::Reactor` instance.
|
22
|
+
# The reactor stores the request in an array and calls `IO.select` on the array in a loop.
|
23
|
+
#
|
24
|
+
# When the request is written to by the client then the `IO.select` will "wake up" and
|
25
|
+
# return the references to any objects that caused it to "wake". The reactor
|
26
|
+
# then loops through each of these request objects, and sees if they're complete. If they
|
27
|
+
# have a full header and body then the reactor passes the request to a thread pool.
|
28
|
+
# Once in a thread pool, a "worker thread" can run the the application's Ruby code against the request.
|
29
|
+
#
|
30
|
+
# If the request is not complete, then it stays in the array, and the next time any
|
31
|
+
# data is written to that socket reference, then the loop is woken up and it is checked for completeness again.
|
32
|
+
#
|
33
|
+
# A detailed example is given in the docs for `run_internal` which is where the bulk
|
34
|
+
# of this logic lives.
|
35
|
+
class Reactor
|
36
|
+
DefaultSleepFor = 5
|
37
|
+
|
38
|
+
# Creates an instance of Puma::Reactor
|
39
|
+
#
|
40
|
+
# The `server` argument is an instance of `Puma::Server`
|
41
|
+
# this is used to write a response for "low level errors"
|
42
|
+
# when there is an exception inside of the reactor.
|
43
|
+
#
|
44
|
+
# The `app_pool` is an instance of `Puma::ThreadPool`.
|
45
|
+
# Once a request is fully formed (header and body are received)
|
46
|
+
# it will be passed to the `app_pool`.
|
47
|
+
def initialize(server, app_pool)
|
48
|
+
@server = server
|
49
|
+
@events = server.events
|
50
|
+
@app_pool = app_pool
|
51
|
+
|
52
|
+
@mutex = Mutex.new
|
53
|
+
|
54
|
+
# Read / Write pipes to wake up internal while loop
|
55
|
+
@ready, @trigger = Puma::Util.pipe
|
56
|
+
@input = []
|
57
|
+
@sleep_for = DefaultSleepFor
|
58
|
+
@timeouts = []
|
59
|
+
|
60
|
+
@sockets = [@ready]
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
|
66
|
+
# Until a request is added via the `add` method this method will internally
|
67
|
+
# loop, waiting on the `sockets` array objects. The only object in this
|
68
|
+
# array at first is the `@ready` IO object, which is the read end of a pipe
|
69
|
+
# connected to `@trigger` object. When `@trigger` is written to, then the loop
|
70
|
+
# will break on `IO.select` and return an array.
|
71
|
+
#
|
72
|
+
# ## When a request is added:
|
73
|
+
#
|
74
|
+
# When the `add` method is called, an instance of `Puma::Client` is added to the `@input` array.
|
75
|
+
# Next the `@ready` pipe is "woken" by writing a string of `"*"` to `@trigger`.
|
76
|
+
#
|
77
|
+
# When that happens, the internal loop stops blocking at `IO.select` and returns a reference
|
78
|
+
# to whatever "woke" it up. On the very first loop, the only thing in `sockets` is `@ready`.
|
79
|
+
# When `@trigger` is written-to, the loop "wakes" and the `ready`
|
80
|
+
# variable returns an array of arrays that looks like `[[#<IO:fd 10>], [], []]` where the
|
81
|
+
# first IO object is the `@ready` object. This first array `[#<IO:fd 10>]`
|
82
|
+
# is saved as a `reads` variable.
|
83
|
+
#
|
84
|
+
# The `reads` variable is iterated through. In the case that the object
|
85
|
+
# is the same as the `@ready` input pipe, then we know that there was a `trigger` event.
|
86
|
+
#
|
87
|
+
# If there was a trigger event, then one byte of `@ready` is read into memory. In the case of the first request,
|
88
|
+
# the reactor sees that it's a `"*"` value and the reactor adds the contents of `@input` into the `sockets` array.
|
89
|
+
# The while then loop continues to iterate again, but now the `sockets` array contains a `Puma::Client` instance in addition
|
90
|
+
# to the `@ready` IO object. For example: `[#<IO:fd 10>, #<Puma::Client:0x3fdc1103bee8 @ready=false>]`.
|
91
|
+
#
|
92
|
+
# Since the `Puma::Client` in this example has data that has not been read yet,
|
93
|
+
# the `IO.select` is immediately able to "wake" and read from the `Puma::Client`. At this point the
|
94
|
+
# `ready` output looks like this: `[[#<Puma::Client:0x3fdc1103bee8 @ready=false>], [], []]`.
|
95
|
+
#
|
96
|
+
# Each element in the first entry is iterated over. The `Puma::Client` object is not
|
97
|
+
# the `@ready` pipe, so the reactor checks to see if it has the fully header and body with
|
98
|
+
# the `Puma::Client#try_to_finish` method. If the full request has been sent,
|
99
|
+
# then the request is passed off to the `@app_pool` thread pool so that a "worker thread"
|
100
|
+
# can pick up the request and begin to execute application logic. This is done
|
101
|
+
# via `@app_pool << c`. The `Puma::Client` is then removed from the `sockets` array.
|
102
|
+
#
|
103
|
+
# If the request body is not present then nothing will happen, and the loop will iterate
|
104
|
+
# again. When the client sends more data to the socket the `Puma::Client` object will
|
105
|
+
# wake up the `IO.select` and it can again be checked to see if it's ready to be
|
106
|
+
# passed to the thread pool.
|
107
|
+
#
|
108
|
+
# ## Time Out Case
|
109
|
+
#
|
110
|
+
# In addition to being woken via a write to one of the sockets the `IO.select` will
|
111
|
+
# periodically "time out" of the sleep. One of the functions of this is to check for
|
112
|
+
# any requests that have "timed out". At the end of the loop it's checked to see if
|
113
|
+
# the first element in the `@timeout` array has exceed it's allowed time. If so,
|
114
|
+
# the client object is removed from the timeout aray, a 408 response is written.
|
115
|
+
# Then it's connection is closed, and the object is removed from the `sockets` array
|
116
|
+
# that watches for new data.
|
117
|
+
#
|
118
|
+
# This behavior loops until all the objects that have timed out have been removed.
|
119
|
+
#
|
120
|
+
# Once all the timeouts have been processed, the next duration of the `IO.select` sleep
|
121
|
+
# will be set to be equal to the amount of time it will take for the next timeout to occur.
|
122
|
+
# This calculation happens in `calculate_sleep`.
|
123
|
+
def run_internal
|
124
|
+
sockets = @sockets
|
125
|
+
|
126
|
+
while true
|
127
|
+
begin
|
128
|
+
ready = IO.select sockets, nil, nil, @sleep_for
|
129
|
+
rescue IOError => e
|
130
|
+
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
131
|
+
if sockets.any? { |socket| socket.closed? }
|
132
|
+
STDERR.puts "Error in select: #{e.message} (#{e.class})"
|
133
|
+
STDERR.puts e.backtrace
|
134
|
+
sockets = sockets.reject { |socket| socket.closed? }
|
135
|
+
retry
|
136
|
+
else
|
137
|
+
raise
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
if ready and reads = ready[0]
|
142
|
+
reads.each do |c|
|
143
|
+
if c == @ready
|
144
|
+
@mutex.synchronize do
|
145
|
+
case @ready.read(1)
|
146
|
+
when "*"
|
147
|
+
sockets += @input
|
148
|
+
@input.clear
|
149
|
+
when "c"
|
150
|
+
sockets.delete_if do |s|
|
151
|
+
if s == @ready
|
152
|
+
false
|
153
|
+
else
|
154
|
+
s.close
|
155
|
+
true
|
156
|
+
end
|
157
|
+
end
|
158
|
+
when "!"
|
159
|
+
return
|
160
|
+
end
|
161
|
+
end
|
162
|
+
else
|
163
|
+
# We have to be sure to remove it from the timeout
|
164
|
+
# list or we'll accidentally close the socket when
|
165
|
+
# it's in use!
|
166
|
+
if c.timeout_at
|
167
|
+
@mutex.synchronize do
|
168
|
+
@timeouts.delete c
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
begin
|
173
|
+
if c.try_to_finish
|
174
|
+
@app_pool << c
|
175
|
+
sockets.delete c
|
176
|
+
end
|
177
|
+
|
178
|
+
# Don't report these to the lowlevel_error handler, otherwise
|
179
|
+
# will be flooding them with errors when persistent connections
|
180
|
+
# are closed.
|
181
|
+
rescue ConnectionError
|
182
|
+
c.write_500
|
183
|
+
c.close
|
184
|
+
|
185
|
+
sockets.delete c
|
186
|
+
|
187
|
+
# SSL handshake failure
|
188
|
+
rescue MiniSSL::SSLError => e
|
189
|
+
@server.lowlevel_error(e, c.env)
|
190
|
+
|
191
|
+
ssl_socket = c.io
|
192
|
+
addr = ssl_socket.peeraddr.last
|
193
|
+
cert = ssl_socket.peercert
|
194
|
+
|
195
|
+
c.close
|
196
|
+
sockets.delete c
|
197
|
+
|
198
|
+
@events.ssl_error @server, addr, cert, e
|
199
|
+
|
200
|
+
# The client doesn't know HTTP well
|
201
|
+
rescue HttpParserError => e
|
202
|
+
@server.lowlevel_error(e, c.env)
|
203
|
+
|
204
|
+
c.write_400
|
205
|
+
c.close
|
206
|
+
|
207
|
+
sockets.delete c
|
208
|
+
|
209
|
+
@events.parse_error @server, c.env, e
|
210
|
+
rescue StandardError => e
|
211
|
+
@server.lowlevel_error(e, c.env)
|
212
|
+
|
213
|
+
c.write_500
|
214
|
+
c.close
|
215
|
+
|
216
|
+
sockets.delete c
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
unless @timeouts.empty?
|
223
|
+
@mutex.synchronize do
|
224
|
+
now = Time.now
|
225
|
+
|
226
|
+
while @timeouts.first.timeout_at < now
|
227
|
+
c = @timeouts.shift
|
228
|
+
c.write_408 if c.in_data_phase
|
229
|
+
c.close
|
230
|
+
sockets.delete c
|
231
|
+
|
232
|
+
break if @timeouts.empty?
|
233
|
+
end
|
234
|
+
|
235
|
+
calculate_sleep
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
public
|
242
|
+
|
243
|
+
def run
|
244
|
+
run_internal
|
245
|
+
ensure
|
246
|
+
@trigger.close
|
247
|
+
@ready.close
|
248
|
+
end
|
249
|
+
|
250
|
+
def run_in_thread
|
251
|
+
@thread = Thread.new do
|
252
|
+
begin
|
253
|
+
run_internal
|
254
|
+
rescue StandardError => e
|
255
|
+
STDERR.puts "Error in reactor loop escaped: #{e.message} (#{e.class})"
|
256
|
+
STDERR.puts e.backtrace
|
257
|
+
retry
|
258
|
+
ensure
|
259
|
+
@trigger.close
|
260
|
+
@ready.close
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# The `calculate_sleep` sets the value that the `IO.select` will
|
266
|
+
# sleep for in the main reactor loop when no sockets are being written to.
|
267
|
+
#
|
268
|
+
# The values kept in `@timeouts` are sorted so that the first timeout
|
269
|
+
# comes first in the array. When there are no timeouts the default timeout is used.
|
270
|
+
#
|
271
|
+
# Otherwise a sleep value is set that is the same as the amount of time it
|
272
|
+
# would take for the first element to time out.
|
273
|
+
#
|
274
|
+
# If that value is in the past, then a sleep value of zero is used.
|
275
|
+
def calculate_sleep
|
276
|
+
if @timeouts.empty?
|
277
|
+
@sleep_for = DefaultSleepFor
|
278
|
+
else
|
279
|
+
diff = @timeouts.first.timeout_at.to_f - Time.now.to_f
|
280
|
+
|
281
|
+
if diff < 0.0
|
282
|
+
@sleep_for = 0
|
283
|
+
else
|
284
|
+
@sleep_for = diff
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# This method adds a connection to the reactor
|
290
|
+
#
|
291
|
+
# Typically called by `Puma::Server` the value passed in
|
292
|
+
# is usually a `Puma::Client` object that responds like an IO
|
293
|
+
# object.
|
294
|
+
#
|
295
|
+
# The main body of the reactor loop is in `run_internal` and it
|
296
|
+
# will sleep on `IO.select`. When a new connection is added to the
|
297
|
+
# reactor it cannot be added directly to the `sockets` aray, because
|
298
|
+
# the `IO.select` will not be watching for it yet.
|
299
|
+
#
|
300
|
+
# Instead what needs to happen is that `IO.select` needs to be woken up,
|
301
|
+
# the contents of `@input` added to the `sockets` array, and then
|
302
|
+
# another call to `IO.select` needs to happen. Since the `Puma::Client`
|
303
|
+
# object can be read immediately, it does not block, but instead returns
|
304
|
+
# right away.
|
305
|
+
#
|
306
|
+
# This behavior is accomplished by writing to `@trigger` which wakes up
|
307
|
+
# the `IO.select` and then there is logic to detect the value of `*`,
|
308
|
+
# pull the contents from `@input` and add them to the sockets array.
|
309
|
+
#
|
310
|
+
# If the object passed in has a timeout value in `timeout_at` then
|
311
|
+
# it is added to a `@timeouts` array. This array is then re-arranged
|
312
|
+
# so that the first element to timeout will be at the front of the
|
313
|
+
# array. Then a value to sleep for is derived in the call to `calculate_sleep`
|
314
|
+
def add(c)
|
315
|
+
@mutex.synchronize do
|
316
|
+
@input << c
|
317
|
+
@trigger << "*"
|
318
|
+
|
319
|
+
if c.timeout_at
|
320
|
+
@timeouts << c
|
321
|
+
@timeouts.sort! { |a,b| a.timeout_at <=> b.timeout_at }
|
322
|
+
|
323
|
+
calculate_sleep
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# Close all watched sockets and clear them from being watched
|
329
|
+
def clear!
|
330
|
+
begin
|
331
|
+
@trigger << "c"
|
332
|
+
rescue IOError
|
333
|
+
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def shutdown
|
338
|
+
begin
|
339
|
+
@trigger << "!"
|
340
|
+
rescue IOError
|
341
|
+
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
342
|
+
end
|
343
|
+
|
344
|
+
@thread.join
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|