resqued 0.8.5 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGES.md +23 -0
  3. data/exe/resqued +41 -22
  4. data/lib/resqued.rb +5 -5
  5. data/lib/resqued/config.rb +7 -7
  6. data/lib/resqued/config/after_fork.rb +1 -1
  7. data/lib/resqued/config/base.rb +1 -1
  8. data/lib/resqued/config/before_fork.rb +1 -1
  9. data/lib/resqued/config/worker.rb +13 -13
  10. data/lib/resqued/daemon.rb +1 -0
  11. data/lib/resqued/exec_on_hup.rb +43 -0
  12. data/lib/resqued/listener.rb +51 -49
  13. data/lib/resqued/listener_pool.rb +97 -0
  14. data/lib/resqued/listener_proxy.rb +40 -31
  15. data/lib/resqued/listener_state.rb +8 -0
  16. data/lib/resqued/logging.rb +15 -8
  17. data/lib/resqued/master.rb +94 -98
  18. data/lib/resqued/master_state.rb +73 -0
  19. data/lib/resqued/procline_version.rb +2 -2
  20. data/lib/resqued/sleepy.rb +6 -4
  21. data/lib/resqued/test_case.rb +3 -3
  22. data/lib/resqued/version.rb +1 -1
  23. data/lib/resqued/worker.rb +18 -13
  24. data/spec/fixtures/test_case_after_fork_raises.rb +5 -2
  25. data/spec/fixtures/test_case_before_fork_raises.rb +4 -1
  26. data/spec/fixtures/test_case_environment.rb +3 -1
  27. data/spec/integration/master_inherits_child_spec.rb +85 -0
  28. data/spec/integration/restart_spec.rb +63 -0
  29. data/spec/resqued/backoff_spec.rb +27 -27
  30. data/spec/resqued/config/fork_event_spec.rb +8 -8
  31. data/spec/resqued/config/worker_spec.rb +63 -50
  32. data/spec/resqued/config_spec.rb +6 -6
  33. data/spec/resqued/sleepy_spec.rb +10 -11
  34. data/spec/resqued/test_case_spec.rb +7 -7
  35. data/spec/spec_helper.rb +5 -1
  36. data/spec/support/custom_matchers.rb +10 -2
  37. data/spec/support/extra-child-shim +6 -0
  38. data/spec/support/resqued_path.rb +11 -0
  39. metadata +44 -27
  40. data/exe/resqued-listener +0 -6
@@ -0,0 +1,73 @@
1
+ module Resqued
2
+ class MasterState
3
+ def initialize
4
+ @listeners_created = 0
5
+ @listener_states = {}
6
+ end
7
+
8
+ # Public: When starting fresh, from command-line options, assign the initial values.
9
+ def init(options)
10
+ @config_paths = options.fetch(:config_paths)
11
+ @exec_on_hup = options.fetch(:exec_on_hup, false)
12
+ @fast_exit = options.fetch(:fast_exit, false)
13
+ @pidfile = options.fetch(:master_pidfile, nil)
14
+ end
15
+
16
+ # Public: Restore state from a serialized form.
17
+ def restore(data)
18
+ @config_paths = data[:config_paths]
19
+ @current_listener_pid = data[:current_listener_pid]
20
+ @exec_on_hup = data[:exec_on_hup]
21
+ @fast_exit = data[:fast_exit]
22
+ @last_good_listener_pid = data[:last_good_listener_pid]
23
+ @listeners_created = data[:listeners_created]
24
+ data[:listener_states].each do |lsh|
25
+ @listener_states[lsh[:pid]] = ListenerState.new.tap do |ls|
26
+ ls.master_socket = lsh[:master_socket] && Socket.for_fd(lsh[:master_socket])
27
+ ls.options = lsh[:options]
28
+ ls.pid = lsh[:pid]
29
+ ls.worker_pids = lsh[:worker_pids]
30
+ end
31
+ end
32
+ @paused = data[:paused]
33
+ @pidfile = data[:pidfile]
34
+ end
35
+
36
+ # Public: Return this state so that it can be serialized.
37
+ def to_h
38
+ {
39
+ config_paths: @config_paths,
40
+ current_listener_pid: @current_listener_pid,
41
+ exec_on_hup: @exec_on_hup,
42
+ fast_exit: @fast_exit,
43
+ last_good_listener_pid: @last_good_listener_pid,
44
+ listeners_created: @listeners_created,
45
+ listener_states: @listener_states.values.map { |ls|
46
+ {
47
+ master_socket: ls.master_socket&.to_i,
48
+ options: ls.options,
49
+ pid: ls.pid,
50
+ worker_pids: ls.worker_pids,
51
+ }
52
+ },
53
+ paused: @paused,
54
+ pidfile: @pidfile,
55
+ }
56
+ end
57
+
58
+ # Public: Return an array of open sockets or other file handles that should be forwarded to a new master.
59
+ def sockets
60
+ @listener_states.values.map { |l| l.master_socket }.compact
61
+ end
62
+
63
+ attr_reader :config_paths
64
+ attr_accessor :current_listener_pid
65
+ attr_reader :exec_on_hup
66
+ attr_reader :fast_exit
67
+ attr_accessor :last_good_listener_pid
68
+ attr_accessor :listeners_created
69
+ attr_reader :listener_states
70
+ attr_accessor :paused
71
+ attr_reader :pidfile
72
+ end
73
+ end
@@ -1,4 +1,4 @@
1
- require 'resqued/version'
1
+ require "resqued/version"
2
2
 
