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

Commit b7ab4b0

Browse files
FelisiaMblgm
andcommitted
v3(services): refactor delete service instance job
[#176032428](https://www.pivotaltracker.com/story/show/176032428) Co-authored-by: George Blue <gblue@pivotal.io>
1 parent 2c9f4c4 commit b7ab4b0

6 files changed

Lines changed: 697 additions & 429 deletions

File tree

Lines changed: 123 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,138 @@
11
require 'jobs/v3/delete_service_instance_job'
2+
require 'actions/services/locks/deleter_lock'
3+
require 'cloud_controller/errors/api_error'
24

35
module VCAP::CloudController
46
module V3
57
class ServiceInstanceDelete
6-
class AssociationNotEmptyError < StandardError; end
8+
class AssociationNotEmptyError < StandardError
9+
end
10+
11+
class InstanceSharedError < StandardError
12+
end
13+
14+
class DeleteFailed < StandardError
15+
end
716

8-
class InstanceSharedError < StandardError; end
17+
DeleteStatus = Struct.new(:finished, :operation).freeze
18+
DeleteStarted = ->(operation) { DeleteStatus.new(false, operation) }
19+
DeleteComplete = DeleteStatus.new(true, nil).freeze
920

10-
def initialize(event_repo)
21+
PollingStatus = Struct.new(:finished, :retry_after).freeze
22+
PollingFinished = PollingStatus.new(true, nil).freeze
23+
ContinuePolling = ->(retry_after) { PollingStatus.new(false, retry_after) }
24+
25+
def initialize(service_instance, event_repo)
26+
@service_instance = service_instance
1127
@service_event_repository = event_repo
1228
end
1329

14-
def delete(service_instance)
15-
association_not_empty! if service_instance.has_bindings? || service_instance.has_keys? || service_instance.has_routes?
30+
def delete
31+
operation_in_progress! if service_instance.operation_in_progress? && service_instance.last_operation.type != 'create'
32+
33+
result = send_deprovison_to_broker
34+
if result[:finished]
35+
destroy
36+
record_delete_event
37+
else
38+
update_last_operation_with_operation_id(result[:operation])
39+
record_start_delete_event
40+
end
1641

42+
result
43+
rescue => e
44+
update_last_operation_with_failure(e.message) unless service_instance.operation_in_progress?
45+
raise e
46+
end
47+
48+
def delete_checks
49+
association_not_empty! if service_instance.has_bindings? || service_instance.has_keys? || service_instance.has_routes?
1750
cannot_delete_shared_instances! if service_instance.shared?
51+
end
1852

19-
lock = DeleterLock.new(service_instance)
53+
def poll
54+
result = client.fetch_service_instance_last_operation(service_instance)
55+
case result[:last_operation][:state]
56+
when 'in progress'
57+
update_last_operation_with_description(result[:last_operation][:description])
58+
ContinuePolling.call(result[:retry_after])
59+
when 'succeeded'
60+
destroy
61+
PollingFinished
62+
else
63+
delete_failed!(result[:last_operation][:description])
64+
end
65+
rescue DeleteFailed => e
66+
update_last_operation_with_failure(e.message)
67+
raise CloudController::Errors::ApiError.new_from_details('UnableToPerform', 'delete', e.message)
68+
rescue => e
69+
update_last_operation_with_failure(e.message)
70+
ContinuePolling.call(nil)
71+
end
72+
73+
def update_last_operation_with_failure(message)
74+
service_instance.save_with_new_operation(
75+
{},
76+
{
77+
type: 'delete',
78+
state: 'failed',
79+
description: message,
80+
}
81+
)
82+
end
2083

84+
private
85+
86+
attr_reader :service_event_repository, :service_instance
87+
88+
def client
89+
VCAP::Services::ServiceClientProvider.provide(instance: service_instance)
90+
end
91+
92+
def send_deprovison_to_broker
93+
result = client.deprovision(service_instance, accepts_incomplete: true)
94+
return DeleteComplete if result[:last_operation][:state] == 'succeeded'
95+
96+
DeleteStarted.call(result[:last_operation][:broker_provided_operation])
97+
end
98+
99+
def record_delete_event
21100
case service_instance
22-
when ManagedServiceInstance
23-
return false
24-
when UserProvidedServiceInstance
25-
lock.lock!
26-
synchronous_destroy(service_instance, lock)
27-
return true
101+
when VCAP::CloudController::ManagedServiceInstance
102+
service_event_repository.record_service_instance_event(:delete, service_instance)
103+
when VCAP::CloudController::UserProvidedServiceInstance
104+
service_event_repository.record_user_provided_service_instance_event(:delete, service_instance)
28105
end
29106
end
30107

31-
private
108+
def record_start_delete_event
109+
service_event_repository.record_service_instance_event(:start_delete, service_instance)
110+
end
32111

33-
def synchronous_destroy(service_instance, lock)
34-
lock.unlock_and_destroy!
35-
service_event_repository.record_user_provided_service_instance_event(:delete, service_instance)
36-
nil
112+
def destroy
113+
ServiceInstance.db.transaction do
114+
service_instance.lock!
115+
service_instance.last_operation&.destroy
116+
service_instance.destroy
117+
end
118+
end
119+
120+
def update_last_operation_with_operation_id(operation_id)
121+
service_instance.save_with_new_operation(
122+
{},
123+
{
124+
type: 'delete',
125+
state: 'in progress',
126+
broker_provided_operation: operation_id
127+
}
128+
)
129+
end
130+
131+
def update_last_operation_with_description(description)
132+
lo = service_instance.last_operation.to_hash
133+
lo[:broker_provided_operation] = service_instance.last_operation.broker_provided_operation
134+
lo[:description] = description
135+
service_instance.save_with_new_operation({}, lo)
37136
end
38137

39138
def association_not_empty!
@@ -44,7 +143,13 @@ def cannot_delete_shared_instances!
44143
raise InstanceSharedError
45144
end
46145

47-
attr_reader :service_event_repository
146+
def operation_in_progress!
147+
raise CloudController::Errors::ApiError.new_from_details('AsyncServiceInstanceOperationInProgress', service_instance.name)
148+
end
149+
150+
def delete_failed!(message)
151+
raise DeleteFailed.new(message)
152+
end
48153
end
49154
end
50155
end

app/controllers/v3/service_instances_controller.rb

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,16 @@ def destroy
112112
return [:no_content, nil]
113113
end
114114

115-
deleted = V3::ServiceInstanceDelete.new(service_event_repository).delete(service_instance)
115+
delete_action = V3::ServiceInstanceDelete.new(service_instance, service_event_repository)
116+
delete_action.delete_checks
116117

117-
if deleted
118-
head :no_content
119-
else
118+
case service_instance
119+
when VCAP::CloudController::ManagedServiceInstance
120120
job_guid = enqueue_delete_job(service_instance)
121121
head :accepted, 'Location' => url_builder.build_url(path: "/v3/jobs/#{job_guid}")
122+
when VCAP::CloudController::UserProvidedServiceInstance
123+
delete_action.delete
124+
head :no_content
122125
end
123126
rescue V3::ServiceInstanceDelete::AssociationNotEmptyError
124127
associations_not_empty!

app/jobs/v3/delete_service_instance_job.rb

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,37 @@
33

44
module VCAP::CloudController
55
module V3
6-
class DeprovisionBadResponse < StandardError
7-
end
6+
class DeleteServiceInstanceJob < VCAP::CloudController::Jobs::ReoccurringJob
7+
attr_reader :resource_guid
88

9-
class DeleteServiceInstanceJob < ServiceInstanceAsyncJob
10-
def initialize(guid, audit_info)
11-
super
12-
@request_attr = nil
9+
def initialize(guid, user_audit_info)
10+
super()
11+
@resource_guid = guid
12+
@user_audit_info = user_audit_info
1313
end
1414

15-
def send_broker_request(client)
16-
deprovision_response = client.deprovision(service_instance, { accepts_incomplete: true })
15+
def perform
16+
return finish unless service_instance
1717

18-
@request_failed = false
18+
self.maximum_duration_seconds = service_instance.service_plan.try(:maximum_polling_duration)
1919

20-
deprovision_response
21-
rescue VCAP::Services::ServiceBrokers::V2::Errors::ServiceBrokerBadResponse => err
22-
@request_failed = true
23-
raise DeprovisionBadResponse.new(err.message)
24-
rescue CloudController::Errors::ApiError => err
25-
raise OperationCancelled.new('The service broker rejected the request') if err.name == 'AsyncServiceInstanceOperationInProgress'
20+
unless delete_in_progress?
21+
result = action.delete
22+
return finish if result[:finished]
23+
end
24+
25+
result = action.poll
26+
return finish if result[:finished]
2627

28+
self.polling_interval_seconds = result[:retry_after].to_i if result[:retry_after]
29+
rescue CloudController::Errors::ApiError => err
2730
raise err
31+
rescue => err
32+
raise CloudController::Errors::ApiError.new_from_details('UnableToPerform', operation_type, err.message)
33+
end
34+
35+
def handle_timeout
36+
action.update_last_operation_with_failure("Service Broker failed to #{operation} within the required time.")
2837
end
2938

3039
def operation
@@ -35,46 +44,29 @@ def operation_type
3544
'delete'
3645
end
3746

38-
def gone!
39-
finish
47+
def resource_type
48+
'service_instance'
4049
end
4150

42-
def restart_on_failure?
43-
true
51+
def display_name
52+
"#{resource_type}.#{operation_type}"
4453
end
4554

46-
def pollable_job_state
47-
return PollableJobModel::PROCESSING_STATE if @request_failed
48-
49-
PollableJobModel::POLLING_STATE
50-
end
55+
private
5156

52-
def restart_job(msg)
53-
super
54-
logger.info("could not complete the operation: #{msg}. Triggering orphan mitigation")
55-
end
57+
attr_reader :user_audit_info
5658

57-
def fail!(err)
58-
case err
59-
when DeprovisionBadResponse
60-
trigger_orphan_mitigation(err)
61-
else
62-
super
63-
end
59+
def service_instance
60+
ManagedServiceInstance.first(guid: resource_guid)
6461
end
6562

66-
private
67-
68-
def operation_succeeded
69-
ServiceInstance.db.transaction do
70-
service_instance.lock!
71-
service_instance.last_operation&.destroy
72-
service_instance.destroy
73-
end
63+
def delete_in_progress?
64+
service_instance.last_operation&.type == 'delete' &&
65+
service_instance.last_operation&.state == 'in progress'
7466
end
7567

76-
def trigger_orphan_mitigation(err)
77-
restart_job(err.message)
68+
def action
69+
ServiceInstanceDelete.new(service_instance, Repositories::ServiceEventRepository.new(user_audit_info))
7870
end
7971
end
8072
end

0 commit comments

Comments
 (0)