Skip to content

Commit 192e6eb

Browse files
committed
Address PR review feedback
- Rename RootJobMixin -> RecursiveDeleteRootJobMixin (it is delete-specific) - Move defer-while-in-flight and raise-if-failed into the mixin; keep sub_jobs_in_flight? side-effect-free - Log sub-resource failures via rescue (log_immediate_failures / log_recursive_delete_failure) instead of looping over sub_resource_errors - Merge the two SubResourceError/StandardError rescue blocks in ServiceInstanceDelete#delete - raise_if_sub_jobs_failed short-circuits with any? instead of building errors - Memoize @service_instance per pass (cleared in ensure); use resource_guid consistently in both recursive-delete jobs - Add ServiceInstance#route_bindings association and use it - SubResourceError#any_in_progress? uses any? (drop in_progress_operations) - Enqueuer exposes current_root_job_guid overridden by GenericEnqueuer - apps_controller: build the job from the locked resource for symmetry - logging_context: align save/restore variable naming - migration: use elsif in the down block - Restore DeleteAppErrorTranslatorJob spec; assert FOR UPDATE via have_queried_db_times
1 parent 8e4afae commit 192e6eb

20 files changed

Lines changed: 249 additions & 154 deletions

app/actions/v3/service_instance_delete.rb

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,10 @@ def delete
5252
end
5353

5454
result
55-
rescue SubResourceError => e
56-
raise if !@fail_if_in_progress && e.any_in_progress? # re-raise SubResourceError so that root job continues to run
57-
58-
update_last_operation_with_failure(e.message) unless service_instance.operation_in_progress?
59-
raise e
6055
rescue StandardError => e
56+
# In-progress sub-resource deletions aren't failures: re-raise so the root job re-enqueues and waits.
57+
raise if e.is_a?(SubResourceError) && e.any_in_progress?
58+
6159
update_last_operation_with_failure(e.message) unless service_instance.operation_in_progress?
6260
raise e
6361
end
@@ -150,7 +148,7 @@ def destroy
150148
end
151149

152150
def remove_associations
153-
errors = delete_bindings(RouteBinding.where(service_instance:), user_audit_info: service_event_repository.user_audit_info)
151+
errors = delete_bindings(service_instance.route_bindings, user_audit_info: service_event_repository.user_audit_info)
154152
errors += delete_bindings(service_instance.service_bindings, user_audit_info: service_event_repository.user_audit_info)
155153
errors += delete_bindings(service_instance.service_keys, user_audit_info: service_event_repository.user_audit_info)
156154
errors + unshare_all_spaces

app/controllers/v3/apps_controller.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,9 @@ def destroy
161161
resource_model: AppModel,
162162
resource_guid: app.guid,
163163
operation: 'app.delete'
164-
) { VCAP::CloudController::V3::RecursiveDeleteAppJob.new(app.guid, user_audit_info) }
164+
) do |locked_app|
165+
VCAP::CloudController::V3::RecursiveDeleteAppJob.new(locked_app.guid, user_audit_info)
166+
end
165167

166168
app_not_found! unless job
167169
else

app/errors/sub_resource_error.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,8 @@ def failures
2020
@errors.reject { |e| e.is_a?(AsyncOperationInProgress) }
2121
end
2222

23-
def in_progress_operations
24-
@errors.select { |e| e.is_a?(AsyncOperationInProgress) }
25-
end
26-
2723
def any_in_progress?
28-
in_progress_operations.any?
24+
@errors.any? { |e| e.is_a?(AsyncOperationInProgress) }
2925
end
3026

3127
def message

app/jobs/enqueuer.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
module VCAP::CloudController
1010
module Jobs
1111
class Enqueuer
12-
attr_accessor :root_job_guid
13-
1412
def initialize(opts={})
1513
@opts = opts
1614
@timeout_calculator = JobTimeoutCalculator.new(VCAP::CloudController::Config.config)
@@ -23,7 +21,7 @@ def enqueue(job, run_at: nil, priority_increment: nil)
2321
end
2422

