vitobotta-resque-pool 0.3.0.dev
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/Changelog.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +20 -0
- data/README.md +153 -0
- data/Rakefile +17 -0
- data/bin/resque-pool +5 -0
- data/config/alternate.yml +4 -0
- data/config/cucumber.yml +3 -0
- data/examples/Gemfile +1 -0
- data/examples/Gemfile.lock +67 -0
- data/examples/Rakefile +8 -0
- data/examples/chef_cookbook/recipes/default.rb +46 -0
- data/examples/chef_cookbook/templates/default/initd.erb +69 -0
- data/examples/chef_cookbook/templates/default/monitrc.erb +6 -0
- data/examples/rails-resque.rake +22 -0
- data/examples/resque-pool.yml +4 -0
- data/features/basic_daemon_config.feature +68 -0
- data/features/step_definitions/daemon_steps.rb +29 -0
- data/features/step_definitions/resque-pool_steps.rb +147 -0
- data/features/support/aruba_daemon_support.rb +60 -0
- data/features/support/env.rb +1 -0
- data/lib/resque/pool/cli.rb +116 -0
- data/lib/resque/pool/logging.rb +58 -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/lib/resque/pool.rb +331 -0
- data/resque-pool.gemspec +34 -0
- data/spec/mock_config.rb +6 -0
- data/spec/resque-pool.yml +13 -0
- data/spec/resque_pool_spec.rb +134 -0
- data/spec/spec_helper.rb +3 -0
- data/tmp/aruba/Rakefile +1 -0
- metadata +175 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
def process_should_exist(pid)
|
|
2
|
+
lambda { Process.kill(0, pid) }.should_not raise_error(Errno::ESRCH)
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
def process_should_not_exist(pid)
|
|
6
|
+
lambda { Process.kill(0, pid) }.should raise_error(Errno::ESRCH)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def grab_worker_pids(count, str)
|
|
10
|
+
announce "TODO: check output_or_log for #{count} worker started messages"
|
|
11
|
+
pid_regex = (1..count).map { '(\d+)' }.join ', '
|
|
12
|
+
full_regex = /resque-pool-manager\[\d+\]: Pool contains worker PIDs: \[#{pid_regex}\]/m
|
|
13
|
+
str.should =~ full_regex
|
|
14
|
+
@worker_pids = full_regex.match(str).captures.map {|pid| pid.to_i }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def output_or_logfiles_string(report_log)
|
|
18
|
+
case report_log
|
|
19
|
+
when "report", "output"
|
|
20
|
+
"output"
|
|
21
|
+
when "log", "logfiles"
|
|
22
|
+
"logfiles"
|
|
23
|
+
else
|
|
24
|
+
raise ArgumentError
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def output_or_log(report_log)
|
|
29
|
+
case report_log
|
|
30
|
+
when "report", "output"
|
|
31
|
+
all_output
|
|
32
|
+
when "log", "logfiles"
|
|
33
|
+
in_current_dir do
|
|
34
|
+
File.read("log/resque-pool.stdout.log") << File.read("log/resque-pool.stderr.log")
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
raise ArgumentError
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def children_of(ppid)
|
|
42
|
+
ps = `ps -eo ppid,pid,cmd | grep '^ *#{ppid} '`
|
|
43
|
+
ps.split(/\s*\n/).map do |line|
|
|
44
|
+
_, pid, cmd = line.strip.split(/\s+/, 3)
|
|
45
|
+
[pid, cmd]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
When /^I run the pool manager as "([^"]*)"$/ do |cmd|
|
|
50
|
+
@pool_manager_process = run_background(unescape(cmd))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
When /^I send the pool manager the "([^"]*)" signal$/ do |signal|
|
|
54
|
+
Process.kill signal, background_pid
|
|
55
|
+
output_logfiles = @pid_from_pidfile ? "logfiles" : "output"
|
|
56
|
+
case signal
|
|
57
|
+
when "QUIT"
|
|
58
|
+
keep_trying do
|
|
59
|
+
Then "the #{output_logfiles} should contain the following lines (with interpolated $PID):", <<-EOF
|
|
60
|
+
resque-pool-manager[$PID]: QUIT: graceful shutdown, waiting for children
|
|
61
|
+
EOF
|
|
62
|
+
end
|
|
63
|
+
else
|
|
64
|
+
raise ArgumentError
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Then /^the pool manager should record its pid in "([^"]*)"$/ do |pidfile|
|
|
69
|
+
in_current_dir do
|
|
70
|
+
keep_trying do
|
|
71
|
+
File.should be_file(pidfile)
|
|
72
|
+
@pid_from_pidfile = File.read(pidfile).to_i
|
|
73
|
+
@pid_from_pidfile.should_not == 0
|
|
74
|
+
process_should_exist(@pid_from_pidfile)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
Then /^the pool manager should daemonize$/ do
|
|
80
|
+
@pool_manager_process.stop
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
Then /^the pool manager daemon should finish$/ do
|
|
84
|
+
keep_trying do
|
|
85
|
+
process_should_not_exist(@pid_from_pidfile)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# nomenclature: "report" => output to stdout/stderr
|
|
90
|
+
# "log" => output to default logfile
|
|
91
|
+
|
|
92
|
+
Then /^the pool manager should (report|log) that it has started up$/ do |report_log|
|
|
93
|
+
keep_trying do
|
|
94
|
+
Then "the #{output_or_logfiles_string(report_log)} should contain the following lines (with interpolated $PID):", <<-EOF
|
|
95
|
+
resque-pool-manager[$PID]: Resque Pool running in development environment
|
|
96
|
+
resque-pool-manager[$PID]: started manager
|
|
97
|
+
EOF
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
Then /^the pool manager should (report|log) that the pool is empty$/ do |report_log|
|
|
102
|
+
Then "the #{output_or_logfiles_string(report_log)} should contain the following lines (with interpolated $PID):", <<-EOF
|
|
103
|
+
resque-pool-manager[$PID]: Pool is empty
|
|
104
|
+
EOF
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
Then /^the pool manager should (report|log) that (\d+) workers are in the pool$/ do |report_log, count|
|
|
108
|
+
grab_worker_pids Integer(count), output_or_log(report_log)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
Then /^the resque workers should all shutdown$/ do
|
|
112
|
+
@worker_pids.each do |pid|
|
|
113
|
+
keep_trying do
|
|
114
|
+
process_should_not_exist(pid)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
Then "the pool manager should have no child processes" do
|
|
120
|
+
children_of(background_pid).should have(:no).keys
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
Then /^the pool manager should have (\d+) "([^"]*)" worker child processes$/ do |count, queues|
|
|
124
|
+
children_of(background_pid).select do |pid, cmd|
|
|
125
|
+
cmd =~ /^resque-\d+.\d+.\d+: Waiting for #{queues}$/
|
|
126
|
+
end.should have(Integer(count)).members
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
Then "the pool manager should finish" do
|
|
130
|
+
# assuming there will not be multiple processes running
|
|
131
|
+
processes.each { |cmd, p| p.stop }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
Then /^the pool manager should (report|log) that it is finished$/ do |report_log|
|
|
135
|
+
Then "the #{output_or_logfiles_string(report_log)} should contain the following lines (with interpolated $PID):", <<-EOF
|
|
136
|
+
resque-pool-manager[$PID]: manager finished
|
|
137
|
+
EOF
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
Then /^the pool manager should (report|log) that a "([^"]*)" worker has been reaped$/ do |report_log, worker_type|
|
|
141
|
+
And 'the '+ output_or_logfiles_string(report_log) +' should match /Reaped resque worker\[\d+\] \(status: 0\) queues: '+ worker_type + '/'
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
Then /^the logfiles should match \/([^\/]*)\/$/ do |partial_output|
|
|
145
|
+
output_or_log("log").should =~ /#{partial_output}/
|
|
146
|
+
end
|
|
147
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require 'aruba/api'
|
|
2
|
+
require 'aruba/process'
|
|
3
|
+
|
|
4
|
+
module Aruba
|
|
5
|
+
|
|
6
|
+
module Api
|
|
7
|
+
|
|
8
|
+
# this is a horrible hack, to make sure that it's done what it needs to do
|
|
9
|
+
# before we do our next step
|
|
10
|
+
def keep_trying(timeout=10, tries=0)
|
|
11
|
+
announce "Try: #{tries}" if @announce_env
|
|
12
|
+
yield
|
|
13
|
+
rescue RSpec::Expectations::ExpectationNotMetError
|
|
14
|
+
if tries < timeout
|
|
15
|
+
sleep 1
|
|
16
|
+
tries += 1
|
|
17
|
+
retry
|
|
18
|
+
else
|
|
19
|
+
raise
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run_background(cmd)
|
|
24
|
+
@background = run(cmd)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def send_signal(cmd, signal)
|
|
28
|
+
announce_or_puts "$ kill -#{signal} #{processes[cmd].pid}" if @announce_env
|
|
29
|
+
processes[cmd].send_signal signal
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def background_pid
|
|
33
|
+
@pid_from_pidfile || @background.pid
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def interpolate_background_pid(string)
|
|
37
|
+
interpolated = string.gsub('$PID', background_pid.to_s)
|
|
38
|
+
announce_or_puts interpolated if @announce_env
|
|
39
|
+
interpolated
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def kill_all_processes!
|
|
43
|
+
stop_processes!
|
|
44
|
+
rescue
|
|
45
|
+
processes.each {|cmd,process| send_signal(cmd, 'KILL') }
|
|
46
|
+
raise
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class Process
|
|
52
|
+
def pid
|
|
53
|
+
@process.pid
|
|
54
|
+
end
|
|
55
|
+
def send_signal signal
|
|
56
|
+
@process.send :send_signal, signal
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'aruba/cucumber'
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
require 'trollop'
|
|
2
|
+
require 'resque/pool'
|
|
3
|
+
|
|
4
|
+
module Resque
|
|
5
|
+
class Pool
|
|
6
|
+
module CLI
|
|
7
|
+
extend Logging
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def run
|
|
11
|
+
opts = parse_options
|
|
12
|
+
daemonize if opts[:daemon]
|
|
13
|
+
manage_pidfile opts[:pidfile]
|
|
14
|
+
redirect opts
|
|
15
|
+
setup_environment opts
|
|
16
|
+
start_pool
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse_options
|
|
20
|
+
opts = Trollop::options do
|
|
21
|
+
version "resque-pool #{VERSION} (c) nicholas a. evans"
|
|
22
|
+
banner <<-EOS
|
|
23
|
+
resque-pool is the best way to manage a group (pool) of resque workers
|
|
24
|
+
|
|
25
|
+
When daemonized, stdout and stderr default to resque-pool.stdxxx.log files in
|
|
26
|
+
the log directory and pidfile defaults to resque-pool.pid in the current dir.
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
resque-pool [options]
|
|
30
|
+
where [options] are:
|
|
31
|
+
EOS
|
|
32
|
+
opt :config, "Alternate path to config file", :type => String, :short => "-c"
|
|
33
|
+
opt :daemon, "Run as a background daemon", :default => false, :short => "-d"
|
|
34
|
+
opt :stdout, "Redirect stdout to logfile", :type => String, :short => '-o'
|
|
35
|
+
opt :stderr, "Redirect stderr to logfile", :type => String, :short => '-e'
|
|
36
|
+
opt :nosync, "Don't sync logfiles on every write"
|
|
37
|
+
opt :pidfile, "PID file location", :type => String, :short => "-p"
|
|
38
|
+
opt :environment, "Set RAILS_ENV/RACK_ENV/RESQUE_ENV", :type => String, :short => "-E"
|
|
39
|
+
end
|
|
40
|
+
if opts[:daemon]
|
|
41
|
+
opts[:stdout] ||= "log/resque-pool.stdout.log"
|
|
42
|
+
opts[:stderr] ||= "log/resque-pool.stderr.log"
|
|
43
|
+
opts[:pidfile] ||= "tmp/pids/resque-pool.pid"
|
|
44
|
+
end
|
|
45
|
+
opts
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def daemonize
|
|
49
|
+
raise 'First fork failed' if (pid = fork) == -1
|
|
50
|
+
exit unless pid.nil?
|
|
51
|
+
Process.setsid
|
|
52
|
+
raise 'Second fork failed' if (pid = fork) == -1
|
|
53
|
+
exit unless pid.nil?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def manage_pidfile(pidfile)
|
|
57
|
+
return unless pidfile
|
|
58
|
+
pid = Process.pid
|
|
59
|
+
if File.exist? pidfile
|
|
60
|
+
if process_still_running? pidfile
|
|
61
|
+
raise "Pidfile already exists at #{pidfile} and process is still running."
|
|
62
|
+
else
|
|
63
|
+
File.delete pidfile
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
File.open pidfile, "w" do |f|
|
|
67
|
+
f.write pid
|
|
68
|
+
end
|
|
69
|
+
at_exit do
|
|
70
|
+
if Process.pid == pid
|
|
71
|
+
File.delete pidfile
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def process_still_running?(pidfile)
|
|
77
|
+
old_pid = open(pidfile).read.strip.to_i
|
|
78
|
+
Process.kill 0, old_pid
|
|
79
|
+
true
|
|
80
|
+
rescue Errno::ESRCH
|
|
81
|
+
false
|
|
82
|
+
rescue Errno::EPERM
|
|
83
|
+
true
|
|
84
|
+
rescue ::Exception
|
|
85
|
+
$stderr.puts "While checking if PID #{old_pid} is running, unexpected #{e.class}: #{e}"
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def redirect(opts)
|
|
90
|
+
$stdin.reopen '/dev/null' if opts[:daemon]
|
|
91
|
+
# need to reopen as File, or else Resque::Pool::Logging.reopen_logs! won't work
|
|
92
|
+
out = File.new(opts[:stdout], "a") if opts[:stdout] && !opts[:stdout].empty?
|
|
93
|
+
err = File.new(opts[:stderr], "a") if opts[:stderr] && !opts[:stderr].empty?
|
|
94
|
+
$stdout.reopen out if out
|
|
95
|
+
$stderr.reopen err if err
|
|
96
|
+
$stdout.sync = $stderr.sync = true unless opts[:nosync]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def setup_environment(opts)
|
|
100
|
+
ENV["RACK_ENV"] = ENV["RAILS_ENV"] = ENV["RESQUE_ENV"] = opts[:environment] if opts[:environment]
|
|
101
|
+
log "Resque Pool running in #{ENV["RAILS_ENV"] || "development"} environment"
|
|
102
|
+
ENV["RESQUE_POOL_CONFIG"] = opts[:config] if opts[:config]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def start_pool
|
|
106
|
+
require 'rake'
|
|
107
|
+
require 'resque/pool/tasks'
|
|
108
|
+
Rake.application.init
|
|
109
|
+
Rake.application.load_rakefile
|
|
110
|
+
Rake.application["resque:pool"].invoke
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Resque
|
|
2
|
+
class Pool
|
|
3
|
+
module Logging
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
# more than a little bit complicated...
|
|
7
|
+
# copied this from Unicorn.
|
|
8
|
+
def self.reopen_logs!
|
|
9
|
+
log "Flushing logs"
|
|
10
|
+
[$stdout, $stderr].each do |fd|
|
|
11
|
+
if fd.instance_of? File
|
|
12
|
+
# skip if the file is the exact same inode and device
|
|
13
|
+
orig_st = fd.stat
|
|
14
|
+
begin
|
|
15
|
+
cur_st = File.stat(fd.path)
|
|
16
|
+
next if orig_st.ino == cur_st.ino && orig_st.dev == cur_st.dev
|
|
17
|
+
rescue Errno::ENOENT
|
|
18
|
+
end
|
|
19
|
+
# match up the encoding
|
|
20
|
+
open_arg = 'a'
|
|
21
|
+
if fd.respond_to?(:external_encoding) && enc = fd.external_encoding
|
|
22
|
+
open_arg << ":#{enc.to_s}"
|
|
23
|
+
enc = fd.internal_encoding and open_arg << ":#{enc.to_s}"
|
|
24
|
+
end
|
|
25
|
+
# match up buffering (does reopen reset this?)
|
|
26
|
+
sync = fd.sync
|
|
27
|
+
# sync to disk
|
|
28
|
+
fd.fsync
|
|
29
|
+
# reopen, and set ruby buffering appropriately
|
|
30
|
+
fd.reopen fd.path, open_arg
|
|
31
|
+
fd.sync = sync
|
|
32
|
+
log "Reopened logfile: #{fd.path}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Given a string, sets the procline ($0)
|
|
38
|
+
# Procline is always in the format of:
|
|
39
|
+
# resque-pool-master: STRING
|
|
40
|
+
def procline(string)
|
|
41
|
+
$0 = "resque-pool-master: #{string}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# TODO: make this use an actual logger
|
|
45
|
+
def log(message)
|
|
46
|
+
puts "resque-pool-manager[#{Process.pid}]: #{message}"
|
|
47
|
+
#$stdout.fsync
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# TODO: make this use an actual logger
|
|
51
|
+
def log_worker(message)
|
|
52
|
+
puts "resque-pool-worker[#{Process.pid}]: #{message}"
|
|
53
|
+
#$stdout.fsync
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class Resque::Pool
|
|
2
|
+
|
|
3
|
+
class PooledWorker < ::Resque::Worker
|
|
4
|
+
|
|
5
|
+
def initialize(*args)
|
|
6
|
+
@pool_master_pid = Process.pid
|
|
7
|
+
super
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def pool_master_has_gone_away?
|
|
11
|
+
@pool_master_pid && @pool_master_pid != Process.ppid
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# override +shutdown?+ method
|
|
15
|
+
def shutdown?
|
|
16
|
+
super || pool_master_has_gone_away?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
require 'resque/tasks'
|
|
3
|
+
require 'resque/pool'
|
|
4
|
+
|
|
5
|
+
namespace :resque do
|
|
6
|
+
|
|
7
|
+
# resque worker config (not pool related). e.g. hoptoad, rails environment
|
|
8
|
+
task :setup
|
|
9
|
+
|
|
10
|
+
namespace :pool do
|
|
11
|
+
# resque pool config. e.g. after_prefork connection handling
|
|
12
|
+
task :setup
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "Launch a pool of resque workers"
|
|
16
|
+
task :pool => %w[resque:setup resque:pool:setup] do
|
|
17
|
+
Resque::Pool.run
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|