tr_resque 1.20.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/HISTORY.md +354 -0
  2. data/LICENSE +20 -0
  3. data/README.markdown +908 -0
  4. data/Rakefile +70 -0
  5. data/bin/resque +81 -0
  6. data/bin/resque-web +27 -0
  7. data/lib/resque.rb +369 -0
  8. data/lib/resque/errors.rb +10 -0
  9. data/lib/resque/failure.rb +96 -0
  10. data/lib/resque/failure/airbrake.rb +17 -0
  11. data/lib/resque/failure/base.rb +64 -0
  12. data/lib/resque/failure/hoptoad.rb +33 -0
  13. data/lib/resque/failure/multiple.rb +54 -0
  14. data/lib/resque/failure/redis.rb +51 -0
  15. data/lib/resque/failure/thoughtbot.rb +33 -0
  16. data/lib/resque/helpers.rb +94 -0
  17. data/lib/resque/job.rb +227 -0
  18. data/lib/resque/plugin.rb +66 -0
  19. data/lib/resque/server.rb +248 -0
  20. data/lib/resque/server/public/favicon.ico +0 -0
  21. data/lib/resque/server/public/idle.png +0 -0
  22. data/lib/resque/server/public/jquery-1.3.2.min.js +19 -0
  23. data/lib/resque/server/public/jquery.relatize_date.js +95 -0
  24. data/lib/resque/server/public/poll.png +0 -0
  25. data/lib/resque/server/public/ranger.js +73 -0
  26. data/lib/resque/server/public/reset.css +44 -0
  27. data/lib/resque/server/public/style.css +86 -0
  28. data/lib/resque/server/public/working.png +0 -0
  29. data/lib/resque/server/test_helper.rb +19 -0
  30. data/lib/resque/server/views/error.erb +1 -0
  31. data/lib/resque/server/views/failed.erb +67 -0
  32. data/lib/resque/server/views/key_sets.erb +19 -0
  33. data/lib/resque/server/views/key_string.erb +11 -0
  34. data/lib/resque/server/views/layout.erb +44 -0
  35. data/lib/resque/server/views/next_more.erb +10 -0
  36. data/lib/resque/server/views/overview.erb +4 -0
  37. data/lib/resque/server/views/queues.erb +49 -0
  38. data/lib/resque/server/views/stats.erb +62 -0
  39. data/lib/resque/server/views/workers.erb +109 -0
  40. data/lib/resque/server/views/working.erb +72 -0
  41. data/lib/resque/stat.rb +53 -0
  42. data/lib/resque/tasks.rb +61 -0
  43. data/lib/resque/version.rb +3 -0
  44. data/lib/resque/worker.rb +546 -0
  45. data/lib/tasks/redis.rake +161 -0
  46. data/lib/tasks/resque.rake +2 -0
  47. data/test/airbrake_test.rb +27 -0
  48. data/test/hoptoad_test.rb +26 -0
  49. data/test/job_hooks_test.rb +423 -0
  50. data/test/job_plugins_test.rb +230 -0
  51. data/test/plugin_test.rb +116 -0
  52. data/test/redis-test-cluster.conf +115 -0
  53. data/test/redis-test.conf +115 -0
  54. data/test/resque-web_test.rb +59 -0
  55. data/test/resque_test.rb +278 -0
  56. data/test/test_helper.rb +160 -0
  57. data/test/worker_test.rb +434 -0
  58. metadata +186 -0
