resque-cedar 1.20.0

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 (64) 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 +385 -0
  8. data/lib/resque/coder.rb +27 -0
  9. data/lib/resque/errors.rb +10 -0
  10. data/lib/resque/failure.rb +96 -0
  11. data/lib/resque/failure/airbrake.rb +17 -0
  12. data/lib/resque/failure/base.rb +64 -0
  13. data/lib/resque/failure/hoptoad.rb +33 -0
  14. data/lib/resque/failure/multiple.rb +54 -0
  15. data/lib/resque/failure/redis.rb +51 -0
  16. data/lib/resque/failure/thoughtbot.rb +33 -0
  17. data/lib/resque/helpers.rb +64 -0
  18. data/lib/resque/job.rb +223 -0
  19. data/lib/resque/multi_json_coder.rb +37 -0
  20. data/lib/resque/multi_queue.rb +73 -0
  21. data/lib/resque/plugin.rb +66 -0
  22. data/lib/resque/queue.rb +117 -0
  23. data/lib/resque/server.rb +248 -0
  24. data/lib/resque/server/public/favicon.ico +0 -0
  25. data/lib/resque/server/public/idle.png +0 -0
  26. data/lib/resque/server/public/jquery-1.3.2.min.js +19 -0
  27. data/lib/resque/server/public/jquery.relatize_date.js +95 -0
  28. data/lib/resque/server/public/poll.png +0 -0
  29. data/lib/resque/server/public/ranger.js +73 -0
  30. data/lib/resque/server/public/reset.css +44 -0
  31. data/lib/resque/server/public/style.css +86 -0
  32. data/lib/resque/server/public/working.png +0 -0
  33. data/lib/resque/server/test_helper.rb +19 -0
  34. data/lib/resque/server/views/error.erb +1 -0
  35. data/lib/resque/server/views/failed.erb +67 -0
  36. data/lib/resque/server/views/key_sets.erb +19 -0
  37. data/lib/resque/server/views/key_string.erb +11 -0
  38. data/lib/resque/server/views/layout.erb +44 -0
  39. data/lib/resque/server/views/next_more.erb +10 -0
  40. data/lib/resque/server/views/overview.erb +4 -0
  41. data/lib/resque/server/views/queues.erb +49 -0
  42. data/lib/resque/server/views/stats.erb +62 -0
  43. data/lib/resque/server/views/workers.erb +109 -0
  44. data/lib/resque/server/views/working.erb +72 -0
  45. data/lib/resque/stat.rb +53 -0
  46. data/lib/resque/tasks.rb +61 -0
  47. data/lib/resque/version.rb +3 -0
  48. data/lib/resque/worker.rb +557 -0
  49. data/lib/tasks/redis.rake +161 -0
  50. data/lib/tasks/resque.rake +2 -0
  51. data/test/airbrake_test.rb +26 -0
  52. data/test/hoptoad_test.rb +26 -0
  53. data/test/job_hooks_test.rb +423 -0
  54. data/test/job_plugins_test.rb +230 -0
  55. data/test/multi_queue_test.rb +95 -0
  56. data/test/plugin_test.rb +116 -0
  57. data/test/redis-test-cluster.conf +115 -0
  58. data/test/redis-test.conf +115 -0
  59. data/test/redis_queue_test.rb +133 -0
  60. data/test/resque-web_test.rb +59 -0
  61. data/test/resque_test.rb +284 -0
  62. data/test/test_helper.rb +135 -0
  63. data/test/worker_test.rb +443 -0
  64. metadata +188 -0
