pitchfork 0.17.0 → 0.18.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/docs/CONFIGURATION.md +31 -0
- data/lib/pitchfork/configurator.rb +9 -1
- data/lib/pitchfork/http_server.rb +32 -11
- data/lib/pitchfork/message.rb +2 -0
- data/lib/pitchfork/version.rb +1 -1
- data/lib/pitchfork/worker.rb +18 -1
- data/lib/pitchfork.rb +6 -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: 735c4d6910ec05b65ae12e6e13f306110d55102d4ec41d93be14033dbda00f6f
|
4
|
+
data.tar.gz: 1452376664a49ff6c81c4c29e04345ef93b891f68d4d87d53e40473c6ed5e67b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4d9d99a1092c508806c2b7877930637c54c7a0e79aeb3cb9d74b22f9fc2a25caa65b26a5d8f73c9a78ecdc072e7da285c1f1fd1b1d025b096acb652bcf46db90
|
7
|
+
data.tar.gz: 05b55878eb9fe2c7d8af70429789f31aaa4ec5c30e025fd5fb4a6d6e3934256fe9c66dce75fbc98f2e0d596f56874a5ccc06578923c9d15f7811e768a02ecf4c
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# 0.18.0
|
4
|
+
|
5
|
+
- Fix a regression introduced in `0.17.0` causing `before_worker_exit` to often not be called.
|
6
|
+
- Implemented the `max_consecutive_spawn_errors` configuration. Purely opt-in for now.
|
7
|
+
- Properly handle magic comments in `config.ru`.
|
8
|
+
- Execute `rack.response_finished` callbacks in reverse order of registration
|
9
|
+
|
3
10
|
# 0.17.0
|
4
11
|
|
5
12
|
- Improve `Pitchfork::Info#live_workers_count` to be more accurate.
|
data/docs/CONFIGURATION.md
CHANGED
@@ -542,6 +542,37 @@ By default automatic reforking isn't enabled.
|
|
542
542
|
|
543
543
|
Make sure to read the [fork safety guide](FORK_SAFETY.md) before enabling reforking.
|
544
544
|
|
545
|
+
### `refork_max_unavailable`
|
546
|
+
|
547
|
+
```ruby
|
548
|
+
refork_max_unavailable 5
|
549
|
+
```
|
550
|
+
|
551
|
+
Sets the number of workers than can be restarted at a given time.
|
552
|
+
|
553
|
+
When a new generation is created, workers from the old generation are rolled out progressively
|
554
|
+
and replaced by fresh workers from the new generation.
|
555
|
+
|
556
|
+
For instance `refork_max_unavailable 5` means 5 workers may be unavailable during the rollout phase.
|
557
|
+
|
558
|
+
The default is `(workers_processes * 0.1).ceil`, or `10%` rounded up.
|
559
|
+
|
560
|
+
### `max_consecutive_spawn_errors`
|
561
|
+
|
562
|
+
```ruby
|
563
|
+
max_consecutive_spawn_errors 5
|
564
|
+
```
|
565
|
+
|
566
|
+
Sets the number of consecutive failures of spawning new workers that trigger a shutdown.
|
567
|
+
|
568
|
+
Whenever a newly spawned worker fail to acheive readiness, either because it crashes or because
|
569
|
+
it times out before becoming ready, a counter is incremented. Whenever a worker successfully
|
570
|
+
reach readiness, the counter is reset.
|
571
|
+
|
572
|
+
This can be useful to better handle issues with slow or broken `after_worker_fork` callbacks.
|
573
|
+
|
574
|
+
The default is `nil`, which means the behavior is disabled.
|
575
|
+
|
545
576
|
## Rack Features
|
546
577
|
|
547
578
|
### `early_hints`
|
@@ -75,7 +75,7 @@ module Pitchfork
|
|
75
75
|
:client_body_buffer_size => Pitchfork::Const::MAX_BODY,
|
76
76
|
:before_service_worker_ready => nil,
|
77
77
|
:before_service_worker_exit => nil,
|
78
|
-
:setpgid =>
|
78
|
+
:setpgid => true,
|
79
79
|
}
|
80
80
|
#:startdoc:
|
81
81
|
|
@@ -216,6 +216,14 @@ module Pitchfork
|
|
216
216
|
set_int(:worker_processes, nr, 1)
|
217
217
|
end
|
218
218
|
|
219
|
+
def refork_max_unavailable(max)
|
220
|
+
set_int(:refork_max_unavailable, max, 1)
|
221
|
+
end
|
222
|
+
|
223
|
+
def max_consecutive_spawn_errors(max)
|
224
|
+
set_int(:max_consecutive_spawn_errors, max, 1)
|
225
|
+
end
|
226
|
+
|
219
227
|
def early_hints(bool)
|
220
228
|
set_bool(:early_hints, bool)
|
221
229
|
end
|
@@ -82,7 +82,8 @@ module Pitchfork
|
|
82
82
|
:listener_opts, :children,
|
83
83
|
:orig_app, :config, :ready_pipe, :early_hints, :setpgid
|
84
84
|
attr_writer :after_worker_exit, :before_worker_exit, :after_worker_ready, :after_request_complete,
|
85
|
-
:refork_condition, :after_worker_timeout, :after_worker_hard_timeout, :after_monitor_ready
|
85
|
+
:refork_condition, :after_worker_timeout, :after_worker_hard_timeout, :after_monitor_ready, :refork_max_unavailable,
|
86
|
+
:max_consecutive_spawn_errors
|
86
87
|
|
87
88
|
attr_reader :logger
|
88
89
|
include Pitchfork::SocketHelper
|
@@ -103,6 +104,9 @@ module Pitchfork
|
|
103
104
|
@exit_status = 0
|
104
105
|
@app = app
|
105
106
|
@respawn = false
|
107
|
+
@refork_max_unavailable = nil
|
108
|
+
@consecutive_spawn_errors = 0
|
109
|
+
@max_consecutive_spawn_errors = nil
|
106
110
|
@last_check = Pitchfork.time_now
|
107
111
|
@promotion_lock = Flock.new("pitchfork-promotion")
|
108
112
|
Info.keep_io(@promotion_lock)
|
@@ -330,6 +334,13 @@ module Pitchfork
|
|
330
334
|
# machine) comes out of suspend/hibernation
|
331
335
|
if (@last_check + @timeout) >= (@last_check = Pitchfork.time_now)
|
332
336
|
sleep_time = murder_lazy_workers
|
337
|
+
if @max_consecutive_spawn_errors && @consecutive_spawn_errors > @max_consecutive_spawn_errors && !SharedMemory.shutting_down?
|
338
|
+
logger.fatal("#{@consecutive_spawn_errors} consecutive failures to spawn children, aborting - broken after_worker_fork callback?")
|
339
|
+
@exit_status = 1
|
340
|
+
SharedMemory.shutting_down!
|
341
|
+
stop(false)
|
342
|
+
return StopIteration
|
343
|
+
end
|
333
344
|
else
|
334
345
|
sleep_time = @timeout/2.0 + 1
|
335
346
|
@logger.debug("waiting #{sleep_time}s after suspend/hibernation")
|
@@ -371,6 +382,7 @@ module Pitchfork
|
|
371
382
|
new_service = @children.update(message)
|
372
383
|
logger.info("#{new_service.to_log} spawned")
|
373
384
|
when Message::MoldReady
|
385
|
+
@consecutive_spawn_errors = 0
|
374
386
|
old_molds = @children.molds
|
375
387
|
new_mold = @children.update(message)
|
376
388
|
logger.info("#{new_mold.to_log} ready")
|
@@ -378,6 +390,8 @@ module Pitchfork
|
|
378
390
|
logger.info("Terminating old #{old_mold.to_log}")
|
379
391
|
old_mold.soft_kill(:TERM)
|
380
392
|
end
|
393
|
+
when Message::WorkerReady, Message::ServiceReady
|
394
|
+
@consecutive_spawn_errors = 0
|
381
395
|
else
|
382
396
|
logger.error("Unexpected message in sig_queue #{message.inspect}")
|
383
397
|
logger.error(@sig_queue.inspect)
|
@@ -467,6 +481,10 @@ module Pitchfork
|
|
467
481
|
|
468
482
|
private
|
469
483
|
|
484
|
+
def refork_max_unavailable
|
485
|
+
@refork_max_unavailable ||= (worker_processes * 0.1).ceil
|
486
|
+
end
|
487
|
+
|
470
488
|
# wait for a signal handler to wake us up and then consume the pipe
|
471
489
|
def monitor_sleep(sec)
|
472
490
|
@control_socket[0].wait(sec) or return
|
@@ -490,6 +508,9 @@ module Pitchfork
|
|
490
508
|
wpid or return
|
491
509
|
worker = @children.reap(wpid) and worker.close rescue nil
|
492
510
|
if worker
|
511
|
+
unless worker.ready?
|
512
|
+
@consecutive_spawn_errors += 1
|
513
|
+
end
|
493
514
|
@after_worker_exit.call(self, worker, status)
|
494
515
|
else
|
495
516
|
logger.info("reaped unknown subprocess #{status.inspect}")
|
@@ -549,6 +570,7 @@ module Pitchfork
|
|
549
570
|
end
|
550
571
|
|
551
572
|
logger.error "#{child.to_log} timed out, killing"
|
573
|
+
@consecutive_spawn_errors += 1 unless child.ready?
|
552
574
|
@children.hard_kill(@timeout_signal.call(child.pid), child) # take no prisoners for hard timeout violations
|
553
575
|
end
|
554
576
|
|
@@ -613,6 +635,7 @@ module Pitchfork
|
|
613
635
|
end
|
614
636
|
end
|
615
637
|
|
638
|
+
service.notify_ready(@control_socket[1])
|
616
639
|
proc_name status: "ready"
|
617
640
|
|
618
641
|
while readers[0]
|
@@ -750,8 +773,7 @@ module Pitchfork
|
|
750
773
|
# We don't shutdown any outdated worker if any worker is already being
|
751
774
|
# spawned or a worker is exiting. Only 10% of workers can be reforked at
|
752
775
|
# once to minimize the impact on capacity.
|
753
|
-
|
754
|
-
workers_to_restart = max_pending_workers - @children.restarting_workers_count
|
776
|
+
workers_to_restart = refork_max_unavailable - @children.restarting_workers_count
|
755
777
|
|
756
778
|
if service = @children.service
|
757
779
|
if service.outdated?
|
@@ -871,7 +893,7 @@ module Pitchfork
|
|
871
893
|
env
|
872
894
|
ensure
|
873
895
|
if env
|
874
|
-
env["rack.response_finished"].
|
896
|
+
env["rack.response_finished"].reverse_each do |callback|
|
875
897
|
if callback.arity == 0
|
876
898
|
callback.call
|
877
899
|
else
|
@@ -926,14 +948,13 @@ module Pitchfork
|
|
926
948
|
end
|
927
949
|
|
928
950
|
def init_service_process(service)
|
929
|
-
proc_name role: "(gen:#{service.generation})
|
951
|
+
proc_name role: "(gen:#{service.generation}) service", status: "init"
|
930
952
|
LISTENERS.each(&:close).clear # Don't appear as listening to incoming requests
|
931
953
|
service.register_to_monitor(@control_socket[1])
|
932
954
|
readers = [service]
|
933
955
|
trap(:QUIT) { nuke_listeners!(readers) }
|
934
956
|
trap(:TERM) { nuke_listeners!(readers) }
|
935
957
|
trap(:INT) { nuke_listeners!(readers); exit!(0) }
|
936
|
-
proc_name role: "(gen:#{service.generation}) service", status: "ready"
|
937
958
|
readers
|
938
959
|
end
|
939
960
|
|
@@ -944,7 +965,6 @@ module Pitchfork
|
|
944
965
|
trap(:QUIT) { nuke_listeners!(readers) }
|
945
966
|
trap(:TERM) { nuke_listeners!(readers) }
|
946
967
|
trap(:INT) { nuke_listeners!(readers); exit!(0) }
|
947
|
-
proc_name role: "(gen:#{mold.generation}) mold", status: "ready"
|
948
968
|
readers
|
949
969
|
end
|
950
970
|
|
@@ -969,10 +989,9 @@ module Pitchfork
|
|
969
989
|
ready = readers.dup
|
970
990
|
@after_worker_ready.call(self, worker)
|
971
991
|
|
992
|
+
worker.notify_ready(@control_socket[1])
|
972
993
|
proc_name status: "ready"
|
973
994
|
|
974
|
-
worker.ready = true
|
975
|
-
|
976
995
|
while readers[0]
|
977
996
|
begin
|
978
997
|
worker.update_deadline(@timeout)
|
@@ -1054,6 +1073,8 @@ module Pitchfork
|
|
1054
1073
|
ready = readers.dup
|
1055
1074
|
|
1056
1075
|
mold.finish_promotion(@control_socket[1])
|
1076
|
+
mold.ready = true
|
1077
|
+
proc_name status: "ready"
|
1057
1078
|
|
1058
1079
|
while readers[0]
|
1059
1080
|
begin
|
@@ -1165,8 +1186,6 @@ module Pitchfork
|
|
1165
1186
|
FORK_TIMEOUT = 5
|
1166
1187
|
|
1167
1188
|
def fork_sibling(role, &block)
|
1168
|
-
reset_signal_handlers
|
1169
|
-
|
1170
1189
|
if REFORKING_AVAILABLE
|
1171
1190
|
r, w = Pitchfork::Info.keep_ios(IO.pipe)
|
1172
1191
|
# We double fork so that the new worker is re-attached back
|
@@ -1190,6 +1209,8 @@ module Pitchfork
|
|
1190
1209
|
raise ForkFailure, "fork_sibling didn't succeed in #{FORK_TIMEOUT} seconds"
|
1191
1210
|
end
|
1192
1211
|
else # first child
|
1212
|
+
reset_signal_handlers
|
1213
|
+
|
1193
1214
|
r.close
|
1194
1215
|
Process.setproctitle("<pitchfork fork_sibling(#{role})>")
|
1195
1216
|
pid = Pitchfork.clean_fork(setpgid: setpgid) do
|
data/lib/pitchfork/message.rb
CHANGED
@@ -124,6 +124,7 @@ module Pitchfork
|
|
124
124
|
class Message
|
125
125
|
SpawnWorker = new(:nr)
|
126
126
|
WorkerSpawned = new(:nr, :pid, :generation, :pipe)
|
127
|
+
WorkerReady = new(:nr, :pid, :generation)
|
127
128
|
PromoteWorker = new(:generation)
|
128
129
|
|
129
130
|
MoldSpawned = new(:nr, :pid, :generation, :pipe)
|
@@ -131,6 +132,7 @@ module Pitchfork
|
|
131
132
|
|
132
133
|
SpawnService = new(:_) # Struct.new requires at least 1 member on Ruby < 3.3
|
133
134
|
ServiceSpawned = new(:pid, :generation, :pipe)
|
135
|
+
ServiceReady = new(:pid, :generation)
|
134
136
|
|
135
137
|
SoftKill = new(:signum)
|
136
138
|
end
|
data/lib/pitchfork/version.rb
CHANGED
data/lib/pitchfork/worker.rb
CHANGED
@@ -67,12 +67,25 @@ module Pitchfork
|
|
67
67
|
end
|
68
68
|
|
69
69
|
def finish_promotion(control_socket)
|
70
|
+
SharedMemory.current_generation = @generation
|
70
71
|
message = Message::MoldReady.new(@nr, @pid, generation)
|
71
72
|
control_socket.sendmsg(message)
|
72
|
-
SharedMemory.current_generation = @generation
|
73
73
|
@state_drop = SharedMemory.mold_state
|
74
74
|
end
|
75
75
|
|
76
|
+
def notify_ready(control_socket)
|
77
|
+
self.ready = true
|
78
|
+
message = if worker?
|
79
|
+
Message::WorkerReady.new(@nr, @pid, @generation)
|
80
|
+
elsif service?
|
81
|
+
Message::ServiceReady.new(@pid, @generation)
|
82
|
+
else
|
83
|
+
raise "Unexpected child type"
|
84
|
+
end
|
85
|
+
|
86
|
+
control_socket.sendmsg(message)
|
87
|
+
end
|
88
|
+
|
76
89
|
def promote(generation)
|
77
90
|
send_message_nonblock(Message::PromoteWorker.new(generation))
|
78
91
|
end
|
@@ -106,6 +119,10 @@ module Pitchfork
|
|
106
119
|
false
|
107
120
|
end
|
108
121
|
|
122
|
+
def worker?
|
123
|
+
!mold? && !service?
|
124
|
+
end
|
125
|
+
|
109
126
|
def to_io # IO.select-compatible
|
110
127
|
@to_io.to_io
|
111
128
|
end
|
data/lib/pitchfork.rb
CHANGED
@@ -87,7 +87,12 @@ module Pitchfork
|
|
87
87
|
when /\.ru$/
|
88
88
|
raw = File.read(ru)
|
89
89
|
raw.sub!(/^__END__\n.*/, '')
|
90
|
-
|
90
|
+
lines = raw.lines
|
91
|
+
trailing_comments_index = lines.index { |line| !line.start_with?('#') }
|
92
|
+
prelude = lines[0...trailing_comments_index].join
|
93
|
+
raw = lines[trailing_comments_index..-1].join
|
94
|
+
|
95
|
+
eval("#{prelude}\nRack::Builder.new do\n#{raw}\nend.to_app\n", TOPLEVEL_BINDING, ru)
|
91
96
|
else
|
92
97
|
require ru
|
93
98
|
Object.const_get(File.basename(ru, '.rb').capitalize)
|
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.18.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: 2025-
|
11
|
+
date: 2025-07-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|