active_job_lock 0.1.0

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: 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