activejob-locking 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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