activejob-locking 0.1.0 → 0.2.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/HISTORY.md +5 -0
  3. data/README.md +34 -33
  4. data/lib/activejob/locking/adapters/memory.rb +3 -5
  5. data/lib/activejob/locking/adapters/redis-semaphore.rb +3 -2
  6. data/lib/activejob/locking/adapters/redlock.rb +3 -3
  7. data/lib/activejob/locking/adapters/suo-redis.rb +2 -2
  8. data/lib/activejob/locking/base.rb +14 -8
  9. data/lib/activejob/locking/options.rb +29 -18
  10. data/lib/activejob/locking/{perform.rb → serialized.rb} +4 -4
  11. data/lib/activejob/locking/{enqueue.rb → unique.rb} +8 -3
  12. data/lib/activejob-locking.rb +4 -4
  13. data/test/jobs/{perform_serially_job.rb → fail_job.rb} +4 -4
  14. data/test/jobs/{enqueue_drop_job.rb → serial_job.rb} +3 -3
  15. data/test/jobs/{enqueue_wait_job.rb → unique_job.rb} +3 -3
  16. data/test/serialized_tests.rb +63 -0
  17. data/test/test_helper.rb +3 -5
  18. data/test/test_serialized_memory.rb +11 -0
  19. data/test/{test_perform_redis_semaphore.rb → test_serialized_redis_semaphore.rb} +3 -3
  20. data/test/{test_enqueue_redlock.rb → test_serialized_redlock.rb} +3 -3
  21. data/test/{test_perform_suo_redis.rb → test_serialized_suo_redis.rb} +3 -3
  22. data/test/test_suite.rb +12 -0
  23. data/test/test_unique_memory.rb +12 -0
  24. data/test/{test_enqueue_redis_semaphore.rb → test_unique_redis_semaphore.rb} +3 -3
  25. data/test/{test_perform_redlock.rb → test_unique_redlock.rb} +3 -3
  26. data/test/{test_enqueue_suo_redis.rb → test_unique_suo_redis.rb} +3 -3
  27. data/test/unique_tests.rb +100 -0
  28. metadata +34 -22
  29. data/test/enqueue_tests.rb +0 -74
  30. data/test/jobs/enqueue_wait_large_timeout_job.rb +0 -16
  31. data/test/jobs/enqueue_wait_timeout_job.rb +0 -16
  32. data/test/jobs/perform_serially_large_timeout_job.rb +0 -16
  33. data/test/perform_tests.rb +0 -37
  34. data/test/test_enqueue_memory.rb +0 -45
  35. data/test/test_perform_memory.rb +0 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cd8286a266d71bc225867b645c8e6839d492d1a3
4
- data.tar.gz: ea9464a6fcd179539542168ab5f40f6a86e76d1d
3
+ metadata.gz: c1b1ea7063b5406f553e6d4282892299b6733cf5
4
+ data.tar.gz: 4fc8b9e358ec0583dc02eb6e6c069ef079653ad4
5
5
  SHA512:
6
- metadata.gz: a20e9295791ae6e571afda6c7d243003cd601c1bf1f403bb757ce5c2022cbe34ecb0b6f7d64ea243021078b9cfa9e783bd4b1b0f6ec374e109d3a7d88a83415d
7
- data.tar.gz: 25ea633bf9c6386a7d8e98b3008cb8cc4a85a865637165acfa0f0ea12b283269257d04b185114609d55f616ce7473b5fdbe21989ff222cef6a193342483f9a92
6
+ metadata.gz: e84f37fc29ad783441943835f2157e530522dc52ff2e6953c047d029e2d19ad654c6c2f300ae1df277013c7f9e2cca8cd048002e234fb81f518f4572aff667e8
7
+ data.tar.gz: 9f05089c530aa8bcb0db266eda5210126d7d25634462495c448861b9057aa7d9ff037301081b74c1ca90de574fc62b9358bca76d88059c035383903e88a523a2
data/HISTORY.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.2.0 (2017-06-20)
2
+
3
+ - Bug fixes
4
+ - Improved tests
5
+
1
6
  ## 0.1.0 (2017-01-16)
2
7
 
3
8
  - Initial release
