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 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: