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

Commit 258f447

Browse files
authored
v3(services): async delete route binding (cloudfoundry#1881)
* v3(services): async delete route binding [#174383882](https://www.pivotaltracker.com/story/show/174383882) * Use named subject to improve test clarity [#173835477](https://www.pivotaltracker.com/story/show/173835477)
1 parent 32f1466 commit 258f447

7 files changed

Lines changed: 574 additions & 79 deletions

File tree

app/actions/service_route_binding_delete.rb

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,71 @@ class UnprocessableDelete < StandardError; end
55

66
RequiresAsync = Class.new.freeze
77
DeleteComplete = Class.new.freeze
8-
DeleteInProgress = Struct.new(:operation).freeze
8+
DeleteStarted = Struct.new(:operation).freeze
9+
DeleteInProgress = Struct.new(:retry_after).freeze
910

1011
def initialize(service_event_repository)
1112
@service_event_repository = service_event_repository
1213
end
1314

1415
def delete(binding, async_allowed:)
15-
return RequiresAsync unless async_allowed || binding.service_instance.user_provided_instance?
16+
return RequiresAsync.new unless async_allowed || binding.service_instance.user_provided_instance?
1617

1718
operation_in_progress! if binding.service_instance.operation_in_progress?
1819

1920
result = send_unbind_to_broker(binding)
20-
unsupported! unless result == DeleteComplete
21+
case result
22+
when DeleteStarted
23+
update_last_operation(binding, operation: result[:operation])
24+
when DeleteComplete
25+
perform_delete_actions(binding)
26+
end
2127

22-
record_audit_event(binding)
23-
binding.destroy
24-
binding.notify_diego
25-
26-
DeleteComplete
28+
result
2729
rescue => e
28-
binding.save_with_new_operation({}, {
29-
type: 'delete',
30-
state: 'failed',
31-
description: e.message,
32-
})
30+
update_last_operation(binding, state: 'failed', description: e.message)
3331

3432
raise e
3533
end
3634

35+
def poll(binding)
36+
client = VCAP::Services::ServiceClientProvider.provide(instance: binding.service_instance)
37+
details = client.fetch_service_binding_last_operation(binding)
38+
case details[:last_operation][:state]
39+
when 'in progress'
40+
update_last_operation(binding, description: details[:last_operation][:description])
41+
DeleteInProgress.new(details[:retry_after])
42+
when 'succeeded'
43+
perform_delete_actions(binding)
44+
DeleteComplete.new
45+
when 'failed'
46+
update_last_operation(binding, state: 'failed', description: details[:last_operation][:description])
47+
DeleteComplete.new
48+
end
49+
rescue => e
50+
update_last_operation(binding, description: e.message)
51+
52+
DeleteInProgress.new(nil)
53+
end
54+
3755
private
3856

3957
attr_reader :service_event_repository
4058

4159
def send_unbind_to_broker(binding)
4260
client = VCAP::Services::ServiceClientProvider.provide(instance: binding.service_instance)
43-
details = client.unbind(binding, accepts_incomplete: false)
44-
details[:async] ? DeleteInProgress.new(details[:operation]) : DeleteComplete
61+
details = client.unbind(binding, nil, true)
62+
details[:async] ? DeleteStarted.new(details[:operation]) : DeleteComplete.new
4563
rescue => err
4664
raise UnprocessableDelete.new("Service broker failed to delete service binding for instance #{binding.service_instance.name}: #{err.message}")
4765
end
4866

67+
def perform_delete_actions(binding)
68+
record_audit_event(binding)
69+
binding.destroy
70+
binding.notify_diego
71+
end
72+
4973
def record_audit_event(binding)
5074
service_event_repository.record_service_instance_event(
5175
:unbind_route,
@@ -54,12 +78,17 @@ def record_audit_event(binding)
5478
)
5579
end
5680

57-
def operation_in_progress!
58-
raise UnprocessableDelete.new('There is an operation in progress for the service instance')
81+
def update_last_operation(binding, description: nil, state: 'in progress', operation: nil)
82+
binding.save_with_new_operation({}, {
83+
type: 'delete',
84+
state: state,
85+
description: description,
86+
broker_provided_operation: operation
87+
})
5988
end
6089

61-
def unsupported!
62-
raise UnprocessableDelete.new('async unbind not supported yet')
90+
def operation_in_progress!
91+
raise UnprocessableDelete.new('There is an operation in progress for the service instance')
6392
end
6493
end
6594
end

app/controllers/v3/service_route_bindings_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def destroy
6868
action = V3::ServiceRouteBindingDelete.new(service_event_repository)
6969
result = action.delete(route_binding, async_allowed: false)
7070

71-
if result == V3::ServiceRouteBindingDelete::RequiresAsync
71+
if result.is_a? V3::ServiceRouteBindingDelete::RequiresAsync
7272
pollable_job_guid = enqueue_unbind_job(route_binding.guid)
7373
head :accepted, 'Location' => url_builder.build_url(path: "/v3/jobs/#{pollable_job_guid}")
7474
else

app/jobs/v3/delete_route_binding_job.rb

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
module VCAP::CloudController
66
module V3
7-
class DeleteRouteBindingJob < VCAP::CloudController::Jobs::CCJob
7+
class DeleteRouteBindingJob < Jobs::ReoccurringJob
88
def initialize(binding_guid, user_audit_info:)
99
super()
10+
@first_time = true
1011
@binding_guid = binding_guid
1112
@user_audit_info = user_audit_info
1213
end
@@ -41,7 +42,23 @@ def perform
4142

4243
service_event_repository = VCAP::CloudController::Repositories::ServiceEventRepository::WithUserActor.new(@user_audit_info)
4344
action = V3::ServiceRouteBindingDelete.new(service_event_repository)
44-
action.delete(binding, async_allowed: true)
45+
compute_maximum_duration
46+
47+
if @first_time
48+
@first_time = false
49+
delete_result = action.delete(binding, async_allowed: true)
50+
return finish if delete_result.is_a? V3::ServiceRouteBindingDelete::DeleteComplete
51+
end
52+
53+
polling_status = action.poll(binding)
54+
case polling_status
55+
when V3::ServiceRouteBindingDelete::DeleteComplete
56+
finish
57+
when V3::ServiceRouteBindingDelete::DeleteInProgress
58+
unless polling_status.retry_after.nil?
59+
self.polling_interval_seconds = polling_status.retry_after.to_i
60+
end
61+
end
4562
rescue => e
4663
raise CloudController::Errors::ApiError.new_from_details('UnableToPerform', 'unbind', e.message)
4764
end
@@ -52,6 +69,11 @@ def route_binding
5269
RouteBinding.first(guid: resource_guid)
5370
end
5471

72+
def compute_maximum_duration
73+
max_poll_duration_on_plan = route_binding.service_instance.service_plan.try(:maximum_polling_duration)
74+
self.maximum_duration_seconds = max_poll_duration_on_plan
75+
end
76+
5577
def gone!
5678
raise CloudController::Errors::ApiError.new_from_details('ResourceNotFound', "The binding could not be found: #{resource_guid}")
5779
end

spec/request/service_route_bindings_spec.rb

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,22 @@
408408
end
409409
end
410410

411-
it_behaves_like 'create binding last operation response handling'
411+
it_behaves_like 'binding last operation response handling', 'create'
412+
413+
context 'last operation response is 410 Gone' do
414+
let(:last_operation_status_code) { 410 }
415+
let(:last_operation_body) { {} }
416+
417+
it 'continues polling' do
418+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
419+
420+
binding.reload
421+
expect(binding.last_operation.type).to eq('create')
422+
expect(binding.last_operation.state).to eq('in progress')
423+
424+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
425+
end
426+
end
412427

413428
context 'binding not retrievable' do
414429
let(:offering) { VCAP::CloudController::Service.make(bindings_retrievable: false, requires: ['route_forwarding']) }
@@ -930,6 +945,7 @@
930945
{
931946
service_id: service_instance.service_plan.service.unique_id,
932947
plan_id: service_instance.service_plan.unique_id,
948+
accepts_incomplete: true,
933949
}
934950
end
935951

@@ -967,6 +983,95 @@
967983
end
968984
end
969985

986+
context 'when the unbind responds asynchronously' do
987+
let(:broker_status_code) { 202 }
988+
let(:operation) { Sham.guid }
989+
let(:broker_response) { { operation: operation } }
990+
let(:broker_binding_last_operation_url) { "#{broker_base_url}/v2/service_instances/#{service_instance.guid}/service_bindings/#{binding.guid}/last_operation" }
991+
let(:last_operation_status_code) { 200 }
992+
let(:description) { Sham.description }
993+
let(:state) { 'in progress' }
994+
let(:last_operation_body) do
995+
{
996+
description: description,
997+
state: state,
998+
}
999+
end
1000+
1001+
before do
1002+
stub_request(:get, broker_binding_last_operation_url).
1003+
with(query: hash_including({
1004+
operation: operation
1005+
})).
1006+
to_return(status: last_operation_status_code, body: last_operation_body.to_json, headers: {})
1007+
end
1008+
1009+
it 'polls the last operation endpoint' do
1010+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1011+
1012+
expect(
1013+
a_request(:get, broker_binding_last_operation_url).
1014+
with(query: {
1015+
operation: operation,
1016+
service_id: service_instance.service_plan.service.unique_id,
1017+
plan_id: service_instance.service_plan.unique_id,
1018+
})
1019+
).to have_been_made.once
1020+
end
1021+
1022+
it 'updates the binding and job' do
1023+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1024+
1025+
binding.reload
1026+
expect(binding.last_operation.type).to eq('delete')
1027+
expect(binding.last_operation.state).to eq(state)
1028+
expect(binding.last_operation.description).to eq(description)
1029+
1030+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
1031+
end
1032+
1033+
it 'enqueues the next fetch last operation job' do
1034+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1035+
expect(Delayed::Job.count).to eq(1)
1036+
end
1037+
1038+
context 'last operation response is 200 OK and indicates success' do
1039+
let(:state) { 'succeeded' }
1040+
let(:last_operation_status_code) { 200 }
1041+
1042+
it 'removes the binding' do
1043+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1044+
1045+
expect(VCAP::CloudController::RouteBinding.all).to be_empty
1046+
end
1047+
1048+
it 'completes the job' do
1049+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1050+
1051+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
1052+
end
1053+
end
1054+
1055+
context 'last operation response is 410 Gone' do
1056+
let(:last_operation_status_code) { 410 }
1057+
let(:last_operation_body) { {} }
1058+
1059+
it 'removes the binding' do
1060+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1061+
1062+
expect(VCAP::CloudController::RouteBinding.all).to be_empty
1063+
end
1064+
1065+
it 'completes the job' do
1066+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1067+
1068+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
1069+
end
1070+
end
1071+
1072+
it_behaves_like 'binding last operation response handling', 'delete'
1073+
end
1074+
9701075
context 'when the broker returns a failure' do
9711076
let(:broker_status_code) { 418 }
9721077
let(:broker_response) { 'nope' }

spec/support/shared_examples/request/last_operation_response_handling.rb

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
RSpec.shared_examples 'create binding last operation response handling' do
1+
RSpec.shared_examples 'binding last operation response handling' do |type|
22
context 'valid http codes' do
33
valid_responses = [
44
{ code: 400, body: { description: 'helpful message' }, expected_description: 'helpful message' },
55
{ code: 200, body: { state: 'failed', description: 'something went wrong' }, expected_description: 'something went wrong' }
66
]
7+
78
valid_responses.each do |response|
89
context "last operation response is #{response[:code]}" do
910
let(:state) { response[:state] }
@@ -13,7 +14,8 @@
1314
it 'updates the binding and job' do
1415
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1516

16-
expect(binding.last_operation.type).to eq('create')
17+
binding.reload
18+
expect(binding.last_operation.type).to eq(type)
1719
expect(binding.last_operation.state).to eq('failed')
1820
expect(binding.last_operation.description).to eq(response[:body][:description])
1921

@@ -24,17 +26,18 @@
2426
end
2527

2628
context 'invalid http codes' do
27-
[404, 410, 500].each do |code|
29+
[404, 500].each do |code|
2830
context "last operation response is #{code}" do
2931
let(:last_operation_status_code) { code }
3032
let(:last_operation_body) { 'something awful' }
3133

3234
it 'continues polling' do
3335
execute_all_jobs(expected_successes: 1, expected_failures: 0)
3436

35-
expect(binding.last_operation.type).to eq('create')
37+
binding.reload
38+
expect(binding.last_operation.type).to eq(type)
3639
expect(binding.last_operation.state).to eq('in progress')
37-
expect(binding.last_operation.description).to include("Status Code: #{code}") unless code == 410
40+
expect(binding.last_operation.description).to include("Status Code: #{code}")
3841

3942
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
4043
end
@@ -53,7 +56,8 @@
5356
it 'continues polling' do
5457
execute_all_jobs(expected_successes: 1, expected_failures: 0)
5558

56-
expect(binding.last_operation.type).to eq('create')
59+
binding.reload
60+
expect(binding.last_operation.type).to eq(type)
5761
expect(binding.last_operation.state).to eq('in progress')
5862

5963
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
@@ -72,7 +76,8 @@
7276
it 'continues polling' do
7377
execute_all_jobs(expected_successes: 1, expected_failures: 0)
7478

75-
expect(binding.last_operation.type).to eq('create')
79+
binding.reload
80+
expect(binding.last_operation.type).to eq(type)
7681
expect(binding.last_operation.state).to eq('in progress')
7782

7883
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)

0 commit comments

Comments
 (0)