gcslock 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|