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.

@@ -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