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.
@@ -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
@@ -0,0 +1,5 @@
1
+ module Resque
2
+ class Pool
3
+ VERSION = "0.3.0.dev"
4
+ end
5
+ end