active_job_lock 0.1.0

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: d693fcdbfbfa1a7b23caad27365204b54ae568a8
4
+ data.tar.gz: 85ac93a9e83d532daffba6d80ac63c55115b9539
5
+ SHA512:
6
+ metadata.gz: 0d916369eea001d36f7d4401208a75195eed7979b5d7137ede68e1ca32a17bb5fc0ccb02457e9334fbbb9f6dd9e265917e2ecaca3615c1120618dc199995b132
7
+ data.tar.gz: 2b3b56f70bcd04dcf62632e11ae9734ac4553b6b487791696e10e81237a8cbe3169b179f68d530f09a44be39f0cdc992b863c39300edec0cefe851bae79e3ca8
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 (2016-07-27)
2
+
3
+ - Initial release of `active_job_lock`, forked and adapted from [resque-lock-timeout](https://github.com/lantins/resque-lock-timeout/tree/v0.4.5).
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2010 Chris Wanstrath
2
+ Copyright (c) 2010 Ryan Carver
3
+ Copyright (c) 2010 Luke Antins
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ Software), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,238 @@
1
+ ActiveJob Lock
2
+ ===================
3
+
4
+ An [ActiveJob][activejob] plugin that adds locking, with optional timeout/deadlock handling.
5
+
6
+ Using a `lock_timeout` allows you to re-acquire the lock should your job
7
+ fail, crash, or is otherwise unable to release the lock. **i.e.** Your server
8
+ unexpectedly loses power. Very handy for jobs that are recurring or may be
9
+ retried.
10
+
11
+ **n.b.** By default, a job that fails to acquire a lock will be dropped. You can handle lock failures by implementing the available [callback](#callbacks).
12
+
13
+ Usage / Examples
14
+ ----------------
15
+
16
+ ### Single Job Instance
17
+
18
+ ```ruby
19
+ class UpdateNetworkGraph < ActiveJob::Base
20
+ include ActiveJobLock::Core
21
+
22
+ queue_as :network_graph
23
+
24
+ def perform(repo_id)
25
+ heavy_lifting
26
+ end
27
+ end
28
+ ```
29
+
30
+ Locking is achieved by storing a identifier/lock key in Redis.
31
+
32
+ Default behavior...
33
+
34
+ * Only one instance of a job may execute at once.
35
+ * The lock is held until the job completes or fails.
36
+ * If another job is executing with the same arguments the job will abort.
37
+
38
+ Please see below for more information about the identifier/lock key.
39
+
40
+ ### Enqueued Exclusivity (Loner Option)
41
+
42
+ Setting the `@loner` boolean to `true` will ensure the job is not enqueued if
43
+ the job (identified by the `identifier` method) is already running/enqueued.
44
+
45
+ ```ruby
46
+ class LonelyJob < ActiveJob::Base
47
+ include ActiveJobLock::Core
48
+
49
+ queue_as :loners
50
+ lock loner: true
51
+
52
+ def perform(repo_id)
53
+ heavy_lifting
54
+ end
55
+ end
56
+ ```
57
+
58
+ ### Lock Expiry/Timeout
59
+
60
+ The locking algorithm used can be found in the [Redis SETNX][redis-setnx]
61
+ documentation.
62
+
63
+ Simply set the lock timeout in seconds, e.g.
64
+
65
+ ```ruby
66
+ class UpdateNetworkGraph < ActiveJob::Base
67
+ include ActiveJobLock::Core
68
+
69
+ queue_as :network_graph
70
+ # Lock may be held for up to an hour.
71
+ lock timeout: 3600
72
+
73
+ def perform(repo_id)
74
+ heavy_lifting
75
+ end
76
+ end
77
+ ```
78
+
79
+ Customize & Extend
80
+ ==================
81
+
82
+ ### Job Identifier/Lock Key
83
+
84
+ By default the key uses this format: `lock:<job class name>:<identifier>`.
85
+
86
+ The default identifier is just your job arguments joined with a dash `-`.
87
+
88
+ If you have a lot of arguments or really long ones, you should consider
89
+ overriding `identifier` to define a more precise or loose custom identifier:
90
+
91
+ ```ruby
92
+ class UpdateNetworkGraph < ActiveJob::Base
93
+ include ActiveJobLock::Core
94
+
95
+ queue_as :network_graph
96
+
97
+ # Run only one at a time, regardless of repo_id.
98
+ def identifier(repo_id)
99
+ nil
100
+ end
101
+
102
+ def perform(repo_id)
103
+ heavy_lifting
104
+ end
105
+ end
106
+ ```
107
+
108
+ The above modification will ensure only one job of class
109
+ UpdateNetworkGraph is running at a time, regardless of the
110
+ repo_id.
111
+
112
+ Its lock key would be: `lock:UpdateNetworkGraph` (the `:<identifier>` part is left out if the identifier is `nil`).
113
+
114
+ You can define the entire key by overriding `redis_lock_key`:
115
+
116
+ ```ruby
117
+ class UpdateNetworkGraph < ActiveJob::Base
118
+ include ActiveJobLock::Core
119
+
120
+ queue_as :network_graph
121
+
122
+ def redis_lock_key(repo_id)
123
+ "lock:updates"
124
+ end
125
+
126
+ def perform(repo_id)
127
+ heavy_lifting
128
+ end
129
+ end
130
+ ```
131
+
132
+ That would use the key `lock:updates`.
133
+
134
+ ### Redis Connection Used for Locking
135
+
136
+ By default all locks are stored via a Redis client. For that, you have to tell `ActiveJobLock`
137
+ which client it should use. Set that through an initializer:
138
+
139
+ ```ruby
140
+ # config/initializers/active_job_lock.rb
141
+
142
+ ActiveJobLock::Config.redis = Redis.new(redis_config)
143
+ ```
144
+
145
+ If you want, you can then override it per job instance by doing:
146
+
147
+ ```ruby
148
+ class UpdateNetworkGraph < ActiveJob::Base
149
+ include ActiveJobLock::Core
150
+
151
+ queue_as :network_graph
152
+
153
+ def lock_redis
154
+ @lock_redis ||= CustomRedis.new
155
+ end
156
+
157
+ def perform(repo_id)
158
+ heavy_lifting
159
+ end
160
+ end
161
+ ```
162
+
163
+ ### Setting Timeout At Runtime
164
+
165
+ You may define the `lock_timeout` method to adjust the timeout at runtime
166
+ using job arguments. e.g.
167
+
168
+ ```ruby
169
+ class UpdateNetworkGraph < ActiveJob::Base
170
+ include ActiveJobLock::Core
171
+
172
+ queue_as :network_graph
173
+
174
+ def lock_timeout(repo_id, timeout_minutes)
175
+ 60 * timeout_minutes
176
+ end
177
+
178
+ def perform(repo_id, timeout_minutes = 1)
179
+ heavy_lifting
180
+ end
181
+ end
182
+ ```
183
+
184
+ ### Helper Methods
185
+
186
+ * `locked?` - checks if the lock is currently held.
187
+ * `enqueued?` - checks if the loner lock is currently held.
188
+ * `loner_locked?` - checks if the job is either enqueued (if a loner) or locked (any job).
189
+ * `refresh_lock!` - Refresh the lock, useful for jobs that are taking longer
190
+ then usual but your okay with them holding on to the lock a little longer.
191
+
192
+ ### <a name="callbacks"></a> Callbacks
193
+
194
+ Several callbacks are available to override and implement your own logic, e.g.
195
+
196
+ ```ruby
197
+ class UpdateNetworkGraph < ActiveJob::Base
198
+ include ActiveJobLock::Core
199
+
200
+ queue_as :network_graph
201
+ lock timeout: 3600, loner: true
202
+
203
+ # Job failed to acquire lock. You may implement retry or other logic.
204
+ def lock_failed(repo_id)
205
+ raise LockFailed
206
+ end
207
+
208
+ # Unable to enqueue job because its running or already enqueued.
209
+ def loner_enqueue_failed(repo_id)
210
+ raise EnqueueFailed
211
+ end
212
+
213
+ # Job has complete; but the lock expired before we could release it.
214
+ # The lock wasn't released; as its *possible* the lock is now held
215
+ # by another job.
216
+ def lock_expired_before_release(repo_id)
217
+ handle_if_needed
218
+ end
219
+
220
+ def perform(repo_id)
221
+ heavy_lifting
222
+ end
223
+ end
224
+ ```
225
+
226
+ Install
227
+ =======
228
+
229
+ $ gem install active_job_lock
230
+
231
+ Acknowledgements
232
+ ================
233
+
234
+ Forked and adapted from Luke Antins' [resque-lock-timeout v0.4.5][resque-lock-timeout] plugin.
235
+
236
+ [activejob]: https://github.com/rails/rails/tree/master/activejob
237
+ [resque-lock-timeout]: https://github.com/lantins/resque-lock-timeout/tree/v0.4.5
238
+ [redis-setnx]: http://redis.io/commands/setnx
@@ -0,0 +1,21 @@
1
+ require 'rake/testtask'
2
+ require 'yard'
3
+ require 'yard/rake/yardoc_task'
4
+ require 'bundler/gem_tasks'
5
+
6
+ task :default => :test
7
+
8
+ desc 'Run unit tests.'
9
+ Rake::TestTask.new(:test) do |task|
10
+ task.test_files = FileList['test/*_test.rb']
11
+ task.verbose = true
12
+ end
13
+
14
+ desc 'Build Yardoc documentation.'
15
+ YARD::Rake::YardocTask.new :yardoc do |t|
16
+ t.files = ['lib/**/*.rb']
17
+ t.options = ['--output-dir', 'doc/',
18
+ '--files', 'LICENSE,HISTORY.md',
19
+ '--readme', 'README.md',
20
+ '--title', 'active_job_lock documentation']
21
+ end
@@ -0,0 +1,5 @@
1
+ require 'active_job_lock/config'
2
+ require 'active_job_lock/core'
3
+
4
+ module ActiveJobLock
5
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveJobLock
2
+ module Config
3
+ extend self
4
+
5
+ attr_accessor :redis
6
+ end
7
+ end
@@ -0,0 +1,314 @@
1
+ module ActiveJobLock
2
+ # If you want only one instance of your job running at a time,
3
+ # include this module:
4
+ #
5
+ # class UpdateNetworkGraph < ActiveJob::Base
6
+ # include ActiveJobLock::Core
7
+ # queue_as :network_graph
8
+ #
9
+ # def perform(repo_id)
10
+ # heavy_lifting
11
+ # end
12
+ # end
13
+ #
14
+ # If you wish to limit the duration a lock may be held for, you can
15
+ # set/override `lock_timeout`. e.g.
16
+ #
17
+ # class UpdateNetworkGraph < ActiveJob::Base
18
+ # include ActiveJobLock::Core
19
+ # queue_as :network_graph
20
+ #
21
+ # # lock may be held for upto an hour.
22
+ # lock timeout: 3600
23
+ #
24
+ # def perform(repo_id)
25
+ # heavy_lifting
26
+ # end
27
+ # end
28
+ #
29
+ # If you wish that only one instance of the job defined by #identifier may be
30
+ # enqueued or running, you can set/override `loner`. e.g.
31
+ #
32
+ # class PdfExport < ActiveJob::Base
33
+ # include ActiveJobLock::Core
34
+ # queue_as :exports
35
+ #
36
+ # # only one job can be running/enqueued at a time. For instance a button
37
+ # # to run a PDF export. If the user clicks several times on it, enqueue
38
+ # # the job if and only if
39
+ # # - the same export is not currently running
40
+ # # - the same export is not currently queued.
41
+ # # ('same' being defined by `identifier`)
42
+ # lock loner: true
43
+ #
44
+ # def perform(repo_id)
45
+ # heavy_lifting
46
+ # end
47
+ # end
48
+ module Core
49
+ def self.included(base)
50
+ base.extend(ClassMethods)
51
+ end
52
+
53
+ module ClassMethods
54
+ attr_accessor :lock_timeout, :loner
55
+
56
+ def lock(options = {})
57
+ self.lock_timeout = options[:timeout]
58
+ self.loner = options[:loner]
59
+ end
60
+ end
61
+
62
+ def initialize(*args)
63
+ super(*args)
64
+ self.extend(OverriddenMethods)
65
+ end
66
+
67
+ module OverriddenMethods
68
+ # @abstract
69
+ # if the job is a `loner`, enqueue only if no other same job
70
+ # is already running/enqueued
71
+ #
72
+ def enqueue(*_)
73
+ if loner
74
+ if loner_locked?(*arguments)
75
+ # Same job is currently running
76
+ loner_enqueue_failed(*arguments)
77
+ return
78
+ else
79
+ acquire_loner_lock!(*arguments)
80
+ end
81
+ end
82
+ super
83
+ end
84
+
85
+ # Where the magic happens.
86
+ #
87
+ def perform(*arguments)
88
+ lock_until = acquire_lock!(*arguments)
89
+
90
+ # Release loner lock as job has been dequeued
91
+ release_loner_lock!(*arguments) if loner
92
+
93
+ # Abort if another job holds the lock.
94
+ return unless lock_until
95
+
96
+ begin
97
+ super(*arguments)
98
+ ensure
99
+ # Release the lock on success and error. Unless a lock_timeout is
100
+ # used, then we need to be more careful before releasing the lock.
101
+ now = Time.now.to_i
102
+ if lock_until != true and lock_until < now
103
+ # Eeek! Lock expired before perform finished. Trigger callback.
104
+ lock_expired_before_release(*arguments)
105
+ else
106
+ release_lock!(*arguments)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ # @abstract You may override to implement a custom identifier,
115
+ # you should consider doing this if your job arguments
116
+ # are many/long or may not cleanly cleanly to strings.
117
+ #
118
+ # Builds an identifier using the job arguments. This identifier
119
+ # is used as part of the redis lock key.
120
+ #
121
+ # @param [Array] args job arguments
122
+ # @return [String, nil] job identifier
123
+ def identifier(*args)
124
+ args.join('-')
125
+ end
126
+
127
+ # Override to fully control the redis object used for storing
128
+ # the locks.
129
+ #
130
+ # @return [Redis] redis object
131
+ def lock_redis
132
+ @lock_redis ||= ActiveJobLock::Config.redis
133
+ end
134
+
135
+ # Override to fully control the lock key used. It is passed
136
+ # the job arguments.
137
+ #
138
+ # The default looks like this: `lock:<class name>:<identifier>`
139
+ #
140
+ # @param [Array] args job arguments
141
+ # @return [String] redis key
142
+ def redis_lock_key(*args)
143
+ ['lock', self.class.name, identifier(*args)].compact.join(':')
144
+ end
145
+
146
+ # Builds lock key used by `@loner` option. Passed job arguments.
147
+ #
148
+ # The default looks like this: `loner:lock:<class name>:<identifier>`
149
+ #
150
+ # @param [Array] args job arguments
151
+ # @return [String] redis key
152
+ def redis_loner_lock_key(*args)
153
+ ['loner', redis_lock_key(*args)].compact.join(':')
154
+ end
155
+
156
+ # Number of seconds the lock may be held for.
157
+ # A value of 0 or below will lock without a timeout.
158
+ #
159
+ # @return [Fixnum]
160
+ def lock_timeout
161
+ @lock_timeout ||= self.class.lock_timeout || 0
162
+ end
163
+
164
+ # Whether one instance of the job should be running or enqueued.
165
+ #
166
+ # @return [TrueClass || FalseClass]
167
+ def loner
168
+ @loner ||= self.class.loner || false
169
+ end
170
+
171
+ # Checks if job is locked or loner locked (if applicable).
172
+ #
173
+ # @return [Boolean] true if the job is locked by someone
174
+ def loner_locked?(*args)
175
+ locked?(*args) || (loner && enqueued?(*args))
176
+ end
177
+
178
+ # Convenience method to check if job is locked and lock did not expire.
179
+ #
180
+ # @return [Boolean] true if the job is locked by someone
181
+ def locked?(*args)
182
+ inspect_lock(:redis_lock_key, *args)
183
+ end
184
+
185
+ # Convenience method to check if a loner job is queued and lock did not expire.
186
+ #
187
+ # @return [Boolean] true if the job is already queued
188
+ def enqueued?(*args)
189
+ inspect_lock(:redis_loner_lock_key, *args)
190
+ end
191
+
192
+ # Check for existence of given key.
193
+ #
194
+ # @param [Array] args job arguments
195
+ # @param [Symbol] lock_key_method the method returning redis key to lock
196
+ # @return [Boolean] true if the lock exists
197
+ def inspect_lock(lock_key_method, *args)
198
+ lock_until = lock_redis.get(self.send(lock_key_method, *args))
199
+ return (lock_until.to_i > Time.now.to_i) if lock_timeout > 0
200
+ !lock_until.nil?
201
+ end
202
+
203
+ # @abstract
204
+ # Hook method; called when unable to acquire the lock.
205
+ #
206
+ # @param [Array] args job arguments
207
+ def lock_failed(*args)
208
+ end
209
+
210
+ # @abstract
211
+ # Hook method; called when unable to enqueue loner job.
212
+ #
213
+ # @param [Array] args job arguments
214
+ def loner_enqueue_failed(*args)
215
+ end
216
+
217
+ # @abstract
218
+ # Hook method; called when the lock expired before we released it.
219
+ #
220
+ # @param [Array] args job arguments
221
+ def lock_expired_before_release(*args)
222
+ end
223
+
224
+ # Try to acquire a lock for running the job.
225
+ # @return [Boolean, Fixnum]
226
+ def acquire_lock!(*args)
227
+ acquire_lock_impl!(:redis_lock_key, :lock_failed, *args)
228
+ end
229
+
230
+ # Try to acquire a lock to enqueue a loner job.
231
+ # @return [Boolean, Fixnum]
232
+ def acquire_loner_lock!(*args)
233
+ acquire_lock_impl!(:redis_loner_lock_key, :loner_enqueue_failed, *args)
234
+ end
235
+
236
+ # Generic implementation of the locking logic
237
+ #
238
+ # Returns false; when unable to acquire the lock.
239
+ # * Returns true; when lock acquired, without a timeout.
240
+ # * Returns timestamp; when lock acquired with a timeout, timestamp is
241
+ # when the lock timeout expires.
242
+ #
243
+ # @param [Symbol] lock_key_method the method returning redis key to lock
244
+ # @param [Symbol] failed_hook the method called if lock failed
245
+ # @param [Array] args job arguments
246
+ # @return [Boolean, Fixnum]
247
+ def acquire_lock_impl!(lock_key_method, failed_hook, *args)
248
+ acquired = false
249
+ lock_key = self.send(lock_key_method, *args)
250
+
251
+ unless lock_timeout > 0
252
+ # Acquire without using a timeout.
253
+ acquired = true if lock_redis.setnx(lock_key, true)
254
+ else
255
+ # Acquire using the timeout algorithm.
256
+ acquired, lock_until = acquire_lock_algorithm!(lock_key, *args)
257
+ end
258
+
259
+ self.send(failed_hook, *args) if !acquired
260
+ lock_until && acquired ? lock_until : acquired
261
+ end
262
+
263
+ # Attempts to acquire the lock using a timeout / deadlock algorithm.
264
+ #
265
+ # Locking algorithm: http://code.google.com/p/redis/wiki/SetnxCommand
266
+ #
267
+ # @param [String] lock_key redis lock key
268
+ # @param [Array] args job arguments
269
+ def acquire_lock_algorithm!(lock_key, *args)
270
+ now = Time.now.to_i
271
+ lock_until = now + lock_timeout
272
+ acquired = false
273
+
274
+ return [true, lock_until] if lock_redis.setnx(lock_key, lock_until)
275
+ # Can't acquire the lock, see if it has expired.
276
+ lock_expiration = lock_redis.get(lock_key)
277
+ if lock_expiration && lock_expiration.to_i < now
278
+ # expired, try to acquire.
279
+ lock_expiration = lock_redis.getset(lock_key, lock_until)
280
+ if lock_expiration.nil? || lock_expiration.to_i < now
281
+ acquired = true
282
+ end
283
+ else
284
+ # Try once more...
285
+ acquired = true if lock_redis.setnx(lock_key, lock_until)
286
+ end
287
+
288
+ [acquired, lock_until]
289
+ end
290
+
291
+ # Release the lock.
292
+ #
293
+ # @param [Array] args job arguments
294
+ def release_lock!(*args)
295
+ lock_redis.del(redis_lock_key(*args))
296
+ end
297
+
298
+ # Release the enqueue lock for loner jobs
299
+ #
300
+ # @param [Array] args job arguments
301
+ def release_loner_lock!(*args)
302
+ lock_redis.del(redis_loner_lock_key(*args))
303
+ end
304
+
305
+ # Refresh the lock.
306
+ #
307
+ # @param [Array] args job arguments
308
+ def refresh_lock!(*args)
309
+ now = Time.now.to_i
310
+ lock_until = now + lock_timeout
311
+ lock_redis.set(redis_lock_key(*args), lock_until)
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveJobLock
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,60 @@
1
+ require_relative 'test_helper'
2
+
3
+ class IntegrationTest < Minitest::Test
4
+ include ActiveJob::TestHelper
5
+
6
+ def setup
7
+ @redis = ActiveJobLock::Config.redis
8
+ @redis.flushall
9
+ end
10
+
11
+ def test_jobs_same_args_running_in_parallel
12
+ r1 = r2 = nil
13
+
14
+ [
15
+ Thread.new { r1 = SlowJob.perform_now(1) },
16
+ Thread.new { sleep 0.01; r2 = SlowJob.perform_now(1) }
17
+ ].map(&:join)
18
+
19
+ assert_equal :performed, r1, 'First job should have been performed'
20
+ assert_equal nil, r2, 'Second job should not have been performed because first job had the lock'
21
+ end
22
+
23
+ def test_jobs_diff_args_running_in_parallel
24
+ r1 = r2 = nil
25
+
26
+ [
27
+ Thread.new { r1 = SlowJob.perform_now(1) },
28
+ Thread.new { sleep 0.01; r2 = SlowJob.perform_now(2) }
29
+ ].map(&:join)
30
+
31
+ assert_equal :performed, r1, 'First job should have been performed'
32
+ assert_equal :performed, r2, 'Second job should have been performed'
33
+ end
34
+
35
+ def test_jobs_same_args_running_in_parallel_with_timeout
36
+ r1 = r2 = nil
37
+
38
+ [
39
+ Thread.new { r1 = SuperSlowWithTimeoutJob.perform_now(1) },
40
+ Thread.new { sleep 2; r2 = SuperSlowWithTimeoutJob.perform_now(1) }
41
+ ].map(&:join)
42
+
43
+ assert_equal :performed, r1, 'First job should have been performed'
44
+ assert_equal :performed, r2, 'Second job should have been performed because the timeout has passed'
45
+ end
46
+
47
+ def test_lock_releasing_with_failing_jobs
48
+ FastJob.to_fail = true
49
+ assert_raises { FastJob.perform_now(1) }
50
+ FastJob.to_fail = false
51
+ assert_equal :performed, FastJob.perform_now(1), 'Should have been performed assuming that the first job released the lock'
52
+ end
53
+
54
+ def test_loner_jobs
55
+ LonerJob.perform_later
56
+ assert_enqueued_jobs 1
57
+ LonerJob.perform_later
58
+ assert_enqueued_jobs 1
59
+ end
60
+ end
@@ -0,0 +1,115 @@
1
+ # Redis configuration file example
2
+
3
+ # By default Redis does not run as a daemon. Use 'yes' if you need it.
4
+ # Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
5
+ daemonize yes
6
+
7
+ # When run as a daemon, Redis write a pid file in /var/run/redis.pid by default.
8
+ # You can specify a custom pid file location here.
9
+ #pidfile ./test/redis-test.pid
10
+
11
+ # Accept connections on the specified port, default is 6379
12
+ port 6379
13
+
14
+ # If you want you can bind a single interface, if the bind option is not
15
+ # specified all the interfaces will listen for connections.
16
+ #
17
+ # bind 127.0.0.1
18
+
19
+ # Close the connection after a client is idle for N seconds (0 to disable)
20
+ timeout 300
21
+
22
+ # Save the DB on disk:
23
+ #
24
+ # save <seconds> <changes>
25
+ #
26
+ # Will save the DB if both the given number of seconds and the given
27
+ # number of write operations against the DB occurred.
28
+ #
29
+ # In the example below the behaviour will be to save:
30
+ # after 900 sec (15 min) if at least 1 key changed
31
+ # after 300 sec (5 min) if at least 10 keys changed
32
+ # after 60 sec if at least 10000 keys changed
33
+ #save 900 1
34
+ #save 300 10
35
+ #save 60 10000
36
+
37
+ # The filename where to dump the DB
38
+ #dbfilename dump.rdb
39
+
40
+ # For default save/load DB in/from the working directory
41
+ # Note that you must specify a directory not a file name.
42
+ #dir ./test/
43
+
44
+ # Set server verbosity to 'debug'
45
+ # it can be one of:
46
+ # debug (a lot of information, useful for development/testing)
47
+ # notice (moderately verbose, what you want in production probably)
48
+ # warning (only very important / critical messages are logged)
49
+ loglevel debug
50
+
51
+ # Specify the log file name. Also 'stdout' can be used to force
52
+ # the demon to log on the standard output. Note that if you use standard
53
+ # output for logging but daemonize, logs will be sent to /dev/null
54
+ logfile stdout
55
+
56
+ # Set the number of databases. The default database is DB 0, you can select
57
+ # a different one on a per-connection basis using SELECT <dbid> where
58
+ # dbid is a number between 0 and 'databases'-1
59
+ databases 16
60
+
61
+ ################################# REPLICATION #################################
62
+
63
+ # Master-Slave replication. Use slaveof to make a Redis instance a copy of
64
+ # another Redis server. Note that the configuration is local to the slave
65
+ # so for example it is possible to configure the slave to save the DB with a
66
+ # different interval, or to listen to another port, and so on.
67
+
68
+ # slaveof <masterip> <masterport>
69
+
70
+ ################################## SECURITY ###################################
71
+
72
+ # Require clients to issue AUTH <PASSWORD> before processing any other
73
+ # commands. This might be useful in environments in which you do not trust
74
+ # others with access to the host running redis-server.
75
+ #
76
+ # This should stay commented out for backward compatibility and because most
77
+ # people do not need auth (e.g. they run their own servers).
78
+
79
+ # requirepass foobared
80
+
81
+ ################################### LIMITS ####################################
82
+
83
+ # Set the max number of connected clients at the same time. By default there
84
+ # is no limit, and it's up to the number of file descriptors the Redis process
85
+ # is able to open. The special value '0' means no limts.
86
+ # Once the limit is reached Redis will close all the new connections sending
87
+ # an error 'max number of clients reached'.
88
+
89
+ # maxclients 128
90
+
91
+ # Don't use more memory than the specified amount of bytes.
92
+ # When the memory limit is reached Redis will try to remove keys with an
93
+ # EXPIRE set. It will try to start freeing keys that are going to expire
94
+ # in little time and preserve keys with a longer time to live.
95
+ # Redis will also try to remove objects from free lists if possible.
96
+ #
97
+ # If all this fails, Redis will start to reply with errors to commands
98
+ # that will use more memory, like SET, LPUSH, and so on, and will continue
99
+ # to reply to most read-only commands like GET.
100
+ #
101
+ # WARNING: maxmemory can be a good idea mainly if you want to use Redis as a
102
+ # 'state' server or cache, not as a real DB. When Redis is used as a real
103
+ # database the memory usage will grow over the weeks, it will be obvious if
104
+ # it is going to use too much memory in the long run, and you'll have the time
105
+ # to upgrade. With maxmemory after the limit is reached you'll start to get
106
+ # errors for write operations, and this may even lead to DB inconsistency.
107
+
108
+ # maxmemory <bytes>
109
+
110
+ ############################### ADVANCED CONFIG ###############################
111
+
112
+ # Glue small output buffers together in order to send small replies in a
113
+ # single TCP packet. Uses a bit more CPU but most of the times it is a win
114
+ # in terms of number of queries per second. Use 'yes' if unsure.
115
+ # glueoutputbuf yes
@@ -0,0 +1,30 @@
1
+ require 'minitest/pride'
2
+ require 'minitest/autorun'
3
+
4
+ require 'redis'
5
+ require 'active_job'
6
+ require 'active_job_lock'
7
+ require_relative 'test_jobs'
8
+
9
+ # make sure we can run redis-server
10
+ if !system('which redis-server')
11
+ puts '', "** `redis-server` was not found in your PATH"
12
+ abort ''
13
+ end
14
+
15
+ # make sure we can shutdown the server using cli.
16
+ if !system('which redis-cli')
17
+ puts '', "** `redis-cli` was not found in your PATH"
18
+ abort ''
19
+ end
20
+
21
+ puts "Starting redis for testing at localhost:6379..."
22
+
23
+ # Start redis server for testing.
24
+ `redis-server ./redis-test.conf`
25
+ ActiveJobLock::Config.redis = Redis.new(host: '127.0.0.1', port: '6379')
26
+
27
+ # After tests are complete, make sure we shutdown redis.
28
+ Minitest.after_run {
29
+ `redis-cli -p 9737 shutdown nosave`
30
+ }
@@ -0,0 +1,49 @@
1
+ # Job that executes quickly
2
+ class FastJob < ActiveJob::Base
3
+ include ActiveJobLock::Core
4
+
5
+ class << self
6
+ attr_accessor :to_fail
7
+ end
8
+
9
+ def perform(*_)
10
+ raise if fail?
11
+ :performed
12
+ end
13
+
14
+ def fail?
15
+ self.class.to_fail
16
+ end
17
+ end
18
+
19
+ # Slow successful job, does not use timeout algorithm.
20
+ class SlowJob < ActiveJob::Base
21
+ include ActiveJobLock::Core
22
+
23
+ def perform(*_)
24
+ sleep 1
25
+ return :performed
26
+ end
27
+ end
28
+
29
+ # Job that enables the timeout algorithm.
30
+ class SuperSlowWithTimeoutJob < ActiveJob::Base
31
+ include ActiveJobLock::Core
32
+
33
+ lock timeout: 1
34
+
35
+ def perform(*_)
36
+ sleep 3
37
+ return :performed
38
+ end
39
+ end
40
+
41
+ # Job that can be enqueued one at a time
42
+ class LonerJob < ActiveJob::Base
43
+ include ActiveJobLock::Core
44
+
45
+ lock loner: true
46
+
47
+ def perform(*_)
48
+ end
49
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_job_lock
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Ferraz
8
+ - Luke Antins
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-07-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activejob
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 4.2.0
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 4.2.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: redis
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '3.2'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '3.2'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '10.3'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '10.3'
56
+ - !ruby/object:Gem::Dependency
57
+ name: minitest
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '5.2'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '5.2'
70
+ - !ruby/object:Gem::Dependency
71
+ name: yard
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.8'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0.8'
84
+ description: |2
85
+ An ActiveJob plugin. Adds locking, with optional timeout/deadlock handling.
86
+
87
+ Using a `lock_timeout` allows you to re-acquire the lock should your job
88
+ fail, crash, or is otherwise unable to relase the lock.
89
+
90
+ i.e. Your server unexpectedly looses power. Very handy for jobs that are
91
+ recurring or may be retried.
92
+ email: ''
93
+ executables: []
94
+ extensions: []
95
+ extra_rdoc_files: []
96
+ files:
97
+ - HISTORY.md
98
+ - LICENSE
99
+ - README.md
100
+ - Rakefile
101
+ - lib/active_job_lock.rb
102
+ - lib/active_job_lock/config.rb
103
+ - lib/active_job_lock/core.rb
104
+ - lib/active_job_lock/version.rb
105
+ - test/integration_test.rb
106
+ - test/redis-test.conf
107
+ - test/test_helper.rb
108
+ - test/test_jobs.rb
109
+ homepage: http://github.com/dferrazm/active_job_lock
110
+ licenses:
111
+ - MIT
112
+ metadata: {}
113
+ post_install_message:
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubyforge_project:
129
+ rubygems_version: 2.5.1
130
+ signing_key:
131
+ specification_version: 4
132
+ summary: An ActiveJob plugin to add locking, with optional timeout/deadlock handling.
133
+ test_files: []
134
+ has_rdoc: false