2523
def enqueue_pollable(job, existing_guid: nil, run_at: nil, priority_increment: nil, preserve_priority: false)
26-
wrapped_job = PollableJobWrapper.new(job, existing_guid:, root_job_guid:)
24+
wrapped_job = PollableJobWrapper.new(job, existing_guid: existing_guid, root_job_guid: current_root_job_guid)
2725

2826
wrapped_job = yield wrapped_job if block_given?
2927

@@ -49,6 +47,11 @@ def self.unwrap_job(job)
4947

5048
private
5149

50+
# Base enqueuer has no root-job context; GenericEnqueuer overrides this while a root job is active.
51+
def current_root_job_guid
52+
nil
53+
end
54+
5255
def enqueue_job(job, run_at: nil, priority_increment: nil, preserve_priority: false)
5356
@opts['guid'] = SecureRandom.uuid
5457
request_id = ::VCAP::Request.current_id

app/jobs/generic_enqueuer.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@ def self.reset!
1717
end
1818

1919
def activate_root_context(root_job_guid:)
20-
self.root_job_guid = root_job_guid
20+
@root_job_guid = root_job_guid
2121
end
2222

2323
def deactivate_root_context
24-
self.root_job_guid = nil
24+
@root_job_guid = nil
25+
end
26+
27+
private
28+
29+
def current_root_job_guid
30+
@root_job_guid
2531
end
2632
end
2733
end

app/jobs/logging_context_job.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ def with_request_id_set
8989
def with_root_job_guid_set
9090
return yield if @root_job_guid.nil?
9191

92-
previous = ::VCAP::Request.current_root_job_guid
92+
current_root_job_guid = ::VCAP::Request.current_root_job_guid
9393
::VCAP::Request.current_root_job_guid = @root_job_guid
9494
yield
9595
ensure
96-
::VCAP::Request.current_root_job_guid = previous unless @root_job_guid.nil?
96+
::VCAP::Request.current_root_job_guid = current_root_job_guid unless @root_job_guid.nil?
9797
end
9898
end
9999
end

app/jobs/mixins/root_job_mixin.rb renamed to app/jobs/mixins/recursive_delete_root_job_mixin.rb

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
module VCAP::CloudController
66
module Jobs
7-
module RootJobMixin
7+
module RecursiveDeleteRootJobMixin
88
# Buffer added on top of the sub-jobs' next run_at so the root wakes just after them, never before.
99
ROOT_JOB_BUFFER_SECONDS = 5
1010

@@ -16,12 +16,26 @@ def root_job_guid
1616

1717
def perform_with_root_job_handling
1818
activate_root_job_context
19+
20+
if sub_jobs_in_flight?
21+
add_in_progress_warning(root_job)
22+
logger.info("#{display_name} #{resource_guid} (job #{root_job_guid}) waiting on in-progress sub-resource deletions")
23+
return
24+
end
25+
26+
raise_if_sub_jobs_failed
27+
1928
yield
2029
rescue SubResourceError => e
21-
return if e.any_in_progress?
30+
if e.any_in_progress?
31+
log_immediate_failures(e.failures) # log failures that surfaced this run; async ones still polling defer us and are retried on the next run
32+
return
33+
end
2234

23-
raise compound_error_for(e.failures)
24-
rescue CloudController::Errors::ApiError, CloudController::Errors::CompoundError
35+
raise log_recursive_delete_failure(compound_error_for(e.failures)) # sync error occurred & no async job pending -> log and fail
36+
rescue CloudController::Errors::CompoundError => e
37+
raise log_recursive_delete_failure(e) # async sub job failed -> log and fail
38+
rescue CloudController::Errors::ApiError
2539
raise
2640
rescue StandardError => e
2741
raise CloudController::Errors::ApiError.new_from_details('UnableToPerform', 'delete', e.message)
@@ -76,18 +90,32 @@ def seconds_until_slowest_sub_job
7690
end
7791

