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.

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