resqued 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|