resque_admin 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.md +530 -0
  3. data/LICENSE +20 -0
  4. data/README.markdown +957 -0
  5. data/Rakefile +57 -0
  6. data/bin/resque-admin +81 -0
  7. data/bin/resque-admin-web +31 -0
  8. data/lib/resque_admin.rb +578 -0
  9. data/lib/resque_admin/data_store.rb +325 -0
  10. data/lib/resque_admin/errors.rb +21 -0
  11. data/lib/resque_admin/failure.rb +119 -0
  12. data/lib/resque_admin/failure/airbrake.rb +33 -0
  13. data/lib/resque_admin/failure/base.rb +73 -0
  14. data/lib/resque_admin/failure/multiple.rb +68 -0
  15. data/lib/resque_admin/failure/redis.rb +128 -0
  16. data/lib/resque_admin/failure/redis_multi_queue.rb +104 -0
  17. data/lib/resque_admin/helpers.rb +48 -0
  18. data/lib/resque_admin/job.rb +296 -0
  19. data/lib/resque_admin/log_formatters/quiet_formatter.rb +7 -0
  20. data/lib/resque_admin/log_formatters/verbose_formatter.rb +7 -0
  21. data/lib/resque_admin/log_formatters/very_verbose_formatter.rb +8 -0
  22. data/lib/resque_admin/logging.rb +18 -0
  23. data/lib/resque_admin/plugin.rb +78 -0
  24. data/lib/resque_admin/server.rb +301 -0
  25. data/lib/resque_admin/server/helpers.rb +64 -0
  26. data/lib/resque_admin/server/public/favicon.ico +0 -0
  27. data/lib/resque_admin/server/public/idle.png +0 -0
  28. data/lib/resque_admin/server/public/jquery-1.12.4.min.js +5 -0
  29. data/lib/resque_admin/server/public/jquery.relatize_date.js +95 -0
  30. data/lib/resque_admin/server/public/poll.png +0 -0
  31. data/lib/resque_admin/server/public/ranger.js +78 -0
  32. data/lib/resque_admin/server/public/reset.css +44 -0
  33. data/lib/resque_admin/server/public/style.css +91 -0
  34. data/lib/resque_admin/server/public/working.png +0 -0
  35. data/lib/resque_admin/server/test_helper.rb +19 -0
  36. data/lib/resque_admin/server/views/error.erb +1 -0
  37. data/lib/resque_admin/server/views/failed.erb +29 -0
  38. data/lib/resque_admin/server/views/failed_job.erb +50 -0
  39. data/lib/resque_admin/server/views/failed_queues_overview.erb +24 -0
  40. data/lib/resque_admin/server/views/job_class.erb +6 -0
  41. data/lib/resque_admin/server/views/key_sets.erb +17 -0
  42. data/lib/resque_admin/server/views/key_string.erb +11 -0
  43. data/lib/resque_admin/server/views/layout.erb +44 -0
  44. data/lib/resque_admin/server/views/next_more.erb +22 -0
  45. data/lib/resque_admin/server/views/overview.erb +4 -0
  46. data/lib/resque_admin/server/views/processing.erb +2 -0
  47. data/lib/resque_admin/server/views/queues.erb +58 -0
  48. data/lib/resque_admin/server/views/stats.erb +62 -0
  49. data/lib/resque_admin/server/views/workers.erb +109 -0
  50. data/lib/resque_admin/server/views/working.erb +71 -0
  51. data/lib/resque_admin/stat.rb +58 -0
  52. data/lib/resque_admin/tasks.rb +72 -0
  53. data/lib/resque_admin/thread_signal.rb +24 -0
  54. data/lib/resque_admin/vendor/utf8_util.rb +24 -0
  55. data/lib/resque_admin/version.rb +3 -0
  56. data/lib/resque_admin/worker.rb +917 -0
  57. data/lib/tasks/redis.rake +161 -0
  58. data/lib/tasks/resque_admin.rake +2 -0
  59. metadata +191 -0
