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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d0120bfbfe6842595ebbdcdb1108937c61ee4e04ea71214ae0027b0ed0e68e1
4
- data.tar.gz: 6002bf7b00837cf7685400909d550104ced2fc6b99fe22097589c6fde59b6ae7
3
+ metadata.gz: 2fa87f56360a57de01a55b6b9cf5607b678f38d719b5e5e82f6a77dba84ecf6c
4
+ data.tar.gz: c121126258ba7b7da738ed1afd61a68d13a191f0e18f5e3318bc30f8a81fb1d8
5
5
  SHA512:
6
- metadata.gz: 3bea490c78bb1e57788500046967379c4f69862e7b49c03eae367547233b811671c0316152ff231d643dbd70378775af3786d292a4d0c7d557fa3b34e53986f2
7
- data.tar.gz: 9388122c6c3a2c3db1250177f4e75dc040de63cb5d0a9f839533a4bb02d7d2d9a143eec0e415466880267b0ed04a770c1437f012bb6c1aa564b4ce31c0ebca8d
6
+ metadata.gz: d4ac377cc6e39833e5ccc8afedcaa1df7c186a5ff230b9cb1c7f991c74e67e0c4d0c6fb29e445e226ba40daebcffe7fc6a1f80224434709ca8710058116c26ef
7
+ data.tar.gz: fc2b9b5c25f468e430f035b48edfe254f35b47ebd45bf81c7a5d84b532ed389764353e24ce9a18e358544e33306838dfb6ff8b7b7349e0d27e7171920b012443
data/README.md CHANGED
@@ -15,7 +15,7 @@ require 'gcslock/mutex'
15
15
 
16
16
  m = GCSLock::Mutex.new('your-bucket-name', 'my-file.lock')
17
17
  m.synchronize do
18
- // Protected and globally serialized computation happens here.
18
+ # Protected and globally serialized computation happens here.
19
19
  end
20
20
  ```
21
21
 
@@ -1,5 +1,6 @@
1
1
  module GCSLock
2
2
  class Error < StandardError; end
3
+
3
4
  class LockAlreadyOwnedError < Error; end
4
5
  class LockNotOwnedError < Error; end
5
6
  class LockNotFoundError < Error; end
@@ -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
- # Raises `ThreadError` if `mutex` was locked by the current thread.
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
- backoff = @min_backoff
24
-
25
- now = Time.now
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
- backoff = backoff_opts.min
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
- # Returns `true` if this lock is currently held by some thread.
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
- # Returns `true` if this lock is currently held by current thread.
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
- # Raises `LockAlreadyOwnedError` if the lock is already owned by the current instance.
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. Returns `true` if the lock was granted.
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. Raises `LockNotOwnedError` if the lock is not owned by the current instance.
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. Raises `LockNotFoundError` if the lock cannot be found.
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
+
@@ -1,3 +1,3 @@
1
1
  module GCSLock
2
- VERSION = '1.0.1'.freeze
2
+ VERSION = '1.0.2'.freeze
3
3
  end
@@ -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 in GCS client when none provided' do
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 'initializes in GCS client when none provided' do
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(@mutex).to receive(:sleep).once
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(@mutex).to receive(:sleep).at_least(2).times
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.1
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-05-20 00:00:00.000000000 Z
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: