pitchfork 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.git-blame-ignore-revs +3 -0
- data/.gitattributes +5 -0
- data/.github/workflows/ci.yml +30 -0
- data/.gitignore +23 -0
- data/COPYING +674 -0
- data/Dockerfile +4 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +30 -0
- data/LICENSE +67 -0
- data/README.md +123 -0
- data/Rakefile +72 -0
- data/docs/Application_Timeouts.md +74 -0
- data/docs/CONFIGURATION.md +388 -0
- data/docs/DESIGN.md +86 -0
- data/docs/FORK_SAFETY.md +80 -0
- data/docs/PHILOSOPHY.md +90 -0
- data/docs/REFORKING.md +113 -0
- data/docs/SIGNALS.md +38 -0
- data/docs/TUNING.md +106 -0
- data/examples/constant_caches.ru +43 -0
- data/examples/echo.ru +25 -0
- data/examples/hello.ru +5 -0
- data/examples/nginx.conf +156 -0
- data/examples/pitchfork.conf.minimal.rb +5 -0
- data/examples/pitchfork.conf.rb +77 -0
- data/examples/unicorn.socket +11 -0
- data/exe/pitchfork +116 -0
- data/ext/pitchfork_http/CFLAGS +13 -0
- data/ext/pitchfork_http/c_util.h +116 -0
- data/ext/pitchfork_http/child_subreaper.h +25 -0
- data/ext/pitchfork_http/common_field_optimization.h +130 -0
- data/ext/pitchfork_http/epollexclusive.h +124 -0
- data/ext/pitchfork_http/ext_help.h +38 -0
- data/ext/pitchfork_http/extconf.rb +14 -0
- data/ext/pitchfork_http/global_variables.h +97 -0
- data/ext/pitchfork_http/httpdate.c +79 -0
- data/ext/pitchfork_http/pitchfork_http.c +4318 -0
- data/ext/pitchfork_http/pitchfork_http.rl +1024 -0
- data/ext/pitchfork_http/pitchfork_http_common.rl +76 -0
- data/lib/pitchfork/app/old_rails/static.rb +59 -0
- data/lib/pitchfork/children.rb +124 -0
- data/lib/pitchfork/configurator.rb +314 -0
- data/lib/pitchfork/const.rb +23 -0
- data/lib/pitchfork/http_parser.rb +206 -0
- data/lib/pitchfork/http_response.rb +63 -0
- data/lib/pitchfork/http_server.rb +822 -0
- data/lib/pitchfork/launcher.rb +9 -0
- data/lib/pitchfork/mem_info.rb +36 -0
- data/lib/pitchfork/message.rb +130 -0
- data/lib/pitchfork/mold_selector.rb +29 -0
- data/lib/pitchfork/preread_input.rb +33 -0
- data/lib/pitchfork/refork_condition.rb +21 -0
- data/lib/pitchfork/select_waiter.rb +9 -0
- data/lib/pitchfork/socket_helper.rb +199 -0
- data/lib/pitchfork/stream_input.rb +152 -0
- data/lib/pitchfork/tee_input.rb +133 -0
- data/lib/pitchfork/tmpio.rb +35 -0
- data/lib/pitchfork/version.rb +8 -0
- data/lib/pitchfork/worker.rb +244 -0
- data/lib/pitchfork.rb +158 -0
- data/pitchfork.gemspec +30 -0
- metadata +137 -0
@@ -0,0 +1,822 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require 'pitchfork/pitchfork_http'
|
3
|
+
|
4
|
+
module Pitchfork
|
5
|
+
# This is the process manager of Pitchfork. This manages worker
|
6
|
+
# processes which in turn handle the I/O and application process.
|
7
|
+
# Listener sockets are started in the master process and shared with
|
8
|
+
# forked worker children.
|
9
|
+
class HttpServer
|
10
|
+
# :stopdoc:
|
11
|
+
attr_accessor :app, :timeout, :worker_processes,
|
12
|
+
:before_fork, :after_fork,
|
13
|
+
:listener_opts, :children,
|
14
|
+
:orig_app, :config, :ready_pipe,
|
15
|
+
:default_middleware, :early_hints
|
16
|
+
attr_writer :after_worker_exit, :after_worker_ready, :refork_condition, :mold_selector
|
17
|
+
|
18
|
+
attr_reader :logger
|
19
|
+
include Pitchfork::SocketHelper
|
20
|
+
include Pitchfork::HttpResponse
|
21
|
+
|
22
|
+
# all bound listener sockets
|
23
|
+
# note: this is public used by raindrops, but not recommended for use
|
24
|
+
# in new projects
|
25
|
+
LISTENERS = []
|
26
|
+
|
27
|
+
NOOP = '.'
|
28
|
+
|
29
|
+
REFORKING_AVAILABLE = Pitchfork::CHILD_SUBREAPER_AVAILABLE || Process.pid == 1
|
30
|
+
MAX_SLEEP = 1 # seconds
|
31
|
+
|
32
|
+
# :startdoc:
|
33
|
+
# This Hash is considered a stable interface and changing its contents
|
34
|
+
# will allow you to switch between different installations of Pitchfork
|
35
|
+
# or even different installations of the same applications without
|
36
|
+
# downtime. Keys of this constant Hash are described as follows:
|
37
|
+
#
|
38
|
+
# * 0 - the path to the pitchfork executable
|
39
|
+
# * :argv - a deep copy of the ARGV array the executable originally saw
|
40
|
+
# * :cwd - the working directory of the application, this is where
|
41
|
+
# you originally started Pitchfork.
|
42
|
+
# TODO: Can we get rid of this?
|
43
|
+
START_CTX = {
|
44
|
+
:argv => ARGV.map(&:dup),
|
45
|
+
0 => $0.dup,
|
46
|
+
}
|
47
|
+
# We favor ENV['PWD'] since it is (usually) symlink aware for Capistrano
|
48
|
+
# and like systems
|
49
|
+
START_CTX[:cwd] = begin
|
50
|
+
a = File.stat(pwd = ENV['PWD'])
|
51
|
+
b = File.stat(Dir.pwd)
|
52
|
+
a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
|
53
|
+
rescue
|
54
|
+
Dir.pwd
|
55
|
+
end
|
56
|
+
# :stopdoc:
|
57
|
+
|
58
|
+
# Creates a working server on host:port (strange things happen if
|
59
|
+
# port isn't a Number). Use HttpServer::run to start the server and
|
60
|
+
# HttpServer.run.join to join the thread that's processing
|
61
|
+
# incoming requests on the socket.
|
62
|
+
def initialize(app, options = {})
|
63
|
+
@app = app
|
64
|
+
@respawn = false
|
65
|
+
@last_check = time_now
|
66
|
+
@default_middleware = true
|
67
|
+
options = options.dup
|
68
|
+
@ready_pipe = options.delete(:ready_pipe)
|
69
|
+
@init_listeners = options[:listeners] ? options[:listeners].dup : []
|
70
|
+
options[:use_defaults] = true
|
71
|
+
self.config = Pitchfork::Configurator.new(options)
|
72
|
+
self.listener_opts = {}
|
73
|
+
|
74
|
+
# We use @control_socket differently in the master and worker processes:
|
75
|
+
#
|
76
|
+
# * The master process never closes or reinitializes this once
|
77
|
+
# initialized. Signal handlers in the master process will write to
|
78
|
+
# it to wake up the master from IO.select in exactly the same manner
|
79
|
+
# djb describes in https://cr.yp.to/docs/selfpipe.html
|
80
|
+
#
|
81
|
+
# * The workers immediately close the pipe they inherit. See the
|
82
|
+
# Pitchfork::Worker class for the pipe workers use.
|
83
|
+
@control_socket = []
|
84
|
+
@children = Children.new
|
85
|
+
@sig_queue = [] # signal queue used for self-piping
|
86
|
+
@pid = nil
|
87
|
+
|
88
|
+
# we try inheriting listeners first, so we bind them later.
|
89
|
+
# we don't write the pid file until we've bound listeners in case
|
90
|
+
# pitchfork was started twice by mistake. Even though our #pid= method
|
91
|
+
# checks for stale/existing pid files, race conditions are still
|
92
|
+
# possible (and difficult/non-portable to avoid) and can be likely
|
93
|
+
# to clobber the pid if the second start was in quick succession
|
94
|
+
# after the first, so we rely on the listener binding to fail in
|
95
|
+
# that case. Some tests (in and outside of this source tree) and
|
96
|
+
# monitoring tools may also rely on pid files existing before we
|
97
|
+
# attempt to connect to the listener(s)
|
98
|
+
config.commit!(self, :skip => [:listeners, :pid])
|
99
|
+
@orig_app = app
|
100
|
+
# list of signals we care about and trap in master.
|
101
|
+
@queue_sigs = [
|
102
|
+
:QUIT, :INT, :TERM, :USR2, :TTIN, :TTOU ]
|
103
|
+
Worker.preallocate_drops(worker_processes)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Runs the thing. Returns self so you can run join on it
|
107
|
+
def start
|
108
|
+
Pitchfork.enable_child_subreaper # noop if not supported
|
109
|
+
|
110
|
+
# This socketpair is used to wake us up from select(2) in #join when signals
|
111
|
+
# are trapped. See trap_deferred.
|
112
|
+
# It's also used by newly spawned children to send their soft_signal pipe
|
113
|
+
# to the master when they are spawned.
|
114
|
+
@control_socket.replace(Pitchfork.socketpair)
|
115
|
+
@master_pid = $$
|
116
|
+
|
117
|
+
# setup signal handlers before writing pid file in case people get
|
118
|
+
# trigger happy and send signals as soon as the pid file exists.
|
119
|
+
# Note that signals don't actually get handled until the #join method
|
120
|
+
@queue_sigs.each { |sig| trap(sig) { @sig_queue << sig; awaken_master } }
|
121
|
+
trap(:CHLD) { awaken_master }
|
122
|
+
|
123
|
+
bind_listeners!
|
124
|
+
if REFORKING_AVAILABLE
|
125
|
+
spawn_initial_mold
|
126
|
+
wait_for_pending_workers
|
127
|
+
unless @children.mold
|
128
|
+
raise BootFailure, "The initial mold failed to boot"
|
129
|
+
end
|
130
|
+
else
|
131
|
+
build_app!
|
132
|
+
end
|
133
|
+
|
134
|
+
spawn_missing_workers
|
135
|
+
# We could just return here as we'd register them later in #join.
|
136
|
+
# However a good part of the test suite assumes #start only return
|
137
|
+
# once all initial workers are spawned.
|
138
|
+
wait_for_pending_workers
|
139
|
+
|
140
|
+
self
|
141
|
+
end
|
142
|
+
|
143
|
+
# replaces current listener set with +listeners+. This will
|
144
|
+
# close the socket if it will not exist in the new listener set
|
145
|
+
def listeners=(listeners)
|
146
|
+
cur_names, dead_names = [], []
|
147
|
+
listener_names.each do |name|
|
148
|
+
if name.start_with?('/')
|
149
|
+
# mark unlinked sockets as dead so we can rebind them
|
150
|
+
(File.socket?(name) ? cur_names : dead_names) << name
|
151
|
+
else
|
152
|
+
cur_names << name
|
153
|
+
end
|
154
|
+
end
|
155
|
+
set_names = listener_names(listeners)
|
156
|
+
dead_names.concat(cur_names - set_names).uniq!
|
157
|
+
|
158
|
+
LISTENERS.delete_if do |io|
|
159
|
+
if dead_names.include?(sock_name(io))
|
160
|
+
(io.close rescue nil).nil? # true
|
161
|
+
else
|
162
|
+
set_server_sockopt(io, listener_opts[sock_name(io)])
|
163
|
+
false
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
(set_names - cur_names).each { |addr| listen(addr) }
|
168
|
+
end
|
169
|
+
|
170
|
+
def logger=(obj)
|
171
|
+
Pitchfork::HttpParser::DEFAULTS["rack.logger"] = @logger = obj
|
172
|
+
end
|
173
|
+
|
174
|
+
# add a given address to the +listeners+ set, idempotently
|
175
|
+
# Allows workers to add a private, per-process listener via the
|
176
|
+
# after_fork hook. Very useful for debugging and testing.
|
177
|
+
# +:tries+ may be specified as an option for the number of times
|
178
|
+
# to retry, and +:delay+ may be specified as the time in seconds
|
179
|
+
# to delay between retries.
|
180
|
+
# A negative value for +:tries+ indicates the listen will be
|
181
|
+
# retried indefinitely, this is useful when workers belonging to
|
182
|
+
# different masters are spawned during a transparent upgrade.
|
183
|
+
def listen(address, opt = {}.merge(listener_opts[address] || {}))
|
184
|
+
address = config.expand_addr(address)
|
185
|
+
return if String === address && listener_names.include?(address)
|
186
|
+
|
187
|
+
delay = opt[:delay] || 0.5
|
188
|
+
tries = opt[:tries] || 5
|
189
|
+
begin
|
190
|
+
io = bind_listen(address, opt)
|
191
|
+
unless TCPServer === io || UNIXServer === io
|
192
|
+
io.autoclose = false
|
193
|
+
io = server_cast(io)
|
194
|
+
end
|
195
|
+
logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
|
196
|
+
LISTENERS << io
|
197
|
+
io
|
198
|
+
rescue Errno::EADDRINUSE => err
|
199
|
+
logger.error "adding listener failed addr=#{address} (in use)"
|
200
|
+
raise err if tries == 0
|
201
|
+
tries -= 1
|
202
|
+
logger.error "retrying in #{delay} seconds " \
|
203
|
+
"(#{tries < 0 ? 'infinite' : tries} tries left)"
|
204
|
+
sleep(delay)
|
205
|
+
retry
|
206
|
+
rescue => err
|
207
|
+
logger.fatal "error adding listener addr=#{address}"
|
208
|
+
raise err
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# monitors children and receives signals forever
|
213
|
+
# (or until a termination signal is sent). This handles signals
|
214
|
+
# one-at-a-time time and we'll happily drop signals in case somebody
|
215
|
+
# is signalling us too often.
|
216
|
+
def join
|
217
|
+
@respawn = true
|
218
|
+
|
219
|
+
proc_name 'master'
|
220
|
+
logger.info "master process ready" # test_exec.rb relies on this message
|
221
|
+
if @ready_pipe
|
222
|
+
begin
|
223
|
+
@ready_pipe.syswrite($$.to_s)
|
224
|
+
rescue => e
|
225
|
+
logger.warn("grandparent died too soon?: #{e.message} (#{e.class})")
|
226
|
+
end
|
227
|
+
@ready_pipe = @ready_pipe.close rescue nil
|
228
|
+
end
|
229
|
+
while true
|
230
|
+
begin
|
231
|
+
if monitor_loop == StopIteration
|
232
|
+
break
|
233
|
+
end
|
234
|
+
rescue => e
|
235
|
+
Pitchfork.log_error(@logger, "master loop error", e)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
stop # gracefully shutdown all workers on our way out
|
239
|
+
logger.info "master complete"
|
240
|
+
end
|
241
|
+
|
242
|
+
def monitor_loop(sleep = true)
|
243
|
+
reap_all_workers
|
244
|
+
case message = @sig_queue.shift
|
245
|
+
when nil
|
246
|
+
# avoid murdering workers after our master process (or the
|
247
|
+
# machine) comes out of suspend/hibernation
|
248
|
+
if (@last_check + @timeout) >= (@last_check = time_now)
|
249
|
+
sleep_time = murder_lazy_workers
|
250
|
+
else
|
251
|
+
sleep_time = @timeout/2.0 + 1
|
252
|
+
@logger.debug("waiting #{sleep_time}s after suspend/hibernation")
|
253
|
+
end
|
254
|
+
if @respawn
|
255
|
+
maintain_worker_count
|
256
|
+
automatically_refork_workers if REFORKING_AVAILABLE
|
257
|
+
end
|
258
|
+
|
259
|
+
master_sleep(sleep_time) if sleep
|
260
|
+
when :QUIT # graceful shutdown
|
261
|
+
return StopIteration
|
262
|
+
when :TERM, :INT # immediate shutdown
|
263
|
+
stop(false)
|
264
|
+
return StopIteration
|
265
|
+
when :USR2 # trigger a promotion
|
266
|
+
trigger_refork
|
267
|
+
when :TTIN
|
268
|
+
@respawn = true
|
269
|
+
self.worker_processes += 1
|
270
|
+
when :TTOU
|
271
|
+
self.worker_processes -= 1 if self.worker_processes > 0
|
272
|
+
when Message::WorkerSpawned
|
273
|
+
worker = @children.update(message)
|
274
|
+
# TODO: should we send a message to the worker to acknowledge?
|
275
|
+
logger.info "worker=#{worker.nr} pid=#{worker.pid} registered"
|
276
|
+
when Message::WorkerPromoted
|
277
|
+
old_molds = @children.molds
|
278
|
+
new_mold = @children.fetch(message.pid)
|
279
|
+
logger.info("worker=#{new_mold.nr} pid=#{new_mold.pid} promoted to a mold")
|
280
|
+
@children.update(message)
|
281
|
+
old_molds.each do |old_mold|
|
282
|
+
logger.info("Terminating old mold pid=#{old_mold.pid}")
|
283
|
+
old_mold.soft_kill(:QUIT)
|
284
|
+
end
|
285
|
+
else
|
286
|
+
logger.error("Unexpected message in sig_queue #{message.inspect}")
|
287
|
+
logger.error(@sig_queue.inspect)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Terminates all workers, but does not exit master process
|
292
|
+
def stop(graceful = true)
|
293
|
+
self.listeners = []
|
294
|
+
limit = time_now + timeout
|
295
|
+
until @children.workers.empty? || time_now > limit
|
296
|
+
if graceful
|
297
|
+
soft_kill_each_child(:QUIT)
|
298
|
+
else
|
299
|
+
kill_each_child(:TERM)
|
300
|
+
end
|
301
|
+
sleep(0.1)
|
302
|
+
reap_all_workers
|
303
|
+
end
|
304
|
+
kill_each_child(:KILL)
|
305
|
+
end
|
306
|
+
|
307
|
+
def rewindable_input
|
308
|
+
Pitchfork::HttpParser.input_class.method_defined?(:rewind)
|
309
|
+
end
|
310
|
+
|
311
|
+
def rewindable_input=(bool)
|
312
|
+
Pitchfork::HttpParser.input_class = bool ?
|
313
|
+
Pitchfork::TeeInput : Pitchfork::StreamInput
|
314
|
+
end
|
315
|
+
|
316
|
+
def client_body_buffer_size
|
317
|
+
Pitchfork::TeeInput.client_body_buffer_size
|
318
|
+
end
|
319
|
+
|
320
|
+
def client_body_buffer_size=(bytes)
|
321
|
+
Pitchfork::TeeInput.client_body_buffer_size = bytes
|
322
|
+
end
|
323
|
+
|
324
|
+
def check_client_connection
|
325
|
+
Pitchfork::HttpParser.check_client_connection
|
326
|
+
end
|
327
|
+
|
328
|
+
def check_client_connection=(bool)
|
329
|
+
Pitchfork::HttpParser.check_client_connection = bool
|
330
|
+
end
|
331
|
+
|
332
|
+
private
|
333
|
+
|
334
|
+
# wait for a signal handler to wake us up and then consume the pipe
|
335
|
+
def master_sleep(sec)
|
336
|
+
sec = MAX_SLEEP if sec > MAX_SLEEP
|
337
|
+
|
338
|
+
@control_socket[0].wait(sec) or return
|
339
|
+
case message = @control_socket[0].recvmsg_nonblock(exception: false)
|
340
|
+
when :wait_readable, NOOP
|
341
|
+
nil
|
342
|
+
else
|
343
|
+
@sig_queue << message
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def awaken_master
|
348
|
+
return if $$ != @master_pid
|
349
|
+
@control_socket[1].sendmsg_nonblock(NOOP, exception: false) # wakeup master process from select
|
350
|
+
end
|
351
|
+
|
352
|
+
# reaps all unreaped workers
|
353
|
+
def reap_all_workers
|
354
|
+
begin
|
355
|
+
wpid, status = Process.waitpid2(-1, Process::WNOHANG)
|
356
|
+
wpid or return
|
357
|
+
worker = @children.reap(wpid) and worker.close rescue nil
|
358
|
+
if worker
|
359
|
+
@after_worker_exit.call(self, worker, status)
|
360
|
+
else
|
361
|
+
logger.error("reaped unknown subprocess #{status.inspect}")
|
362
|
+
end
|
363
|
+
rescue Errno::ECHILD
|
364
|
+
break
|
365
|
+
end while true
|
366
|
+
end
|
367
|
+
|
368
|
+
def listener_sockets
|
369
|
+
listener_fds = {}
|
370
|
+
LISTENERS.each do |sock|
|
371
|
+
sock.close_on_exec = false
|
372
|
+
listener_fds[sock.fileno] = sock
|
373
|
+
end
|
374
|
+
listener_fds
|
375
|
+
end
|
376
|
+
|
377
|
+
def close_sockets_on_exec(sockets)
|
378
|
+
(3..1024).each do |io|
|
379
|
+
next if sockets.include?(io)
|
380
|
+
io = IO.for_fd(io) rescue next
|
381
|
+
io.autoclose = false
|
382
|
+
io.close_on_exec = true
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
# forcibly terminate all workers that haven't checked in in timeout seconds. The timeout is implemented using an unlinked File
|
387
|
+
def murder_lazy_workers
|
388
|
+
next_sleep = @timeout - 1
|
389
|
+
now = time_now.to_i
|
390
|
+
@children.workers.each do |worker|
|
391
|
+
tick = worker.tick
|
392
|
+
0 == tick and next # skip workers that haven't processed any clients
|
393
|
+
diff = now - tick
|
394
|
+
tmp = @timeout - diff
|
395
|
+
if tmp >= 0
|
396
|
+
next_sleep > tmp and next_sleep = tmp
|
397
|
+
next
|
398
|
+
end
|
399
|
+
next_sleep = 0
|
400
|
+
if worker.mold?
|
401
|
+
logger.error "mold pid=#{worker.pid} timeout (#{diff}s > #{@timeout}s), killing"
|
402
|
+
else
|
403
|
+
logger.error "worker=#{worker.nr} pid=#{worker.pid} timeout (#{diff}s > #{@timeout}s), killing"
|
404
|
+
end
|
405
|
+
kill_worker(:KILL, worker.pid) # take no prisoners for timeout violations
|
406
|
+
end
|
407
|
+
next_sleep <= 0 ? 1 : next_sleep
|
408
|
+
end
|
409
|
+
|
410
|
+
def trigger_refork
|
411
|
+
unless REFORKING_AVAILABLE
|
412
|
+
logger.error("This system doesn't support PR_SET_CHILD_SUBREAPER, can't promote a worker")
|
413
|
+
end
|
414
|
+
|
415
|
+
unless @children.pending_promotion?
|
416
|
+
@children.refresh
|
417
|
+
if new_mold = @mold_selector.call(self)
|
418
|
+
@children.promote(new_mold)
|
419
|
+
else
|
420
|
+
logger.error("The mold select didn't return a candidate")
|
421
|
+
end
|
422
|
+
else
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
def after_fork_internal
|
427
|
+
@control_socket[0].close_write # this is master-only, now
|
428
|
+
@ready_pipe.close if @ready_pipe
|
429
|
+
Pitchfork::Configurator::RACKUP.clear
|
430
|
+
@ready_pipe = @init_listeners = nil
|
431
|
+
|
432
|
+
# The OpenSSL PRNG is seeded with only the pid, and apps with frequently
|
433
|
+
# dying workers can recycle pids
|
434
|
+
OpenSSL::Random.seed(rand.to_s) if defined?(OpenSSL::Random)
|
435
|
+
end
|
436
|
+
|
437
|
+
def spawn_worker(worker, detach:)
|
438
|
+
before_fork.call(self, worker)
|
439
|
+
|
440
|
+
pid = fork do
|
441
|
+
# We double fork so that the new worker is re-attached back
|
442
|
+
# to the master.
|
443
|
+
# This requires either PR_SET_CHILD_SUBREAPER which is exclusive to Linux 3.4
|
444
|
+
# or the master to be PID 1.
|
445
|
+
if detach && fork
|
446
|
+
exit
|
447
|
+
end
|
448
|
+
worker.pid = Process.pid
|
449
|
+
|
450
|
+
after_fork_internal
|
451
|
+
worker_loop(worker)
|
452
|
+
if worker.mold?
|
453
|
+
mold_loop(worker)
|
454
|
+
end
|
455
|
+
exit
|
456
|
+
end
|
457
|
+
|
458
|
+
if detach
|
459
|
+
# If we double forked, we need to wait(2) so that the middle
|
460
|
+
# process doesn't end up a zombie.
|
461
|
+
Process.wait(pid)
|
462
|
+
end
|
463
|
+
|
464
|
+
worker
|
465
|
+
end
|
466
|
+
|
467
|
+
def spawn_initial_mold
|
468
|
+
mold = Worker.new(nil)
|
469
|
+
mold.create_socketpair!
|
470
|
+
mold.pid = fork do
|
471
|
+
after_fork_internal
|
472
|
+
mold.after_fork_in_child
|
473
|
+
build_app!
|
474
|
+
mold_loop(mold)
|
475
|
+
end
|
476
|
+
@children.register_mold(mold)
|
477
|
+
end
|
478
|
+
|
479
|
+
def spawn_missing_workers
|
480
|
+
worker_nr = -1
|
481
|
+
until (worker_nr += 1) == @worker_processes
|
482
|
+
if @children.nr_alive?(worker_nr)
|
483
|
+
next
|
484
|
+
end
|
485
|
+
worker = Pitchfork::Worker.new(worker_nr)
|
486
|
+
|
487
|
+
if !@children.mold || !@children.mold.spawn_worker(worker)
|
488
|
+
# If there's no mold, or the mold was somehow unreachable
|
489
|
+
# we fallback to spawning the missing workers ourselves.
|
490
|
+
spawn_worker(worker, detach: false)
|
491
|
+
end
|
492
|
+
# We could directly register workers when we spawn from the
|
493
|
+
# master, like pitchfork does. However it is preferable to
|
494
|
+
# always go through the asynchronous registering process for
|
495
|
+
# consistency.
|
496
|
+
@children.register(worker)
|
497
|
+
end
|
498
|
+
rescue => e
|
499
|
+
@logger.error(e) rescue nil
|
500
|
+
exit!
|
501
|
+
end
|
502
|
+
|
503
|
+
def wait_for_pending_workers
|
504
|
+
while @children.pending_workers?
|
505
|
+
master_sleep(0.5)
|
506
|
+
if monitor_loop(false) == StopIteration
|
507
|
+
break
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
def maintain_worker_count
|
513
|
+
(off = @children.workers_count - worker_processes) == 0 and return
|
514
|
+
off < 0 and return spawn_missing_workers
|
515
|
+
@children.each_worker { |w| w.nr >= worker_processes and w.soft_kill(:QUIT) }
|
516
|
+
end
|
517
|
+
|
518
|
+
def automatically_refork_workers
|
519
|
+
# If we're already in the middle of forking a new generation, we just continue
|
520
|
+
if @children.mold
|
521
|
+
# We don't shutdown any outdated worker if any worker is already being spawned
|
522
|
+
# or a worker is exiting. Workers are only reforked one by one to minimize the
|
523
|
+
# impact on capacity.
|
524
|
+
# In the future we may want to use a dynamic limit, e.g. 10% of workers may be down at
|
525
|
+
# a time.
|
526
|
+
return if @children.pending_workers?
|
527
|
+
return if @children.workers.any?(&:exiting?)
|
528
|
+
|
529
|
+
if outdated_worker = @children.workers.find { |w| w.generation < @children.mold.generation }
|
530
|
+
logger.info("worker=#{outdated_worker.nr} pid=#{outdated_worker.pid} restarting")
|
531
|
+
outdated_worker.soft_kill(:QUIT)
|
532
|
+
return # That's all folks
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
# If all workers are alive and well, we can consider reforking a new generation
|
537
|
+
if @refork_condition
|
538
|
+
@children.refresh
|
539
|
+
if @refork_condition.met?(@children, logger)
|
540
|
+
logger.info("Refork condition met, scheduling a promotion")
|
541
|
+
unless @sig_queue.include?(:USR2)
|
542
|
+
@sig_queue << :USR2
|
543
|
+
awaken_master
|
544
|
+
end
|
545
|
+
end
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
# if we get any error, try to write something back to the client
|
550
|
+
# assuming we haven't closed the socket, but don't get hung up
|
551
|
+
# if the socket is already closed or broken. We'll always ensure
|
552
|
+
# the socket is closed at the end of this function
|
553
|
+
def handle_error(client, e)
|
554
|
+
code = case e
|
555
|
+
when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::ENOTCONN
|
556
|
+
# client disconnected on us and there's nothing we can do
|
557
|
+
when Pitchfork::RequestURITooLongError
|
558
|
+
414
|
559
|
+
when Pitchfork::RequestEntityTooLargeError
|
560
|
+
413
|
561
|
+
when Pitchfork::HttpParserError # try to tell the client they're bad
|
562
|
+
400
|
563
|
+
else
|
564
|
+
Pitchfork.log_error(@logger, "app error", e)
|
565
|
+
500
|
566
|
+
end
|
567
|
+
if code
|
568
|
+
client.write_nonblock(err_response(code, @request.response_start_sent), exception: false)
|
569
|
+
end
|
570
|
+
client.close
|
571
|
+
rescue
|
572
|
+
end
|
573
|
+
|
574
|
+
def e103_response_write(client, headers)
|
575
|
+
response = if @request.response_start_sent
|
576
|
+
"103 Early Hints\r\n"
|
577
|
+
else
|
578
|
+
"HTTP/1.1 103 Early Hints\r\n"
|
579
|
+
end
|
580
|
+
|
581
|
+
headers.each_pair do |k, vs|
|
582
|
+
next if !vs || vs.empty?
|
583
|
+
values = vs.to_s.split("\n".freeze)
|
584
|
+
values.each do |v|
|
585
|
+
response << "#{k}: #{v}\r\n"
|
586
|
+
end
|
587
|
+
end
|
588
|
+
response << "\r\n".freeze
|
589
|
+
response << "HTTP/1.1 ".freeze if @request.response_start_sent
|
590
|
+
client.write(response)
|
591
|
+
end
|
592
|
+
|
593
|
+
def e100_response_write(client, env)
|
594
|
+
# We use String#freeze to avoid allocations under Ruby 2.1+
|
595
|
+
# Not many users hit this code path, so it's better to reduce the
|
596
|
+
# constant table sizes even for Ruby 2.0 users who'll hit extra
|
597
|
+
# allocations here.
|
598
|
+
client.write(@request.response_start_sent ?
|
599
|
+
"100 Continue\r\n\r\nHTTP/1.1 ".freeze :
|
600
|
+
"HTTP/1.1 100 Continue\r\n\r\n".freeze)
|
601
|
+
env.delete('HTTP_EXPECT'.freeze)
|
602
|
+
end
|
603
|
+
|
604
|
+
# once a client is accepted, it is processed in its entirety here
|
605
|
+
# in 3 easy steps: read request, call app, write app response
|
606
|
+
def process_client(client)
|
607
|
+
@request = Pitchfork::HttpParser.new
|
608
|
+
env = @request.read(client)
|
609
|
+
|
610
|
+
if early_hints
|
611
|
+
env["rack.early_hints"] = lambda do |headers|
|
612
|
+
e103_response_write(client, headers)
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
env["rack.after_reply"] = []
|
617
|
+
|
618
|
+
status, headers, body = @app.call(env)
|
619
|
+
|
620
|
+
begin
|
621
|
+
return if @request.hijacked?
|
622
|
+
|
623
|
+
if 100 == status.to_i
|
624
|
+
e100_response_write(client, env)
|
625
|
+
status, headers, body = @app.call(env)
|
626
|
+
return if @request.hijacked?
|
627
|
+
end
|
628
|
+
@request.headers? or headers = nil
|
629
|
+
http_response_write(client, status, headers, body, @request)
|
630
|
+
ensure
|
631
|
+
body.respond_to?(:close) and body.close
|
632
|
+
end
|
633
|
+
|
634
|
+
unless client.closed? # rack.hijack may've close this for us
|
635
|
+
client.shutdown # in case of fork() in Rack app
|
636
|
+
client.close # flush and uncork socket immediately, no keepalive
|
637
|
+
end
|
638
|
+
rescue => e
|
639
|
+
handle_error(client, e)
|
640
|
+
ensure
|
641
|
+
env["rack.after_reply"].each(&:call) if env
|
642
|
+
end
|
643
|
+
|
644
|
+
def nuke_listeners!(readers)
|
645
|
+
# only called from the worker, ordering is important here
|
646
|
+
tmp = readers.dup
|
647
|
+
readers.replace([false]) # ensure worker does not continue ASAP
|
648
|
+
tmp.each { |io| io.close rescue nil } # break out of IO.select
|
649
|
+
end
|
650
|
+
|
651
|
+
# gets rid of stuff the worker has no business keeping track of
|
652
|
+
# to free some resources and drops all sig handlers.
|
653
|
+
# traps for USR2, and HUP may be set in the after_fork Proc
|
654
|
+
# by the user.
|
655
|
+
def init_worker_process(worker)
|
656
|
+
worker.reset
|
657
|
+
worker.register_to_master(@control_socket[1])
|
658
|
+
# we'll re-trap :QUIT later for graceful shutdown iff we accept clients
|
659
|
+
exit_sigs = [ :QUIT, :TERM, :INT ]
|
660
|
+
exit_sigs.each { |sig| trap(sig) { exit!(0) } }
|
661
|
+
exit!(0) if (@sig_queue & exit_sigs)[0]
|
662
|
+
(@queue_sigs - exit_sigs).each { |sig| trap(sig, nil) }
|
663
|
+
trap(:CHLD, 'DEFAULT')
|
664
|
+
@sig_queue.clear
|
665
|
+
proc_name "worker[#{worker.nr}] (gen:#{worker.generation})"
|
666
|
+
@children = nil
|
667
|
+
|
668
|
+
after_fork.call(self, worker) # can drop perms and create listeners
|
669
|
+
LISTENERS.each { |sock| sock.close_on_exec = true }
|
670
|
+
|
671
|
+
@config = nil
|
672
|
+
@listener_opts = @orig_app = nil
|
673
|
+
readers = LISTENERS.dup
|
674
|
+
readers << worker
|
675
|
+
trap(:QUIT) { nuke_listeners!(readers) }
|
676
|
+
readers
|
677
|
+
end
|
678
|
+
|
679
|
+
def init_mold_process(worker)
|
680
|
+
proc_name "mold (gen: #{worker.generation})"
|
681
|
+
readers = [worker]
|
682
|
+
trap(:QUIT) { nuke_listeners!(readers) }
|
683
|
+
readers
|
684
|
+
end
|
685
|
+
|
686
|
+
if Pitchfork.const_defined?(:Waiter)
|
687
|
+
def prep_readers(readers)
|
688
|
+
Pitchfork::Waiter.prep_readers(readers)
|
689
|
+
end
|
690
|
+
else
|
691
|
+
require_relative 'select_waiter'
|
692
|
+
def prep_readers(_readers)
|
693
|
+
Pitchfork::SelectWaiter.new
|
694
|
+
end
|
695
|
+
end
|
696
|
+
|
697
|
+
# runs inside each forked worker, this sits around and waits
|
698
|
+
# for connections and doesn't die until the parent dies (or is
|
699
|
+
# given a INT, QUIT, or TERM signal)
|
700
|
+
def worker_loop(worker)
|
701
|
+
readers = init_worker_process(worker)
|
702
|
+
waiter = prep_readers(readers)
|
703
|
+
|
704
|
+
ready = readers.dup
|
705
|
+
@after_worker_ready.call(self, worker)
|
706
|
+
|
707
|
+
begin
|
708
|
+
worker.tick = time_now.to_i
|
709
|
+
while sock = ready.shift
|
710
|
+
# Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
|
711
|
+
# but that will return false
|
712
|
+
client = sock.accept_nonblock(exception: false)
|
713
|
+
client = false if client == :wait_readable
|
714
|
+
if client
|
715
|
+
case client
|
716
|
+
when Message
|
717
|
+
worker.update(client)
|
718
|
+
else
|
719
|
+
process_client(client)
|
720
|
+
worker.increment_requests_count
|
721
|
+
end
|
722
|
+
worker.tick = time_now.to_i
|
723
|
+
end
|
724
|
+
return if worker.mold? # We've been promoted we can exit the loop
|
725
|
+
end
|
726
|
+
|
727
|
+
# timeout so we can .tick and keep parent from SIGKILL-ing us
|
728
|
+
worker.tick = time_now.to_i
|
729
|
+
waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
|
730
|
+
rescue => e
|
731
|
+
Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
|
732
|
+
end while readers[0]
|
733
|
+
end
|
734
|
+
|
735
|
+
def mold_loop(mold)
|
736
|
+
readers = init_mold_process(mold)
|
737
|
+
waiter = prep_readers(readers)
|
738
|
+
mold.acknowlege_promotion(@control_socket[1])
|
739
|
+
|
740
|
+
ready = readers.dup
|
741
|
+
# TODO: mold ready callback?
|
742
|
+
|
743
|
+
begin
|
744
|
+
mold.tick = time_now.to_i
|
745
|
+
while sock = ready.shift
|
746
|
+
# Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
|
747
|
+
# but that will return false
|
748
|
+
message = sock.accept_nonblock(exception: false)
|
749
|
+
case message
|
750
|
+
when false
|
751
|
+
# no message, keep looping
|
752
|
+
when Message::SpawnWorker
|
753
|
+
spawn_worker(Worker.new(message.nr, generation: mold.generation), detach: true)
|
754
|
+
else
|
755
|
+
logger.error("Unexpected mold message #{message.inspect}")
|
756
|
+
end
|
757
|
+
end
|
758
|
+
|
759
|
+
# timeout so we can .tick and keep parent from SIGKILL-ing us
|
760
|
+
mold.tick = time_now.to_i
|
761
|
+
waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
|
762
|
+
rescue => e
|
763
|
+
Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
|
764
|
+
end while readers[0]
|
765
|
+
end
|
766
|
+
|
767
|
+
# delivers a signal to a worker and fails gracefully if the worker
|
768
|
+
# is no longer running.
|
769
|
+
def kill_worker(signal, wpid)
|
770
|
+
Process.kill(signal, wpid)
|
771
|
+
rescue Errno::ESRCH
|
772
|
+
worker = @children.reap(wpid) and worker.close rescue nil
|
773
|
+
end
|
774
|
+
|
775
|
+
# delivers a signal to each worker
|
776
|
+
def kill_each_child(signal)
|
777
|
+
@children.each { |w| kill_worker(signal, w.pid) }
|
778
|
+
end
|
779
|
+
|
780
|
+
def soft_kill_each_child(signal)
|
781
|
+
@children.each { |worker| worker.soft_kill(signal) }
|
782
|
+
end
|
783
|
+
|
784
|
+
# returns an array of string names for the given listener array
|
785
|
+
def listener_names(listeners = LISTENERS)
|
786
|
+
listeners.map { |io| sock_name(io) }
|
787
|
+
end
|
788
|
+
|
789
|
+
def build_app!
|
790
|
+
return unless app.respond_to?(:arity)
|
791
|
+
|
792
|
+
self.app = case app.arity
|
793
|
+
when 0
|
794
|
+
app.call
|
795
|
+
when 2
|
796
|
+
app.call(nil, self)
|
797
|
+
when 1
|
798
|
+
app # already a rack app
|
799
|
+
end
|
800
|
+
end
|
801
|
+
|
802
|
+
def proc_name(tag)
|
803
|
+
$0 = ([ File.basename(START_CTX[0]), tag
|
804
|
+
]).concat(START_CTX[:argv]).join(' ')
|
805
|
+
end
|
806
|
+
|
807
|
+
def bind_listeners!
|
808
|
+
listeners = config[:listeners].dup
|
809
|
+
if listeners.empty?
|
810
|
+
listeners << Pitchfork::Const::DEFAULT_LISTEN
|
811
|
+
@init_listeners << Pitchfork::Const::DEFAULT_LISTEN
|
812
|
+
START_CTX[:argv] << "-l#{Pitchfork::Const::DEFAULT_LISTEN}"
|
813
|
+
end
|
814
|
+
listeners.each { |addr| listen(addr) }
|
815
|
+
raise ArgumentError, "no listeners" if LISTENERS.empty?
|
816
|
+
end
|
817
|
+
|
818
|
+
def time_now
|
819
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
820
|
+
end
|
821
|
+
end
|
822
|
+
end
|