activejob-locking 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.
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: []