resqued 0.0.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,9 +1,6 @@
1
1
  # resqued - a long-running daemon for resque workers.
2
2
 
3
- [image of a ninja rescuing an ear of corn]
4
-
5
- resqued provides a resque worker that works well with
6
- slow jobs and continuous delivery.
3
+ Resqued is a multi-process daemon that controls and monitors a pool of resque workers. It works well with slow jobs and continuous delivery.
7
4
 
8
5
  ## Installation
9
6
 
@@ -11,7 +8,15 @@ Install by adding resqued to your Gemfile
11
8
 
12
9
  gem 'resqued'
13
10
 
14
- ## Set up
11
+ Run resqued with a config file, like this:
12
+
13
+ resqued config/resqued.rb
14
+
15
+ Or like this to daemonize it:
16
+
17
+ resqued -p tmp/pids/resqued-master.pid -D config/resqued.rb
18
+
19
+ ## Configuring workers
15
20
 
16
21
  Let's say you were running workers like this:
17
22
 
@@ -24,43 +29,97 @@ Let's say you were running workers like this:
24
29
  To run the same fleet of workers with resqued, create a config file
25
30
  `config/resqued.rb` like this:
26
31
 
27
- base = File.expand_path('..', File.dirname(__FILE__))
28
- pidfile File.join(base, 'tmp/pids/resqued-listener.pid')
32
+ 2.times { worker 'high' }
33
+ worker 'slow'
34
+ worker 'medium'
35
+ worker 'medium', 'low'
29
36
 
30
- worker do
31
- workers 2
32
- queue 'high'
33
- end
37
+ Another syntax for workers:
38
+
39
+ worker_pool 5
40
+ queue 'low', '20%'
41
+ queue 'normal', '60%'
42
+ queue '*'
43
+
44
+ This time, you'd end up with something similar to this:
45
+
46
+ rake resque:work QUEUE=low,normal,* &
47
+ rake resque:work QUEUE=normal,* &
48
+ rake resque:work QUEUE=normal,* &
49
+ rake resque:work QUEUE=* &
50
+ rake resque:work QUEUE=* &
51
+
52
+ `worker` and `worker_pool` accept a hash of options that will be passed to `Resqued::Worker`. The supported options are:
53
+
54
+ * `:interval` - The interval to pass to `Resque::Worker#run`.
34
55
 
35
- worker do
36
- queue 'slow'
37
- timeout -1 # never time out
56
+ ## Loading your application
57
+
58
+ An advantage of using resqued (over `rake resque:work`) is that you can load your application just once, before forking all the workers.
59
+
60
+ For a Rails application, you might do this:
61
+
62
+ before_fork do
63
+ require "./config/environment.rb"
64
+ Rails.application.eager_load!
65
+ # `before_fork` runs in the Listener, and it doesn't actually run any application code.
66
+ ActiveRecord::Base.connection.disconnect!
38
67
  end
39
68
 
40
- worker do
41
- queue 'medium'
69
+ after_fork do |resque_worker|
70
+ # Set up a new connection to the database.
71
+ ActiveRecord::Base.establish_connection
72
+ # `resque_worker.reconnect` already happens
42
73
  end
43
74
 
44
- worker do
45
- queues 'medium', 'low'
75
+ ## Customizing Resque::Worker
76
+
77
+ You can configure the Resque worker in the `after_fork` block
78
+
79
+ after_fork do |resque_worker|
80
+ # Do not fork to `perform` jobs.
81
+ resque_worker.cant_fork = true
82
+
83
+ # Wait a loooong time on SIGTERM.
84
+ resque_worker.term_timeout = 1.day
85
+
86
+ resque_worker.run_at_exit_hooks = true
87
+
88
+ Resque.before_first_fork do
89
+ # ...
90
+ end
46
91
  end
47
92
 
48
- Run it like this:
93
+ ## Full config file example
49
94
 
50
- resqued config/resqued.rb
95
+ worker 'high'
96
+ worker 'low', :interval => 30
51
97
 
52
- Or like this to daemonize it:
98
+ worker_pool 5, :interval => 1
99
+ queue 'low', '20%'
100
+ queue 'normal', 4
101
+ queue '*'
53
102
 
54
- resqued -p tmp/pids/resqued-master.pid -D config/resqued.rb
103
+ before_fork do
104
+ require "./config/environment.rb"
105
+ Rails.application.eager_load!
106
+ ActiveRecord::Base.connection.disconnect!
107
+ end
55
108
 
