Skip to content
This repository was archived by the owner on Jun 2, 2021. It is now read-only.

Commit 754c76b

Browse files
author
Derik Evangelista
authored
v3(route-binding): delete when another operation is in progress (cloudfoundry#1929)
[#174748134](https://www.pivotaltracker.com/story/show/174748134)
1 parent 803bc1f commit 754c76b

7 files changed

Lines changed: 153 additions & 21 deletions

File tree

app/actions/service_route_binding_delete.rb

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module V3
33
class ServiceRouteBindingDelete
44
class UnprocessableDelete < StandardError; end
55

6+
class ConcurrencyError < StandardError; end
7+
68
RequiresAsync = Class.new.freeze
79

810
DeleteStatus = Struct.new(:finished, :operation).freeze
@@ -18,9 +20,8 @@ def initialize(service_event_repository)
1820
end
1921

2022
def delete(binding, async_allowed:)
21-
return RequiresAsync.new unless async_allowed || binding.service_instance.user_provided_instance?
22-
2323
operation_in_progress! if binding.service_instance.operation_in_progress?
24+
return RequiresAsync.new unless async_allowed || binding.service_instance.user_provided_instance?
2425

2526
result = send_unbind_to_broker(binding)
2627
if result[:finished]
@@ -31,7 +32,9 @@ def delete(binding, async_allowed:)
3132

3233
return result
3334
rescue => e
34-
update_last_operation(binding, state: 'failed', description: e.message)
35+
unless e.is_a? ConcurrencyError
36+
update_last_operation(binding, state: 'failed', description: e.message)
37+
end
3538

3639
raise e
3740
end
@@ -65,6 +68,10 @@ def send_unbind_to_broker(binding)
6568
client = VCAP::Services::ServiceClientProvider.provide(instance: binding.service_instance)
6669
details = client.unbind(binding, nil, true)
6770
details[:async] ? DeleteStarted.call(details[:operation]) : DeleteComplete
71+
rescue VCAP::Services::ServiceBrokers::V2::Errors::ConcurrencyError
72+
raise ConcurrencyError.new(
73+
'The service broker rejected the request due to an operation being in progress for the service route binding'
74+
)
6875
rescue => err
6976
raise UnprocessableDelete.new("Service broker failed to delete service binding for instance #{binding.service_instance.name}: #{err.message}")
7077
end
@@ -88,7 +95,7 @@ def update_last_operation(binding, description: nil, state: 'in progress', opera
8895
type: 'delete',
8996
state: state,
9097
description: description,
91-
broker_provided_operation: operation
98+
broker_provided_operation: operation || binding.last_operation.broker_provided_operation
9299
})
93100
end
94101

app/controllers/v3/service_route_bindings_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ def destroy
7474
else
7575
head :no_content
7676
end
77+
rescue V3::ServiceRouteBindingDelete::UnprocessableDelete => e
78+
unprocessable!(e.message)
7779
end
7880

7981
def parameters

app/jobs/v3/delete_route_binding_job.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def perform
6161
self.polling_interval_seconds = polling_status[:retry_after]
6262
end
6363
rescue => e
64-
if route_binding.reload.last_operation.state != 'failed'
64+
if route_binding.reload.last_operation.state != 'failed' && !e.is_a?(V3::ServiceRouteBindingDelete::ConcurrencyError)
6565
save_failure(e.message)
6666
end
6767
raise CloudController::Errors::ApiError.new_from_details('UnableToPerform', 'unbind', e.message)

lib/services/service_brokers/v2/client.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,12 @@ def unbind(service_binding, user_guid=nil, accepts_incomplete=false)
162162
async: async_response?(response),
163163
operation: parsed_response['operation']
164164
}
165-
rescue VCAP::Services::ServiceBrokers::V2::Errors::ConcurrencyError
166-
raise CloudController::Errors::ApiError.new_from_details('AsyncServiceBindingOperationInProgress', service_binding.app.name, service_binding.service_instance.name)
165+
rescue VCAP::Services::ServiceBrokers::V2::Errors::ConcurrencyError => e
166+
if service_binding.is_a? VCAP::CloudController::ServiceBinding
167+
raise CloudController::Errors::ApiError.new_from_details('AsyncServiceBindingOperationInProgress', service_binding.app.name, service_binding.service_instance.name)
168+
end
169+
170+
raise e
167171
end
168172