data/README.md CHANGED
@@ -6,8 +6,8 @@ ActiveJob Locking
6
6
 
7
7
  activejob-locking lets you control how ActiveJobs are enqueued and performed:
8
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)
9
+ * Allow only one job to be enqueued at a time - thus a "unique" job
10
+ * Allow only one job to be performed at a time - thus a "serialized" job
11
11
 
12
12
  There are many other similar gems including [resque-lock-timeout](https://github.com/lantins/resque-lock-timeout),
13
13
  [activejob-traffic-control](https://github.com/nickelser/activejob-traffic_control), [activejob-lock](https://github.com/idolweb/activejob-lock),
@@ -24,14 +24,14 @@ Add this line to your application's Gemfile:
24
24
  gem 'activejob-locking'
25
25
  ```
26
26
 
27
- Enqueueing
27
+ Unique Jobs
28
28
  ------------
29
29
  Sometime you only want to enqueue one instance of a job. No other similar job should be enqueued until the first one
30
30
  is completed.
31
31
 
32
32
  ```ruby
33
- class EnqueueDropJob < ActiveJob::Base
34
- include ActiveJob::Locking::Enqueue
33
+ class UniqueJob < ActiveJob::Base
34
+ include ActiveJob::Locking::Unique
35
35
 
36
36
  # Make sure the lock_key is always the same
37
37
  def lock_key
@@ -48,14 +48,14 @@ never be enqueued or it will wait to the first job is performed. That is contro
48
48
  [options](##options) described below.
49
49
 
50
50
 
51
- Performing
51
+ Serialized Jobs
52
52
  ------------
53
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
54
  is completed.
55
55
 
56
56
  ```ruby
57
- class EnqueueDropJob < ActiveJob::Base
58
- include ActiveJob::Locking::Perform
57
+ class SerializedJob < ActiveJob::Base
58
+ include ActiveJob::Locking::Serialized
59
59
 
60
60
  # Make sure the lock_key is always the same
61
61
  def lock_key
@@ -136,7 +136,7 @@ available at:
136
136
  ```ruby
137
137
  ActiveJob::Locking.options
138
138
  ```
139
- This should be updated using a Rails initializer. Each job class can override invidual options as it sees fit.
139
+ This should be updated using a Rails initializer. Each job class can override individual options as it sees fit.
140
140
 
141
141
  ### Adapter
142
142
 
@@ -151,7 +151,7 @@ Locally update:
151
151
 
152
152
  ```ruby
153
153
  class ExampleJob < ActiveJob::Base
154
- include ActiveJob::Locking::Perform
154
+ include ActiveJob::Locking::Serialized
155
155
 
156
156
  self.adapter = ActiveJob::Locking::Adapters::SuoRedis
157
157
  end
@@ -171,58 +171,59 @@ Locally update:
171
171
 
172
172
  ```ruby
173
173
  class ExampleJob < ActiveJob::Base
174
- include ActiveJob::Locking::Perform
174
+ include ActiveJob::Locking::Serialized
175
175
 
176
176
  self.hosts = 'localhost'
177
177
  end
178
178
  ```
179
179
 
180
- ### Time
180
+ ### lock_acquire_time
181
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.
182
+ The is the timeout for acquiring a lock. The value is specified in seconds and defaults to 1. It must
183
+ be greater than zero and cannot be nil.
187
184
 
188
185
  Globally update:
189
186
 
190
187
  ```ruby
191
- ActiveJob::Locking.options.time = 100
188
+ ActiveJob::Locking.options.lock_acquire_time = 1
192
189
  ```
193
- Locally update (notice the different method name to avoid potential conflicts):
190
+ Locally update:
194
191
 
195
192
  ```ruby
196
193
  class ExampleJob < ActiveJob::Base
197
- include ActiveJob::Locking::Perform
194
+ include ActiveJob::Locking::Unique
198
195
 
199
- self.lock_time = 100
196
+ self.lock_acquire_time = 1
200
197
  end
201
198
  ```
199
+ This greatly influences how enqueuing behavior works. If the timeout is short, then jobs that are waiting to
200
+ be enqueued are dropped and the before_enqueue callback will fail. If the timeout is infinite, then jobs will wait
201
+ in turn to get enqueued. If the timeout is somewhere in between then it will depend on how long the jobs
202
+ take to execute.
202
203
 
203
- ### Timeout
204
+ ### lock_time
204
205
 
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.
206
+ The is the time to live for any acquired locks. For most locking gems this is mapped to their concept of "stale" locks.
207
+ That means that if an attempt is made to access the lock after it is expired, it will be considered unlocked. That is in
208
+ contrast to aggressively removing locks for running jobs even if no other job has requested them.
209
+
210
+ The value is specified in seconds and defaults to 100.
207
211
 
208
212
  Globally update:
209
213
 
210
214
  ```ruby
211
- ActiveJob::Locking.options.timeout = 1
215
+ ActiveJob::Locking.options.lock_time = 100
212
216
  ```
213
- Locally update (notice the different method name to avoid potential conflicts):
217
+ Locally update:
214
218
 
215
219
  ```ruby
216
220
  class ExampleJob < ActiveJob::Base
217
- include ActiveJob::Locking::Enqueue
221
+ include ActiveJob::Locking::Serialized
218
222
 
219
- self.lock_acquire_timeout= = 1
223
+ self.lock_time = 100
220
224
  end
221
225
  ```
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
+
226
227
 
227
228
  ### AdapterOptions
228
229
 
@@ -238,7 +239,7 @@ Locally update (notice the different method name to avoid potential conflicts):
238
239
 
239
240
  ```ruby
240
241
  class ExampleJob < ActiveJob::Base
241
- include ActiveJob::Locking::Enqueue
242
+ include ActiveJob::Locking::Unique
242
243
 
243
244
  self.adapter_options = {}
244
245
  end
@@ -2,8 +2,6 @@ module ActiveJob
2
2
  module Locking
3
3
  module Adapters
4
4
  class Memory < Base
5
- attr_reader :timeout
6
-
7
5
  @hash = Hash.new
8
6
  @mutex = Mutex.new
9
7
 
@@ -39,8 +37,8 @@ module ActiveJob
39
37
  end
40
38
 
41
39
  def lock
42
- finish = Time.now + self.options.timeout
43
- sleep_time = [5, self.options.timeout / 5].min
40
+ finish = Time.now + self.options.lock_acquire_time
41
+ sleep_time = [5, self.options.lock_acquire_time / 5].min
44
42
 
45
43
  begin
46
44
  lock = self.class.lock(key)
@@ -48,7 +46,7 @@ module ActiveJob
48
46
  sleep(sleep_time)
49
47
  end while Time.now < finish
50
48
 
51
- return false
49
+ false
52
50
  end
53
51
 
54
52
  def unlock
@@ -6,13 +6,14 @@ module ActiveJob
6
6
  class RedisSemaphore < Base
7
7
  def create_lock_manager
8
8
  mapped_options = {host: self.options.hosts,
9
- stale_client_timeout: self.options.time}.merge(self.options.adapter_options)
9
+ resources: 1,
10
+ stale_client_timeout: self.options.lock_time}.merge(self.options.adapter_options)
10
11
 
11
12
  Redis::Semaphore.new(self.key, mapped_options)
12
13
  end
13
14
 
14
15
  def lock
15
- self.lock_token = self.lock_manager.lock(self.options.timeout)
16
+ self.lock_token = self.lock_manager.lock(self.options.lock_acquire_time)
16
17
  end
17
18
 
18
19
  def unlock
@@ -6,14 +6,14 @@ module ActiveJob
6
6
  class Redlock < Base
7
7
  def create_lock_manager
8
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))
9
+ mapped_options[:retry_count] = 2 # Try to get the lock and then try again when timeout is expiring--
10
+ mapped_options[:retry_delay] = self.options.lock_acquire_time * 1000 # convert from seconds to milliseconds
11
11
 
12
12
  ::Redlock::Client.new(Array(self.options.hosts), mapped_options)
13
13
  end
14
14
 
15
15
  def lock
16
- self.lock_token = self.lock_manager.lock(self.key, self.options.time * 1000)
16
+ self.lock_token = self.lock_manager.lock(self.key, self.options.lock_time * 1000)
17
17
  end
18
18
 
19
19
  def unlock
@@ -6,8 +6,8 @@ module ActiveJob
6
6
  class SuoRedis < Base
7
7
  def create_lock_manager
8
8
  mapped_options = {connection: {host: self.options.hosts},
9
- stale_lock_expiration: self.options.time,
10
- acquisition_timeout: self.options.timeout}
9
+ stale_lock_expiration: self.options.lock_time,
10
+ acquisition_timeout: self.options.lock_acquire_time}
11
11
 
12
12
  Suo::Client::Redis.new(self.key, mapped_options)
13
13
  end
@@ -7,15 +7,15 @@ module ActiveJob
7
7
  def lock_options
8
8
  @lock_options ||= ActiveJob::Locking::Options.new
9
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
10
+ delegate :adapter, :hosts, :lock_time, :lock_acquire_time, :adapter_options, to: :lock_options
11
+ delegate :adapter=, :hosts=, :lock_time=, :lock_acquire_time=, :adapter_options=, to: :lock_options
12
12
  end
13
13
 
14
14
  included do
15
- # We need to serialize the lock token because it could be released in a different process
15
+ # We need to serialize the lock token that some gems create because it could be released in a different process
16
16
  def serialize
17
17
  result = super
18
- result = result.merge('lock_token' => self.adapter.lock_token) if self.adapter.lock_token
18
+ result['lock_token'] = self.adapter.lock_token
19
19
  result
20
20
  end
21
21
 
@@ -29,11 +29,17 @@ module ActiveJob
29
29
  end
30
30
 
31
31
  def adapter
32
- # Merge local and global options
33
- merged_options = ActiveJob::Locking.options.dup.merge(self.class.lock_options)
32
+ @adapter ||= begin
33
+ # Make sure arguments are deserialized so calling lock key is safe
34
+ deserialize_arguments_if_needed
34
35
 
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)
36
+ # Merge local and global options
37
+ merged_options = ActiveJob::Locking.options.dup.merge(self.class.lock_options)
38
+
39
+ # Remember the lock might be acquired in one process and released in another
40
+ merged_options.adapter.new(self.lock_key, merged_options)
41
+ end
42
+ @adapter
37
43
  end
38
44
  end
39
45
  end
@@ -5,30 +5,41 @@ module ActiveJob
5
5
  class Options
6
6
  attr_accessor :adapter
7
7
  attr_accessor :hosts
8
- attr_accessor :time
9
- attr_accessor :timeout
8
+ attr_accessor :lock_time
9
+ attr_accessor :lock_acquire_time
10
10
  attr_accessor :adapter_options
11
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
12
  def initialize(options = {})
18
13
  @adapter = options[:adapter]
19
14
  @hosts = options[:hosts]
20
- @time = options[:time]
21
- @timeout = options[:timeout]
15
+ @lock_time = options[:lock_time]
16
+ @lock_acquire_time = options[:lock_acquire_time]
22
17
  @adapter_options = options[:adapter_options]
23
18
  end
24
19
 
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
20
+ def lock_time=(value)
21
+ case value
22
+ when NilClass
23
+ raise(ArgumentError, 'Lock time must be set')
24
+ when ActiveSupport::Duration
25
+ @lock_time = value.value
26
+ when 0
27
+ raise(ArgumentError, 'Lock time must be greater than zero')
28
+ else
29
+ @lock_time = value
30
+ end
31
+ end
32
+
33
+ def lock_acquire_time=(value)
34
+ case value
35
+ when NilClass
36
+ raise(ArgumentError, 'Lock acquire time must be set')
37
+ when ActiveSupport::Duration
38
+ @lock_acquire_time = value.value
39
+ when 0
40
+ raise(ArgumentError, 'Lock acquire time must be greater than zero')
41
+ else
42
+ @lock_acquire_time = value
32
43
  end
33
44
  end
34
45
 
@@ -36,8 +47,8 @@ module ActiveJob
36
47
  result = self.dup
37
48
  result.adapter = other.adapter if other.adapter
38
49
  result.hosts = other.hosts if other.hosts
39
- result.time = other.time if other.time
40
- result.timeout = other.timeout if other.timeout
50
+ result.lock_time = other.lock_time if other.lock_time
51
+ result.lock_acquire_time = other.lock_acquire_time if other.lock_acquire_time
41
52
  result.adapter_options = other.adapter_options if other.adapter_options
42
53
  result
43
54
  end
@@ -1,20 +1,20 @@
1
1
  module ActiveJob
2
2
  module Locking
3
- module Perform
3
+ module Serialized
4
4
  extend ::ActiveSupport::Concern
5
5
 
6
6
  included do
7
7
  include ::ActiveJob::Locking::Base
8
8
 
9
9
  around_perform do |job, block|
10
- if self.adapter.lock
10
+ if job.adapter.lock
11
11
  begin
12
12
  block.call
13
13
  ensure
14
- self.adapter.unlock
14
+ job.adapter.unlock
15
15
  end
16
16
  else
17
- self.class.set(wait: 5.seconds).perform_later(job)
17
+ job.class.set(wait: job.class.lock_acquire_time).perform_later(*job.arguments)
18
18
  end
19
19
  end
20
20
  end
@@ -1,18 +1,23 @@
1
1
  module ActiveJob
2
2
  module Locking
3
- module Enqueue
3
+ module Unique
4
4
  extend ::ActiveSupport::Concern
5
5
 
6
6
  included do
7
7
  include ::ActiveJob::Locking::Base
8
8
 
9
9
  before_enqueue do |job|
10
- lock = self.adapter.lock
10
+ lock = job.adapter.lock
11
11
  throw :abort unless lock
12
12
  end
13
13
 
14
- after_perform do |job|
14
+ rescue_from(Exception) do |exception|
15
15
  self.adapter.unlock
16
+ raise
17
+ end
18
+
19
+ after_perform do |job|
20
+ job.adapter.unlock
16
21
  end
17
22
  end
18
23
  end
@@ -2,8 +2,8 @@ require 'activejob/locking/adapters/base'
2
2
  require 'activejob/locking/adapters/memory'
3
3
 
4
4
  require 'activejob/locking/base'
5
- require 'activejob/locking/enqueue'
6
- require 'activejob/locking/perform'
5
+ require 'activejob/locking/unique'
6
+ require 'activejob/locking/serialized'
7
7
 
8
8
  require 'activejob/locking/options'
9
9
 
@@ -11,8 +11,8 @@ module ActiveJob
11
11
  module Locking
12
12
  @options = ActiveJob::Locking::Options.new(adapter: ActiveJob::Locking::Adapters::Memory,
13
13
  hosts: 'localhost',
14
- time: 100,
15
- timeout: 1,
14
+ lock_time: 100,
15
+ lock_acquire_time: 1,
16
16
  adapter_options: {})
17
17
 
18
18
  def self.options
@@ -1,7 +1,7 @@
1
- class PerformSeriallyJob < ActiveJob::Base
2
- include ActiveJob::Locking::Perform
1
+ class FailJob < ActiveJob::Base
2
+ include ActiveJob::Locking::Unique
3
3
 
4
- self.lock_acquire_timeout = 1.hour
4
+ self.lock_acquire_time = 2
5
5
 
6
6
  # We want the job ids to be all the same for testing
7
7
  def lock_key
@@ -10,6 +10,6 @@ class PerformSeriallyJob < ActiveJob::Base
10
10
 
11
11
  # Pass in index so we can distinguish different jobs
12
12
  def perform(index, sleep_time)
13
- sleep(sleep_time)
13
+ raise(ArgumentError, 'Job failed')
14
14
  end
15
15
  end
@@ -1,7 +1,7 @@
1
- class EnqueueDropJob < ActiveJob::Base
2
- include ActiveJob::Locking::Enqueue
1
+ class SerialJob < ActiveJob::Base
2
+ include ActiveJob::Locking::Serialized
3
3
 
4
- self.lock_acquire_timeout = 0.1
4
+ self.lock_acquire_time = 2
5
5
 
6
6
  # We want the job ids to be all the same for testing
7
7
  def lock_key
@@ -1,7 +1,7 @@
1
- class EnqueueWaitJob < ActiveJob::Base
2
- include ActiveJob::Locking::Enqueue
1
+ class UniqueJob < ActiveJob::Base
2
+ include ActiveJob::Locking::Unique
3
3
 
4
- self.lock_acquire_timeout = 1.hour
4
+ self.lock_acquire_time = 2
5
5
 
6
6
  # We want the job ids to be all the same for testing
7
7
  def lock_key
@@ -0,0 +1,63 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ module SerializedTests
4
+ def test_one_completed
5
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
6
+ assert_equal(0, ActiveJob::Base.queue_adapter.performed_jobs.count)
7
+
8
+ start_time = Time.now
9
+ sleep_time = SerialJob.lock_acquire_time / 0.9
10
+ threads = 3.times.map do |i|
11
+ Thread.new do
12
+ SerialJob.perform_later(i, sleep_time)
13
+ end
14
+ end
15
+
16
+ # All the threads will complete after the sleep time has expired - since two jobs get requeued
17
+ threads.each {|thread| thread.join}
18
+ assert_equal(2, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
19
+ assert_equal(3, ActiveJob::Base.queue_adapter.performed_jobs.count)
20
+
21
+ assert(Time.now - start_time > (1 * sleep_time))
22
+ end
23
+
24
+ def test_some_completed
25
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
26
+ assert_equal(0, ActiveJob::Base.queue_adapter.performed_jobs.count)
27
+
28
+ start_time = Time.now
29
+ sleep_time = SerialJob.lock_acquire_time / 1.9
30
+ threads = 3.times.map do |i|
31
+ Thread.new do
32
+ SerialJob.perform_later(i, sleep_time)
33
+ end
34
+ end
35
+
36
+ # All the threads will complete after the sleep time has expired - since two jobs get requeued
37
+ threads.each {|thread| thread.join}
38
+ assert_equal(1, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
39
+ assert_equal(3, ActiveJob::Base.queue_adapter.performed_jobs.count)
40
+
41
+ assert(Time.now - start_time > (1 * sleep_time))
42
+ end
43
+
44
+ def test_all_completed
45
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
46
+ assert_equal(0, ActiveJob::Base.queue_adapter.performed_jobs.count)
47
+
48
+ start_time = Time.now
49
+ sleep_time = SerialJob.lock_acquire_time / 4
50
+ threads = 3.times.map do |i|
51
+ Thread.new do
52
+ SerialJob.perform_later(i, sleep_time)
53
+ end
54
+ end
55
+
56
+ # All the threads will complete after the sleep time has expired - since two jobs get requeued
57
+ threads.each {|thread| thread.join}
58
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
59
+ assert_equal(3, ActiveJob::Base.queue_adapter.performed_jobs.count)
60
+
61
+ assert(Time.now - start_time > (1 * sleep_time))
62
+ end
63
+ end
data/test/test_helper.rb CHANGED
@@ -11,11 +11,9 @@ require 'activejob/locking/adapters/redis-semaphore'
11
11
  require 'activejob/locking/adapters/redlock'
12
12
  require 'activejob/locking/adapters/suo-redis'
13
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'
14
+ require_relative './jobs/unique_job'
15
+ require_relative './jobs/fail_job'
16
+ require_relative './jobs/serial_job'
19
17
 
20
18
  def redis_reset
21
19
  Kernel.system('redis-cli FLUSHALL')
@@ -0,0 +1,11 @@
1
+ require_relative('./serialized_tests')
2
+
3
+ class SerializedMemory < MiniTest::Test
4
+ include SerializedTests
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
+ end
@@ -1,7 +1,7 @@
1
- require_relative('./perform_tests')
1
+ require_relative('./serialized_tests')
2
2
 
3
- class EnqueueRedisSemaphoreTest < MiniTest::Test
4
- include PerformTests
3
+ class UniqueRedisSemaphoreTest < MiniTest::Test
4
+ include SerializedTests
5
5
 
6
6
  def setup
7
7
  redis_reset
@@ -1,7 +1,7 @@
1
- require_relative('./enqueue_tests')
1
+ require_relative('./serialized_tests')
2
2
 
3
- class EnqueueRedlockTest < MiniTest::Test
4
- include EnqueueTests
3
+ class SerializedRedlockTest < MiniTest::Test
4
+ include SerializedTests
5
5
 
6
6
  def setup
7
7
  redis_reset
@@ -1,7 +1,7 @@
1
- require_relative('./perform_tests')
1
+ require_relative('./serialized_tests')
2
2
 
3
- class PerformSuoRedisTest < MiniTest::Test
4
- include PerformTests
3
+ class SerializedSuoRedisTest < MiniTest::Test
4
+ include SerializedTests
5
5
 
6
6
  def setup
7
7
  redis_reset
@@ -0,0 +1,12 @@
1
+ %w(
2
+ test_serialized_memory
3
+ test_serialized_redis_semaphore
4
+ test_serialized_redlock
5
+ test_serialized_suo_redis
6
+ test_unique_memory
7
+ test_unique_redis_semaphore
8
+ test_unique_redlock
9
+ test_unique_suo_redis
10
+ ).each do |test|
11
+ require File.expand_path("../#{test}", __FILE__)
12
+ end
@@ -0,0 +1,12 @@
1
+ require_relative('./unique_tests')
2
+
3
+ class UniqueMemoryTest < MiniTest::Test
4
+ include UniqueTests
5
+
6
+ def setup
7
+ ActiveJob::Locking::Adapters::Memory.reset
8
+ ActiveJob::Base.queue_adapter = :test
9
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
10
+ ActiveJob::Locking.options.adapter = ActiveJob::Locking::Adapters::Memory
11
+ end
12
+ end
@@ -1,7 +1,7 @@
1
- require_relative('./enqueue_tests')
1
+ require_relative('./unique_tests')
2
2
 
3
- class EnqueueRedisSemaphoreTest < MiniTest::Test
4
- include EnqueueTests
3
+ class UniqueRedisSemaphoreTest < MiniTest::Test
4
+ include UniqueTests
5
5
 
6
6
  def setup
7
7
  redis_reset
@@ -1,7 +1,7 @@
1
- require_relative('./perform_tests')
1
+ require_relative('./unique_tests')
2
2
 
3
- class PerformRedlockTest < MiniTest::Test
4
- include PerformTests
3
+ class UniqueRedlockTest < MiniTest::Test
4
+ include UniqueTests
5
5
 
6
6
  def setup
7
7
  redis_reset
@@ -1,7 +1,7 @@
1
- require_relative('./enqueue_tests')
1
+ require_relative('./unique_tests')
2
2
 
3
- class EnqueueSuoRedisTest < MiniTest::Test
4
- include EnqueueTests
3
+ class UniqueSuoRedisTest < MiniTest::Test
4
+ include UniqueTests
5
5
 
6
6
  def setup
7
7
  redis_reset
@@ -0,0 +1,100 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ module UniqueTests
4
+ def test_none_performed
5
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = false
6
+
7
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
8
+ assert_equal(0, ActiveJob::Base.queue_adapter.performed_jobs.count)
9
+
10
+ sleep_time = UniqueJob.lock_acquire_time
11
+ threads = 3.times.map do |i|
12
+ Thread.new do
13
+ UniqueJob.perform_later(i, sleep_time)
14
+ end
15
+ end
16
+
17
+ threads.each {|thread| thread.join}
18
+ assert_equal(1, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
19
+ assert_equal(0, ActiveJob::Base.queue_adapter.performed_jobs.count)
20
+ ensure
21
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
22
+ ActiveJob::Base.queue_adapter.enqueued_jobs.clear
23
+ end
24
+
25
+ def test_one_performed
26
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
27
+ assert_equal(0, ActiveJob::Base.queue_adapter.performed_jobs.count)
28
+
29
+ sleep_time = UniqueJob.lock_acquire_time * 2
30
+ threads = 3.times.map do |i|
31
+ Thread.new do
32
+ UniqueJob.perform_later(i, sleep_time)
33
+ end
34
+ end
35
+
36
+ threads.each {|thread| thread.join}
37
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
38
+ assert_equal(1, ActiveJob::Base.queue_adapter.performed_jobs.count)
39
+ end
40
+
41
+ def test_all_performed
42
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
43
+ assert_equal(0, ActiveJob::Base.queue_adapter.performed_jobs.count)
44
+
45
+ start_time = Time.now
46
+ sleep_time = UniqueJob.lock_acquire_time / 4.0
47
+ threads = 3.times.map do |i|
48
+ Thread.new do
49
+ UniqueJob.perform_later(i, sleep_time)
50
+ end
51
+ end
52
+
53
+ threads.each {|thread| thread.join}
54
+
55
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
56
+ assert_equal(threads.count, ActiveJob::Base.queue_adapter.performed_jobs.count)
57
+ assert(Time.now - start_time > (threads.count * sleep_time))
58
+ end
59
+
60
+ def test_some_performed
61
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
62
+ assert_equal(0, ActiveJob::Base.queue_adapter.performed_jobs.count)
63
+
64
+ start_time = Time.now
65
+ sleep_time = UniqueJob.lock_acquire_time / 2.0
66
+ threads = 3.times.map do |i|
67
+ Thread.new do
68
+ UniqueJob.perform_later(i, sleep_time)
69
+ end
70
+ end
71
+
72
+ threads.each {|thread| thread.join}
73
+
74
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
75
+ assert_equal(threads.count - 1, ActiveJob::Base.queue_adapter.performed_jobs.count)
76
+ assert(Time.now - start_time > ((threads.count - 1) * sleep_time))
77
+ end
78
+
79
+ def test_fail
80
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
81
+ assert_equal(0, ActiveJob::Base.queue_adapter.performed_jobs.count)
82
+
83
+ start_time = Time.now
84
+ sleep_time = UniqueJob.lock_acquire_time
85
+ threads = 3.times.map do |i|
86
+ Thread.new do
87
+ begin
88
+ FailJob.perform_later(i, sleep_time)
89
+ rescue => e
90
+ # do nothing
91
+ end
92
+ end
93
+ end
94
+
95
+ threads.each {|thread| thread.join}
96
+
97
+ assert_equal(0, ActiveJob::Base.queue_adapter.enqueued_jobs.count)
98
+ assert_equal(threads.count, ActiveJob::Base.queue_adapter.performed_jobs.count)
99
+ end
100
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activejob-locking
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charlie Savage
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-01-16 00:00:00.000000000 Z
11
+ date: 2017-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 5.10.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: redis-mutex
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'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: redis-semaphore
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -84,7 +98,7 @@ description: |
84
98
  activejob-locking lets you control how ActiveJobs are enqueued and performed:
85
99
 
86
100
  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)
101
+ Allow only one job to be performed at a time (also based on a lock_id)
88
102
  email:
89
103
  executables: []
90
104
  extensions: []
@@ -102,26 +116,24 @@ files:
102
116
  - lib/activejob/locking/adapters/redlock.rb
103
117
  - lib/activejob/locking/adapters/suo-redis.rb
104
118
  - lib/activejob/locking/base.rb
105
- - lib/activejob/locking/enqueue.rb
106
119
  - 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
+ - lib/activejob/locking/serialized.rb
121
+ - lib/activejob/locking/unique.rb
122
+ - test/jobs/fail_job.rb
123
+ - test/jobs/serial_job.rb
124
+ - test/jobs/unique_job.rb
125
+ - test/serialized_tests.rb
120
126
  - 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
127
+ - test/test_serialized_memory.rb
128
+ - test/test_serialized_redis_semaphore.rb
129
+ - test/test_serialized_redlock.rb
130
+ - test/test_serialized_suo_redis.rb
131
+ - test/test_suite.rb
132
+ - test/test_unique_memory.rb
133
+ - test/test_unique_redis_semaphore.rb
134
+ - test/test_unique_redlock.rb
135
+ - test/test_unique_suo_redis.rb
136
+ - test/unique_tests.rb
125
137
  homepage: http://github.com/cfis/activejob-locking
126
138
  licenses:
127
139
  - MIT
@@ -142,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
154
  version: '0'
143
155
  requirements: []
144
156
  rubyforge_project:
145
- rubygems_version: 2.6.8
157
+ rubygems_version: 2.6.11
146
158
  signing_key:
147
159
  specification_version: 4
148
160
  summary: ActiveJob locking to control how jobs are enqueued and performed.
@@ -1,74 +0,0 @@
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
@@ -1,16 +0,0 @@
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
@@ -1,16 +0,0 @@
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
@@ -1,16 +0,0 @@
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
@@ -1,37 +0,0 @@
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
@@ -1,45 +0,0 @@
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
@@ -1,14 +0,0 @@
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