resqued 0.0.1 → 0.4.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.
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