resqued 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'resqued/listener'
4
+ Resqued::Listener.exec!
@@ -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,3 @@
1
+ module Resqued
2
+ VERSION = '0.0.1'
3
+ 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
@@ -0,0 +1,2 @@
1
+ require 'resqued/master'
2
+ require 'resqued/version'
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: []