pitchfork 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- child = @children[message.pid] || (message.nr && @workers[message.nr])
41
- old_nr = child.nr
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
- :after_fork => lambda { |server, worker|
33
+ :after_worker_fork => lambda { |server, worker|
34
34
  server.logger.info("worker=#{worker.nr} gen=#{worker.generation} pid=#{$$} spawned")
35
35
  },
36
- :after_promotion => lambda { |server, worker|
37
- server.logger.info("gen=#{worker.generation} pid=#{$$} promoted")
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 after_fork(*args, &block)
123
- set_hook(:after_fork, block_given? ? block : args[0])
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 after_promotion(*args, &block)
127
- set_hook(:after_promotion, block_given? ? block : args[0])
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)
@@ -1,7 +1,6 @@
1
1
  # -*- encoding: binary -*-
2
2
  # :enddoc:
3
3
  # no stable API here
4
- require 'pitchfork/pitchfork_http'
5
4
 
6
5
  module Pitchfork
7
6
  class HttpParser
@@ -10,7 +10,7 @@ module Pitchfork
10
10
  class HttpServer
11
11
  # :stopdoc:
12
12
  attr_accessor :app, :timeout, :worker_processes,
13
- :after_fork, :after_promotion,
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
- after_promotion.call(self, Worker.new(nil, pid: $$).promoted!)
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
- # after_fork hook. Very useful for debugging and testing.
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::WorkerPromoted
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.fetch(message.pid)
295
- logger.info("worker=#{new_mold.nr} pid=#{new_mold.pid} promoted to a mold")
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
- sleep(0.1)
319
- reap_all_workers
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.to_i
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
- pid = Pitchfork.clean_fork do
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 spawn_woker command")
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 spawned
533
- # or a worker is exiting. Workers are only reforked one by one to minimize the
534
- # impact on capacity.
535
- # In the future we may want to use a dynamic limit, e.g. 10% of workers may be down at
536
- # a time.
537
- return if @children.pending_workers?
538
- return if @children.workers.any?(&:exiting?)
539
-
540
- if outdated_worker = @children.workers.find { |w| w.generation < @children.mold.generation }
541
- logger.info("worker=#{outdated_worker.nr} pid=#{outdated_worker.pid} restarting")
542
- outdated_worker.soft_kill(:QUIT)
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}] (gen:#{worker.generation})"
639
+ proc_name "(gen:#{worker.generation}) worker[#{worker.nr}]"
652
640
  @children = nil
653
641
 
654
- after_fork.call(self, worker) # can drop perms and create listeners
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(worker)
666
- proc_name "mold (gen: #{worker.generation})"
667
- after_promotion.call(self, worker)
668
- readers = [worker]
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
- begin
695
- worker.tick = time_now.to_i
696
- while sock = ready.shift
697
- # Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
698
- # but that will return false
699
- client = sock.accept_nonblock(exception: false)
700
- client = false if client == :wait_readable
701
- if client
702
- case client
703
- when Message::PromoteWorker
704
- if @promotion_lock.try_lock
705
- logger.info("Refork asked by master, promoting ourselves")
706
- worker.tick = time_now.to_i
707
- return worker.promoted!
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
- when Message
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
- # timeout so we can .tick and keep parent from SIGKILL-ing us
720
- worker.tick = time_now.to_i
721
- if @refork_condition && !worker.outdated?
722
- if @refork_condition.met?(worker, logger)
723
- if @promotion_lock.try_lock
724
- logger.info("Refork condition met, promoting ourselves")
725
- return worker.promote! # We've been promoted we can exit the loop
726
- else
727
- # TODO: if we couldn't acquire the lock, we should backoff the refork_condition to avoid hammering the lock
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
- waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
733
- rescue => e
734
- Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
735
- end while readers[0]
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
- begin
746
- mold.tick = time_now.to_i
747
- while sock = ready.shift
748
- # Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
749
- # but that will return false
750
- message = sock.accept_nonblock(exception: false)
751
- case message
752
- when false
753
- # no message, keep looping
754
- when Message::SpawnWorker
755
- begin
756
- spawn_worker(Worker.new(message.nr, generation: mold.generation), detach: true)
757
- rescue => error
758
- raise BootFailure, error.message
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
- # timeout so we can .tick and keep parent from SIGKILL-ing us
766
- mold.tick = time_now.to_i
767
- waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
768
- rescue => e
769
- Pitchfork.log_error(@logger, "mold loop error", e) if readers[0]
770
- end while readers[0]
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
@@ -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
- WorkerPromoted = Message.new(:nr, :pid, :generation)
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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pitchfork
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  module Const
6
6
  UNICORN_VERSION = '6.1.0'
7
7
  end