steini-resque 1.18.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/HISTORY.md +322 -0
  2. data/LICENSE +20 -0
  3. data/README.markdown +881 -0
  4. data/Rakefile +78 -0
  5. data/bin/resque +81 -0
  6. data/bin/resque-web +23 -0
  7. data/lib/resque.rb +352 -0
  8. data/lib/resque/errors.rb +10 -0
  9. data/lib/resque/failure.rb +70 -0
  10. data/lib/resque/failure/base.rb +64 -0
  11. data/lib/resque/failure/hoptoad.rb +48 -0
  12. data/lib/resque/failure/multiple.rb +54 -0
  13. data/lib/resque/failure/redis.rb +51 -0
  14. data/lib/resque/helpers.rb +63 -0
  15. data/lib/resque/job.rb +205 -0
  16. data/lib/resque/plugin.rb +56 -0
  17. data/lib/resque/server.rb +231 -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 +85 -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 +64 -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 +44 -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 +49 -0
  36. data/lib/resque/server/views/stats.erb +62 -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/stat.rb +53 -0
  40. data/lib/resque/tasks.rb +51 -0
  41. data/lib/resque/version.rb +3 -0
  42. data/lib/resque/worker.rb +533 -0
  43. data/lib/tasks/redis.rake +161 -0
  44. data/lib/tasks/resque.rake +2 -0
  45. data/test/hoptoad_test.rb +25 -0
  46. data/test/job_hooks_test.rb +363 -0
  47. data/test/job_plugins_test.rb +230 -0
  48. data/test/plugin_test.rb +116 -0
  49. data/test/redis-test.conf +115 -0
  50. data/test/resque-web_test.rb +53 -0
  51. data/test/resque_test.rb +259 -0
  52. data/test/test_helper.rb +148 -0
  53. data/test/worker_test.rb +332 -0
  54. metadata +183 -0