56
- When resqued is running, it has the following processes:
109
+ after_fork do |worker|
110
+ ActiveRecord::Base.establish_connection
111
+ worker.term_timeout = 1.minute
112
+ end
57
113
 
58
- * master - brokers signals to child processes.
59
- * queue reader - retrieves jobs from queues and forks worker processes.
60
- * worker - runs a single job.
114
+ In this example, a Rails application is being set up with 7 workers:
115
+ * high
116
+ * low (interval = 30)
117
+ * low, normal, * (interval = 1)
118
+ * normal, * (interval = 1)
119
+ * normal, * (interval = 1)
120
+ * normal, * (interval = 1)
121
+ * * (interval = 1)
61
122
 
62
- The following signals are handled by the resqued master process:
123
+ ## See also
63
124
 
64
- * HUP - reread config file and gracefully restart all workers.
65
- * INT / TERM - immediately kill all workers and shut down.
66
- * QUIT - graceful shutdown. Waits for workers to finish.
125
+ For information about how resqued works, see the [documentation](docs/).
@@ -4,16 +4,20 @@ module Resqued
4
4
  @time = options.fetch(:time) { Time }
5
5
  @min = options.fetch(:min) { 1.0 }
6
6
  @max = options.fetch(:max) { 16.0 }
7
+ @backoff_duration = @min
7
8
  end
8
9
 
9
10
  # Public: Tell backoff that the thing we might want to back off from just started.
10
11
  def started
11
12
  @last_started_at = now
12
- @backoff_duration = @backoff_duration ? [@backoff_duration * 2.0, @max].min : @min
13
+ @backoff_duration = @min if @last_event == :start
14
+ @last_event = :start
13
15
  end
14
16
 
15
- def finished
16
- @backoff_duration = nil if ok?
17
+ # Public: Tell backoff that the thing unexpectedly died.
18
+ def died
19
+ @backoff_duration = @backoff_duration ? [@backoff_duration * 2.0, @max].min : @min
20
+ @last_event = :died
17
21
  end
18
22
 
19
23
  # Public: Check if we should wait before starting again.
@@ -21,14 +25,9 @@ module Resqued
21
25
  @last_started_at && next_start_at > now
22
26
  end
23
27
 
24
- # Public: Check if we are ok to start (i.e. we don't need to back off).
25
- def ok?
26
- ! wait?
27
- end
28
-
29
- # Public: How much longer until `ok?` will be true?
28
+ # Public: How much longer until `wait?` will be false?
30
29
  def how_long?
31
- ok? ? nil : next_start_at - now
30
+ wait? ? next_start_at - now : nil
32
31
  end
33
32
 
34
33
  private
@@ -1,87 +1,36 @@
1
- module Resqued
2
- class Config
3
- # Public: Build a new config instance from the given file.
4
- def self.load_file(filename)
5
- new.load_file(filename)
6
- end
7
-
8
- # Public: Build a new config instance from the given `config` script.
9
- def self.load_string(config, filename = nil)
10
- new.load_string(config, filename)
11
- end
12
-
13
- # Public: Build a new config instance.
14
- def initialize
15
- @workers = []
16
- end
17
-
18
- # Public: The configured pidfile path, or nil.
19
- attr_reader :pidfile
20
-
21
- # Public: An array of configured workers.
22
- attr_reader :workers
23
-
24
- # Public: Add to this config using the script `config`.
25
- def load_string(config, filename = nil)
26
- DSL.new(self)._apply(config, filename)
27
- self
28
- end
1
+ require 'resqued/config/after_fork'
2
+ require 'resqued/config/before_fork'
3
+ require 'resqued/config/worker'
29
4
 
30
- # Public: Add to this config using the script in the given file.
31
- def load_file(filename)
32
- load_string(File.read(filename), filename)
5
+ module Resqued
6
+ module Config
7
+ # Public: Build a new ConfigFile instance.
8
+ #
9
+ # Resqued::Config is a module because the evaluators say so, so this `new` is a factory for another class.
10
+ def self.new(*args)
11
+ ConfigFile.new(*args)
33
12
  end
34
13
 
