pitchfork 0.10.0 → 0.11.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 +8 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +0 -1
- data/docs/CONFIGURATION.md +17 -2
- data/docs/FORK_SAFETY.md +6 -2
- data/lib/pitchfork/children.rb +12 -8
- data/lib/pitchfork/configurator.rb +19 -0
- data/lib/pitchfork/http_response.rb +10 -2
- data/lib/pitchfork/http_server.rb +114 -26
- data/lib/pitchfork/info.rb +1 -1
- data/lib/pitchfork/refork_condition.rb +7 -1
- data/lib/pitchfork/shared_memory.rb +6 -1
- data/lib/pitchfork/version.rb +1 -1
- data/lib/pitchfork/worker.rb +14 -10
- data/lib/pitchfork.rb +1 -37
- 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: 3b4583ce30977e64c83d59160b7798f93073c6d0935d77c552e91397388bb434
|
4
|
+
data.tar.gz: 02c2c0abe2a400496c07bbe7f41c4d316eaed60b69a7681518a805b8e6f91a4a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 21360afc97d6172038ad4bfeae1c0fab4eeef787024c5a6743b0fb72fd61feb259666f0e9b0d519c9173072505f6f205d4bec2d764ec964173ea9116be20336f
|
7
|
+
data.tar.gz: b1faf79e0f186011c41523ff6bacc50a8ab1ad039fc0a9e934abc8fb17ffbbedcf9295fa50415f650b2a7ca30e019d3825079da1f83eaf10068c24c2b1734b08
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# 0.11.0
|
4
|
+
|
5
|
+
- Drop invalid response headers.
|
6
|
+
- Enforce `spawn_timeout` for molds too.
|
7
|
+
- Gracefully shutdown the server if the mold appear to be corrupted (#79).
|
8
|
+
- Add more information in proctitle when forking a new sibbling.
|
9
|
+
- Add a `before_fork` callback called before forking new molds and new workers.
|
10
|
+
|
3
11
|
# 0.10.0
|
4
12
|
|
5
13
|
- Include requests count in workers proctitle.
|
data/Gemfile.lock
CHANGED
data/Rakefile
CHANGED
@@ -42,7 +42,6 @@ namespace :test do
|
|
42
42
|
# preferable to edit the test suite as little as possible.
|
43
43
|
task legacy_integration: :compile do
|
44
44
|
File.write("test/integration/random_blob", File.read("/dev/random", 1_000_000))
|
45
|
-
lib = File.expand_path("lib", __dir__)
|
46
45
|
path = "#{File.expand_path("exe", __dir__)}:#{ENV["PATH"]}"
|
47
46
|
old_path = ENV["PATH"]
|
48
47
|
ENV["PATH"] = "#{path}:#{old_path}"
|
data/docs/CONFIGURATION.md
CHANGED
@@ -241,10 +241,10 @@ for more details on nginx upstream configuration.
|
|
241
241
|
### `spawn_timeout`
|
242
242
|
|
243
243
|
```ruby
|
244
|
-
|
244
|
+
spawn_timeout 5
|
245
245
|
```
|
246
246
|
|
247
|
-
Sets the timeout for a newly spawned worker to be ready after being spawned.
|
247
|
+
Sets the timeout for a newly spawned worker or mold to be ready after being spawned.
|
248
248
|
|
249
249
|
This timeout is a safeguard against various low-level fork safety bugs that could cause
|
250
250
|
a process to dead-lock.
|
@@ -284,6 +284,16 @@ after_monitor_ready do |server|
|
|
284
284
|
end
|
285
285
|
```
|
286
286
|
|
287
|
+
### `before_fork`
|
288
|
+
|
289
|
+
Called by the mold before forking a new workers, and by workers before they spawn a new mold.
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
before_fork do |server|
|
293
|
+
server.logger.info("About to fork, closing connections!")
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
287
297
|
### `after_mold_fork`
|
288
298
|
|
289
299
|
```ruby
|
@@ -307,6 +317,11 @@ That is the case for instance of many SQL databases protocols.
|
|
307
317
|
This is also the callback in which memory optimizations, such as
|
308
318
|
heap compaction should be done.
|
309
319
|
|
320
|
+
This callback is also a good place to check for potential corruption
|
321
|
+
issues caused by forking. If you detect something wrong, you can
|
322
|
+
call `Process.exit`, and this mold won't be used, another one will be
|
323
|
+
spawned later. e.g. you can check `Socket.getaddrinfo` still works, etc.
|
324
|
+
|
310
325
|
### `after_worker_fork`
|
311
326
|
|
312
327
|
```ruby
|
data/docs/FORK_SAFETY.md
CHANGED
@@ -22,13 +22,17 @@ reopen connections and restart threads:
|
|
22
22
|
```ruby
|
23
23
|
# pitchfork.conf.rb
|
24
24
|
|
25
|
-
|
25
|
+
before_fork do
|
26
26
|
Sequel::DATABASES.each(&:disconnect)
|
27
27
|
end
|
28
28
|
|
29
|
-
|
29
|
+
after_mold_fork do
|
30
30
|
SomeLibary.connection.close
|
31
31
|
end
|
32
|
+
|
33
|
+
after_worker_fork do
|
34
|
+
SomeOtherLibary.connection.close
|
35
|
+
end
|
32
36
|
```
|
33
37
|
|
34
38
|
The documentation of any database client or network library you use should be
|
data/lib/pitchfork/children.rb
CHANGED
@@ -66,6 +66,11 @@ module Pitchfork
|
|
66
66
|
@workers.key?(nr)
|
67
67
|
end
|
68
68
|
|
69
|
+
def abandon(worker)
|
70
|
+
@workers.delete(worker.nr)
|
71
|
+
@pending_workers.delete(worker.nr)
|
72
|
+
end
|
73
|
+
|
69
74
|
def reap(pid)
|
70
75
|
if child = @children.delete(pid)
|
71
76
|
@pending_workers.delete(child.nr)
|
@@ -73,6 +78,9 @@ module Pitchfork
|
|
73
78
|
@molds.delete(child.pid)
|
74
79
|
@workers.delete(child.nr)
|
75
80
|
if @mold == child
|
81
|
+
@pending_workers.reject! do |nr, worker|
|
82
|
+
worker.generation == @mold.generation
|
83
|
+
end
|
76
84
|
@mold = nil
|
77
85
|
end
|
78
86
|
end
|
@@ -99,6 +107,10 @@ module Pitchfork
|
|
99
107
|
@molds.values
|
100
108
|
end
|
101
109
|
|
110
|
+
def empty?
|
111
|
+
@children.empty?
|
112
|
+
end
|
113
|
+
|
102
114
|
def each(&block)
|
103
115
|
@children.each_value(&block)
|
104
116
|
end
|
@@ -126,14 +138,6 @@ module Pitchfork
|
|
126
138
|
end
|
127
139
|
end
|
128
140
|
|
129
|
-
def hard_timeout(child)
|
130
|
-
child.hard_timeout!
|
131
|
-
rescue Errno::ESRCH
|
132
|
-
reap(child.pid)
|
133
|
-
child.close
|
134
|
-
true
|
135
|
-
end
|
136
|
-
|
137
141
|
def workers
|
138
142
|
@workers.values
|
139
143
|
end
|
@@ -34,9 +34,11 @@ module Pitchfork
|
|
34
34
|
:soft_timeout => 20,
|
35
35
|
:cleanup_timeout => 2,
|
36
36
|
:spawn_timeout => 10,
|
37
|
+
:timeout_signal => -> (_pid) { :KILL },
|
37
38
|
:timeout => 22,
|
38
39
|
:logger => default_logger,
|
39
40
|
:worker_processes => 1,
|
41
|
+
:before_fork => nil,
|
40
42
|
:after_worker_fork => lambda { |server, worker|
|
41
43
|
server.logger.info("worker=#{worker.nr} gen=#{worker.generation} pid=#{$$} spawned")
|
42
44
|
},
|
@@ -133,6 +135,10 @@ module Pitchfork
|
|
133
135
|
set[:logger] = obj
|
134
136
|
end
|
135
137
|
|
138
|
+
def before_fork(*args, &block)
|
139
|
+
set_hook(:before_fork, block_given? ? block : args[0], 1)
|
140
|
+
end
|
141
|
+
|
136
142
|
def after_worker_fork(*args, &block)
|
137
143
|
set_hook(:after_worker_fork, block_given? ? block : args[0])
|
138
144
|
end
|
@@ -175,6 +181,19 @@ module Pitchfork
|
|
175
181
|
set_int(:timeout, soft_timeout + cleanup_timeout, 5)
|
176
182
|
end
|
177
183
|
|
184
|
+
def timeout_signal(*args, &block)
|
185
|
+
if block_given?
|
186
|
+
set_hook(:timeout_signal, block, 1)
|
187
|
+
elsif args.first.respond_to?(:call)
|
188
|
+
set_hook(:timeout_signal, args.first, 1)
|
189
|
+
elsif args.first.is_a?(Symbol)
|
190
|
+
signal = args.first
|
191
|
+
set_hook(:timeout_signal, ->(_pid) { signal }, 1)
|
192
|
+
else
|
193
|
+
raise ArgumentError, "timeout_signal must be a symbol or a proc"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
178
197
|
def spawn_timeout(seconds)
|
179
198
|
set_int(:spawn_timeout, seconds, 1)
|
180
199
|
end
|
@@ -14,6 +14,8 @@ module Pitchfork
|
|
14
14
|
STATUS_CODES = defined?(Rack::Utils::HTTP_STATUS_CODES) ?
|
15
15
|
Rack::Utils::HTTP_STATUS_CODES : {}
|
16
16
|
|
17
|
+
ILLEGAL_HEADER_VALUE = /[\x00-\x08\x0A-\x1F]/
|
18
|
+
|
17
19
|
# internal API, code will always be common-enough-for-even-old-Rack
|
18
20
|
def err_response(code, response_start_sent)
|
19
21
|
"#{response_start_sent ? '' : 'HTTP/1.1 '}" \
|
@@ -23,10 +25,16 @@ module Pitchfork
|
|
23
25
|
def append_header(buf, key, value)
|
24
26
|
case value
|
25
27
|
when Array # Rack 3
|
26
|
-
value.each
|
28
|
+
value.each do |v|
|
29
|
+
next if ILLEGAL_HEADER_VALUE.match?(v)
|
30
|
+
buf << "#{key}: #{v}\r\n"
|
31
|
+
end
|
27
32
|
when /\n/ # Rack 2
|
28
33
|
# avoiding blank, key-only cookies with /\n+/
|
29
|
-
value.split(/\n+/).each
|
34
|
+
value.split(/\n+/).each do |v|
|
35
|
+
next if ILLEGAL_HEADER_VALUE.match?(v)
|
36
|
+
buf << "#{key}: #{v}\r\n"
|
37
|
+
end
|
30
38
|
else
|
31
39
|
buf << "#{key}: #{value}\r\n"
|
32
40
|
end
|
@@ -74,8 +74,8 @@ module Pitchfork
|
|
74
74
|
end
|
75
75
|
|
76
76
|
# :stopdoc:
|
77
|
-
attr_accessor :app, :timeout, :soft_timeout, :cleanup_timeout, :spawn_timeout, :worker_processes,
|
78
|
-
:after_worker_fork, :after_mold_fork,
|
77
|
+
attr_accessor :app, :timeout, :timeout_signal, :soft_timeout, :cleanup_timeout, :spawn_timeout, :worker_processes,
|
78
|
+
:before_fork, :after_worker_fork, :after_mold_fork,
|
79
79
|
:listener_opts, :children,
|
80
80
|
:orig_app, :config, :ready_pipe,
|
81
81
|
:default_middleware, :early_hints
|
@@ -201,7 +201,7 @@ module Pitchfork
|
|
201
201
|
else
|
202
202
|
build_app!
|
203
203
|
bind_listeners!
|
204
|
-
after_mold_fork.call(self, Worker.new(nil, pid: $$).promoted!)
|
204
|
+
after_mold_fork.call(self, Worker.new(nil, pid: $$).promoted!(@spawn_timeout))
|
205
205
|
end
|
206
206
|
|
207
207
|
if sync
|
@@ -315,7 +315,7 @@ module Pitchfork
|
|
315
315
|
end
|
316
316
|
end
|
317
317
|
stop # gracefully shutdown all workers on our way out
|
318
|
-
logger.info "master complete"
|
318
|
+
logger.info "master complete status=#{@exit_status}"
|
319
319
|
@exit_status
|
320
320
|
end
|
321
321
|
|
@@ -368,7 +368,7 @@ module Pitchfork
|
|
368
368
|
when Message::WorkerSpawned
|
369
369
|
worker = @children.update(message)
|
370
370
|
# TODO: should we send a message to the worker to acknowledge?
|
371
|
-
logger.info "worker=#{worker.nr} pid=#{worker.pid} registered"
|
371
|
+
logger.info "worker=#{worker.nr} pid=#{worker.pid} gen=#{worker.generation} registered"
|
372
372
|
when Message::MoldSpawned
|
373
373
|
new_mold = @children.update(message)
|
374
374
|
logger.info("mold pid=#{new_mold.pid} gen=#{new_mold.generation} spawned")
|
@@ -394,7 +394,7 @@ module Pitchfork
|
|
394
394
|
wait_for_pending_workers
|
395
395
|
self.listeners = []
|
396
396
|
limit = Pitchfork.time_now + timeout
|
397
|
-
until @children.
|
397
|
+
until @children.empty? || Pitchfork.time_now > limit
|
398
398
|
if graceful
|
399
399
|
@children.soft_kill_all(:TERM)
|
400
400
|
else
|
@@ -404,11 +404,17 @@ module Pitchfork
|
|
404
404
|
return StopIteration
|
405
405
|
end
|
406
406
|
end
|
407
|
-
|
407
|
+
|
408
|
+
@children.each do |child|
|
409
|
+
if child.pid
|
410
|
+
@children.hard_kill(@timeout_signal.call(child.pid), child)
|
411
|
+
end
|
412
|
+
end
|
408
413
|
@promotion_lock.unlink
|
409
414
|
end
|
410
415
|
|
411
416
|
def worker_exit(worker)
|
417
|
+
logger.info "worker=#{worker.nr} pid=#{worker.pid} gen=#{worker.generation} exiting"
|
412
418
|
proc_name status: "exiting"
|
413
419
|
|
414
420
|
if @before_worker_exit
|
@@ -494,8 +500,8 @@ module Pitchfork
|
|
494
500
|
now = Pitchfork.time_now(true)
|
495
501
|
next_sleep = @timeout - 1
|
496
502
|
|
497
|
-
@children.
|
498
|
-
deadline =
|
503
|
+
@children.each do |child|
|
504
|
+
deadline = child.deadline
|
499
505
|
if 0 == deadline # worker is idle
|
500
506
|
next
|
501
507
|
elsif deadline > now # worker still has time
|
@@ -506,28 +512,34 @@ module Pitchfork
|
|
506
512
|
next
|
507
513
|
else # worker is out of time
|
508
514
|
next_sleep = 0
|
509
|
-
hard_timeout(
|
515
|
+
hard_timeout(child)
|
510
516
|
end
|
511
517
|
end
|
512
518
|
|
513
519
|
next_sleep <= 0 ? 1 : next_sleep
|
514
520
|
end
|
515
521
|
|
516
|
-
def hard_timeout(
|
517
|
-
if
|
522
|
+
def hard_timeout(child)
|
523
|
+
if child.pid.nil? # Not yet registered, likely never spawned
|
524
|
+
logger.error "worker=#{child.nr} timed out during spawn, abandoning"
|
525
|
+
@children.abandon(worker)
|
526
|
+
return
|
527
|
+
end
|
528
|
+
|
529
|
+
if @after_worker_hard_timeout && !child.mold?
|
518
530
|
begin
|
519
|
-
@after_worker_hard_timeout.call(self,
|
531
|
+
@after_worker_hard_timeout.call(self, child)
|
520
532
|
rescue => error
|
521
533
|
Pitchfork.log_error(@logger, "after_worker_hard_timeout callback", error)
|
522
534
|
end
|
523
535
|
end
|
524
536
|
|
525
|
-
if
|
526
|
-
logger.error "mold pid=#{
|
537
|
+
if child.mold?
|
538
|
+
logger.error "mold pid=#{child.pid} gen=#{child.generation} timed out, killing"
|
527
539
|
else
|
528
|
-
logger.error "worker=#{
|
540
|
+
logger.error "worker=#{child.nr} pid=#{child.pid} gen=#{child.generation} timed out, killing"
|
529
541
|
end
|
530
|
-
@children.
|
542
|
+
@children.hard_kill(@timeout_signal.call(child.pid), child) # take no prisoners for hard timeout violations
|
531
543
|
end
|
532
544
|
|
533
545
|
def trigger_refork
|
@@ -563,7 +575,8 @@ module Pitchfork
|
|
563
575
|
# reason it gets stuck before reaching the worker loop,
|
564
576
|
# the monitor process will kill it.
|
565
577
|
worker.update_deadline(@spawn_timeout)
|
566
|
-
|
578
|
+
@before_fork&.call(self)
|
579
|
+
fork_sibling("spawn_worker") do
|
567
580
|
worker.pid = Process.pid
|
568
581
|
|
569
582
|
after_fork_internal
|
@@ -598,6 +611,8 @@ module Pitchfork
|
|
598
611
|
worker = Pitchfork::Worker.new(worker_nr)
|
599
612
|
|
600
613
|
if REFORKING_AVAILABLE
|
614
|
+
worker.generation = @children.mold&.generation || 0
|
615
|
+
|
601
616
|
unless @children.mold&.spawn_worker(worker)
|
602
617
|
@logger.error("Failed to send a spawn_worker command")
|
603
618
|
end
|
@@ -784,15 +799,18 @@ module Pitchfork
|
|
784
799
|
readers << worker
|
785
800
|
trap(:QUIT) { nuke_listeners!(readers) }
|
786
801
|
trap(:TERM) { nuke_listeners!(readers) }
|
802
|
+
trap(:INT) { nuke_listeners!(readers); exit!(0) }
|
787
803
|
readers
|
788
804
|
end
|
789
805
|
|
790
806
|
def init_mold_process(mold)
|
791
|
-
proc_name role: "(gen:#{mold.generation}) mold", status: "
|
807
|
+
proc_name role: "(gen:#{mold.generation}) mold", status: "init"
|
792
808
|
after_mold_fork.call(self, mold)
|
793
809
|
readers = [mold]
|
794
810
|
trap(:QUIT) { nuke_listeners!(readers) }
|
795
811
|
trap(:TERM) { nuke_listeners!(readers) }
|
812
|
+
trap(:INT) { nuke_listeners!(readers); exit!(0) }
|
813
|
+
proc_name role: "(gen:#{mold.generation}) mold", status: "ready"
|
796
814
|
readers
|
797
815
|
end
|
798
816
|
|
@@ -831,7 +849,7 @@ module Pitchfork
|
|
831
849
|
case client
|
832
850
|
when Message::PromoteWorker
|
833
851
|
if Info.fork_safe?
|
834
|
-
spawn_mold(worker
|
852
|
+
spawn_mold(worker)
|
835
853
|
else
|
836
854
|
logger.error("worker=#{worker.nr} gen=#{worker.generation} is no longer fork safe, can't refork")
|
837
855
|
end
|
@@ -852,8 +870,8 @@ module Pitchfork
|
|
852
870
|
if @refork_condition && Info.fork_safe? && !worker.outdated?
|
853
871
|
if @refork_condition.met?(worker, logger)
|
854
872
|
proc_name status: "requests: #{worker.requests_count}, spawning mold"
|
855
|
-
if spawn_mold(worker
|
856
|
-
logger.info("Refork condition met, promoting ourselves")
|
873
|
+
if spawn_mold(worker)
|
874
|
+
logger.info("worker=#{worker.nr} gen=#{worker.generation} Refork condition met, promoting ourselves")
|
857
875
|
end
|
858
876
|
@refork_condition.backoff!
|
859
877
|
end
|
@@ -867,13 +885,17 @@ module Pitchfork
|
|
867
885
|
end
|
868
886
|
end
|
869
887
|
|
870
|
-
def spawn_mold(
|
888
|
+
def spawn_mold(worker)
|
871
889
|
return false unless @promotion_lock.try_lock
|
872
890
|
|
891
|
+
worker.update_deadline(@spawn_timeout)
|
892
|
+
|
893
|
+
@before_fork&.call(self)
|
894
|
+
|
873
895
|
begin
|
874
|
-
|
875
|
-
mold = Worker.new(nil, pid: Process.pid, generation:
|
876
|
-
mold.promote!
|
896
|
+
fork_sibling("spawn_mold") do
|
897
|
+
mold = Worker.new(nil, pid: Process.pid, generation: worker.generation)
|
898
|
+
mold.promote!(@spawn_timeout)
|
877
899
|
mold.start_promotion(@control_socket[1])
|
878
900
|
mold_loop(mold)
|
879
901
|
end
|
@@ -907,8 +929,18 @@ module Pitchfork
|
|
907
929
|
when false
|
908
930
|
# no message, keep looping
|
909
931
|
when Message::SpawnWorker
|
932
|
+
retries = 1
|
910
933
|
begin
|
911
934
|
spawn_worker(Worker.new(message.nr, generation: mold.generation), detach: true)
|
935
|
+
rescue ForkFailure
|
936
|
+
if retries > 0
|
937
|
+
@logger.fatal("mold pid=#{mold.pid} gen=#{mold.generation} Failed to spawn a worker. Retrying.")
|
938
|
+
retries -= 1
|
939
|
+
retry
|
940
|
+
else
|
941
|
+
@logger.fatal("mold pid=#{mold.pid} gen=#{mold.generation} Failed to spawn a worker twice in a row. Corrupted mold process?")
|
942
|
+
Process.exit(1)
|
943
|
+
end
|
912
944
|
rescue => error
|
913
945
|
raise BootFailure, error.message
|
914
946
|
end
|
@@ -977,5 +1009,61 @@ module Pitchfork
|
|
977
1009
|
handler.timeout_request = SoftTimeout.request(@soft_timeout, handler)
|
978
1010
|
handler
|
979
1011
|
end
|
1012
|
+
|
1013
|
+
FORK_TIMEOUT = 5
|
1014
|
+
|
1015
|
+
def fork_sibling(role, &block)
|
1016
|
+
if REFORKING_AVAILABLE
|
1017
|
+
r, w = Pitchfork::Info.keep_ios(IO.pipe)
|
1018
|
+
# We double fork so that the new worker is re-attached back
|
1019
|
+
# to the master.
|
1020
|
+
# This requires either PR_SET_CHILD_SUBREAPER which is exclusive to Linux 3.4
|
1021
|
+
# or the master to be PID 1.
|
1022
|
+
if middle_pid = FORK_LOCK.synchronize { Process.fork } # parent
|
1023
|
+
w.close
|
1024
|
+
# We need to wait(2) so that the middle process doesn't end up a zombie.
|
1025
|
+
# The process only call fork again an exit so it should be pretty fast.
|
1026
|
+
# However it might need to execute some `Process._fork` or `at_exit` callbacks,
|
1027
|
+
# as well as Ruby's cleanup procedure to run finalizers etc, and there is a risk
|
1028
|
+
# of deadlock.
|
1029
|
+
# So in case it takes more than 5 seconds to exit, we kill it.
|
1030
|
+
# TODO: rather than to busy loop here, we handle it in the worker/mold loop
|
1031
|
+
process_wait_with_timeout(middle_pid, FORK_TIMEOUT)
|
1032
|
+
pid_str = r.gets
|
1033
|
+
r.close
|
1034
|
+
if pid_str
|
1035
|
+
Integer(pid_str)
|
1036
|
+
else
|
1037
|
+
raise ForkFailure, "fork_sibling didn't succeed in #{FORK_TIMEOUT} seconds"
|
1038
|
+
end
|
1039
|
+
else # first child
|
1040
|
+
r.close
|
1041
|
+
Process.setproctitle("<pitchfork fork_sibling(#{role})>")
|
1042
|
+
pid = Pitchfork.clean_fork do
|
1043
|
+
# detach into a grand child
|
1044
|
+
w.close
|
1045
|
+
yield
|
1046
|
+
end
|
1047
|
+
w.puts(pid)
|
1048
|
+
w.close
|
1049
|
+
exit
|
1050
|
+
end
|
1051
|
+
else
|
1052
|
+
clean_fork(&block)
|
1053
|
+
end
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
def process_wait_with_timeout(pid, timeout)
|
1057
|
+
(timeout * 50).times do
|
1058
|
+
_, status = Process.waitpid2(pid, Process::WNOHANG)
|
1059
|
+
return status if status
|
1060
|
+
sleep 0.02 # 50 * 20ms => 1s
|
1061
|
+
end
|
1062
|
+
|
1063
|
+
# The process didn't exit in the allotted time, so we kill it.
|
1064
|
+
Process.kill(@timeout_signal.call(pid), pid)
|
1065
|
+
_, status = Process.waitpid2(pid)
|
1066
|
+
status
|
1067
|
+
end
|
980
1068
|
end
|
981
1069
|
end
|
data/lib/pitchfork/info.rb
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
module Pitchfork
|
4
4
|
module ReforkCondition
|
5
|
+
@backoff_delay = 10.0
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :backoff_delay
|
9
|
+
end
|
10
|
+
|
5
11
|
class RequestsCount
|
6
12
|
def initialize(request_counts)
|
7
13
|
@limits = request_counts
|
@@ -31,7 +37,7 @@ module Pitchfork
|
|
31
37
|
end
|
32
38
|
end
|
33
39
|
|
34
|
-
def backoff!(delay =
|
40
|
+
def backoff!(delay = ReforkCondition.backoff_delay)
|
35
41
|
@backoff_until = Pitchfork.time_now + delay
|
36
42
|
end
|
37
43
|
end
|
@@ -10,7 +10,8 @@ module Pitchfork
|
|
10
10
|
CURRENT_GENERATION_OFFSET = 0
|
11
11
|
SHUTDOWN_OFFSET = 1
|
12
12
|
MOLD_TICK_OFFSET = 2
|
13
|
-
|
13
|
+
MOLD_PROMOTION_TICK_OFFSET = 3
|
14
|
+
WORKER_TICK_OFFSET = 4
|
14
15
|
|
15
16
|
DROPS = [Raindrops.new(PER_DROP)]
|
16
17
|
|
@@ -49,6 +50,10 @@ module Pitchfork
|
|
49
50
|
self[MOLD_TICK_OFFSET]
|
50
51
|
end
|
51
52
|
|
53
|
+
def mold_promotion_deadline
|
54
|
+
self[MOLD_PROMOTION_TICK_OFFSET]
|
55
|
+
end
|
56
|
+
|
52
57
|
def worker_deadline(worker_nr)
|
53
58
|
self[WORKER_TICK_OFFSET + worker_nr]
|
54
59
|
end
|
data/lib/pitchfork/version.rb
CHANGED
data/lib/pitchfork/worker.rb
CHANGED
@@ -27,7 +27,7 @@ module Pitchfork
|
|
27
27
|
@deadline_drop = SharedMemory.worker_deadline(nr)
|
28
28
|
self.deadline = 0
|
29
29
|
else
|
30
|
-
promoted!
|
30
|
+
promoted!(nil)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
@@ -55,6 +55,13 @@ module Pitchfork
|
|
55
55
|
message.class.members.each do |member|
|
56
56
|
send("#{member}=", message.public_send(member))
|
57
57
|
end
|
58
|
+
|
59
|
+
case message
|
60
|
+
when Message::MoldSpawned
|
61
|
+
@deadline_drop = SharedMemory.mold_promotion_deadline
|
62
|
+
when Message::MoldReady
|
63
|
+
@deadline_drop = SharedMemory.mold_deadline
|
64
|
+
end
|
58
65
|
end
|
59
66
|
|
60
67
|
def register_to_master(control_socket)
|
@@ -75,6 +82,7 @@ module Pitchfork
|
|
75
82
|
message = Message::MoldReady.new(@nr, @pid, generation)
|
76
83
|
control_socket.sendmsg(message)
|
77
84
|
SharedMemory.current_generation = @generation
|
85
|
+
@deadline_drop = SharedMemory.mold_deadline
|
78
86
|
end
|
79
87
|
|
80
88
|
def promote(generation)
|
@@ -85,16 +93,16 @@ module Pitchfork
|
|
85
93
|
send_message_nonblock(Message::SpawnWorker.new(new_worker.nr))
|
86
94
|
end
|
87
95
|
|
88
|
-
def promote!
|
96
|
+
def promote!(timeout)
|
89
97
|
@generation += 1
|
90
|
-
promoted!
|
98
|
+
promoted!(timeout)
|
91
99
|
end
|
92
100
|
|
93
|
-
def promoted!
|
101
|
+
def promoted!(timeout)
|
94
102
|
@mold = true
|
95
103
|
@nr = nil
|
96
|
-
@deadline_drop = SharedMemory.
|
97
|
-
|
104
|
+
@deadline_drop = SharedMemory.mold_promotion_deadline
|
105
|
+
update_deadline(timeout) if timeout
|
98
106
|
self
|
99
107
|
end
|
100
108
|
|
@@ -143,10 +151,6 @@ module Pitchfork
|
|
143
151
|
Process.kill(sig, pid)
|
144
152
|
end
|
145
153
|
|
146
|
-
def hard_timeout!
|
147
|
-
hard_kill(:KILL)
|
148
|
-
end
|
149
|
-
|
150
154
|
# this only runs when the Rack app.call is not running
|
151
155
|
# act like a listener
|
152
156
|
def accept_nonblock(exception: nil) # :nodoc:
|
data/lib/pitchfork.rb
CHANGED
@@ -33,6 +33,7 @@ module Pitchfork
|
|
33
33
|
ClientShutdown = Class.new(EOFError)
|
34
34
|
|
35
35
|
BootFailure = Class.new(StandardError)
|
36
|
+
ForkFailure = Class.new(StandardError)
|
36
37
|
|
37
38
|
# :stopdoc:
|
38
39
|
|
@@ -196,43 +197,6 @@ module Pitchfork
|
|
196
197
|
end
|
197
198
|
end
|
198
199
|
|
199
|
-
def fork_sibling(&block)
|
200
|
-
if REFORKING_AVAILABLE
|
201
|
-
# We double fork so that the new worker is re-attached back
|
202
|
-
# to the master.
|
203
|
-
# This requires either PR_SET_CHILD_SUBREAPER which is exclusive to Linux 3.4
|
204
|
-
# or the master to be PID 1.
|
205
|
-
if middle_pid = FORK_LOCK.synchronize { Process.fork } # parent
|
206
|
-
# We need to wait(2) so that the middle process doesn't end up a zombie.
|
207
|
-
# The process only call fork again an exit so it should be pretty fast.
|
208
|
-
# However it might need to execute some `Process._fork` or `at_exit` callbacks,
|
209
|
-
# so it case it takes more than 5 seconds to exit, we kill it with SIGBUS
|
210
|
-
# to produce a crash report, as this is indicative of a nasty bug.
|
211
|
-
process_wait_with_timeout(middle_pid, 5, :BUS)
|
212
|
-
else # first child
|
213
|
-
Process.setproctitle("<pitchfork fork_sibling>")
|
214
|
-
clean_fork(&block) # detach into a grand child
|
215
|
-
exit
|
216
|
-
end
|
217
|
-
else
|
218
|
-
clean_fork(&block)
|
219
|
-
end
|
220
|
-
|
221
|
-
nil # it's tricky to return the PID
|
222
|
-
end
|
223
|
-
|
224
|
-
def process_wait_with_timeout(pid, timeout, timeout_signal = :KILL)
|
225
|
-
(timeout * 200).times do
|
226
|
-
status = Process.wait(pid, Process::WNOHANG)
|
227
|
-
return status if status
|
228
|
-
sleep 0.005 # 200 * 5ms => 1s
|
229
|
-
end
|
230
|
-
|
231
|
-
# The process didn't exit in the allotted time, so we kill it.
|
232
|
-
Process.kill(timeout_signal, pid)
|
233
|
-
Process.wait(pid)
|
234
|
-
end
|
235
|
-
|
236
200
|
def time_now(int = false)
|
237
201
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, int ? :second : :float_second)
|
238
202
|
end
|
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.11.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-12-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: raindrops
|