@@ -0,0 +1,10 @@
1
+ module Resque
2
+ # Raised whenever we need a queue but none is provided.
3
+ class NoQueueError < RuntimeError; end
4
+
5
+ # Raised when trying to create a job without a class
6
+ class NoClassError < RuntimeError; end
7
+
8
+ # Raised when a worker was killed while processing a job.
9
+ class DirtyExit < RuntimeError; end
10
+ end
@@ -0,0 +1,70 @@
1
+ module Resque
2
+ # The Failure module provides an interface for working with different
3
+ # failure backends.
4
+ #
5
+ # You can use it to query the failure backend without knowing which specific
6
+ # backend is being used. For instance, the Resque web app uses it to display
7
+ # stats and other information.
8
+ module Failure
9
+ # Creates a new failure, which is delegated to the appropriate backend.
10
+ #
11
+ # Expects a hash with the following keys:
12
+ # :exception - The Exception object
13
+ # :worker - The Worker object who is reporting the failure
14
+ # :queue - The string name of the queue from which the job was pulled
15
+ # :payload - The job's payload
16
+ def self.create(options = {})
17
+ backend.new(*options.values_at(:exception, :worker, :queue, :payload)).save
18
+ end
19
+
20
+ #
21
+ # Sets the current backend. Expects a class descendent of
22
+ # `Resque::Failure::Base`.
23
+ #
24
+ # Example use:
25
+ # require 'resque/failure/hoptoad'
26
+ # Resque::Failure.backend = Resque::Failure::Hoptoad
27
+ def self.backend=(backend)
28
+ @backend = backend
29
+ end
30
+
31
+ # Returns the current backend class. If none has been set, falls
32
+ # back to `Resque::Failure::Redis`
33
+ def self.backend
34
+ return @backend if @backend
35
+ require 'resque/failure/redis'
36
+ @backend = Failure::Redis
37
+ end
38
+
39
+ # Returns the int count of how many failures we have seen.
40
+ def self.count
41
+ backend.count
42
+ end
43
+
44
+ # Returns an array of all the failures, paginated.
45
+ #
46
+ # `start` is the int of the first item in the page, `count` is the
47
+ # number of items to return.
48
+ def self.all(start = 0, count = 1)
49
+ backend.all(start, count)
50
+ end
51
+
52
+ # The string url of the backend's web interface, if any.
53
+ def self.url
54
+ backend.url
55
+ end
56
+
57
+ # Clear all failure jobs
58
+ def self.clear
59
+ backend.clear
60
+ end
61
+
62
+ def self.requeue(index)
63
+ backend.requeue(index)
64
+ end
65
+
66
+ def self.remove(index)
67
+ backend.remove(index)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,64 @@
1
+ module Resque
2
+ module Failure
3
+ # All Failure classes are expected to subclass Base.
4
+ #
5
+ # When a job fails, a new instance of your Failure backend is created
6
+ # and #save is called.
7
+ class Base
8
+ # The exception object raised by the failed job
9
+ attr_accessor :exception
10
+
11
+ # The worker object who detected the failure
12
+ attr_accessor :worker
13
+
14
+ # The string name of the queue from which the failed job was pulled
15
+ attr_accessor :queue
16
+
17
+ # The payload object associated with the failed job
18
+ attr_accessor :payload
19
+
20
+ def initialize(exception, worker, queue, payload)
21
+ @exception = exception
22
+ @worker = worker
23
+ @queue = queue
24
+ @payload = payload
25
+ end
26
+
27
+ # When a job fails, a new instance of your Failure backend is created
28
+ # and #save is called.
29
+ #
30
+ # This is where you POST or PUT or whatever to your Failure service.
31
+ def save
32
+ end
33
+
34
+ # The number of failures.
35
+ def self.count
36
+ 0
37
+ end
38
+
39
+ # Returns a paginated array of failure objects.
40
+ def self.all(start = 0, count = 1)
41
+ []
42
+ end
43
+
44
+ # A URL where someone can go to view failures.
45
+ def self.url
46
+ end
47
+
48
+ # Clear all failure objects
49
+ def self.clear
50
+ end
51
+
52
+ def self.requeue(index)
53
+ end
54
+
55
+ def self.remove(index)
56
+ end
57
+
58
+ # Logging!
59
+ def log(message)
60
+ @worker.log(message)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,48 @@
1
+ begin
2
+ require 'hoptoad_notifier'
3
+ rescue LoadError
4
+ raise "Can't find 'hoptoad_notifier' gem. Please add it to your Gemfile or install it."
5
+ end
6
+
7
+ module Resque
8
+ module Failure
9
+ # A Failure backend that sends exceptions raised by jobs to Hoptoad.
10
+ #
11
+ # To use it, put this code in an initializer, Rake task, or wherever:
12
+ #
13
+ # require 'resque/failure/hoptoad'
14
+ #
15
+ # Resque::Failure::Multiple.classes = [Resque::Failure::Redis, Resque::Failure::Hoptoad]
16
+ # Resque::Failure.backend = Resque::Failure::Multiple
17
+ #
18
+ # Once you've configured resque to use the Hoptoad failure backend,
19
+ # you'll want to setup an initializer to configure the Hoptoad.
20
+ #
21
+ # HoptoadNotifier.configure do |config|
22
+ # config.api_key = 'your_key_here'
23
+ # end
24
+ # For more information see https://github.com/thoughtbot/hoptoad_notifier
25
+ class Hoptoad < Base
26
+ def self.configure(&block)
27
+ Resque::Failure.backend = self
28
+ HoptoadNotifier.configure(&block)
29
+ end
30
+
31
+ def self.count
32
+ # We can't get the total # of errors from Hoptoad so we fake it
33
+ # by asking Resque how many errors it has seen.
34
+ Stat[:failed]
35
+ end
36
+
37
+ def save
38
+ HoptoadNotifier.notify_or_ignore(exception,
39
+ :parameters => {
40
+ :payload_class => payload['class'].to_s,
41
+ :payload_args => payload['args'].inspect
42
+ }
43
+ )
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,54 @@
1
+ module Resque
2
+ module Failure
3
+ # A Failure backend that uses multiple backends
4
+ # delegates all queries to the first backend
5
+ class Multiple < Base
6
+
7
+ class << self
8
+ attr_accessor :classes
9
+ end
10
+
11
+ def self.configure
12
+ yield self
13
+ Resque::Failure.backend = self
14
+ end
15
+
16
+ def initialize(*args)
17
+ super
18
+ @backends = self.class.classes.map {|klass| klass.new(*args)}
19
+ end
20
+
21
+ def save
22
+ @backends.each(&:save)
23
+ end
24
+
25
+ # The number of failures.
26
+ def self.count
27
+ classes.first.count
28
+ end
29
+
30
+ # Returns a paginated array of failure objects.
31
+ def self.all(start = 0, count = 1)
32
+ classes.first.all(start,count)
33
+ end
34
+
35
+ # A URL where someone can go to view failures.
36
+ def self.url
37
+ classes.first.url
38
+ end
39
+
40
+ # Clear all failure objects
41
+ def self.clear
42
+ classes.first.clear
43
+ end
44
+
45
+ def self.requeue(*args)
46
+ classes.first.requeue(*args)
47
+ end
48
+
49
+ def self.remove(index)
50
+ classes.each { |klass| klass.remove(index) }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,51 @@
1
+ module Resque
2
+ module Failure
3
+ # A Failure backend that stores exceptions in Redis. Very simple but
4
+ # works out of the box, along with support in the Resque web app.
5
+ class Redis < Base
6
+ def save
7
+ data = {
8
+ :failed_at => Time.now.strftime("%Y/%m/%d %H:%M:%S %Z"),
9
+ :payload => payload,
10
+ :exception => exception.class.to_s,
11
+ :error => exception.to_s,
12
+ :backtrace => filter_backtrace(Array(exception.backtrace)),
13
+ :worker => worker.to_s,
14
+ :queue => queue
15
+ }
16
+ data = Resque.encode(data)
17
+ Resque.redis.rpush(:failed, data)
18
+ end
19
+
20
+ def self.count
21
+ Resque.redis.llen(:failed).to_i
22
+ end
23
+
24
+ def self.all(start = 0, count = 1)
25
+ Resque.list_range(:failed, start, count)
26
+ end
27
+
28
+ def self.clear
29
+ Resque.redis.del(:failed)
30
+ end
31
+
32
+ def self.requeue(index)
33
+ item = all(index)
34
+ item['retried_at'] = Time.now.strftime("%Y/%m/%d %H:%M:%S")
35
+ Resque.redis.lset(:failed, index, Resque.encode(item))
36
+ Job.create(item['queue'], item['payload']['class'], *item['payload']['args'])
37
+ end
38
+
39
+ def self.remove(index)
40
+ id = rand(0xffffff)
41
+ Resque.redis.lset(:failed, index, id)
42
+ Resque.redis.lrem(:failed, 1, id)
43
+ end
44
+
45
+ def filter_backtrace(backtrace)
46
+ index = backtrace.index { |item| item.include?('/lib/resque/job.rb') }
47
+ backtrace.first(index.to_i)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ require 'multi_json'
2
+
3
+ # OkJson won't work because it doesn't serialize symbols
4
+ # in the same way yajl and json do.
5
+ if MultiJson.engine.to_s == 'MultiJson::Engines::OkJson'
6
+ raise "Please install the yajl-ruby or json gem"
7
+ end
8
+
9
+ module Resque
10
+ # Methods used by various classes in Resque.
11
+ module Helpers
12
+ class DecodeException < StandardError; end
13
+
14
+ # Direct access to the Redis instance.
15
+ def redis
16
+ Resque.redis
17
+ end
18
+
19
+ # Given a Ruby object, returns a string suitable for storage in a
20
+ # queue.
21
+ def encode(object)
22
+ ::MultiJson.encode(object)
23
+ end
24
+
25
+ # Given a string, returns a Ruby object.
26
+ def decode(object)
27
+ return unless object
28
+
29
+ begin
30
+ ::MultiJson.decode(object)
31
+ rescue ::MultiJson::DecodeError => e
32
+ raise DecodeException, e
33
+ end
34
+ end
35
+
36
+ # Given a word with dashes, returns a camel cased version of it.
37
+ #
38
+ # classify('job-name') # => 'JobName'
39
+ def classify(dashed_word)
40
+ dashed_word.split('-').each { |part| part[0] = part[0].chr.upcase }.join
41
+ end
42
+
43
+ # Given a camel cased word, returns the constant it represents
44
+ #
45
+ # constantize('JobName') # => JobName
46
+ def constantize(camel_cased_word)
47
+ camel_cased_word = camel_cased_word.to_s
48
+
49
+ if camel_cased_word.include?('-')
50
+ camel_cased_word = classify(camel_cased_word)
51
+ end
52
+
53
+ names = camel_cased_word.split('::')
54
+ names.shift if names.empty? || names.first.empty?
55
+
56
+ constant = Object
57
+ names.each do |name|
58
+ constant = constant.const_get(name) || constant.const_missing(name)
59
+ end
60
+ constant
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,205 @@
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
+ end
36
+
37
+ # Creates a job by placing it on a queue. Expects a string queue
38
+ # name, a string class name, and an optional array of arguments to
39
+ # pass to the class' `perform` method.
40
+ #
41
+ # Raises an exception if no queue or class is given.
42
+ def self.create(queue, klass, *args)
43
+ Resque.validate(klass, queue)
44
+
45
+ if Resque.inline?
46
+ constantize(klass).perform(*decode(encode(args)))
47
+ else
48
+ Resque.push(queue, :class => klass.to_s, :args => args)
49
+ end
50
+ end
51
+
52
+ # Removes a job from a queue. Expects a string queue name, a
53
+ # string class name, and, optionally, args.
54
+ #
55
+ # Returns the number of jobs destroyed.
56
+ #
57
+ # If no args are provided, it will remove all jobs of the class
58
+ # provided.
59
+ #
60
+ # That is, for these two jobs:
61
+ #
62
+ # { 'class' => 'UpdateGraph', 'args' => ['defunkt'] }
63
+ # { 'class' => 'UpdateGraph', 'args' => ['mojombo'] }
64
+ #
65
+ # The following call will remove both:
66
+ #
67
+ # Resque::Job.destroy(queue, 'UpdateGraph')
68
+ #
69
+ # Whereas specifying args will only remove the 2nd job:
70
+ #
71
+ # Resque::Job.destroy(queue, 'UpdateGraph', 'mojombo')
72
+ #
73
+ # This method can be potentially very slow and memory intensive,
74
+ # depending on the size of your queue, as it loads all jobs into
75
+ # a Ruby array before processing.
76
+ def self.destroy(queue, klass, *args)
77
+ klass = klass.to_s
78
+ queue = "queue:#{queue}"
79
+ destroyed = 0
80
+
81
+ if args.empty?
82
+ redis.lrange(queue, 0, -1).each do |string|
83
+ if decode(string)['class'] == klass
84
+ destroyed += redis.lrem(queue, 0, string).to_i
85
+ end
86
+ end
87
+ else
88
+ destroyed += redis.lrem(queue, 0, encode(:class => klass, :args => args))
89
+ end
90
+
91
+ destroyed
92
+ end
93
+
94
+ # Given a string queue name, returns an instance of Resque::Job
95
+ # if any jobs are available. If not, returns nil.
96
+ def self.reserve(queue)
97
+ return unless payload = Resque.pop(queue)
98
+ new(queue, payload)
99
+ end
100
+
101
+ # Attempts to perform the work represented by this job instance.
102
+ # Calls #perform on the class given in the payload with the
103
+ # arguments given in the payload.
104
+ def perform
105
+ job = payload_class
106
+ job_args = args || []
107
+ job_was_performed = false
108
+
109
+ before_hooks = Plugin.before_hooks(job)
110
+ around_hooks = Plugin.around_hooks(job)
111
+ after_hooks = Plugin.after_hooks(job)
112
+ failure_hooks = Plugin.failure_hooks(job)
113
+
114
+ begin
115
+ # Execute before_perform hook. Abort the job gracefully if
116
+ # Resque::DontPerform is raised.
117
+ begin
118
+ before_hooks.each do |hook|
119
+ job.send(hook, *job_args)
120
+ end
121
+ rescue DontPerform
122
+ return false
123
+ end
124
+
125
+ # Execute the job. Do it in an around_perform hook if available.
126
+ if around_hooks.empty?
127
+ job.perform(*job_args)
128
+ job_was_performed = true
129
+ else
130
+ # We want to nest all around_perform plugins, with the last one
131
+ # finally calling perform
132
+ stack = around_hooks.reverse.inject(nil) do |last_hook, hook|
133
+ if last_hook
134
+ lambda do
135
+ job.send(hook, *job_args) { last_hook.call }
136
+ end
137
+ else
138
+ lambda do
139
+ job.send(hook, *job_args) do
140
+ result = job.perform(*job_args)
141
+ job_was_performed = true
142
+ result
143
+ end
144
+ end
145
+ end
146
+ end
147
+ stack.call
148
+ end
149
+
150
+ # Execute after_perform hook
151
+ after_hooks.each do |hook|
152
+ job.send(hook, *job_args)
153
+ end
154
+
155
+ # Return true if the job was performed
156
+ return job_was_performed
157
+
158
+ # If an exception occurs during the job execution, look for an
159
+ # on_failure hook then re-raise.
160
+ rescue Object => e
161
+ failure_hooks.each { |hook| job.send(hook, e, *job_args) }
162
+ raise e
163
+ end
164
+ end
165
+
166
+ # Returns the actual class constant represented in this job's payload.
167
+ def payload_class
168
+ @payload_class ||= constantize(@payload['class'])
169
+ end
170
+
171
+ # Returns an array of args represented in this job's payload.
172
+ def args
173
+ @payload['args']
174
+ end
175
+
176
+ # Given an exception object, hands off the needed parameters to
177
+ # the Failure module.
178
+ def fail(exception)
179
+ Failure.create \
180
+ :payload => payload,
181
+ :exception => exception,
182
+ :worker => worker,
183
+ :queue => queue
184
+ end
185
+
186
+ # Creates an identical job, essentially placing this job back on
187
+ # the queue.
188
+ def recreate
189
+ self.class.create(queue, payload_class, *args)
190
+ end
191
+
192
+ # String representation
193
+ def inspect
194
+ obj = @payload
195
+ "(Job{%s} | %s | %s)" % [ @queue, obj['class'], obj['args'].inspect ]
196
+ end
197
+
198
+ # Equality
199
+ def ==(other)
200
+ queue == other.queue &&
201
+ payload_class == other.payload_class &&
202
+ args == other.args
203
+ end
204
+ end
205
+ end