data/Rakefile ADDED
@@ -0,0 +1,57 @@
1
+ #
2
+ # Setup
3
+ #
4
+
5
+ load 'lib/tasks/redis.rake'
6
+
7
+ $LOAD_PATH.unshift 'lib'
8
+ require 'resque_admin/tasks'
9
+
10
+ def command?(command)
11
+ system("type #{command} > /dev/null 2>&1")
12
+ end
13
+
14
+ require 'rubygems'
15
+ require 'bundler/setup'
16
+ require 'bundler/gem_tasks'
17
+
18
+
19
+ #
20
+ # Tests
21
+ #
22
+
23
+ require 'rake/testtask'
24
+
25
+ task :default => :test
26
+
27
+ Rake::TestTask.new do |test|
28
+ test.verbose = true
29
+ test.libs << "test"
30
+ test.libs << "lib"
31
+ test.test_files = FileList['test/**/*_test.rb']
32
+ end
33
+
34
+ if command? :kicker
35
+ desc "Launch Kicker (like autotest)"
36
+ task :kicker do
37
+ puts "Kicking... (ctrl+c to cancel)"
38
+ exec "kicker -e rake test lib examples"
39
+ end
40
+ end
41
+
42
+
43
+ #
44
+ # Install
45
+ #
46
+
47
+ task :install => [ 'redis:install', 'dtach:install' ]
48
+
49
+
50
+ #
51
+ # Documentation
52
+ #
53
+
54
+ begin
55
+ require 'sdoc_helpers'
56
+ rescue LoadError
57
+ end
data/bin/resque-admin ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ begin
5
+ require 'redis-namespace'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'redis-namespace'
9
+ end
10
+ require 'resque_admin'
11
+ require 'optparse'
12
+
13
+ parser = OptionParser.new do |opts|
14
+ opts.banner = "Usage: resque [options] COMMAND"
15
+
16
+ opts.separator ""
17
+ opts.separator "Options:"
18
+
19
+ opts.on("-r", "--redis [HOST:PORT]", "Redis connection string") do |host|
20
+ ResqueAdmin.redis = host
21
+ end
22
+
23
+ opts.on("-N", "--namespace [NAMESPACE]", "Redis namespace") do |namespace|
24
+ ResqueAdmin.redis.namespace = namespace
25
+ end
26
+
27
+ opts.on("-h", "--help", "Show this message") do
28
+ puts opts
29
+ exit
30
+ end
31
+
32
+ opts.separator ""
33
+ opts.separator "Commands:"
34
+ opts.separator " remove WORKER Removes a worker"
35
+ opts.separator " kill WORKER Kills a worker"
36
+ opts.separator " list Lists known workers"
37
+ end
38
+
39
+ def kill(worker)
40
+ abort "** kill WORKER_ID" if worker.nil?
41
+ pid = worker.split(':')[1].to_i
42
+
43
+ begin
44
+ Process.kill("KILL", pid)
45
+ puts "** killed #{worker}"
46
+ rescue Errno::ESRCH
47
+ puts "** worker #{worker} not running"
48
+ end
49
+
50
+ remove worker
51
+ end
52
+
53
+ def remove(worker)
54
+ abort "** remove WORKER_ID" if worker.nil?
55
+
56
+ ResqueAdmin.remove_worker(worker)
57
+ puts "** removed #{worker}"
58
+ end
59
+
60
+ def list
61
+ if ResqueAdmin.workers.any?
62
+ ResqueAdmin.workers.each do |worker|
63
+ puts "#{worker} (#{worker.state})"
64
+ end
65
+ else
66
+ puts "None"
67
+ end
68
+ end
69
+
70
+ parser.parse!
71
+
72
+ case ARGV[0]
73
+ when 'kill'
74
+ kill ARGV[1]
75
+ when 'remove'
76
+ remove ARGV[1]
77
+ when 'list'
78
+ list
79
+ else
80
+ puts parser.help
81
+ end
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ begin
5
+ require 'vegas'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'vegas'
9
+ end
10
+ require 'resque_admin/server'
11
+
12
+
13
+ Vegas::Runner.new(ResqueAdmin::Server, 'resque-web', {
14
+ :before_run => lambda {|v|
15
+ path = (ENV['RESQUECONFIG'] || v.args.first)
16
+ load path.to_s.strip if path
17
+ }
18
+ }) do |runner, opts, app|
19
+ opts.on('-N NAMESPACE', "--namespace NAMESPACE", "set the Redis namespace") {|namespace|
20
+ runner.logger.info "Using Redis namespace '#{namespace}'"
21
+ ResqueAdmin.redis.namespace = namespace
22
+ }
23
+ opts.on('-r redis-connection', "--redis redis-connection", "set the Redis connection string") {|redis_conf|
24
+ runner.logger.info "Using Redis connection '#{redis_conf}'"
25
+ ResqueAdmin.redis = redis_conf
26
+ }
27
+ opts.on('-a url-prefix', "--append url-prefix", "set reverse_proxy friendly prefix to links") {|url_prefix|
28
+ runner.logger.info "Using URL Prefix '#{url_prefix}'"
29
+ ResqueAdmin::Server.url_prefix = url_prefix
30
+ }
31
+ end
@@ -0,0 +1,578 @@
1
+ require 'mono_logger'
2
+ require 'redis/namespace'
3
+ require 'forwardable'
4
+
5
+ require 'resque/version'
6
+
7
+ require 'resque/errors'
8
+
9
+ require 'resque/failure'
10
+ require 'resque/failure/base'
11
+
12
+ require 'resque/helpers'
13
+ require 'resque/stat'
14
+ require 'resque/logging'
15
+ require 'resque/log_formatters/quiet_formatter'
16
+ require 'resque/log_formatters/verbose_formatter'
17
+ require 'resque/log_formatters/very_verbose_formatter'
18
+ require 'resque/job'
19
+ require 'resque/worker'
20
+ require 'resque/plugin'
21
+ require 'resque/data_store'
22
+ require 'resque/thread_signal'
23
+
24
+ require 'resque/vendor/utf8_util'
25
+
26
+ module ResqueAdmin
27
+ include Helpers
28
+ extend self
29
+
30
+ # Given a Ruby object, returns a string suitable for storage in a
31
+ # queue.
32
+ def encode(object)
33
+ if MultiJson.respond_to?(:dump) && MultiJson.respond_to?(:load)
34
+ MultiJson.dump object
35
+ else
36
+ MultiJson.encode object
37
+ end
38
+ end
39
+
40
+ # Given a string, returns a Ruby object.
41
+ def decode(object)
42
+ return unless object
43
+
44
+ begin
45
+ if MultiJson.respond_to?(:dump) && MultiJson.respond_to?(:load)
46
+ MultiJson.load object
47
+ else
48
+ MultiJson.decode object
49
+ end
50
+ rescue ::MultiJson::DecodeError => e
51
+ raise Helpers::DecodeException, e.message, e.backtrace
52
+ end
53
+ end
54
+
55
+ # Given a word with dashes, returns a camel cased version of it.
56
+ #
57
+ # classify('job-name') # => 'JobName'
58
+ def classify(dashed_word)
59
+ dashed_word.split('-').map(&:capitalize).join
60
+ end
61
+
62
+ # Tries to find a constant with the name specified in the argument string:
63
+ #
64
+ # constantize("Module") # => Module
65
+ # constantize("Test::Unit") # => Test::Unit
66
+ #
67
+ # The name is assumed to be the one of a top-level constant, no matter
68
+ # whether it starts with "::" or not. No lexical context is taken into
69
+ # account:
70
+ #
71
+ # C = 'outside'
72
+ # module M
73
+ # C = 'inside'
74
+ # C # => 'inside'
75
+ # constantize("C") # => 'outside', same as ::C
76
+ # end
77
+ #
78
+ # NameError is raised when the constant is unknown.
79
+ def constantize(camel_cased_word)
80
+ camel_cased_word = camel_cased_word.to_s
81
+
82
+ if camel_cased_word.include?('-')
83
+ camel_cased_word = classify(camel_cased_word)
84
+ end
85
+
86
+ names = camel_cased_word.split('::')
87
+ names.shift if names.empty? || names.first.empty?
88
+
89
+ constant = Object
90
+ names.each do |name|
91
+ args = Module.method(:const_get).arity != 1 ? [false] : []
92
+
93
+ if constant.const_defined?(name, *args)
94
+ constant = constant.const_get(name)
95
+ else
96
+ constant = constant.const_missing(name)
97
+ end
98
+ end
99
+ constant
100
+ end
101
+
102
+ extend ::Forwardable
103
+
104
+ # Accepts:
105
+ # 1. A 'hostname:port' String
106
+ # 2. A 'hostname:port:db' String (to select the Redis db)
107
+ # 3. A 'hostname:port/namespace' String (to set the Redis namespace)
108
+ # 4. A Redis URL String 'redis://host:port'
109
+ # 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
110
+ # or `Redis::Namespace`.
111
+ # 6. An Hash of a redis connection {:host => 'localhost', :port => 6379, :db => 0}
112
+ def redis=(server)
113
+ case server
114
+ when String
115
+ if server =~ /redis\:\/\//
116
+ redis = Redis.connect(:url => server, :thread_safe => true)
117
+ else
118
+ server, namespace = server.split('/', 2)
119
+ host, port, db = server.split(':')
120
+ redis = Redis.new(:host => host, :port => port,
121
+ :thread_safe => true, :db => db)
122
+ end
123
+ namespace ||= :resque
124
+
125
+ @data_store = ResqueAdmin::DataStore.new(Redis::Namespace.new(namespace, :redis => redis))
126
+ when Redis::Namespace
127
+ @data_store = ResqueAdmin::DataStore.new(server)
128
+ when ResqueAdmin::DataStore
129
+ @data_store = server
130
+ when Hash
131
+ @data_store = ResqueAdmin::DataStore.new(Redis::Namespace.new(:resque, :redis => Redis.new(server)))
132
+ else
133
+ @data_store = ResqueAdmin::DataStore.new(Redis::Namespace.new(:resque, :redis => server))
134
+ end
135
+ end
136
+
137
+ # Returns the current Redis connection. If none has been created, will
138
+ # create a new one.
139
+ def redis
140
+ return @data_store if @data_store
141
+ self.redis = Redis.respond_to?(:connect) ? Redis.connect : "localhost:6379"
142
+ self.redis
143
+ end
144
+ alias :data_store :redis
145
+
146
+ def redis_id
147
+ data_store.identifier
148
+ end
149
+
150
+ # Set or retrieve the current logger object
151
+ attr_accessor :logger
152
+
153
+ DEFAULT_HEARTBEAT_INTERVAL = 60
154
+ DEFAULT_PRUNE_INTERVAL = DEFAULT_HEARTBEAT_INTERVAL * 5
155
+
156
+ attr_writer :heartbeat_interval
157
+ def heartbeat_interval
158
+ @heartbeat_interval || DEFAULT_HEARTBEAT_INTERVAL
159
+ end
160
+
161
+ attr_writer :prune_interval
162
+ def prune_interval
163
+ @prune_interval || DEFAULT_PRUNE_INTERVAL
164
+ end
165
+
166
+ attr_writer :enqueue_front
167
+ def enqueue_front
168
+ return @enqueue_front unless @enqueue_front.nil?
169
+ @enqueue_front = false
170
+ end
171
+
172
+ # The `before_first_fork` hook will be run in the **parent** process
173
+ # only once, before forking to run the first job. Be careful- any
174
+ # changes you make will be permanent for the lifespan of the
175
+ # worker.
176
+ #
177
+ # Call with a block to register a hook.
178
+ # Call with no arguments to return all registered hooks.
179
+ def before_first_fork(&block)
180
+ block ? register_hook(:before_first_fork, block) : hooks(:before_first_fork)
181
+ end
182
+
183
+ # Register a before_first_fork proc.
184
+ def before_first_fork=(block)
185
+ register_hook(:before_first_fork, block)
186
+ end
187
+
188
+ # The `before_fork` hook will be run in the **parent** process
189
+ # before every job, so be careful- any changes you make will be
190
+ # permanent for the lifespan of the worker.
191
+ #
192
+ # Call with a block to register a hook.
193
+ # Call with no arguments to return all registered hooks.
194
+ def before_fork(&block)
195
+ block ? register_hook(:before_fork, block) : hooks(:before_fork)
196
+ end
197
+
198
+ # Register a before_fork proc.
199
+ def before_fork=(block)
200
+ register_hook(:before_fork, block)
201
+ end
202
+
203
+ # The `after_fork` hook will be run in the child process and is passed
204
+ # the current job. Any changes you make, therefore, will only live as
205
+ # long as the job currently being processed.
206
+ #
207
+ # Call with a block to register a hook.
208
+ # Call with no arguments to return all registered hooks.
209
+ def after_fork(&block)
210
+ block ? register_hook(:after_fork, block) : hooks(:after_fork)
211
+ end
212
+
213
+ # Register an after_fork proc.
214
+ def after_fork=(block)
215
+ register_hook(:after_fork, block)
216
+ end
217
+
218
+ # The `before_pause` hook will be run in the parent process before the
219
+ # worker has paused processing (via #pause_processing or SIGUSR2).
220
+ def before_pause(&block)
221
+ block ? register_hook(:before_pause, block) : hooks(:before_pause)
222
+ end
223
+
224
+ # Register a before_pause proc.
225
+ def before_pause=(block)
226
+ register_hook(:before_pause, block)
227
+ end
228
+
229
+ # The `after_pause` hook will be run in the parent process after the
230
+ # worker has paused (via SIGCONT).
231
+ def after_pause(&block)
232
+ block ? register_hook(:after_pause, block) : hooks(:after_pause)
233
+ end
234
+
235
+ # Register an after_pause proc.
236
+ def after_pause=(block)
237
+ register_hook(:after_pause, block)
238
+ end
239
+
240
+ def to_s
241
+ "ResqueAdmin Client connected to #{redis_id}"
242
+ end
243
+
244
+ attr_accessor :inline
245
+
246
+ # If 'inline' is true ResqueAdmin will call #perform method inline
247
+ # without queuing it into Redis and without any ResqueAdmin callbacks.
248
+ # The 'inline' is false ResqueAdmin jobs will be put in queue regularly.
249
+ alias :inline? :inline
250
+
251
+ #
252
+ # queue manipulation
253
+ #
254
+
255
+ # Pushes a job onto a queue. Queue name should be a string and the
256
+ # item should be any JSON-able Ruby object.
257
+ #
258
+ # ResqueAdmin works generally expect the `item` to be a hash with the following
259
+ # keys:
260
+ #
261
+ # class - The String name of the job to run.
262
+ # args - An Array of arguments to pass the job. Usually passed
263
+ # via `class.to_class.perform(*args)`.
264
+ #
265
+ # Example
266
+ #
267
+ # ResqueAdmin.push('archive', :class => 'Archive', :args => [ 35, 'tar' ])
268
+ #
269
+ # Returns nothing
270
+ def push(queue, item)
271
+ data_store.push_to_queue(queue,encode(item))
272
+ end
273
+
274
+ # Pops a job off a queue. Queue name should be a string.
275
+ #
276
+ # Returns a Ruby object.
277
+ def pop(queue)
278
+ decode(data_store.pop_from_queue(queue))
279
+ end
280
+
281
+ # Returns an integer representing the size of a queue.
282
+ # Queue name should be a string.
283
+ def size(queue)
284
+ data_store.queue_size(queue)
285
+ end
286
+
287
+ # Returns an array of items currently queued. Queue name should be
288
+ # a string.
289
+ #
290
+ # start and count should be integer and can be used for pagination.
291
+ # start is the item to begin, count is how many items to return.
292
+ #
293
+ # To get the 3rd page of a 30 item, paginatied list one would use:
294
+ # ResqueAdmin.peek('my_list', 59, 30)
295
+ def peek(queue, start = 0, count = 1)
296
+ results = data_store.peek_in_queue(queue,start,count)
297
+ if count == 1
298
+ decode(results)
299
+ else
300
+ results.map { |result| decode(result) }
301
+ end
302
+ end
303
+
304
+ # Does the dirty work of fetching a range of items from a Redis list
305
+ # and converting them into Ruby objects.
306
+ def list_range(key, start = 0, count = 1)
307
+ results = data_store.list_range(key, start, count)
308
+ if count == 1
309
+ decode(results)
310
+ else
311
+ results.map { |result| decode(result) }
312
+ end
313
+ end
314
+
315
+ # Returns an array of all known ResqueAdmin queues as strings.
316
+ def queues
317
+ data_store.queue_names
318
+ end
319
+
320
+ # Given a queue name, completely deletes the queue.
321
+ def remove_queue(queue)
322
+ data_store.remove_queue(queue)
323
+ end
324
+
325
+ # Used internally to keep track of which queues we've created.
326
+ # Don't call this directly.
327
+ def watch_queue(queue)
328
+ data_store.watch_queue(queue)
329
+ end
330
+
331
+
332
+ #
333
+ # job shortcuts
334
+ #
335
+
336
+ # This method can be used to conveniently add a job to a queue.
337
+ # It assumes the class you're passing it is a real Ruby class (not
338
+ # a string or reference) which either:
339
+ #
340
+ # a) has a @queue ivar set
341
+ # b) responds to `queue`
342
+ #
343
+ # If either of those conditions are met, it will use the value obtained
344
+ # from performing one of the above operations to determine the queue.
345
+ #
346
+ # If no queue can be inferred this method will raise a `ResqueAdmin::NoQueueError`
347
+ #
348
+ # Returns true if the job was queued, nil if the job was rejected by a
349
+ # before_enqueue hook.
350
+ #
351
+ # This method is considered part of the `stable` API.
352
+ def enqueue(klass, *args)
353
+ enqueue_to(queue_from_class(klass), klass, *args)
354
+ end
355
+
356
+ # Just like `enqueue` but allows you to specify the queue you want to
357
+ # use. Runs hooks.
358
+ #
359
+ # `queue` should be the String name of the queue you're targeting.
360
+ #
361
+ # Returns true if the job was queued, nil if the job was rejected by a
362
+ # before_enqueue hook.
363
+ #
364
+ # This method is considered part of the `stable` API.
365
+ def enqueue_to(queue, klass, *args)
366
+ # Perform before_enqueue hooks. Don't perform enqueue if any hook returns false
367
+ before_hooks = Plugin.before_enqueue_hooks(klass).collect do |hook|
368
+ klass.send(hook, *args)
369
+ end
370
+ return nil if before_hooks.any? { |result| result == false }
371
+
372
+ Job.create(queue, klass, *args)
373
+
374
+ Plugin.after_enqueue_hooks(klass).each do |hook|
375
+ klass.send(hook, *args)
376
+ end
377
+
378
+ return true
379
+ end
380
+
381
+ # This method can be used to conveniently remove a job from a queue.
382
+ # It assumes the class you're passing it is a real Ruby class (not
383
+ # a string or reference) which either:
384
+ #
385
+ # a) has a @queue ivar set
386
+ # b) responds to `queue`
387
+ #
388
+ # If either of those conditions are met, it will use the value obtained
389
+ # from performing one of the above operations to determine the queue.
390
+ #
391
+ # If no queue can be inferred this method will raise a `ResqueAdmin::NoQueueError`
392
+ #
393
+ # If no args are given, this method will dequeue *all* jobs matching
394
+ # the provided class. See `ResqueAdmin::Job.destroy` for more
395
+ # information.
396
+ #
397
+ # Returns the number of jobs destroyed.
398
+ #
399
+ # Example:
400
+ #
401
+ # # Removes all jobs of class `UpdateNetworkGraph`
402
+ # ResqueAdmin.dequeue(GitHub::Jobs::UpdateNetworkGraph)
403
+ #
404
+ # # Removes all jobs of class `UpdateNetworkGraph` with matching args.
405
+ # ResqueAdmin.dequeue(GitHub::Jobs::UpdateNetworkGraph, 'repo:135325')
406
+ #
407
+ # This method is considered part of the `stable` API.
408
+ def dequeue(klass, *args)
409
+ # Perform before_dequeue hooks. Don't perform dequeue if any hook returns false
410
+ before_hooks = Plugin.before_dequeue_hooks(klass).collect do |hook|
411
+ klass.send(hook, *args)
412
+ end
413
+ return if before_hooks.any? { |result| result == false }
414
+
415
+ destroyed = Job.destroy(queue_from_class(klass), klass, *args)
416
+
417
+ Plugin.after_dequeue_hooks(klass).each do |hook|
418
+ klass.send(hook, *args)
419
+ end
420
+
421
+ destroyed
422
+ end
423
+
424
+ # Given a class, try to extrapolate an appropriate queue based on a
425
+ # class instance variable or `queue` method.
426
+ def queue_from_class(klass)
427
+ klass.instance_variable_get(:@queue) ||
428
+ (klass.respond_to?(:queue) and klass.queue)
429
+ end
430
+
431
+ # This method will return a `ResqueAdmin::Job` object or a non-true value
432
+ # depending on whether a job can be obtained. You should pass it the
433
+ # precise name of a queue: case matters.
434
+ #
435
+ # This method is considered part of the `stable` API.
436
+ def reserve(queue)
437
+ Job.reserve(queue)
438
+ end
439
+
440
+ # Validates if the given klass could be a valid ResqueAdmin job
441
+ #
442
+ # If no queue can be inferred this method will raise a `ResqueAdmin::NoQueueError`
443
+ #
444
+ # If given klass is nil this method will raise a `ResqueAdmin::NoClassError`
445
+ def validate(klass, queue = nil)
446
+ queue ||= queue_from_class(klass)
447
+
448
+ if !queue
449
+ raise NoQueueError.new("Jobs must be placed onto a queue. No queue could be inferred for class #{klass}")
450
+ end
451
+
452
+ if klass.to_s.empty?
453
+ raise NoClassError.new("Jobs must be given a class.")
454
+ end
455
+ end
456
+
457
+
458
+ #
459
+ # worker shortcuts
460
+ #
461
+
462
+ # A shortcut to Worker.all
463
+ def workers
464
+ Worker.all
465
+ end
466
+
467
+ # A shortcut to Worker.working
468
+ def working
469
+ Worker.working
470
+ end
471
+
472
+ # A shortcut to unregister_worker
473
+ # useful for command line tool
474
+ def remove_worker(worker_id)
475
+ worker = ResqueAdmin::Worker.find(worker_id)
476
+ worker.unregister_worker
477
+ end
478
+
479
+ #
480
+ # stats
481
+ #
482
+
483
+ # Returns a hash, similar to redis-rb's #info, of interesting stats.
484
+ def info
485
+ return {
486
+ :pending => queue_sizes.inject(0) { |sum, (queue_name, queue_size)| sum + queue_size },
487
+ :processed => Stat[:processed],
488
+ :queues => queues.size,
489
+ :workers => workers.size.to_i,
490
+ :working => working.size,
491
+ :failed => data_store.num_failed,
492
+ :servers => [redis_id],
493
+ :environment => ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
494
+ }
495
+ end
496
+
497
+ # Returns an array of all known ResqueAdmin keys in Redis. Redis' KEYS operation
498
+ # is O(N) for the keyspace, so be careful - this can be slow for big databases.
499
+ def keys
500
+ data_store.all_resque_keys
501
+ end
502
+
503
+ # Returns a hash, mapping queue names to queue sizes
504
+ def queue_sizes
505
+ queue_names = queues
506
+
507
+ sizes = redis.pipelined do
508
+ queue_names.each do |name|
509
+ redis.llen("queue:#{name}")
510
+ end
511
+ end
512
+
513
+ Hash[queue_names.zip(sizes)]
514
+ end
515
+
516
+ # Returns a hash, mapping queue names to (up to `sample_size`) samples of jobs in that queue
517
+ def sample_queues(sample_size = 1000)
518
+ queue_names = queues
519
+
520
+ samples = redis.pipelined do
521
+ queue_names.each do |name|
522
+ key = "queue:#{name}"
523
+ redis.llen(key)
524
+ redis.lrange(key, 0, sample_size - 1)
525
+ end
526
+ end
527
+
528
+ hash = {}
529
+
530
+ queue_names.zip(samples.each_slice(2).to_a) do |queue_name, (queue_size, serialized_samples)|
531
+ samples = serialized_samples.map do |serialized_sample|
532
+ Job.decode(serialized_sample)
533
+ end
534
+
535
+ hash[queue_name] = {
536
+ :size => queue_size,
537
+ :samples => samples
538
+ }
539
+ end
540
+
541
+ hash
542
+ end
543
+
544
+ private
545
+
546
+ # Register a new proc as a hook. If the block is nil this is the
547
+ # equivalent of removing all hooks of the given name.
548
+ #
549
+ # `name` is the hook that the block should be registered with.
550
+ def register_hook(name, block)
551
+ return clear_hooks(name) if block.nil?
552
+
553
+ @hooks ||= {}
554
+ @hooks[name] ||= []
555
+
556
+ block = Array(block)
557
+ @hooks[name].concat(block)
558
+ end
559
+
560
+ # Clear all hooks given a hook name.
561
+ def clear_hooks(name)
562
+ @hooks && @hooks[name] = []
563
+ end
564
+
565
+ # Retrieve all hooks
566
+ def hooks
567
+ @hooks || {}
568
+ end
569
+
570
+ # Retrieve all hooks of a given name.
571
+ def hooks(name)
572
+ (@hooks && @hooks[name]) || []
573
+ end
574
+ end
575
+
576
+ # Log to STDOUT by default
577
+ ResqueAdmin.logger = MonoLogger.new(STDOUT)
578
+ ResqueAdmin.logger.formatter = ResqueAdmin::QuietFormatter.new