@@ -0,0 +1,223 @@
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
+ begin
110
+ # Execute before_perform hook. Abort the job gracefully if
111
+ # Resque::DontPerform is raised.
112
+ begin
113
+ before_hooks.each do |hook|
114
+ job.send(hook, *job_args)
115
+ end
116
+ rescue DontPerform
117
+ return false
118
+ end
119
+
120
+ # Execute the job. Do it in an around_perform hook if available.
121
+ if around_hooks.empty?
122
+ job.perform(*job_args)
123
+ job_was_performed = true
124
+ else
125
+ # We want to nest all around_perform plugins, with the last one
126
+ # finally calling perform
127
+ stack = around_hooks.reverse.inject(nil) do |last_hook, hook|
128
+ if last_hook
129
+ lambda do
130
+ job.send(hook, *job_args) { last_hook.call }
131
+ end
132
+ else
133
+ lambda do
134
+ job.send(hook, *job_args) do
135
+ result = job.perform(*job_args)
136
+ job_was_performed = true
137
+ result
138
+ end
139
+ end
140
+ end
141
+ end
142
+ stack.call
143
+ end
144
+
145
+ # Execute after_perform hook
146
+ after_hooks.each do |hook|
147
+ job.send(hook, *job_args)
148
+ end
149
+
150
+ # Return true if the job was performed
151
+ return job_was_performed
152
+
153
+ # If an exception occurs during the job execution, look for an
154
+ # on_failure hook then re-raise.
155
+ rescue Object => e
156
+ run_failure_hooks(e)
157
+ raise e
158
+ end
159
+ end
160
+
161
+ # Returns the actual class constant represented in this job's payload.
162
+ def payload_class
163
+ @payload_class ||= constantize(@payload['class'])
164
+ end
165
+
166
+ # Returns an array of args represented in this job's payload.
167
+ def args
168
+ @payload['args']
169
+ end
170
+
171
+ # Given an exception object, hands off the needed parameters to
172
+ # the Failure module.
173
+ def fail(exception)
174
+ run_failure_hooks(exception)
175
+ Failure.create \
176
+ :payload => payload,
177
+ :exception => exception,
178
+ :worker => worker,
179
+ :queue => queue
180
+ end
181
+
182
+ # Creates an identical job, essentially placing this job back on
183
+ # the queue.
184
+ def recreate
185
+ self.class.create(queue, payload_class, *args)
186
+ end
187
+
188
+ # String representation
189
+ def inspect
190
+ obj = @payload
191
+ "(Job{%s} | %s | %s)" % [ @queue, obj['class'], obj['args'].inspect ]
192
+ end
193
+
194
+ # Equality
195
+ def ==(other)
196
+ queue == other.queue &&
197
+ payload_class == other.payload_class &&
198
+ args == other.args
199
+ end
200
+
201
+ def before_hooks
202
+ @before_hooks ||= Plugin.before_hooks(payload_class)
203
+ end
204
+
205
+ def around_hooks
206
+ @around_hooks ||= Plugin.around_hooks(payload_class)
207
+ end
208
+
209
+ def after_hooks
210
+ @after_hooks ||= Plugin.after_hooks(payload_class)
211
+ end
212
+
213
+ def failure_hooks
214
+ @failure_hooks ||= Plugin.failure_hooks(payload_class)
215
+ end
216
+
217
+ def run_failure_hooks(exception)
218
+ job_args = args || []
219
+ failure_hooks.each { |hook| payload_class.send(hook, exception, *job_args) }
220
+ end
221
+
222
+ end
223
+ end
@@ -0,0 +1,37 @@
1
+ require 'multi_json'
2
+ require 'resque/coder'
3
+
4
+ # OkJson won't work because it doesn't serialize symbols
5
+ # in the same way yajl and json do.
6
+
7
+ if MultiJson.respond_to?(:adapter)
8
+ raise "Please install the yajl-ruby or json gem" if MultiJson.adapter.to_s == 'MultiJson::Adapters::OkJson'
9
+ elsif MultiJson.respond_to?(:engine)
10
+ raise "Please install the yajl-ruby or json gem" if MultiJson.engine.to_s == 'MultiJson::Engines::OkJson'
11
+ end
12
+
13
+ module Resque
14
+ class MultiJsonCoder < Coder
15
+ def encode(object)
16
+ if MultiJson.respond_to?(:dump) && MultiJson.respond_to?(:load)
17
+ MultiJson.dump object
18
+ else
19
+ MultiJson.encode object
20
+ end
21
+ end
22
+
23
+ def decode(object)
24
+ return unless object
25
+
26
+ begin
27
+ if MultiJson.respond_to?(:dump) && MultiJson.respond_to?(:load)
28
+ MultiJson.load object
29
+ else
30
+ MultiJson.decode object
31
+ end
32
+ rescue ::MultiJson::DecodeError => e
33
+ raise DecodeException, e.message, e.backtrace
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,73 @@
1
+ require 'redis'
2
+ require 'redis-namespace'
3
+ require 'thread'
4
+ require 'mutex_m'
5
+
6
+ module Resque
7
+ ###
8
+ # Holds multiple queues, allowing you to pop the first available job
9
+ class MultiQueue
10
+ include Mutex_m
11
+
12
+ ###
13
+ # Create a new MultiQueue using the +queues+ from the +redis+ connection
14
+ def initialize(queues, redis)
15
+ super()
16
+
17
+ @queues = queues # since ruby 1.8 doesn't have Ordered Hashes
18
+ @queue_hash = {}
19
+ @redis = redis
20
+
21
+ queues.each do |queue|
22
+ key = @redis.is_a?(Redis::Namespace) ? "#{@redis.namespace}:" : ""
23
+ key += queue.redis_name
24
+ @queue_hash[key] = queue
25
+ end
26
+ end
27
+
28
+ # Pop an item off one of the queues. This method will block until an item
29
+ # is available. This method returns a tuple of the queue object and job.
30
+ #
31
+ # Pass +true+ for a non-blocking pop. If nothing is read on a non-blocking
32
+ # pop, a ThreadError is raised.
33
+ def pop(non_block = false)
34
+ if non_block
35
+ synchronize do
36
+ value = nil
37
+
38
+ @queues.each do |queue|
39
+ begin
40
+ return [queue, queue.pop(true)]
41
+ rescue ThreadError
42
+ end
43
+ end
44
+
45
+ raise ThreadError
46
+ end
47
+ else
48
+ queue_names = @queues.map {|queue| queue.redis_name }
49
+ synchronize do
50
+ value = @redis.blpop(*(queue_names + [1])) until value
51
+ queue_name, payload = value
52
+ queue = @queue_hash[queue_name]
53
+ [queue, queue.decode(payload)]
54
+ end
55
+ end
56
+ end
57
+
58
+ # Retrieves data from the queue head, and removes it.
59
+ #
60
+ # Blocks for +timeout+ seconds if the queue is empty, and returns nil if
61
+ # the timeout expires.
62
+ def poll(timeout)
63
+ queue_names = @queues.map {|queue| queue.redis_name }
64
+ queue_name, payload = @redis.blpop(*(queue_names + [timeout]))
65
+ return unless payload
66
+
67
+ synchronize do
68
+ queue = @queue_hash[queue_name]
69
+ [queue, queue.decode(payload)]
70
+ end
71
+ end
72
+ end
73
+ 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,117 @@
1
+ require 'redis'
2
+ require 'redis-namespace'
3
+ require 'thread'
4
+ require 'mutex_m'
5
+
6
+ module Resque
7
+ ###
8
+ # Exception raised when trying to access a queue that's already destroyed
9
+ class QueueDestroyed < RuntimeError; end
10
+
11
+ ###
12
+ # A queue interface that quacks like Queue from Ruby's stdlib.
13
+ class Queue
14
+ include Mutex_m
15
+
16
+ attr_reader :name, :redis_name
17
+
18
+ ###
19
+ # Create a new Queue object with +name+ on +redis+ connection, and using
20
+ # the +coder+ for encoding and decoding objects that are stored in redis.
21
+ def initialize name, redis, coder = Marshal
22
+ super()
23
+ @name = name
24
+ @redis_name = "queue:#{@name}"
25
+ @redis = redis
26
+ @coder = coder
27
+ @destroyed = false
28
+
29
+ @redis.sadd(:queues, @name)
30
+ end
31
+
32
+ # Add +object+ to the queue
33
+ # If trying to push to an already destroyed queue, it will raise a Resque::QueueDestroyed exception
34
+ def push object
35
+ raise QueueDestroyed if destroyed?
36
+
37
+ synchronize do
38
+ @redis.rpush @redis_name, encode(object)
39
+ end
40
+ end
41
+
42
+ alias :<< :push
43
+ alias :enq :push
44
+
45
+ # Returns a list of objects in the queue. This method is *not* available
46
+ # on the stdlib Queue.
47
+ def slice start, length
48
+ if length == 1
49
+ synchronize do
50
+ decode @redis.lindex @redis_name, start
51
+ end
52
+ else
53
+ synchronize do
54
+ Array(@redis.lrange(@redis_name, start, start + length - 1)).map do |item|
55
+ decode item
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Pop an item off the queue. This method will block until an item is
62
+ # available.
63
+ #
64
+ # Pass +true+ for a non-blocking pop. If nothing is read on a non-blocking
65
+ # pop, a ThreadError is raised.
66
+ def pop non_block = false
67
+ if non_block
68
+ synchronize do
69
+ value = @redis.lpop(@redis_name)
70
+ raise ThreadError unless value
71
+ decode value
72
+ end
73
+ else
74
+ synchronize do
75
+ value = @redis.blpop(@redis_name, 1) until value
76
+ decode value.last
77
+ end
78
+ end
79
+ end
80
+
81
+ # Get the length of the queue
82
+ def length
83
+ @redis.llen @redis_name
84
+ end
85
+ alias :size :length
86
+
87
+ # Is the queue empty?
88
+ def empty?
89
+ size == 0
90
+ end
91
+
92
+ # Deletes this Queue from redis. This method is *not* available on the
93
+ # stdlib Queue.
94
+ #
95
+ # If there are multiple queue objects of the same name, Queue A and Queue
96
+ # B and you delete Queue A, pushing to Queue B will have unknown side
97
+ # effects. Queue A will be marked destroyed, but Queue B will not.
98
+ def destroy
99
+ @redis.del @redis_name
100
+ @redis.srem(:queues, @name)
101
+ @destroyed = true
102
+ end
103
+
104
+ # returns +true+ if the queue is destroyed and +false+ if it isn't
105
+ def destroyed?
106
+ @destroyed
107
+ end
108
+
109
+ def encode object
110
+ @coder.dump object
111
+ end
112
+
113
+ def decode object
114
+ @coder.load object
115
+ end
116
+ end
117
+ end