35
- # Private.
36
- class DSL
37
- def initialize(config)
38
- @config = config
39
- end
40
-
41
- # Internal.
42
- def _apply(script, filename)
43
- if filename.nil?
44
- instance_eval(script)
45
- else
46
- instance_eval(script, filename)
47
- end
48
- end
49
-
50
- # Public: Set the pidfile path.
51
- def pidfile(path)
52
- raise ArgumentError unless path.is_a?(String)
53
- _set(:pidfile, path)
54
- end
55
-
56
- # Public: Define a worker.
57
- def worker
58
- @current_worker = {:size => 1, :queues => []}
59
- yield
60
- _push(:workers, @current_worker)
61
- @current_worker = nil
62
- end
63
-
64
- # Public: Add queues to a worker
65
- def queues(*queues)
66
- queues = [queues].flatten.map { |q| q.to_s }
67
- @current_worker[:queues] += queues
14
+ # Does the things that the config file says to do.
15
+ class ConfigFile
16
+ def initialize(config_path)
17
+ @path = config_path
18
+ @contents = File.read(@path)
68
19
  end
69
- alias queue queues
70
20
 
71
- # Public: Set the number of workers
72
- def workers(count)
73
- raise ArgumentError unless count.is_a?(Fixnum)
74
- @current_worker[:size] = count
21
+ # Public: Performs the `before_fork` action from the config.
22
+ def before_fork
23
+ Resqued::Config::BeforeFork.new.apply(@contents, @path)
75
24
  end
76
25
 
77
- # Private.
78
- def _set(instance_variable, value)
79
- @config.instance_variable_set("@#{instance_variable}", value)
26
+ # Public: Performs the `after_fork` action from the config.
27
+ def after_fork(worker)
28
+ Resqued::Config::AfterFork.new(:worker => worker).apply(@contents, @path)
80
29
  end
81
30
 
82
- # Private.
83
- def _push(instance_variable, value)
84
- @config.instance_variable_get("@#{instance_variable}").push(value)
31
+ # Public: Builds the workers specified in the config.
32
+ def build_workers
33
+ Resqued::Config::Worker.new(:config => self).apply(@contents, @path)
85
34
  end
86
35
  end
87
36
  end
