Skip to content

Commit 2a1f574

Browse files
committed
Lock service instance to prevent concurrent binding creation
1 parent 86adfc7 commit 2a1f574

File tree

2 files changed

+27
-6
lines changed

2 files changed

+27
-6
lines changed

app/actions/service_credential_binding_app_create.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ def precursor(service_instance, message:, app: nil, volume_mount_services_enable
3030

3131
ServiceBinding.new.tap do |new_binding|
3232
ServiceBinding.db.transaction do
33-
validate_app_guid_name_uniqueness!(app.guid, message.name, service_instance.guid) # if max_bindings_per_app_service_instance == 1
33+
# Lock the service instance to prevent multiple bindings being created when not allowed
34+
service_instance.lock! if max_bindings_per_app_service_instance == 1
35+
36+
validate_app_guid_name_uniqueness!(app.guid, message.name, service_instance.guid)
3437

3538
num_valid_bindings = 0
3639
existing_bindings = ServiceBinding.where(service_instance:, app:)

spec/unit/actions/service_credential_binding_app_create_spec.rb

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,6 @@ module V3
197197
action.precursor(service_instance, app:, message:)
198198
end.not_to raise_error
199199

200-
# Mock the validation for the second request to simulate the race condition and trigger a
201-
# unique constraint violation on service_instance_guid + app_guid
202-
allow_any_instance_of(ServiceBinding).to receive(:validate).and_return(true)
203-
allow(ServiceBinding).to receive(:first).with(service_instance:, app:).and_return(nil)
204-
205200
# Second request, should fail with correct error
206201
expect do
207202
action.precursor(service_instance, app: app, message: message2)
@@ -233,6 +228,29 @@ module V3
233228
)
234229
end
235230

231+
context 'concurrent credential binding creation' do
232+
let(:name) { nil }
233+
234+
# TODO: Once the unique constraints to allow multiple bindings are removed, this needs to be set to 1
235+
# before { TestConfig.override(max_service_credential_bindings_per_app_service_instance: 1) }
236+
237+
def attempt_precursor
238+
action.precursor(service_instance, app:, message:)
239+
:ok
240+
rescue StandardError => e
241+
e
242+
end
243+
244+
it 'allows only one binding when two creates run in parallel' do
245+
results = [Thread.new { attempt_precursor }, Thread.new { attempt_precursor }].map(&:value)
246+
247+
expect(ServiceBinding.where(app:, service_instance:).count).to eq(1)
248+
expect(results.count(:ok)).to eq(1)
249+
expect(results.count { |r| r.is_a?(VCAP::CloudController::V3::ServiceBindingCreate::UnprocessableCreate) }).to eq(1)
250+
expect(results.grep(Exception).map(&:message)).to include('The app is already bound to the service instance')
251+
end
252+
end
253+
236254
context 'when multiple bindings are allowed' do
237255
before do
238256
# TODO: Remove skip when the service bindings unique constraints are removed

0 commit comments

Comments
 (0)