activejob-locking 0.4.0 → 0.6.2
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 +5 -5
- data/Gemfile +10 -10
- data/HISTORY.md +37 -16
- data/LICENSE +20 -20
- data/README.md +274 -247
- data/Rakefile +20 -20
- data/lib/activejob-locking.rb +24 -23
- data/lib/activejob/locking/adapters/base.rb +31 -31
- data/lib/activejob/locking/adapters/memory.rb +57 -57
- data/lib/activejob/locking/adapters/redis-semaphore.rb +26 -26
- data/lib/activejob/locking/adapters/redlock.rb +26 -26
- data/lib/activejob/locking/adapters/suo-redis.rb +25 -25
- data/lib/activejob/locking/base.rb +54 -50
- data/lib/activejob/locking/options.rb +72 -56
- data/lib/activejob/locking/serialized.rb +23 -23
- data/lib/activejob/locking/unique.rb +25 -25
- data/test/jobs/fail_job.rb +14 -14
- data/test/jobs/serial_job.rb +14 -14
- data/test/jobs/unique_job.rb +14 -14
- data/test/serialized_tests.rb +62 -62
- data/test/test_helper.rb +19 -19
- data/test/test_serialized_memory.rb +10 -10
- data/test/test_serialized_redis_semaphore.rb +11 -11
- data/test/test_serialized_redlock.rb +13 -13
- data/test/test_serialized_suo_redis.rb +11 -11
- data/test/test_suite.rb +12 -12
- data/test/test_unique_memory.rb +12 -12
- data/test/test_unique_redis_semaphore.rb +11 -11
- data/test/test_unique_redlock.rb +13 -13
- data/test/test_unique_suo_redis.rb +11 -11
- data/test/unique_tests.rb +99 -99
- metadata +11 -12
data/Rakefile
CHANGED
@@ -1,21 +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
|
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
21
|
end
|
data/lib/activejob-locking.rb
CHANGED
@@ -1,23 +1,24 @@
|
|
1
|
-
require 'activejob/locking/adapters/base'
|
2
|
-
require 'activejob/locking/adapters/memory'
|
3
|
-
|
4
|
-
require 'activejob/locking/base'
|
5
|
-
require 'activejob/locking/unique'
|
6
|
-
require 'activejob/locking/serialized'
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
23
|
-
|
1
|
+
require 'activejob/locking/adapters/base'
|
2
|
+
require 'activejob/locking/adapters/memory'
|
3
|
+
|
4
|
+
require 'activejob/locking/base'
|
5
|
+
require 'activejob/locking/unique'
|
6
|
+
require 'activejob/locking/serialized'
|
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
|
+
enqueue_time: 100, # seconds
|
15
|
+
lock_time: 100, # seconds
|
16
|
+
lock_acquire_time: 1, # seconds
|
17
|
+
adapter_options: {})
|
18
|
+
|
19
|
+
def self.options
|
20
|
+
@options
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -1,32 +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
|
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
32
|
end
|
@@ -1,58 +1,58 @@
|
|
1
|
-
module ActiveJob
|
2
|
-
module Locking
|
3
|
-
module Adapters
|
4
|
-
class Memory < Base
|
5
|
-
@hash = Hash.new
|
6
|
-
@mutex = Mutex.new
|
7
|
-
|
8
|
-
def self.lock(key)
|
9
|
-
@mutex.synchronize do
|
10
|
-
if @hash[key]
|
11
|
-
false
|
12
|
-
else
|
13
|
-
@hash[key] = Time.now
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.unlock(key)
|
19
|
-
@mutex.synchronize do
|
20
|
-
@hash.delete(key)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def self.locked?(key)
|
25
|
-
@mutex.synchronize do
|
26
|
-
@hash.include?(key)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.reset
|
31
|
-
@mutex.synchronize do
|
32
|
-
@hash = Hash.new
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def create_lock_manager
|
37
|
-
end
|
38
|
-
|
39
|
-
def lock
|
40
|
-
finish = Time.now + self.options.lock_acquire_time
|
41
|
-
sleep_time = [5, self.options.lock_acquire_time / 5].min
|
42
|
-
|
43
|
-
begin
|
44
|
-
lock = self.class.lock(key)
|
45
|
-
return lock if lock
|
46
|
-
sleep(sleep_time)
|
47
|
-
end while Time.now < finish
|
48
|
-
|
49
|
-
false
|
50
|
-
end
|
51
|
-
|
52
|
-
def unlock
|
53
|
-
self.class.unlock(self.key)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
1
|
+
module ActiveJob
|
2
|
+
module Locking
|
3
|
+
module Adapters
|
4
|
+
class Memory < Base
|
5
|
+
@hash = Hash.new
|
6
|
+
@mutex = Mutex.new
|
7
|
+
|
8
|
+
def self.lock(key)
|
9
|
+
@mutex.synchronize do
|
10
|
+
if @hash[key]
|
11
|
+
false
|
12
|
+
else
|
13
|
+
@hash[key] = Time.now
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.unlock(key)
|
19
|
+
@mutex.synchronize do
|
20
|
+
@hash.delete(key)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.locked?(key)
|
25
|
+
@mutex.synchronize do
|
26
|
+
@hash.include?(key)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.reset
|
31
|
+
@mutex.synchronize do
|
32
|
+
@hash = Hash.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_lock_manager
|
37
|
+
end
|
38
|
+
|
39
|
+
def lock
|
40
|
+
finish = Time.now + self.options.lock_acquire_time
|
41
|
+
sleep_time = [5, self.options.lock_acquire_time / 5].min
|
42
|
+
|
43
|
+
begin
|
44
|
+
lock = self.class.lock(key)
|
45
|
+
return lock if lock
|
46
|
+
sleep(sleep_time)
|
47
|
+
end while Time.now < finish
|
48
|
+
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
def unlock
|
53
|
+
self.class.unlock(self.key)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
58
|
end
|
@@ -1,26 +1,26 @@
|
|
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.first,
|
9
|
-
resources: 1,
|
10
|
-
stale_client_timeout: self.options.lock_time}.merge(self.options.adapter_options)
|
11
|
-
|
12
|
-
Redis::Semaphore.new(self.key, mapped_options)
|
13
|
-
end
|
14
|
-
|
15
|
-
def lock
|
16
|
-
self.lock_token = self.lock_manager.lock(self.options.lock_acquire_time)
|
17
|
-
end
|
18
|
-
|
19
|
-
def unlock
|
20
|
-
self.lock_manager.signal(self.lock_token)
|
21
|
-
self.lock_token = nil
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
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.first,
|
9
|
+
resources: 1,
|
10
|
+
stale_client_timeout: self.options.lock_time}.merge(self.options.adapter_options)
|
11
|
+
|
12
|
+
Redis::Semaphore.new(self.key, mapped_options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def lock
|
16
|
+
self.lock_token = self.lock_manager.lock(self.options.lock_acquire_time)
|
17
|
+
end
|
18
|
+
|
19
|
+
def unlock
|
20
|
+
self.lock_manager.signal(self.lock_token)
|
21
|
+
self.lock_token = nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,26 +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] = 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
|
-
|
12
|
-
::Redlock::Client.new(self.options.hosts, mapped_options)
|
13
|
-
end
|
14
|
-
|
15
|
-
def lock
|
16
|
-
self.lock_token = self.lock_manager.lock(self.key, self.options.lock_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
|
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] = 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
|
+
|
12
|
+
::Redlock::Client.new(self.options.hosts, mapped_options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def lock
|
16
|
+
self.lock_token = self.lock_manager.lock(self.key, self.options.lock_time * 1000)
|
17
|
+
end
|
18
|
+
|
19
|
+
def unlock
|
20
|
+
self.lock_manager.unlock(self.lock_token.symbolize_keys)
|
21
|
+
self.lock_token = nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,25 +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.first},
|
9
|
-
stale_lock_expiration: self.options.lock_time,
|
10
|
-
acquisition_timeout: self.options.lock_acquire_time}
|
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
|
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.first},
|
9
|
+
stale_lock_expiration: self.options.lock_time,
|
10
|
+
acquisition_timeout: self.options.lock_acquire_time}
|
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
|
@@ -1,51 +1,55 @@
|
|
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_time, :adapter_options, to: :lock_options
|
11
|
-
delegate :adapter=, :hosts=, :lock_time=, :lock_acquire_time=, :adapter_options=, to: :lock_options
|
12
|
-
end
|
13
|
-
|
14
|
-
included do
|
15
|
-
# We need to serialize the lock token that some gems create because it could be released in a different process
|
16
|
-
def serialize
|
17
|
-
result = super
|
18
|
-
result['lock_token'] = 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(*args)
|
28
|
-
[self.class.name, serialize_arguments(self.arguments)].join('/')
|
29
|
-
end
|
30
|
-
|
31
|
-
def adapter
|
32
|
-
@adapter ||= begin
|
33
|
-
# Make sure arguments are deserialized so calling lock key is safe
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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, :enqueue_time, :hosts, :lock_time, :lock_acquire_time, :adapter_options, to: :lock_options
|
11
|
+
delegate :adapter=, :enqueue_time=, :hosts=, :lock_time=, :lock_acquire_time=, :adapter_options=, to: :lock_options
|
12
|
+
end
|
13
|
+
|
14
|
+
included do
|
15
|
+
# We need to serialize the lock token that some gems create because it could be released in a different process
|
16
|
+
def serialize
|
17
|
+
result = super
|
18
|
+
result['lock_token'] = 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(*args)
|
28
|
+
[self.class.name, serialize_arguments(self.arguments)].join('/')
|
29
|
+
end
|
30
|
+
|
31
|
+
def adapter
|
32
|
+
@adapter ||= begin
|
33
|
+
# Make sure arguments are deserialized so calling lock key is safe
|
34
|
+
begin
|
35
|
+
deserialize_arguments_if_needed
|
36
|
+
rescue => exception
|
37
|
+
rescue_with_handler(exception) || raise
|
38
|
+
end
|
39
|
+
|
40
|
+
# Merge local and global options
|
41
|
+
merged_options = ActiveJob::Locking.options.dup.merge(self.class.lock_options)
|
42
|
+
|
43
|
+
# Get the key
|
44
|
+
base_key = self.lock_key(*self.arguments)
|
45
|
+
key = "activejoblocking:#{base_key}"
|
46
|
+
|
47
|
+
# Remember the lock might be acquired in one process and released in another
|
48
|
+
merged_options.adapter.new(key, merged_options)
|
49
|
+
end
|
50
|
+
@adapter
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
51
55
|
end
|