@@ -0,0 +1,22 @@
1
+ require 'resqued/config/base'
2
+
3
+ module Resqued
4
+ module Config
5
+ # A config handler that executes the `after_fork` block.
6
+ #
7
+ # after_fork do |resque_worker|
8
+ # # Runs in each worker.
9
+ # end
10
+ class AfterFork < Base
11
+ # Public.
12
+ def initialize(options = {})
13
+ @resque_worker = options.fetch(:worker)
14
+ end
15
+
16
+ # DSL: execute the `after_fork` block.
17
+ def after_fork
18
+ yield @resque_worker
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ require 'resqued/config/dsl'
2
+
3
+ module Resqued
4
+ module Config
5
+ # Base class for config handlers.
6
+ class Base
7
+ # Implement the DSL on the config handler itself.
8
+ include Dsl
9
+
10
+ # Public: Apply the configuration in `str`.
11
+ #
12
+ # Currently, this is a simple wrapper around `instance_eval`.
13
+ def apply(str, filename = "INLINE")
14
+ instance_eval(str, filename)
15
+ results
16
+ end
17
+
18
+ private
19
+
20
+ # Private: The results of applying the config.
21
+ def results
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ require 'resqued/config/base'
2
+
3
+ module Resqued
4
+ module Config
5
+ # A config handler that executes the `before_fork` block.
6
+ #
7
+ # before_fork do
8
+ # # Runs once, before forking all the workers.
9
+ # end
10
+ class BeforeFork < Base
11
+ # DSL: Execute the `before_fork` block.
12
+ def before_fork
13
+ yield
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ module Resqued
2
+ module Config
3
+ # Defines the DSL for resqued config files.
4
+ #
5
+ # Each subclass should override parts of the dsl that it cares about.
6
+ module Dsl
7
+ # Public: Define a block to be run once, before forking all the workers.
8
+ def before_fork(&block)
9
+ end
10
+
11
+ # Public: Define a block to be run in each worker.
12
+ def after_fork(&block)
13
+ end
14
+
15
+ # Public: Define a worker that will work on a queue.
16
+ def worker(*queues)
17
+ end
18
+
19
+ # Public: Define a pool of workers that will work '*', or the queues specified by `queue`.
20
+ def worker_pool(count, options = {})
21
+ end
22
+
23
+ # Public: Define the queues worked by members of the worker pool.
24
+ def queue(queue_name, concurrency = nil)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,111 @@
1
+ require 'resqued/config/base'
2
+
3
+ module Resqued
4
+ module Config
5
+ # A config handler that builds workers.
6
+ #
7
+ # No worker processes are spawned by this class.
8
+ class Worker < Base
9
+ # Public.
10
+ def initialize(options = {})
11
+ options = options.dup
12
+ @worker_class = options.delete(:worker_class) || Resqued::Worker
13
+ @worker_options = options
14
+ @workers = []
15
+ end
16
+
17
+ # DSL: Create a worker for the exact queues listed.
18
+ #
19
+ # worker 'one', :interval => 1
20
+ def worker(*queues)
21
+ options = queues.last.is_a?(Hash) ? queues.pop : {}
22
+ queues = ['*'] if queues.empty?
23
+ @workers << @worker_class.new(options.merge(@worker_options).merge(:queues => queues.flatten))
24
+ end
25
+
26
+ # DSL: Set up a pool of workers. Define queues for the members of the pool with `queue`.
27
+ #
28
+ # worker_pool 20, :interval => 1
29
+ def worker_pool(count, options = {})
30
+ @pool_size = count
31
+ @pool_options = options
32
+ @pool_queues = {}
33
+ end
34
+
35
+ # DSL: Define a queue for the worker_pool to work from.
36
+ #
37
+ # queue 'one'
38
+ # queue '*'
39
+ # queue 'two', '10%'
40
+ # queue 'three', 5
41
+ # queue 'four', :percent => 10
42
+ # queue 'five', :count => 5
43
+ def queue(queue_name, concurrency = nil)
44
+ @pool_queues[queue_name] =
45
+ case concurrency
46
+ when Hash
47
+ if percent = concurrency[:percent]
48
+ percent * 0.01
49
+ elsif count = concurrency[:count]
50
+ count
51
+ else
52
+ 1.0
53
+ end
54
+ when nil, ''; 1.0
55
+ when /%$/; concurrency.chomp('%').to_i * 0.01
56
+ else concurrency.to_i
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def results
63
+ build_pool_workers!
64
+ @workers
65
+ end
66
+
67
+ # Internal: Build the pool workers.
68
+ #
69
+ # Build an array of Worker objects with queue lists configured based
70
+ # on the concurrency values established and the total number of workers.
71
+ def build_pool_workers!
72
+ return unless @pool_size
73
+ queues = _fixed_concurrency_queues
74
+ 1.upto(@pool_size) do |worker_num|
75
+ queue_names = queues.
76
+ select { |name, concurrency| concurrency >= worker_num }.
77
+ map { |name, _| name }
78
+ if queue_names.any?
79
+ worker(queue_names, @pool_options)
80
+ else
81
+ worker('*', @pool_options)
82
+ end
83
+ end
84
+ end
85
+
86
+ # Internal: Like @queues but with concrete fixed concurrency values. All
87
+ # percentage based concurrency values are converted to fixnum total number
88
+ # of workers that queue should run on.
89
+ def _fixed_concurrency_queues
90
+ @pool_queues.map { |name, concurrency| [name, _translate_concurrency_value(concurrency)] }
91
+ end
92
+
93
+ # Internal: Convert a queue worker concurrency value to a fixed number of
94
+ # workers. This supports values that are fixed numbers as well as percentage
95
+ # values (between 0.0 and 1.0). The value may also be nil, in which case the
96
+ # maximum worker_processes value is returned.
97
+ def _translate_concurrency_value(value)
98
+ case
99
+ when value.nil?
100
+ @pool_size
101
+ when value.is_a?(Fixnum)
102
+ value < @pool_size ? value : @pool_size
103
+ when value.is_a?(Float) && value >= 0.0 && value <= 1.0
104
+ (@pool_size * value).to_i
105
+ else
106
+ raise TypeError, "Unknown concurrency value: #{value.inspect}"
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -23,6 +23,7 @@ module Resqued
23
23
  exit
24
24
  else
25
25
  # master
26
+ rd.close
26
27
  @master.run(wr)
27
28
  end
28
29
  end
@@ -2,7 +2,6 @@ require 'socket'
2
2
 
3
3
  require 'resqued/config'
4
4
  require 'resqued/logging'
5
- require 'resqued/pidfile'
6
5
  require 'resqued/sleepy'
