gcslock 1.0.0
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 +7 -0
- data/.gitignore +5 -0
- data/Gemfile +3 -0
- data/README.md +21 -0
- data/bin/rspec +16 -0
- data/gcslock.gemspec +29 -0
- data/lib/gcslock/errors.rb +7 -0
- data/lib/gcslock/mutex.rb +97 -0
- data/lib/gcslock/version.rb +3 -0
- data/spec/gcslock/mutex_spec.rb +308 -0
- data/spec/spec_helper.rb +9 -0
- metadata +111 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1068b280fd4e77bb1270880eaa80aa0145d64f35d58121329e8500ecc2321948
|
|
4
|
+
data.tar.gz: 33b5f11756a7c89f55384730322fd43aebcc810dff452c4cbf13bb05f2920aea
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: de5e846755a1c556d263b6af0806b6e102bbd4c474aad7513aa3df8d84d6a8e1afb56b35cc3006e52f76508eac3bc691b5776175142f0e3d1cd9a0fb68ad40fa
|
|
7
|
+
data.tar.gz: fc5c9318fcdabb46acff6b89b54c9967ed13749c8cda75cc78699d063f233135a126013874667adc4e300c4975d82ad8da0971f1a972aed23ae01fdf236001b7
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# gcslock-ruby
|
|
2
|
+
|
|
3
|
+
This is inspired by [the Golang version](https://github.com/marcacohen/gcslock).
|
|
4
|
+
|
|
5
|
+
## Google Cloud Storage setup
|
|
6
|
+
|
|
7
|
+
1. Setup a new project at the [Google APIs Console](https://console.developers.google.com) and enable the Cloud Storage API.
|
|
8
|
+
1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/downloads) tool and configure your project and your OAuth credentials.
|
|
9
|
+
1. Create a bucket in which to store your lock file using the command `gsutil mb gs://your-bucket-name`.
|
|
10
|
+
1. Enable object versioning in your bucket using the command `gsutil versioning set on gs://your-bucket-name`.
|
|
11
|
+
1. In your Ruby code, require `gcslock/mutex` and use it as follows:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require 'gcslock/mutex'
|
|
15
|
+
|
|
16
|
+
m = GCSLock::Mutex.new('your-bucket-name', 'my-file.lock')
|
|
17
|
+
m.synchronize do
|
|
18
|
+
// Protected and globally serialized computation happens here.
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
data/bin/rspec
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# This file was generated by Bundler.
|
|
4
|
+
#
|
|
5
|
+
# The application 'rspec' is installed as part of a gem, and
|
|
6
|
+
# this file is here to facilitate running it.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
require 'pathname'
|
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
|
11
|
+
Pathname.new(__FILE__).realpath)
|
|
12
|
+
|
|
13
|
+
require 'rubygems'
|
|
14
|
+
require 'bundler/setup'
|
|
15
|
+
|
|
16
|
+
load(Gem.bin_path('rspec-core', 'rspec'))
|
data/gcslock.gemspec
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
+
require 'gcslock/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'gcslock'
|
|
7
|
+
spec.version = GCSLock::VERSION
|
|
8
|
+
spec.authors = ['Raphaël Beamonte']
|
|
9
|
+
spec.email = ['raphael.beamonte@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Google Cloud Storage distributed locking'
|
|
12
|
+
spec.description = "Allows to use a Google Cloud Storage bucket as a distributed locking system"
|
|
13
|
+
spec.homepage = 'https://github.com/XaF/gcslock-ruby'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
|
|
16
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes." unless spec.respond_to?(:metadata)
|
|
17
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
|
18
|
+
|
|
19
|
+
spec.files = %x(git ls-files -z).split("\x0").reject { |f| f.match(/^(test|DESIGN)/) }
|
|
20
|
+
spec.require_paths = ['lib']
|
|
21
|
+
|
|
22
|
+
spec.required_ruby_version = '>= 2.0'
|
|
23
|
+
|
|
24
|
+
spec.add_runtime_dependency 'google-api-client'
|
|
25
|
+
spec.add_runtime_dependency 'google-cloud-storage', '~> 1.26.1'
|
|
26
|
+
|
|
27
|
+
spec.add_development_dependency 'rspec'
|
|
28
|
+
spec.add_development_dependency 'codecov'
|
|
29
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require 'google/cloud/storage'
|
|
2
|
+
require 'securerandom'
|
|
3
|
+
|
|
4
|
+
require_relative 'errors'
|
|
5
|
+
|
|
6
|
+
module GCSLock
|
|
7
|
+
class Mutex
|
|
8
|
+
def initialize(bucket, object, client: nil, uuid: nil, min_backoff: nil, max_backoff: nil)
|
|
9
|
+
@client = client || Google::Cloud::Storage.new
|
|
10
|
+
@bucket = @client.bucket(bucket, skip_lookup: true)
|
|
11
|
+
@object = @bucket.file(object, skip_lookup: true)
|
|
12
|
+
|
|
13
|
+
@uuid = uuid || SecureRandom.uuid
|
|
14
|
+
@min_backoff = min_backoff || 0.01
|
|
15
|
+
@max_backoff = max_backoff || 5.0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Attempts to grab the lock and waits if it isn't available.
|
|
19
|
+
# Raises `ThreadError` if `mutex` was locked by the current thread.
|
|
20
|
+
def lock(timeout: nil)
|
|
21
|
+
raise LockAlreadyOwnedError, "Mutex for #{@object.name} is already owned by this process" if owned?
|
|
22
|
+
|
|
23
|
+
backoff = @min_backoff
|
|
24
|
+
waited = 0.0 unless timeout.nil?
|
|
25
|
+
|
|
26
|
+
loop do
|
|
27
|
+
return true if try_lock
|
|
28
|
+
break if !timeout.nil? && waited + backoff > timeout
|
|
29
|
+
sleep(backoff)
|
|
30
|
+
|
|
31
|
+
backoff_opts = [@max_backoff, backoff * 2]
|
|
32
|
+
|
|
33
|
+
unless timeout.nil?
|
|
34
|
+
waited += backoff
|
|
35
|
+
backoff_opts.push(timeout - waited) if timeout > waited
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
backoff = backoff_opts.min
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
raise LockTimeoutError, "Unable to get mutex for #{@object.name} before timeout"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns `true` if this lock is currently held by some thread.
|
|
45
|
+
def locked?
|
|
46
|
+
@object.reload!
|
|
47
|
+
@object.exists?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns `true` if this lock is currently held by current thread.
|
|
51
|
+
def owned?
|
|
52
|
+
locked? && @object.size == @uuid.size && @object.download.read == @uuid
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Obtains a lock, runs the block, and releases the lock when the block completes.
|
|
56
|
+
# Raises `LockAlreadyOwnedError` if the lock is already owned by the current instance.
|
|
57
|
+
def synchronize(timeout: nil)
|
|
58
|
+
raise LockAlreadyOwnedError, "Mutex for #{@object.name} is already owned by this process" if owned?
|
|
59
|
+
|
|
60
|
+
lock(timeout: timeout)
|
|
61
|
+
begin
|
|
62
|
+
yield
|
|
63
|
+
ensure
|
|
64
|
+
unlock
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Attempts to obtain the lock and returns immediately. Returns `true` if the lock was granted.
|
|
69
|
+
def try_lock
|
|
70
|
+
@client.service.service.insert_object(
|
|
71
|
+
@bucket.name,
|
|
72
|
+
name: @object.name,
|
|
73
|
+
if_generation_match: 0,
|
|
74
|
+
upload_source: StringIO.new(@uuid),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
true
|
|
78
|
+
rescue Google::Apis::ClientError => e
|
|
79
|
+
raise unless e.status_code == 412 && e.message.start_with?('conditionNotMet:')
|
|
80
|
+
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Releases the lock. Raises `LockNotOwnedError` if the lock is not owned by the current instance.
|
|
85
|
+
def unlock
|
|
86
|
+
raise LockNotOwnedError, "Mutex for #{@object.name} is not owned by this process" unless owned?
|
|
87
|
+
@object.delete
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Releases the lock even if not owned by this instance. Raises `LockNotFoundError` if the lock cannot be found.
|
|
91
|
+
def unlock!
|
|
92
|
+
@object.delete
|
|
93
|
+
rescue Google::Cloud::NotFoundError => e
|
|
94
|
+
raise LockNotFoundError, "Mutex for #{@object.name} not found"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'gcslock/mutex'
|
|
3
|
+
|
|
4
|
+
describe GCSLock::Mutex do
|
|
5
|
+
before do
|
|
6
|
+
@bucket_name = 'bucket'
|
|
7
|
+
@object_name = 'object'
|
|
8
|
+
|
|
9
|
+
@gcs = instance_double(Google::Cloud::Storage::Project)
|
|
10
|
+
allow(Google::Cloud::Storage).to receive(:new).and_return(@gcs)
|
|
11
|
+
|
|
12
|
+
@bucket = instance_double(Google::Cloud::Storage::Bucket)
|
|
13
|
+
allow(@bucket).to receive(:name).and_return(@bucket_name)
|
|
14
|
+
allow(@gcs).to receive(:bucket).and_return(@bucket)
|
|
15
|
+
|
|
16
|
+
@object = instance_double(Google::Cloud::Storage::File)
|
|
17
|
+
allow(@object).to receive(:name).and_return(@object_name)
|
|
18
|
+
allow(@bucket).to receive(:file).and_return(@object)
|
|
19
|
+
|
|
20
|
+
@uuid = 'some_uuid'
|
|
21
|
+
allow(SecureRandom).to receive(:uuid).and_return(@uuid)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '.initialize' do
|
|
25
|
+
before do
|
|
26
|
+
@gcs = double(Google::Cloud::Storage)
|
|
27
|
+
allow(Google::Cloud::Storage).to receive(:new).and_return(@gcs)
|
|
28
|
+
|
|
29
|
+
@bucket = double(Google::Cloud::Storage::Bucket)
|
|
30
|
+
allow(@gcs).to receive(:bucket).and_return(@bucket)
|
|
31
|
+
|
|
32
|
+
@object = double(Google::Cloud::Storage::File)
|
|
33
|
+
allow(@bucket).to receive(:file).and_return(@object)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'initializes in GCS client when none provided' do
|
|
37
|
+
expect(Google::Cloud::Storage).to receive(:new).once
|
|
38
|
+
|
|
39
|
+
GCSLock::Mutex.new(@bucket_name, @object_name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'initializes in GCS client when none provided' do
|
|
43
|
+
expect(Google::Cloud::Storage).not_to receive(:new)
|
|
44
|
+
|
|
45
|
+
GCSLock::Mutex.new(@bucket_name, @object_name, client: @gcs)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'initializes a bucket with lazy loading' do
|
|
49
|
+
expect(Google::Cloud::Storage).to receive(:new).once
|
|
50
|
+
expect(@gcs).to receive(:bucket).with(@bucket_name, skip_lookup: true).once
|
|
51
|
+
|
|
52
|
+
GCSLock::Mutex.new(@bucket_name, @object_name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'initializes a file with lazy loading' do
|
|
56
|
+
expect(Google::Cloud::Storage).to receive(:new).once
|
|
57
|
+
expect(@bucket).to receive(:file).with(@object_name, skip_lookup: true).once
|
|
58
|
+
|
|
59
|
+
GCSLock::Mutex.new(@bucket_name, @object_name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'initializes a randomly generated unique ID' do
|
|
63
|
+
expect(SecureRandom).to receive(:uuid).once
|
|
64
|
+
|
|
65
|
+
GCSLock::Mutex.new(@bucket_name, @object_name)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context 'initialized' do
|
|
70
|
+
before do
|
|
71
|
+
@mutex = GCSLock::Mutex.new(@bucket_name, @object_name)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe '.lock' do
|
|
75
|
+
it 'sleeps and retry when failing on the first try_lock' do
|
|
76
|
+
expect(@mutex).to receive(:owned?).once.and_return(false)
|
|
77
|
+
expect(@mutex).to receive(:try_lock).once.and_return(false)
|
|
78
|
+
expect(@mutex).to receive(:sleep).once
|
|
79
|
+
expect(@mutex).to receive(:try_lock).once.and_return(true)
|
|
80
|
+
|
|
81
|
+
@mutex.lock(timeout: 2)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'sleeps just the time needed to retry once at the end' do
|
|
85
|
+
expect(@mutex).to receive(:owned?).once.and_return(false)
|
|
86
|
+
expect(@mutex).to receive(:sleep).exactly(2).times
|
|
87
|
+
expect(@mutex).to receive(:try_lock).exactly(3).times.and_return(false)
|
|
88
|
+
|
|
89
|
+
expect do
|
|
90
|
+
@mutex.lock(timeout: 0.03)
|
|
91
|
+
end.to raise_error(GCSLock::LockTimeoutError)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'raises an error if unable to get the lock when reaching the timeout' do
|
|
95
|
+
expect(@mutex).to receive(:owned?).once.and_return(false)
|
|
96
|
+
expect(@mutex).to receive(:try_lock).once.and_return(false)
|
|
97
|
+
|
|
98
|
+
expect do
|
|
99
|
+
@mutex.lock(timeout: 0)
|
|
100
|
+
end.to raise_error(GCSLock::LockTimeoutError)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'raises an error if the lock is already owned' do
|
|
104
|
+
expect(@mutex).to receive(:owned?).once.and_return(true)
|
|
105
|
+
expect(@mutex).not_to receive(:try_lock)
|
|
106
|
+
|
|
107
|
+
expect do
|
|
108
|
+
@mutex.lock
|
|
109
|
+
end.to raise_error(GCSLock::LockAlreadyOwnedError)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe '.locked?' do
|
|
114
|
+
before do
|
|
115
|
+
allow(@object).to receive(:reload!)
|
|
116
|
+
allow(@object).to receive(:exists?)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'calls reload! on the lock file' do
|
|
120
|
+
expect(@object).to receive(:reload!).once
|
|
121
|
+
|
|
122
|
+
@mutex.locked?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'calls exists? on the lock file' do
|
|
126
|
+
expect(@object).to receive(:exists?).once
|
|
127
|
+
|
|
128
|
+
@mutex.locked?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'returns true if exists is true' do
|
|
132
|
+
expect(@object).to receive(:exists?).once.and_return(true)
|
|
133
|
+
|
|
134
|
+
expect(@mutex.locked?).to be(true)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'returns false if exists is false' do
|
|
138
|
+
expect(@object).to receive(:exists?).once.and_return(false)
|
|
139
|
+
|
|
140
|
+
expect(@mutex.locked?).to be(false)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe '.owned?' do
|
|
145
|
+
it 'returns false if mutex is not locked' do
|
|
146
|
+
expect(@mutex).to receive(:locked?).once.and_return(false)
|
|
147
|
+
|
|
148
|
+
expect(@mutex.owned?).to be(false)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'returns false if mutex is locked but object size != uuid size' do
|
|
152
|
+
expect(@mutex).to receive(:locked?).once.and_return(true)
|
|
153
|
+
expect(@object).to receive(:size).once.and_return(@uuid.size + 10)
|
|
154
|
+
|
|
155
|
+
expect(@mutex.owned?).to be(false)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it 'returns false if mutex is locked and object size == uuid size but object content != uuid' do
|
|
159
|
+
expect(@mutex).to receive(:locked?).once.and_return(true)
|
|
160
|
+
expect(@object).to receive(:size).once.and_return(@uuid.size)
|
|
161
|
+
|
|
162
|
+
download = StringIO.new('blah')
|
|
163
|
+
expect(@object).to receive(:download).once.and_return(download)
|
|
164
|
+
|
|
165
|
+
expect(@mutex.owned?).to be(false)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it 'returns true if mutex is locked and object contains uuid' do
|
|
169
|
+
expect(@mutex).to receive(:locked?).once.and_return(true)
|
|
170
|
+
expect(@object).to receive(:size).once.and_return(@uuid.size)
|
|
171
|
+
|
|
172
|
+
download = StringIO.new(@uuid)
|
|
173
|
+
expect(@object).to receive(:download).once.and_return(download)
|
|
174
|
+
|
|
175
|
+
expect(@mutex.owned?).to be(true)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
describe '.synchronize' do
|
|
180
|
+
it 'locks, yields and unlock the mutex' do
|
|
181
|
+
expect(@mutex).to receive(:owned?).once.and_return(false)
|
|
182
|
+
expect(@mutex).to receive(:lock).once.and_return(true)
|
|
183
|
+
expect(@mutex).to receive(:unlock).once
|
|
184
|
+
|
|
185
|
+
has_yielded = false
|
|
186
|
+
@mutex.synchronize do
|
|
187
|
+
has_yielded = true
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
expect(has_yielded).to be(true)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'raises an error if the lock is already owned' do
|
|
194
|
+
expect(@mutex).to receive(:owned?).once.and_return(true)
|
|
195
|
+
expect(@mutex).not_to receive(:lock)
|
|
196
|
+
expect(@mutex).not_to receive(:unlock)
|
|
197
|
+
|
|
198
|
+
has_yielded = false
|
|
199
|
+
|
|
200
|
+
expect do
|
|
201
|
+
@mutex.synchronize do
|
|
202
|
+
has_yielded = true
|
|
203
|
+
end
|
|
204
|
+
end.to raise_error(GCSLock::LockAlreadyOwnedError)
|
|
205
|
+
|
|
206
|
+
expect(has_yielded).to be(false)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
describe '.try_lock' do
|
|
211
|
+
before do
|
|
212
|
+
@service = instance_double(Google::Cloud::Storage::Service)
|
|
213
|
+
allow(@gcs).to receive(:service).and_return(@service)
|
|
214
|
+
|
|
215
|
+
@servicev1 = instance_double(Google::Apis::StorageV1::StorageService)
|
|
216
|
+
allow(@service).to receive(:service).and_return(@servicev1)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it 'returns true if lock obtained' do
|
|
220
|
+
expect(@servicev1).to receive(:insert_object).with(
|
|
221
|
+
@bucket_name,
|
|
222
|
+
name: @object_name,
|
|
223
|
+
if_generation_match: 0,
|
|
224
|
+
upload_source: instance_of(StringIO),
|
|
225
|
+
).once
|
|
226
|
+
|
|
227
|
+
expect(@mutex.try_lock).to be(true)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it 'returns false if lock already taken (precondition failed)' do
|
|
231
|
+
client_error = Google::Apis::ClientError.new('conditionNotMet: Precondition failed', status_code: 412)
|
|
232
|
+
|
|
233
|
+
expect(@servicev1).to receive(:insert_object).with(
|
|
234
|
+
@bucket_name,
|
|
235
|
+
name: @object_name,
|
|
236
|
+
if_generation_match: 0,
|
|
237
|
+
upload_source: instance_of(StringIO),
|
|
238
|
+
).once.and_raise(client_error)
|
|
239
|
+
|
|
240
|
+
expect(@mutex.try_lock).to be(false)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'raises in case of precondition failed for other reason than conditionNotMet' do
|
|
244
|
+
client_error = Google::Apis::ClientError.new('blah: Precondition failed', status_code: 412)
|
|
245
|
+
|
|
246
|
+
expect(@servicev1).to receive(:insert_object).with(
|
|
247
|
+
@bucket_name,
|
|
248
|
+
name: @object_name,
|
|
249
|
+
if_generation_match: 0,
|
|
250
|
+
upload_source: instance_of(StringIO),
|
|
251
|
+
).once.and_raise(client_error)
|
|
252
|
+
|
|
253
|
+
expect do
|
|
254
|
+
@mutex.try_lock
|
|
255
|
+
end.to raise_error(client_error)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'raises in case of other error than precondition failed' do
|
|
259
|
+
client_error = Google::Apis::ClientError.new('blah', status_code: 400)
|
|
260
|
+
|
|
261
|
+
expect(@servicev1).to receive(:insert_object).with(
|
|
262
|
+
@bucket_name,
|
|
263
|
+
name: @object_name,
|
|
264
|
+
if_generation_match: 0,
|
|
265
|
+
upload_source: instance_of(StringIO),
|
|
266
|
+
).once.and_raise(client_error)
|
|
267
|
+
|
|
268
|
+
expect do
|
|
269
|
+
@mutex.try_lock
|
|
270
|
+
end.to raise_error(client_error)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
describe '.unlock' do
|
|
275
|
+
it 'calls delete on the object if lock is owned' do
|
|
276
|
+
expect(@mutex).to receive(:owned?).once.and_return(true)
|
|
277
|
+
expect(@object).to receive(:delete).once
|
|
278
|
+
|
|
279
|
+
@mutex.unlock
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it 'raises an error if the lock is not owned' do
|
|
283
|
+
expect(@mutex).to receive(:owned?).once.and_return(false)
|
|
284
|
+
expect(@object).not_to receive(:delete)
|
|
285
|
+
|
|
286
|
+
expect do
|
|
287
|
+
@mutex.unlock
|
|
288
|
+
end.to raise_error(GCSLock::LockNotOwnedError)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
describe '.unlock!' do
|
|
293
|
+
it 'calls delete on the object' do
|
|
294
|
+
expect(@object).to receive(:delete).once
|
|
295
|
+
|
|
296
|
+
@mutex.unlock!
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it 'raises an error if the object is not found' do
|
|
300
|
+
expect(@object).to receive(:delete).once.and_raise(Google::Cloud::NotFoundError.new('blah'))
|
|
301
|
+
|
|
302
|
+
expect do
|
|
303
|
+
@mutex.unlock!
|
|
304
|
+
end.to raise_error(GCSLock::LockNotFoundError)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gcslock
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Raphaël Beamonte
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2020-05-20 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: google-api-client
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: google-cloud-storage
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 1.26.1
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 1.26.1
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: codecov
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
description: Allows to use a Google Cloud Storage bucket as a distributed locking
|
|
70
|
+
system
|
|
71
|
+
email:
|
|
72
|
+
- raphael.beamonte@gmail.com
|
|
73
|
+
executables: []
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- ".gitignore"
|
|
78
|
+
- Gemfile
|
|
79
|
+
- README.md
|
|
80
|
+
- bin/rspec
|
|
81
|
+
- gcslock.gemspec
|
|
82
|
+
- lib/gcslock/errors.rb
|
|
83
|
+
- lib/gcslock/mutex.rb
|
|
84
|
+
- lib/gcslock/version.rb
|
|
85
|
+
- spec/gcslock/mutex_spec.rb
|
|
86
|
+
- spec/spec_helper.rb
|
|
87
|
+
homepage: https://github.com/XaF/gcslock-ruby
|
|
88
|
+
licenses:
|
|
89
|
+
- MIT
|
|
90
|
+
metadata:
|
|
91
|
+
allowed_push_host: https://rubygems.org
|
|
92
|
+
post_install_message:
|
|
93
|
+
rdoc_options: []
|
|
94
|
+
require_paths:
|
|
95
|
+
- lib
|
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '2.0'
|
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
requirements: []
|
|
107
|
+
rubygems_version: 3.0.3
|
|
108
|
+
signing_key:
|
|
109
|
+
specification_version: 4
|
|
110
|
+
summary: Google Cloud Storage distributed locking
|
|
111
|
+
test_files: []
|