resqued 0.0.1
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 +66 -0
- data/exe/resqued +49 -0
- data/exe/resqued-listener +4 -0
- data/lib/resqued/backoff.rb +46 -0
- data/lib/resqued/config.rb +88 -0
- data/lib/resqued/daemon.rb +30 -0
- data/lib/resqued/listener.rb +221 -0
- data/lib/resqued/listener_proxy.rb +98 -0
- data/lib/resqued/logging.rb +37 -0
- data/lib/resqued/master.rb +195 -0
- data/lib/resqued/pidfile.rb +27 -0
- data/lib/resqued/sleepy.rb +20 -0
- data/lib/resqued/version.rb +3 -0
- data/lib/resqued/worker.rb +96 -0
- data/lib/resqued.rb +2 -0
- metadata +173 -0
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# resqued - a long-running daemon for resque workers.
|
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.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Install by adding resqued to your Gemfile
|
11
|
+
|
12
|
+
gem 'resqued'
|
13
|
+
|
14
|
+
## Set up
|
15
|
+
|
16
|
+
Let's say you were running workers like this:
|
17
|
+
|
18
|
+
rake resque:work QUEUE=high &
|
19
|
+
rake resque:work QUEUE=high &
|
20
|
+
rake resque:work QUEUE=slow &
|
21
|
+
rake resque:work QUEUE=medium &
|
22
|
+
rake resque:work QUEUE=medium,low &
|
23
|
+
|
24
|
+
To run the same fleet of workers with resqued, create a config file
|
25
|
+
`config/resqued.rb` like this:
|
26
|
+
|
27
|
+
base = File.expand_path('..', File.dirname(__FILE__))
|
28
|
+
pidfile File.join(base, 'tmp/pids/resqued-listener.pid')
|
29
|
+
|
30
|
+
worker do
|
31
|
+
workers 2
|
32
|
+
queue 'high'
|
33
|
+
end
|
34
|
+
|
35
|
+
worker do
|
36
|
+
queue 'slow'
|
37
|
+
timeout -1 # never time out
|
38
|
+
end
|
39
|
+
|
40
|
+
worker do
|
41
|
+
queue 'medium'
|
42
|
+
end
|
43
|
+
|
44
|
+
worker do
|
45
|
+
queues 'medium', 'low'
|
46
|
+
end
|
47
|
+
|
48
|
+
Run it like this:
|
49
|
+
|
50
|
+
resqued config/resqued.rb
|
51
|
+
|
52
|
+
Or like this to daemonize it:
|
53
|
+
|
54
|
+
resqued -p tmp/pids/resqued-master.pid -D config/resqued.rb
|
55
|
+
|
56
|
+
When resqued is running, it has the following processes:
|
57
|
+
|
58
|
+
* master - brokers signals to child processes.
|
59
|
+
* queue reader - retrieves jobs from queues and forks worker processes.
|
60
|
+
* worker - runs a single job.
|
61
|
+
|
62
|
+
The following signals are handled by the resqued master process:
|
63
|
+
|
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.
|
data/exe/resqued
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
options = {}
|
6
|
+
daemonize = false
|
7
|
+
|
8
|
+
opts = OptionParser.new do |opts|
|
9
|
+
opts.banner = "Usage: resqued [options] resqued-config-file"
|
10
|
+
|
11
|
+
opts.on '-h', '--help', 'Show this message' do
|
12
|
+
puts opts
|
13
|
+
exit
|
14
|
+
end
|
15
|
+
|
16
|
+
opts.on '-v', '--version', 'Show the version' do
|
17
|
+
require 'resqued/version'
|
18
|
+
puts Resqued::VERSION
|
19
|
+
exit
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on '-p', '--pidfile PIDFILE', 'Store the pid of the master process in PIDFILE' do |v|
|
23
|
+
options[:master_pidfile] = v
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on '-l', '--logfile LOGFILE', 'Write output to LOGFILE instead of stdout' do |v|
|
27
|
+
require 'resqued/logging'
|
28
|
+
Resqued::Logging.log_file = v
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on '-D', '--daemonize', 'Run daemonized in the background' do
|
32
|
+
daemonize = true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.parse!
|
37
|
+
|
38
|
+
unless options[:config_path] = ARGV[0]
|
39
|
+
puts opts
|
40
|
+
exit 1
|
41
|
+
end
|
42
|
+
|
43
|
+
require 'resqued/master'
|
44
|
+
resqued = Resqued::Master.new(options)
|
45
|
+
if daemonize
|
46
|
+
require 'resqued/daemon'
|
47
|
+
resqued = Resqued::Daemon.new(resqued)
|
48
|
+
end
|
49
|
+
resqued.run
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Resqued
|
2
|
+
class Backoff
|
3
|
+
def initialize(options = {})
|
4
|
+
@time = options.fetch(:time) { Time }
|
5
|
+
@min = options.fetch(:min) { 1.0 }
|
6
|
+
@max = options.fetch(:max) { 16.0 }
|
7
|
+
end
|
8
|
+
|
9
|
+
# Public: Tell backoff that the thing we might want to back off from just started.
|
10
|
+
def started
|
11
|
+
@last_started_at = now
|
12
|
+
@backoff_duration = @backoff_duration ? [@backoff_duration * 2.0, @max].min : @min
|
13
|
+
end
|
14
|
+
|
15
|
+
def finished
|
16
|
+
@backoff_duration = nil if ok?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Public: Check if we should wait before starting again.
|
20
|
+
def wait?
|
21
|
+
@last_started_at && next_start_at > now
|
22
|
+
end
|
23
|
+
|
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?
|
30
|
+
def how_long?
|
31
|
+
ok? ? nil : next_start_at - now
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Private: The next time when you're allowed to start a process.
|
37
|
+
def next_start_at
|
38
|
+
@last_started_at && @backoff_duration ? @last_started_at + @backoff_duration : now
|
39
|
+
end
|
40
|
+
|
41
|
+
# Private.
|
42
|
+
def now
|
43
|
+
@time.now
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,88 @@
|
|
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
|
29
|
+
|
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)
|
33
|
+
end
|
34
|
+
|
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
|
68
|
+
end
|
69
|
+
alias queue queues
|
70
|
+
|
71
|
+
# Public: Set the number of workers
|
72
|
+
def workers(count)
|
73
|
+
raise ArgumentError unless count.is_a?(Fixnum)
|
74
|
+
@current_worker[:size] = count
|
75
|
+
end
|
76
|
+
|
77
|
+
# Private.
|
78
|
+
def _set(instance_variable, value)
|
79
|
+
@config.instance_variable_set("@#{instance_variable}", value)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Private.
|
83
|
+
def _push(instance_variable, value)
|
84
|
+
@config.instance_variable_get("@#{instance_variable}").push(value)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Resqued
|
2
|
+
class Daemon
|
3
|
+
def initialize(master)
|
4
|
+
@master = master
|
5
|
+
end
|
6
|
+
|
7
|
+
# Public: daemonize and run the master process.
|
8
|
+
def run
|
9
|
+
rd, wr = IO.pipe
|
10
|
+
if fork
|
11
|
+
# grandparent
|
12
|
+
wr.close
|
13
|
+
begin
|
14
|
+
master_pid = rd.readpartial(16).to_i
|
15
|
+
exit
|
16
|
+
rescue EOFError
|
17
|
+
puts "Master process failed to start!"
|
18
|
+
exit! 1
|
19
|
+
end
|
20
|
+
elsif fork
|
21
|
+
# parent
|
22
|
+
Process.setsid
|
23
|
+
exit
|
24
|
+
else
|
25
|
+
# master
|
26
|
+
@master.run(wr)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
require 'resqued/config'
|
4
|
+
require 'resqued/logging'
|
5
|
+
require 'resqued/pidfile'
|
6
|
+
require 'resqued/sleepy'
|
7
|
+
require 'resqued/worker'
|
8
|
+
|
9
|
+
module Resqued
|
10
|
+
# A listener process. Watches resque queues and forks workers.
|
11
|
+
class Listener
|
12
|
+
include Resqued::Logging
|
13
|
+
include Resqued::Pidfile
|
14
|
+
include Resqued::Sleepy
|
15
|
+
|
16
|
+
# Configure a new listener object.
|
17
|
+
def initialize(options)
|
18
|
+
@config_path = options.fetch(:config_path)
|
19
|
+
@running_workers = options.fetch(:running_workers) { [] }
|
20
|
+
@socket = options.fetch(:socket)
|
21
|
+
@listener_id = options.fetch(:listener_id) { nil }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public: As an alternative to #run, exec a new ruby instance for this listener.
|
25
|
+
def exec
|
26
|
+
ENV['RESQUED_SOCKET'] = @socket.fileno.to_s
|
27
|
+
ENV['RESQUED_CONFIG_PATH'] = @config_path
|
28
|
+
ENV['RESQUED_STATE'] = (@running_workers.map { |r| "#{r[:pid]}|#{r[:queue]}" }.join('||'))
|
29
|
+
ENV['RESQUED_LISTENER_ID'] = @listener_id.to_s
|
30
|
+
Kernel.exec('resqued-listener')
|
31
|
+
end
|
32
|
+
|
33
|
+
# Public: Given args from #exec, start this listener.
|
34
|
+
def self.exec!
|
35
|
+
options = {}
|
36
|
+
if socket = ENV['RESQUED_SOCKET']
|
37
|
+
options[:socket] = Socket.for_fd(socket.to_i)
|
38
|
+
end
|
39
|
+
if path = ENV['RESQUED_CONFIG_PATH']
|
40
|
+
options[:config_path] = path
|
41
|
+
end
|
42
|
+
if state = ENV['RESQUED_STATE']
|
43
|
+
options[:running_workers] = state.split('||').map { |s| Hash[[:pid,:queue].zip(s.split('|'))] }
|
44
|
+
end
|
45
|
+
if listener_id = ENV['RESQUED_LISTENER_ID']
|
46
|
+
options[:listener_id] = listener_id
|
47
|
+
end
|
48
|
+
new(options).run
|
49
|
+
end
|
50
|
+
|
51
|
+
# Private: memoizes the worker configuration.
|
52
|
+
def config
|
53
|
+
@config ||= Config.load_file(@config_path)
|
54
|
+
end
|
55
|
+
|
56
|
+
SIGNALS = [ :QUIT ]
|
57
|
+
|
58
|
+
SIGNAL_QUEUE = []
|
59
|
+
|
60
|
+
# Public: Run the main loop.
|
61
|
+
def run
|
62
|
+
trap(:CHLD) { awake }
|
63
|
+
SIGNALS.each { |signal| trap(signal) { SIGNAL_QUEUE << signal ; awake } }
|
64
|
+
@socket.close_on_exec = true
|
65
|
+
|
66
|
+
with_pidfile(config.pidfile) do
|
67
|
+
write_procline('running')
|
68
|
+
load_environment
|
69
|
+
init_workers
|
70
|
+
run_workers_run
|
71
|
+
end
|
72
|
+
|
73
|
+
write_procline('shutdown')
|
74
|
+
burn_down_workers(:QUIT)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Private.
|
78
|
+
def run_workers_run
|
79
|
+
loop do
|
80
|
+
reap_workers(Process::WNOHANG)
|
81
|
+
check_for_expired_workers
|
82
|
+
start_idle_workers
|
83
|
+
case signal = SIGNAL_QUEUE.shift
|
84
|
+
when nil
|
85
|
+
yawn
|
86
|
+
when :QUIT
|
87
|
+
return
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Private: make sure all the workers stop.
|
93
|
+
#
|
94
|
+
# Resque workers have gaps in their signal-handling ability.
|
95
|
+
def burn_down_workers(signal)
|
96
|
+
loop do
|
97
|
+
check_for_expired_workers
|
98
|
+
SIGNAL_QUEUE.clear
|
99
|
+
|
100
|
+
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) }
|
104
|
+
|
105
|
+
sleep 1 # Don't kill any more often than every 1s.
|
106
|
+
yawn 5
|
107
|
+
end
|
108
|
+
# One last time.
|
109
|
+
reap_workers
|
110
|
+
end
|
111
|
+
|
112
|
+
# Private: all available workers
|
113
|
+
attr_reader :workers
|
114
|
+
|
115
|
+
# Private: just the running workers.
|
116
|
+
def running_workers
|
117
|
+
workers.select { |worker| ! worker.idle? }
|
118
|
+
end
|
119
|
+
|
120
|
+
# Private.
|
121
|
+
def yawn(sleep_time = nil)
|
122
|
+
sleep_time ||=
|
123
|
+
begin
|
124
|
+
sleep_times = [60.0] + workers.map { |worker| worker.backing_off_for }
|
125
|
+
[sleep_times.compact.min, 0.0].max
|
126
|
+
end
|
127
|
+
super(sleep_time, @socket)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Private: Check for workers that have stopped running
|
131
|
+
def reap_workers(waitpidflags = 0)
|
132
|
+
loop do
|
133
|
+
worker_pid, status = Process.waitpid2(-1, waitpidflags)
|
134
|
+
return :none_ready if worker_pid.nil?
|
135
|
+
finish_worker(worker_pid, status)
|
136
|
+
report_to_master("-#{worker_pid}")
|
137
|
+
end
|
138
|
+
rescue Errno::ECHILD
|
139
|
+
# All done
|
140
|
+
:no_child
|
141
|
+
end
|
142
|
+
|
143
|
+
# Private: Check if master reports any dead workers.
|
144
|
+
def check_for_expired_workers
|
145
|
+
loop do
|
146
|
+
IO.select([@socket], nil, nil, 0) or return
|
147
|
+
line = @socket.readline
|
148
|
+
finish_worker(line.to_i, nil)
|
149
|
+
end
|
150
|
+
rescue EOFError
|
151
|
+
log "eof from master"
|
152
|
+
Process.kill(:QUIT, $$)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Private.
|
156
|
+
def finish_worker(worker_pid, status)
|
157
|
+
workers.each do |worker|
|
158
|
+
if worker.pid == worker_pid
|
159
|
+
worker.finished!(status)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Private.
|
165
|
+
def start_idle_workers
|
166
|
+
workers.each do |worker|
|
167
|
+
if worker.idle?
|
168
|
+
worker.try_start
|
169
|
+
if pid = worker.pid
|
170
|
+
report_to_master("+#{pid},#{worker.queue_key}")
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# 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
|
184
|
+
@running_workers.each do |running_worker|
|
185
|
+
if blocked_worker = workers.detect { |worker| worker.idle? && worker.queue_key == running_worker[:queue] }
|
186
|
+
blocked_worker.wait_for(running_worker[:pid].to_i)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
@workers = workers
|
190
|
+
end
|
191
|
+
|
192
|
+
# Private: Report child process status.
|
193
|
+
#
|
194
|
+
# Examples:
|
195
|
+
#
|
196
|
+
# report_to_master("+12345,queue") # Worker process PID:12345 started, working on a job from "queue".
|
197
|
+
# report_to_master("-12345") # Worker process PID:12345 exited.
|
198
|
+
def report_to_master(status)
|
199
|
+
@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!
|
210
|
+
end
|
211
|
+
|
212
|
+
# Private.
|
213
|
+
def write_procline(status)
|
214
|
+
procline = "resqued listener"
|
215
|
+
procline << " #{@listener_id}" if @listener_id
|
216
|
+
procline << " [#{status}]"
|
217
|
+
procline << " #{@config_path}"
|
218
|
+
$0 = procline
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'fcntl'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
require 'resqued/listener'
|
5
|
+
require 'resqued/logging'
|
6
|
+
|
7
|
+
module Resqued
|
8
|
+
class ListenerProxy
|
9
|
+
include Resqued::Logging
|
10
|
+
|
11
|
+
# Public.
|
12
|
+
def initialize(options)
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: wrap up all the things, this object is going home.
|
17
|
+
def dispose
|
18
|
+
if @master_socket
|
19
|
+
@master_socket.close
|
20
|
+
@master_socket = nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public: An IO to select on to check if there is incoming data available.
|
25
|
+
def read_pipe
|
26
|
+
@master_socket
|
27
|
+
end
|
28
|
+
|
29
|
+
# Public: The pid of the running listener process.
|
30
|
+
attr_reader :pid
|
31
|
+
|
32
|
+
# Public: Start the listener process.
|
33
|
+
def run
|
34
|
+
return if pid
|
35
|
+
listener_socket, master_socket = UNIXSocket.pair
|
36
|
+
if @pid = fork
|
37
|
+
# master
|
38
|
+
listener_socket.close
|
39
|
+
master_socket.close_on_exec = true
|
40
|
+
log "Started listener #{@pid}"
|
41
|
+
@master_socket = master_socket
|
42
|
+
else
|
43
|
+
# listener
|
44
|
+
master_socket.close
|
45
|
+
Master::SIGNALS.each { |signal| trap(signal, 'DEFAULT') }
|
46
|
+
Listener.new(@options.merge(:socket => listener_socket)).exec
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Public: Stop the listener process.
|
52
|
+
def kill(signal)
|
53
|
+
log "kill -#{signal} #{pid}"
|
54
|
+
Process.kill(signal.to_s, pid)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Public: Get the list of workers running from this listener.
|
58
|
+
def running_workers
|
59
|
+
worker_pids.map { |pid, queue| { :pid => pid, :queue => queue } }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Private: Map worker pids to queue names
|
63
|
+
def worker_pids
|
64
|
+
@worker_pids ||= {}
|
65
|
+
end
|
66
|
+
|
67
|
+
# Public: Check for updates on running worker information.
|
68
|
+
def read_worker_status(options)
|
69
|
+
on_finished = options[:on_finished]
|
70
|
+
until @master_socket.nil?
|
71
|
+
IO.select([@master_socket], nil, nil, 0) or return
|
72
|
+
line = @master_socket.readline
|
73
|
+
if line =~ /^\+(\d+),(.*)$/
|
74
|
+
worker_pids[$1] = $2
|
75
|
+
elsif line =~ /^-(\d+)$/
|
76
|
+
worker_pids.delete($1)
|
77
|
+
on_finished.worker_finished($1) if on_finished
|
78
|
+
elsif line == ''
|
79
|
+
break
|
80
|
+
else
|
81
|
+
log "Malformed data from listener: #{line.inspect}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
rescue EOFError
|
85
|
+
@master_socket.close
|
86
|
+
@master_socket = nil
|
87
|
+
end
|
88
|
+
|
89
|
+
# Public: Tell the listener process that a worker finished.
|
90
|
+
def worker_finished(pid)
|
91
|
+
return if @master_socket.nil?
|
92
|
+
@master_socket.puts(pid)
|
93
|
+
rescue Errno::EPIPE
|
94
|
+
@master_socket.close
|
95
|
+
@master_socket = nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Resqued
|
2
|
+
module Logging
|
3
|
+
# Public.
|
4
|
+
def self.log_file=(path)
|
5
|
+
ENV['RESQUED_LOGFILE'] = File.expand_path(path)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Public.
|
9
|
+
def self.log_file
|
10
|
+
ENV['RESQUED_LOGFILE']
|
11
|
+
end
|
12
|
+
|
13
|
+
# Public.
|
14
|
+
def log_to_stdout?
|
15
|
+
Resqued::Logging.log_file.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
# Private (in classes that include this module)
|
19
|
+
def log(message)
|
20
|
+
logging_io.puts "[#{$$} #{Time.now.strftime('%H:%M:%S')}] #{message}"
|
21
|
+
end
|
22
|
+
|
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
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'resqued/backoff'
|
2
|
+
require 'resqued/listener_proxy'
|
3
|
+
require 'resqued/logging'
|
4
|
+
require 'resqued/pidfile'
|
5
|
+
require 'resqued/sleepy'
|
6
|
+
|
7
|
+
module Resqued
|
8
|
+
# The master process.
|
9
|
+
# * Spawns a listener.
|
10
|
+
# * Tracks all work. (IO pipe from listener.)
|
11
|
+
# * Handles signals.
|
12
|
+
class Master
|
13
|
+
include Resqued::Logging
|
14
|
+
include Resqued::Pidfile
|
15
|
+
include Resqued::Sleepy
|
16
|
+
|
17
|
+
def initialize(options)
|
18
|
+
@config_path = options.fetch(:config_path)
|
19
|
+
@pidfile = options.fetch(:master_pidfile) { nil }
|
20
|
+
@listener_backoff = Backoff.new
|
21
|
+
@listeners_created = 0
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public: Starts the master process.
|
25
|
+
def run(ready_pipe = nil)
|
26
|
+
report_unexpected_exits
|
27
|
+
with_pidfile(@pidfile) do
|
28
|
+
write_procline
|
29
|
+
install_signal_handlers
|
30
|
+
if ready_pipe
|
31
|
+
ready_pipe.syswrite($$.to_s)
|
32
|
+
ready_pipe.close rescue nil
|
33
|
+
end
|
34
|
+
go_ham
|
35
|
+
end
|
36
|
+
no_more_unexpected_exits
|
37
|
+
end
|
38
|
+
|
39
|
+
# Private: dat main loop.
|
40
|
+
def go_ham
|
41
|
+
loop do
|
42
|
+
read_listeners
|
43
|
+
reap_all_listeners(Process::WNOHANG)
|
44
|
+
start_listener
|
45
|
+
case signal = SIGNAL_QUEUE.shift
|
46
|
+
when nil
|
47
|
+
yawn(@listener_backoff.how_long? || 30.0)
|
48
|
+
when :INFO
|
49
|
+
dump_object_counts
|
50
|
+
when :HUP
|
51
|
+
log "Restarting listener with new configuration and application."
|
52
|
+
kill_listener(:QUIT)
|
53
|
+
when :INT, :TERM, :QUIT
|
54
|
+
log "Shutting down..."
|
55
|
+
kill_all_listeners(signal)
|
56
|
+
wait_for_workers
|
57
|
+
break
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Private.
|
63
|
+
def dump_object_counts
|
64
|
+
log GC.stat.inspect
|
65
|
+
counts = {}
|
66
|
+
total = 0
|
67
|
+
ObjectSpace.each_object do |o|
|
68
|
+
count = counts[o.class.name] || 0
|
69
|
+
counts[o.class.name] = count + 1
|
70
|
+
total += 1
|
71
|
+
end
|
72
|
+
top = 10
|
73
|
+
log "#{total} objects. top #{top}:"
|
74
|
+
counts.sort_by { |name, count| count }.reverse.each_with_index do |(name, count), i|
|
75
|
+
if i < top
|
76
|
+
diff = ""
|
77
|
+
if last = @last_counts && @last_counts[name]
|
78
|
+
diff = " (#{'%+d' % (count - last)})"
|
79
|
+
end
|
80
|
+
log " #{count} #{name}#{diff}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
@last_counts = counts
|
84
|
+
log GC.stat.inspect
|
85
|
+
rescue => e
|
86
|
+
log "Error while counting objects: #{e}"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Private: Map listener pids to ListenerProxy objects.
|
90
|
+
def listener_pids
|
91
|
+
@listener_pids ||= {}
|
92
|
+
end
|
93
|
+
|
94
|
+
# Private: All the ListenerProxy objects.
|
95
|
+
def all_listeners
|
96
|
+
listener_pids.values
|
97
|
+
end
|
98
|
+
|
99
|
+
attr_reader :config_path
|
100
|
+
attr_reader :pidfile
|
101
|
+
|
102
|
+
def start_listener
|
103
|
+
return if @current_listener || @listener_backoff.wait?
|
104
|
+
@current_listener = ListenerProxy.new(:config_path => @config_path, :running_workers => all_listeners.map { |l| l.running_workers }.flatten, :listener_id => next_listener_id)
|
105
|
+
@current_listener.run
|
106
|
+
@listener_backoff.started
|
107
|
+
listener_pids[@current_listener.pid] = @current_listener
|
108
|
+
write_procline
|
109
|
+
end
|
110
|
+
|
111
|
+
def next_listener_id
|
112
|
+
@listeners_created += 1
|
113
|
+
end
|
114
|
+
|
115
|
+
def read_listeners
|
116
|
+
all_listeners.each do |l|
|
117
|
+
l.read_worker_status(:on_finished => self)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def worker_finished(pid)
|
122
|
+
all_listeners.each do |other|
|
123
|
+
other.worker_finished(pid)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def kill_listener(signal)
|
128
|
+
if @current_listener
|
129
|
+
@current_listener.kill(signal)
|
130
|
+
@current_listener = nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def kill_all_listeners(signal)
|
135
|
+
all_listeners.each do |l|
|
136
|
+
l.kill(signal)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def wait_for_workers
|
141
|
+
reap_all_listeners
|
142
|
+
end
|
143
|
+
|
144
|
+
def reap_all_listeners(waitpid_flags = 0)
|
145
|
+
begin
|
146
|
+
lpid, status = Process.waitpid2(-1, waitpid_flags)
|
147
|
+
if lpid
|
148
|
+
log "Listener exited #{status}"
|
149
|
+
if @current_listener && @current_listener.pid == lpid
|
150
|
+
@listener_backoff.finished
|
151
|
+
@current_listener = nil
|
152
|
+
end
|
153
|
+
listener_pids.delete(lpid).dispose # This may leak workers.
|
154
|
+
write_procline
|
155
|
+
else
|
156
|
+
return
|
157
|
+
end
|
158
|
+
rescue Errno::ECHILD
|
159
|
+
return
|
160
|
+
end while true
|
161
|
+
end
|
162
|
+
|
163
|
+
SIGNALS = [ :HUP, :INT, :TERM, :QUIT, :INFO ]
|
164
|
+
|
165
|
+
SIGNAL_QUEUE = []
|
166
|
+
|
167
|
+
def install_signal_handlers
|
168
|
+
trap(:CHLD) { awake }
|
169
|
+
SIGNALS.each { |signal| trap(signal) { SIGNAL_QUEUE << signal ; awake } }
|
170
|
+
end
|
171
|
+
|
172
|
+
def report_unexpected_exits
|
173
|
+
trap('EXIT') do
|
174
|
+
log("EXIT #{$!.inspect}")
|
175
|
+
if $!
|
176
|
+
$!.backtrace.each do |line|
|
177
|
+
log(line)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def no_more_unexpected_exits
|
184
|
+
trap('EXIT', nil)
|
185
|
+
end
|
186
|
+
|
187
|
+
def yawn(duration)
|
188
|
+
super(duration, all_listeners.map { |l| l.read_pipe })
|
189
|
+
end
|
190
|
+
|
191
|
+
def write_procline
|
192
|
+
$0 = "resqued master [gen #{@listeners_created}] [#{listener_pids.size} running] #{ARGV.join(' ')}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Resqued
|
2
|
+
module Pidfile
|
3
|
+
def with_pidfile(filename)
|
4
|
+
write_pidfile(filename) if filename
|
5
|
+
yield
|
6
|
+
ensure
|
7
|
+
remove_pidfile(filename) if filename
|
8
|
+
end
|
9
|
+
|
10
|
+
def write_pidfile(filename)
|
11
|
+
pf =
|
12
|
+
begin
|
13
|
+
tmp = "#{filename}.#{rand}.#{$$}"
|
14
|
+
File.open(tmp, File::RDWR | File::CREAT | File::EXCL, 0644)
|
15
|
+
rescue Errno::EEXIST
|
16
|
+
retry
|
17
|
+
end
|
18
|
+
pf.syswrite("#{$$}\n")
|
19
|
+
File.rename(pf.path, filename)
|
20
|
+
pf.close
|
21
|
+
end
|
22
|
+
|
23
|
+
def remove_pidfile(filename)
|
24
|
+
(File.read(filename).to_i == $$) and File.unlink(filename) rescue nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'fcntl'
|
2
|
+
require 'kgio'
|
3
|
+
|
4
|
+
module Resqued
|
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
|
+
|
10
|
+
def yawn(duration, *inputs)
|
11
|
+
inputs = [self_pipe[0]] + [inputs].flatten.compact
|
12
|
+
IO.select(inputs, nil, nil, duration) or return
|
13
|
+
self_pipe[0].kgio_tryread(11)
|
14
|
+
end
|
15
|
+
|
16
|
+
def awake
|
17
|
+
self_pipe[1].kgio_trywrite('.')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'resque'
|
2
|
+
|
3
|
+
require 'resqued/backoff'
|
4
|
+
require 'resqued/logging'
|
5
|
+
|
6
|
+
module Resqued
|
7
|
+
# Models a worker process.
|
8
|
+
class Worker
|
9
|
+
include Resqued::Logging
|
10
|
+
|
11
|
+
def initialize(options)
|
12
|
+
@queues = options.fetch(:queues)
|
13
|
+
@backoff = Backoff.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: The pid of the worker process.
|
17
|
+
attr_reader :pid
|
18
|
+
|
19
|
+
# Private.
|
20
|
+
attr_reader :queues
|
21
|
+
|
22
|
+
# Public: True if there is no worker process mapped to this object.
|
23
|
+
def idle?
|
24
|
+
pid.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: Checks if this worker works on jobs from the queue.
|
28
|
+
def queue_key
|
29
|
+
queues.sort.join(';')
|
30
|
+
end
|
31
|
+
|
32
|
+
# Public: Claim this worker for another listener's worker.
|
33
|
+
def wait_for(pid)
|
34
|
+
raise "Already running #{@pid} (can't wait for #{pid})" if @pid
|
35
|
+
@self_started = nil
|
36
|
+
@pid = pid
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public: The old worker process finished!
|
40
|
+
def finished!(process_status)
|
41
|
+
@pid = nil
|
42
|
+
@backoff.finished
|
43
|
+
end
|
44
|
+
|
45
|
+
# Public: The amount of time we need to wait before starting a new worker.
|
46
|
+
def backing_off_for
|
47
|
+
@pid ? nil : @backoff.how_long?
|
48
|
+
end
|
49
|
+
|
50
|
+
# Public: Start a job, if there's one waiting in one of my queues.
|
51
|
+
def try_start
|
52
|
+
return if @backoff.wait?
|
53
|
+
@backoff.started
|
54
|
+
@self_started = true
|
55
|
+
if @pid = fork
|
56
|
+
# still in the listener
|
57
|
+
else
|
58
|
+
# In case we get a signal before the process is all the way up.
|
59
|
+
[:QUIT, :TERM, :INT].each { |signal| trap(signal) { exit 1 } }
|
60
|
+
$0 = "STARTING RESQUE FOR #{queues.join(',')}"
|
61
|
+
if ! log_to_stdout?
|
62
|
+
lf = logging_io
|
63
|
+
if Resque.respond_to?("logger=")
|
64
|
+
Resque.logger = Resque.logger.class.new(lf)
|
65
|
+
else
|
66
|
+
$stdout.reopen(lf)
|
67
|
+
lf.close
|
68
|
+
end
|
69
|
+
end
|
70
|
+
resque_worker = Resque::Worker.new(*queues)
|
71
|
+
resque_worker.log "Starting worker #{resque_worker}"
|
72
|
+
resque_worker.term_child = true # Hopefully do away with those warnings!
|
73
|
+
resque_worker.work(5)
|
74
|
+
exit 0
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# 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
|
+
def kill(signal)
|
93
|
+
Process.kill(signal.to_s, pid) if pid && @self_started
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/resqued.rb
ADDED
metadata
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: resqued
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Matt Burke
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-08-20 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: kgio
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.6'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.6'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: resque
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.22.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.22.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.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: '2.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rake
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.9.0
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.9.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: guard-rspec
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 2.4.1
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 2.4.1
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: guard-bundler
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 1.0.0
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 1.0.0
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rb-fsevent
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: Daemon of resque workers
|
127
|
+
email: spraints@gmail.com
|
128
|
+
executables:
|
129
|
+
- resqued
|
130
|
+
- resqued-listener
|
131
|
+
extensions: []
|
132
|
+
extra_rdoc_files: []
|
133
|
+
files:
|
134
|
+
- lib/resqued/backoff.rb
|
135
|
+
- lib/resqued/config.rb
|
136
|
+
- lib/resqued/daemon.rb
|
137
|
+
- lib/resqued/listener.rb
|
138
|
+
- lib/resqued/listener_proxy.rb
|
139
|
+
- lib/resqued/logging.rb
|
140
|
+
- lib/resqued/master.rb
|
141
|
+
- lib/resqued/pidfile.rb
|
142
|
+
- lib/resqued/sleepy.rb
|
143
|
+
- lib/resqued/version.rb
|
144
|
+
- lib/resqued/worker.rb
|
145
|
+
- lib/resqued.rb
|
146
|
+
- README.md
|
147
|
+
- exe/resqued
|
148
|
+
- exe/resqued-listener
|
149
|
+
homepage: https://github.com
|
150
|
+
licenses: []
|
151
|
+
post_install_message:
|
152
|
+
rdoc_options: []
|
153
|
+
require_paths:
|
154
|
+
- lib
|
155
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
156
|
+
none: false
|
157
|
+
requirements:
|
158
|
+
- - ! '>='
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '0'
|
161
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
162
|
+
none: false
|
163
|
+
requirements:
|
164
|
+
- - ! '>='
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
requirements: []
|
168
|
+
rubyforge_project:
|
169
|
+
rubygems_version: 1.8.23
|
170
|
+
signing_key:
|
171
|
+
specification_version: 3
|
172
|
+
summary: Daemon of resque workers
|
173
|
+
test_files: []
|