7
6
  require 'resqued/worker'
8
7
 
@@ -10,10 +9,11 @@ module Resqued
10
9
  # A listener process. Watches resque queues and forks workers.
11
10
  class Listener
12
11
  include Resqued::Logging
13
- include Resqued::Pidfile
14
12
  include Resqued::Sleepy
15
13
 
16
14
  # Configure a new listener object.
15
+ #
16
+ # Runs in the master process.
17
17
  def initialize(options)
18
18
  @config_path = options.fetch(:config_path)
19
19
  @running_workers = options.fetch(:running_workers) { [] }
@@ -22,6 +22,8 @@ module Resqued
22
22
  end
23
23
 
24
24
  # Public: As an alternative to #run, exec a new ruby instance for this listener.
25
+ #
26
+ # Runs in the master process.
25
27
  def exec
26
28
  ENV['RESQUED_SOCKET'] = @socket.fileno.to_s
27
29
  ENV['RESQUED_CONFIG_PATH'] = @config_path
@@ -48,12 +50,7 @@ module Resqued
48
50
  new(options).run
49
51
  end
50
52
 
51
- # Private: memoizes the worker configuration.
52
- def config
53
- @config ||= Config.load_file(@config_path)
54
- end
55
-
56
- SIGNALS = [ :QUIT ]
53
+ SIGNALS = [ :QUIT, :INT, :TERM ]
57
54
 
58
55
  SIGNAL_QUEUE = []
59
56
 
@@ -63,15 +60,15 @@ module Resqued
63
60
  SIGNALS.each { |signal| trap(signal) { SIGNAL_QUEUE << signal ; awake } }
64
61
  @socket.close_on_exec = true
65
62
 
66
- with_pidfile(config.pidfile) do
67
- write_procline('running')
68
- load_environment
69
- init_workers
70
- run_workers_run
71
- end
63
+ config = Resqued::Config.new(@config_path)
64
+ config.before_fork
65
+
66
+ write_procline('running')
67
+ init_workers(config)
68
+ exit_signal = run_workers_run
72
69
 
73
70
  write_procline('shutdown')
74
- burn_down_workers(:QUIT)
71
+ burn_down_workers(exit_signal || :QUIT)
75
72
  end
76
73
 
77
74
  # Private.
@@ -83,8 +80,8 @@ module Resqued
83
80
  case signal = SIGNAL_QUEUE.shift
84
81
  when nil
85
82
  yawn
86
- when :QUIT
87
- return
83
+ when :QUIT, :INT, :TERM
84
+ return signal
88
85
  end
89
86
  end
90
87
  end
@@ -98,9 +95,7 @@ module Resqued
98
95
  SIGNAL_QUEUE.clear
99
96
 
100
97
  break if :no_child == reap_workers(Process::WNOHANG)
101
-
102
- log "kill -#{signal} #{running_workers.map { |r| r.pid }.inspect}"
103
- running_workers.each { |worker| worker.kill(signal) }
98
+ kill_all(signal)
104
99
 
105
100
  sleep 1 # Don't kill any more often than every 1s.
106
101
  yawn 5
@@ -109,6 +104,12 @@ module Resqued
109
104
  reap_workers
110
105
  end
111
106
 
107
+ # Private: send a signal to all the workers.
108
+ def kill_all(signal)
109
+ log "kill -#{signal} #{running_workers.map { |r| r.pid }.inspect}"
110
+ running_workers.each { |worker| worker.kill(signal) }
111
+ end
112
+
112
113
  # Private: all available workers
113
114
  attr_reader :workers
114
115
 
@@ -174,19 +175,13 @@ module Resqued
174
175
  end
175
176
 
176
177
  # Private.
177
- def init_workers
178
- workers = []
179
- config.workers.each do |worker_config|
180
- worker_config[:size].times do
181
- workers << Worker.new(worker_config)
182
- end
183
- end
178
+ def init_workers(config)
179
+ @workers = config.build_workers
184
180
  @running_workers.each do |running_worker|
185
- if blocked_worker = workers.detect { |worker| worker.idle? && worker.queue_key == running_worker[:queue] }
181
+ if blocked_worker = @workers.detect { |worker| worker.idle? && worker.queue_key == running_worker[:queue] }
186
182
  blocked_worker.wait_for(running_worker[:pid].to_i)
187
183
  end
188
184
  end
189
- @workers = workers
190
185
  end
