resqued 0.0.1

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/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: []