tr_resque 1.20.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.
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