191
186
 
192
187
  # Private: Report child process status.
@@ -197,16 +192,8 @@ module Resqued
197
192
  # report_to_master("-12345") # Worker process PID:12345 exited.
198
193
  def report_to_master(status)
199
194
  @socket.puts(status)
200
- end
201
-
202
- # Private: load the application.
203
- #
204
- # To do:
205
- # * Does this reload correctly if the bundle changes and `bundle exec resqued config/resqued.rb`?
206
- # * Maybe make the specific app environment configurable (i.e. load rails, load rackup, load some custom thing)
207
- def load_environment
208
- require File.expand_path('config/environment.rb')
209
- Rails.application.eager_load!
195
+ rescue Errno::EPIPE
196
+ Process.kill(:QUIT, $$) # If the master is gone, LIFE IS NOW MEANINGLESS.
210
197
  end
211
198
 
212
199
  # Private.
@@ -5,6 +5,7 @@ require 'resqued/listener'
5
5
  require 'resqued/logging'
6
6
 
7
7
  module Resqued
8
+ # Controls a listener process from the master process.
8
9
  class ListenerProxy
9
10
  include Resqued::Logging
10
11
 
@@ -42,7 +43,7 @@ module Resqued
42
43
  else
43
44
  # listener
44
45
  master_socket.close
45
- Master::SIGNALS.each { |signal| trap(signal, 'DEFAULT') }
46
+ Master::TRAPS.each { |signal| trap(signal, 'DEFAULT') rescue nil }
46
47
  Listener.new(@options.merge(:socket => listener_socket)).exec
47
48
  exit
48
49
  end
@@ -81,7 +82,7 @@ module Resqued
81
82
  log "Malformed data from listener: #{line.inspect}"
82
83
  end
83
84
  end
84
- rescue EOFError
85
+ rescue EOFError, Errno::ECONNRESET
85
86
  @master_socket.close
86
87
  @master_socket = nil
87
88
  end
@@ -1,13 +1,40 @@
1
1
  module Resqued
2
+ # Mixin for any class that wants to write messages to the log file.
2
3
  module Logging
3
- # Public.
4
- def self.log_file=(path)
5
- ENV['RESQUED_LOGFILE'] = File.expand_path(path)
6
- end
4
+ # Global logging state.
5
+ class << self
6
+ # Public: Get an IO to write log messages to.
7
+ def logging_io
8
+ @logging_io = nil if @logging_io && @logging_io.closed?
9
+ @logging_io ||=
10
+ if path = Resqued::Logging.log_file
11
+ File.open(path, 'a').tap do |f|
12
+ f.sync = true
13
+ f.close_on_exec = true
14
+ end
15
+ else
16
+ $stdout
17
+ end
18
+ end
7
19
 
8
- # Public.
9
- def self.log_file
10
- ENV['RESQUED_LOGFILE']
20
+ # Public: Make sure the log IO is closed.
21
+ def close_log
22
+ if @logging_io && @logging_io != $stdout
23
+ @logging_io.close
24
+ @logging_io = nil
25
+ end
26
+ end
27
+
28
+ # Public.
29
+ def log_file=(path)
30
+ ENV['RESQUED_LOGFILE'] = File.expand_path(path)
31
+ close_log
32
+ end
33
+
34
+ # Public.
35
+ def log_file
36
+ ENV['RESQUED_LOGFILE']
37
+ end
11
38
  end
12
39
 
13
40
  # Public.
@@ -15,23 +42,14 @@ module Resqued
15
42
  Resqued::Logging.log_file.nil?
16
43
  end
17
44
 
18
- # Private (in classes that include this module)
19
- def log(message)
20
- logging_io.puts "[#{$$} #{Time.now.strftime('%H:%M:%S')}] #{message}"
45
+ # Public: Re-open all log files.
46
+ def reopen_logs
47
+ Resqued::Logging.close_log # it gets opened the next time it's needed.
21
48
  end
22
49
 
23
- # Private (may be overridden in classes that include this module to send output to a different IO)
24
- def logging_io
25
- @logging_io = nil if @logging_io && @logging_io.closed?
26
- @logging_io ||=
27
- if path = Resqued::Logging.log_file
28
- File.open(path, 'a').tap do |f|
29
- f.sync = true
30
- f.close_on_exec = true
31
- end
32
- else
33
- $stdout
34
- end
50
+ # Private (in classes that include this module)
51
+ def log(message)
52
+ Resqued::Logging.logging_io.puts "[#{$$} #{Time.now.strftime('%H:%M:%S')}] #{message}"
35
53
  end
