Skip to content

Commit 8f3137b

Browse files
committed
wrap calls to aws configure set in a file lock to prevent multiprocess file trashing
1 parent 88ecc06 commit 8f3137b

2 files changed

Lines changed: 83 additions & 1 deletion

File tree

lib/aws/google/cached_credentials.rb

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def refresh_if_near_expiration
2828
@mutex.synchronize do
2929
if near_expiration?(SYNC_EXPIRATION_LENGTH)
3030
refresh
31-
write_credentials
31+
with_write_lock { write_credentials }
3232
end
3333
end
3434
end
@@ -51,6 +51,39 @@ def write_credentials
5151
system("aws configure set #{key} #{value} --profile #{@session_profile}")
5252
end
5353
end
54+
55+
private
56+
57+
def now_monotonic
58+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
59+
end
60+
61+
# Lock file ~/.aws/aws-google.lock to prevent multiple simultaneous calls from
62+
# different processes from corrupting ~/.aws/config and ~/.aws/credentials
63+
def with_write_lock
64+
lock_path = aws_google_lock_path
65+
start_time = now_monotonic
66+
67+
# Open aws-google.lock for read/write
68+
File.open(lock_path, File::RDWR | File::CREAT) do |lock|
69+
# Keep trying to exclusively file lock it every 0.1s until we succeed
70+
until lock.flock(File::LOCK_EX | File::LOCK_NB)
71+
raise "Timed out after 60s waiting for: #{lock_path}" if now_monotonic - start_time >= 60
72+
sleep 0.1
73+
end
74+
75+
yield
76+
end
77+
end
78+
79+
def aws_google_lock_path
80+
dot_aws_dir = File.dirname(
81+
Aws.shared_config.config_path ||
82+
Aws.shared_config.credentials_path ||
83+
File.expand_path('~/.aws/config')
84+
)
85+
File.join(dot_aws_dir, 'aws-google.lock')
86+
end
5487
end
5588
end
5689
end

test/aws/google_test.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
ENV.stubs(:[]).returns(nil)
2323
end
2424

25+
after do
26+
lock_path = File.expand_path(File.join(__dir__, 'fixtures', 'aws-google.lock'))
27+
File.delete(lock_path) if File.exist?(lock_path)
28+
end
29+
2530
describe 'not configured' do
2631
it 'does nothing' do
2732
Aws::Google.expects(:new).never
@@ -136,6 +141,50 @@
136141
_(c.credentials.session_token).must_equal credentials[:session_token]
137142
end
138143

144+
describe 'write lock' do
145+
let :provider do
146+
Aws::Google.allocate.tap do |google|
147+
google.instance_variable_set(:@credentials, Aws::Credentials.new('x', 'y', 'z'))
148+
google.instance_variable_set(:@expiration, 123)
149+
google.instance_variable_set(:@session_profile, 'cdo_session')
150+
end
151+
end
152+
153+
let(:lock_path) { File.expand_path(File.join(__dir__, 'fixtures', 'aws-google.lock')) }
154+
let(:lock) { mock }
155+
156+
it 'writes credentials while holding the lock' do
157+
File.expects(:open).with(lock_path, File::RDWR | File::CREAT).yields(lock)
158+
lock.expects(:flock).with(File::LOCK_EX | File::LOCK_NB).returns(true)
159+
system.times(5)
160+
161+
provider.send(:with_write_lock) { provider.send(:write_credentials) }
162+
end
163+
164+
it 'retries until the lock is available' do
165+
File.expects(:open).with(lock_path, File::RDWR | File::CREAT).yields(lock)
166+
lock.expects(:flock).with(File::LOCK_EX | File::LOCK_NB).times(3).returns(false, false, true)
167+
Process.stubs(:clock_gettime).with(Process::CLOCK_MONOTONIC).returns(0, 10, 20)
168+
provider.expects(:sleep).with(0.1).twice
169+
system.times(5)
170+
171+
provider.send(:with_write_lock) { provider.send(:write_credentials) }
172+
end
173+
174+
it 'raises when the lock times out' do
175+
File.expects(:open).with(lock_path, File::RDWR | File::CREAT).yields(lock)
176+
lock.expects(:flock).with(File::LOCK_EX | File::LOCK_NB).returns(false)
177+
Process.stubs(:clock_gettime).with(Process::CLOCK_MONOTONIC).returns(0, 60)
178+
provider.expects(:system).never
179+
180+
err = assert_raises(RuntimeError) do
181+
provider.send(:with_write_lock) { provider.send(:write_credentials) }
182+
end
183+
184+
_(err.message).must_equal "Timed out after 60s waiting for: #{lock_path}"
185+
end
186+
end
187+
139188
describe 'valid Google auth, no AWS permissions' do
140189
before do
141190
config[:client].stub_responses(

0 commit comments

Comments
 (0)