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.
@@ -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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tresque.gemspec
4
+ gemspec
@@ -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.
@@ -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
+
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -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