36
54
  end
37
55
  end
@@ -48,6 +48,7 @@ module Resqued
48
48
  when :INFO
49
49
  dump_object_counts
50
50
  when :HUP
51
+ reopen_logs
51
52
  log "Restarting listener with new configuration and application."
52
53
  kill_listener(:QUIT)
53
54
  when :INT, :TERM, :QUIT
@@ -147,7 +148,7 @@ module Resqued
147
148
  if lpid
148
149
  log "Listener exited #{status}"
149
150
  if @current_listener && @current_listener.pid == lpid
150
- @listener_backoff.finished
151
+ @listener_backoff.died
151
152
  @current_listener = nil
152
153
  end
153
154
  listener_pids.delete(lpid).dispose # This may leak workers.
@@ -160,13 +161,17 @@ module Resqued
160
161
  end while true
161
162
  end
162
163
 
163
- SIGNALS = [ :HUP, :INT, :TERM, :QUIT, :INFO ]
164
+ SIGNALS = [ :HUP, :INT, :TERM, :QUIT ]
165
+ OPTIONAL_SIGNALS = [ :INFO ]
166
+ OTHER_SIGNALS = [:CHLD, 'EXIT']
167
+ TRAPS = SIGNALS + OPTIONAL_SIGNALS + OTHER_SIGNALS
164
168
 
165
169
  SIGNAL_QUEUE = []
166
170
 
167
171
  def install_signal_handlers
168
172
  trap(:CHLD) { awake }
169
173
  SIGNALS.each { |signal| trap(signal) { SIGNAL_QUEUE << signal ; awake } }
174
+ OPTIONAL_SIGNALS.each { |signal| trap(signal) { SIGNAL_QUEUE << signal ; awake } rescue nil }
170
175
  end
171
176
 
172
177
  def report_unexpected_exits
@@ -1,5 +1,7 @@
1
1
  module Resqued
2
+ # Mixin that manages a pidfile for a process.
2
3
  module Pidfile
4
+ # Public: Create a pidfile, execute the block, then remove the pidfile.
3
5
  def with_pidfile(filename)
4
6
  write_pidfile(filename) if filename
5
7
  yield
@@ -7,6 +9,7 @@ module Resqued
7
9
  remove_pidfile(filename) if filename
8
10
  end
9
11
 
12
+ # Private.
10
13
  def write_pidfile(filename)
11
14
  pf =
12
15
  begin
@@ -20,6 +23,7 @@ module Resqued
20
23
  pf.close
21
24
  end
22
25
 
26
+ # Private.
23
27
  def remove_pidfile(filename)
24
28
  (File.read(filename).to_i == $$) and File.unlink(filename) rescue nil
25
29
  end
@@ -3,18 +3,21 @@ require 'kgio'
3
3
 
4
4
  module Resqued
5
5
  module Sleepy
6
- def self_pipe
7
- @self_pipe ||= Kgio::Pipe.new.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
8
- end
9
-
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).
10
7
  def yawn(duration, *inputs)
11
8
  inputs = [self_pipe[0]] + [inputs].flatten.compact
12
9
  IO.select(inputs, nil, nil, duration) or return
13
10
  self_pipe[0].kgio_tryread(11)
14
11
  end
15
12
 
13
+ # Public: Break out of `yawn`.
16
14
  def awake
17
15
  self_pipe[1].kgio_trywrite('.')
18
16
  end
17
+
18
+ # Private.
19
+ def self_pipe
20
+ @self_pipe ||= Kgio::Pipe.new.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
21
+ end
19
22
  end
20
23
  end
@@ -1,3 +1,4 @@
1
1
  module Resqued
2
- VERSION = '0.0.1'
2
+ # Oh look, he's getting so big!
3
+ VERSION = '0.4.0'
3
4
  end
@@ -10,6 +10,8 @@ module Resqued
10
10
 
11
11
  def initialize(options)
12
12
  @queues = options.fetch(:queues)
13
+ @config = options.fetch(:config)
14
+ @interval = options[:interval]
13
15
  @backoff = Backoff.new
14
16
  end
15
17
 
@@ -24,7 +26,7 @@ module Resqued
24
26
  pid.nil?
