pitchfork 0.6.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 +7 -0
- data/Dockerfile +1 -1
- data/Gemfile.lock +3 -3
- data/docs/CONFIGURATION.md +1 -1
- data/docs/FORK_SAFETY.md +8 -6
- data/lib/pitchfork/configurator.rb +1 -1
- data/lib/pitchfork/flock.rb +4 -0
- data/lib/pitchfork/http_server.rb +52 -18
- data/lib/pitchfork/info.rb +48 -0
- data/lib/pitchfork/version.rb +1 -1
- data/lib/pitchfork/worker.rb +4 -1
- metadata +2 -2
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,12 @@
|
|
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
|
+
|
3
10
|
# 0.6.0
|
4
11
|
|
5
12
|
- Expose `Pitchfork::Info.workers_count` and `.live_workers_count` to be consumed by application health checks.
|
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/docs/CONFIGURATION.md
CHANGED
@@ -371,7 +371,7 @@ Called in the worker processes after a request has completed.
|
|
371
371
|
Can be used for out of band work, or to exit unhealthy workers.
|
372
372
|
|
373
373
|
```ruby
|
374
|
-
after_request_complete do |server, worker|
|
374
|
+
after_request_complete do |server, worker, env|
|
375
375
|
if something_wrong?
|
376
376
|
exit
|
377
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.
|
@@ -158,7 +158,7 @@ module Pitchfork
|
|
158
158
|
end
|
159
159
|
|
160
160
|
def after_request_complete(*args, &block)
|
161
|
-
set_hook(:after_request_complete, block_given? ? block : args[0])
|
161
|
+
set_hook(:after_request_complete, block_given? ? block : args[0], 3)
|
162
162
|
end
|
163
163
|
|
164
164
|
def timeout(seconds, cleanup: 2)
|
data/lib/pitchfork/flock.rb
CHANGED
@@ -91,7 +91,7 @@ module Pitchfork
|
|
91
91
|
# in new projects
|
92
92
|
LISTENERS = []
|
93
93
|
|
94
|
-
NOOP = '.'
|
94
|
+
NOOP = '.'.freeze
|
95
95
|
|
96
96
|
# :startdoc:
|
97
97
|
# This Hash is considered a stable interface and changing its contents
|
@@ -129,6 +129,7 @@ module Pitchfork
|
|
129
129
|
@respawn = false
|
130
130
|
@last_check = Pitchfork.time_now
|
131
131
|
@promotion_lock = Flock.new("pitchfork-promotion")
|
132
|
+
Info.keep_io(@promotion_lock)
|
132
133
|
|
133
134
|
options = options.dup
|
134
135
|
@ready_pipe = options.delete(:ready_pipe)
|
@@ -137,6 +138,8 @@ module Pitchfork
|
|
137
138
|
self.config = Pitchfork::Configurator.new(options)
|
138
139
|
self.listener_opts = {}
|
139
140
|
|
141
|
+
proc_name role: 'monitor', status: START_CTX[:argv].join(' ')
|
142
|
+
|
140
143
|
# We use @control_socket differently in the master and worker processes:
|
141
144
|
#
|
142
145
|
# * The master process never closes or reinitializes this once
|
@@ -180,6 +183,7 @@ module Pitchfork
|
|
180
183
|
# It's also used by newly spawned children to send their soft_signal pipe
|
181
184
|
# to the master when they are spawned.
|
182
185
|
@control_socket.replace(Pitchfork.socketpair)
|
186
|
+
Info.keep_ios(@control_socket)
|
183
187
|
@master_pid = $$
|
184
188
|
|
185
189
|
# setup signal handlers before writing pid file in case people get
|
@@ -264,6 +268,7 @@ module Pitchfork
|
|
264
268
|
io = server_cast(io)
|
265
269
|
end
|
266
270
|
logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
|
271
|
+
Info.keep_io(io)
|
267
272
|
LISTENERS << io
|
268
273
|
io
|
269
274
|
rescue Errno::EADDRINUSE => err
|
@@ -287,7 +292,8 @@ module Pitchfork
|
|
287
292
|
def join
|
288
293
|
@respawn = true
|
289
294
|
|
290
|
-
proc_name '
|
295
|
+
proc_name role: 'monitor', status: START_CTX[:argv].join(' ')
|
296
|
+
|
291
297
|
logger.info "master process ready" # test_exec.rb relies on this message
|
292
298
|
if @ready_pipe
|
293
299
|
begin
|
@@ -347,7 +353,11 @@ module Pitchfork
|
|
347
353
|
stop(false)
|
348
354
|
return StopIteration
|
349
355
|
when :USR2 # trigger a promotion
|
350
|
-
|
356
|
+
if @respawn
|
357
|
+
trigger_refork
|
358
|
+
else
|
359
|
+
logger.error "Can't trigger a refork as the server is shutting down"
|
360
|
+
end
|
351
361
|
when :TTIN
|
352
362
|
@respawn = true
|
353
363
|
self.worker_processes += 1
|
@@ -376,6 +386,7 @@ module Pitchfork
|
|
376
386
|
|
377
387
|
# Terminates all workers, but does not exit master process
|
378
388
|
def stop(graceful = true)
|
389
|
+
proc_name role: 'monitor', status: 'shutting down'
|
379
390
|
@respawn = false
|
380
391
|
SharedMemory.shutting_down!
|
381
392
|
wait_for_pending_workers
|
@@ -396,6 +407,8 @@ module Pitchfork
|
|
396
407
|
end
|
397
408
|
|
398
409
|
def worker_exit(worker)
|
410
|
+
proc_name status: "exiting"
|
411
|
+
|
399
412
|
if @before_worker_exit
|
400
413
|
begin
|
401
414
|
@before_worker_exit.call(self, worker)
|
@@ -514,7 +527,7 @@ module Pitchfork
|
|
514
527
|
|
515
528
|
def trigger_refork
|
516
529
|
unless REFORKING_AVAILABLE
|
517
|
-
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")
|
518
531
|
end
|
519
532
|
|
520
533
|
unless @children.pending_promotion?
|
@@ -523,7 +536,6 @@ module Pitchfork
|
|
523
536
|
else
|
524
537
|
logger.error("No children at all???")
|
525
538
|
end
|
526
|
-
else
|
527
539
|
end
|
528
540
|
end
|
529
541
|
|
@@ -622,8 +634,13 @@ module Pitchfork
|
|
622
634
|
if workers_to_restart > 0
|
623
635
|
outdated_workers = @children.workers.select { |w| !w.exiting? && w.generation < @children.mold.generation }
|
624
636
|
outdated_workers.each do |worker|
|
625
|
-
|
626
|
-
|
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
|
627
644
|
end
|
628
645
|
end
|
629
646
|
end
|
@@ -678,6 +695,9 @@ module Pitchfork
|
|
678
695
|
env = nil
|
679
696
|
@request = Pitchfork::HttpParser.new
|
680
697
|
env = @request.read(client)
|
698
|
+
|
699
|
+
proc_name status: "processing: #{env["PATH_INFO"]}"
|
700
|
+
|
681
701
|
timeout_handler.rack_env = env
|
682
702
|
env["pitchfork.timeout"] = timeout_handler
|
683
703
|
|
@@ -692,12 +712,12 @@ module Pitchfork
|
|
692
712
|
status, headers, body = @app.call(env)
|
693
713
|
|
694
714
|
begin
|
695
|
-
return if @request.hijacked?
|
715
|
+
return env if @request.hijacked?
|
696
716
|
|
697
717
|
if 100 == status.to_i
|
698
718
|
e100_response_write(client, env)
|
699
719
|
status, headers, body = @app.call(env)
|
700
|
-
return if @request.hijacked?
|
720
|
+
return env if @request.hijacked?
|
701
721
|
end
|
702
722
|
@request.headers? or headers = nil
|
703
723
|
http_response_write(client, status, headers, body, @request)
|
@@ -712,11 +732,14 @@ module Pitchfork
|
|
712
732
|
end
|
713
733
|
client.close # flush and uncork socket immediately, no keepalive
|
714
734
|
end
|
735
|
+
env
|
715
736
|
rescue => e
|
716
737
|
handle_error(client, e)
|
738
|
+
env
|
717
739
|
ensure
|
718
740
|
env["rack.after_reply"].each(&:call) if env
|
719
741
|
timeout_handler.finished
|
742
|
+
env
|
720
743
|
end
|
721
744
|
|
722
745
|
def nuke_listeners!(readers)
|
@@ -731,6 +754,7 @@ module Pitchfork
|
|
731
754
|
# traps for USR2, and HUP may be set in the after_fork Proc
|
732
755
|
# by the user.
|
733
756
|
def init_worker_process(worker)
|
757
|
+
proc_name role: "(gen:#{worker.generation}) worker[#{worker.nr}]", status: "init"
|
734
758
|
worker.reset
|
735
759
|
worker.register_to_master(@control_socket[1])
|
736
760
|
# we'll re-trap :QUIT and :TERM later for graceful shutdown iff we accept clients
|
@@ -740,7 +764,6 @@ module Pitchfork
|
|
740
764
|
(@queue_sigs - exit_sigs).each { |sig| trap(sig, nil) }
|
741
765
|
trap(:CHLD, 'DEFAULT')
|
742
766
|
@sig_queue.clear
|
743
|
-
proc_name "(gen:#{worker.generation}) worker[#{worker.nr}]"
|
744
767
|
@children = nil
|
745
768
|
|
746
769
|
after_worker_fork.call(self, worker) # can drop perms and create listeners
|
@@ -756,7 +779,7 @@ module Pitchfork
|
|
756
779
|
end
|
757
780
|
|
758
781
|
def init_mold_process(mold)
|
759
|
-
proc_name "(gen
|
782
|
+
proc_name role: "(gen:#{mold.generation}) mold", status: "ready"
|
760
783
|
after_mold_fork.call(self, mold)
|
761
784
|
readers = [mold]
|
762
785
|
trap(:QUIT) { nuke_listeners!(readers) }
|
@@ -785,6 +808,8 @@ module Pitchfork
|
|
785
808
|
ready = readers.dup
|
786
809
|
@after_worker_ready.call(self, worker)
|
787
810
|
|
811
|
+
proc_name status: "ready"
|
812
|
+
|
788
813
|
while readers[0]
|
789
814
|
begin
|
790
815
|
worker.update_deadline(@timeout)
|
@@ -796,12 +821,16 @@ module Pitchfork
|
|
796
821
|
if client
|
797
822
|
case client
|
798
823
|
when Message::PromoteWorker
|
799
|
-
|
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
|
800
829
|
when Message
|
801
830
|
worker.update(client)
|
802
831
|
else
|
803
|
-
process_client(client, prepare_timeout(worker))
|
804
|
-
@after_request_complete&.call(self, worker)
|
832
|
+
request_env = process_client(client, prepare_timeout(worker))
|
833
|
+
@after_request_complete&.call(self, worker, request_env)
|
805
834
|
worker.increment_requests_count
|
806
835
|
end
|
807
836
|
worker.update_deadline(@timeout)
|
@@ -811,7 +840,7 @@ module Pitchfork
|
|
811
840
|
# timeout so we can update .deadline and keep parent from SIGKILL-ing us
|
812
841
|
worker.update_deadline(@timeout)
|
813
842
|
|
814
|
-
if @refork_condition && !worker.outdated?
|
843
|
+
if @refork_condition && Info.fork_safe? && !worker.outdated?
|
815
844
|
if @refork_condition.met?(worker, logger)
|
816
845
|
if spawn_mold(worker.generation)
|
817
846
|
logger.info("Refork condition met, promoting ourselves")
|
@@ -820,6 +849,7 @@ module Pitchfork
|
|
820
849
|
end
|
821
850
|
end
|
822
851
|
|
852
|
+
proc_name status: "waiting"
|
823
853
|
waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
|
824
854
|
rescue => e
|
825
855
|
Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
|
@@ -911,6 +941,8 @@ module Pitchfork
|
|
911
941
|
def build_app!
|
912
942
|
return unless app.respond_to?(:arity)
|
913
943
|
|
944
|
+
proc_name status: "booting"
|
945
|
+
|
914
946
|
self.app = case app.arity
|
915
947
|
when 0
|
916
948
|
app.call
|
@@ -921,9 +953,11 @@ module Pitchfork
|
|
921
953
|
end
|
922
954
|
end
|
923
955
|
|
924
|
-
def proc_name(
|
925
|
-
|
926
|
-
|
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}")
|
927
961
|
end
|
928
962
|
|
929
963
|
def bind_listeners!
|
data/lib/pitchfork/info.rb
CHANGED
@@ -5,10 +5,58 @@ require 'pitchfork/shared_memory'
|
|
5
5
|
module Pitchfork
|
6
6
|
module Info
|
7
7
|
@workers_count = 0
|
8
|
+
@fork_safe = true
|
9
|
+
@kept_ios = ObjectSpace::WeakMap.new
|
8
10
|
|
9
11
|
class << self
|
10
12
|
attr_accessor :workers_count
|
11
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
|
+
|
12
60
|
def live_workers_count
|
13
61
|
now = Pitchfork.time_now(true)
|
14
62
|
(0...workers_count).count do |nr|
|
data/lib/pitchfork/version.rb
CHANGED
data/lib/pitchfork/worker.rb
CHANGED
@@ -198,7 +198,7 @@ module Pitchfork
|
|
198
198
|
end
|
199
199
|
|
200
200
|
def create_socketpair!
|
201
|
-
@to_io, @master = Pitchfork.socketpair
|
201
|
+
@to_io, @master = Info.keep_ios(Pitchfork.socketpair)
|
202
202
|
end
|
203
203
|
|
204
204
|
def after_fork_in_child
|
@@ -208,11 +208,14 @@ module Pitchfork
|
|
208
208
|
private
|
209
209
|
|
210
210
|
def pipe=(socket)
|
211
|
+
raise ArgumentError, "pipe can't be nil" unless socket
|
212
|
+
Info.keep_io(socket)
|
211
213
|
@master = MessageSocket.new(socket)
|
212
214
|
end
|
213
215
|
|
214
216
|
def send_message_nonblock(message)
|
215
217
|
success = false
|
218
|
+
return false unless @master
|
216
219
|
begin
|
217
220
|
case @master.sendmsg_nonblock(message, exception: false)
|
218
221
|
when :wait_writable
|
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
|