global_lock 0.0.4 → 0.0.5
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 +4 -4
- data/.gitignore +2 -0
- data/.rspec +3 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +40 -0
- data/global_lock.gemspec +17 -0
- data/lib/global_lock/config.rb +32 -0
- data/lib/global_lock/errors.rb +7 -0
- data/lib/global_lock/lock.rb +99 -0
- data/lib/global_lock/lockable.rb +37 -0
- data/spec/global_lock/config_spec.rb +34 -0
- data/spec/global_lock/lock_spec.rb +97 -0
- data/spec/global_lock/lockable_spec.rb +51 -0
- data/spec/global_lock_spec.rb +1 -0
- data/spec/spec_helper.rb +5 -0
- metadata +21 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 97815ec2fc504cc021191f09ae1ae6e375dfff0a94bf8ee6f5d1eed164a246d1
|
4
|
+
data.tar.gz: 3629dda16e66bd051fc0c66f47c4b85f6f22eb4ca728940f9f8cb3f36e17fa7e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3374a9371f5945b30efc735bd2ef2c75437515d8c967f01bd69cb35b4313c0498735f512fde9dd2885c7748f170c17e4ecb5d0c1c6189327fb6e1177badcfe8f
|
7
|
+
data.tar.gz: f17a19a4d05b744c823c88fe5def7d709b31c8cca57c362a202fde5156da4984482f83d6877a4ebce54c9b3aa67b819b7ea1fee8f62765551c4bf9ca0be0b2ab
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
global_lock (0.0.5)
|
5
|
+
connection_pool
|
6
|
+
redis
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
connection_pool (2.2.3)
|
12
|
+
diff-lcs (1.4.4)
|
13
|
+
mock_redis (0.27.3)
|
14
|
+
ruby2_keywords
|
15
|
+
redis (4.2.5)
|
16
|
+
rspec (3.10.0)
|
17
|
+
rspec-core (~> 3.10.0)
|
18
|
+
rspec-expectations (~> 3.10.0)
|
19
|
+
rspec-mocks (~> 3.10.0)
|
20
|
+
rspec-core (3.10.1)
|
21
|
+
rspec-support (~> 3.10.0)
|
22
|
+
rspec-expectations (3.10.1)
|
23
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
24
|
+
rspec-support (~> 3.10.0)
|
25
|
+
rspec-mocks (3.10.2)
|
26
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
27
|
+
rspec-support (~> 3.10.0)
|
28
|
+
rspec-support (3.10.2)
|
29
|
+
ruby2_keywords (0.0.4)
|
30
|
+
|
31
|
+
PLATFORMS
|
32
|
+
ruby
|
33
|
+
|
34
|
+
DEPENDENCIES
|
35
|
+
global_lock!
|
36
|
+
mock_redis
|
37
|
+
rspec
|
38
|
+
|
39
|
+
BUNDLED WITH
|
40
|
+
2.1.4
|
data/global_lock.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "global_lock"
|
3
|
+
s.version = "0.0.5"
|
4
|
+
s.summary = "Global Lock"
|
5
|
+
s.description = "Global Lock"
|
6
|
+
s.authors = ["Samuel Ballan"]
|
7
|
+
s.email = ["sgb4622@gmail.com"]
|
8
|
+
s.files = `git ls-files`.split("\n")
|
9
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
10
|
+
s.require_paths = ["lib"]
|
11
|
+
s.license = "MIT"
|
12
|
+
|
13
|
+
s.add_dependency "redis"
|
14
|
+
s.add_dependency "connection_pool"
|
15
|
+
s.add_development_dependency "rspec"
|
16
|
+
s.add_development_dependency "mock_redis"
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class GlobalLock::Config
|
2
|
+
attr_accessor :default_ttl,
|
3
|
+
:default_retry_time,
|
4
|
+
:default_backoff_time,
|
5
|
+
:redis_prefix,
|
6
|
+
:redis_connection,
|
7
|
+
:redis_pool
|
8
|
+
|
9
|
+
def initialize(opts = {})
|
10
|
+
self.default_ttl = opts[:default_ttl] || 60 * 60 * 24
|
11
|
+
self.default_retry_time = opts[:default_retry_time] || 30
|
12
|
+
self.default_backoff_time = opts[:default_backoff_time] || 0.01
|
13
|
+
self.redis_prefix = opts[:redis_prefix] || "GlobalLock/"
|
14
|
+
self.redis_connection = opts[:redis_connection] || Redis.new
|
15
|
+
self.redis_pool = opts[:redis_pool] || ConnectionPool.new { self.redis_connection }
|
16
|
+
end
|
17
|
+
|
18
|
+
def lock_opts
|
19
|
+
{
|
20
|
+
ttl: default_ttl,
|
21
|
+
retry_time: default_retry_time,
|
22
|
+
backoff_time: default_backoff_time,
|
23
|
+
redis_prefix: redis_prefix,
|
24
|
+
redis_connection: redis_connection,
|
25
|
+
redis_pool: redis_pool
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_redis(&block)
|
30
|
+
redis_pool.with(&block)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
class GlobalLock::Lock
|
2
|
+
attr_reader :config
|
3
|
+
|
4
|
+
def initialize(opts = {})
|
5
|
+
if opts.is_a?(GlobalLock::Config)
|
6
|
+
@config = opts
|
7
|
+
else
|
8
|
+
@config = GlobalLock::Config.new(opts)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def with_lock(name, existing_key=nil, opts={}, &block)
|
13
|
+
opts = config.lock_opts.merge(opts)
|
14
|
+
raise ArgumentError.new("Block required") unless block_given?
|
15
|
+
|
16
|
+
ret_val = nil
|
17
|
+
|
18
|
+
if !existing_key.nil && correct_key?(name, existing_key)
|
19
|
+
ret_val = block.call(existing_key)
|
20
|
+
elsif !existing_key.nil?
|
21
|
+
raise GlobalLock::Errors::FailedToLockError.new("Used incorrect existing key")
|
22
|
+
else
|
23
|
+
key = lock(name, opts)
|
24
|
+
raise GlobalLock::Errors::FailedToLockError.new("Failed to acquire lock") if (key == false)
|
25
|
+
|
26
|
+
ret_val = block.call(key)
|
27
|
+
|
28
|
+
unlock_success = unlock(name, key)
|
29
|
+
raise GlobalLock::Errors::FailedToUnlockError.new("Failed to unlock") unless unlock_success
|
30
|
+
end
|
31
|
+
|
32
|
+
ret_val
|
33
|
+
end
|
34
|
+
|
35
|
+
def lock(name, opts={})
|
36
|
+
opts = config.lock_opts.merge(opts)
|
37
|
+
ttl, retry_time, backoff_time = opts.values_at(:ttl, :retry_time, :backoff_time)
|
38
|
+
|
39
|
+
key = SecureRandom.uuid
|
40
|
+
success = write_lock(name, key, ex: ttl)
|
41
|
+
|
42
|
+
if success
|
43
|
+
key
|
44
|
+
elsif retry_time > 0
|
45
|
+
sleep backoff_time
|
46
|
+
|
47
|
+
# Note: really, the backoff factor should be multiplied by retry_wait
|
48
|
+
|
49
|
+
lock(
|
50
|
+
name,
|
51
|
+
ttl: ttl,
|
52
|
+
retry_time: retry_time - backoff_time,
|
53
|
+
backoff_time: backoff_time * 2 * rand(0.5..1.5)
|
54
|
+
)
|
55
|
+
else
|
56
|
+
false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def unlock(name, key)
|
61
|
+
if correct_key?(name, key)
|
62
|
+
delete_lock(name)
|
63
|
+
else
|
64
|
+
false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def correct_key?(name, possible_key)
|
69
|
+
return false unless name && possible_key && !name.empty? && !possible_key.empty?
|
70
|
+
|
71
|
+
actual_key = fetch_lock_key(name)
|
72
|
+
possible_key == actual_key
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def write_lock(name, key, ex: nil)
|
78
|
+
ex ||= config.default_ttl
|
79
|
+
raise ArgumentError.new("Cannot write_lock with blank name") if name.empty?
|
80
|
+
|
81
|
+
res = config.with_redis do |redis|
|
82
|
+
redis.set(config.redis_prefix + name, key, ex: ex, nx: true)
|
83
|
+
end
|
84
|
+
res == true
|
85
|
+
end
|
86
|
+
|
87
|
+
def fetch_lock_key(name)
|
88
|
+
config.with_redis do |redis|
|
89
|
+
redis.get(config.redis_prefix + name)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def delete_lock(name)
|
94
|
+
res = config.with_redis do |redis|
|
95
|
+
redis.del(config.redis_prefix + name)
|
96
|
+
end
|
97
|
+
res == 1
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module GlobalLock
|
2
|
+
module Lockable
|
3
|
+
def self.included(other_mod)
|
4
|
+
# This pattern lets us have instance _and_ class methods in this module
|
5
|
+
other_mod.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
def lock_id
|
9
|
+
send(self.class.lock_id_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def lock(opts={})
|
13
|
+
GlobalLock.singleton.lock(lock_id, opts)
|
14
|
+
end
|
15
|
+
|
16
|
+
def unlock(key)
|
17
|
+
GlobalLock.singleton.unlock(lock_id, key)
|
18
|
+
end
|
19
|
+
|
20
|
+
def with_lock(existing_key=nil, opts={}, &block)
|
21
|
+
GlobalLock.singleton.with_lock(lock_id, existing_key, opts, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
# This pattern lets us have instance _and_ class methods in this module
|
25
|
+
module ClassMethods
|
26
|
+
def lock_id_name
|
27
|
+
@lock_id_name || :id
|
28
|
+
end
|
29
|
+
|
30
|
+
def set_lock_id_name(lock_id_name)
|
31
|
+
raise "Lock name must be symbol" unless lock_id_name.is_a? Symbol
|
32
|
+
|
33
|
+
@lock_id_name = lock_id_name
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe GlobalLock::Config do
|
4
|
+
|
5
|
+
context "Basics" do
|
6
|
+
it "can be created with no arguments" do
|
7
|
+
config = GlobalLock::Config.new
|
8
|
+
expect(config).to be
|
9
|
+
end
|
10
|
+
|
11
|
+
it "has correct defaults" do
|
12
|
+
config = GlobalLock::Config.new
|
13
|
+
|
14
|
+
expect(config.default_ttl).to eql(60 * 60 * 24)
|
15
|
+
expect(config.default_retry_time).to eql(30)
|
16
|
+
expect(config.default_backoff_time).to eql(0.01)
|
17
|
+
expect(config.redis_prefix).to eql("GlobalLock/")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "can be configured" do
|
21
|
+
config = GlobalLock::Config.new(
|
22
|
+
default_ttl: 1,
|
23
|
+
default_retry_time: 2,
|
24
|
+
default_backoff_time: 3,
|
25
|
+
redis_prefix: "4"
|
26
|
+
)
|
27
|
+
|
28
|
+
expect(config.default_ttl).to eql(1)
|
29
|
+
expect(config.default_retry_time).to eql(2)
|
30
|
+
expect(config.default_backoff_time).to eql(3)
|
31
|
+
expect(config.redis_prefix).to eql("4")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe GlobalLock::Lock do
|
4
|
+
before do
|
5
|
+
@mock_redis = MockRedis.new
|
6
|
+
@gl = GlobalLock::Lock.new redis_connection: @mock_redis
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:test_lock_name) { 'test_lock_name' }
|
10
|
+
let(:redis_test_lock_name) { @gl.config.redis_prefix + test_lock_name }
|
11
|
+
|
12
|
+
after(:each) do
|
13
|
+
@mock_redis.del(redis_test_lock_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "lock" do
|
17
|
+
context "is available" do
|
18
|
+
it "gets the lock" do
|
19
|
+
key = @gl.lock(test_lock_name)
|
20
|
+
found_key = @mock_redis.get(redis_test_lock_name)
|
21
|
+
expect(found_key).to eql(key)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "is not available" do
|
26
|
+
it "does not get the lock" do
|
27
|
+
real_key = @gl.lock(test_lock_name, retry_time: 0)
|
28
|
+
false_key = @gl.lock(test_lock_name, retry_time: 0)
|
29
|
+
|
30
|
+
expect(false_key).to eql(false)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "unlock" do
|
36
|
+
it "can unlock existing lock" do
|
37
|
+
key = 'test_lock_key'
|
38
|
+
@mock_redis.set(redis_test_lock_name, key)
|
39
|
+
found_key = @mock_redis.get(redis_test_lock_name)
|
40
|
+
expect(found_key).to eql(key)
|
41
|
+
|
42
|
+
@gl.unlock(test_lock_name, key)
|
43
|
+
|
44
|
+
found_key = @mock_redis.get(redis_test_lock_name)
|
45
|
+
expect(found_key).to be_nil
|
46
|
+
end
|
47
|
+
|
48
|
+
it "returns false with wrong key" do
|
49
|
+
correct_key = 'test_lock_key'
|
50
|
+
incorrect_key = 'bad_key'
|
51
|
+
@mock_redis.set(redis_test_lock_name, correct_key)
|
52
|
+
|
53
|
+
success = @gl.unlock(test_lock_name, incorrect_key)
|
54
|
+
|
55
|
+
expect(success).to be_falsey
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "private methods" do
|
60
|
+
describe "write_lock" do
|
61
|
+
it "writes the correct key" do
|
62
|
+
key = 'test_lock_key'
|
63
|
+
@gl.send(:write_lock, test_lock_name, key)
|
64
|
+
found_key = @mock_redis.get(redis_test_lock_name)
|
65
|
+
expect(found_key).to eql(key)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "fetch_lock_key" do
|
70
|
+
it "fetches the correct key" do
|
71
|
+
key = 'test_lock_key'
|
72
|
+
@mock_redis.set(redis_test_lock_name, key)
|
73
|
+
found_key = @gl.send(:fetch_lock_key, test_lock_name)
|
74
|
+
expect(key).to eql(found_key)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "delete_lock" do
|
79
|
+
it "deletes correct lock" do
|
80
|
+
other_test_lock_name = 'other_text_lock_name'
|
81
|
+
other_redis_test_lock_name = @gl.config.redis_prefix + other_test_lock_name
|
82
|
+
other_key = 'other_test_lock_key'
|
83
|
+
@mock_redis.set(other_redis_test_lock_name, other_key)
|
84
|
+
|
85
|
+
key = 'test_lock_key'
|
86
|
+
@mock_redis.set(redis_test_lock_name, key)
|
87
|
+
@gl.send(:delete_lock, test_lock_name)
|
88
|
+
|
89
|
+
found_key = @mock_redis.get(redis_test_lock_name)
|
90
|
+
other_found_key = @mock_redis.get(other_redis_test_lock_name)
|
91
|
+
|
92
|
+
expect(found_key).to be_nil
|
93
|
+
expect(other_found_key).to eql(other_key)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe GlobalLock::Lockable do
|
4
|
+
before do
|
5
|
+
@mock_redis = MockRedis.new
|
6
|
+
GlobalLock.config do |c|
|
7
|
+
c.redis_connection = @mock_redis
|
8
|
+
c.default_ttl = 60 * 5
|
9
|
+
c.default_retry_time = 1
|
10
|
+
c.default_backoff_time = 1
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:test_lock_name) { 'test_lock_name' }
|
15
|
+
let(:redis_test_lock_name) { GlobalLock.config.redis_prefix + test_lock_name }
|
16
|
+
|
17
|
+
after(:each) do
|
18
|
+
@mock_redis.flushdb
|
19
|
+
end
|
20
|
+
|
21
|
+
class MockLockable
|
22
|
+
include GlobalLock::Lockable
|
23
|
+
set_lock_id_name :id
|
24
|
+
|
25
|
+
def id
|
26
|
+
@id ||= SecureRandom.uuid
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'included' do
|
31
|
+
let(:lockable) { MockLockable.new }
|
32
|
+
describe 'lock' do
|
33
|
+
it 'returns the lock key' do
|
34
|
+
key = lockable.lock
|
35
|
+
expected_key = @mock_redis.get(GlobalLock.config.redis_prefix + lockable.id)
|
36
|
+
|
37
|
+
expect(key).to eql(expected_key)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'returns false if already locked' do
|
41
|
+
successful_key = lockable.lock
|
42
|
+
failed_key = lockable.lock
|
43
|
+
|
44
|
+
expect(successful_key).to be_a String
|
45
|
+
expect(failed_key).to eql(false)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require "spec_helper"
|
data/spec/spec_helper.rb
ADDED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: global_lock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Ballan
|
@@ -73,7 +73,21 @@ executables: []
|
|
73
73
|
extensions: []
|
74
74
|
extra_rdoc_files: []
|
75
75
|
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".rspec"
|
78
|
+
- Gemfile
|
79
|
+
- Gemfile.lock
|
80
|
+
- global_lock.gemspec
|
76
81
|
- lib/global_lock.rb
|
82
|
+
- lib/global_lock/config.rb
|
83
|
+
- lib/global_lock/errors.rb
|
84
|
+
- lib/global_lock/lock.rb
|
85
|
+
- lib/global_lock/lockable.rb
|
86
|
+
- spec/global_lock/config_spec.rb
|
87
|
+
- spec/global_lock/lock_spec.rb
|
88
|
+
- spec/global_lock/lockable_spec.rb
|
89
|
+
- spec/global_lock_spec.rb
|
90
|
+
- spec/spec_helper.rb
|
77
91
|
homepage:
|
78
92
|
licenses:
|
79
93
|
- MIT
|
@@ -97,4 +111,9 @@ rubygems_version: 3.1.4
|
|
97
111
|
signing_key:
|
98
112
|
specification_version: 4
|
99
113
|
summary: Global Lock
|
100
|
-
test_files:
|
114
|
+
test_files:
|
115
|
+
- spec/global_lock/config_spec.rb
|
116
|
+
- spec/global_lock/lock_spec.rb
|
117
|
+
- spec/global_lock/lockable_spec.rb
|
118
|
+
- spec/global_lock_spec.rb
|
119
|
+
- spec/spec_helper.rb
|