tresque 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +186 -0
- data/Rakefile +1 -0
- data/lib/tresque.rb +24 -0
- data/lib/tresque/delay.rb +115 -0
- data/lib/tresque/delay_execution_worker.rb +58 -0
- data/lib/tresque/queue_lock.rb +70 -0
- data/lib/tresque/registry.rb +94 -0
- data/lib/tresque/resque_spec/ext.rb +92 -0
- data/lib/tresque/resque_spec/helpers.rb +25 -0
- data/lib/tresque/resque_spec/matchers.rb +318 -0
- data/lib/tresque/resque_spec/resque_spec.rb +201 -0
- data/lib/tresque/resque_spec/scheduler.rb +93 -0
- data/lib/tresque/spec.rb +2 -0
- data/lib/tresque/spec/delay.rb +27 -0
- data/lib/tresque/util.rb +16 -0
- data/lib/tresque/version.rb +3 -0
- data/lib/tresque/worker.rb +228 -0
- data/lib/tresque/worker_lock.rb +53 -0
- data/tresque.gemspec +30 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e49c5387c1d0949971273fc5c215714def53530d
|
4
|
+
data.tar.gz: 51efde231efbe4a817705db15e6cb98d673e3045
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4b402c7bab3c066d4a7d3de8feb58beb1d2be6d897975fdacbdbd925a198623a353e424bf69ddd56ca3b02cf142a40351af78eeb29b33fbf6850725cc5aa38bc
|
7
|
+
data.tar.gz: 6fe1ec450d11825103f6d1b2904df3d20ba5b40d3205fdbff73607ecd9a6e526af5a26a398b6167c4b88847dc16bd5e6f861eea20f0b2dad137bd2c14b88e200
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2015 TaskRabbit
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
# TResque
|
2
|
+
|
3
|
+
Patterns for Resque usage at TaskRabbit
|
4
|
+
|
5
|
+
* So work in instance method `work` instead of class method `perform`
|
6
|
+
* Enqueue hash instead of struct for more flexibility and change management
|
7
|
+
* Locking based on queue or timing
|
8
|
+
* Easy way to delay method calls
|
9
|
+
* Method for registering queue names to centralize ops
|
10
|
+
* Abstraction so we can move to Sidekiq even easier when that is the goal
|
11
|
+
|
12
|
+
## Worker
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
require 'tresque'
|
16
|
+
|
17
|
+
module MyEngine
|
18
|
+
class ImageProcessor
|
19
|
+
include ::TResque::Worker
|
20
|
+
|
21
|
+
inputs :user_id, :size
|
22
|
+
|
23
|
+
def work
|
24
|
+
User.find(user_id).upload_image!(size)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
#### Example Usage
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
MyEngine::ImageProcessor.enqueue(size: "small", user_id: 255)
|
34
|
+
```
|
35
|
+
|
36
|
+
## Queue Management
|
37
|
+
|
38
|
+
Say what queues you process
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
require 'tresque'
|
42
|
+
|
43
|
+
TResque.register("account") do
|
44
|
+
queue :default, 100
|
45
|
+
queue :refresh, -5000
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
Can put workers in those queues
|
50
|
+
|
51
|
+
```
|
52
|
+
module Account
|
53
|
+
class RegularWorker
|
54
|
+
include ::TResque::Worker
|
55
|
+
# defaults to account_default queue
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module Account
|
60
|
+
class RegularWorker
|
61
|
+
include ::TResque::Worker
|
62
|
+
queue :refresh # lower priority account_refresh queue
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
#### Rake setup
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
require 'resque/tasks'
|
71
|
+
require 'resque_scheduler/tasks'
|
72
|
+
require "resque_bus/tasks"
|
73
|
+
|
74
|
+
namespace :resque do
|
75
|
+
task :setup => [:environment] do
|
76
|
+
require 'resque_scheduler'
|
77
|
+
require 'resque/scheduler'
|
78
|
+
require 'tresque'
|
79
|
+
end
|
80
|
+
|
81
|
+
task :queues => [:setup] do
|
82
|
+
queues = ::TResque::Registry.queues
|
83
|
+
ENV["QUEUES"] = queues.join(",")
|
84
|
+
puts "TResque: #{ENV["QUEUES"]}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
```
|
89
|
+
|
90
|
+
Work those queues by priority
|
91
|
+
```
|
92
|
+
$ bundle exec rake resque:queues resque:work
|
93
|
+
TResque: account_default, account_refresh
|
94
|
+
```
|
95
|
+
|
96
|
+
## Locking
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
module MyEngine
|
100
|
+
class SingletonWorker
|
101
|
+
include ::TResque::Worker
|
102
|
+
inputs :user_id
|
103
|
+
|
104
|
+
# does not enqueue another worker if this worker with same user_id waiting to be processed
|
105
|
+
queue_lock :user_id
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
module MyEngine
|
110
|
+
class MutexWorker
|
111
|
+
include ::TResque::Worker
|
112
|
+
inputs :user_id, :any_other_input
|
113
|
+
|
114
|
+
# does work two of these workers at the same time for the same user_id
|
115
|
+
worker_lock :user_id
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
Those locks are for the same worker. You can also coordinate across workers using a namespace. Or, in other words, the default namespace is the worker class name but can be overridden. The keys need the same name.
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
module MyEngine
|
124
|
+
class FirstWorker
|
125
|
+
include ::TResque::Worker
|
126
|
+
inputs :user_id
|
127
|
+
|
128
|
+
lock_namespace :user_calculations
|
129
|
+
worker_lock :user_id
|
130
|
+
end
|
131
|
+
|
132
|
+
class SecondWorker
|
133
|
+
include ::TResque::Worker
|
134
|
+
inputs :user_id, :other_input
|
135
|
+
|
136
|
+
lock_namespace :user_calculations
|
137
|
+
worker_lock :user_id
|
138
|
+
end
|
139
|
+
|
140
|
+
class ThirdWorker
|
141
|
+
include ::TResque::Worker
|
142
|
+
inputs :other_key
|
143
|
+
|
144
|
+
lock_namespace :user_calculations
|
145
|
+
worker_lock :user_id
|
146
|
+
|
147
|
+
def user_id
|
148
|
+
# can be a method too
|
149
|
+
User.find_by_other_key(other_key).id
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
## Delay
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
class User < ::ActiveRecord::Base
|
159
|
+
include ::TResque::Delay
|
160
|
+
|
161
|
+
def heavy_lifting
|
162
|
+
# stuff in background
|
163
|
+
end
|
164
|
+
async :heavy_lifting, queue: 'my_queue'
|
165
|
+
|
166
|
+
def other_stuff
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
170
|
+
```
|
171
|
+
#### Example Usage
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
user = User.find(1)
|
175
|
+
|
176
|
+
# Always in the background
|
177
|
+
user.heavy_lifting
|
178
|
+
|
179
|
+
# optionally in the background
|
180
|
+
user.delay.other_stuff
|
181
|
+
```
|
182
|
+
|
183
|
+
## Notes
|
184
|
+
|
185
|
+
Generally based on [qe](https://github.com/fnando/qe).
|
186
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/tresque.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "tresque/version"
|
2
|
+
require 'resque'
|
3
|
+
|
4
|
+
module TResque
|
5
|
+
autoload :Util, 'tresque/util'
|
6
|
+
autoload :Worker, 'tresque/worker'
|
7
|
+
autoload :Delay, 'tresque/delay'
|
8
|
+
autoload :DelayExecutionWorker, 'tresque/delay_execution_worker'
|
9
|
+
autoload :WorkerLock, 'tresque/worker_lock'
|
10
|
+
autoload :QueueLock, 'tresque/queue_lock'
|
11
|
+
autoload :Registry, 'tresque/registry'
|
12
|
+
|
13
|
+
module Spec
|
14
|
+
autoload :Delay, 'tresque/spec/delay'
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def register(app_key, &block)
|
19
|
+
registry = ::TResque::Registry.new(app_key)
|
20
|
+
registry.instance_eval(&block)
|
21
|
+
registry
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module TResque
|
2
|
+
module Delay
|
3
|
+
extend ::ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
def delay(options = {})
|
8
|
+
InvocationProxy.new(self, options)
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
def async(method_name, options = {})
|
13
|
+
raise "Attempted to handle #{self.name}##{method_name} asyncronously but #{method_name} was not yet defined" unless (self.instance_methods + self.private_instance_methods).include?(method_name)
|
14
|
+
|
15
|
+
class_eval <<-EV, __FILE__, __LINE__ + 1
|
16
|
+
def #{method_name}_with_delay(*args)
|
17
|
+
self.delay(#{options.inspect}).#{method_name}_without_delay(*args)
|
18
|
+
end
|
19
|
+
alias_method_chain :#{method_name}, :delay
|
20
|
+
EV
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
def delay(options = {})
|
26
|
+
InvocationProxy.new(self, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def method_missing(method_name, *args)
|
31
|
+
if method_name.to_s =~ /^delay__(.+)$/
|
32
|
+
self.delay.send($1, *args)
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def respond_to_missing?(method_name, include_private = false)
|
39
|
+
method_name.to_s =~ /^delay__(.+)/ || super
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
class InvocationProxy
|
44
|
+
|
45
|
+
def initialize(object, options = {})
|
46
|
+
|
47
|
+
@object = object
|
48
|
+
@run_at = options[:run_at]
|
49
|
+
@run_at ||= (!!options[:force] ? true : nil)
|
50
|
+
@synchronous = !!options[:synchronous]
|
51
|
+
|
52
|
+
@queue_namespace = options[:queue_namespace] || Util.calculate_namespace_from_class(object)
|
53
|
+
@queue_name = options[:queue] || 'default'
|
54
|
+
|
55
|
+
@lock_namespace = options[:lock_namespace]
|
56
|
+
@queue_lock_key = options[:queue_lock_key]
|
57
|
+
@worker_lock_key = options[:worker_lock_key]
|
58
|
+
end
|
59
|
+
|
60
|
+
def method_missing(method_name, *args)
|
61
|
+
if !@synchronous && (!in_resque? || @run_at == true || @run_at.to_i > Time.now.to_i)
|
62
|
+
@method_name = method_name.to_s
|
63
|
+
@args = args
|
64
|
+
queue_delayed_invocation!
|
65
|
+
else
|
66
|
+
@object.send(method_name, *args)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def respond_to?(*args)
|
71
|
+
return true unless in_resque?
|
72
|
+
@object.respond_to?(*args)
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def in_resque?
|
78
|
+
!!(ENV['QUEUE'] || ENV['QUEUES'])
|
79
|
+
end
|
80
|
+
|
81
|
+
def queue_delayed_invocation!
|
82
|
+
push = {}
|
83
|
+
|
84
|
+
if @object.is_a?(Class)
|
85
|
+
push["class_name"] = @object.name
|
86
|
+
else
|
87
|
+
push["class_name"] = @object.class.name
|
88
|
+
push["id"] = @object.respond_to?(:delay_id) ? @object.delay_id : @object.id
|
89
|
+
end
|
90
|
+
|
91
|
+
push["method_name"] = @method_name
|
92
|
+
push["args"] = @args
|
93
|
+
push["queue_namespace"] = @queue_namespace
|
94
|
+
push["queue"] = @queue_name
|
95
|
+
push["run_at"] = @run_at if @run_at && @run_at != true
|
96
|
+
push["lock_namespace"] = @lock_namespace if @lock_namespace
|
97
|
+
|
98
|
+
if @queue_lock_key
|
99
|
+
push["queue_lock"] = @queue_lock_key.to_s
|
100
|
+
push[@queue_lock_key.to_s] = push["id"]
|
101
|
+
end
|
102
|
+
|
103
|
+
if @worker_lock_key
|
104
|
+
push["worker_lock"] = @worker_lock_key.to_s
|
105
|
+
push[@worker_lock_key.to_s] = push["id"]
|
106
|
+
end
|
107
|
+
|
108
|
+
TResque::DelayExecutionWorker.enqueue(push)
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module TResque
|
2
|
+
class DelayExecutionWorker
|
3
|
+
include ::TResque::Worker
|
4
|
+
inputs :class_name, :id, :method_name, :args
|
5
|
+
|
6
|
+
# enabled both of these by option
|
7
|
+
extend ::TResque::WorkerLock
|
8
|
+
extend ::TResque::QueueLock
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# handle dynamic locks
|
12
|
+
def get_lock_namespace(options)
|
13
|
+
options["lock_namespace"] || options["class_name"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_queue_lock_attributes(options)
|
17
|
+
return nil unless options["queue_lock"]
|
18
|
+
[options["queue_lock"]].flatten
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_worker_lock_attributes(options)
|
22
|
+
return nil unless options["worker_lock"]
|
23
|
+
[options["worker_lock"]].flatten
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def work
|
28
|
+
return unless record
|
29
|
+
if args.nil? || args.empty?
|
30
|
+
record.send(self.method_name)
|
31
|
+
else
|
32
|
+
record.send(self.method_name, *self.args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def klass
|
39
|
+
@klass ||= class_name.constantize
|
40
|
+
end
|
41
|
+
|
42
|
+
def record
|
43
|
+
@record ||= if id.nil?
|
44
|
+
klass
|
45
|
+
else
|
46
|
+
if klass.respond_to?(:find_by)
|
47
|
+
klass.find_by(id: id)
|
48
|
+
elsif klass.respond_to?(:find_by_id)
|
49
|
+
klass.find_by_id(id)
|
50
|
+
else
|
51
|
+
klass.find(id)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module TResque
|
2
|
+
# If you want only one instance of your job queued at a time,
|
3
|
+
# extend it with this module.
|
4
|
+
module QueueLock
|
5
|
+
|
6
|
+
# Override in your job to control the lock experiation time. This is the
|
7
|
+
# time in seconds that the lock should be considered valid. The default
|
8
|
+
# is one hour (3600 seconds).
|
9
|
+
def queue_lock_timeout
|
10
|
+
3600
|
11
|
+
end
|
12
|
+
|
13
|
+
# Override in your job to control the lock key. It is
|
14
|
+
# passed the same arguments as `perform`, that is, your job's
|
15
|
+
# payload.
|
16
|
+
#def queue_lock_key(options)
|
17
|
+
# "#{name}-#{options.to_s}"
|
18
|
+
#end
|
19
|
+
|
20
|
+
# See the documentation for SETNX http://redis.io/commands/setnx for an
|
21
|
+
# explanation of this deadlock free locking pattern
|
22
|
+
def before_enqueue_queue_lock(options)
|
23
|
+
val = queue_lock_key(options)
|
24
|
+
if val
|
25
|
+
key = "lock:#{val}"
|
26
|
+
now = Time.now.to_i
|
27
|
+
from_now = queue_lock_timeout + 1
|
28
|
+
key_expire = from_now + 600 # some exra time
|
29
|
+
timeout = now + from_now
|
30
|
+
|
31
|
+
# return true if we successfully acquired the lock
|
32
|
+
if Resque.redis.setnx(key, timeout)
|
33
|
+
# expire in case of error to make sure it goes away
|
34
|
+
Resque.redis.expire(key, key_expire)
|
35
|
+
return true
|
36
|
+
end
|
37
|
+
|
38
|
+
# see if the existing timeout is still valid and return false if it is
|
39
|
+
# (we cannot acquire the lock during the timeout period)
|
40
|
+
return false if now <= Resque.redis.get(key).to_i
|
41
|
+
|
42
|
+
# otherwise set the timeout and ensure that no other worker has
|
43
|
+
# acquired the lock
|
44
|
+
if now > Resque.redis.getset(key, timeout).to_i
|
45
|
+
# expire in case of error to make sure it goes away
|
46
|
+
Resque.redis.expire(key, key_expire)
|
47
|
+
return true
|
48
|
+
else
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def clear_queue_lock(options)
|
56
|
+
val = queue_lock_key(options)
|
57
|
+
if val
|
58
|
+
Resque.redis.del("lock:#{val}")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def before_perform_queue_lock(options)
|
63
|
+
clear_queue_lock(options)
|
64
|
+
end
|
65
|
+
|
66
|
+
def after_dequeue_queue_lock(options)
|
67
|
+
clear_queue_lock(options)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|