3
3
  module Resqued
4
4
  module ProclineVersion
@@ -6,7 +6,7 @@ module Resqued
6
6
  @version ||=
7
7
  begin
8
8
  # If we've built a custom version, this should show the custom version.
9
- Gem.loaded_specs['resqued'].version.to_s
9
+ Gem.loaded_specs["resqued"].version.to_s
10
10
  rescue Object
11
11
  # If this isn't a gem, fall back to the version in resqued/version.rb.
12
12
  Resqued::VERSION
@@ -1,9 +1,11 @@
1
- require 'fcntl'
2
- require 'kgio'
1
+ require "fcntl"
2
+ require "kgio"
3
3
 
4
4
  module Resqued
5
5
  module Sleepy
6
- # Public: Like sleep, but the sleep is interrupted if input is detected on one of the provided IO objects, or if `awake` is called (e.g. from a signal handler).
6
+ # Public: Like sleep, but the sleep is interrupted if input is
7
+ # detected on one of the provided IO objects, or if `awake` is
8
+ # called (e.g. from a signal handler).
7
9
  def yawn(duration, *inputs)
8
10
  if duration > 0
9
11
  inputs = [self_pipe[0]] + [inputs].flatten.compact
@@ -14,7 +16,7 @@ module Resqued
14
16
 
15
17
  # Public: Break out of `yawn`.
16
18
  def awake
17
- self_pipe[1].kgio_trywrite('.')
19
+ self_pipe[1].kgio_trywrite(".")
18
20
  end
19
21
 
20
22
  # Private.
@@ -1,5 +1,5 @@
1
- require 'resqued/config'
2
- require 'resqued/runtime_info'
1
+ require "resqued/config"
2
+ require "resqued/runtime_info"
3
3
 
4
4
  module Resqued
5
5
  module TestCase
@@ -17,7 +17,7 @@ module Resqued
17
17
  config = Resqued::Config.new(paths)
18
18
  config.before_fork(RuntimeInfo.new)
19
19
  config.build_workers
20
- config.after_fork(Resque::Worker.new('*'))
20
+ config.after_fork(Resque::Worker.new("*"))
21
21
  end
22
22
  end
23
23
 
@@ -1,3 +1,3 @@
1
1
  module Resqued
2
- VERSION = '0.8.5'
2
+ VERSION = '0.10.2'
3
3
  end
@@ -1,17 +1,19 @@
1
- require 'resque'
1
+ require "resque"
2
+ require "digest"
2
3
 
3
- require 'resqued/backoff'
4
- require 'resqued/logging'
4
+ require "resqued/backoff"
5
+ require "resqued/logging"
5
6
 
6
7
  module Resqued
7
8
  # Models a worker process.
8
9
  class Worker
9
10
  include Resqued::Logging
10
11
 
