mongo-resque 1.17.1

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