resque-pool-vinted 0.4.0.rc1
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.
- checksums.yaml +7 -0
- data/Changelog.md +77 -0
- data/LICENSE.txt +20 -0
- data/README.md +164 -0
- data/Rakefile +30 -0
- data/bin/resque-pool +7 -0
- data/features/basic_daemon_config.feature +68 -0
- data/features/step_definitions/daemon_steps.rb +33 -0
- data/features/step_definitions/resque-pool_steps.rb +159 -0
- data/features/support/aruba_daemon_support.rb +76 -0
- data/features/support/env.rb +1 -0
- data/lib/resque/pool.rb +426 -0
- data/lib/resque/pool/cli.rb +138 -0
- data/lib/resque/pool/logging.rb +65 -0
- data/lib/resque/pool/pooled_worker.rb +21 -0
- data/lib/resque/pool/tasks.rb +20 -0
- data/lib/resque/pool/version.rb +5 -0
- data/man/resque-pool.1 +88 -0
- data/man/resque-pool.1.ronn +92 -0
- data/man/resque-pool.yml.5 +46 -0
- data/man/resque-pool.yml.5.ronn +41 -0
- data/spec/mock_config.rb +6 -0
- data/spec/resque-pool-custom.yml.erb +1 -0
- data/spec/resque-pool.yml +13 -0
- data/spec/resque_pool_spec.rb +202 -0
- data/spec/spec_helper.rb +3 -0
- metadata +195 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'aruba/cucumber'
|
2
|
+
require 'aruba/api'
|
3
|
+
require 'aruba/process'
|
4
|
+
|
5
|
+
module Aruba
|
6
|
+
|
7
|
+
module Api
|
8
|
+
|
9
|
+
# this is a horrible hack, to make sure that it's done what it needs to do
|
10
|
+
# before we do our next step
|
11
|
+
def keep_trying(timeout=10, tries=0)
|
12
|
+
puts "Try: #{tries}" if @announce_env
|
13
|
+
yield
|
14
|
+
rescue RSpec::Expectations::ExpectationNotMetError
|
15
|
+
if tries < timeout
|
16
|
+
sleep 1
|
17
|
+
tries += 1
|
18
|
+
retry
|
19
|
+
else
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def run_background(cmd)
|
25
|
+
@background = run(cmd)
|
26
|
+
end
|
27
|
+
|
28
|
+
def send_signal(cmd, signal)
|
29
|
+
announce_or_puts "$ kill -#{signal} #{processes[cmd].pid}" if @announce_env
|
30
|
+
processes[cmd].send_signal signal
|
31
|
+
end
|
32
|
+
|
33
|
+
def background_pid
|
34
|
+
@pid_from_pidfile || @background.pid
|
35
|
+
end
|
36
|
+
|
37
|
+
# like all_stdout, but doesn't stop processes first
|
38
|
+
def interactive_stdout
|
39
|
+
only_processes.inject("") { |out, ps| out << ps.stdout(@aruba_keep_ansi) }
|
40
|
+
end
|
41
|
+
|
42
|
+
# like all_stderr, but doesn't stop processes first
|
43
|
+
def interactive_stderr
|
44
|
+
only_processes.inject("") { |out, ps| out << ps.stderr(@aruba_keep_ansi) }
|
45
|
+
end
|
46
|
+
|
47
|
+
# like all_output, but doesn't stop processes first
|
48
|
+
def interactive_output
|
49
|
+
interactive_stdout << interactive_stderr
|
50
|
+
end
|
51
|
+
|
52
|
+
def interpolate_background_pid(string)
|
53
|
+
interpolated = string.gsub('$PID', background_pid.to_s)
|
54
|
+
announce_or_puts interpolated if @announce_env
|
55
|
+
interpolated
|
56
|
+
end
|
57
|
+
|
58
|
+
def kill_all_processes!
|
59
|
+
# stop_processes!
|
60
|
+
#rescue
|
61
|
+
# processes.each {|cmd,process| send_signal(cmd, 'KILL') }
|
62
|
+
# raise
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
class Process
|
68
|
+
def pid
|
69
|
+
@process.pid
|
70
|
+
end
|
71
|
+
def send_signal signal
|
72
|
+
@process.send :send_signal, signal
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
ENV["RAILS_ENV"] = "test"
|
data/lib/resque/pool.rb
ADDED
@@ -0,0 +1,426 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'resque'
|
3
|
+
require 'resque/worker'
|
4
|
+
require 'resque/pool/version'
|
5
|
+
require 'resque/pool/logging'
|
6
|
+
require 'resque/pool/pooled_worker'
|
7
|
+
require 'erb'
|
8
|
+
require 'fcntl'
|
9
|
+
require 'yaml'
|
10
|
+
|
11
|
+
module Resque
|
12
|
+
class Pool
|
13
|
+
SIG_QUEUE_MAX_SIZE = 5
|
14
|
+
DEFAULT_WORKER_INTERVAL = 5
|
15
|
+
QUEUE_SIGS = [ :QUIT, :INT, :TERM, :USR1, :USR2, :CONT, :HUP, :WINCH, ]
|
16
|
+
CHUNK_SIZE = (16 * 1024)
|
17
|
+
|
18
|
+
include Logging
|
19
|
+
extend Logging
|
20
|
+
attr_reader :config
|
21
|
+
attr_reader :workers
|
22
|
+
|
23
|
+
def initialize(config)
|
24
|
+
init_config(config)
|
25
|
+
@workers = Hash.new { |workers, queues| workers[queues] = {} }
|
26
|
+
procline "(initialized)"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Config: after_prefork {{{
|
30
|
+
|
31
|
+
# The `after_prefork` hooks will be run in workers if you are using the
|
32
|
+
# preforking master worker to save memory. Use these hooks to reload
|
33
|
+
# database connections and so forth to ensure that they're not shared
|
34
|
+
# among workers.
|
35
|
+
#
|
36
|
+
# Call with a block to set a hook.
|
37
|
+
# Call with no arguments to return all registered hooks.
|
38
|
+
#
|
39
|
+
def self.after_prefork(&block)
|
40
|
+
@after_prefork ||= []
|
41
|
+
block ? (@after_prefork << block) : @after_prefork
|
42
|
+
end
|
43
|
+
|
44
|
+
# Sets the after_prefork proc, clearing all pre-existing hooks.
|
45
|
+
# Warning: you probably don't want to clear out the other hooks.
|
46
|
+
# You can use `Resque::Pool.after_prefork << my_hook` instead.
|
47
|
+
#
|
48
|
+
def self.after_prefork=(after_prefork)
|
49
|
+
@after_prefork = [after_prefork]
|
50
|
+
end
|
51
|
+
|
52
|
+
def call_after_prefork!
|
53
|
+
self.class.after_prefork.each do |hook|
|
54
|
+
hook.call
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# }}}
|
59
|
+
# Config: class methods to start up the pool using the default config {{{
|
60
|
+
|
61
|
+
@config_files = ["resque-pool.yml", "config/resque-pool.yml"]
|
62
|
+
class << self; attr_accessor :config_files, :app_name; end
|
63
|
+
|
64
|
+
def self.app_name
|
65
|
+
@app_name ||= File.basename(Dir.pwd)
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.handle_winch?
|
69
|
+
@handle_winch ||= false
|
70
|
+
end
|
71
|
+
def self.handle_winch=(bool)
|
72
|
+
@handle_winch = bool
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.single_process_group=(bool)
|
76
|
+
ENV["RESQUE_SINGLE_PGRP"] = !!bool ? "YES" : "NO"
|
77
|
+
end
|
78
|
+
def self.single_process_group
|
79
|
+
%w[yes y true t 1 okay sure please].include?(
|
80
|
+
ENV["RESQUE_SINGLE_PGRP"].to_s.downcase
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.choose_config_file
|
85
|
+
if ENV["RESQUE_POOL_CONFIG"]
|
86
|
+
ENV["RESQUE_POOL_CONFIG"]
|
87
|
+
else
|
88
|
+
@config_files.detect { |f| File.exist?(f) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.run
|
93
|
+
if GC.respond_to?(:copy_on_write_friendly=)
|
94
|
+
GC.copy_on_write_friendly = true
|
95
|
+
end
|
96
|
+
Resque::Pool.new(choose_config_file).start.join
|
97
|
+
end
|
98
|
+
|
99
|
+
# }}}
|
100
|
+
# Config: load config and config file {{{
|
101
|
+
|
102
|
+
def config_file
|
103
|
+
@config_file || (!@config && ::Resque::Pool.choose_config_file)
|
104
|
+
end
|
105
|
+
|
106
|
+
def init_config(config)
|
107
|
+
case config
|
108
|
+
when String, nil
|
109
|
+
@config_file = config
|
110
|
+
else
|
111
|
+
@config = config.dup
|
112
|
+
end
|
113
|
+
load_config
|
114
|
+
end
|
115
|
+
|
116
|
+
def load_config
|
117
|
+
if config_file
|
118
|
+
@config = YAML.load(ERB.new(IO.read(config_file)).result)
|
119
|
+
else
|
120
|
+
@config ||= {}
|
121
|
+
end
|
122
|
+
environment and @config[environment] and config.merge!(@config[environment])
|
123
|
+
config.delete_if {|key, value| value.is_a? Hash }
|
124
|
+
end
|
125
|
+
|
126
|
+
def environment
|
127
|
+
if defined? RAILS_ENV
|
128
|
+
RAILS_ENV
|
129
|
+
elsif defined?(Rails) && Rails.respond_to?(:env)
|
130
|
+
Rails.env
|
131
|
+
else
|
132
|
+
ENV['RACK_ENV'] || ENV['RAILS_ENV'] || ENV['RESQUE_ENV']
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# }}}
|
137
|
+
|
138
|
+
# Sig handlers and self pipe management {{{
|
139
|
+
|
140
|
+
def self_pipe; @self_pipe ||= [] end
|
141
|
+
def sig_queue; @sig_queue ||= [] end
|
142
|
+
def term_child; @term_child ||= ENV['TERM_CHILD'] end
|
143
|
+
|
144
|
+
|
145
|
+
def init_self_pipe!
|
146
|
+
self_pipe.each { |io| io.close rescue nil }
|
147
|
+
self_pipe.replace(IO.pipe)
|
148
|
+
self_pipe.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
|
149
|
+
end
|
150
|
+
|
151
|
+
def init_sig_handlers!
|
152
|
+
QUEUE_SIGS.each { |sig| trap_deferred(sig) }
|
153
|
+
trap(:CHLD) { |_| awaken_master }
|
154
|
+
end
|
155
|
+
|
156
|
+
def awaken_master
|
157
|
+
begin
|
158
|
+
self_pipe.last.write_nonblock('.') # wakeup master process from select
|
159
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
160
|
+
# pipe is full, master should wake up anyways
|
161
|
+
retry
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
class QuitNowException < Exception; end
|
166
|
+
# defer a signal for later processing in #join (master process)
|
167
|
+
def trap_deferred(signal)
|
168
|
+
trap(signal) do |sig_nr|
|
169
|
+
if @waiting_for_reaper && [:INT, :TERM].include?(signal)
|
170
|
+
log "Recieved #{signal}: short circuiting QUIT waitpid"
|
171
|
+
raise QuitNowException
|
172
|
+
end
|
173
|
+
if sig_queue.size < SIG_QUEUE_MAX_SIZE
|
174
|
+
sig_queue << signal
|
175
|
+
awaken_master
|
176
|
+
else
|
177
|
+
log "ignoring SIG#{signal}, queue=#{sig_queue.inspect}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def reset_sig_handlers!
|
183
|
+
QUEUE_SIGS.each {|sig| trap(sig, "DEFAULT") }
|
184
|
+
end
|
185
|
+
|
186
|
+
def handle_sig_queue!
|
187
|
+
case signal = sig_queue.shift
|
188
|
+
when :USR1, :USR2, :CONT
|
189
|
+
log "#{signal}: sending to all workers"
|
190
|
+
signal_all_workers(signal)
|
191
|
+
when :HUP
|
192
|
+
log "HUP: reload config file and reload logfiles"
|
193
|
+
load_config
|
194
|
+
Logging.reopen_logs!
|
195
|
+
log "HUP: gracefully shutdown old children (which have old logfiles open)"
|
196
|
+
if term_child
|
197
|
+
signal_all_workers(:TERM)
|
198
|
+
else
|
199
|
+
signal_all_workers(:QUIT)
|
200
|
+
end
|
201
|
+
log "HUP: new children will inherit new logfiles"
|
202
|
+
maintain_worker_count
|
203
|
+
when :WINCH
|
204
|
+
if self.class.handle_winch?
|
205
|
+
log "WINCH: gracefully stopping all workers"
|
206
|
+
@config = {}
|
207
|
+
maintain_worker_count
|
208
|
+
end
|
209
|
+
when :QUIT
|
210
|
+
if term_child
|
211
|
+
shutdown_everything_now!(signal)
|
212
|
+
else
|
213
|
+
graceful_worker_shutdown_and_wait!(signal)
|
214
|
+
end
|
215
|
+
when :INT
|
216
|
+
graceful_worker_shutdown!(signal)
|
217
|
+
when :TERM
|
218
|
+
if term_child
|
219
|
+
graceful_worker_shutdown!(signal)
|
220
|
+
else
|
221
|
+
case self.class.term_behavior
|
222
|
+
when "graceful_worker_shutdown_and_wait"
|
223
|
+
graceful_worker_shutdown_and_wait!(signal)
|
224
|
+
when "graceful_worker_shutdown"
|
225
|
+
graceful_worker_shutdown!(signal)
|
226
|
+
else
|
227
|
+
shutdown_everything_now!(signal)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
class << self
|
234
|
+
attr_accessor :term_behavior
|
235
|
+
end
|
236
|
+
|
237
|
+
def graceful_worker_shutdown_and_wait!(signal)
|
238
|
+
log "#{signal}: graceful shutdown, waiting for children"
|
239
|
+
if term_child
|
240
|
+
signal_all_workers(:TERM)
|
241
|
+
else
|
242
|
+
signal_all_workers(:QUIT)
|
243
|
+
end
|
244
|
+
reap_all_workers(0) # will hang until all workers are shutdown
|
245
|
+
:break
|
246
|
+
end
|
247
|
+
|
248
|
+
def graceful_worker_shutdown!(signal)
|
249
|
+
log "#{signal}: immediate shutdown (graceful worker shutdown)"
|
250
|
+
if term_child
|
251
|
+
signal_all_workers(:TERM)
|
252
|
+
else
|
253
|
+
signal_all_workers(:QUIT)
|
254
|
+
end
|
255
|
+
:break
|
256
|
+
end
|
257
|
+
|
258
|
+
def shutdown_everything_now!(signal)
|
259
|
+
log "#{signal}: immediate shutdown (and immediate worker shutdown)"
|
260
|
+
if term_child
|
261
|
+
signal_all_workers(:QUIT)
|
262
|
+
else
|
263
|
+
signal_all_workers(:TERM)
|
264
|
+
end
|
265
|
+
:break
|
266
|
+
end
|
267
|
+
|
268
|
+
# }}}
|
269
|
+
# start, join, and master sleep {{{
|
270
|
+
|
271
|
+
def start
|
272
|
+
procline("(starting)")
|
273
|
+
init_self_pipe!
|
274
|
+
init_sig_handlers!
|
275
|
+
maintain_worker_count
|
276
|
+
procline("(started)")
|
277
|
+
log "started manager"
|
278
|
+
report_worker_pool_pids
|
279
|
+
self
|
280
|
+
end
|
281
|
+
|
282
|
+
def report_worker_pool_pids
|
283
|
+
if workers.empty?
|
284
|
+
log "Pool is empty"
|
285
|
+
else
|
286
|
+
log "Pool contains worker PIDs: #{all_pids.inspect}"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def join
|
291
|
+
loop do
|
292
|
+
reap_all_workers
|
293
|
+
break if handle_sig_queue! == :break
|
294
|
+
if sig_queue.empty?
|
295
|
+
master_sleep
|
296
|
+
maintain_worker_count
|
297
|
+
end
|
298
|
+
procline("managing #{all_pids.inspect}")
|
299
|
+
end
|
300
|
+
procline("(shutting down)")
|
301
|
+
#stop # gracefully shutdown all workers on our way out
|
302
|
+
log "manager finished"
|
303
|
+
#unlink_pid_safe(pid) if pid
|
304
|
+
end
|
305
|
+
|
306
|
+
def master_sleep
|
307
|
+
begin
|
308
|
+
ready = IO.select([self_pipe.first], nil, nil, 1) or return
|
309
|
+
ready.first && ready.first.first or return
|
310
|
+
loop { self_pipe.first.read_nonblock(CHUNK_SIZE) }
|
311
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# }}}
|
316
|
+
# worker process management {{{
|
317
|
+
|
318
|
+
def reap_all_workers(waitpid_flags=Process::WNOHANG)
|
319
|
+
@waiting_for_reaper = waitpid_flags == 0
|
320
|
+
begin
|
321
|
+
loop do
|
322
|
+
# -1, wait for any child process
|
323
|
+
wpid, status = Process.waitpid2(-1, waitpid_flags)
|
324
|
+
break unless wpid
|
325
|
+
|
326
|
+
if worker = delete_worker(wpid)
|
327
|
+
log "Reaped resque worker[#{status.pid}] (status: #{status.exitstatus}) queues: #{worker.queues.join(",")}"
|
328
|
+
else
|
329
|
+
# this died before it could be killed, so it's not going to have any extra info
|
330
|
+
log "Tried to reap worker [#{status.pid}], but it had already died. (status: #{status.exitstatus})"
|
331
|
+
end
|
332
|
+
end
|
333
|
+
rescue Errno::ECHILD, QuitNowException
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# TODO: close any file descriptors connected to worker, if any
|
338
|
+
def delete_worker(pid)
|
339
|
+
worker = nil
|
340
|
+
workers.detect do |queues, pid_to_worker|
|
341
|
+
worker = pid_to_worker.delete(pid)
|
342
|
+
end
|
343
|
+
worker
|
344
|
+
end
|
345
|
+
|
346
|
+
def all_pids
|
347
|
+
workers.map {|q,workers| workers.keys }.flatten
|
348
|
+
end
|
349
|
+
|
350
|
+
def signal_all_workers(signal)
|
351
|
+
all_pids.each do |pid|
|
352
|
+
Process.kill signal, pid
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# }}}
|
357
|
+
# ???: maintain_worker_count, all_known_queues {{{
|
358
|
+
|
359
|
+
def maintain_worker_count
|
360
|
+
all_known_queues.each do |queues|
|
361
|
+
delta = worker_delta_for(queues)
|
362
|
+
spawn_missing_workers_for(queues) if delta > 0
|
363
|
+
quit_excess_workers_for(queues) if delta < 0
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
def all_known_queues
|
368
|
+
config.keys | workers.keys
|
369
|
+
end
|
370
|
+
|
371
|
+
# }}}
|
372
|
+
# methods that operate on a single grouping of queues {{{
|
373
|
+
# perhaps this means a class is waiting to be extracted
|
374
|
+
|
375
|
+
def spawn_missing_workers_for(queues)
|
376
|
+
worker_delta_for(queues).times do |nr|
|
377
|
+
spawn_worker!(queues)
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def quit_excess_workers_for(queues)
|
382
|
+
delta = -worker_delta_for(queues)
|
383
|
+
pids_for(queues)[0...delta].each do |pid|
|
384
|
+
Process.kill("QUIT", pid)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
def worker_delta_for(queues)
|
389
|
+
config.fetch(queues, 0) - workers.fetch(queues, []).size
|
390
|
+
end
|
391
|
+
|
392
|
+
def pids_for(queues)
|
393
|
+
workers[queues].keys
|
394
|
+
end
|
395
|
+
|
396
|
+
def spawn_worker!(queues)
|
397
|
+
worker = create_worker(queues)
|
398
|
+
pid = fork do
|
399
|
+
Process.setpgrp unless Resque::Pool.single_process_group
|
400
|
+
log_worker "Starting worker #{worker}"
|
401
|
+
call_after_prefork!
|
402
|
+
reset_sig_handlers!
|
403
|
+
#self_pipe.each {|io| io.close }
|
404
|
+
worker.work(ENV['INTERVAL'] || DEFAULT_WORKER_INTERVAL) # interval, will block
|
405
|
+
end
|
406
|
+
workers[queues][pid] = worker
|
407
|
+
end
|
408
|
+
|
409
|
+
def create_worker(queues)
|
410
|
+
queues = queues.to_s.split(',')
|
411
|
+
worker = ::Resque::Worker.new(*queues)
|
412
|
+
worker.term_timeout = ENV['RESQUE_TERM_TIMEOUT'] || 4.0
|
413
|
+
worker.term_child = ENV['TERM_CHILD']
|
414
|
+
if ENV['LOGGING'] || ENV['VERBOSE']
|
415
|
+
worker.verbose = ENV['LOGGING'] || ENV['VERBOSE']
|
416
|
+
end
|
417
|
+
if ENV['VVERBOSE']
|
418
|
+
worker.very_verbose = ENV['VVERBOSE']
|
419
|
+
end
|
420
|
+
worker
|
421
|
+
end
|
422
|
+
|
423
|
+
# }}}
|
424
|
+
|
425
|
+
end
|
426
|
+
end
|