activejob-locking 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cd8286a266d71bc225867b645c8e6839d492d1a3
4
+ data.tar.gz: ea9464a6fcd179539542168ab5f40f6a86e76d1d
5
+ SHA512:
6
+ metadata.gz: a20e9295791ae6e571afda6c7d243003cd601c1bf1f403bb757ce5c2022cbe34ecb0b6f7d64ea243021078b9cfa9e783bd4b1b0f6ec374e109d3a7d88a83415d
7
+ data.tar.gz: 25ea633bf9c6386a7d8e98b3008cb8cc4a85a865637165acfa0f0ea12b283269257d04b185114609d55f616ce7473b5fdbe21989ff222cef6a193342483f9a92
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activejob', :require => 'active_job'
4
+
5
+ group :test do
6
+ gem 'minitest'
7
+ gem 'redis-semaphore'
8
+ gem 'redlock'
9
+ gem 'suo'
10
+ end
data/HISTORY.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 (2017-01-16)
2
+
3
+ - Initial release
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2017 Charlie Savage
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,245 @@
1
+ ActiveJob Locking
2
+ ===================
3
+
4
+ [![Build Status](https://secure.travis-ci.org/lantins/activejob-locking.png?branch=master)](http://travis-ci.org/cfis/activejob-locking)
5
+ [![Gem Version](https://badge.fury.io/rb/activejob-locking.png)](http://badge.fury.io/rb/activejob-locking)
6
+
7
+ activejob-locking lets you control how ActiveJobs are enqueued and performed:
8
+
9
+ * Allow only one job to be enqueued at a time (based on a lock_id)
10
+ * Allow only one job to be peformed at a time (also based on a lock_id)
11
+
12
+ There are many other similar gems including [resque-lock-timeout](https://github.com/lantins/resque-lock-timeout),
13
+ [activejob-traffic-control](https://github.com/nickelser/activejob-traffic_control), [activejob-lock](https://github.com/idolweb/activejob-lock),
14
+ [activejob-locks](https://github.com/erickrause/activejob-locks). What is different about this gem is that it
15
+ is agnostic on the locking mechanism. In the same way that ActiveJob works with many apapters, ActiveJob Locking
16
+ works with a variety of locking gems.
17
+
18
+ Installation
19
+ ------------
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'activejob-locking'
25
+ ```
26
+
27
+ Enqueueing
28
+ ------------
29
+ Sometime you only want to enqueue one instance of a job. No other similar job should be enqueued until the first one
30
+ is completed.
31
+
32
+ ```ruby
33
+ class EnqueueDropJob < ActiveJob::Base
34
+ include ActiveJob::Locking::Enqueue
35
+
36
+ # Make sure the lock_key is always the same
37
+ def lock_key
38
+ self.class.name
39
+ end
40
+
41
+ def perform
42
+ # do some work
43
+ end
44
+ end
45
+ ```
46
+ Only one instance of this job will ever be enqueued. If an additional job is enqueued, it will either be dropped and
47
+ never be enqueued or it will wait to the first job is performed. That is controlled by the job
48
+ [options](##options) described below.
49
+
50
+
51
+ Performing
52
+ ------------
53
+ Sometime you only want to perform one instance of a job at a time. No other similar job should be performed until the first one
54
+ is completed.
55
+
56
+ ```ruby
57
+ class EnqueueDropJob < ActiveJob::Base
58
+ include ActiveJob::Locking::Perform
59
+
60
+ # Make sure the lock_key is always the same
61
+ def lock_key
62
+ self.class.name
63
+ end
64
+
65
+ def perform
66
+ # do some work
67
+ end
68
+ end
69
+ ```
70
+ Only one instance of this job will ever be performed. If an additional job is enqueued, it will wait in its que until
71
+ to the first job is performed.
72
+
73
+ Locking
74
+ ------------
75
+ Locks are used to control how jobs are enqueued and performed. The idea is that locks are stored in a distributed
76
+ system such as [Redis](https://redis.io/) or [Memcached](https://memcached.org/) so they can be used by
77
+ multiple servers to coordinate the enqueueing and performing of jobs.
78
+
79
+ The ActiveJob Locking gem does not include a locking implementation. Instead it provides adapters for
80
+ distributed locking gems.
81
+
82
+ Currently three gems are supported:
83
+
84
+ * [redis-semaphore](https://github.com/dv/redis-semaphore)
85
+
86
+ * [suo](https://github.com/nickelser/suo)
87
+
88
+ * [redlock-rb](https://github.com/leandromoreira/redlock-rb)
89
+
90
+ If you would like to have an additional locking mechanism supported, please feel free to send in a pull request.
91
+
92
+ Please see the [options](##options) section below on how to specify a locking adapter.
93
+
94
+
95
+ Lock Key
96
+ ---------
97
+
98
+ Notice that the code samples above include a `lock_key` method. The return value of this method is used by the
99
+ gem to create locks behind the scenes. Thus it holds the key (pun intended) to controlling how jobs are enqueued
100
+ and performed.
101
+
102
+ By default the key is defined as:
103
+
104
+ ```ruby
105
+ def lock_key
106
+ [self.class.name, serialize_arguments(self.arguments)].join('/')
107
+ end
108
+ ```
109
+ Thus it has the format `<job class name>/<serialized_job_arguments>`
110
+
111
+ To use this gem, you will want to override this method per job.
112
+
113
+ ### Examples
114
+
115
+ Allow only one job per queue to be enqueued or performed:
116
+
117
+ ```ruby
118
+ def lock_key
119
+ self.queue
120
+ end
121
+ ```
122
+
123
+ Allow only one instance of a job class to be enqueued of performed:
124
+
125
+ ```ruby
126
+ def lock_key
127
+ self.class.name
128
+ end
129
+ ```
130
+
131
+ Options
132
+ -------
133
+ The locking behavior can be dramatically changed by tweaking various options. There is a global set of options
134
+ available at:
135
+
136
+ ```ruby
137
+ ActiveJob::Locking.options
138
+ ```
139
+ This should be updated using a Rails initializer. Each job class can override invidual options as it sees fit.
140
+
141
+ ### Adapter
142
+
143
+ Use the adapter option to specify which locking gem to use.
144
+
145
+ Globally update:
146
+
147
+ ```ruby
148
+ ActiveJob::Locking.options.adapter = ActiveJob::Locking::Adapters::SuoRedis
149
+ ```
150
+ Locally update:
151
+
152
+ ```ruby
153
+ class ExampleJob < ActiveJob::Base
154
+ include ActiveJob::Locking::Perform
155
+
156
+ self.adapter = ActiveJob::Locking::Adapters::SuoRedis
157
+ end
158
+ ```
159
+
160
+ ### Host
161
+
162
+ The host(s) of the distributed system. This format will be dependent on the locking gem, so please read its
163
+ documentation.
164
+
165
+ Globally update:
166
+
167
+ ```ruby
168
+ ActiveJob::Locking.options.hosts = 'localhost'
169
+ ```
170
+ Locally update:
171
+
172
+ ```ruby
173
+ class ExampleJob < ActiveJob::Base
174
+ include ActiveJob::Locking::Perform
175
+
176
+ self.hosts = 'localhost'
177
+ end
178
+ ```
179
+
180
+ ### Time
181
+
182
+ The is the time to live for any acquired locks. For most locking gems this is mapped to their concept of "stale" locks.
183
+ That means that if an attempt is made to access the lock after it is expired, it will be considered unlocked. That is in
184
+ contrast to aggressively removing locks for running jobs even if no other job has requested them.
185
+
186
+ The value is specified in seconds and defaults to 100.
187
+
188
+ Globally update:
189
+
190
+ ```ruby
191
+ ActiveJob::Locking.options.time = 100
192
+ ```
193
+ Locally update (notice the different method name to avoid potential conflicts):
194
+
195
+ ```ruby
196
+ class ExampleJob < ActiveJob::Base
197
+ include ActiveJob::Locking::Perform
198
+
199
+ self.lock_time = 100
200
+ end
201
+ ```
202
+
203
+ ### Timeout
204
+
205
+ The is the timeout for acquiring a lock. The value is specified in seconds and defaults to 1. It must
206
+ be greater than zero and cannot be nil.
207
+
208
+ Globally update:
209
+
210
+ ```ruby
211
+ ActiveJob::Locking.options.timeout = 1
212
+ ```
213
+ Locally update (notice the different method name to avoid potential conflicts):
214
+
215
+ ```ruby
216
+ class ExampleJob < ActiveJob::Base
217
+ include ActiveJob::Locking::Enqueue
218
+
219
+ self.lock_acquire_timeout= = 1
220
+ end
221
+ ```
222
+ This greatly influences how enqueuing behavior works. If the timeout is short, then jobs that are waiting to
223
+ be enqueued are dropped and the before_enqueue callback will fail. If the timeout is infinite, then jobs will wait
224
+ in turn to get enqueued. If the timeout is somewhere in between then it will depend on how long the jobs
225
+ take to execute.
226
+
227
+ ### AdapterOptions
228
+
229
+ This is a hash table of options that should be sent to the lock gem when it is instantiated. Read the lock
230
+ gems documentation to find appropriate values.
231
+
232
+ Globally update:
233
+
234
+ ```ruby
235
+ ActiveJob::Locking.options.adapter_options = {}
236
+ ```
237
+ Locally update (notice the different method name to avoid potential conflicts):
238
+
239
+ ```ruby
240
+ class ExampleJob < ActiveJob::Base
241
+ include ActiveJob::Locking::Enqueue
242
+
243
+ self.adapter_options = {}
244
+ end
245
+ ```
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rubygems/package_task'
6
+
7
+ # Set global variable so other tasks can access them
8
+ ::PROJECT_ROOT = File.expand_path(".")
9
+ ::GEM_NAME = 'activejob-locking'
10
+
11
+ # Read the spec file
12
+ spec = Gem::Specification.load("#{GEM_NAME}.gemspec")
13
+
14
+ # Setup Rake tasks for managing the gem
15
+ Gem::PackageTask.new(spec).define
16
+
17
+ desc 'Run unit tests.'
18
+ Rake::TestTask.new(:test) do |task|
19
+ task.test_files = FileList['test/test_*.rb']
20
+ task.verbose = true
21
+ end
@@ -0,0 +1,32 @@
1
+ module ActiveJob
2
+ module Locking
3
+ module Adapters
4
+ class Base
5
+ attr_reader :key, :options, :lock_manager
6
+ attr_accessor :lock_token
7
+
8
+ def initialize(key, options)
9
+ @key = key
10
+ @options = options
11
+ @lock_manager = self.create_lock_manager
12
+ end
13
+
14
+ def create_lock_manager
15
+ raise('Subclass must implement')
16
+ end
17
+
18
+ def lock
19
+ raise('Subclass must implement')
20
+ end
21
+
22
+ def unlock
23
+ raise('Subclass must implement')
24
+ end
25
+
26
+ def refresh_lock!(refresh)
27
+ raise('Subclass must implement')
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,60 @@
1
+ module ActiveJob
2
+ module Locking
3
+ module Adapters
4
+ class Memory < Base
5
+ attr_reader :timeout
6
+
7
+ @hash = Hash.new
8
+ @mutex = Mutex.new
9
+
10
+ def self.lock(key)
11
+ @mutex.synchronize do
12
+ if @hash[key]
13
+ false
14
+ else
15
+ @hash[key] = Time.now
16
+ end
17
+ end
18
+ end
19
+
20
+ def self.unlock(key)
21
+ @mutex.synchronize do
22
+ @hash.delete(key)
23
+ end
24
+ end
25
+
26
+ def self.locked?(key)
27
+ @mutex.synchronize do
28
+ @hash.include?(key)
29
+ end
30
+ end
31
+
32
+ def self.reset
33
+ @mutex.synchronize do
34
+ @hash = Hash.new
35
+ end
36
+ end
37
+
38
+ def create_lock_manager
39
+ end
40
+
41
+ def lock
42
+ finish = Time.now + self.options.timeout
43
+ sleep_time = [5, self.options.timeout / 5].min
44
+
45
+ begin
46
+ lock = self.class.lock(key)
47
+ return lock if lock
48
+ sleep(sleep_time)
49
+ end while Time.now < finish
50
+
51
+ return false
52
+ end
53
+
54
+ def unlock
55
+ self.class.unlock(self.key)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ require 'redis-semaphore'
2
+
3
+ module ActiveJob
4
+ module Locking
5
+ module Adapters
6
+ class RedisSemaphore < Base
7
+ def create_lock_manager
8
+ mapped_options = {host: self.options.hosts,
9
+ stale_client_timeout: self.options.time}.merge(self.options.adapter_options)
10
+
11
+ Redis::Semaphore.new(self.key, mapped_options)
12
+ end
13
+
14
+ def lock
15
+ self.lock_token = self.lock_manager.lock(self.options.timeout)
16
+ end
17
+
18
+ def unlock
19
+ self.lock_manager.signal(self.lock_token)
20
+ self.lock_token = nil
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ require 'redlock'
2
+
3
+ module ActiveJob
4
+ module Locking
5
+ module Adapters
6
+ class Redlock < Base
7
+ def create_lock_manager
8
+ mapped_options = self.options.adapter_options
9
+ mapped_options[:retry_count] = ::Redlock::Client::DEFAULT_RETRY_COUNT
10
+ mapped_options[:retry_delay] = 2000 * ((self.options.timeout || 2**32) / (::Redlock::Client::DEFAULT_RETRY_COUNT * 1.0))
11
+
12
+ ::Redlock::Client.new(Array(self.options.hosts), mapped_options)
13
+ end
14
+
15
+ def lock
16
+ self.lock_token = self.lock_manager.lock(self.key, self.options.time * 1000)
17
+ end
18
+
19
+ def unlock
20
+ self.lock_manager.unlock(self.lock_token)
21
+ self.lock_token = nil
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ require 'suo'
2
+
3
+ module ActiveJob
4
+ module Locking
5
+ module Adapters
6
+ class SuoRedis < Base
7
+ def create_lock_manager
8
+ mapped_options = {connection: {host: self.options.hosts},
9
+ stale_lock_expiration: self.options.time,
10
+ acquisition_timeout: self.options.timeout}
11
+
12
+ Suo::Client::Redis.new(self.key, mapped_options)
13
+ end
14
+
15
+ def lock
16
+ self.lock_token = self.lock_manager.lock
17
+ end
18
+
19
+ def unlock
20
+ self.lock_manager.unlock(self.lock_token)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ module ActiveJob
2
+ module Locking
3
+ module Base
4
+ extend ::ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def lock_options
8
+ @lock_options ||= ActiveJob::Locking::Options.new
9
+ end
10
+ delegate :adapter, :hosts, :lock_time, :lock_acquire_timeout, :adapter_options, to: :lock_options
11
+ delegate :adapter=, :hosts=, :lock_time=, :lock_acquire_timeout=, :adapter_options=, to: :lock_options
12
+ end
13
+
14
+ included do
15
+ # We need to serialize the lock token because it could be released in a different process
16
+ def serialize
17
+ result = super
18
+ result = result.merge('lock_token' => self.adapter.lock_token) if self.adapter.lock_token
19
+ result
20
+ end
21
+
22
+ def deserialize(job_data)
23
+ super
24
+ self.adapter.lock_token = job_data['lock_token']
25
+ end
26
+
27
+ def lock_key
28
+ [self.class.name, serialize_arguments(self.arguments)].join('/')
29
+ end
30
+
31
+ def adapter
32
+ # Merge local and global options
33
+ merged_options = ActiveJob::Locking.options.dup.merge(self.class.lock_options)
34
+
35
+ # Remember the lock might be acquired in one process and released in another
36
+ @adapter ||= merged_options.adapter.new(self.lock_key, merged_options)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ module ActiveJob
2
+ module Locking
3
+ module Enqueue
4
+ extend ::ActiveSupport::Concern
5
+
6
+ included do
7
+ include ::ActiveJob::Locking::Base
8
+
9
+ before_enqueue do |job|
10
+ lock = self.adapter.lock
11
+ throw :abort unless lock
12
+ end
13
+
14
+ after_perform do |job|
15
+ self.adapter.unlock
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ require 'ostruct'
2
+
3
+ module ActiveJob
4
+ module Locking
5
+ class Options
6
+ attr_accessor :adapter
7
+ attr_accessor :hosts
8
+ attr_accessor :time
9
+ attr_accessor :timeout
10
+ attr_accessor :adapter_options
11
+
12
+ alias :lock_time :time
13
+ alias :lock_time= :time=
14
+ alias :lock_acquire_timeout :timeout
15
+ alias :lock_acquire_timeout= :timeout=
16
+
17
+ def initialize(options = {})
18
+ @adapter = options[:adapter]
19
+ @hosts = options[:hosts]
20
+ @time = options[:time]
21
+ @timeout = options[:timeout]
22
+ @adapter_options = options[:adapter_options]
23
+ end
24
+
25
+ def timeout=(value)
26
+ if value.nil?
27
+ raise(ArgumentError, 'Lock timeout must be set')
28
+ elsif value == 0
29
+ raise(ArgumentError, 'Lock timeout must be greater than zero')
30
+ else
31
+ @timeout = value
32
+ end
33
+ end
34
+
35
+ def merge(other)
36
+ result = self.dup
37
+ result.adapter = other.adapter if other.adapter
38
+ result.hosts = other.hosts if other.hosts
39
+ result.time = other.time if other.time
40
+ result.timeout = other.timeout if other.timeout
41
+ result.adapter_options = other.adapter_options if other.adapter_options
42
+ result
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveJob
2
+ module Locking
3
+ module Perform
4
+ extend ::ActiveSupport::Concern
5
+
6
+ included do
7
+ include ::ActiveJob::Locking::Base
8
+
9
+ around_perform do |job, block|
10
+ if self.adapter.lock
11
+ begin
12
+ block.call
13
+ ensure
14
+ self.adapter.unlock
15
+ end
16
+ else
17
+ self.class.set(wait: 5.seconds).perform_later(job)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ require 'activejob/locking/adapters/base'
2
+ require 'activejob/locking/adapters/memory'
3
+
4
+ require 'activejob/locking/base'
5
+ require 'activejob/locking/enqueue'
6
+ require 'activejob/locking/perform'
7
+
8
+ require 'activejob/locking/options'
9
+
10
+ module ActiveJob
11
+ module Locking
12
+ @options = ActiveJob::Locking::Options.new(adapter: ActiveJob::Locking::Adapters::Memory,
13
+ hosts: 'localhost',
14
+ time: 100,
15
+ timeout: 1,
16
+ adapter_options: {})
17
+
18
+ def self.options
19
+ @options
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,74 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ module EnqueueTests
4
+ def test_drop
5
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
6
+
7
+ start_time = Time.now
8
+ sleep_time = 2
9
+ threads = 2.times.map do |i|
10
+ Thread.new do
11
+ EnqueueDropJob.perform_later(i, sleep_time)
12
+ end
13
+ end
14
+
15
+ threads.each {|thread| thread.join}
16
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
17
+ assert_equal(1, ActiveJob::Base.queue_adapter.performed_jobs.count)
18
+ assert(Time.now - start_time > (1 * sleep_time))
19
+ end
20
+
21
+ def test_wait
22
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
23
+
24
+ start_time = Time.now
25
+ sleep_time = 2
26
+ threads = 3.times.map do |i|
27
+ Thread.new do
28
+ EnqueueWaitJob.perform_later(i, sleep_time)
29
+ end
30
+ end
31
+
32
+ threads.each {|thread| thread.join}
33
+
34
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
35
+ assert_equal(threads.count, ActiveJob::Base.queue_adapter.performed_jobs.count)
36
+ assert(Time.now - start_time > (threads.count * sleep_time))
37
+ end
38
+
39
+ def test_wait_large_timeout
40
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
41
+
42
+ start_time = Time.now
43
+ sleep_time = 2 * EnqueueWaitTimeoutJob.lock_acquire_timeout
44
+ threads = 3.times.map do |i|
45
+ Thread.new do
46
+ EnqueueWaitTimeoutJob.perform_later(i, sleep_time)
47
+ end
48
+ end
49
+
50
+ threads.each {|thread| thread.join}
51
+
52
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
53
+ assert_equal(1, ActiveJob::Base.queue_adapter.performed_jobs.count)
54
+ assert(Time.now - start_time > (1 * sleep_time))
55
+ end
56
+
57
+ def test_wait_timeout
58
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
59
+
60
+ start_time = Time.now
61
+ sleep_time = 0.2 * EnqueueWaitLargeTimeoutJob.lock_acquire_timeout
62
+ threads = 3.times.map do |i|
63
+ Thread.new do
64
+ EnqueueWaitLargeTimeoutJob.perform_later(i, sleep_time)
65
+ end
66
+ end
67
+
68
+ threads.each {|thread| thread.join}
69
+
70
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
71
+ assert_equal(threads.count, ActiveJob::Base.queue_adapter.performed_jobs.count)
72
+ assert(Time.now - start_time > (threads.count * sleep_time))
73
+ end
74
+ end
@@ -0,0 +1,15 @@
1
+ class EnqueueDropJob < ActiveJob::Base
2
+ include ActiveJob::Locking::Enqueue
3
+
4
+ self.lock_acquire_timeout = 0.1
5
+
6
+ # We want the job ids to be all the same for testing
7
+ def lock_key
8
+ self.class.name
9
+ end
10
+
11
+ # Pass in index so we can distinguish different jobs
12
+ def perform(index, sleep_time)
13
+ sleep(sleep_time)
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ class EnqueueWaitJob < ActiveJob::Base
2
+ include ActiveJob::Locking::Enqueue
3
+
4
+ self.lock_acquire_timeout = 1.hour
5
+
6
+ # We want the job ids to be all the same for testing
7
+ def lock_key
8
+ self.class.name
9
+ end
10
+
11
+ # Pass in index so we can distinguish different jobs
12
+ def perform(index, sleep_time)
13
+ sleep(sleep_time)
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ class EnqueueWaitLargeTimeoutJob < ActiveJob::Base
2
+ include ActiveJob::Locking::Enqueue
3
+
4
+ # Wait for 10 seconds to get a lock
5
+ self.lock_acquire_timeout = 10
6
+
7
+ # We want the job ids to be all the same for testing
8
+ def lock_key
9
+ self.class.name
10
+ end
11
+
12
+ # Pass in index so we can distinguish different jobs
13
+ def perform(index, sleep_time)
14
+ sleep(sleep_time)
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ class EnqueueWaitTimeoutJob < ActiveJob::Base
2
+ include ActiveJob::Locking::Enqueue
3
+
4
+ # Wait for 1 second to get a lock
5
+ self.lock_acquire_timeout = 1
6
+
7
+ # We want the job ids to be all the same for testing
8
+ def lock_key
9
+ self.class.name
10
+ end
11
+
12
+ # Pass in index so we can distinguish different jobs
13
+ def perform(index, sleep_time)
14
+ sleep(sleep_time)
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ class PerformSeriallyJob < ActiveJob::Base
2
+ include ActiveJob::Locking::Perform
3
+
4
+ self.lock_acquire_timeout = 1.hour
5
+
6
+ # We want the job ids to be all the same for testing
7
+ def lock_key
8
+ self.class.name
9
+ end
10
+
11
+ # Pass in index so we can distinguish different jobs
12
+ def perform(index, sleep_time)
13
+ sleep(sleep_time)
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ class PerformSeriallyLargeTimeoutJob < ActiveJob::Base
2
+ include ActiveJob::Locking::Enqueue
3
+
4
+ # Wait for 10 seconds to get a lock
5
+ self.lock_acquire_timeout = 10
6
+
7
+ # We want the job ids to be all the same for testing
8
+ def lock_key
9
+ self.class.name
10
+ end
11
+
12
+ # Pass in index so we can distinguish different jobs
13
+ def perform(index, sleep_time)
14
+ sleep(sleep_time)
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ module PerformTests
4
+ def test_serialize
5
+ start_time = Time.now
6
+ sleep_time = 2
7
+ threads = 3.times.map do |i|
8
+ Thread.new do
9
+ PerformSeriallyJob.perform_later(i, sleep_time)
10
+ end
11
+ end
12
+
13
+ threads.each {|thread| thread.join}
14
+
15
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
16
+ assert_equal(threads.count, ActiveJob::Base.queue_adapter.performed_jobs.count)
17
+ assert(Time.now - start_time > (threads.count * sleep_time))
18
+ end
19
+
20
+ def test_wait_large_timeout
21
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
22
+
23
+ start_time = Time.now
24
+ sleep_time = 2 * EnqueueWaitTimeoutJob.lock_acquire_timeout
25
+ threads = 3.times.map do |i|
26
+ Thread.new do
27
+ EnqueueWaitTimeoutJob.perform_later(i, sleep_time)
28
+ end
29
+ end
30
+
31
+ threads.each {|thread| thread.join}
32
+
33
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
34
+ assert_equal(1, ActiveJob::Base.queue_adapter.performed_jobs.count)
35
+ assert(Time.now - start_time > (1 * sleep_time))
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ require_relative('./enqueue_tests')
2
+
3
+ class EnqueueMemoryTest < MiniTest::Test
4
+ include EnqueueTests
5
+
6
+ def setup
7
+ ActiveJob::Base.queue_adapter = :test
8
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
9
+ ActiveJob::Locking.options.adapter = ActiveJob::Locking::Adapters::Memory
10
+ end
11
+
12
+ def test_enqueue_one
13
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = false
14
+ ActiveJob::Locking.options.adapter = ActiveJob::Locking::Adapters::Memory
15
+
16
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = false
17
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
18
+
19
+ sleep_time = 2
20
+ threads = 3.times.map do |i|
21
+ Thread.new do
22
+ EnqueueDropJob.perform_later(i, sleep_time)
23
+ end
24
+ end
25
+
26
+ threads.each {|thread| thread.join}
27
+ assert_equal(1, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
28
+ end
29
+
30
+ def test_perform_one
31
+ ActiveJob::Locking.options.adapter = ActiveJob::Locking::Adapters::Memory
32
+
33
+ assert_equal(0, ActiveJob::Base.queue_adapter.performed_jobs.count)
34
+
35
+ sleep_time = 1
36
+ threads = 3.times.map do |i|
37
+ Thread.new do
38
+ EnqueueDropJob.perform_later(i, sleep_time)
39
+ end
40
+ end
41
+
42
+ threads.each {|thread| thread.join}
43
+ assert_equal(1, ActiveJob::Base.queue_adapter.performed_jobs.count)
44
+ end
45
+ end
@@ -0,0 +1,12 @@
1
+ require_relative('./enqueue_tests')
2
+
3
+ class EnqueueRedisSemaphoreTest < MiniTest::Test
4
+ include EnqueueTests
5
+
6
+ def setup
7
+ redis_reset
8
+ ActiveJob::Base.queue_adapter = :test
9
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
10
+ ActiveJob::Locking.options.adapter = ActiveJob::Locking::Adapters::RedisSemaphore
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ require_relative('./enqueue_tests')
2
+
3
+ class EnqueueRedlockTest < MiniTest::Test
4
+ include EnqueueTests
5
+
6
+ def setup
7
+ redis_reset
8
+
9
+ ActiveJob::Base.queue_adapter = :test
10
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
11
+ ActiveJob::Locking.options.adapter = ActiveJob::Locking::Adapters::Redlock
12
+ ActiveJob::Locking.options.hosts = Redlock::Client::DEFAULT_REDIS_URLS
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ require_relative('./enqueue_tests')
2
+
3
+ class EnqueueSuoRedisTest < MiniTest::Test
4
+ include EnqueueTests
5
+
6
+ def setup
7
+ redis_reset
8
+ ActiveJob::Base.queue_adapter = :test
9
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
10
+ ActiveJob::Locking.options.adapter = ActiveJob::Locking::Adapters::SuoRedis
11
+ end
12
+ end
@@ -0,0 +1,22 @@
1
+ require 'bundler'
2
+ Bundler.require(:default, :test)
3
+
4
+ require 'minitest/autorun'
5
+
6
+ # To make debugging easier, test within this source tree versus an installed gem
7
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
8
+ require 'activejob-locking'
9
+
10
+ require 'activejob/locking/adapters/redis-semaphore'
11
+ require 'activejob/locking/adapters/redlock'
12
+ require 'activejob/locking/adapters/suo-redis'
13
+
14
+ require_relative './jobs/enqueue_drop_job'
15
+ require_relative './jobs/enqueue_wait_job'
16
+ require_relative './jobs/enqueue_wait_timeout_job'
17
+ require_relative './jobs/enqueue_wait_large_timeout_job'
18
+ require_relative './jobs/perform_serially_job'
19
+
20
+ def redis_reset
21
+ Kernel.system('redis-cli FLUSHALL')
22
+ end
@@ -0,0 +1,14 @@
1
+ require_relative('./perform_tests')
2
+
3
+ class PerformMemory < MiniTest::Test
4
+ include PerformTests
5
+
6
+ def setup
7
+ redis_reset
8
+
9
+ ActiveJob::Base.queue_adapter = :test
10
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
11
+ ActiveJob::Locking.options.adapter = ActiveJob::Locking::Adapters::Memory
12
+ ActiveJob::Locking.options.hosts = Redlock::Client::DEFAULT_REDIS_URLS
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ require_relative('./perform_tests')
2
+
3
+ class EnqueueRedisSemaphoreTest < MiniTest::Test
4
+ include PerformTests
5
+
6
+ def setup
7
+ redis_reset
8
+ ActiveJob::Base.queue_adapter = :test
9
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
10
+ ActiveJob::Locking.options.adapter = ActiveJob::Locking::Adapters::RedisSemaphore
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ require_relative('./perform_tests')
2
+
3
+ class PerformRedlockTest < MiniTest::Test
4
+ include PerformTests
5
+
6
+ def setup
7
+ redis_reset
8
+
9
+ ActiveJob::Base.queue_adapter = :test
10
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
11
+ ActiveJob::Locking.options.adapter = ActiveJob::Locking::Adapters::Redlock
12
+ ActiveJob::Locking.options.hosts = Redlock::Client::DEFAULT_REDIS_URLS
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ require_relative('./perform_tests')
2
+
3
+ class PerformSuoRedisTest < MiniTest::Test
4
+ include PerformTests
5
+
6
+ def setup
7
+ redis_reset
8
+ ActiveJob::Base.queue_adapter = :test
9
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
10
+ ActiveJob::Locking.options.adapter = ActiveJob::Locking::Adapters::SuoRedis
11
+ end
12
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activejob-locking
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Charlie Savage
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activejob
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.0.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.0.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">"
32
+ - !ruby/object:Gem::Version
33
+ version: 5.10.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">"
39
+ - !ruby/object:Gem::Version
40
+ version: 5.10.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: redis-semaphore
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: redlock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: suo
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: |
84
+ activejob-locking lets you control how ActiveJobs are enqueued and performed:
85
+
86
+ Allow only one job to be enqueued at a time (based on a lock_id)
87
+ Allow only one job to be peformed at a time (also based on a lock_id)
88
+ email:
89
+ executables: []
90
+ extensions: []
91
+ extra_rdoc_files: []
92
+ files:
93
+ - Gemfile
94
+ - HISTORY.md
95
+ - LICENSE
96
+ - README.md
97
+ - Rakefile
98
+ - lib/activejob-locking.rb
99
+ - lib/activejob/locking/adapters/base.rb
100
+ - lib/activejob/locking/adapters/memory.rb
101
+ - lib/activejob/locking/adapters/redis-semaphore.rb
102
+ - lib/activejob/locking/adapters/redlock.rb
103
+ - lib/activejob/locking/adapters/suo-redis.rb
104
+ - lib/activejob/locking/base.rb
105
+ - lib/activejob/locking/enqueue.rb
106
+ - lib/activejob/locking/options.rb
107
+ - lib/activejob/locking/perform.rb
108
+ - test/enqueue_tests.rb
109
+ - test/jobs/enqueue_drop_job.rb
110
+ - test/jobs/enqueue_wait_job.rb
111
+ - test/jobs/enqueue_wait_large_timeout_job.rb
112
+ - test/jobs/enqueue_wait_timeout_job.rb
113
+ - test/jobs/perform_serially_job.rb
114
+ - test/jobs/perform_serially_large_timeout_job.rb
115
+ - test/perform_tests.rb
116
+ - test/test_enqueue_memory.rb
117
+ - test/test_enqueue_redis_semaphore.rb
118
+ - test/test_enqueue_redlock.rb
119
+ - test/test_enqueue_suo_redis.rb
120
+ - test/test_helper.rb
121
+ - test/test_perform_memory.rb
122
+ - test/test_perform_redis_semaphore.rb
123
+ - test/test_perform_redlock.rb
124
+ - test/test_perform_suo_redis.rb
125
+ homepage: http://github.com/cfis/activejob-locking
126
+ licenses:
127
+ - MIT
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubyforge_project:
145
+ rubygems_version: 2.6.8
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: ActiveJob locking to control how jobs are enqueued and performed.
149
+ test_files: []