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 +89 -30
- data/lib/resqued/backoff.rb +9 -10
- data/lib/resqued/config.rb +24 -75
- data/lib/resqued/config/after_fork.rb +22 -0
- data/lib/resqued/config/base.rb +25 -0
- data/lib/resqued/config/before_fork.rb +17 -0
- data/lib/resqued/config/dsl.rb +28 -0
- data/lib/resqued/config/worker.rb +111 -0
- data/lib/resqued/daemon.rb +1 -0
- data/lib/resqued/listener.rb +26 -39
- data/lib/resqued/listener_proxy.rb +3 -2
- data/lib/resqued/logging.rb +40 -22
- data/lib/resqued/master.rb +7 -2
- data/lib/resqued/pidfile.rb +4 -0
- data/lib/resqued/sleepy.rb +7 -4
- data/lib/resqued/version.rb +2 -1
- data/lib/resqued/worker.rb +17 -20
- metadata +23 -2
data/README.md
CHANGED
@@ -1,9 +1,6 @@
|
|
1
1
|
# resqued - a long-running daemon for resque workers.
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
-
|
28
|
-
|
32
|
+
2.times { worker 'high' }
|
33
|
+
worker 'slow'
|
34
|
+
worker 'medium'
|
35
|
+
worker 'medium', 'low'
|
29
36
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
93
|
+
## Full config file example
|
49
94
|
|
50
|
-
|
95
|
+
worker 'high'
|
96
|
+
worker 'low', :interval => 30
|
51
97
|
|
52
|
-
|
98
|
+
worker_pool 5, :interval => 1
|
99
|
+
queue 'low', '20%'
|
100
|
+
queue 'normal', 4
|
101
|
+
queue '*'
|
53
102
|
|
54
|
-
|
103
|
+
before_fork do
|
104
|
+
require "./config/environment.rb"
|
105
|
+
Rails.application.eager_load!
|
106
|
+
ActiveRecord::Base.connection.disconnect!
|
107
|
+
end
|
55
108
|
|
56
|
-
|
109
|
+
after_fork do |worker|
|
110
|
+
ActiveRecord::Base.establish_connection
|
111
|
+
worker.term_timeout = 1.minute
|
112
|
+
end
|
57
113
|
|
58
|
-
|
59
|
-
*
|
60
|
-
*
|
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
|
-
|
123
|
+
## See also
|
63
124
|
|
64
|
-
|
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/).
|
data/lib/resqued/backoff.rb
CHANGED
@@ -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 = @
|
13
|
+
@backoff_duration = @min if @last_event == :start
|
14
|
+
@last_event = :start
|
13
15
|
end
|
14
16
|
|
15
|
-
|
16
|
-
|
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:
|
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
|
-
|
30
|
+
wait? ? next_start_at - now : nil
|
32
31
|
end
|
33
32
|
|
34
33
|
private
|
data/lib/resqued/config.rb
CHANGED
@@ -1,87 +1,36 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
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
|
-
#
|
36
|
-
class
|
37
|
-
def initialize(
|
38
|
-
@
|
39
|
-
|
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:
|
72
|
-
def
|
73
|
-
|
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
|
-
#
|
78
|
-
def
|
79
|
-
|
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
|
-
#
|
83
|
-
def
|
84
|
-
|
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
|
data/lib/resqued/daemon.rb
CHANGED
data/lib/resqued/listener.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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::
|
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
|
data/lib/resqued/logging.rb
CHANGED
@@ -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
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
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
|
-
#
|
19
|
-
def
|
20
|
-
|
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 (
|
24
|
-
def
|
25
|
-
|
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
|
data/lib/resqued/master.rb
CHANGED
@@ -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.
|
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
|
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
|
data/lib/resqued/pidfile.rb
CHANGED
@@ -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
|
data/lib/resqued/sleepy.rb
CHANGED
@@ -3,18 +3,21 @@ require 'kgio'
|
|
3
3
|
|
4
4
|
module Resqued
|
5
5
|
module Sleepy
|
6
|
-
|
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
|
data/lib/resqued/version.rb
CHANGED
data/lib/resqued/worker.rb
CHANGED
@@ -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:
|
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 =
|
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.
|
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
|
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
|
73
|
-
resque_worker.
|
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
|
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-
|
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
|