169173
def update(instance, plan, accepts_incomplete: false, arbitrary_parameters: nil, previous_values: {}, maintenance_info: nil, name: instance.name)

spec/request/service_route_bindings_spec.rb

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,18 @@
935935
let(:expected_codes_and_responses) { responses_for_space_restricted_async_delete_endpoint }
936936
let(:db_check) { lambda {} }
937937
let(:job) { VCAP::CloudController::PollableJobModel.last }
938+
let(:broker_base_url) { service_instance.service_broker.broker_url }
939+
let(:broker_unbind_url) { "#{broker_base_url}/v2/service_instances/#{service_instance.guid}/service_bindings/#{binding.guid}" }
940+
let(:route_service_url) { 'https://route_service_url.com' }
941+
let(:broker_unbind_status_code) { 200 }
942+
let(:broker_response) { {} }
943+
let(:query) do
944+
{
945+
service_id: service_instance.service_plan.service.unique_id,
946+
plan_id: service_instance.service_plan.unique_id,
947+
accepts_incomplete: true,
948+
}
949+
end
938950

939951
it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS
940952

@@ -955,19 +967,6 @@
955967
end
956968

957969
describe 'the pollable job' do
958-
let(:broker_base_url) { service_instance.service_broker.broker_url }
959-
let(:broker_unbind_url) { "#{broker_base_url}/v2/service_instances/#{service_instance.guid}/service_bindings/#{binding.guid}" }
960-
let(:route_service_url) { 'https://route_service_url.com' }
961-
let(:broker_unbind_status_code) { 200 }
962-
let(:broker_response) { {} }
963-
let(:query) do
964-
{
965-
service_id: service_instance.service_plan.service.unique_id,
966-
plan_id: service_instance.service_plan.unique_id,
967-
accepts_incomplete: true,
968-
}
969-
end
970-
971970
before do
972971
api_call.call(space_dev_headers)
973972
expect(last_response).to have_status_code(202)
@@ -1113,6 +1112,75 @@
11131112
end
11141113
end
11151114
end
1115+
1116+
context 'when the service instance has an operation in progress' do
1117+
it 'responds with 422' do
1118+
service_instance.save_with_new_operation({}, { type: 'guacamole', state: 'in progress' })
1119+
1120+
api_call.call admin_headers
1121+
expect(last_response).to have_status_code(422)
1122+
expect(parsed_response['errors']).to include(include({
1123+
'detail' => include('There is an operation in progress for the service instance'),
1124+
'title' => 'CF-UnprocessableEntity',
1125+
'code' => 10008,
1126+
}))
1127+
end
1128+
end
1129+
1130+
context 'when the route binding is still creating' do
1131+
before do
1132+
binding.save_with_new_operation(
1133+
{},
1134+
{ type: 'create', state: 'in progress', broker_provided_operation: 'very important info' }
1135+
)
1136+
end
1137+
1138+
context 'but the broker accepts the delete request' do
1139+
before do
1140+
@delete_stub = stub_request(:delete, broker_unbind_url).
1141+
with(query: query).
1142+
to_return(status: 202, body: '{"operation": "very important delete info"}', headers: {})
1143+
1144+
@last_op_stub = stub_request(:get, "#{broker_unbind_url}/last_operation").
1145+
with(query: hash_including({
1146+
operation: 'very important delete info'
1147+
})).
1148+
to_return(status: 200, body: '{"state": "in progress"}', headers: {})
1149+
end
1150+
1151+
it 'starts the route binding deletion' do
1152+
api_call.call(admin_headers)
1153+
expect(last_response).to have_status_code(202)
1154+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1155+
1156+
expect(@delete_stub).to have_been_requested.once
1157+
expect(@last_op_stub).to have_been_requested
1158+
1159+
binding.reload
1160+
expect(binding.last_operation.type).to eq('delete')
1161+
expect(binding.last_operation.state).to eq('in progress')
1162+
expect(binding.last_operation.broker_provided_operation).to eq('very important delete info')
1163+
end
1164+
end
1165+
1166+
context 'and the broker rejects the delete request' do
1167+
before do
1168+
stub_request(:delete, broker_unbind_url).
1169+
with(query: query).
1170+
to_return(status: 422, body: '{"error": "ConcurrencyError"}', headers: {})
1171+
1172+
api_call.call(admin_headers)
1173+
execute_all_jobs(expected_successes: 0, expected_failures: 1)
1174+
binding.reload
1175+
end
1176+
1177+
it 'leaves the route binding in its current state' do
1178+
expect(binding.last_operation.type).to eq('create')
1179+
expect(binding.last_operation.state).to eq('in progress')
1180+
expect(binding.last_operation.broker_provided_operation).to eq('very important info')
1181+
end
1182+
end
1183+
end
11161184
end
11171185
end
11181186