7892
def sub_jobs_in_flight?
79-
return false if active_sub_jobs.empty?
80-
81-
add_in_progress_warning(root_job)
82-
true
93+
active_sub_jobs.any?
8394
end
8495

8596
def raise_if_sub_jobs_failed
86-
return if sub_job_errors.empty?
97+
return unless sub_jobs.any? { |s| s.state == PollableJobModel::FAILED_STATE }
8798

8899
raise CloudController::Errors::CompoundError.new(all_failure_errors)
89100
end
90101

102+
# Logs failures that surfaced during this run (a binding's unbind failed immediately rather than going
103+
# async-in-progress). Makes them visible to operators even though the run defers on the async ones,
104+
# which are retried on the next run.
105+
def log_immediate_failures(failures)
106+
failures.each do |error|
107+
logger.warn("#{display_name} #{resource_guid} (job #{root_job_guid}) sub-resource deletion failed: #{error.message}")
108+
end
109+
end
110+
111+
# Logs each underlying failure and returns the error so callers can `raise log_recursive_delete_failure(error)`.
112+
def log_recursive_delete_failure(error)
113+
error.underlying_errors.each do |underlying|
114+
logger.warn("#{display_name} #{resource_guid} (job #{root_job_guid}) sub-resource deletion failed: #{underlying.message}")
115+
end
116+
error
117+
end
118+
91119
def add_in_progress_warning(job)
92120
return if job.warnings_dataset.any?
93121

app/jobs/v3/recursive_delete_app_job.rb

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,24 @@
11
require 'jobs/reoccurring_job'
2-
require 'jobs/mixins/root_job_mixin'
2+
require 'jobs/mixins/recursive_delete_root_job_mixin'
33
require 'actions/app_delete'
44
require 'actions/app_stop'
55

66
module VCAP::CloudController
77
module V3
88
class RecursiveDeleteAppJob < Jobs::ReoccurringJob
9-
include Jobs::RootJobMixin
9+
include Jobs::RecursiveDeleteRootJobMixin
1010

11-
attr_reader :app_guid
11+
attr_reader :resource_guid
1212

13-
def initialize(app_guid, user_audit_info)
13+
def initialize(resource_guid, user_audit_info)
1414
super()
15-
@app_guid = app_guid
15+
@resource_guid = resource_guid
1616
@user_audit_info = user_audit_info
1717
end
1818

1919
def perform
2020
perform_with_root_job_handling do
21-
if sub_jobs_in_flight?
22-
logger.info("app delete #{app_guid} (job #{root_job_guid}) waiting on in-progress service binding deletions")
23-
return
24-
end
25-
26-
log_failed_bindings
27-
raise_if_sub_jobs_failed
28-
29-
app = AppModel.first(guid: app_guid)
21+
app = AppModel.first(guid: resource_guid)
3022
return finish unless app
3123

3224
AppStop.stop(app: app, user_audit_info: @user_audit_info, delete_triggered: true) if app.desired_state != ProcessModel::STOPPED
@@ -37,10 +29,6 @@ def perform
3729

3830
def handle_timeout; end
3931

40-
def resource_guid
41-
app_guid
42-
end
43-
4432
def resource_type
4533
'app'
4634
end
@@ -57,19 +45,13 @@ def max_attempts
5745

5846
attr_reader :user_audit_info
5947

60-
def log_failed_bindings
61-
sub_resource_errors.each do |guid, error|
62-
logger.warn("app delete #{app_guid} (job #{root_job_guid}): service binding #{guid} deletion failed: #{error.message}")
63-
end
64-
end
65-
6648
def in_progress_warning_detail
6749
'Deletion of the app is still in progress: one or more service bindings are still being deleted. ' \
6850
'It will complete once those operations finish.'
6951
end
7052