25
27
  end
26
28
 
27
- # Public: Checks if this worker works on jobs from the queue.
29
+ # Public: A string that compares if this worker is equivalent to a worker in another Resqued::Listener.
28
30
  def queue_key
29
31
  queues.sort.join(';')
30
32
  end
@@ -32,14 +34,14 @@ module Resqued
32
34
  # Public: Claim this worker for another listener's worker.
33
35
  def wait_for(pid)
34
36
  raise "Already running #{@pid} (can't wait for #{pid})" if @pid
35
- @self_started = nil
37
+ @self_started = false
36
38
  @pid = pid
37
39
  end
38
40
 
39
41
  # Public: The old worker process finished!
40
42
  def finished!(process_status)
41
43
  @pid = nil
42
- @backoff.finished
44
+ @backoff.died unless @killed
43
45
  end
44
46
 
45
47
  # Public: The amount of time we need to wait before starting a new worker.
@@ -52,14 +54,19 @@ module Resqued
52
54
  return if @backoff.wait?
53
55
  @backoff.started
54
56
  @self_started = true
57
+ @killed = false
55
58
  if @pid = fork
56
59
  # still in the listener
57
60
  else
58
- # In case we get a signal before the process is all the way up.
61
+ # In case we get a signal before resque is ready for it.
59
62
  [:QUIT, :TERM, :INT].each { |signal| trap(signal) { exit 1 } }
60
63
  $0 = "STARTING RESQUE FOR #{queues.join(',')}"
64
+ if Resque.respond_to?("logger")
65
+ Resque.logger.level = Logger::INFO
66
+ Resque.logger.formatter = Resque::VerboseFormatter.new
67
+ end
61
68
  if ! log_to_stdout?
62
- lf = logging_io
69
+ lf = Resqued::Logging.logging_io
63
70
  if Resque.respond_to?("logger=")
64
71
  Resque.logger = Resque.logger.class.new(lf)
65
72
  else
@@ -69,28 +76,18 @@ module Resqued
69
76
  end
70
77
  resque_worker = Resque::Worker.new(*queues)
71
78
  resque_worker.log "Starting worker #{resque_worker}"
72
- resque_worker.term_child = true # Hopefully do away with those warnings!
73
- resque_worker.work(5)
79
+ resque_worker.term_child = true
80
+ resque_worker.reconnect
81
+ @config.after_fork(resque_worker)
82
+ resque_worker.work(@interval || 5)
74
83
  exit 0
75
84
  end
76
85
  end
77
86
 
78
87
  # Public: Shut this worker down.
79
- #
80
- # We are using these signal semantics:
81
- # HUP: restart (QUIT workers)
82
- # INT/TERM: immediately exit
83
- # QUIT: graceful shutdown
84
- #
85
- # Resque uses these (compatible) signal semantics:
86
- # TERM: Shutdown immediately, stop processing jobs.
87
- # INT: Shutdown immediately, stop processing jobs.
88
- # QUIT: Shutdown after the current job has finished processing.
89
- # USR1: Kill the forked child immediately, continue processing jobs.
90
- # USR2: Don't process any new jobs
91
- # CONT: Start processing jobs again after a USR2
92
88
  def kill(signal)
93
89
  Process.kill(signal.to_s, pid) if pid && @self_started
90
+ @killed = true
94
91
  end
95
92
  end
96
93
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resqued
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2013-08-20 00:00:00.000000000 Z
12
+ date: 2013-09-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: kgio
@@ -43,6 +43,22 @@ dependencies:
43
43
  - - ! '>='
44
44
  - !ruby/object:Gem::Version
45
45
  version: 1.22.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: debugger
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
46
62
  - !ruby/object:Gem::Dependency
47
63
  name: rspec
48
64
  requirement: !ruby/object:Gem::Requirement
@@ -132,6 +148,11 @@ extensions: []
132
148
  extra_rdoc_files: []
133
149
  files:
134
150
  - lib/resqued/backoff.rb
151
+ - lib/resqued/config/after_fork.rb
152
+ - lib/resqued/config/base.rb
153
+ - lib/resqued/config/before_fork.rb
154
+ - lib/resqued/config/dsl.rb
155
+ - lib/resqued/config/worker.rb
135
156
  - lib/resqued/config.rb
136
157
  - lib/resqued/daemon.rb
137
158
  - lib/resqued/listener.rb