resque-unique-job 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README.markdown +15 -0
- data/Rakefile +1 -0
- data/lib/resque/plugins/resque_ext/destroy.rb +15 -0
- data/lib/resque/plugins/unique_job.rb +255 -0
- data/lib/resque/plugins/unique_job/version.rb +7 -0
- data/spec/unique_job_spec.rb +149 -0
- metadata +117 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2011 Engine Yard
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.markdown
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
#TODO
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Resque.class_eval do
|
2
|
+
|
3
|
+
def self.dequeue_with_unique_job(klass, *args)
|
4
|
+
klass.destroy_matching_keys(queue_from_class(klass), args)
|
5
|
+
dequeue_without_unique_job(klass, *args)
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
class << self
|
10
|
+
alias_method :dequeue_without_unique_job, :dequeue
|
11
|
+
alias_method :dequeue, :dequeue_with_unique_job
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,255 @@
|
|
1
|
+
require 'resque/plugins/resque_ext/destroy'
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Plugins
|
5
|
+
module UniqueJob
|
6
|
+
include Resque::Helpers
|
7
|
+
|
8
|
+
def enqueue(*args)
|
9
|
+
key = unique_redis_key(args)
|
10
|
+
ttl = Resque.redis.ttl(key)
|
11
|
+
# if it's already enQ'd
|
12
|
+
if Resque.redis.getset(key, "1")
|
13
|
+
if expire = unique_redis_expiration
|
14
|
+
# reset existing expiration
|
15
|
+
Resque.redis.expire(key, ttl)
|
16
|
+
end
|
17
|
+
else
|
18
|
+
# set new expiration
|
19
|
+
if expire = unique_redis_expiration
|
20
|
+
Resque.redis.expire(key, expire)
|
21
|
+
end
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def before_perform_unique_job(*args)
|
27
|
+
Resque.redis.del(unique_redis_key(args))
|
28
|
+
end
|
29
|
+
|
30
|
+
def unique_key(args)
|
31
|
+
Digest::MD5.hexdigest(encode(:class => self.to_s, :args => args))
|
32
|
+
end
|
33
|
+
|
34
|
+
def destroy_matching_keys(queue, args)
|
35
|
+
queue = "queue:#{queue}"
|
36
|
+
|
37
|
+
klass = self.to_s
|
38
|
+
if args.empty?
|
39
|
+
redis.lrange(queue, 0, -1).each do |string|
|
40
|
+
if decode(string)['class'] == klass
|
41
|
+
key = unique_redis_key(decode(string)['args'])
|
42
|
+
Resque.redis.del(key)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
else
|
46
|
+
Resque.redis.del(unique_redis_key(args))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def unique_redis_expiration
|
53
|
+
600
|
54
|
+
end
|
55
|
+
|
56
|
+
def unique_redis_key(args)
|
57
|
+
job_unique_key = unique_key(args)
|
58
|
+
"plugin:unique_job:#{job_unique_key}"
|
59
|
+
end
|
60
|
+
# def self.extended(mod)
|
61
|
+
# mod.extend(Resque::Plugins::Meta)
|
62
|
+
# mod.extend(Resque::Plugins::Lock)
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# class Step
|
66
|
+
# def initialize(args, run_last = false, &block)
|
67
|
+
# @run_last = run_last
|
68
|
+
# @signature = args.map{|a| a.to_s}.join(" ")
|
69
|
+
# if args.size == 1
|
70
|
+
# #no inputs or output
|
71
|
+
# @inputs = []
|
72
|
+
# elsif args.size >= 2
|
73
|
+
# unless @run_last
|
74
|
+
# @output = args.pop.to_s
|
75
|
+
# end
|
76
|
+
# @inputs = []
|
77
|
+
# args.each_with_index do |a, index|
|
78
|
+
# if index % 2 == 1
|
79
|
+
# @inputs << a.to_s
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
# else
|
83
|
+
# raise ArgumentError, "invalid arguments #{args.inspect}"
|
84
|
+
# end
|
85
|
+
# @block = block
|
86
|
+
# end
|
87
|
+
# attr_reader :block, :signature, :inputs, :output, :run_last
|
88
|
+
# # attr_accessor :run_last
|
89
|
+
# # attr_reader :step_name, :what_it_makes, :block
|
90
|
+
# def run(available_inputs)
|
91
|
+
# begin
|
92
|
+
# block_args = @inputs.map do |input_name|
|
93
|
+
# available_inputs[input_name]
|
94
|
+
# end
|
95
|
+
# @block.call(*block_args)
|
96
|
+
# rescue Exception => e
|
97
|
+
# puts e.inspect
|
98
|
+
# puts e.backtrace.join("\n")
|
99
|
+
# raise e
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# #TODO:
|
105
|
+
# #
|
106
|
+
# # Need a way to edit meta data BEFORE a job is enQ'd
|
107
|
+
# # so that we can we sure that data is available on dQ
|
108
|
+
# #
|
109
|
+
# # Need a way to "Lock" editing of the meta data on a job
|
110
|
+
# # so other editors of job data must wait before editing job
|
111
|
+
# # (2 jobs marking a step done and then, therefore re-enQ-ing the parent)
|
112
|
+
# #
|
113
|
+
# # need a way to mark a job as already enQ'd (and not yet running)
|
114
|
+
# # so that if the tomato just re-enQ'd the sandwich, the cheese will see it on the Q and let it be
|
115
|
+
# #
|
116
|
+
# # basically, there should be a lock such that if there are any child jobs still enQ'd for a job
|
117
|
+
# # it should not run but prioritize the child jobs first
|
118
|
+
#
|
119
|
+
# class StepDependency
|
120
|
+
# def initialize(job_class, args)
|
121
|
+
# @job_class = job_class
|
122
|
+
# @job_args = args
|
123
|
+
# end
|
124
|
+
# attr_reader :job_class, :job_args
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# class Retry
|
128
|
+
# def initialize(seconds)
|
129
|
+
# @seconds = seconds
|
130
|
+
# end
|
131
|
+
# attr_reader :seconds
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
# def run_steps(meta_id, *args)
|
135
|
+
# @step_list = []
|
136
|
+
# @meta = self.get_jobdata(meta_id)
|
137
|
+
# # puts "I have meta of: " + @meta.inspect
|
138
|
+
#
|
139
|
+
# #implicitly builds the @step_list
|
140
|
+
# steps(*args)
|
141
|
+
#
|
142
|
+
# #TODO: raise error if there are duplicate steps defined?
|
143
|
+
#
|
144
|
+
# # require 'pp'
|
145
|
+
# # pp @step_list
|
146
|
+
#
|
147
|
+
# #figure out which step we are on from meta data
|
148
|
+
# @meta["step_count"] ||= @step_list.size
|
149
|
+
# steps_ran = @meta["steps_ran"] ||= []
|
150
|
+
# steps_running = @meta["steps_running"] ||= []
|
151
|
+
# # puts "my steps_ran are #{steps_ran.inspect}"
|
152
|
+
# # @step_list.map{ |step| step.signature }
|
153
|
+
# available_inputs = @meta["available_inputs"] ||= {}
|
154
|
+
# # puts "my available_inputs are #{available_inputs.inspect}"
|
155
|
+
#
|
156
|
+
# #run last step if no more steps are needed
|
157
|
+
# @step_list.each do |step|
|
158
|
+
# if steps_ran.include?(step.signature)
|
159
|
+
# #already ran
|
160
|
+
# elsif steps_running.include?(step.signature)
|
161
|
+
# #already Q'd
|
162
|
+
# elsif step.run_last
|
163
|
+
# # puts "Can't run last step #{step.signature} yet"
|
164
|
+
# #this is the last step, only run if all other steps are run
|
165
|
+
# elsif (step.inputs - available_inputs.keys).empty?
|
166
|
+
# #all of the steps needed inputs are available
|
167
|
+
# #run!
|
168
|
+
# result = step.run(available_inputs)
|
169
|
+
# puts "running step #{step.signature}"
|
170
|
+
# if result.is_a?(Retry)
|
171
|
+
# puts "enqueue #{self} in #{result.seconds}"
|
172
|
+
# Resque.enqueue_in(result.seconds, self, meta_id, *args)
|
173
|
+
# elsif result.is_a?(StepDependency)
|
174
|
+
# #TODO: what if the child job is dQ'd before caller has a chance to set parent_job
|
175
|
+
# # don't re-enQ a child that's already enQ'd!
|
176
|
+
# # it might not be in steps ran but it doesn't need to be duplicated!
|
177
|
+
# puts "enqueue #{result.job_class}"
|
178
|
+
# child_job = result.job_class.enqueue(*result.job_args)
|
179
|
+
# @meta["steps_running"] << step.signature
|
180
|
+
# child_job["parent_job"] = [self, meta_id, args]
|
181
|
+
# child_job["expected_output"] = step.output
|
182
|
+
# child_job["signature_from_parent"] = step.signature
|
183
|
+
# child_job.save
|
184
|
+
# else
|
185
|
+
# if step.output
|
186
|
+
# available_inputs[step.output] = result
|
187
|
+
# end
|
188
|
+
# # puts "available_inputs are now #{available_inputs.inspect}"
|
189
|
+
# if @meta["steps_ran"].include?(step.signature)
|
190
|
+
# raise "WHAT? ran #{step.signature} twice!"
|
191
|
+
# end
|
192
|
+
# @meta["steps_ran"] << step.signature
|
193
|
+
# end
|
194
|
+
# else
|
195
|
+
# # puts "waiting before we can run step #{step.signature} -- need #{step.inputs}"
|
196
|
+
# end
|
197
|
+
# end
|
198
|
+
#
|
199
|
+
# if steps_ran.size + 1 == @step_list.size
|
200
|
+
# puts "now running last step of #{self} -- already ran #{steps_ran.inspect}"
|
201
|
+
#
|
202
|
+
# step = @step_list.last
|
203
|
+
# result = step.run(available_inputs)
|
204
|
+
# if @meta["parent_job"]
|
205
|
+
# # puts "#{meta_id} has parent"
|
206
|
+
# parent_job_class_name, parent_meta_id, parent_args = @meta["parent_job"]
|
207
|
+
# parent_job_class = const_get(parent_job_class_name)
|
208
|
+
# parent_meta = parent_job_class.get_jobdata(parent_meta_id)
|
209
|
+
# if expected_output = @meta["expected_output"]
|
210
|
+
# parent_meta["available_inputs"][expected_output] = result
|
211
|
+
# parent_meta.save
|
212
|
+
# end
|
213
|
+
# if @meta["signature_from_parent"]
|
214
|
+
# if parent_meta["steps_ran"].include?(@meta["signature_from_parent"])
|
215
|
+
# raise "WHAT? ran #{@meta["signature_from_parent"]} twice!"
|
216
|
+
# end
|
217
|
+
# parent_meta["steps_ran"] << @meta["signature_from_parent"]
|
218
|
+
# parent_meta.save
|
219
|
+
# end
|
220
|
+
# puts "enqueue #{parent_job_class}"
|
221
|
+
# Resque.enqueue(parent_job_class, parent_meta_id, *parent_args)
|
222
|
+
# end
|
223
|
+
# if @meta["steps_ran"].include?(step.signature)
|
224
|
+
# raise "WHAT? ran #{step.signature} twice!"
|
225
|
+
# end
|
226
|
+
# @meta["steps_ran"] << step.signature
|
227
|
+
# end
|
228
|
+
# @meta.save
|
229
|
+
#
|
230
|
+
# puts "End of #{self}"
|
231
|
+
# end
|
232
|
+
#
|
233
|
+
# def step(*args, &block)
|
234
|
+
# @step_list << Step.new(args, &block)
|
235
|
+
# end
|
236
|
+
#
|
237
|
+
# def last_step(*args, &block)
|
238
|
+
# @step_list << Step.new(args, true, &block)
|
239
|
+
# end
|
240
|
+
#
|
241
|
+
# def depend_on(job_class, *args)
|
242
|
+
# StepDependency.new(job_class, args)
|
243
|
+
# end
|
244
|
+
#
|
245
|
+
# def retry_in(seconds)
|
246
|
+
# Retry.new(seconds)
|
247
|
+
# end
|
248
|
+
#
|
249
|
+
# def get_jobdata(meta_id)
|
250
|
+
# get_meta(meta_id)
|
251
|
+
# end
|
252
|
+
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'resque'
|
2
|
+
require 'resque/plugins/unique_job'
|
3
|
+
|
4
|
+
class WhatHappened
|
5
|
+
require 'tempfile'
|
6
|
+
def self.reset!
|
7
|
+
@what_happened = Tempfile.new("what_happened")
|
8
|
+
end
|
9
|
+
def self.what_happened
|
10
|
+
File.read(@what_happened.path)
|
11
|
+
end
|
12
|
+
def self.record(*event)
|
13
|
+
@what_happened.write(event.to_s)
|
14
|
+
@what_happened.flush
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class BaseJob
|
19
|
+
|
20
|
+
def self.enqueue(*args)
|
21
|
+
Resque.enqueue(self, *args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.perform(*args)
|
25
|
+
begin
|
26
|
+
WhatHappened.record(self, args)
|
27
|
+
rescue => e
|
28
|
+
puts e.inspect
|
29
|
+
puts e.backtrace.join("\n")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class BasicJob < BaseJob
|
36
|
+
extend Resque::Plugins::UniqueJob
|
37
|
+
@queue = :test
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
class DifferentJob < BaseJob
|
42
|
+
extend Resque::Plugins::UniqueJob
|
43
|
+
@queue = :test
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
class NotUniqueJob < BaseJob
|
48
|
+
@queue = :test
|
49
|
+
end
|
50
|
+
|
51
|
+
class ShortExpiringJob < BaseJob
|
52
|
+
extend Resque::Plugins::UniqueJob
|
53
|
+
@queue = :test
|
54
|
+
|
55
|
+
def self.unique_redis_expiration
|
56
|
+
1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
describe Resque::Plugins::UniqueJob do
|
62
|
+
before(:each) do
|
63
|
+
WhatHappened.reset!
|
64
|
+
Resque.redis.flushall
|
65
|
+
end
|
66
|
+
|
67
|
+
it "works for 1 job" do
|
68
|
+
BasicJob.enqueue("foo", "bar")
|
69
|
+
worker = Resque::Worker.new(:test)
|
70
|
+
worker.work(0)
|
71
|
+
WhatHappened.what_happened.should == "BasicJobfoobar"
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "uniqueness" do
|
75
|
+
|
76
|
+
it "cares about job class" do
|
77
|
+
BasicJob.enqueue("foo", "bar")
|
78
|
+
DifferentJob.enqueue("foo", "bar")
|
79
|
+
worker = Resque::Worker.new(:test)
|
80
|
+
worker.work(0)
|
81
|
+
WhatHappened.what_happened.should == "BasicJobfoobarDifferentJobfoobar"
|
82
|
+
end
|
83
|
+
|
84
|
+
it "only enqueues the job once" do
|
85
|
+
BasicJob.enqueue("foo", "bar")
|
86
|
+
BasicJob.enqueue("foo", "bar")
|
87
|
+
worker = Resque::Worker.new(:test)
|
88
|
+
worker.work(0)
|
89
|
+
WhatHappened.what_happened.should == "BasicJobfoobar"
|
90
|
+
end
|
91
|
+
|
92
|
+
it "re-enqueues after the job has been processed" do
|
93
|
+
worker = Resque::Worker.new(:test)
|
94
|
+
BasicJob.enqueue("foo", "bar")
|
95
|
+
worker.work(0)
|
96
|
+
BasicJob.enqueue("foo", "bar")
|
97
|
+
worker.work(0)
|
98
|
+
WhatHappened.what_happened.should == "BasicJobfoobarBasicJobfoobar"
|
99
|
+
end
|
100
|
+
|
101
|
+
it "doesn't make non unique jobs unique" do
|
102
|
+
NotUniqueJob.enqueue("foo", "bar")
|
103
|
+
NotUniqueJob.enqueue("foo", "bar")
|
104
|
+
worker = Resque::Worker.new(:test)
|
105
|
+
worker.work(0)
|
106
|
+
WhatHappened.what_happened.should == "NotUniqueJobfoobarNotUniqueJobfoobar"
|
107
|
+
end
|
108
|
+
|
109
|
+
it "respects dequeue" do
|
110
|
+
BasicJob.enqueue("foo", "bar")
|
111
|
+
Resque.dequeue(BasicJob, "foo", "bar")
|
112
|
+
BasicJob.enqueue("foo", "bar")
|
113
|
+
worker = Resque::Worker.new(:test)
|
114
|
+
worker.work(0)
|
115
|
+
WhatHappened.what_happened.should == "BasicJobfoobar"
|
116
|
+
end
|
117
|
+
|
118
|
+
it "respects destroy dequeue all" do
|
119
|
+
BasicJob.enqueue("foo", "bar")
|
120
|
+
BasicJob.enqueue("bar", "baz")
|
121
|
+
Resque.dequeue(BasicJob)
|
122
|
+
BasicJob.enqueue("foo", "bar")
|
123
|
+
worker = Resque::Worker.new(:test)
|
124
|
+
worker.work(0)
|
125
|
+
WhatHappened.what_happened.should == "BasicJobfoobar"
|
126
|
+
end
|
127
|
+
|
128
|
+
it "doesn't destroy too much" do
|
129
|
+
BasicJob.enqueue("foo", "bar")
|
130
|
+
DifferentJob.enqueue("foo", "bar")
|
131
|
+
Resque.dequeue(BasicJob, "foo", "bar")
|
132
|
+
# DifferentJob.enqueue("foo", "bar")
|
133
|
+
worker = Resque::Worker.new(:test)
|
134
|
+
worker.work(0)
|
135
|
+
WhatHappened.what_happened.should == "DifferentJobfoobar"
|
136
|
+
end
|
137
|
+
|
138
|
+
it "expires the uniqueness key" do
|
139
|
+
ShortExpiringJob.enqueue("foo", "bar")
|
140
|
+
sleep 2
|
141
|
+
ShortExpiringJob.enqueue("foo", "bar")
|
142
|
+
worker = Resque::Worker.new(:test)
|
143
|
+
worker.work(0)
|
144
|
+
WhatHappened.what_happened.should == "ShortExpiringJobfoobarShortExpiringJobfoobar"
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: resque-unique-job
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Andy Delcambre
|
14
|
+
- Jacob Burkhart
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2011-03-04 00:00:00 -08:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: resque
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 55
|
31
|
+
segments:
|
32
|
+
- 1
|
33
|
+
- 8
|
34
|
+
- 0
|
35
|
+
version: 1.8.0
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id001
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: rspec
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
hash: 3
|
47
|
+
segments:
|
48
|
+
- 0
|
49
|
+
version: "0"
|
50
|
+
type: :development
|
51
|
+
version_requirements: *id002
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: ruby-debug
|
54
|
+
prerelease: false
|
55
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 3
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
type: :development
|
65
|
+
version_requirements: *id003
|
66
|
+
description: A Resque plugin for unique jobs
|
67
|
+
email: cloud@engineyard.com
|
68
|
+
executables: []
|
69
|
+
|
70
|
+
extensions: []
|
71
|
+
|
72
|
+
extra_rdoc_files: []
|
73
|
+
|
74
|
+
files:
|
75
|
+
- README.markdown
|
76
|
+
- Rakefile
|
77
|
+
- LICENSE
|
78
|
+
- lib/resque/plugins/resque_ext/destroy.rb
|
79
|
+
- lib/resque/plugins/unique_job/version.rb
|
80
|
+
- lib/resque/plugins/unique_job.rb
|
81
|
+
- spec/unique_job_spec.rb
|
82
|
+
has_rdoc: true
|
83
|
+
homepage: http://github.com/engineyard/resque-unique-job
|
84
|
+
licenses: []
|
85
|
+
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
hash: 3
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
version: "0"
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
hash: 3
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
version: "0"
|
109
|
+
requirements: []
|
110
|
+
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 1.5.2
|
113
|
+
signing_key:
|
114
|
+
specification_version: 3
|
115
|
+
summary: A Resque plugin for unique jobs
|
116
|
+
test_files: []
|
117
|
+
|