11
- DEFAULT_WORKER_FACTORY = ->(queues) {
12
+ DEFAULT_WORKER_FACTORY = lambda { |queues|
12
13
  resque_worker = Resque::Worker.new(*queues)
13
- resque_worker.term_child = true if resque_worker.respond_to?('term_child=')
14
- Resque.redis.client.reconnect
14
+ resque_worker.term_child = true if resque_worker.respond_to?("term_child=")
15
+ redis_client = Resque.redis.respond_to?(:_client) ? Resque.redis._client : Resque.redis.client
16
+ redis_client.reconnect
15
17
  resque_worker
16
18
  }
17
19
 
@@ -42,12 +44,13 @@ module Resqued
42
44
 
43
45
  # Public: A string that compares if this worker is equivalent to a worker in another Resqued::Listener.
44
46
  def queue_key
45
- queues.sort.join(';')
47
+ Digest::SHA256.hexdigest(queues.sort.join(";"))
46
48
  end
47
49
 
48
50
  # Public: Claim this worker for another listener's worker.
49
51
  def wait_for(pid)
50
52
  raise "Already running #{@pid} (can't wait for #{pid})" if @pid
53
+
51
54
  @self_started = false
52
55
  @pids << pid
53
56
  @pid = pid
@@ -55,16 +58,17 @@ module Resqued
55
58
 
56
59
  # Public: The old worker process finished!
57
60
  def finished!(process_status)
58
- if process_status.nil? && ! @self_started
59
- log :debug, "(#{@pid}/#{@pids.inspect}/self_started=#{@self_started}/killed=#{@killed}) I am no longer blocked."
61
+ summary = "(#{@pid}/#{@pids.inspect}/self_started=#{@self_started}/killed=#{@killed})"
62
+ if process_status.nil? && !@self_started
63
+ log :debug, "#{summary} I am no longer blocked."
60
64
  @pid = nil
61
65
  @backoff.died unless @killed
62
- elsif ! process_status.nil? && @self_started
63
- log :debug, "(#{@pid}/#{@pids.inspect}/self_started=#{@self_started}/killed=#{@killed}) I exited: #{process_status}"
66
+ elsif !process_status.nil? && @self_started
67
+ log :debug, "#{summary} I exited: #{process_status}"
64
68
  @pid = nil
65
69
  @backoff.died unless @killed
66
70
  else
67
- log :debug, "(#{@pid}/#{@pids.inspect}/self_started=#{@self_started}/killed=#{@killed}) Reports of my death are highly exaggerated (#{process_status.inspect})"
71
+ log :debug, "#{summary} Reports of my death are highly exaggerated (#{process_status.inspect})"
68
72
  end
69
73
  end
70
74
 
@@ -76,6 +80,7 @@ module Resqued
76
80
  # Public: Start a job, if there's one waiting in one of my queues.
77
81
  def try_start
78
82
  return if @backoff.wait?
83
+
79
84
  @backoff.started
80
85
  @self_started = true
81
86
  @killed = false
@@ -85,7 +90,7 @@ module Resqued
85
90
  log "Forked worker #{@pid}"
86
91
  else
87
92
  # In case we get a signal before resque is ready for it.
88
- Resqued::Listener::ALL_SIGNALS.each { |signal| trap(signal, 'DEFAULT') }
93
+ Resqued::Listener::ALL_SIGNALS.each { |signal| trap(signal, "DEFAULT") }
89
94
  trap(:QUIT) { exit! 0 } # If we get a QUIT during boot, just spin back down.
90
95
  $0 = "STARTING RESQUE FOR #{queues.join(',')}"
91
96
  resque_worker = @worker_factory.call(queues)
@@ -1,3 +1,6 @@
1
- after_fork { raise 'boom' }
1
+ after_fork do
2
+ raise "boom"
3
+ end
4
+
2
5
  worker_pool 100
3
- queue 'test'
6
+ queue "test"
@@ -1,2 +1,5 @@
1
- before_fork { raise 'boom' }
1
+ before_fork do
2
+ raise "boom"
3
+ end
4
+
2
5
  worker "test"
@@ -1 +1,3 @@
1
- before_fork { Resque.redis = Redis.new(:host => 'localhost', :port => ENV['RESQUED_TEST_REDIS_PORT'].to_i) }
1
+ before_fork do
2
+ Resque.redis = Redis.new(host: "localhost", port: ENV["RESQUED_TEST_REDIS_PORT"].to_i)
3
+ end
@@ -0,0 +1,85 @@
1
+ require "spec_helper"
2
+ require "timeout"
3
+
4
+ describe "Resqued master with an extra child process" do
5
+ include ResquedPath
6
+
7
+ # Starts resqued with an extra child process.
8
+ def start_resqued_with_extra_child
9
+ shim_path = File.expand_path("../support/extra-child-shim", File.dirname(__FILE__))
10
+
11
+ config_path = File.join(SPEC_TEMPDIR, "config.rb")
12
+ File.write(config_path, <<-CONFIG)
13
+ before_fork { File.write(ENV["LISTENER_PIDFILE"], $$.to_s) }
14
+ CONFIG
15
+
16
+ logfile = File.join(SPEC_TEMPDIR, "resqued.log")
17
+ File.write(logfile, "") # truncate it
18
+
19
+ env = {
20
+ "LISTENER_PIDFILE" => listener_pidfile,
21
+ "EXTRA_CHILD_PIDFILE" => extra_child_pidfile,
22
+ }
23
+
24
+ pid = spawn(env, shim_path, resqued_path, "--logfile", logfile, config_path)
25
+ sleep 1.0
26
+ pid
27
+ end
28
+
29
+ let(:extra_child_pidfile) { File.join(SPEC_TEMPDIR, "extra-child.pid") }
30
+ def extra_child_pid
31
+ File.read(extra_child_pidfile).to_i
32
+ end
33
+
34
+ let(:listener_pidfile) { File.join(File.join(SPEC_TEMPDIR, "listener.pid")) }
35
+ def listener_pid
36
+ File.read(listener_pidfile).to_i
37
+ end
38
+
39
+ before do
40
+ File.unlink(extra_child_pidfile) rescue nil
41
+ File.unlink(listener_pidfile) rescue nil
42
+ @resqued_pid = start_resqued_with_extra_child
43
+ end
44
+
45
+ after do
46
+ kill_safely(:TERM) { @resqued_pid }
47
+ kill_safely(:KILL) { extra_child_pid }
48
+ sleep 0.1
49
+ kill_safely(:KILL) { @resqued_pid }
50
+ end
51
+
52
+ it "doesn't exit when listener dies unexpectedly" do
53
+ # Kill off the listener process.
54
+ first_listener_pid = listener_pid
55
+ Process.kill :QUIT, first_listener_pid
56
+ # Let Resqued::Backoff decide it's OK to start the listener again.
57
+ sleep 2.5
58
+ # Resqued should start a new listener to replace the dead one.
59
+ expect(listener_pid).not_to eq(first_listener_pid)
60
+ end
61
+
62
+ it "exits when listeners have all exited during shutdown" do
63
+ # Do a normal shutdown.
64
+ Process.kill :QUIT, @resqued_pid
65
+ # Expect the resqued process to exit.
66
+ expect(Timeout.timeout(5.0) { Process.waitpid(@resqued_pid) }).to eq(@resqued_pid)
67
+ end
68
+
69
+ it "doesn't crash when extra child exits" do
70
+ # Kill off the extra child process. Resqued should wait on it, but not exit.
71
+ Process.kill :KILL, extra_child_pid
72
+ sleep 1.0
73
+ # The resqued process should not have exited.
74
+ expect(Process.waitpid(@resqued_pid, Process::WNOHANG)).to be_nil
75
+ expect(Process.kill(0, @resqued_pid)).to eq(1)
76
+ end
77
+
78
+ def kill_safely(signal)
79
+ return unless pid = yield
80
+
81
+ Process.kill(signal, pid)
82
+ rescue Errno::ESRCH, Errno::ENOENT
83
+ # Process isn't there anymore, or pidfile isn't there. :+1:
84
+ end
85
+ end
@@ -0,0 +1,63 @@
1
+ require "spec_helper"
2
+
3
+ describe "Resqued can restart" do
4
+ include ResquedPath
5
+
6
+ it "expect to be able to restart" do
7
+ start_resqued
8
+ expect_running listener: "listener #1"
9
+ restart_resqued
10
+ expect_running listener: "listener #2"
11
+ stop_resqued
12
+ expect_not_running
13
+ end
14
+
15
+ after do
16
+ begin
17
+ Process.kill(:QUIT, @pid) if @pid
18
+ rescue Errno::ESRCH
19
+ end
20
+ end
21
+
22
+ def expect_running(listener:)
23
+ processes = list_processes
24
+ expect(processes).to include(is_resqued_master)
25
+ listeners = processes.select { |p| p[:ppid] == @pid }.map { |p| p[:args] }
26
+ expect(listeners).to all(match(/#{listener}/)).and(satisfy { |l| l.size == 1 })
27
+ end
28
+
29
+ def expect_not_running
30
+ processes = list_processes
31
+ expect(processes).not_to include(is_resqued_master)
32
+ end
33
+
34
+ def start_resqued
35
+ # Don't configure any workers. That way, we don't need to have redis running.
36
+ config_path = File.join(SPEC_TEMPDIR, "config.rb")
37
+ File.write(config_path, "")
38
+
39
+ logfile = File.join(SPEC_TEMPDIR, "resqued.log")
40
+ File.write(logfile, "") # truncate it
41
+
42
+ @pid = spawn resqued_path, "--logfile", logfile, config_path
43
+ sleep 1.0
44
+ end
45
+
46
+ def restart_resqued
47
+ Process.kill(:HUP, @pid)
48
+ sleep 1.0
49
+ end
50
+
51
+ def stop_resqued
52
+ Process.kill(:TERM, @pid)
53
+ sleep 1.0
54
+ end
55
+
56
+ def list_processes
57
+ `ps axo pid,ppid,args`.lines.map { |line| pid, ppid, args = line.strip.split(/\s+/, 3); { pid: pid.to_i, ppid: ppid.to_i, args: args } }
58
+ end
59
+
60
+ def is_resqued_master
61
+ satisfy { |p| p[:pid] == @pid && p[:args] =~ /resqued-/ }
62
+ end
63
+ end
@@ -1,61 +1,61 @@
1
- require 'spec_helper'
1
+ require "spec_helper"
2
2
 
3
- require 'resqued/backoff'
3
+ require "resqued/backoff"
4
4
 
5
5
  describe Resqued::Backoff do
6
- let(:backoff) { described_class.new(:min => 0.5, :max => 64.0) }
6
+ let(:backoff) { described_class.new(min: 0.5, max: 64.0) }
7
7
 
8
- it 'can start on the first try' do
9
- expect(backoff.wait?).to be_false
8
+ it "can start on the first try" do
9
+ expect(backoff.wait?).to be_falsey
10
10
  end
11
11
 
12
- it 'has no waiting at first' do
12
+ it "has no waiting at first" do
13
13
  expect(backoff.how_long?).to be_nil
14
14
  end
15
15
 
16
- context 'after expected exits' do
16
+ context "after expected exits" do
17
17
  before { 3.times { backoff.started } }
18
- it { expect(backoff.wait?).to be_true }
18
+ it { expect(backoff.wait?).to be true }
19
19
  it { expect(backoff.how_long?).to be_close_to(0.5) }
20
20
  end
21
21
 
22
- context 'after one quick exit' do
23
- before { 1.times { backoff.started ; backoff.died } }
24
- it { expect(backoff.wait?).to be_true }
22
+ context "after one quick exit" do
23
+ before { 1.times { backoff.started; backoff.died } }
24
+ it { expect(backoff.wait?).to be true }
25
25
  it { expect(backoff.how_long?).to be_close_to(1.0) }
26
26
  end
27
27
 
28
- context 'after two quick starts' do
29
- before { 2.times { backoff.started ; backoff.died } }
30
- it { expect(backoff.wait?).to be_true }
28
+ context "after two quick starts" do
29
+ before { 2.times { backoff.started; backoff.died } }
30
+ it { expect(backoff.wait?).to be true }
31
31
  it { expect(backoff.how_long?).to be_close_to(2.0) }
32
32
  end
33
33
 
34
- context 'after five quick starts' do
35
- before { 6.times { backoff.started ; backoff.died } }
36
- it { expect(backoff.wait?).to be_true }
34
+ context "after five quick starts" do
35
+ before { 6.times { backoff.started; backoff.died } }
36
+ it { expect(backoff.wait?).to be true }
37
37
  it { expect(backoff.how_long?).to be_close_to(32.0) }
38
38
  end
39
39
 
40
- context 'after six quick starts' do
41
- before { 7.times { backoff.started ; backoff.died } }
42
- it { expect(backoff.wait?).to be_true }
40
+ context "after six quick starts" do
41
+ before { 7.times { backoff.started; backoff.died } }
42
+ it { expect(backoff.wait?).to be true }
43
43
  it { expect(backoff.how_long?).to be_close_to(64.0) }
44
44
  end
45
45
 
46
- context 'does not wait longer than 64s' do
47
- before { 8.times { backoff.started ; backoff.died } }
48
- it { expect(backoff.wait?).to be_true }
46
+ context "does not wait longer than 64s" do
47
+ before { 8.times { backoff.started; backoff.died } }
48
+ it { expect(backoff.wait?).to be true }
49
49
  it { expect(backoff.how_long?).to be_close_to(64.0) }
50
- it 'and resets after an expected exit' do
50
+ it "and resets after an expected exit" do
51
51
  backoff.started
52
52
  backoff.started
53
- expect(backoff.wait?).to be_true
53
+ expect(backoff.wait?).to be true
54
54
  expect(backoff.how_long?).to be_close_to(0.5)
55
55
  end
56
56
  end
57
57
 
58
- def be_close_to(x)
59
- be_within(0.005).of(x)
58
+ def be_close_to(number)
59
+ be_within(0.005).of(number)
60
60
  end
61
61
  end