nfo-resque-mongo 1.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/HISTORY.md +259 -0
  2. data/LICENSE +20 -0
  3. data/README.markdown +828 -0
  4. data/Rakefile +73 -0
  5. data/bin/resque +75 -0
  6. data/bin/resque-web +23 -0
  7. data/lib/resque/errors.rb +10 -0
  8. data/lib/resque/failure/base.rb +74 -0
  9. data/lib/resque/failure/hoptoad.rb +139 -0
  10. data/lib/resque/failure/mongo.rb +92 -0
  11. data/lib/resque/failure/multiple.rb +60 -0
  12. data/lib/resque/failure.rb +82 -0
  13. data/lib/resque/helpers.rb +79 -0
  14. data/lib/resque/job.rb +228 -0
  15. data/lib/resque/plugin.rb +51 -0
  16. data/lib/resque/queue_stats.rb +58 -0
  17. data/lib/resque/server/public/idle.png +0 -0
  18. data/lib/resque/server/public/jquery-1.3.2.min.js +19 -0
  19. data/lib/resque/server/public/jquery.relatize_date.js +95 -0
  20. data/lib/resque/server/public/poll.png +0 -0
  21. data/lib/resque/server/public/ranger.js +73 -0
  22. data/lib/resque/server/public/reset.css +48 -0
  23. data/lib/resque/server/public/style.css +86 -0
  24. data/lib/resque/server/public/working.png +0 -0
  25. data/lib/resque/server/test_helper.rb +19 -0
  26. data/lib/resque/server/views/error.erb +1 -0
  27. data/lib/resque/server/views/failed.erb +75 -0
  28. data/lib/resque/server/views/key_sets.erb +19 -0
  29. data/lib/resque/server/views/key_string.erb +11 -0
  30. data/lib/resque/server/views/layout.erb +38 -0
  31. data/lib/resque/server/views/next_more.erb +19 -0
  32. data/lib/resque/server/views/overview.erb +4 -0
  33. data/lib/resque/server/views/queues.erb +49 -0
  34. data/lib/resque/server/views/stats.erb +62 -0
  35. data/lib/resque/server/views/workers.erb +109 -0
  36. data/lib/resque/server/views/working.erb +68 -0
  37. data/lib/resque/server.rb +222 -0
  38. data/lib/resque/stat.rb +55 -0
  39. data/lib/resque/tasks.rb +42 -0
  40. data/lib/resque/version.rb +3 -0
  41. data/lib/resque/worker.rb +524 -0
  42. data/lib/resque.rb +384 -0
  43. data/tasks/redis.rake +161 -0
  44. data/tasks/resque.rake +2 -0
  45. data/test/dump.rdb +0 -0
  46. data/test/job_hooks_test.rb +323 -0
  47. data/test/job_plugins_test.rb +230 -0
  48. data/test/plugin_test.rb +116 -0
  49. data/test/queue_stats_test.rb +57 -0
  50. data/test/redis-test.conf +115 -0
  51. data/test/resque-web_test.rb +48 -0
  52. data/test/resque_test.rb +256 -0
  53. data/test/test_helper.rb +151 -0
  54. data/test/worker_test.rb +356 -0
  55. metadata +166 -0
