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