pitchfork 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +1 -1
- data/docs/CONFIGURATION.md +10 -21
- data/docs/DESIGN.md +1 -1
- data/docs/FORK_SAFETY.md +8 -3
- data/docs/REFORKING.md +33 -23
- data/examples/echo.ru +0 -1
- data/examples/pitchfork.conf.rb +3 -41
- data/ext/pitchfork_http/pitchfork_http.c +166 -166
- data/lib/pitchfork/children.rb +15 -5
- data/lib/pitchfork/chunked.rb +123 -0
- data/lib/pitchfork/configurator.rb +7 -7
- data/lib/pitchfork/http_parser.rb +0 -1
- data/lib/pitchfork/http_server.rb +115 -115
- data/lib/pitchfork/message.rb +2 -1
- data/lib/pitchfork/refork_condition.rb +18 -0
- data/lib/pitchfork/version.rb +1 -1
- data/lib/pitchfork/worker.rb +17 -11
- data/lib/pitchfork.rb +51 -17
- metadata +4 -3
data/lib/pitchfork/children.rb
CHANGED
@@ -29,7 +29,6 @@ module Pitchfork
|
|
29
29
|
def register_mold(mold)
|
30
30
|
@pending_molds[mold.pid] = mold
|
31
31
|
@children[mold.pid] = mold
|
32
|
-
@mold = mold
|
33
32
|
end
|
34
33
|
|
35
34
|
def fetch(pid)
|
@@ -37,21 +36,29 @@ module Pitchfork
|
|
37
36
|
end
|
38
37
|
|
39
38
|
def update(message)
|
40
|
-
|
41
|
-
|
39
|
+
case message
|
40
|
+
when Message::MoldSpawned
|
41
|
+
mold = Worker.new(nil)
|
42
|
+
mold.update(message)
|
43
|
+
@pending_molds[mold.pid] = mold
|
44
|
+
@children[mold.pid] = mold
|
45
|
+
return mold
|
46
|
+
end
|
42
47
|
|
48
|
+
child = @children[message.pid] || (message.nr && @workers[message.nr])
|
43
49
|
child.update(message)
|
44
50
|
|
45
51
|
if child.mold?
|
46
|
-
@workers.delete(old_nr)
|
47
52
|
@pending_molds.delete(child.pid)
|
48
53
|
@molds[child.pid] = child
|
49
54
|
@mold = child
|
50
55
|
end
|
56
|
+
|
51
57
|
if child.pid
|
52
58
|
@children[child.pid] = child
|
53
59
|
@pending_workers.delete(child.nr)
|
54
60
|
end
|
61
|
+
|
55
62
|
child
|
56
63
|
end
|
57
64
|
|
@@ -73,7 +80,6 @@ module Pitchfork
|
|
73
80
|
end
|
74
81
|
|
75
82
|
def promote(worker)
|
76
|
-
@pending_molds[worker.pid] = worker
|
77
83
|
worker.promote(self.last_generation += 1)
|
78
84
|
end
|
79
85
|
|
@@ -81,6 +87,10 @@ module Pitchfork
|
|
81
87
|
!(@pending_workers.empty? && @pending_molds.empty?)
|
82
88
|
end
|
83
89
|
|
90
|
+
def restarting_workers_count
|
91
|
+
@pending_workers.size + @workers.count { |_, w| w.exiting? }
|
92
|
+
end
|
93
|
+
|
84
94
|
def pending_promotion?
|
85
95
|
!@pending_molds.empty?
|
86
96
|
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
if Rack::VERSION.first >= 3
|
5
|
+
require "rack/constants"
|
6
|
+
require "rack/utils"
|
7
|
+
end
|
8
|
+
|
9
|
+
module Pitchfork
|
10
|
+
# Middleware that applies chunked transfer encoding to response bodies
|
11
|
+
# when the response does not include a content-length header.
|
12
|
+
#
|
13
|
+
# This supports the trailer response header to allow the use of trailing
|
14
|
+
# headers in the chunked encoding. However, using this requires you manually
|
15
|
+
# specify a response body that supports a +trailers+ method. Example:
|
16
|
+
#
|
17
|
+
# [200, { 'trailer' => 'expires'}, ["Hello", "World"]]
|
18
|
+
# # error raised
|
19
|
+
#
|
20
|
+
# body = ["Hello", "World"]
|
21
|
+
# def body.trailers
|
22
|
+
# { 'expires' => Time.now.to_s }
|
23
|
+
# end
|
24
|
+
# [200, { 'trailer' => 'expires'}, body]
|
25
|
+
# # No exception raised
|
26
|
+
class Chunked
|
27
|
+
include Rack::Utils
|
28
|
+
|
29
|
+
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
30
|
+
|
31
|
+
# A body wrapper that emits chunked responses.
|
32
|
+
class Body
|
33
|
+
TERM = "\r\n"
|
34
|
+
TAIL = "0#{TERM}"
|
35
|
+
|
36
|
+
# Store the response body to be chunked.
|
37
|
+
def initialize(body)
|
38
|
+
@body = body
|
39
|
+
end
|
40
|
+
|
41
|
+
# For each element yielded by the response body, yield
|
42
|
+
# the element in chunked encoding.
|
43
|
+
def each(&block)
|
44
|
+
term = TERM
|
45
|
+
@body.each do |chunk|
|
46
|
+
size = chunk.bytesize
|
47
|
+
next if size == 0
|
48
|
+
|
49
|
+
yield [size.to_s(16), term, chunk.b, term].join
|
50
|
+
end
|
51
|
+
yield TAIL
|
52
|
+
yield_trailers(&block)
|
53
|
+
yield term
|
54
|
+
end
|
55
|
+
|
56
|
+
# Close the response body if the response body supports it.
|
57
|
+
def close
|
58
|
+
@body.close if @body.respond_to?(:close)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Do nothing as this class does not support trailer headers.
|
64
|
+
def yield_trailers
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# A body wrapper that emits chunked responses and also supports
|
69
|
+
# sending Trailer headers. Note that the response body provided to
|
70
|
+
# initialize must have a +trailers+ method that returns a hash
|
71
|
+
# of trailer headers, and the rack response itself should have a
|
72
|
+
# Trailer header listing the headers that the +trailers+ method
|
73
|
+
# will return.
|
74
|
+
class TrailerBody < Body
|
75
|
+
private
|
76
|
+
|
77
|
+
# Yield strings for each trailer header.
|
78
|
+
def yield_trailers
|
79
|
+
@body.trailers.each_pair do |k, v|
|
80
|
+
yield "#{k}: #{v}\r\n"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def initialize(app)
|
86
|
+
@app = app
|
87
|
+
end
|
88
|
+
|
89
|
+
# Whether the HTTP version supports chunked encoding (HTTP 1.1 does).
|
90
|
+
def chunkable_version?(ver)
|
91
|
+
case ver
|
92
|
+
# pre-HTTP/1.0 (informally "HTTP/0.9") HTTP requests did not have
|
93
|
+
# a version (nor response headers)
|
94
|
+
when 'HTTP/1.0', nil, 'HTTP/0.9'
|
95
|
+
false
|
96
|
+
else
|
97
|
+
true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# If the rack app returns a response that should have a body,
|
102
|
+
# but does not have content-length or transfer-encoding headers,
|
103
|
+
# modify the response to use chunked transfer-encoding.
|
104
|
+
def call(env)
|
105
|
+
status, headers, body = response = @app.call(env)
|
106
|
+
|
107
|
+
if chunkable_version?(env[Rack::SERVER_PROTOCOL]) &&
|
108
|
+
!STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
|
109
|
+
!headers[Rack::CONTENT_LENGTH] &&
|
110
|
+
!headers[Rack::TRANSFER_ENCODING]
|
111
|
+
|
112
|
+
headers[Rack::TRANSFER_ENCODING] = 'chunked'
|
113
|
+
if headers['trailer']
|
114
|
+
response[2] = TrailerBody.new(body)
|
115
|
+
else
|
116
|
+
response[2] = Body.new(body)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
response
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -30,11 +30,11 @@ module Pitchfork
|
|
30
30
|
:timeout => 20,
|
31
31
|
:logger => Logger.new($stderr),
|
32
32
|
:worker_processes => 1,
|
33
|
-
:
|
33
|
+
:after_worker_fork => lambda { |server, worker|
|
34
34
|
server.logger.info("worker=#{worker.nr} gen=#{worker.generation} pid=#{$$} spawned")
|
35
35
|
},
|
36
|
-
:
|
37
|
-
server.logger.info("gen=#{worker.generation} pid=#{$$}
|
36
|
+
:after_mold_fork => lambda { |server, worker|
|
37
|
+
server.logger.info("mold gen=#{worker.generation} pid=#{$$} spawned")
|
38
38
|
},
|
39
39
|
:after_worker_exit => lambda { |server, worker, status|
|
40
40
|
m = if worker.nil?
|
@@ -119,12 +119,12 @@ module Pitchfork
|
|
119
119
|
set[:logger] = obj
|
120
120
|
end
|
121
121
|
|
122
|
-
def
|
123
|
-
set_hook(:
|
122
|
+
def after_worker_fork(*args, &block)
|
123
|
+
set_hook(:after_worker_fork, block_given? ? block : args[0])
|
124
124
|
end
|
125
125
|
|
126
|
-
def
|
127
|
-
set_hook(:
|
126
|
+
def after_mold_fork(*args, &block)
|
127
|
+
set_hook(:after_mold_fork, block_given? ? block : args[0])
|
128
128
|
end
|
129
129
|
|
130
130
|
def after_worker_ready(*args, &block)
|
@@ -10,7 +10,7 @@ module Pitchfork
|
|
10
10
|
class HttpServer
|
11
11
|
# :stopdoc:
|
12
12
|
attr_accessor :app, :timeout, :worker_processes,
|
13
|
-
:
|
13
|
+
:after_worker_fork, :after_mold_fork,
|
14
14
|
:listener_opts, :children,
|
15
15
|
:orig_app, :config, :ready_pipe,
|
16
16
|
:default_middleware, :early_hints
|
@@ -27,8 +27,6 @@ module Pitchfork
|
|
27
27
|
|
28
28
|
NOOP = '.'
|
29
29
|
|
30
|
-
REFORKING_AVAILABLE = Pitchfork::CHILD_SUBREAPER_AVAILABLE || Process.pid == 1
|
31
|
-
|
32
30
|
# :startdoc:
|
33
31
|
# This Hash is considered a stable interface and changing its contents
|
34
32
|
# will allow you to switch between different installations of Pitchfork
|
@@ -63,7 +61,7 @@ module Pitchfork
|
|
63
61
|
@exit_status = 0
|
64
62
|
@app = app
|
65
63
|
@respawn = false
|
66
|
-
@last_check = time_now
|
64
|
+
@last_check = Pitchfork.time_now
|
67
65
|
@promotion_lock = Flock.new("pitchfork-promotion")
|
68
66
|
|
69
67
|
options = options.dup
|
@@ -131,7 +129,7 @@ module Pitchfork
|
|
131
129
|
else
|
132
130
|
build_app!
|
133
131
|
bind_listeners!
|
134
|
-
|
132
|
+
after_mold_fork.call(self, Worker.new(nil, pid: $$).promoted!)
|
135
133
|
end
|
136
134
|
|
137
135
|
if sync
|
@@ -178,7 +176,7 @@ module Pitchfork
|
|
178
176
|
|
179
177
|
# add a given address to the +listeners+ set, idempotently
|
180
178
|
# Allows workers to add a private, per-process listener via the
|
181
|
-
#
|
179
|
+
# after_worker_fork hook. Very useful for debugging and testing.
|
182
180
|
# +:tries+ may be specified as an option for the number of times
|
183
181
|
# to retry, and +:delay+ may be specified as the time in seconds
|
184
182
|
# to delay between retries.
|
@@ -259,7 +257,7 @@ module Pitchfork
|
|
259
257
|
when nil
|
260
258
|
# avoid murdering workers after our master process (or the
|
261
259
|
# machine) comes out of suspend/hibernation
|
262
|
-
if (@last_check + @timeout) >= (@last_check = time_now)
|
260
|
+
if (@last_check + @timeout) >= (@last_check = Pitchfork.time_now)
|
263
261
|
sleep_time = murder_lazy_workers
|
264
262
|
else
|
265
263
|
sleep_time = @timeout/2.0 + 1
|
@@ -289,13 +287,15 @@ module Pitchfork
|
|
289
287
|
worker = @children.update(message)
|
290
288
|
# TODO: should we send a message to the worker to acknowledge?
|
291
289
|
logger.info "worker=#{worker.nr} pid=#{worker.pid} registered"
|
292
|
-
when Message::
|
290
|
+
when Message::MoldSpawned
|
291
|
+
new_mold = @children.update(message)
|
292
|
+
logger.info("mold pid=#{new_mold.pid} gen=#{new_mold.generation} spawned")
|
293
|
+
when Message::MoldReady
|
293
294
|
old_molds = @children.molds
|
294
|
-
new_mold = @children.
|
295
|
-
logger.info("
|
296
|
-
@children.update(message)
|
295
|
+
new_mold = @children.update(message)
|
296
|
+
logger.info("mold pid=#{new_mold.pid} gen=#{new_mold.generation} ready")
|
297
297
|
old_molds.each do |old_mold|
|
298
|
-
logger.info("Terminating old mold pid=#{old_mold.pid}")
|
298
|
+
logger.info("Terminating old mold pid=#{old_mold.pid} gen=#{old_mold.generation}")
|
299
299
|
old_mold.soft_kill(:QUIT)
|
300
300
|
end
|
301
301
|
else
|
@@ -306,17 +306,19 @@ module Pitchfork
|
|
306
306
|
|
307
307
|
# Terminates all workers, but does not exit master process
|
308
308
|
def stop(graceful = true)
|
309
|
+
@respawn = false
|
309
310
|
wait_for_pending_workers
|
310
311
|
self.listeners = []
|
311
|
-
limit = time_now + timeout
|
312
|
-
until @children.workers.empty? || time_now > limit
|
312
|
+
limit = Pitchfork.time_now + timeout
|
313
|
+
until @children.workers.empty? || Pitchfork.time_now > limit
|
313
314
|
if graceful
|
314
315
|
soft_kill_each_child(:QUIT)
|
315
316
|
else
|
316
317
|
kill_each_child(:TERM)
|
317
318
|
end
|
318
|
-
|
319
|
-
|
319
|
+
if monitor_loop(false) == StopIteration
|
320
|
+
return StopIteration
|
321
|
+
end
|
320
322
|
end
|
321
323
|
kill_each_child(:KILL)
|
322
324
|
@promotion_lock.unlink
|
@@ -393,7 +395,7 @@ module Pitchfork
|
|
393
395
|
# forcibly terminate all workers that haven't checked in in timeout seconds. The timeout is implemented using an unlinked File
|
394
396
|
def murder_lazy_workers
|
395
397
|
next_sleep = @timeout - 1
|
396
|
-
now = time_now
|
398
|
+
now = Pitchfork.time_now(true)
|
397
399
|
@children.workers.each do |worker|
|
398
400
|
tick = worker.tick
|
399
401
|
0 == tick and next # skip workers that haven't processed any clients
|
@@ -444,28 +446,11 @@ module Pitchfork
|
|
444
446
|
def spawn_worker(worker, detach:)
|
445
447
|
logger.info("worker=#{worker.nr} gen=#{worker.generation} spawning...")
|
446
448
|
|
447
|
-
|
448
|
-
# We double fork so that the new worker is re-attached back
|
449
|
-
# to the master.
|
450
|
-
# This requires either PR_SET_CHILD_SUBREAPER which is exclusive to Linux 3.4
|
451
|
-
# or the master to be PID 1.
|
452
|
-
if detach && fork
|
453
|
-
exit
|
454
|
-
end
|
449
|
+
Pitchfork.fork_sibling do
|
455
450
|
worker.pid = Process.pid
|
456
451
|
|
457
452
|
after_fork_internal
|
458
453
|
worker_loop(worker)
|
459
|
-
if worker.mold?
|
460
|
-
mold_loop(worker)
|
461
|
-
end
|
462
|
-
exit
|
463
|
-
end
|
464
|
-
|
465
|
-
if detach
|
466
|
-
# If we double forked, we need to wait(2) so that the middle
|
467
|
-
# process doesn't end up a zombie.
|
468
|
-
Process.wait(pid)
|
469
454
|
end
|
470
455
|
|
471
456
|
worker
|
@@ -475,12 +460,14 @@ module Pitchfork
|
|
475
460
|
mold = Worker.new(nil)
|
476
461
|
mold.create_socketpair!
|
477
462
|
mold.pid = Pitchfork.clean_fork do
|
463
|
+
mold.pid = Process.pid
|
478
464
|
@promotion_lock.try_lock
|
479
465
|
mold.after_fork_in_child
|
480
466
|
build_app!
|
481
467
|
bind_listeners!
|
482
468
|
mold_loop(mold)
|
483
469
|
end
|
470
|
+
@promotion_lock.at_fork
|
484
471
|
@children.register_mold(mold)
|
485
472
|
end
|
486
473
|
|
@@ -494,7 +481,7 @@ module Pitchfork
|
|
494
481
|
|
495
482
|
if REFORKING_AVAILABLE
|
496
483
|
unless @children.mold&.spawn_worker(worker)
|
497
|
-
@logger.error("Failed to send a
|
484
|
+
@logger.error("Failed to send a spawn_worker command")
|
498
485
|
end
|
499
486
|
else
|
500
487
|
spawn_worker(worker, detach: false)
|
@@ -529,17 +516,18 @@ module Pitchfork
|
|
529
516
|
# If we're already in the middle of forking a new generation, we just continue
|
530
517
|
return unless @children.mold
|
531
518
|
|
532
|
-
# We don't shutdown any outdated worker if any worker is already being
|
533
|
-
# or a worker is exiting.
|
534
|
-
# impact on capacity.
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
519
|
+
# We don't shutdown any outdated worker if any worker is already being
|
520
|
+
# spawned or a worker is exiting. Only 10% of workers can be reforked at
|
521
|
+
# once to minimize the impact on capacity.
|
522
|
+
max_pending_workers = (worker_processes * 0.1).ceil
|
523
|
+
workers_to_restart = max_pending_workers - @children.restarting_workers_count
|
524
|
+
|
525
|
+
if workers_to_restart > 0
|
526
|
+
outdated_workers = @children.workers.select { |w| !w.exiting? && w.generation < @children.mold.generation }
|
527
|
+
outdated_workers.each do |worker|
|
528
|
+
logger.info("worker=#{worker.nr} pid=#{worker.pid} gen=#{worker.generation} restarting")
|
529
|
+
worker.soft_kill(:QUIT)
|
530
|
+
end
|
543
531
|
end
|
544
532
|
end
|
545
533
|
|
@@ -648,10 +636,10 @@ module Pitchfork
|
|
648
636
|
(@queue_sigs - exit_sigs).each { |sig| trap(sig, nil) }
|
649
637
|
trap(:CHLD, 'DEFAULT')
|
650
638
|
@sig_queue.clear
|
651
|
-
proc_name "worker[#{worker.nr}]
|
639
|
+
proc_name "(gen:#{worker.generation}) worker[#{worker.nr}]"
|
652
640
|
@children = nil
|
653
641
|
|
654
|
-
|
642
|
+
after_worker_fork.call(self, worker) # can drop perms and create listeners
|
655
643
|
LISTENERS.each { |sock| sock.close_on_exec = true }
|
656
644
|
|
657
645
|
@config = nil
|
@@ -662,10 +650,10 @@ module Pitchfork
|
|
662
650
|
readers
|
663
651
|
end
|
664
652
|
|
665
|
-
def init_mold_process(
|
666
|
-
proc_name "
|
667
|
-
|
668
|
-
readers = [
|
653
|
+
def init_mold_process(mold)
|
654
|
+
proc_name "(gen: #{mold.generation}) mold"
|
655
|
+
after_mold_fork.call(self, mold)
|
656
|
+
readers = [mold]
|
669
657
|
trap(:QUIT) { nuke_listeners!(readers) }
|
670
658
|
readers
|
671
659
|
end
|
@@ -691,83 +679,99 @@ module Pitchfork
|
|
691
679
|
ready = readers.dup
|
692
680
|
@after_worker_ready.call(self, worker)
|
693
681
|
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
682
|
+
while readers[0]
|
683
|
+
begin
|
684
|
+
worker.tick = Pitchfork.time_now(true)
|
685
|
+
while sock = ready.shift
|
686
|
+
# Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
|
687
|
+
# but that will return false
|
688
|
+
client = sock.accept_nonblock(exception: false)
|
689
|
+
client = false if client == :wait_readable
|
690
|
+
if client
|
691
|
+
case client
|
692
|
+
when Message::PromoteWorker
|
693
|
+
spawn_mold(worker.generation)
|
694
|
+
when Message
|
695
|
+
worker.update(client)
|
696
|
+
else
|
697
|
+
process_client(client)
|
698
|
+
worker.increment_requests_count
|
708
699
|
end
|
709
|
-
|
710
|
-
worker.update(client)
|
711
|
-
else
|
712
|
-
process_client(client)
|
713
|
-
worker.increment_requests_count
|
700
|
+
worker.tick = Pitchfork.time_now(true)
|
714
701
|
end
|
715
|
-
worker.tick = time_now.to_i
|
716
702
|
end
|
717
|
-
end
|
718
703
|
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
if @refork_condition.
|
723
|
-
if @
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
704
|
+
# timeout so we can .tick and keep parent from SIGKILL-ing us
|
705
|
+
worker.tick = Pitchfork.time_now(true)
|
706
|
+
|
707
|
+
if @refork_condition && !worker.outdated?
|
708
|
+
if @refork_condition.met?(worker, logger)
|
709
|
+
if spawn_mold(worker.generation)
|
710
|
+
logger.info("Refork condition met, promoting ourselves")
|
711
|
+
end
|
712
|
+
@refork_condition.backoff!
|
728
713
|
end
|
729
714
|
end
|
715
|
+
|
716
|
+
waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
|
717
|
+
rescue => e
|
718
|
+
Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
|
730
719
|
end
|
720
|
+
end
|
721
|
+
end
|
722
|
+
|
723
|
+
def spawn_mold(current_generation)
|
724
|
+
return false unless @promotion_lock.try_lock
|
731
725
|
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
726
|
+
begin
|
727
|
+
Pitchfork.fork_sibling do
|
728
|
+
mold = Worker.new(nil, pid: Process.pid, generation: current_generation)
|
729
|
+
mold.promote!
|
730
|
+
mold.start_promotion(@control_socket[1])
|
731
|
+
mold_loop(mold)
|
732
|
+
end
|
733
|
+
true
|
734
|
+
ensure
|
735
|
+
@promotion_lock.at_fork # We let the spawned mold own the lock
|
736
|
+
end
|
736
737
|
end
|
737
738
|
|
738
739
|
def mold_loop(mold)
|
739
740
|
readers = init_mold_process(mold)
|
740
741
|
waiter = prep_readers(readers)
|
741
|
-
mold.declare_promotion(@control_socket[1])
|
742
742
|
@promotion_lock.unlock
|
743
743
|
ready = readers.dup
|
744
744
|
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
745
|
+
mold.finish_promotion(@control_socket[1])
|
746
|
+
|
747
|
+
while readers[0]
|
748
|
+
begin
|
749
|
+
mold.tick = Pitchfork.time_now(true)
|
750
|
+
while sock = ready.shift
|
751
|
+
# Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
|
752
|
+
# but that will return false
|
753
|
+
message = sock.accept_nonblock(exception: false)
|
754
|
+
case message
|
755
|
+
when false
|
756
|
+
# no message, keep looping
|
757
|
+
when Message::SpawnWorker
|
758
|
+
begin
|
759
|
+
spawn_worker(Worker.new(message.nr, generation: mold.generation), detach: true)
|
760
|
+
rescue => error
|
761
|
+
raise BootFailure, error.message
|
762
|
+
end
|
763
|
+
else
|
764
|
+
logger.error("Unexpected mold message #{message.inspect}")
|
759
765
|
end
|
760
|
-
else
|
761
|
-
logger.error("Unexpected mold message #{message.inspect}")
|
762
766
|
end
|
763
|
-
end
|
764
767
|
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
768
|
+
# timeout so we can .tick and keep parent from SIGKILL-ing us
|
769
|
+
mold.tick = Pitchfork.time_now(true)
|
770
|
+
waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
|
771
|
+
rescue => e
|
772
|
+
Pitchfork.log_error(@logger, "mold loop error", e) if readers[0]
|
773
|
+
end
|
774
|
+
end
|
771
775
|
end
|
772
776
|
|
773
777
|
# delivers a signal to a worker and fails gracefully if the worker
|
@@ -820,9 +824,5 @@ module Pitchfork
|
|
820
824
|
listeners.each { |addr| listen(addr) }
|
821
825
|
raise ArgumentError, "no listeners" if LISTENERS.empty?
|
822
826
|
end
|
823
|
-
|
824
|
-
def time_now
|
825
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
826
|
-
end
|
827
827
|
end
|
828
828
|
end
|
data/lib/pitchfork/message.rb
CHANGED
@@ -123,7 +123,8 @@ module Pitchfork
|
|
123
123
|
SpawnWorker = Message.new(:nr)
|
124
124
|
WorkerSpawned = Message.new(:nr, :pid, :generation, :pipe)
|
125
125
|
PromoteWorker = Message.new(:generation)
|
126
|
-
|
126
|
+
MoldSpawned = Message.new(:nr, :pid, :generation, :pipe)
|
127
|
+
MoldReady = Message.new(:nr, :pid, :generation)
|
127
128
|
|
128
129
|
SoftKill = Message.new(:signum)
|
129
130
|
end
|
@@ -5,17 +5,35 @@ module Pitchfork
|
|
5
5
|
class RequestsCount
|
6
6
|
def initialize(request_counts)
|
7
7
|
@limits = request_counts
|
8
|
+
@backoff_until = nil
|
8
9
|
end
|
9
10
|
|
10
11
|
def met?(worker, logger)
|
11
12
|
if limit = @limits.fetch(worker.generation) { @limits.last }
|
12
13
|
if worker.requests_count >= limit
|
14
|
+
return false if backoff?
|
15
|
+
|
13
16
|
logger.info("worker=#{worker.nr} pid=#{worker.pid} processed #{worker.requests_count} requests, triggering a refork")
|
14
17
|
return true
|
15
18
|
end
|
16
19
|
end
|
17
20
|
false
|
18
21
|
end
|
22
|
+
|
23
|
+
def backoff?
|
24
|
+
return false if @backoff_until.nil?
|
25
|
+
|
26
|
+
if @backoff_until > Pitchfork.time_now
|
27
|
+
true
|
28
|
+
else
|
29
|
+
@backoff_until = nil
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def backoff!(delay = 10.0)
|
35
|
+
@backoff_until = Pitchfork.time_now + delay
|
36
|
+
end
|
19
37
|
end
|
20
38
|
end
|
21
39
|
end
|