tresque 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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