pitchfork 0.5.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +0 -1
- data/CHANGELOG.md +19 -0
- data/Dockerfile +1 -1
- data/Gemfile.lock +3 -3
- data/benchmark/cow_benchmark.rb +1 -1
- data/docs/CONFIGURATION.md +16 -1
- data/docs/FORK_SAFETY.md +8 -6
- data/docs/SIGNALS.md +4 -4
- data/lib/pitchfork/configurator.rb +16 -2
- data/lib/pitchfork/flock.rb +4 -0
- data/lib/pitchfork/http_server.rb +104 -39
- data/lib/pitchfork/info.rb +78 -0
- data/lib/pitchfork/shared_memory.rb +71 -0
- data/lib/pitchfork/version.rb +1 -1
- data/lib/pitchfork/worker.rb +15 -43
- data/lib/pitchfork.rb +8 -4
- metadata +4 -3
- data/lib/pitchfork/preread_input.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 05372d35dd4784eb22e204b614b1c7add13fd0e298495543c5395e2f03b42073
|
4
|
+
data.tar.gz: 14f241efeb95774f6de6e9fc8926815eb7f10c25965a09a0700f77dae75dbb92
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f26ebeeb3bc9f7533d25cc41b29a317fb8a80ce108a1859657adfa80b9eb8f88812cfe85dfecf0fbd8093b6e91f45c6ce51ab7b9a15864d96b9d0f5f77835786
|
7
|
+
data.tar.gz: 8d1f09bb24226f2c89b2db2d549c28508a9f2ea716a1612fcc3367d27445657b9d9da281e623af696dc803acd649847a724366306caf672dbc523a3e7495579b
|
data/.github/workflows/ci.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,24 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# 0.7.0
|
4
|
+
|
5
|
+
- Set nicer `proctile` to better see the state of the process tree at a glance.
|
6
|
+
- Pass the last request env to `after_request_complete` callback.
|
7
|
+
- Fix the slow rollout of workers on a new generation.
|
8
|
+
- Expose `Pitchfork::Info.fork_safe?` and `Pitchfork::Info.no_longer_fork_safe!`.
|
9
|
+
|
10
|
+
# 0.6.0
|
11
|
+
|
12
|
+
- Expose `Pitchfork::Info.workers_count` and `.live_workers_count` to be consumed by application health checks.
|
13
|
+
- Implement `before_worker_exit` callback.
|
14
|
+
- Make each mold and worker a process group leader.
|
15
|
+
- Get rid of `Pitchfork::PrereadInput`.
|
16
|
+
- Add `Pitchfork.shutting_down?` to allow health check endpoints to fail sooner on graceful shutdowns.
|
17
|
+
- Treat `TERM` as graceful shutdown rather than quick shutdown.
|
18
|
+
- Implement `after_worker_hard_timeout` callback.
|
19
|
+
|
20
|
+
# 0.5.0
|
21
|
+
|
3
22
|
- Added a soft timeout in addition to the historical Unicorn hard timeout.
|
4
23
|
On soft timeout, the `after_worker_timeout` callback is invoked.
|
5
24
|
- Implement `after_request_complete` callback.
|
data/Dockerfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pitchfork (0.
|
4
|
+
pitchfork (0.7.0)
|
5
5
|
rack (>= 2.0)
|
6
6
|
raindrops (~> 0.7)
|
7
7
|
|
@@ -9,8 +9,8 @@ GEM
|
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
11
|
minitest (5.15.0)
|
12
|
-
nio4r (2.5.
|
13
|
-
puma (6.
|
12
|
+
nio4r (2.5.9)
|
13
|
+
puma (6.3.0)
|
14
14
|
nio4r (~> 2.0)
|
15
15
|
rack (3.0.8)
|
16
16
|
raindrops (0.20.1)
|
data/benchmark/cow_benchmark.rb
CHANGED
data/docs/CONFIGURATION.md
CHANGED
@@ -336,6 +336,21 @@ really have to, make sure to configure the `cleanup` timeout so that the
|
|
336
336
|
callback has time to complete before the "hard" timeout triggers.
|
337
337
|
By default the cleanup timeout is 2 seconds.
|
338
338
|
|
339
|
+
### `after_worker_hard_timeout`
|
340
|
+
|
341
|
+
Called in the master process when a worker hard timeout is elapsed:
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
after_worker_timeout do |server, worker|
|
345
|
+
$stderr.puts "Worker hard timeout, pid=#{worker.pid}"
|
346
|
+
end
|
347
|
+
```
|
348
|
+
|
349
|
+
Once the callback complete, the worker will be signaled with `SIGKILL`.
|
350
|
+
|
351
|
+
This callback being called in an indication that something is preventing the
|
352
|
+
soft timeout from working.
|
353
|
+
|
339
354
|
### `after_worker_exit`
|
340
355
|
|
341
356
|
Called in the master process after a worker exits.
|
@@ -356,7 +371,7 @@ Called in the worker processes after a request has completed.
|
|
356
371
|
Can be used for out of band work, or to exit unhealthy workers.
|
357
372
|
|
358
373
|
```ruby
|
359
|
-
after_request_complete do |server, worker|
|
374
|
+
after_request_complete do |server, worker, env|
|
360
375
|
if something_wrong?
|
361
376
|
exit
|
362
377
|
end
|
data/docs/FORK_SAFETY.md
CHANGED
@@ -76,10 +76,12 @@ impact of discovering such bug.
|
|
76
76
|
|
77
77
|
## Known Incompatible Gems
|
78
78
|
|
79
|
-
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
If you really have to consume a gRPC API, you can consider `grpc_kit` as a replacement.
|
79
|
+
- The `grpc` isn't fork safe by default, but starting from version `1.57.0`, it does provide an experimental
|
80
|
+
fork safe option that requires setting an environment variable before loading the library, and calling
|
81
|
+
`GRPC.prefork`, `GRPC.postfork_parent` and `GRPC.postfork_child` around fork calls.
|
82
|
+
(https://github.com/grpc/grpc/pull/33430)
|
84
83
|
|
85
|
-
|
84
|
+
- The `ruby-vips` gem binds the `libvips` image processing library that isn't fork safe.
|
85
|
+
(https://github.com/libvips/libvips/discussions/3577)
|
86
|
+
|
87
|
+
No other gem is known to be incompatible for now, but if you find one please open an issue to add it to the list.
|
data/docs/SIGNALS.md
CHANGED
@@ -6,9 +6,9 @@ processes are documented here as well.
|
|
6
6
|
|
7
7
|
### Master Process
|
8
8
|
|
9
|
-
* `INT
|
9
|
+
* `INT` - quick shutdown, kills all workers immediately
|
10
10
|
|
11
|
-
* `QUIT` - graceful shutdown, waits for workers to finish their
|
11
|
+
* `QUIT/TERM` - graceful shutdown, waits for workers to finish their
|
12
12
|
current request before finishing.
|
13
13
|
|
14
14
|
* `USR2` - trigger a manual refork. A worker is promoted as
|
@@ -29,10 +29,10 @@ Sending signals directly to the worker processes should not normally be
|
|
29
29
|
needed. If the master process is running, any exited worker will be
|
30
30
|
automatically respawned.
|
31
31
|
|
32
|
-
* `INT
|
32
|
+
* `INT` - Quick shutdown, immediately exit.
|
33
33
|
The master process will respawn a worker to replace this one.
|
34
34
|
Immediate shutdown is still triggered using kill(2) and not the
|
35
35
|
internal pipe as of unicorn 4.8
|
36
36
|
|
37
|
-
* `QUIT` - Gracefully exit after finishing the current request.
|
37
|
+
* `QUIT/TERM` - Gracefully exit after finishing the current request.
|
38
38
|
The master process will respawn a worker to replace this one.
|
@@ -26,11 +26,15 @@ module Pitchfork
|
|
26
26
|
}
|
27
27
|
|
28
28
|
# Default settings for Pitchfork
|
29
|
+
default_logger = Logger.new($stderr)
|
30
|
+
default_logger.formatter = Logger::Formatter.new
|
31
|
+
default_logger.progname = "[Pitchfork]"
|
32
|
+
|
29
33
|
DEFAULTS = {
|
30
34
|
:soft_timeout => 20,
|
31
35
|
:cleanup_timeout => 2,
|
32
36
|
:timeout => 22,
|
33
|
-
:logger =>
|
37
|
+
:logger => default_logger,
|
34
38
|
:worker_processes => 1,
|
35
39
|
:after_worker_fork => lambda { |server, worker|
|
36
40
|
server.logger.info("worker=#{worker.nr} gen=#{worker.generation} pid=#{$$} spawned")
|
@@ -38,6 +42,7 @@ module Pitchfork
|
|
38
42
|
:after_mold_fork => lambda { |server, worker|
|
39
43
|
server.logger.info("mold gen=#{worker.generation} pid=#{$$} spawned")
|
40
44
|
},
|
45
|
+
:before_worker_exit => nil,
|
41
46
|
:after_worker_exit => lambda { |server, worker, status|
|
42
47
|
m = if worker.nil?
|
43
48
|
"repead unknown process (#{status.inspect})"
|
@@ -56,6 +61,7 @@ module Pitchfork
|
|
56
61
|
server.logger.info("worker=#{worker.nr} gen=#{worker.generation} ready")
|
57
62
|
},
|
58
63
|
:after_worker_timeout => nil,
|
64
|
+
:after_worker_hard_timeout => nil,
|
59
65
|
:after_request_complete => nil,
|
60
66
|
:early_hints => false,
|
61
67
|
:refork_condition => nil,
|
@@ -139,12 +145,20 @@ module Pitchfork
|
|
139
145
|
set_hook(:after_worker_timeout, block_given? ? block : args[0], 3)
|
140
146
|
end
|
141
147
|
|
148
|
+
def after_worker_hard_timeout(*args, &block)
|
149
|
+
set_hook(:after_worker_hard_timeout, block_given? ? block : args[0], 2)
|
150
|
+
end
|
151
|
+
|
152
|
+
def before_worker_exit(*args, &block)
|
153
|
+
set_hook(:before_worker_exit, block_given? ? block : args[0], 2)
|
154
|
+
end
|
155
|
+
|
142
156
|
def after_worker_exit(*args, &block)
|
143
157
|
set_hook(:after_worker_exit, block_given? ? block : args[0], 3)
|
144
158
|
end
|
145
159
|
|
146
160
|
def after_request_complete(*args, &block)
|
147
|
-
set_hook(:after_request_complete, block_given? ? block : args[0])
|
161
|
+
set_hook(:after_request_complete, block_given? ? block : args[0], 3)
|
148
162
|
end
|
149
163
|
|
150
164
|
def timeout(seconds, cleanup: 2)
|
data/lib/pitchfork/flock.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
require 'pitchfork/pitchfork_http'
|
3
3
|
require 'pitchfork/flock'
|
4
4
|
require 'pitchfork/soft_timeout'
|
5
|
+
require 'pitchfork/shared_memory'
|
6
|
+
require 'pitchfork/info'
|
5
7
|
|
6
8
|
module Pitchfork
|
7
9
|
# This is the process manager of Pitchfork. This manages worker
|
@@ -44,14 +46,15 @@ module Pitchfork
|
|
44
46
|
end
|
45
47
|
|
46
48
|
def call(original_thread) # :nodoc:
|
47
|
-
|
48
|
-
|
49
|
-
@callback
|
49
|
+
begin
|
50
|
+
@server.logger.error("worker=#{@worker.nr} pid=#{@worker.pid} timed out, exiting")
|
51
|
+
if @callback
|
52
|
+
@callback.call(@server, @worker, Info.new(original_thread, @rack_env))
|
53
|
+
end
|
54
|
+
rescue => error
|
55
|
+
Pitchfork.log_error(@server.logger, "after_worker_timeout error", error)
|
50
56
|
end
|
51
|
-
|
52
|
-
Pitchfork.log_error(@server.logger, "after_worker_timeout error", error)
|
53
|
-
ensure
|
54
|
-
exit
|
57
|
+
@server.worker_exit(@worker)
|
55
58
|
end
|
56
59
|
|
57
60
|
def finished # :nodoc:
|
@@ -76,8 +79,8 @@ module Pitchfork
|
|
76
79
|
:listener_opts, :children,
|
77
80
|
:orig_app, :config, :ready_pipe,
|
78
81
|
:default_middleware, :early_hints
|
79
|
-
attr_writer :after_worker_exit, :
|
80
|
-
:after_worker_timeout
|
82
|
+
attr_writer :after_worker_exit, :before_worker_exit, :after_worker_ready, :after_request_complete,
|
83
|
+
:refork_condition, :after_worker_timeout, :after_worker_hard_timeout
|
81
84
|
|
82
85
|
attr_reader :logger
|
83
86
|
include Pitchfork::SocketHelper
|
@@ -88,7 +91,7 @@ module Pitchfork
|
|
88
91
|
# in new projects
|
89
92
|
LISTENERS = []
|
90
93
|
|
91
|
-
NOOP = '.'
|
94
|
+
NOOP = '.'.freeze
|
92
95
|
|
93
96
|
# :startdoc:
|
94
97
|
# This Hash is considered a stable interface and changing its contents
|
@@ -126,6 +129,7 @@ module Pitchfork
|
|
126
129
|
@respawn = false
|
127
130
|
@last_check = Pitchfork.time_now
|
128
131
|
@promotion_lock = Flock.new("pitchfork-promotion")
|
132
|
+
Info.keep_io(@promotion_lock)
|
129
133
|
|
130
134
|
options = options.dup
|
131
135
|
@ready_pipe = options.delete(:ready_pipe)
|
@@ -134,6 +138,8 @@ module Pitchfork
|
|
134
138
|
self.config = Pitchfork::Configurator.new(options)
|
135
139
|
self.listener_opts = {}
|
136
140
|
|
141
|
+
proc_name role: 'monitor', status: START_CTX[:argv].join(' ')
|
142
|
+
|
137
143
|
# We use @control_socket differently in the master and worker processes:
|
138
144
|
#
|
139
145
|
# * The master process never closes or reinitializes this once
|
@@ -163,7 +169,9 @@ module Pitchfork
|
|
163
169
|
# list of signals we care about and trap in master.
|
164
170
|
@queue_sigs = [
|
165
171
|
:QUIT, :INT, :TERM, :USR2, :TTIN, :TTOU ]
|
166
|
-
|
172
|
+
|
173
|
+
Info.workers_count = worker_processes
|
174
|
+
SharedMemory.preallocate_drops(worker_processes)
|
167
175
|
end
|
168
176
|
|
169
177
|
# Runs the thing. Returns self so you can run join on it
|
@@ -175,6 +183,7 @@ module Pitchfork
|
|
175
183
|
# It's also used by newly spawned children to send their soft_signal pipe
|
176
184
|
# to the master when they are spawned.
|
177
185
|
@control_socket.replace(Pitchfork.socketpair)
|
186
|
+
Info.keep_ios(@control_socket)
|
178
187
|
@master_pid = $$
|
179
188
|
|
180
189
|
# setup signal handlers before writing pid file in case people get
|
@@ -259,6 +268,7 @@ module Pitchfork
|
|
259
268
|
io = server_cast(io)
|
260
269
|
end
|
261
270
|
logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
|
271
|
+
Info.keep_io(io)
|
262
272
|
LISTENERS << io
|
263
273
|
io
|
264
274
|
rescue Errno::EADDRINUSE => err
|
@@ -282,7 +292,8 @@ module Pitchfork
|
|
282
292
|
def join
|
283
293
|
@respawn = true
|
284
294
|
|
285
|
-
proc_name '
|
295
|
+
proc_name role: 'monitor', status: START_CTX[:argv].join(' ')
|
296
|
+
|
286
297
|
logger.info "master process ready" # test_exec.rb relies on this message
|
287
298
|
if @ready_pipe
|
288
299
|
begin
|
@@ -312,7 +323,7 @@ module Pitchfork
|
|
312
323
|
if REFORKING_AVAILABLE && @respawn && @children.molds.empty?
|
313
324
|
logger.info("No mold alive, shutting down")
|
314
325
|
@exit_status = 1
|
315
|
-
@sig_queue << :
|
326
|
+
@sig_queue << :TERM
|
316
327
|
@respawn = false
|
317
328
|
end
|
318
329
|
|
@@ -332,15 +343,21 @@ module Pitchfork
|
|
332
343
|
end
|
333
344
|
|
334
345
|
master_sleep(sleep_time) if sleep
|
335
|
-
when :QUIT # graceful shutdown
|
336
|
-
|
346
|
+
when :QUIT, :TERM # graceful shutdown
|
347
|
+
SharedMemory.shutting_down!
|
348
|
+
logger.info "#{message} received, starting graceful shutdown"
|
337
349
|
return StopIteration
|
338
|
-
when :
|
350
|
+
when :INT # immediate shutdown
|
351
|
+
SharedMemory.shutting_down!
|
339
352
|
logger.info "#{message} received, starting immediate shutdown"
|
340
353
|
stop(false)
|
341
354
|
return StopIteration
|
342
355
|
when :USR2 # trigger a promotion
|
343
|
-
|
356
|
+
if @respawn
|
357
|
+
trigger_refork
|
358
|
+
else
|
359
|
+
logger.error "Can't trigger a refork as the server is shutting down"
|
360
|
+
end
|
344
361
|
when :TTIN
|
345
362
|
@respawn = true
|
346
363
|
self.worker_processes += 1
|
@@ -359,7 +376,7 @@ module Pitchfork
|
|
359
376
|
logger.info("mold pid=#{new_mold.pid} gen=#{new_mold.generation} ready")
|
360
377
|
old_molds.each do |old_mold|
|
361
378
|
logger.info("Terminating old mold pid=#{old_mold.pid} gen=#{old_mold.generation}")
|
362
|
-
old_mold.soft_kill(:
|
379
|
+
old_mold.soft_kill(:TERM)
|
363
380
|
end
|
364
381
|
else
|
365
382
|
logger.error("Unexpected message in sig_queue #{message.inspect}")
|
@@ -369,15 +386,17 @@ module Pitchfork
|
|
369
386
|
|
370
387
|
# Terminates all workers, but does not exit master process
|
371
388
|
def stop(graceful = true)
|
389
|
+
proc_name role: 'monitor', status: 'shutting down'
|
372
390
|
@respawn = false
|
391
|
+
SharedMemory.shutting_down!
|
373
392
|
wait_for_pending_workers
|
374
393
|
self.listeners = []
|
375
394
|
limit = Pitchfork.time_now + timeout
|
376
395
|
until @children.workers.empty? || Pitchfork.time_now > limit
|
377
396
|
if graceful
|
378
|
-
soft_kill_each_child(:
|
397
|
+
soft_kill_each_child(:TERM)
|
379
398
|
else
|
380
|
-
kill_each_child(:
|
399
|
+
kill_each_child(:INT)
|
381
400
|
end
|
382
401
|
if monitor_loop(false) == StopIteration
|
383
402
|
return StopIteration
|
@@ -387,6 +406,19 @@ module Pitchfork
|
|
387
406
|
@promotion_lock.unlink
|
388
407
|
end
|
389
408
|
|
409
|
+
def worker_exit(worker)
|
410
|
+
proc_name status: "exiting"
|
411
|
+
|
412
|
+
if @before_worker_exit
|
413
|
+
begin
|
414
|
+
@before_worker_exit.call(self, worker)
|
415
|
+
rescue => error
|
416
|
+
Pitchfork.log_error(logger, "before_worker_exit error", error)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
Process.exit
|
420
|
+
end
|
421
|
+
|
390
422
|
def rewindable_input
|
391
423
|
Pitchfork::HttpParser.input_class.method_defined?(:rewind)
|
392
424
|
end
|
@@ -473,10 +505,19 @@ module Pitchfork
|
|
473
505
|
else # worker is out of time
|
474
506
|
next_sleep = 0
|
475
507
|
if worker.mold?
|
476
|
-
logger.error "mold pid=#{worker.pid}
|
508
|
+
logger.error "mold pid=#{worker.pid} timed out, killing"
|
477
509
|
else
|
478
|
-
logger.error "worker=#{worker.nr} pid=#{worker.pid}
|
510
|
+
logger.error "worker=#{worker.nr} pid=#{worker.pid} timed out, killing"
|
511
|
+
end
|
512
|
+
|
513
|
+
if @after_worker_hard_timeout
|
514
|
+
begin
|
515
|
+
@after_worker_hard_timeout.call(self, worker)
|
516
|
+
rescue => error
|
517
|
+
Pitchfork.log_error(@logger, "after_worker_hard_timeout callback", error)
|
518
|
+
end
|
479
519
|
end
|
520
|
+
|
480
521
|
kill_worker(:KILL, worker.pid) # take no prisoners for hard timeout violations
|
481
522
|
end
|
482
523
|
end
|
@@ -486,7 +527,7 @@ module Pitchfork
|
|
486
527
|
|
487
528
|
def trigger_refork
|
488
529
|
unless REFORKING_AVAILABLE
|
489
|
-
logger.error("This system doesn't support PR_SET_CHILD_SUBREAPER, can't
|
530
|
+
logger.error("This system doesn't support PR_SET_CHILD_SUBREAPER, can't refork")
|
490
531
|
end
|
491
532
|
|
492
533
|
unless @children.pending_promotion?
|
@@ -495,7 +536,6 @@ module Pitchfork
|
|
495
536
|
else
|
496
537
|
logger.error("No children at all???")
|
497
538
|
end
|
498
|
-
else
|
499
539
|
end
|
500
540
|
end
|
501
541
|
|
@@ -519,6 +559,7 @@ module Pitchfork
|
|
519
559
|
|
520
560
|
after_fork_internal
|
521
561
|
worker_loop(worker)
|
562
|
+
worker_exit(worker)
|
522
563
|
end
|
523
564
|
|
524
565
|
worker
|
@@ -577,7 +618,7 @@ module Pitchfork
|
|
577
618
|
def maintain_worker_count
|
578
619
|
(off = @children.workers_count - worker_processes) == 0 and return
|
579
620
|
off < 0 and return spawn_missing_workers
|
580
|
-
@children.each_worker { |w| w.nr >= worker_processes and w.soft_kill(:
|
621
|
+
@children.each_worker { |w| w.nr >= worker_processes and w.soft_kill(:TERM) }
|
581
622
|
end
|
582
623
|
|
583
624
|
def restart_outdated_workers
|
@@ -593,8 +634,13 @@ module Pitchfork
|
|
593
634
|
if workers_to_restart > 0
|
594
635
|
outdated_workers = @children.workers.select { |w| !w.exiting? && w.generation < @children.mold.generation }
|
595
636
|
outdated_workers.each do |worker|
|
596
|
-
|
597
|
-
|
637
|
+
if worker.soft_kill(:TERM)
|
638
|
+
logger.info("Sent SIGTERM to worker=#{worker.nr} pid=#{worker.pid} gen=#{worker.generation}")
|
639
|
+
workers_to_restart -= 1
|
640
|
+
else
|
641
|
+
logger.info("Failed to send SIGTERM to worker=#{worker.nr} pid=#{worker.pid} gen=#{worker.generation}")
|
642
|
+
end
|
643
|
+
break if workers_to_restart <= 0
|
598
644
|
end
|
599
645
|
end
|
600
646
|
end
|
@@ -649,6 +695,9 @@ module Pitchfork
|
|
649
695
|
env = nil
|
650
696
|
@request = Pitchfork::HttpParser.new
|
651
697
|
env = @request.read(client)
|
698
|
+
|
699
|
+
proc_name status: "processing: #{env["PATH_INFO"]}"
|
700
|
+
|
652
701
|
timeout_handler.rack_env = env
|
653
702
|
env["pitchfork.timeout"] = timeout_handler
|
654
703
|
|
@@ -663,12 +712,12 @@ module Pitchfork
|
|
663
712
|
status, headers, body = @app.call(env)
|
664
713
|
|
665
714
|
begin
|
666
|
-
return if @request.hijacked?
|
715
|
+
return env if @request.hijacked?
|
667
716
|
|
668
717
|
if 100 == status.to_i
|
669
718
|
e100_response_write(client, env)
|
670
719
|
status, headers, body = @app.call(env)
|
671
|
-
return if @request.hijacked?
|
720
|
+
return env if @request.hijacked?
|
672
721
|
end
|
673
722
|
@request.headers? or headers = nil
|
674
723
|
http_response_write(client, status, headers, body, @request)
|
@@ -683,11 +732,14 @@ module Pitchfork
|
|
683
732
|
end
|
684
733
|
client.close # flush and uncork socket immediately, no keepalive
|
685
734
|
end
|
735
|
+
env
|
686
736
|
rescue => e
|
687
737
|
handle_error(client, e)
|
738
|
+
env
|
688
739
|
ensure
|
689
740
|
env["rack.after_reply"].each(&:call) if env
|
690
741
|
timeout_handler.finished
|
742
|
+
env
|
691
743
|
end
|
692
744
|
|
693
745
|
def nuke_listeners!(readers)
|
@@ -702,16 +754,16 @@ module Pitchfork
|
|
702
754
|
# traps for USR2, and HUP may be set in the after_fork Proc
|
703
755
|
# by the user.
|
704
756
|
def init_worker_process(worker)
|
757
|
+
proc_name role: "(gen:#{worker.generation}) worker[#{worker.nr}]", status: "init"
|
705
758
|
worker.reset
|
706
759
|
worker.register_to_master(@control_socket[1])
|
707
|
-
# we'll re-trap :QUIT later for graceful shutdown iff we accept clients
|
760
|
+
# we'll re-trap :QUIT and :TERM later for graceful shutdown iff we accept clients
|
708
761
|
exit_sigs = [ :QUIT, :TERM, :INT ]
|
709
762
|
exit_sigs.each { |sig| trap(sig) { exit!(0) } }
|
710
763
|
exit!(0) if (@sig_queue & exit_sigs)[0]
|
711
764
|
(@queue_sigs - exit_sigs).each { |sig| trap(sig, nil) }
|
712
765
|
trap(:CHLD, 'DEFAULT')
|
713
766
|
@sig_queue.clear
|
714
|
-
proc_name "(gen:#{worker.generation}) worker[#{worker.nr}]"
|
715
767
|
@children = nil
|
716
768
|
|
717
769
|
after_worker_fork.call(self, worker) # can drop perms and create listeners
|
@@ -722,14 +774,16 @@ module Pitchfork
|
|
722
774
|
readers = LISTENERS.dup
|
723
775
|
readers << worker
|
724
776
|
trap(:QUIT) { nuke_listeners!(readers) }
|
777
|
+
trap(:TERM) { nuke_listeners!(readers) }
|
725
778
|
readers
|
726
779
|
end
|
727
780
|
|
728
781
|
def init_mold_process(mold)
|
729
|
-
proc_name "(gen
|
782
|
+
proc_name role: "(gen:#{mold.generation}) mold", status: "ready"
|
730
783
|
after_mold_fork.call(self, mold)
|
731
784
|
readers = [mold]
|
732
785
|
trap(:QUIT) { nuke_listeners!(readers) }
|
786
|
+
trap(:TERM) { nuke_listeners!(readers) }
|
733
787
|
readers
|
734
788
|
end
|
735
789
|
|
@@ -754,6 +808,8 @@ module Pitchfork
|
|
754
808
|
ready = readers.dup
|
755
809
|
@after_worker_ready.call(self, worker)
|
756
810
|
|
811
|
+
proc_name status: "ready"
|
812
|
+
|
757
813
|
while readers[0]
|
758
814
|
begin
|
759
815
|
worker.update_deadline(@timeout)
|
@@ -765,12 +821,16 @@ module Pitchfork
|
|
765
821
|
if client
|
766
822
|
case client
|
767
823
|
when Message::PromoteWorker
|
768
|
-
|
824
|
+
if Info.fork_safe?
|
825
|
+
spawn_mold(worker.generation)
|
826
|
+
else
|
827
|
+
logger.error("worker=#{worker.nr} gen=#{worker.generation} is no longer fork safe, can't refork")
|
828
|
+
end
|
769
829
|
when Message
|
770
830
|
worker.update(client)
|
771
831
|
else
|
772
|
-
process_client(client, prepare_timeout(worker))
|
773
|
-
@after_request_complete&.call(self, worker)
|
832
|
+
request_env = process_client(client, prepare_timeout(worker))
|
833
|
+
@after_request_complete&.call(self, worker, request_env)
|
774
834
|
worker.increment_requests_count
|
775
835
|
end
|
776
836
|
worker.update_deadline(@timeout)
|
@@ -780,7 +840,7 @@ module Pitchfork
|
|
780
840
|
# timeout so we can update .deadline and keep parent from SIGKILL-ing us
|
781
841
|
worker.update_deadline(@timeout)
|
782
842
|
|
783
|
-
if @refork_condition && !worker.outdated?
|
843
|
+
if @refork_condition && Info.fork_safe? && !worker.outdated?
|
784
844
|
if @refork_condition.met?(worker, logger)
|
785
845
|
if spawn_mold(worker.generation)
|
786
846
|
logger.info("Refork condition met, promoting ourselves")
|
@@ -789,6 +849,7 @@ module Pitchfork
|
|
789
849
|
end
|
790
850
|
end
|
791
851
|
|
852
|
+
proc_name status: "waiting"
|
792
853
|
waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
|
793
854
|
rescue => e
|
794
855
|
Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
|
@@ -880,6 +941,8 @@ module Pitchfork
|
|
880
941
|
def build_app!
|
881
942
|
return unless app.respond_to?(:arity)
|
882
943
|
|
944
|
+
proc_name status: "booting"
|
945
|
+
|
883
946
|
self.app = case app.arity
|
884
947
|
when 0
|
885
948
|
app.call
|
@@ -890,9 +953,11 @@ module Pitchfork
|
|
890
953
|
end
|
891
954
|
end
|
892
955
|
|
893
|
-
def proc_name(
|
894
|
-
|
895
|
-
|
956
|
+
def proc_name(role: nil, status: nil)
|
957
|
+
@proctitle_role = role if role
|
958
|
+
@proctitle_status = status if status
|
959
|
+
|
960
|
+
Process.setproctitle("#{File.basename(START_CTX[0])} #{@proctitle_role} - #{@proctitle_status}")
|
896
961
|
end
|
897
962
|
|
898
963
|
def bind_listeners!
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pitchfork/shared_memory'
|
4
|
+
|
5
|
+
module Pitchfork
|
6
|
+
module Info
|
7
|
+
@workers_count = 0
|
8
|
+
@fork_safe = true
|
9
|
+
@kept_ios = ObjectSpace::WeakMap.new
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :workers_count
|
13
|
+
|
14
|
+
def keep_io(io)
|
15
|
+
raise ArgumentError, "#{io.inspect} doesn't respond to :to_io" unless io.respond_to?(:to_io)
|
16
|
+
@kept_ios[io] = io
|
17
|
+
io
|
18
|
+
end
|
19
|
+
|
20
|
+
def keep_ios(ios)
|
21
|
+
ios.each { |io| keep_io(io) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def close_all_ios!
|
25
|
+
ignored_ios = [$stdin, $stdout, $stderr]
|
26
|
+
|
27
|
+
@kept_ios.each_value do |io_like|
|
28
|
+
ignored_ios << (io_like.is_a?(IO) ? io_like : io_like.to_io)
|
29
|
+
end
|
30
|
+
|
31
|
+
ObjectSpace.each_object(IO) do |io|
|
32
|
+
closed = begin
|
33
|
+
io.closed?
|
34
|
+
rescue IOError
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
if !closed && io.autoclose? && !ignored_ios.include?(io)
|
39
|
+
if io.is_a?(TCPSocket)
|
40
|
+
# If we inherited a TCP Socket, calling #close directly could send FIN or RST.
|
41
|
+
# So we first reopen /dev/null to avoid that.
|
42
|
+
io.reopen(File::NULL)
|
43
|
+
end
|
44
|
+
begin
|
45
|
+
io.close
|
46
|
+
rescue Errno::EBADF
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def fork_safe?
|
53
|
+
@fork_safe
|
54
|
+
end
|
55
|
+
|
56
|
+
def no_longer_fork_safe!
|
57
|
+
@fork_safe = false
|
58
|
+
end
|
59
|
+
|
60
|
+
def live_workers_count
|
61
|
+
now = Pitchfork.time_now(true)
|
62
|
+
(0...workers_count).count do |nr|
|
63
|
+
SharedMemory.worker_deadline(nr).value > now
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns true if the server is shutting down.
|
68
|
+
# This can be useful to implement health check endpoints, so they
|
69
|
+
# can fail immediately after TERM/QUIT/INT was received by the master
|
70
|
+
# process.
|
71
|
+
# Otherwise they may succeed while Pitchfork is draining requests causing
|
72
|
+
# more requests to be sent.
|
73
|
+
def shutting_down?
|
74
|
+
SharedMemory.shutting_down?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'raindrops'
|
4
|
+
|
5
|
+
module Pitchfork
|
6
|
+
module SharedMemory
|
7
|
+
extend self
|
8
|
+
|
9
|
+
PER_DROP = Raindrops::PAGE_SIZE / Raindrops::SIZE
|
10
|
+
CURRENT_GENERATION_OFFSET = 0
|
11
|
+
SHUTDOWN_OFFSET = 1
|
12
|
+
MOLD_TICK_OFFSET = 2
|
13
|
+
WORKER_TICK_OFFSET = 3
|
14
|
+
|
15
|
+
DROPS = [Raindrops.new(PER_DROP)]
|
16
|
+
|
17
|
+
def current_generation
|
18
|
+
DROPS[0][CURRENT_GENERATION_OFFSET]
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_generation=(value)
|
22
|
+
DROPS[0][CURRENT_GENERATION_OFFSET] = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def shutting_down!
|
26
|
+
DROPS[0][SHUTDOWN_OFFSET] = 1
|
27
|
+
end
|
28
|
+
|
29
|
+
def shutting_down?
|
30
|
+
DROPS[0][SHUTDOWN_OFFSET] > 0
|
31
|
+
end
|
32
|
+
|
33
|
+
class Field
|
34
|
+
def initialize(offset)
|
35
|
+
@drop = DROPS.fetch(offset / PER_DROP)
|
36
|
+
@offset = offset % PER_DROP
|
37
|
+
end
|
38
|
+
|
39
|
+
def value
|
40
|
+
@drop[@offset]
|
41
|
+
end
|
42
|
+
|
43
|
+
def value=(value)
|
44
|
+
@drop[@offset] = value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def mold_deadline
|
49
|
+
self[MOLD_TICK_OFFSET]
|
50
|
+
end
|
51
|
+
|
52
|
+
def worker_deadline(worker_nr)
|
53
|
+
self[WORKER_TICK_OFFSET + worker_nr]
|
54
|
+
end
|
55
|
+
|
56
|
+
def [](offset)
|
57
|
+
Field.new(offset)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Since workers are created from another process, we have to
|
61
|
+
# pre-allocate the drops so they are shared between everyone.
|
62
|
+
#
|
63
|
+
# However this doesn't account for TTIN signals that increase the
|
64
|
+
# number of workers, but we should probably remove that feature too.
|
65
|
+
def preallocate_drops(workers_count)
|
66
|
+
0.upto(((WORKER_TICK_OFFSET + workers_count) / PER_DROP.to_f).ceil) do |i|
|
67
|
+
DROPS[i] ||= Raindrops.new(PER_DROP)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/pitchfork/version.rb
CHANGED
data/lib/pitchfork/worker.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
|
-
require
|
2
|
+
require 'pitchfork/shared_memory'
|
3
3
|
|
4
4
|
module Pitchfork
|
5
5
|
# This class and its members can be considered a stable interface
|
@@ -24,7 +24,8 @@ module Pitchfork
|
|
24
24
|
@exiting = false
|
25
25
|
@requests_count = 0
|
26
26
|
if nr
|
27
|
-
|
27
|
+
@deadline_drop = SharedMemory.worker_deadline(nr)
|
28
|
+
self.deadline = 0
|
28
29
|
else
|
29
30
|
promoted!
|
30
31
|
end
|
@@ -47,7 +48,7 @@ module Pitchfork
|
|
47
48
|
end
|
48
49
|
|
49
50
|
def outdated?
|
50
|
-
|
51
|
+
SharedMemory.current_generation > @generation
|
51
52
|
end
|
52
53
|
|
53
54
|
def update(message)
|
@@ -73,7 +74,7 @@ module Pitchfork
|
|
73
74
|
def finish_promotion(control_socket)
|
74
75
|
message = Message::MoldReady.new(@nr, @pid, generation)
|
75
76
|
control_socket.sendmsg(message)
|
76
|
-
|
77
|
+
SharedMemory.current_generation = @generation
|
77
78
|
end
|
78
79
|
|
79
80
|
def promote(generation)
|
@@ -92,8 +93,8 @@ module Pitchfork
|
|
92
93
|
def promoted!
|
93
94
|
@mold = true
|
94
95
|
@nr = nil
|
95
|
-
@
|
96
|
-
|
96
|
+
@deadline_drop = SharedMemory.mold_deadline
|
97
|
+
self.deadline = 0
|
97
98
|
self
|
98
99
|
end
|
99
100
|
|
@@ -173,20 +174,12 @@ module Pitchfork
|
|
173
174
|
|
174
175
|
# called in the worker process
|
175
176
|
def deadline=(value) # :nodoc:
|
176
|
-
|
177
|
-
MOLD_DROP[0] = value
|
178
|
-
else
|
179
|
-
@deadline_drop[@drop_offset] = value
|
180
|
-
end
|
177
|
+
@deadline_drop.value = value
|
181
178
|
end
|
182
179
|
|
183
180
|
# called in the master process
|
184
181
|
def deadline # :nodoc:
|
185
|
-
|
186
|
-
MOLD_DROP[0]
|
187
|
-
else
|
188
|
-
@deadline_drop[@drop_offset]
|
189
|
-
end
|
182
|
+
@deadline_drop.value
|
190
183
|
end
|
191
184
|
|
192
185
|
def reset
|
@@ -199,12 +192,13 @@ module Pitchfork
|
|
199
192
|
|
200
193
|
# called in both the master (reaping worker) and worker (SIGQUIT handler)
|
201
194
|
def close # :nodoc:
|
195
|
+
self.deadline = 0
|
202
196
|
@master.close if @master
|
203
197
|
@to_io.close if @to_io
|
204
198
|
end
|
205
199
|
|
206
200
|
def create_socketpair!
|
207
|
-
@to_io, @master = Pitchfork.socketpair
|
201
|
+
@to_io, @master = Info.keep_ios(Pitchfork.socketpair)
|
208
202
|
end
|
209
203
|
|
210
204
|
def after_fork_in_child
|
@@ -214,46 +208,24 @@ module Pitchfork
|
|
214
208
|
private
|
215
209
|
|
216
210
|
def pipe=(socket)
|
211
|
+
raise ArgumentError, "pipe can't be nil" unless socket
|
212
|
+
Info.keep_io(socket)
|
217
213
|
@master = MessageSocket.new(socket)
|
218
214
|
end
|
219
215
|
|
220
216
|
def send_message_nonblock(message)
|
221
217
|
success = false
|
218
|
+
return false unless @master
|
222
219
|
begin
|
223
220
|
case @master.sendmsg_nonblock(message, exception: false)
|
224
221
|
when :wait_writable
|
225
222
|
else
|
226
223
|
success = true
|
227
224
|
end
|
228
|
-
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNREFUSED
|
225
|
+
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ENOTCONN
|
229
226
|
# worker will be reaped soon
|
230
227
|
end
|
231
228
|
success
|
232
229
|
end
|
233
|
-
|
234
|
-
MOLD_DROP = Raindrops.new(1)
|
235
|
-
CURRENT_GENERATION_DROP = Raindrops.new(1)
|
236
|
-
PER_DROP = Raindrops::PAGE_SIZE / Raindrops::SIZE
|
237
|
-
TICK_DROPS = []
|
238
|
-
|
239
|
-
class << self
|
240
|
-
# Since workers are created from another process, we have to
|
241
|
-
# pre-allocate the drops so they are shared between everyone.
|
242
|
-
#
|
243
|
-
# However this doesn't account for TTIN signals that increase the
|
244
|
-
# number of workers, but we should probably remove that feature too.
|
245
|
-
def preallocate_drops(workers_count)
|
246
|
-
0.upto(workers_count / PER_DROP) do |i|
|
247
|
-
TICK_DROPS[i] = Raindrops.new(PER_DROP)
|
248
|
-
end
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
def build_raindrops(drop_nr)
|
253
|
-
drop_index = drop_nr / PER_DROP
|
254
|
-
@drop_offset = drop_nr % PER_DROP
|
255
|
-
@deadline_drop = TICK_DROPS[drop_index] ||= Raindrops.new(PER_DROP)
|
256
|
-
@deadline_drop[@drop_offset] = 0
|
257
|
-
end
|
258
230
|
end
|
259
231
|
end
|
data/lib/pitchfork.rb
CHANGED
@@ -26,9 +26,10 @@ module Pitchfork
|
|
26
26
|
# application dispatch. This is always raised with an empty backtrace
|
27
27
|
# since there is nothing in the application stack that is responsible
|
28
28
|
# for client shutdowns/disconnects. This exception is visible to Rack
|
29
|
-
# applications
|
30
|
-
#
|
31
|
-
#
|
29
|
+
# applications. This is a subclass of the standard EOFError class and
|
30
|
+
# applications should not rescue it explicitly, but rescue EOFError instead.
|
31
|
+
# Such an error is likely an indication that the reverse proxy in front
|
32
|
+
# of Pitchfork isn't properly buffering requests.
|
32
33
|
ClientShutdown = Class.new(EOFError)
|
33
34
|
|
34
35
|
BootFailure = Class.new(StandardError)
|
@@ -120,8 +121,11 @@ module Pitchfork
|
|
120
121
|
end
|
121
122
|
end
|
122
123
|
|
123
|
-
def self.clean_fork(&block)
|
124
|
+
def self.clean_fork(setpgid: true, &block)
|
124
125
|
if pid = Process.fork
|
126
|
+
if setpgid
|
127
|
+
Process.setpgid(pid, pid) # Make into a group leader
|
128
|
+
end
|
125
129
|
return pid
|
126
130
|
end
|
127
131
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pitchfork
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-08-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: raindrops
|
@@ -101,12 +101,13 @@ files:
|
|
101
101
|
- lib/pitchfork/http_parser.rb
|
102
102
|
- lib/pitchfork/http_response.rb
|
103
103
|
- lib/pitchfork/http_server.rb
|
104
|
+
- lib/pitchfork/info.rb
|
104
105
|
- lib/pitchfork/launcher.rb
|
105
106
|
- lib/pitchfork/mem_info.rb
|
106
107
|
- lib/pitchfork/message.rb
|
107
|
-
- lib/pitchfork/preread_input.rb
|
108
108
|
- lib/pitchfork/refork_condition.rb
|
109
109
|
- lib/pitchfork/select_waiter.rb
|
110
|
+
- lib/pitchfork/shared_memory.rb
|
110
111
|
- lib/pitchfork/socket_helper.rb
|
111
112
|
- lib/pitchfork/soft_timeout.rb
|
112
113
|
- lib/pitchfork/stream_input.rb
|
@@ -1,33 +0,0 @@
|
|
1
|
-
# -*- encoding: binary -*-
|
2
|
-
|
3
|
-
module Pitchfork
|
4
|
-
# This middleware is used to ensure input is buffered to memory
|
5
|
-
# or disk (depending on size) before the application is dispatched
|
6
|
-
# by entirely consuming it (from TeeInput) beforehand.
|
7
|
-
#
|
8
|
-
# Usage (in config.ru):
|
9
|
-
#
|
10
|
-
# require 'pitchfork/preread_input'
|
11
|
-
# if defined?(Pitchfork)
|
12
|
-
# use Pitchfork::PrereadInput
|
13
|
-
# end
|
14
|
-
# run YourApp.new
|
15
|
-
class PrereadInput
|
16
|
-
|
17
|
-
# :stopdoc:
|
18
|
-
def initialize(app)
|
19
|
-
@app = app
|
20
|
-
end
|
21
|
-
|
22
|
-
def call(env)
|
23
|
-
buf = ""
|
24
|
-
input = env["rack.input"]
|
25
|
-
if input.respond_to?(:rewind)
|
26
|
-
true while input.read(16384, buf)
|
27
|
-
input.rewind
|
28
|
-
end
|
29
|
-
@app.call(env)
|
30
|
-
end
|
31
|
-
# :startdoc:
|
32
|
-
end
|
33
|
-
end
|