pitchfork 0.10.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|