data/lib/resque/job.rb ADDED
@@ -0,0 +1,227 @@
1
+ module Resque
2
+ # A Resque::Job represents a unit of work. Each job lives on a
3
+ # single queue and has an associated payload object. The payload
4
+ # is a hash with two attributes: `class` and `args`. The `class` is
5
+ # the name of the Ruby class which should be used to run the
6
+ # job. The `args` are an array of arguments which should be passed
7
+ # to the Ruby class's `perform` class-level method.
8
+ #
9
+ # You can manually run a job using this code:
10
+ #
11
+ # job = Resque::Job.reserve(:high)
12
+ # klass = Resque::Job.constantize(job.payload['class'])
13
+ # klass.perform(*job.payload['args'])
14
+ class Job
15
+ include Helpers
16
+ extend Helpers
17
+
18
+ # Raise Resque::Job::DontPerform from a before_perform hook to
19
+ # abort the job.
20
+ DontPerform = Class.new(StandardError)
21
+
22
+ # The worker object which is currently processing this job.
23
+ attr_accessor :worker
24
+
25
+ # The name of the queue from which this job was pulled (or is to be
26
+ # placed)
27
+ attr_reader :queue
28
+
29
+ # This job's associated payload object.
30
+ attr_reader :payload
31
+
32
+ def initialize(queue, payload)
33
+ @queue = queue
34
+ @payload = payload
35
+ @failure_hooks_ran = false
36
+ end
37
+
38
+ # Creates a job by placing it on a queue. Expects a string queue
39
+ # name, a string class name, and an optional array of arguments to
40
+ # pass to the class' `perform` method.
41
+ #
42
+ # Raises an exception if no queue or class is given.
43
+ def self.create(queue, klass, *args)
44
+ Resque.validate(klass, queue)
45
+
46
+ if Resque.inline?
47
+ constantize(klass).perform(*decode(encode(args)))
48
+ else
49
+ Resque.push(queue, :class => klass.to_s, :args => args)
50
+ end
51
+ end
52
+
53
+ # Removes a job from a queue. Expects a string queue name, a
54
+ # string class name, and, optionally, args.
55
+ #
56
+ # Returns the number of jobs destroyed.
57
+ #
58
+ # If no args are provided, it will remove all jobs of the class
59
+ # provided.
60
+ #
61
+ # That is, for these two jobs:
62
+ #
63
+ # { 'class' => 'UpdateGraph', 'args' => ['defunkt'] }
64
+ # { 'class' => 'UpdateGraph', 'args' => ['mojombo'] }
65
+ #
66
+ # The following call will remove both:
67
+ #
68
+ # Resque::Job.destroy(queue, 'UpdateGraph')
69
+ #
70
+ # Whereas specifying args will only remove the 2nd job:
71
+ #
72
+ # Resque::Job.destroy(queue, 'UpdateGraph', 'mojombo')
73
+ #
74
+ # This method can be potentially very slow and memory intensive,
75
+ # depending on the size of your queue, as it loads all jobs into
76
+ # a Ruby array before processing.
77
+ def self.destroy(queue, klass, *args)
78
+ klass = klass.to_s
79
+ queue = "queue:#{queue}"
80
+ destroyed = 0
81
+
82
+ if args.empty?
83
+ redis.lrange(queue, 0, -1).each do |string|
84
+ if decode(string)['class'] == klass
85
+ destroyed += redis.lrem(queue, 0, string).to_i
86
+ end
87
+ end
88
+ else
89
+ destroyed += redis.lrem(queue, 0, encode(:class => klass, :args => args))
90
+ end
91
+
92
+ destroyed
93
+ end
94
+
95
+ # Given a string queue name, returns an instance of Resque::Job
96
+ # if any jobs are available. If not, returns nil.
97
+ def self.reserve(queue)
98
+ return unless payload = Resque.pop(queue)
99
+ new(queue, payload)
100
+ end
101
+
102
+ # Attempts to perform the work represented by this job instance.
103
+ # Calls #perform on the class given in the payload with the
104
+ # arguments given in the payload.
105
+ def perform
106
+ job = payload_class
107
+ job_args = args || []
108
+ job_was_performed = false
109
+
110
+ begin
111
+ # Execute before_perform hook. Abort the job gracefully if
112
+ # Resque::DontPerform is raised.
113
+ begin
114
+ before_hooks.each do |hook|
115
+ job.send(hook, *job_args)
116
+ end
117
+ rescue DontPerform
118
+ return false
119
+ end
120
+
121
+ # Execute the job. Do it in an around_perform hook if available.
122
+ if around_hooks.empty?
123
+ job.perform(*job_args)
124
+ job_was_performed = true
125
+ else
126
+ # We want to nest all around_perform plugins, with the last one
127
+ # finally calling perform
128
+ stack = around_hooks.reverse.inject(nil) do |last_hook, hook|
129
+ if last_hook
130
+ lambda do
131
+ job.send(hook, *job_args) { last_hook.call }
132
+ end
133
+ else
134
+ lambda do
135
+ job.send(hook, *job_args) do
136
+ result = job.perform(*job_args)
137
+ job_was_performed = true
138
+ result
139
+ end
140
+ end
141
+ end
142
+ end
143
+ stack.call
144
+ end
145
+
146
+ # Execute after_perform hook
147
+ after_hooks.each do |hook|
148
+ job.send(hook, *job_args)
149
+ end
150
+
151
+ # Return true if the job was performed
152
+ return job_was_performed
153
+
154
+ # If an exception occurs during the job execution, look for an
155
+ # on_failure hook then re-raise.
156
+ rescue Object => e
157
+ run_failure_hooks(e)
158
+ raise e
159
+ end
160
+ end
161
+
162
+ # Returns the actual class constant represented in this job's payload.
163
+ def payload_class
164
+ @payload_class ||= constantize(@payload['class'])
165
+ end
166
+
167
+ # Returns an array of args represented in this job's payload.
168
+ def args
169
+ @payload['args']
170
+ end
171
+
172
+ # Given an exception object, hands off the needed parameters to
173
+ # the Failure module.
174
+ def fail(exception)
175
+ run_failure_hooks(exception)
176
+ Failure.create \
177
+ :payload => payload,
178
+ :exception => exception,
179
+ :worker => worker,
180
+ :queue => queue
181
+ end
182
+
183
+ # Creates an identical job, essentially placing this job back on
184
+ # the queue.
185
+ def recreate
186
+ self.class.create(queue, payload_class, *args)
187
+ end
188
+
189
+ # String representation
190
+ def inspect
191
+ obj = @payload
192
+ "(Job{%s} | %s | %s)" % [ @queue, obj['class'], obj['args'].inspect ]
193
+ end
194
+
195
+ # Equality
196
+ def ==(other)
197
+ queue == other.queue &&
198
+ payload_class == other.payload_class &&
199
+ args == other.args
200
+ end
201
+
202
+ def before_hooks
203
+ @before_hooks ||= Plugin.before_hooks(payload_class)
204
+ end
205
+
206
+ def around_hooks
207
+ @around_hooks ||= Plugin.around_hooks(payload_class)
208
+ end
209
+
210
+ def after_hooks
211
+ @after_hooks ||= Plugin.after_hooks(payload_class)
212
+ end
213
+
214
+ def failure_hooks
215
+ @failure_hooks ||= Plugin.failure_hooks(payload_class)
216
+ end
217
+
218
+ def run_failure_hooks(exception)
219
+ begin
220
+ job_args = args || []
221
+ failure_hooks.each { |hook| payload_class.send(hook, exception, *job_args) } unless @failure_hooks_ran
222
+ ensure
223
+ @failure_hooks_ran = true
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,66 @@
1
+ module Resque
2
+ module Plugin
3
+ extend self
4
+
5
+ LintError = Class.new(RuntimeError)
6
+
7
+ # Ensure that your plugin conforms to good hook naming conventions.
8
+ #
9
+ # Resque::Plugin.lint(MyResquePlugin)
10
+ def lint(plugin)
11
+ hooks = before_hooks(plugin) + around_hooks(plugin) + after_hooks(plugin)
12
+
13
+ hooks.each do |hook|
14
+ if hook =~ /perform$/
15
+ raise LintError, "#{plugin}.#{hook} is not namespaced"
16
+ end
17
+ end
18
+
19
+ failure_hooks(plugin).each do |hook|
20
+ if hook =~ /failure$/
21
+ raise LintError, "#{plugin}.#{hook} is not namespaced"
22
+ end
23
+ end
24
+ end
25
+
26
+ # Given an object, returns a list `before_perform` hook names.
27
+ def before_hooks(job)
28
+ job.methods.grep(/^before_perform/).sort
29
+ end
30
+
31
+ # Given an object, returns a list `around_perform` hook names.
32
+ def around_hooks(job)
33
+ job.methods.grep(/^around_perform/).sort
34
+ end
35
+
36
+ # Given an object, returns a list `after_perform` hook names.
37
+ def after_hooks(job)
38
+ job.methods.grep(/^after_perform/).sort
39
+ end
40
+
41
+ # Given an object, returns a list `on_failure` hook names.
42
+ def failure_hooks(job)
43
+ job.methods.grep(/^on_failure/).sort
44
+ end
45
+
46
+ # Given an object, returns a list `after_enqueue` hook names.
47
+ def after_enqueue_hooks(job)
48
+ job.methods.grep(/^after_enqueue/).sort
49
+ end
50
+
51
+ # Given an object, returns a list `before_enqueue` hook names.
52
+ def before_enqueue_hooks(job)
53
+ job.methods.grep(/^before_enqueue/).sort
54
+ end
55
+
56
+ # Given an object, returns a list `after_dequeue` hook names.
57
+ def after_dequeue_hooks(job)
58
+ job.methods.grep(/^after_dequeue/).sort
59
+ end
60
+
61
+ # Given an object, returns a list `before_dequeue` hook names.
62
+ def before_dequeue_hooks(job)
63
+ job.methods.grep(/^before_dequeue/).sort
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,248 @@
1
+ require 'sinatra/base'
2
+ require 'erb'
3
+ require 'resque'
4
+ require 'resque/version'
5
+ require 'time'
6
+
7
+ if defined? Encoding
8
+ Encoding.default_external = Encoding::UTF_8
9
+ end
10
+
11
+ module Resque
12
+ class Server < Sinatra::Base
13
+ dir = File.dirname(File.expand_path(__FILE__))
14
+
15
+ set :views, "#{dir}/server/views"
16
+
17
+ if respond_to? :public_folder
18
+ set :public_folder, "#{dir}/server/public"
19
+ else
20
+ set :public, "#{dir}/server/public"
21
+ end
22
+
23
+ set :static, true
24
+
25
+ helpers do
26
+ include Rack::Utils
27
+ alias_method :h, :escape_html
28
+
29
+ def current_section
30
+ url_path request.path_info.sub('/','').split('/')[0].downcase
31
+ end
32
+
33
+ def current_page
34
+ url_path request.path_info.sub('/','')
35
+ end
36
+
37
+ def url_path(*path_parts)
38
+ [ path_prefix, path_parts ].join("/").squeeze('/')
39
+ end
40
+ alias_method :u, :url_path
41
+
42
+ def path_prefix
43
+ request.env['SCRIPT_NAME']
44
+ end
45
+
46
+ def class_if_current(path = '')
47
+ 'class="current"' if current_page[0, path.size] == path
48
+ end
49
+
50
+ def tab(name)
51
+ dname = name.to_s.downcase
52
+ path = url_path(dname)
53
+ "<li #{class_if_current(path)}><a href='#{path}'>#{name}</a></li>"
54
+ end
55
+
56
+ def tabs
57
+ Resque::Server.tabs
58
+ end
59
+
60
+ def redis_get_size(key)
61
+ case Resque.redis.type(key)
62
+ when 'none'
63
+ []
64
+ when 'list'
65
+ Resque.redis.llen(key)
66
+ when 'set'
67
+ Resque.redis.scard(key)
68
+ when 'string'
69
+ Resque.redis.get(key).length
70
+ when 'zset'
71
+ Resque.redis.zcard(key)
72
+ end
73
+ end
74
+
75
+ def redis_get_value_as_array(key, start=0)
76
+ case Resque.redis.type(key)
77
+ when 'none'
78
+ []
79
+ when 'list'
80
+ Resque.redis.lrange(key, start, start + 20)
81
+ when 'set'
82
+ Resque.redis.smembers(key)[start..(start + 20)]
83
+ when 'string'
84
+ [Resque.redis.get(key)]
85
+ when 'zset'
86
+ Resque.redis.zrange(key, start, start + 20)
87
+ end
88
+ end
89
+
90
+ def show_args(args)
91
+ Array(args).map { |a| a.inspect }.join("\n")
92
+ end
93
+
94
+ def worker_hosts
95
+ @worker_hosts ||= worker_hosts!
96
+ end
97
+
98
+ def worker_hosts!
99
+ hosts = Hash.new { [] }
100
+
101
+ Resque.workers.each do |worker|
102
+ host, _ = worker.to_s.split(':')
103
+ hosts[host] += [worker.to_s]
104
+ end
105
+
106
+ hosts
107
+ end
108
+
109
+ def partial?
110
+ @partial
111
+ end
112
+
113
+ def partial(template, local_vars = {})
114
+ @partial = true
115
+ erb(template.to_sym, {:layout => false}, local_vars)
116
+ ensure
117
+ @partial = false
118
+ end
119
+
120
+ def poll
121
+ if @polling
122
+ text = "Last Updated: #{Time.now.strftime("%H:%M:%S")}"
123
+ else
124
+ text = "<a href='#{u(request.path_info)}.poll' rel='poll'>Live Poll</a>"
125
+ end
126
+ "<p class='poll'>#{text}</p>"
127
+ end
128
+
129
+ end
130
+
131
+ def show(page, layout = true)
132
+ response["Cache-Control"] = "max-age=0, private, must-revalidate"
133
+ begin
134
+ erb page.to_sym, {:layout => layout}, :resque => Resque
135
+ rescue Errno::ECONNREFUSED
136
+ erb :error, {:layout => false}, :error => "Can't connect to Redis! (#{Resque.redis_id})"
137
+ end
138
+ end
139
+
140
+ def show_for_polling(page)
141
+ content_type "text/html"
142
+ @polling = true
143
+ show(page.to_sym, false).gsub(/\s{1,}/, ' ')
144
+ end
145
+
146
+ # to make things easier on ourselves
147
+ get "/?" do
148
+ redirect url_path(:overview)
149
+ end
150
+
151
+ %w( overview workers ).each do |page|
152
+ get "/#{page}.poll/?" do
153
+ show_for_polling(page)
154
+ end
155
+
156
+ get "/#{page}/:id.poll/?" do
157
+ show_for_polling(page)
158
+ end
159
+ end
160
+
161
+ %w( overview queues working workers key ).each do |page|
162
+ get "/#{page}/?" do
163
+ show page
164
+ end
165
+
166
+ get "/#{page}/:id/?" do
167
+ show page
168
+ end
169
+ end
170
+
171
+ post "/queues/:id/remove" do
172
+ Resque.remove_queue(params[:id])
173
+ redirect u('queues')
174
+ end
175
+
176
+ get "/failed/?" do
177
+ if Resque::Failure.url
178
+ redirect Resque::Failure.url
179
+ else
180
+ show :failed
181
+ end
182
+ end
183
+
184
+ post "/failed/clear" do
185
+ Resque::Failure.clear
186
+ redirect u('failed')
187
+ end
188
+
189
+ post "/failed/requeue/all" do
190
+ Resque::Failure.count.times do |num|
191
+ Resque::Failure.requeue(num)
192
+ end
193
+ redirect u('failed')
194
+ end
195
+
196
+ get "/failed/requeue/:index/?" do
197
+ Resque::Failure.requeue(params[:index])
198
+ if request.xhr?
199
+ return Resque::Failure.all(params[:index])['retried_at']
200
+ else
201
+ redirect u('failed')
202
+ end
203
+ end
204
+
205
+ get "/failed/remove/:index/?" do
206
+ Resque::Failure.remove(params[:index])
207
+ redirect u('failed')
208
+ end
209
+
210
+ get "/stats/?" do
211
+ redirect url_path("/stats/resque")
212
+ end
213
+
214
+ get "/stats/:id/?" do
215
+ show :stats
216
+ end
217
+
218
+ get "/stats/keys/:key/?" do
219
+ show :stats
220
+ end
221
+
222
+ get "/stats.txt/?" do
223
+ info = Resque.info
224
+
225
+ stats = []
226
+ stats << "resque.pending=#{info[:pending]}"
227
+ stats << "resque.processed+=#{info[:processed]}"
228
+ stats << "resque.failed+=#{info[:failed]}"
229
+ stats << "resque.workers=#{info[:workers]}"
230
+ stats << "resque.working=#{info[:working]}"
231
+
232
+ Resque.queues.each do |queue|
233
+ stats << "queues.#{queue}=#{Resque.size(queue)}"
234
+ end
235
+
236
+ content_type 'text/html'
237
+ stats.join "\n"
238
+ end
239
+
240
+ def resque
241
+ Resque
242
+ end
243
+
244
+ def self.tabs
245
+ @tabs ||= ["Overview", "Working", "Failed", "Queues", "Workers", "Stats"]
246
+ end
247
+ end
248
+ end