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

Commit ab01bc9

Browse files
authored
V3 handle last operation on unbind route service 174748133 (cloudfoundry#1913)
* v3(servise): Handling of last operation responses on unbind request * v3(services): Set that Last operation to failed on unbind job timeout * v3(services): Make sure to set LO to failed on unbind job failures * v3(services): Delete job succeds when binding is not found. Updated the result from binding delete action and * v3(services) job completes when it should * Add missing test setup * Use if/else to imporve code readability
1 parent a0402f4 commit ab01bc9

6 files changed

Lines changed: 119 additions & 165 deletions

File tree

app/actions/service_route_binding_delete.rb

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ class ServiceRouteBindingDelete
44
class UnprocessableDelete < StandardError; end
55

66
RequiresAsync = Class.new.freeze
7-
DeleteComplete = Class.new.freeze
8-
DeleteStarted = Struct.new(:operation).freeze
9-
DeleteInProgress = Struct.new(:retry_after).freeze
7+
8+
DeleteStatus = Struct.new(:finished, :operation).freeze
9+
DeleteStarted = ->(operation) { DeleteStatus.new(false, operation) }
10+
DeleteComplete = DeleteStatus.new(true, nil).freeze
11+
12+
PollingStatus = Struct.new(:finished, :retry_after).freeze
13+
PollingFinished = PollingStatus.new(true, nil).freeze
14+
ContinuePolling = ->(retry_after) { PollingStatus.new(false, retry_after) }
1015

1116
def initialize(service_event_repository)
1217
@service_event_repository = service_event_repository
@@ -18,14 +23,13 @@ def delete(binding, async_allowed:)
1823
operation_in_progress! if binding.service_instance.operation_in_progress?
1924

2025
result = send_unbind_to_broker(binding)
21-
case result
22-
when DeleteStarted
23-
update_last_operation(binding, operation: result[:operation])
24-
when DeleteComplete
26+
if result[:finished]
2527
perform_delete_actions(binding)
28+
else
29+
update_last_operation(binding, operation: result[:operation])
2630
end
2731

28-
result
32+
return result
2933
rescue => e
3034
update_last_operation(binding, state: 'failed', description: e.message)
3135

@@ -34,22 +38,23 @@ def delete(binding, async_allowed:)
3438

3539
def poll(binding)
3640
client = VCAP::Services::ServiceClientProvider.provide(instance: binding.service_instance)
37-
details = client.fetch_service_binding_last_operation(binding)
41+
details = client.fetch_and_handle_service_binding_last_operation(binding)
3842
case details[:last_operation][:state]
3943
when 'in progress'
4044
update_last_operation(binding, description: details[:last_operation][:description])
41-
DeleteInProgress.new(details[:retry_after])
45+
return ContinuePolling.call(details[:retry_after])
4246
when 'succeeded'
4347
perform_delete_actions(binding)
44-
DeleteComplete.new
48+
return PollingFinished
4549
when 'failed'
4650
update_last_operation(binding, state: 'failed', description: details[:last_operation][:description])
47-
DeleteComplete.new
51+
raise LastOperationFailedState
4852
end
53+
rescue LastOperationFailedState => e
54+
raise e
4955
rescue => e
50-
update_last_operation(binding, description: e.message)
51-
52-
DeleteInProgress.new(nil)
56+
update_last_operation(binding, state: 'failed', description: e.message)
57+
raise e
5358
end
5459

5560
private
@@ -59,7 +64,7 @@ def poll(binding)
5964
def send_unbind_to_broker(binding)
6065
client = VCAP::Services::ServiceClientProvider.provide(instance: binding.service_instance)
6166
details = client.unbind(binding, nil, true)
62-
details[:async] ? DeleteStarted.new(details[:operation]) : DeleteComplete.new
67+
details[:async] ? DeleteStarted.call(details[:operation]) : DeleteComplete
6368
rescue => err
6469
raise UnprocessableDelete.new("Service broker failed to delete service binding for instance #{binding.service_instance.name}: #{err.message}")
6570
end

app/jobs/v3/delete_route_binding_job.rb

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def resource_type
3838

3939
def perform
4040
binding = route_binding
41-
gone! unless binding
41+
return finish if binding.nil?
4242

4343
service_event_repository = VCAP::CloudController::Repositories::ServiceEventRepository::WithUserActor.new(@user_audit_info)
4444
action = V3::ServiceRouteBindingDelete.new(service_event_repository)
@@ -47,24 +47,43 @@ def perform
4747
if @first_time
4848
@first_time = false
4949
delete_result = action.delete(binding, async_allowed: true)
50-
return finish if delete_result.is_a? V3::ServiceRouteBindingDelete::DeleteComplete
50+
if delete_result[:finished]
51+
return finish
52+
end
5153
end
5254

5355
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
56+
if polling_status[:finished]
57+
return finish
58+
end
59+
60+
if polling_status[:retry_after].present?
61+
self.polling_interval_seconds = polling_status[:retry_after]
6162
end
6263
rescue => e
64+
if route_binding.reload.last_operation.state != 'failed'
65+
save_failure(e.message)
66+
end
6367
raise CloudController::Errors::ApiError.new_from_details('UnableToPerform', 'unbind', e.message)
6468
end
6569

70+
def handle_timeout
71+
save_failure("Service Broker failed to #{operation} within the required time.")
72+
end
73+
6674
private
6775

76+
def save_failure(description)
77+
route_binding.save_with_attributes_and_new_operation(
78+
{},
79+
{
80+
type: operation_type,
81+
state: 'failed',
82+
description: description,
83+
}
84+
)
85+
end
86+
6887
def route_binding
6988
RouteBinding.first(guid: resource_guid)
7089
end
@@ -73,10 +92,6 @@ def compute_maximum_duration
7392
max_poll_duration_on_plan = route_binding.service_instance.service_plan.try(:maximum_polling_duration)
7493
self.maximum_duration_seconds = max_poll_duration_on_plan
7594
end
76-
77-
def gone!
78-
raise CloudController::Errors::ApiError.new_from_details('ResourceNotFound', "The binding could not be found: #{resource_guid}")
79-
end
8095
end
8196
end
8297
end

spec/request/service_route_bindings_spec.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -943,7 +943,7 @@
943943
let(:broker_base_url) { service_instance.service_broker.broker_url }
944944
let(:broker_unbind_url) { "#{broker_base_url}/v2/service_instances/#{service_instance.guid}/service_bindings/#{binding.guid}" }
945945
let(:route_service_url) { 'https://route_service_url.com' }
946-
let(:broker_status_code) { 200 }
946+
let(:broker_unbind_status_code) { 200 }
947947
let(:broker_response) { {} }
948948
let(:query) do
949949
{
@@ -959,7 +959,7 @@
959959

960960
stub_request(:delete, broker_unbind_url).
961961
with(query: query).
962-
to_return(status: broker_status_code, body: broker_response.to_json, headers: {})
962+
to_return(status: broker_unbind_status_code, body: broker_response.to_json, headers: {})
963963
end
964964

965965
it 'sends an unbind request with the right arguments to the service broker' do
@@ -988,7 +988,7 @@
988988
end
989989

990990
context 'when the unbind responds asynchronously' do
991-
let(:broker_status_code) { 202 }
991+
let(:broker_unbind_status_code) { 202 }
992992
let(:operation) { Sham.guid }
993993
let(:broker_response) { { operation: operation } }
994994
let(:broker_binding_last_operation_url) { "#{broker_base_url}/v2/service_instances/#{service_instance.guid}/service_bindings/#{binding.guid}/last_operation" }
@@ -1073,11 +1073,11 @@
10731073
end
10741074
end
10751075

1076-
it_behaves_like 'unbinding last operation response handling', 'delete'
1076+
it_behaves_like 'binding last operation response handling', 'delete'
10771077
end
10781078

10791079
context 'when the broker returns a failure' do
1080-
let(:broker_status_code) { 418 }
1080+
let(:broker_unbind_status_code) { 418 }
10811081
let(:broker_response) { 'nope' }
10821082

10831083
it 'does not remove the binding' do

spec/support/shared_examples/request/last_operation_response_handling.rb

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -85,92 +85,3 @@
8585
end
8686
end
8787
end
88-
89-
# TODO: Merge these two shared examples into one in https://www.pivotaltracker.com/story/show/174748133
90-
RSpec.shared_examples 'unbinding last operation response handling' do |operation_type|
91-
context 'failure http codes' do
92-
valid_responses = [
93-
{ code: 400, body: { description: 'helpful message' } },
94-
{ code: 200, body: { state: 'failed', description: 'something went wrong' } }
95-
]
96-
97-
valid_responses.each do |response|
98-
context "last operation response is #{response[:code]}" do
99-
let(:state) { response[:state] }
100-
let(:last_operation_status_code) { response[:code] }
101-
let(:last_operation_body) { response[:body] }
102-
103-
it 'updates the binding and job to failed' do
104-
execute_all_jobs(expected_successes: 1, expected_failures: 0)
105-
106-
binding.reload
107-
expect(binding.last_operation.type).to eq(operation_type)
108-
expect(binding.last_operation.state).to eq('failed')
109-
expect(binding.last_operation.description).to eq(response[:body][:description])
110-
111-
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
112-
end
113-
end
114-
end
115-
end
116-
117-
context 'keep polling http codes' do
118-
[404, 500].each do |code|
119-
context "last operation response is #{code}" do
120-
let(:last_operation_status_code) { code }
121-
let(:last_operation_body) { 'something awful' }
122-
123-
it 'continues polling' do
124-
execute_all_jobs(expected_successes: 1, expected_failures: 0)
125-
126-
binding.reload
127-
expect(binding.last_operation.type).to eq(operation_type)
128-
expect(binding.last_operation.state).to eq('in progress')
129-
expect(binding.last_operation.description).to include("Status Code: #{code}")
130-
131-
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
132-
end
133-
end
134-
end
135-
end
136-
137-
context 'timeout' do
138-
before do
139-
stub_request(:get, broker_binding_last_operation_url).
140-
with(query: hash_including({
141-
operation: operation
142-
})).to_timeout
143-
end
144-
145-
it 'continues polling' do
146-
execute_all_jobs(expected_successes: 1, expected_failures: 0)
147-
148-
binding.reload
149-
expect(binding.last_operation.type).to eq(operation_type)
150-
expect(binding.last_operation.state).to eq('in progress')
151-
152-
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
153-
end
154-
end
155-
156-
context 'connection errors' do
157-
[SocketError, Errno::ECONNREFUSED, RuntimeError].each do |error|
158-
before do
159-
stub_request(:get, broker_binding_last_operation_url).
160-
with(query: hash_including({
161-
operation: operation
162-
})).to_raise(error)
163-
end
164-
165-
it 'continues polling' do
166-
execute_all_jobs(expected_successes: 1, expected_failures: 0)
167-
168-
binding.reload
169-
expect(binding.last_operation.type).to eq(operation_type)
170-
expect(binding.last_operation.state).to eq('in progress')
171-
172-
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
173-
end
174-
end
175-
end
176-
end

0 commit comments

Comments
 (0)