7153
def sub_resource_errors
72-
app = AppModel.first(guid: app_guid)
54+
app = AppModel.first(guid: resource_guid)
7355
return [] unless app
7456

7557
app.service_bindings.select(&:delete_failed?).map do |binding|

app/jobs/v3/recursive_delete_service_instance_job.rb

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,22 @@
11
require 'jobs/reoccurring_job'
2-
require 'jobs/mixins/root_job_mixin'
2+
require 'jobs/mixins/recursive_delete_root_job_mixin'
33
require 'actions/v3/service_instance_delete'
44

55
module VCAP::CloudController
66
module V3
77
class RecursiveDeleteServiceInstanceJob < VCAP::CloudController::Jobs::ReoccurringJob
8-
include Jobs::RootJobMixin
8+
include Jobs::RecursiveDeleteRootJobMixin
99

1010
attr_reader :resource_guid
1111

12-
def initialize(guid, user_audit_info)
12+
def initialize(resource_guid, user_audit_info)
1313
super()
14-
@resource_guid = guid
14+
@resource_guid = resource_guid
1515
@user_audit_info = user_audit_info
1616
end
1717

1818
def perform
1919
perform_with_root_job_handling do
20-
if sub_jobs_in_flight?
21-
logger.info("service instance delete #{resource_guid} (job #{root_job_guid}) waiting on in-progress binding deletions")
22-
return
23-
end
24-
25-
log_failed_children
26-
raise_if_sub_jobs_failed
27-
2820
return finish unless service_instance
2921

3022
self.maximum_duration_seconds = service_instance.service_plan.try(:maximum_polling_duration)
@@ -39,6 +31,8 @@ def perform
3931

4032
self.polling_interval_seconds = result[:retry_after].to_i if result[:retry_after]
4133
end
34+
ensure
35+
@service_instance = nil # drop the per-pass cache so it is not serialised into the reoccurring-job reschedule
4236
end
4337

4438
def handle_timeout
@@ -71,7 +65,7 @@ def in_progress_warning_detail
7165
end
7266

7367
def service_instance
74-
ManagedServiceInstance.first(guid: resource_guid)
68+
@service_instance ||= ManagedServiceInstance.first(guid: resource_guid)
7569
end
7670

7771
def delete_in_progress?
@@ -83,17 +77,11 @@ def action
8377
ServiceInstanceDelete.new(service_instance, Repositories::ServiceEventRepository.new(user_audit_info), fail_if_in_progress: false)
8478
end
8579

86-
def log_failed_children
87-
sub_resource_errors.each do |guid, error|
88-
logger.warn("service instance delete #{resource_guid} (job #{root_job_guid}): binding #{guid} deletion failed: #{error.message}")
89-
end
90-
end
91-
9280
def sub_resource_errors
9381
si = service_instance
9482
return [] unless si
9583

96-
children = si.service_bindings + si.service_keys + RouteBinding.where(service_instance: si).all
84+
children = si.service_bindings + si.service_keys + si.route_bindings
9785
children.select(&:delete_failed?).map do |child|
9886
identity = "#{child.class.name.demodulize.underscore} #{child.guid}"
9987
[child.guid, CloudController::Errors::ApiError.new_from_details('UnprocessableEntity', "#{identity}: #{child.last_operation.description}")]

app/models/services/service_instance.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class InvalidServiceBinding < StandardError; end
3030

3131
one_to_many :service_bindings, before_add: :validate_service_binding, key: :service_instance_guid, primary_key: :guid
3232
one_to_many :service_keys
33+
one_to_many :route_bindings
3334

3435
one_to_many :labels, class: 'VCAP::CloudController::ServiceInstanceLabelModel', key: :resource_guid, primary_key: :guid
3536
one_to_many :annotations, class: 'VCAP::CloudController::ServiceInstanceAnnotationModel', key: :resource_guid, primary_key: :guid

0 commit comments

Comments
 (0)