@@ -0,0 +1,222 @@
1
+ require 'sinatra/base'
2
+ require 'erb'
3
+ require 'resque'
4
+ require 'resque/version'
5
+
6
+ module Resque
7
+ class Server < Sinatra::Base
8
+ dir = File.dirname(File.expand_path(__FILE__))
9
+
10
+ set :views, "#{dir}/server/views"
11
+ set :public, "#{dir}/server/public"
12
+ set :static, true
13
+
14
+ helpers do
15
+ include Rack::Utils
16
+ alias_method :h, :escape_html
17
+
18
+ def current_section
19
+ url_path request.path_info.sub('/','').split('/')[0].downcase
20
+ end
21
+
22
+ def current_page
23
+ url_path request.path_info.sub('/','')
24
+ end
25
+
26
+ def url_path(*path_parts)
27
+ [ path_prefix, path_parts ].join("/").squeeze('/')
28
+ end
29
+ alias_method :u, :url_path
30
+
31
+ def path_prefix
32
+ request.env['SCRIPT_NAME']
33
+ end
34
+
35
+ def class_if_current(path = '')
36
+ 'class="current"' if current_page[0, path.size] == path
37
+ end
38
+
39
+ def tab(name)
40
+ dname = name.to_s.downcase
41
+ path = url_path(dname)
42
+ "<li #{class_if_current(path)}><a href='#{path}'>#{name}</a></li>"
43
+ end
44
+
45
+ def tabs
46
+ Resque::Server.tabs
47
+ end
48
+
49
+ def redis_get_size(key)
50
+ case Resque.redis.type(key)
51
+ when 'none'
52
+ []
53
+ when 'list'
54
+ Resque.redis.llen(key)
55
+ when 'set'
56
+ Resque.redis.scard(key)
57
+ when 'string'
58
+ Resque.redis.get(key).length
59
+ when 'zset'
60
+ Resque.redis.zcard(key)
61
+ end
62
+ end
63
+
64
+ def redis_get_value_as_array(key, start=0)
65
+ case Resque.redis.type(key)
66
+ when 'none'
67
+ []
68
+ when 'list'
69
+ Resque.redis.lrange(key, start, start + 20)
70
+ when 'set'
71
+ Resque.redis.smembers(key)[start..(start + 20)]
72
+ when 'string'
73
+ [Resque.redis.get(key)]
74
+ when 'zset'
75
+ Resque.redis.zrange(key, start, start + 20)
76
+ end
77
+ end
78
+
79
+ def show_args(args)
80
+ Array(args).map { |a| a.inspect }.join("\n")
81
+ end
82
+
83
+ def worker_hosts
84
+ @worker_hosts ||= worker_hosts!
85
+ end
86
+
87
+ def worker_hosts!
88
+ hosts = Hash.new { [] }
89
+
90
+ Resque.workers.each do |worker|
91
+ host, _ = worker.to_s.split(':')
92
+ hosts[host] += [worker.to_s]
93
+ end
94
+
95
+ hosts
96
+ end
97
+
98
+ def partial?
99
+ @partial
100
+ end
101
+
102
+ def partial(template, local_vars = {})
103
+ @partial = true
104
+ erb(template.to_sym, {:layout => false}, local_vars)
105
+ ensure
106
+ @partial = false
107
+ end
108
+
109
+ def poll
110
+ if @polling
111
+ text = "Last Updated: #{Time.now.strftime("%H:%M:%S")}"
112
+ else
113
+ text = "<a href='#{u(request.path_info)}.poll' rel='poll'>Live Poll</a>"
114
+ end
115
+ "<p class='poll'>#{text}</p>"
116
+ end
117
+
118
+ end
119
+
120
+ def show(page, layout = true)
121
+ begin
122
+ erb page.to_sym, {:layout => layout}, :resque => Resque
123
+ rescue Errno::ECONNREFUSED
124
+ erb :error, {:layout => false}, :error => "Can't connect to Mongo!"
125
+ end
126
+ end
127
+
128
+ # to make things easier on ourselves
129
+ get "/?" do
130
+ redirect url_path(:overview)
131
+ end
132
+
133
+ %w( overview queues working workers key ).each do |page|
134
+ get "/#{page}" do
135
+ show page
136
+ end
137
+
138
+ get "/#{page}/:id" do
139
+ show page
140
+ end
141
+ end
142
+
143
+ post "/queues/:id/remove" do
144
+ Resque.remove_queue(params[:id])
145
+ redirect u('queues')
146
+ end
147
+
148
+ %w( overview workers ).each do |page|
149
+ get "/#{page}.poll" do
150
+ content_type "text/html"
151
+ @polling = true
152
+ show(page.to_sym, false).gsub(/\s{1,}/, ' ')
153
+ end
154
+ end
155
+
156
+ get "/failed" do
157
+ if Resque::Failure.url
158
+ redirect Resque::Failure.url
159
+ else
160
+ show :failed
161
+ end
162
+ end
163
+
164
+ post "/failed/clear" do
165
+ Resque::Failure.clear
166
+ redirect u('failed')
167
+ end
168
+
169
+ get "/failed/requeue/:index" do
170
+ Resque::Failure.requeue(params[:index])
171
+ if request.xhr?
172
+ return Resque::Failure.all(params[:index])['retried_at']
173
+ else
174
+ redirect u('failed')
175
+ end
176
+ end
177
+
178
+ get "/failed/remove/:index" do
179
+ Resque::Failure.remove(params[:index])
180
+ redirect u('failed')
181
+ end
182
+
183
+ get "/stats" do
184
+ redirect url_path("/stats/resque")
185
+ end
186
+
187
+ get "/stats/:id" do
188
+ show :stats
189
+ end
190
+
191
+ get "/stats/keys/:key" do
192
+ show :stats
193
+ end
194
+
195
+ get "/stats.txt" do
196
+ info = Resque.info
197
+
198
+ stats = []
199
+ stats << "resque.servers=#{info[:servers].inspect}"
200
+ stats << "resque.pending=#{info[:pending]}"
201
+ stats << "resque.processed+=#{info[:processed]}"
202
+ stats << "resque.failed+=#{info[:failed]}"
203
+ stats << "resque.workers=#{info[:workers]}"
204
+ stats << "resque.working=#{info[:working]}"
205
+
206
+ Resque.queues.each do |queue|
207
+ stats << "queues.#{queue}=#{Resque.size(queue)}"
208
+ end
209
+
210
+ content_type 'text/html'
211
+ stats.join "\n"
212
+ end
213
+
214
+ def resque
215
+ Resque
216
+ end
217
+
218
+ def self.tabs
219
+ @tabs ||= ["Overview", "Working", "Failed", "Queues", "Workers", "Stats"]
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,55 @@
1
+ module Resque
2
+ # The stat subsystem. Used to keep track of integer counts.
3
+ #
4
+ # Get a stat: Stat[name]
5
+ # Incr a stat: Stat.incr(name)
6
+ # Decr a stat: Stat.decr(name)
7
+ # Kill a stat: Stat.clear(name)
8
+ module Stat
9
+ extend self
10
+ extend Helpers
11
+
12
+ # Returns the int value of a stat, given a string stat name.
13
+ def get(stat)
14
+ res = mongo_stats.find_one(:stat => stat)
15
+ return 0 unless res
16
+ res['value'].to_i
17
+ end
18
+
19
+ # Alias of `get`
20
+ def [](stat)
21
+ get(stat)
22
+ end
23
+
24
+ # For a string stat name, increments the stat by one.
25
+ #
26
+ # Can optionally accept a second int parameter. The stat is then
27
+ # incremented by that amount.
28
+ def incr(stat, by = 1)
29
+ mongo_stats.update({:stat => stat}, {'$inc' => {:value => by}}, :upsert => true)
30
+ end
31
+
32
+ # Increments a stat by one.
33
+ def <<(stat)
34
+ incr stat
35
+ end
36
+
37
+ # For a string stat name, decrements the stat by one.
38
+ #
39
+ # Can optionally accept a second int parameter. The stat is then
40
+ # decremented by that amount.
41
+ def decr(stat, by = 1)
42
+ mongo_stats.update({:stat => stat}, {'$inc' => {:value => -by}})
43
+ end
44
+
45
+ # Decrements a stat by one.
46
+ def >>(stat)
47
+ decr stat
48
+ end
49
+
50
+ # Removes a stat from Redis, effectively setting it to 0.
51
+ def clear(stat)
52
+ mongo_stats.remove(:stat => stat)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,42 @@
1
+ # require 'resque/tasks'
2
+ # will give you the resque tasks
3
+
4
+ namespace :resque do
5
+ task :setup
6
+
7
+ desc "Start a Resque worker"
8
+ task :work => :setup do
9
+ require 'resque'
10
+
11
+ queues = (ENV['QUEUES'] || ENV['QUEUE']).to_s.split(',')
12
+
13
+ begin
14
+ worker = Resque::Worker.new(*queues)
15
+ worker.verbose = ENV['LOGGING'] || ENV['VERBOSE']
16
+ worker.very_verbose = ENV['VVERBOSE']
17
+ rescue Resque::NoQueueError
18
+ abort "set QUEUE env var, e.g. $ QUEUE=critical,high rake resque:work"
19
+ end
20
+
21
+ if ENV['PIDFILE']
22
+ File.open(ENV['PIDFILE'], 'w') { |f| f << worker.pid }
23
+ end
24
+
25
+ worker.log "Starting worker #{worker}"
26
+
27
+ worker.work(ENV['INTERVAL'] || 5) # interval, will block
28
+ end
29
+
30
+ desc "Start multiple Resque workers. Should only be used in dev mode."
31
+ task :workers do
32
+ threads = []
33
+
34
+ ENV['COUNT'].to_i.times do
35
+ threads << Thread.new do
36
+ system "rake resque:work"
37
+ end
38
+ end
39
+
40
+ threads.each { |thread| thread.join }
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module Resque
2
+ Version = VERSION = '1.15.0'
3
+ end
@@ -0,0 +1,524 @@
1
+ module Resque
2
+ # A Resque Worker processes jobs. On platforms that support fork(2),
3
+ # the worker will fork off a child to process each job. This ensures
4
+ # a clean slate when beginning the next job and cuts down on gradual
5
+ # memory growth as well as low level failures.
6
+ #
7
+ # It also ensures workers are always listening to signals from you,
8
+ # their master, and can react accordingly.
9
+ class Worker
10
+ include Resque::Helpers
11
+ extend Resque::Helpers
12
+
13
+ # Whether the worker should log basic info to STDOUT
14
+ attr_accessor :verbose
15
+
16
+ # Whether the worker should log lots of info to STDOUT
17
+ attr_accessor :very_verbose
18
+
19
+ # Boolean indicating whether this worker can or can not fork.
20
+ # Automatically set if a fork(2) fails.
21
+ attr_accessor :cant_fork
22
+
23
+ attr_writer :to_s
24
+
25
+ attr_accessor :job
26
+
27
+ # Returns an array of all worker objects.
28
+ def self.all
29
+ mongo_workers.distinct(:worker).map { |w| queues = w.split(','); worker = new(*queues); worker.to_s = w; worker }.compact
30
+ end
31
+
32
+ # Returns an array of all worker objects currently processing
33
+ # jobs.
34
+ def self.working
35
+ select = {'working_on' => { '$exists' => true }}
36
+ # select['working_on'] = {"$exists" => true}
37
+ working = mongo_workers.find(select).to_a
38
+ # working.map! {|w| w['worker'] }
39
+ working.map do |w|
40
+ queues = w['worker'].split(',')
41
+ worker = new(*queues)
42
+ worker.to_s = w['worker']
43
+ worker.job = w['working_on'] || {}
44
+ worker
45
+ end
46
+ end
47
+
48
+ # Returns a single worker object. Accepts a string id.
49
+ def self.find(worker_id)
50
+ w = mongo_workers.find_one(:worker => worker_id)
51
+ return nil unless w
52
+ queues = w['worker'].split(',')
53
+ worker = new(*queues)
54
+ worker.job = w['working_on'] || {} ## avoid a new call to mongo just to retrieve what's this worker is doing
55
+ worker.to_s = worker_id
56
+ worker
57
+ end
58
+
59
+ # Alias of `find`
60
+ def self.attach(worker_id)
61
+ find(worker_id)
62
+ end
63
+
64
+ # # Given a string worker id, return a boolean indicating whether the
65
+ # # worker exists
66
+ def self.exists?(worker_id)
67
+ not mongo_workers.find_one(:worker => worker_id.to_s).nil?
68
+ end
69
+
70
+ # Workers should be initialized with an array of string queue
71
+ # names. The order is important: a Worker will check the first
72
+ # queue given for a job. If none is found, it will check the
73
+ # second queue name given. If a job is found, it will be
74
+ # processed. Upon completion, the Worker will again check the
75
+ # first queue given, and so forth. In this way the queue list
76
+ # passed to a Worker on startup defines the priorities of queues.
77
+ #
78
+ # If passed a single "*", this Worker will operate on all queues
79
+ # in alphabetical order. Queues can be dynamically added or
80
+ # removed without needing to restart workers using this method.
81
+ def initialize(*queues)
82
+ @queues = queues.map { |queue| queue.to_s.strip }
83
+ validate_queues
84
+ end
85
+
86
+ # A worker must be given a queue, otherwise it won't know what to
87
+ # do with itself.
88
+ #
89
+ # You probably never need to call this.
90
+ def validate_queues
91
+ if @queues.nil? || @queues.empty?
92
+ raise NoQueueError.new("Please give each worker at least one queue.")
93
+ end
94
+ end
95
+
96
+ # This is the main workhorse method. Called on a Worker instance,
97
+ # it begins the worker life cycle.
98
+ #
99
+ # The following events occur during a worker's life cycle:
100
+ #
101
+ # 1. Startup: Signals are registered, dead workers are pruned,
102
+ # and this worker is registered.
103
+ # 2. Work loop: Jobs are pulled from a queue and processed.
104
+ # 3. Teardown: This worker is unregistered.
105
+ #
106
+ # Can be passed a float representing the polling frequency.
107
+ # The default is 5 seconds, but for a semi-active site you may
108
+ # want to use a smaller value.
109
+ #
110
+ # Also accepts a block which will be passed the job as soon as it
111
+ # has completed processing. Useful for testing.
112
+ def work(interval = 5.0, &block)
113
+ interval = Float(interval)
114
+ $0 = "resque: Starting"
115
+ job_count = 0
116
+ startup
117
+ loop do
118
+ break if shutdown?
119
+
120
+ if not @paused and job = reserve
121
+ log "got: #{job.inspect}"
122
+ run_hook :before_fork, job
123
+ working_on job
124
+
125
+ if @child = fork
126
+ rand # Reseeding
127
+ procline "Forked #{@child} at #{Time.now.to_s}"
128
+ Process.wait
129
+ else
130
+ procline "Processing #{job.queue} since #{Time.now.to_s} (#{job_count} so far)"
131
+ perform(job, &block)
132
+ job_count += 1
133
+ exit! unless @cant_fork
134
+ end
135
+
136
+ done_working
137
+ @child = nil
138
+ else
139
+ break if interval.zero?
140
+ log! "Sleeping for #{interval} seconds"
141
+ procline @paused ? "Paused" : "Waiting for #{@queues.join(',')}"
142
+ sleep interval
143
+ end
144
+ end
145
+
146
+ ensure
147
+ unregister_worker
148
+ end
149
+
150
+ # DEPRECATED. Processes a single job. If none is given, it will
151
+ # try to produce one. Usually run in the child.
152
+ def process(j = nil, &block)
153
+ return unless j ||= reserve
154
+ working_on j
155
+ perform(j, &block)
156
+ ensure
157
+ done_working
158
+ end
159
+
160
+ # Processes a given job in the child.
161
+ def perform(job)
162
+ begin
163
+ run_hook :after_fork, job
164
+ job.perform
165
+ rescue Object => e
166
+ log "#{job.inspect} failed: #{e.inspect}"
167
+ begin
168
+ job.fail(e)
169
+ rescue Object => e
170
+ log "Received exception when reporting failure: #{e.inspect}"
171
+ end
172
+ failed!
173
+ else
174
+ log "done: #{job.inspect}"
175
+ ensure
176
+ yield job if block_given?
177
+ end
178
+ end
179
+
180
+ # Attempts to grab a job off one of the provided queues. Returns
181
+ # nil if no job can be found.
182
+ def reserve
183
+ queues.each do |queue|
184
+ log! "Checking #{queue}"
185
+ if j = Resque::Job.reserve(queue)
186
+ log! "Found job on #{queue}"
187
+ return j
188
+ end
189
+ end
190
+
191
+ nil
192
+ rescue Exception => e
193
+ log "Error reserving job: #{e.inspect}"
194
+ log e.backtrace.join("\n")
195
+ raise e
196
+ end
197
+
198
+ # Returns a list of queues to use when searching for a job.
199
+ # A splat ("*") means you want every queue (in alpha order) - this
200
+ # can be useful for dynamically adding new queues.
201
+ def queues
202
+ @queues[0] == "*" ? Resque.queues.sort : Resque.queues(@queues).sort
203
+ end
204
+
205
+ # Not every platform supports fork. Here we do our magic to
206
+ # determine if yours does.
207
+ def fork
208
+ @cant_fork = true if $TESTING
209
+
210
+ return if @cant_fork
211
+
212
+ begin
213
+ # IronRuby doesn't support `Kernel.fork` yet
214
+ if Kernel.respond_to?(:fork)
215
+ Kernel.fork
216
+ else
217
+ raise NotImplementedError
218
+ end
219
+ rescue NotImplementedError
220
+ @cant_fork = true
221
+ nil
222
+ end
223
+ end
224
+
225
+ # Runs all the methods needed when a worker begins its lifecycle.
226
+ def startup
227
+ enable_gc_optimizations
228
+ register_signal_handlers
229
+ prune_dead_workers
230
+ run_hook :before_first_fork
231
+ register_worker
232
+
233
+ # Fix buffering so we can `rake resque:work > resque.log` and
234
+ # get output from the child in there.
235
+ $stdout.sync = true
236
+ end
237
+
238
+ # Enables GC Optimizations if you're running REE.
239
+ # http://www.rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
240
+ def enable_gc_optimizations
241
+ if GC.respond_to?(:copy_on_write_friendly=)
242
+ GC.copy_on_write_friendly = true
243
+ end
244
+ end
245
+
246
+ # Registers the various signal handlers a worker responds to.
247
+ #
248
+ # TERM: Shutdown immediately, stop processing jobs.
249
+ # INT: Shutdown immediately, stop processing jobs.
250
+ # QUIT: Shutdown after the current job has finished processing.
251
+ # USR1: Kill the forked child immediately, continue processing jobs.
252
+ # USR2: Don't process any new jobs
253
+ # CONT: Start processing jobs again after a USR2
254
+ def register_signal_handlers
255
+ trap('TERM') { shutdown! }
256
+ trap('INT') { shutdown! }
257
+
258
+ begin
259
+ trap('QUIT') { shutdown }
260
+ trap('USR1') { kill_child }
261
+ trap('USR2') { pause_processing }
262
+ trap('CONT') { unpause_processing }
263
+ rescue ArgumentError
264
+ warn "Signals QUIT, USR1, USR2, and/or CONT not supported."
265
+ end
266
+
267
+ log! "Registered signals"
268
+ end
269
+
270
+ # Schedule this worker for shutdown. Will finish processing the
271
+ # current job.
272
+ def shutdown
273
+ log 'Exiting...'
274
+ @shutdown = true
275
+ end
276
+
277
+ # Kill the child and shutdown immediately.
278
+ def shutdown!
279
+ shutdown
280
+ kill_child
281
+ end
282
+
283
+ # Should this worker shutdown as soon as current job is finished?
284
+ def shutdown?
285
+ @shutdown
286
+ end
287
+
288
+ # Kills the forked child immediately, without remorse. The job it
289
+ # is processing will not be completed.
290
+ def kill_child
291
+ if @child
292
+ log! "Killing child at #{@child}"
293
+ if system("ps -o pid,state -p #{@child}")
294
+ Process.kill("KILL", @child) rescue nil
295
+ else
296
+ log! "Child #{@child} not found, restarting."
297
+ shutdown
298
+ end
299
+ end
300
+ end
301
+
302
+ # Stop processing jobs after the current one has completed (if we're
303
+ # currently running one).
304
+ def pause_processing
305
+ log "USR2 received; pausing job processing"
306
+ @paused = true
307
+ end
308
+
309
+ # Start processing jobs again after a pause
310
+ def unpause_processing
311
+ log "CONT received; resuming job processing"
312
+ @paused = false
313
+ end
314
+
315
+ # Looks for any workers which should be running on this server
316
+ # and, if they're not, removes them from Redis.
317
+ #
318
+ # This is a form of garbage collection. If a server is killed by a
319
+ # hard shutdown, power failure, or something else beyond our
320
+ # control, the Resque workers will not die gracefully and therefore
321
+ # will leave stale state information in Redis.
322
+ #
323
+ # By checking the current Redis state against the actual
324
+ # environment, we can determine if Redis is old and clean it up a bit.
325
+ def prune_dead_workers
326
+ all_workers = Worker.all
327
+ known_workers = worker_pids unless all_workers.empty?
328
+ all_workers.each do |worker|
329
+ host, pid, queues = worker.to_s.split(':')
330
+ next unless host == hostname
331
+ next if known_workers.include?(pid)
332
+ log! "Pruning dead worker: #{worker}"
333
+ worker.unregister_worker
334
+ end
335
+ end
336
+
337
+ # Registers ourself as a worker. Useful when entering the worker
338
+ # lifecycle on startup.
339
+ def register_worker
340
+ mongo_workers.insert(:worker => self.to_s)
341
+ started!
342
+ end
343
+
344
+ # Runs a named hook, passing along any arguments.
345
+ def run_hook(name, *args)
346
+ return unless hook = Resque.send(name)
347
+ msg = "Running #{name} hook"
348
+ msg << " with #{args.inspect}" if args.any?
349
+ log msg
350
+
351
+ args.any? ? hook.call(*args) : hook.call
352
+ end
353
+
354
+ # Unregisters ourself as a worker. Useful when shutting down.
355
+ def unregister_worker
356
+ # If we're still processing a job, make sure it gets logged as a
357
+ # failure.
358
+ if (hash = processing) && !hash.empty?
359
+ j = Job.new(hash[:queue], hash[:payload])
360
+ # Ensure the proper worker is attached to this job, even if
361
+ # it's not the precise instance that died.
362
+ j.worker = self
363
+ j.fail(DirtyExit.new)
364
+ end
365
+
366
+ mongo_workers.remove(:worker => self.to_s)
367
+
368
+ Stat.clear("processed:#{self}")
369
+ Stat.clear("failed:#{self}")
370
+ end
371
+
372
+ def check_payload(payload)
373
+ case payload.class.to_s
374
+ when 'Class'
375
+ payload.to_s
376
+ when 'Array'
377
+ payload.map { |e| check_payload(e) }
378
+ when 'Hash'
379
+ result = {}
380
+ payload.each { |k,v| result[k] = check_payload(v) }
381
+ result
382
+ else
383
+ return payload
384
+ end
385
+ end
386
+
387
+ # Given a job, tells Redis we're working on it. Useful for seeing
388
+ # what workers are doing and when.
389
+ def working_on(j)
390
+ j.worker = self
391
+ data = {
392
+ 'queue' => j.queue,
393
+ 'run_at' => Time.now.to_s,
394
+ 'payload' => check_payload(j.payload)
395
+ }
396
+ @job = data
397
+ working_on = {'working_on' => data}
398
+ mongo_workers.update({:worker => self.to_s}, {'$set' => working_on}, :upsert => true )
399
+ end
400
+
401
+ # Called when we are done working - clears our `working_on` state
402
+ # and tells Redis we processed a job.
403
+ def done_working
404
+ @job = {}
405
+ working_on = {'working_on' => 1}
406
+ mongo_workers.update({:worker => self.to_s}, {'$unset' => working_on})
407
+ processed!
408
+ end
409
+
410
+ # How many jobs has this worker processed? Returns an int.
411
+ def processed
412
+ Stat["processed:#{self}"]
413
+ end
414
+
415
+ # Tell Redis we've processed a job.
416
+ def processed!
417
+ Stat << "processed"
418
+ Stat << "processed:#{self}"
419
+ end
420
+
421
+ # How many failed jobs has this worker seen? Returns an int.
422
+ def failed
423
+ Stat["failed:#{self}"]
424
+ end
425
+
426
+ # Tells Redis we've failed a job.
427
+ def failed!
428
+ Stat << "failed"
429
+ Stat << "failed:#{self}"
430
+ end
431
+
432
+ # What time did this worker start? Returns an instance of `Time`
433
+ def started
434
+ worker = mongo_workers.find_one(:worker => self.to_s)
435
+ return nil if !worker
436
+ worker['started']
437
+ end
438
+
439
+ # Tell Redis we've started
440
+ def started!
441
+ started = {'started' => Time.now }
442
+ mongo_workers.update({:worker => self.to_s}, {'$set' => started})
443
+ end
444
+
445
+ # Returns a hash explaining the Job we're currently processing, if any.
446
+ def processing
447
+ job || {}
448
+ end
449
+
450
+ # Boolean - true if working, false if not
451
+ def working?
452
+ state == :working
453
+ end
454
+
455
+ # Boolean - true if idle, false if not
456
+ def idle?
457
+ state == :idle
458
+ end
459
+
460
+ # Returns a symbol representing the current worker state,
461
+ # which can be either :working or :idle
462
+ def state
463
+ worker = mongo_workers.find_one(:worker => self.to_s)
464
+ worker ? :working : :idle
465
+ end
466
+
467
+ # Is this worker the same as another worker?
468
+ def ==(other)
469
+ to_s == other.to_s
470
+ end
471
+
472
+ def inspect
473
+ "#<Worker #{to_s}>"
474
+ end
475
+
476
+ # The string representation is the same as the id for this worker
477
+ # instance. Can be used with `Worker.find`.
478
+ def to_s
479
+ @to_s ||= "#{hostname}:#{Process.pid}:#{@queues.join(',')}"
480
+ end
481
+ alias_method :worker_id, :to_s
482
+
483
+ # chomp'd hostname of this machine
484
+ def hostname
485
+ @hostname ||= `hostname`.chomp
486
+ end
487
+
488
+ # Returns Integer PID of running worker
489
+ def pid
490
+ @pid ||= to_s.split(":")[1].to_i
491
+ end
492
+
493
+ # Returns an array of string pids of all the other workers on this
494
+ # machine. Useful when pruning dead workers on startup.
495
+ def worker_pids
496
+ `ps -A -o pid,command | grep [r]esque | grep -v "resque-web"`.split("\n").map do |line|
497
+ line.split(' ')[0]
498
+ end
499
+ end
500
+
501
+ # Given a string, sets the procline ($0) and logs.
502
+ # Procline is always in the format of:
503
+ # resque-VERSION: STRING
504
+ def procline(string)
505
+ $0 = "resque-#{Resque::Version}: #{string}"
506
+ log! $0
507
+ end
508
+
509
+ # Log a message to STDOUT if we are verbose or very_verbose.
510
+ def log(message)
511
+ if verbose
512
+ puts "*** #{message}"
513
+ elsif very_verbose
514
+ time = Time.now.strftime('%H:%M:%S %Y-%m-%d')
515
+ puts "** [#{time}] #$$: #{message}"
516
+ end
517
+ end
518
+
519
+ # Logs a very verbose message to STDOUT.
520
+ def log!(message)
521
+ log message if very_verbose
522
+ end
523
+ end
524
+ end