pitchfork 0.6.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 +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
|