gcslock 1.0.1 → 1.0.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 +4 -4
- data/README.md +1 -1
- data/lib/gcslock/errors.rb +1 -0
- data/lib/gcslock/mutex.rb +48 -30
- data/lib/gcslock/semaphore.rb +161 -0
- data/lib/gcslock/utils.rb +31 -0
- data/lib/gcslock/version.rb +1 -1
- data/spec/gcslock/mutex_spec.rb +4 -6
- data/spec/gcslock/semaphore_spec.rb +401 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2fa87f56360a57de01a55b6b9cf5607b678f38d719b5e5e82f6a77dba84ecf6c
|
4
|
+
data.tar.gz: c121126258ba7b7da738ed1afd61a68d13a191f0e18f5e3318bc30f8a81fb1d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d4ac377cc6e39833e5ccc8afedcaa1df7c186a5ff230b9cb1c7f991c74e67e0c4d0c6fb29e445e226ba40daebcffe7fc6a1f80224434709ca8710058116c26ef
|
7
|
+
data.tar.gz: fc2b9b5c25f468e430f035b48edfe254f35b47ebd45bf81c7a5d84b532ed389764353e24ce9a18e358544e33306838dfb6ff8b7b7349e0d27e7171920b012443
|
data/README.md
CHANGED
data/lib/gcslock/errors.rb
CHANGED
data/lib/gcslock/mutex.rb
CHANGED
@@ -2,6 +2,7 @@ require 'google/cloud/storage'
|
|
2
2
|
require 'securerandom'
|
3
3
|
|
4
4
|
require_relative 'errors'
|
5
|
+
require_relative 'utils'
|
5
6
|
|
6
7
|
module GCSLock
|
7
8
|
class Mutex
|
@@ -16,35 +17,29 @@ module GCSLock
|
|
16
17
|
end
|
17
18
|
|
18
19
|
# Attempts to grab the lock and waits if it isn't available.
|
19
|
-
#
|
20
|
+
#
|
21
|
+
# @param timeout [Integer] the duration to wait before cancelling the operation
|
22
|
+
# if the lock was not obtained (unlimited if _nil_).
|
23
|
+
#
|
24
|
+
# @return [Boolean] `true` if the lock was obtained.
|
25
|
+
#
|
26
|
+
# @raise [LockAlreadyOwnedError] if the lock is already owned by the current instance.
|
27
|
+
# @raise [LockTimeoutError] if the lock was not obtained before reaching the timeout.
|
20
28
|
def lock(timeout: nil)
|
21
29
|
raise LockAlreadyOwnedError, "Mutex for #{@object.name} is already owned by this process" if owned?
|
22
30
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end_time = now + timeout unless timeout.nil?
|
27
|
-
|
28
|
-
loop do
|
29
|
-
return true if try_lock
|
30
|
-
break if !timeout.nil? && now + backoff >= end_time
|
31
|
-
sleep(backoff)
|
32
|
-
|
33
|
-
backoff_opts = [@max_backoff, backoff * 2]
|
34
|
-
|
35
|
-
unless timeout.nil?
|
36
|
-
now = Time.now
|
37
|
-
diff = end_time - now
|
38
|
-
backoff_opts.push(diff) if diff > 0
|
31
|
+
begin
|
32
|
+
Utils.backoff(min_backoff: @min_backoff, max_backoff: @max_backoff, timeout: timeout) do
|
33
|
+
try_lock
|
39
34
|
end
|
40
|
-
|
41
|
-
|
35
|
+
rescue LockTimeoutError
|
36
|
+
raise LockTimeoutError, "Unable to get mutex for #{@object.name} before timeout"
|
42
37
|
end
|
43
|
-
|
44
|
-
raise LockTimeoutError, "Unable to get mutex for #{@object.name} before timeout"
|
45
38
|
end
|
46
39
|
|
47
|
-
#
|
40
|
+
# Verifies if the lock is already taken.
|
41
|
+
#
|
42
|
+
# @return [Boolean] `true` if this lock is currently held.
|
48
43
|
def locked?
|
49
44
|
@object.reload!
|
50
45
|
@object.exists?
|
@@ -52,25 +47,36 @@ module GCSLock
|
|
52
47
|
false
|
53
48
|
end
|
54
49
|
|
55
|
-
#
|
50
|
+
# Verifies if the lock is already owned by this instance.
|
51
|
+
#
|
52
|
+
# @return [Boolean] `true` if this lock is currently held by this instance.
|
56
53
|
def owned?
|
57
54
|
locked? && @object.size == @uuid.size && @object.download.read == @uuid
|
58
55
|
end
|
59
56
|
|
60
57
|
# Obtains a lock, runs the block, and releases the lock when the block completes.
|
61
|
-
#
|
58
|
+
#
|
59
|
+
# @param timeout [Integer] the duration to wait before cancelling the operation
|
60
|
+
# if the lock was not obtained (unlimited if _nil_).
|
61
|
+
#
|
62
|
+
# @return [Object] what the called block returned.
|
63
|
+
#
|
64
|
+
# @raise [LockAlreadyOwnedError] if the lock is already owned by the current instance.
|
65
|
+
# @raise [LockTimeoutError] if the lock was not obtained before reaching the timeout.
|
62
66
|
def synchronize(timeout: nil)
|
63
|
-
raise LockAlreadyOwnedError, "Mutex for #{@object.name} is already owned by this process" if owned?
|
64
|
-
|
65
67
|
lock(timeout: timeout)
|
66
68
|
begin
|
67
|
-
yield
|
69
|
+
block = yield
|
68
70
|
ensure
|
69
71
|
unlock
|
70
72
|
end
|
73
|
+
|
74
|
+
block
|
71
75
|
end
|
72
76
|
|
73
|
-
# Attempts to obtain the lock and returns immediately.
|
77
|
+
# Attempts to obtain the lock and returns immediately.
|
78
|
+
#
|
79
|
+
# @return [Boolean] `true` if the lock was granted.
|
74
80
|
def try_lock
|
75
81
|
@client.service.service.insert_object(
|
76
82
|
@bucket.name,
|
@@ -86,15 +92,27 @@ module GCSLock
|
|
86
92
|
false
|
87
93
|
end
|
88
94
|
|
89
|
-
# Releases the lock.
|
95
|
+
# Releases the lock.
|
96
|
+
#
|
97
|
+
# @return _nil_
|
98
|
+
#
|
99
|
+
# @raise [LockNotOwnedError] if the lock is not owned by the current instance.
|
90
100
|
def unlock
|
91
101
|
raise LockNotOwnedError, "Mutex for #{@object.name} is not owned by this process" unless owned?
|
92
102
|
@object.delete
|
103
|
+
|
104
|
+
nil
|
93
105
|
end
|
94
106
|
|
95
|
-
# Releases the lock even if not owned by this instance.
|
107
|
+
# Releases the lock even if not owned by this instance.
|
108
|
+
#
|
109
|
+
# @return _nil_
|
110
|
+
#
|
111
|
+
# @raise [LockNotFoundError] if the lock is not held by anyone.
|
96
112
|
def unlock!
|
97
113
|
@object.delete
|
114
|
+
|
115
|
+
nil
|
98
116
|
rescue Google::Cloud::NotFoundError => e
|
99
117
|
raise LockNotFoundError, "Mutex for #{@object.name} not found"
|
100
118
|
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'google/cloud/storage'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
require_relative 'errors'
|
5
|
+
require_relative 'mutex'
|
6
|
+
require_relative 'utils'
|
7
|
+
|
8
|
+
module GCSLock
|
9
|
+
class Semaphore
|
10
|
+
def initialize(bucket, object, count, client: nil, uuid: nil, min_backoff: nil, max_backoff: nil)
|
11
|
+
@client = client || Google::Cloud::Storage.new
|
12
|
+
@bucket = bucket
|
13
|
+
@object = object
|
14
|
+
@count = count
|
15
|
+
|
16
|
+
@uuid = uuid || SecureRandom.uuid
|
17
|
+
@min_backoff = min_backoff || 0.01
|
18
|
+
@max_backoff = max_backoff || 5.0
|
19
|
+
|
20
|
+
@permits = []
|
21
|
+
end
|
22
|
+
|
23
|
+
# Attempts to grab permits and waits if it isn't available.
|
24
|
+
#
|
25
|
+
# @param permits [Integer] the number of permits to acquire
|
26
|
+
# @param timeout [Integer] the duration to wait before cancelling the operation
|
27
|
+
# if the lock was not obtained (unlimited if _nil_).
|
28
|
+
# @param permits_to_check [Integer] the number of permits to check for acquisition
|
29
|
+
# until the required number of permits is secured for each iteration
|
30
|
+
# (defaults to _nil_, all permits if _nil_)
|
31
|
+
#
|
32
|
+
# @return [Boolean] `true` if the lock was obtained.
|
33
|
+
#
|
34
|
+
# @raise [LockAlreadyOwnedError] if the permit is already owned by the current instance.
|
35
|
+
# @raise [LockTimeoutError] if the permits were not obtained before reaching the timeout.
|
36
|
+
def acquire(permits: 1, timeout: nil, permits_to_check: nil)
|
37
|
+
begin
|
38
|
+
Utils.backoff(min_backoff: @min_backoff, max_backoff: @max_backoff, timeout: timeout) do
|
39
|
+
try_acquire(permits: permits, permits_to_check: permits_to_check)
|
40
|
+
end
|
41
|
+
rescue LockTimeoutError
|
42
|
+
raise LockTimeoutError, "Unable to get semaphore permit for #{@object} before timeout"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Attempts to obtain a permit and returns immediately.
|
47
|
+
#
|
48
|
+
# @param permits [Integer] the number of permits to acquire
|
49
|
+
# @param permits_to_check [Integer] the number of permits to check for acquisition
|
50
|
+
# until the required number of permits is secured (defaults to _nil_, all permits if _nil_)
|
51
|
+
#
|
52
|
+
# @return [Boolean] `true` if the requested number of permits was granted.
|
53
|
+
def try_acquire(permits: 1, permits_to_check: nil)
|
54
|
+
acquired = []
|
55
|
+
|
56
|
+
@count.times.to_a.sample(permits_to_check || @count).each do |index|
|
57
|
+
mutex = mutex_object(index: index)
|
58
|
+
if mutex.try_lock
|
59
|
+
acquired.push(mutex)
|
60
|
+
break if acquired.size == permits
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if acquired.size < permits
|
65
|
+
acquired.each { |mutex| mutex.unlock }
|
66
|
+
return false
|
67
|
+
end
|
68
|
+
|
69
|
+
@permits.push(*acquired)
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
# Releases the given number of permits.
|
74
|
+
#
|
75
|
+
# @param permits [Integer] the number of permits to acquire
|
76
|
+
#
|
77
|
+
# @return _nil_
|
78
|
+
#
|
79
|
+
# @raise [LockNotOwnedError] if the permit is not owned by the current instance.
|
80
|
+
def release(permits: 1)
|
81
|
+
permits.times do
|
82
|
+
raise LockNotOwnedError, "No semaphore for #{@object} is owned by this process" unless @permits&.any?
|
83
|
+
|
84
|
+
@permits.pop.unlock
|
85
|
+
end
|
86
|
+
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
|
90
|
+
# Releases all of the owned permits.
|
91
|
+
#
|
92
|
+
# @return _nil_
|
93
|
+
#
|
94
|
+
# @raise [LockNotOwnedError] if the permit is not owned by the current instance.
|
95
|
+
def release_all
|
96
|
+
while @permits&.any?
|
97
|
+
@permits.pop.unlock
|
98
|
+
end
|
99
|
+
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
|
103
|
+
# Force releases all of the permits in the semaphore, even if not owned.
|
104
|
+
#
|
105
|
+
# @return _nil_
|
106
|
+
def release_all!
|
107
|
+
mutexes = @count.times.map { |index| mutex_object(index: index) }
|
108
|
+
mutexes.each do |mut|
|
109
|
+
mut.unlock!
|
110
|
+
rescue LockNotFoundError
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
|
114
|
+
@permits = []
|
115
|
+
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
|
119
|
+
# Acquires and returns all permits that are immediately available.
|
120
|
+
#
|
121
|
+
# @return [Integer] The number of permits acquired
|
122
|
+
def drain_permits
|
123
|
+
mutexes = @count.times.map { |index| mutex_object(index: index) }
|
124
|
+
mutexes.select! { |mutex| mutex.try_lock }
|
125
|
+
|
126
|
+
@permits.push(*mutexes)
|
127
|
+
|
128
|
+
mutexes.size
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns the current number of permits available for this semaphore.
|
132
|
+
#
|
133
|
+
# @return [Integer] The number of permits available
|
134
|
+
def available_permits
|
135
|
+
mutexes = @count.times.map { |index| mutex_object(index: index) }
|
136
|
+
mutexes.select! { |mutex| !mutex.locked? }
|
137
|
+
|
138
|
+
mutexes.size
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns the current number of permits owned by this process for this semaphore.
|
142
|
+
#
|
143
|
+
# @return [Integer] The number of permits owned by this process
|
144
|
+
def owned_permits
|
145
|
+
@permits.select! { |mutex| mutex.owned? }
|
146
|
+
@permits.size
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def mutex_object(index: nil)
|
152
|
+
GCSLock::Mutex.new(
|
153
|
+
@bucket, "#{@object}.#{index.nil? ? rand(@count) : index}",
|
154
|
+
client: @client,
|
155
|
+
uuid: @uuid,
|
156
|
+
min_backoff: @min_backoff,
|
157
|
+
max_backoff: @max_backoff,
|
158
|
+
)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module GCSLock
|
2
|
+
class Utils
|
3
|
+
class << self
|
4
|
+
def backoff(min_backoff:, max_backoff:, timeout: nil)
|
5
|
+
backoff = min_backoff
|
6
|
+
|
7
|
+
now = Time.now
|
8
|
+
end_time = now + timeout unless timeout.nil?
|
9
|
+
|
10
|
+
loop do
|
11
|
+
return true if yield
|
12
|
+
break if !timeout.nil? && now + backoff >= end_time
|
13
|
+
sleep(backoff)
|
14
|
+
|
15
|
+
backoff_opts = [max_backoff, backoff * 2]
|
16
|
+
|
17
|
+
unless timeout.nil?
|
18
|
+
now = Time.now
|
19
|
+
diff = end_time - now
|
20
|
+
backoff_opts.push(diff) if diff > 0
|
21
|
+
end
|
22
|
+
|
23
|
+
backoff = backoff_opts.min
|
24
|
+
end
|
25
|
+
|
26
|
+
raise LockTimeoutError, "Backoff timed out"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
data/lib/gcslock/version.rb
CHANGED
data/spec/gcslock/mutex_spec.rb
CHANGED
@@ -33,13 +33,13 @@ describe GCSLock::Mutex do
|
|
33
33
|
allow(@bucket).to receive(:file).and_return(@object)
|
34
34
|
end
|
35
35
|
|
36
|
-
it 'initializes
|
36
|
+
it 'initializes a GCS client when none provided' do
|
37
37
|
expect(Google::Cloud::Storage).to receive(:new).once
|
38
38
|
|
39
39
|
GCSLock::Mutex.new(@bucket_name, @object_name)
|
40
40
|
end
|
41
41
|
|
42
|
-
it '
|
42
|
+
it 'does not initialize a GCS client when none provided' do
|
43
43
|
expect(Google::Cloud::Storage).not_to receive(:new)
|
44
44
|
|
45
45
|
GCSLock::Mutex.new(@bucket_name, @object_name, client: @gcs)
|
@@ -75,7 +75,7 @@ describe GCSLock::Mutex do
|
|
75
75
|
it 'sleeps and retry when failing on the first try_lock' do
|
76
76
|
expect(@mutex).to receive(:owned?).once.and_return(false)
|
77
77
|
expect(@mutex).to receive(:try_lock).once.and_return(false)
|
78
|
-
expect(
|
78
|
+
expect(GCSLock::Utils).to receive(:sleep).once
|
79
79
|
expect(@mutex).to receive(:try_lock).once.and_return(true)
|
80
80
|
|
81
81
|
@mutex.lock(timeout: 2)
|
@@ -83,7 +83,7 @@ describe GCSLock::Mutex do
|
|
83
83
|
|
84
84
|
it 'sleeps just the time needed to retry once at the end' do
|
85
85
|
expect(@mutex).to receive(:owned?).once.and_return(false)
|
86
|
-
expect(
|
86
|
+
expect(GCSLock::Utils).to receive(:sleep).at_least(2).times
|
87
87
|
expect(@mutex).to receive(:try_lock).at_least(3).times.and_return(false)
|
88
88
|
|
89
89
|
expect do
|
@@ -178,7 +178,6 @@ describe GCSLock::Mutex do
|
|
178
178
|
|
179
179
|
describe '.synchronize' do
|
180
180
|
it 'locks, yields and unlock the mutex' do
|
181
|
-
expect(@mutex).to receive(:owned?).once.and_return(false)
|
182
181
|
expect(@mutex).to receive(:lock).once.and_return(true)
|
183
182
|
expect(@mutex).to receive(:unlock).once
|
184
183
|
|
@@ -192,7 +191,6 @@ describe GCSLock::Mutex do
|
|
192
191
|
|
193
192
|
it 'raises an error if the lock is already owned' do
|
194
193
|
expect(@mutex).to receive(:owned?).once.and_return(true)
|
195
|
-
expect(@mutex).not_to receive(:lock)
|
196
194
|
expect(@mutex).not_to receive(:unlock)
|
197
195
|
|
198
196
|
has_yielded = false
|
@@ -0,0 +1,401 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'gcslock/semaphore'
|
3
|
+
|
4
|
+
describe GCSLock::Semaphore do
|
5
|
+
before do
|
6
|
+
@bucket_name = 'bucket'
|
7
|
+
@object_name = 'object'
|
8
|
+
@count = 5
|
9
|
+
|
10
|
+
@gcs = instance_double(Google::Cloud::Storage::Project)
|
11
|
+
allow(Google::Cloud::Storage).to receive(:new).and_return(@gcs)
|
12
|
+
|
13
|
+
@uuid = 'some_uuid'
|
14
|
+
allow(SecureRandom).to receive(:uuid).and_return(@uuid)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '.initialize' do
|
18
|
+
before do
|
19
|
+
@gcs = double(Google::Cloud::Storage)
|
20
|
+
allow(Google::Cloud::Storage).to receive(:new).and_return(@gcs)
|
21
|
+
|
22
|
+
@bucket = double(Google::Cloud::Storage::Bucket)
|
23
|
+
allow(@gcs).to receive(:bucket).and_return(@bucket)
|
24
|
+
|
25
|
+
@object = double(Google::Cloud::Storage::File)
|
26
|
+
allow(@bucket).to receive(:file).and_return(@object)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'initializes in GCS client when none provided' do
|
30
|
+
expect(Google::Cloud::Storage).to receive(:new).once
|
31
|
+
|
32
|
+
GCSLock::Semaphore.new(@bucket_name, @object_name, @count)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'does not initialize a GCS client when none provided' do
|
36
|
+
expect(Google::Cloud::Storage).not_to receive(:new)
|
37
|
+
|
38
|
+
GCSLock::Semaphore.new(@bucket_name, @object_name, @count, client: @gcs)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'initializes a randomly generated unique ID' do
|
42
|
+
expect(SecureRandom).to receive(:uuid).once
|
43
|
+
|
44
|
+
GCSLock::Semaphore.new(@bucket_name, @object_name, @count)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'initialized' do
|
49
|
+
before do
|
50
|
+
@sem = GCSLock::Semaphore.new(@bucket_name, @object_name, @count)
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '.acquire' do
|
54
|
+
it 'sleeps and retry when failing on the first try_acquire' do
|
55
|
+
expect(@sem).to receive(:try_acquire).once.and_return(false)
|
56
|
+
expect(GCSLock::Utils).to receive(:sleep).once
|
57
|
+
expect(@sem).to receive(:try_acquire).once.and_return(true)
|
58
|
+
|
59
|
+
@sem.acquire(timeout: 2)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'sleeps just the time needed to retry once at the end' do
|
63
|
+
expect(GCSLock::Utils).to receive(:sleep).at_least(2).times
|
64
|
+
expect(@sem).to receive(:try_acquire).at_least(3).times.and_return(false)
|
65
|
+
|
66
|
+
expect do
|
67
|
+
@sem.acquire(timeout: 0.03)
|
68
|
+
end.to raise_error(GCSLock::LockTimeoutError)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'raises an error if unable to get a permit when reaching the timeout' do
|
72
|
+
expect(@sem).to receive(:try_acquire).once.and_return(false)
|
73
|
+
|
74
|
+
expect do
|
75
|
+
@sem.acquire(timeout: 0)
|
76
|
+
end.to raise_error(GCSLock::LockTimeoutError)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'calls try_acquire with the right value for permits_to_check if passed as argument' do
|
80
|
+
permits_to_check = 2
|
81
|
+
|
82
|
+
expect(@sem).to receive(:try_acquire).with(permits: 1, permits_to_check: permits_to_check).once.and_return(true)
|
83
|
+
|
84
|
+
@sem.acquire(permits_to_check: permits_to_check)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe '.try_acquire' do
|
89
|
+
before do
|
90
|
+
@mutexes = @count.times.map do |index|
|
91
|
+
mutex = instance_double(GCSLock::Mutex)
|
92
|
+
|
93
|
+
allow(@sem).to receive(:mutex_object).with(index: index).and_return(mutex)
|
94
|
+
|
95
|
+
mutex
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'returns true if permit (1) obtained, while all mutex available' do
|
100
|
+
@mutexes.each do |mutex|
|
101
|
+
allow(mutex).to receive(:try_lock).and_return(true)
|
102
|
+
end
|
103
|
+
|
104
|
+
expect(@sem.try_acquire).to be(true)
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'returns true if permit (1) obtained, while only a single mutex available' do
|
108
|
+
@mutexes.each_with_index do |mutex, index|
|
109
|
+
allow(mutex).to receive(:try_lock).and_return(index == 2)
|
110
|
+
end
|
111
|
+
|
112
|
+
expect(@sem.try_acquire).to be(true)
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'returns true if permit (3) obtained, while all mutex available' do
|
116
|
+
@mutexes.each do |mutex|
|
117
|
+
allow(mutex).to receive(:try_lock).and_return(true)
|
118
|
+
end
|
119
|
+
|
120
|
+
expect(@sem.try_acquire(permits: 3)).to be(true)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'returns true if permit (3) obtained, while only a single mutex available' do
|
124
|
+
available_mutexes = @count.times.to_a.sample(3)
|
125
|
+
|
126
|
+
@mutexes.each_with_index do |mutex, index|
|
127
|
+
allow(mutex).to receive(:try_lock).and_return(available_mutexes.include?(index))
|
128
|
+
end
|
129
|
+
|
130
|
+
expect(@sem.try_acquire(permits: 3)).to be(true)
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'returns false if no permit available' do
|
134
|
+
@mutexes.each_with_index do |mutex, index|
|
135
|
+
allow(mutex).to receive(:try_lock).and_return(false)
|
136
|
+
end
|
137
|
+
|
138
|
+
expect(@sem.try_acquire).to be(false)
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'returns false if not enough permits available' do
|
142
|
+
available_mutexes = @count.times.to_a.sample(2)
|
143
|
+
|
144
|
+
@mutexes.each_with_index do |mutex, index|
|
145
|
+
allow(mutex).to receive(:try_lock).and_return(available_mutexes.include?(index))
|
146
|
+
expect(mutex).to receive(:unlock).once if available_mutexes.include?(index)
|
147
|
+
end
|
148
|
+
|
149
|
+
expect(@sem.try_acquire(permits: 3)).to be(false)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe '.release' do
|
154
|
+
before do
|
155
|
+
@mutexes = @count.times.map do |index|
|
156
|
+
mutex = instance_double(GCSLock::Mutex)
|
157
|
+
|
158
|
+
mutex
|
159
|
+
end
|
160
|
+
|
161
|
+
@permits = @count.times.to_a.sample(3).map { |index| @mutexes[index] }
|
162
|
+
|
163
|
+
@sem.instance_variable_set(:@permits, @permits)
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'releases the permits (1) when called' do
|
167
|
+
expect(@permits.last).to receive(:unlock).once
|
168
|
+
|
169
|
+
@sem.release
|
170
|
+
|
171
|
+
expect(@sem.instance_variable_get(:@permits).size).to eq(2)
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'releases the permits (3) when called' do
|
175
|
+
@permits.each do |mutex|
|
176
|
+
expect(mutex).to receive(:unlock).once
|
177
|
+
end
|
178
|
+
|
179
|
+
@sem.release(permits: 3)
|
180
|
+
|
181
|
+
expect(@sem.instance_variable_get(:@permits).size).to eq(0)
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'raises an error when releasing more permits (4) than owned (3), but still released the owned locks' do
|
185
|
+
@permits.each do |mutex|
|
186
|
+
expect(mutex).to receive(:unlock).once
|
187
|
+
end
|
188
|
+
|
189
|
+
expect do
|
190
|
+
@sem.release(permits: 4)
|
191
|
+
end.to raise_error(GCSLock::LockNotOwnedError)
|
192
|
+
|
193
|
+
expect(@sem.instance_variable_get(:@permits).size).to eq(0)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe '.release_all' do
|
198
|
+
before do
|
199
|
+
@mutexes = @count.times.map do |index|
|
200
|
+
mutex = instance_double(GCSLock::Mutex)
|
201
|
+
|
202
|
+
mutex
|
203
|
+
end
|
204
|
+
|
205
|
+
@permits = @count.times.to_a.sample(3).map { |index| @mutexes[index] }
|
206
|
+
|
207
|
+
@sem.instance_variable_set(:@permits, @permits)
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'releases all owned permits when called' do
|
211
|
+
@permits.each do |mutex|
|
212
|
+
expect(mutex).to receive(:unlock).once
|
213
|
+
end
|
214
|
+
|
215
|
+
@sem.release_all
|
216
|
+
|
217
|
+
expect(@sem.instance_variable_get(:@permits).size).to eq(0)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
describe '.release_all!' do
|
222
|
+
before do
|
223
|
+
@mutexes = @count.times.map do |index|
|
224
|
+
mutex = instance_double(GCSLock::Mutex)
|
225
|
+
|
226
|
+
allow(@sem).to receive(:mutex_object).with(index: index).and_return(mutex)
|
227
|
+
|
228
|
+
mutex
|
229
|
+
end
|
230
|
+
|
231
|
+
@permits = @count.times.to_a.sample(3).map { |index| @mutexes[index] }
|
232
|
+
|
233
|
+
@sem.instance_variable_set(:@permits, @permits)
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'releases all permits when called' do
|
237
|
+
@mutexes.each do |mutex|
|
238
|
+
expect(mutex).to receive(:unlock!).once
|
239
|
+
end
|
240
|
+
|
241
|
+
@sem.release_all!
|
242
|
+
|
243
|
+
expect(@sem.instance_variable_get(:@permits).size).to eq(0)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
describe '.drain_permits' do
|
248
|
+
before do
|
249
|
+
@mutexes = @count.times.map do |index|
|
250
|
+
mutex = instance_double(GCSLock::Mutex)
|
251
|
+
|
252
|
+
allow(@sem).to receive(:mutex_object).with(index: index).and_return(mutex)
|
253
|
+
|
254
|
+
mutex
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'gets all the available permits (all)' do
|
259
|
+
@mutexes.each do |mutex|
|
260
|
+
expect(mutex).to receive(:try_lock).and_return(true)
|
261
|
+
end
|
262
|
+
|
263
|
+
expect(@sem.drain_permits).to eq(@count)
|
264
|
+
|
265
|
+
expect(@sem.instance_variable_get(:@permits).size).to eq(@count)
|
266
|
+
end
|
267
|
+
|
268
|
+
it 'gets all the available permits (2) when already owning some' do
|
269
|
+
owned_permits = @count.times.to_a.sample(3)
|
270
|
+
permits = owned_permits.map { |index| @mutexes[index] }
|
271
|
+
@sem.instance_variable_set(:@permits, permits)
|
272
|
+
|
273
|
+
@mutexes.each_with_index do |mutex, index|
|
274
|
+
expect(mutex).to receive(:try_lock).and_return(!owned_permits.include?(index))
|
275
|
+
end
|
276
|
+
|
277
|
+
expect(@sem.drain_permits).to eq(@count - owned_permits.size)
|
278
|
+
|
279
|
+
expect(@sem.instance_variable_get(:@permits).size).to eq(@count)
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'gets all available permits (2) when somebody else is already owning some' do
|
283
|
+
owned_permits = @count.times.to_a.sample(3)
|
284
|
+
|
285
|
+
@mutexes.each_with_index do |mutex, index|
|
286
|
+
expect(mutex).to receive(:try_lock).and_return(!owned_permits.include?(index))
|
287
|
+
end
|
288
|
+
|
289
|
+
expect(@sem.drain_permits).to eq(@count - owned_permits.size)
|
290
|
+
|
291
|
+
expect(@sem.instance_variable_get(:@permits).size).to eq(@count - owned_permits.size)
|
292
|
+
end
|
293
|
+
|
294
|
+
it 'gets nothing when all the permits are owned' do
|
295
|
+
@mutexes.each_with_index do |mutex, index|
|
296
|
+
expect(mutex).to receive(:try_lock).and_return(false)
|
297
|
+
end
|
298
|
+
|
299
|
+
expect(@sem.drain_permits).to eq(0)
|
300
|
+
|
301
|
+
expect(@sem.instance_variable_get(:@permits).size).to eq(0)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
describe '.available_permits' do
|
306
|
+
before do
|
307
|
+
@mutexes = @count.times.map do |index|
|
308
|
+
mutex = instance_double(GCSLock::Mutex)
|
309
|
+
|
310
|
+
allow(@sem).to receive(:mutex_object).with(index: index).and_return(mutex)
|
311
|
+
|
312
|
+
mutex
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
it 'returns the number of available permits (all)' do
|
317
|
+
@mutexes.each do |mutex|
|
318
|
+
expect(mutex).to receive(:locked?).and_return(false)
|
319
|
+
end
|
320
|
+
|
321
|
+
expect(@sem.available_permits).to eq(@count)
|
322
|
+
|
323
|
+
expect(@sem.instance_variable_get(:@permits).size).to eq(0)
|
324
|
+
end
|
325
|
+
|
326
|
+
it 'returns the number of available permits (2) when already owning some' do
|
327
|
+
owned_permits = @count.times.to_a.sample(3)
|
328
|
+
permits = owned_permits.map { |index| @mutexes[index] }
|
329
|
+
@sem.instance_variable_set(:@permits, permits.dup)
|
330
|
+
|
331
|
+
@mutexes.each_with_index do |mutex, index|
|
332
|
+
expect(mutex).to receive(:locked?).and_return(owned_permits.include?(index))
|
333
|
+
end
|
334
|
+
|
335
|
+
expect(@sem.available_permits).to eq(@count - owned_permits.size)
|
336
|
+
|
337
|
+
expect(@sem.instance_variable_get(:@permits)).to eq(permits)
|
338
|
+
end
|
339
|
+
|
340
|
+
it 'returns the number of available permits (2) when somebody else is already owning some' do
|
341
|
+
owned_permits = @count.times.to_a.sample(3)
|
342
|
+
|
343
|
+
@mutexes.each_with_index do |mutex, index|
|
344
|
+
expect(mutex).to receive(:locked?).and_return(owned_permits.include?(index))
|
345
|
+
end
|
346
|
+
|
347
|
+
expect(@sem.available_permits).to eq(@count - owned_permits.size)
|
348
|
+
|
349
|
+
expect(@sem.instance_variable_get(:@permits).size).to eq(0)
|
350
|
+
end
|
351
|
+
|
352
|
+
it 'returns 0 when all the permits are owned' do
|
353
|
+
@mutexes.each_with_index do |mutex, index|
|
354
|
+
expect(mutex).to receive(:locked?).and_return(true)
|
355
|
+
end
|
356
|
+
|
357
|
+
expect(@sem.available_permits).to eq(0)
|
358
|
+
|
359
|
+
expect(@sem.instance_variable_get(:@permits).size).to eq(0)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
describe '.owned_permits' do
|
364
|
+
before do
|
365
|
+
@mutexes = @count.times.map do |index|
|
366
|
+
mutex = instance_double(GCSLock::Mutex)
|
367
|
+
|
368
|
+
mutex
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'returns the owned permits after checking they are properly owned (all owned)' do
|
373
|
+
owned_permits = @count.times.to_a.sample(3)
|
374
|
+
permits = owned_permits.map { |index| @mutexes[index] }
|
375
|
+
@sem.instance_variable_set(:@permits, permits.dup)
|
376
|
+
|
377
|
+
permits.each do |mutex|
|
378
|
+
expect(mutex).to receive(:owned?).and_return(true)
|
379
|
+
end
|
380
|
+
|
381
|
+
expect(@sem.owned_permits).to eq(owned_permits.size)
|
382
|
+
expect(@sem.instance_variable_get(:@permits)).to eq(permits)
|
383
|
+
end
|
384
|
+
|
385
|
+
it 'returns the owned permits after checking they are properly owned (one not owned)' do
|
386
|
+
owned_permits = @count.times.to_a.sample(3)
|
387
|
+
permits = owned_permits.map { |index| @mutexes[index] }
|
388
|
+
not_really_owned = permits.sample
|
389
|
+
@sem.instance_variable_set(:@permits, permits.dup)
|
390
|
+
|
391
|
+
permits.each do |mutex|
|
392
|
+
expect(mutex).to receive(:owned?).and_return(mutex != not_really_owned)
|
393
|
+
end
|
394
|
+
|
395
|
+
expect(@sem.owned_permits).to eq(owned_permits.size - 1)
|
396
|
+
expect(@sem.instance_variable_get(:@permits)).to eq(permits.select { |mutex| mutex != not_really_owned })
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
end
|
401
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gcslock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Raphaël Beamonte
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: google-api-client
|
@@ -81,8 +81,11 @@ files:
|
|
81
81
|
- gcslock.gemspec
|
82
82
|
- lib/gcslock/errors.rb
|
83
83
|
- lib/gcslock/mutex.rb
|
84
|
+
- lib/gcslock/semaphore.rb
|
85
|
+
- lib/gcslock/utils.rb
|
84
86
|
- lib/gcslock/version.rb
|
85
87
|
- spec/gcslock/mutex_spec.rb
|
88
|
+
- spec/gcslock/semaphore_spec.rb
|
86
89
|
- spec/spec_helper.rb
|
87
90
|
homepage: https://github.com/XaF/gcslock-ruby
|
88
91
|
licenses:
|