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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69155a838b0da2011d49f045a4c83707291d39e9ca4995d4e42f794513e6712b
4
- data.tar.gz: cb8f0baa5d7a03849f6df53cd9da64c3a2e48d6d3937d61d4d29adc81ae89d0e
3
+ metadata.gz: 97815ec2fc504cc021191f09ae1ae6e375dfff0a94bf8ee6f5d1eed164a246d1
4
+ data.tar.gz: 3629dda16e66bd051fc0c66f47c4b85f6f22eb4ca728940f9f8cb3f36e17fa7e
5
5
  SHA512:
6
- metadata.gz: 4fb7d9fcd49c6f93edc58803ef48c7fdcf56cbcbdd4c7b72d0cc53f656e05fd16ddbb2afa217c05737b861d25297029f28d3345c10f8e24d2d8bf65991d28ce5
7
- data.tar.gz: 4cd37814c3603c1b65db39a77dabb3089c3ebef9aa36963ffb377b1b9a9fb27349dd719f3f0d118a2038cee64fcc8c56f34334e3cf031d3a42e1e18d29e2d280
6
+ metadata.gz: 3374a9371f5945b30efc735bd2ef2c75437515d8c967f01bd69cb35b4313c0498735f512fde9dd2885c7748f170c17e4ecb5d0c1c6189327fb6e1177badcfe8f
7
+ data.tar.gz: f17a19a4d05b744c823c88fe5def7d709b31c8cca57c362a202fde5156da4984482f83d6877a4ebce54c9b3aa67b819b7ea1fee8f62765551c4bf9ca0be0b2ab
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ *.gem
2
+ **/*.swp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --color
3
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
4
+
5
+ gemspec
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
@@ -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,7 @@
1
+ module GlobalLock
2
+ module Errors
3
+ class Base < StandardError; end
4
+ class FailedToLockError < Base; end
5
+ class FailedToUnlockError < Base; end
6
+ end
7
+ 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"
@@ -0,0 +1,5 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ require 'mock_redis'
5
+ require 'global_lock.rb'
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
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