tresque 0.0.2
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.
- 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
|