pitchfork 0.2.0 → 0.3.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/.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
|