spec/unit/actions/service_route_binding_delete_spec.rb

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ module V3
151151
end
152152
end
153153

154-
context 'broker returns an error' do
154+
context 'broker returns a generic error' do
155155
let(:broker_client) do
156156
dbl = instance_double(VCAP::Services::ServiceBrokers::V2::Client)
157157
allow(dbl).to receive(:unbind).and_raise(StandardError, 'awful thing')
@@ -171,6 +171,38 @@ module V3
171171
expect(route_binding.last_operation.description).to eq("Service broker failed to delete service binding for instance #{service_instance.name}: awful thing")
172172
end
173173
end
174+
175+
context 'broker returns a concurrency error' do
176+
let(:broker_client) do
177+
dbl = instance_double(VCAP::Services::ServiceBrokers::V2::Client)
178+
allow(dbl).to receive(:unbind).and_raise(
179+
VCAP::Services::ServiceBrokers::V2::Errors::ConcurrencyError.new(
180+
'foo',
181+
:delete,
182+
double(code: '500', reason: '', body: '')
183+
)
184+
)
185+
dbl
186+
end
187+
188+
before do
189+
route_binding.save_with_new_operation({}, { type: 'create', state: 'in progress', description: 'doing stuff' })
190+
end
191+
192+
it 'fails with an appropriate error and does not alter the binding' do
193+
expect {
194+
delete_binding
195+
}.to raise_error(
196+
described_class::ConcurrencyError,
197+
'The service broker rejected the request due to an operation being in progress for the service route binding',
198+
)
199+
200+
route_binding.reload
201+
expect(route_binding.last_operation.type).to eq('create')
202+
expect(route_binding.last_operation.state).to eq('in progress')
203+
expect(route_binding.last_operation.description).to eq('doing stuff')
204+
end
205+
end
174206
end
175207
end
176208

spec/unit/lib/services/service_brokers/v2/client_spec.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,6 +1541,25 @@ module VCAP::Services::ServiceBrokers::V2
15411541
end
15421542
end
15431543
end
1544+
1545+
context 'ConcurrencyError error' do
1546+
let(:code) { 422 }
1547+
let(:response_body) { '{"error":"ConcurrencyError"}' }
1548+
1549+
context 'for app bindings' do
1550+
it 'propagates the error as an API error' do
1551+
expect { client.unbind(binding) }.to raise_error(CloudController::Errors::ApiError, /An operation for the service binding between app/)
1552+
end
1553+
end
1554+
1555+
context 'for a route binding' do
1556+
let(:binding) { VCAP::CloudController::RouteBinding.make }
1557+
1558+
it 'propagates the error as a ConcurrencyError' do
1559+
expect { client.unbind(binding) }.to raise_error(Errors::ConcurrencyError)
1560+
end
1561+
end
1562+
end
15441563
end
15451564

15461565
describe '#deprovision' do

0 commit comments

Comments
 (0)