pitchfork 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.git-blame-ignore-revs +3 -0
  3. data/.gitattributes +5 -0
  4. data/.github/workflows/ci.yml +30 -0
  5. data/.gitignore +23 -0
  6. data/COPYING +674 -0
  7. data/Dockerfile +4 -0
  8. data/Gemfile +9 -0
  9. data/Gemfile.lock +30 -0
  10. data/LICENSE +67 -0
  11. data/README.md +123 -0
  12. data/Rakefile +72 -0
  13. data/docs/Application_Timeouts.md +74 -0
  14. data/docs/CONFIGURATION.md +388 -0
  15. data/docs/DESIGN.md +86 -0
  16. data/docs/FORK_SAFETY.md +80 -0
  17. data/docs/PHILOSOPHY.md +90 -0
  18. data/docs/REFORKING.md +113 -0
  19. data/docs/SIGNALS.md +38 -0
  20. data/docs/TUNING.md +106 -0
  21. data/examples/constant_caches.ru +43 -0
  22. data/examples/echo.ru +25 -0
  23. data/examples/hello.ru +5 -0
  24. data/examples/nginx.conf +156 -0
  25. data/examples/pitchfork.conf.minimal.rb +5 -0
  26. data/examples/pitchfork.conf.rb +77 -0
  27. data/examples/unicorn.socket +11 -0
  28. data/exe/pitchfork +116 -0
  29. data/ext/pitchfork_http/CFLAGS +13 -0
  30. data/ext/pitchfork_http/c_util.h +116 -0
  31. data/ext/pitchfork_http/child_subreaper.h +25 -0
  32. data/ext/pitchfork_http/common_field_optimization.h +130 -0
  33. data/ext/pitchfork_http/epollexclusive.h +124 -0
  34. data/ext/pitchfork_http/ext_help.h +38 -0
  35. data/ext/pitchfork_http/extconf.rb +14 -0
  36. data/ext/pitchfork_http/global_variables.h +97 -0
  37. data/ext/pitchfork_http/httpdate.c +79 -0
  38. data/ext/pitchfork_http/pitchfork_http.c +4318 -0
  39. data/ext/pitchfork_http/pitchfork_http.rl +1024 -0
  40. data/ext/pitchfork_http/pitchfork_http_common.rl +76 -0
  41. data/lib/pitchfork/app/old_rails/static.rb +59 -0
  42. data/lib/pitchfork/children.rb +124 -0
  43. data/lib/pitchfork/configurator.rb +314 -0
  44. data/lib/pitchfork/const.rb +23 -0
  45. data/lib/pitchfork/http_parser.rb +206 -0
  46. data/lib/pitchfork/http_response.rb +63 -0
  47. data/lib/pitchfork/http_server.rb +822 -0
  48. data/lib/pitchfork/launcher.rb +9 -0
  49. data/lib/pitchfork/mem_info.rb +36 -0
  50. data/lib/pitchfork/message.rb +130 -0
  51. data/lib/pitchfork/mold_selector.rb +29 -0
  52. data/lib/pitchfork/preread_input.rb +33 -0
  53. data/lib/pitchfork/refork_condition.rb +21 -0
  54. data/lib/pitchfork/select_waiter.rb +9 -0
  55. data/lib/pitchfork/socket_helper.rb +199 -0
  56. data/lib/pitchfork/stream_input.rb +152 -0
  57. data/lib/pitchfork/tee_input.rb +133 -0
  58. data/lib/pitchfork/tmpio.rb +35 -0
  59. data/lib/pitchfork/version.rb +8 -0
  60. data/lib/pitchfork/worker.rb +244 -0
  61. data/lib/pitchfork.rb +158 -0
  62. data/pitchfork.gemspec +30 -0
  63. 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