pitchfork 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pitchfork might be problematic. Click here for more details.
- 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
|