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