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