Skip to content

Add async recursive delete for apps and service instances - #5299

Draft
johha wants to merge 9 commits into
mainfrom
recursive-app-service-delete
Draft

Add async recursive delete for apps and service instances#5299
johha wants to merge 9 commits into
mainfrom
recursive-app-service-delete

Conversation

@johha

@johha johha commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Behind temporary_enable_async_recursive_delete flag (default off). Recursive delete jobs re-enqueue instead of failing when sub-resources (service bindings) are deleted asynchronously, waiting for them to settle and surfacing the original broker error on failure. Adds root/sub job tracking via root_job_guid on the jobs table. Foundation for future recursive deletes (org, space).

Thanks for contributing to cloud_controller_ng. To speed up the process of reviewing your pull request please provide us with:

  • A short explanation of the proposed change:

  • An explanation of the use cases your change solves

  • Links to any other associated PRs

  • I have reviewed the contributing guide

  • I have viewed, signed, and submitted the Contributor License Agreement

  • I have made this pull request to the main branch

  • I have run all the unit tests using bundle exec rake

  • I have run CF Acceptance Tests

@johha
johha force-pushed the recursive-app-service-delete branch 8 times, most recently from 1539023 to a87ea6c Compare July 22, 2026 12:17
johha added 2 commits July 22, 2026 17:33
Behind `temporary_enable_async_recursive_delete flag` (default off). Recursive
delete jobs re-enqueue instead of failing when sub-resources (service bindings)
are deleted asynchronously, waiting for them to settle and surfacing the
original broker error on failure. Adds root/sub job tracking via `root_job_guid`
on the jobs table. Foundation for future recursive deletes (org, space).
@johha
johha force-pushed the recursive-app-service-delete branch from a87ea6c to 05ca1aa Compare July 22, 2026 15:45

result
rescue SubResourceError => e
raise if !@fail_if_in_progress && e.any_in_progress? # re-raise SubResourceError so that root job continues to run

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point @fail_if_in_progress will always be false.

Comment thread app/controllers/v3/apps_controller.rb Outdated
resource_model: AppModel,
resource_guid: app.guid,
operation: 'app.delete'
) { VCAP::CloudController::V3::RecursiveDeleteAppJob.new(app.guid, user_audit_info) }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use do |locked_instance| for symmetry.

Comment thread app/jobs/mixins/root_job_mixin.rb Outdated
end

def raise_if_sub_jobs_failed
return if sub_job_errors.empty?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructs sub_job_errors only to check if it is empty or not. Should do something like sub_jobs.any? { |s| s.state == PollableJobModel::FAILED_STATE } instead.

Comment thread app/jobs/mixins/root_job_mixin.rb Outdated
def sub_jobs_in_flight?
return false if active_sub_jobs.empty?

add_in_progress_warning(root_job)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhat unexpected that this predicate method has a side-effect (the warning).

AppStop.stop(app: app, user_audit_info: @user_audit_info, delete_triggered: true) if app.desired_state != ProcessModel::STOPPED
AppDelete.new(@user_audit_info).delete([app])
finish
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of log_failed_bindings (and log_failed_children in RecursiveDeleteServiceInstanceJob):

rescue CloudController::Errors::CompoundError => e
  // log errors from the CompoundError
  raise

Maybe this could even go into the RootJobMixin.

Comment thread app/errors/sub_resource_error.rb Outdated
end

def any_in_progress?
in_progress_operations.any?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@errors.any? { |e| e.is_a?(AsyncOperationInProgress) } is slightly more efficient.

end

def with_root_job_guid_set
return yield if @root_job_guid.nil?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this make sense? This would mean that there could be logs with an old root_job_guid, or?

drop_column :jobs, :root_job_guid, if_exists: true
end

if database_type == :mysql

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elsif?

Comment on lines 58 to 62
update_last_operation_with_failure(e.message) unless service_instance.operation_in_progress?
raise e
rescue StandardError => e
update_last_operation_with_failure(e.message) unless service_instance.operation_in_progress?
raise e

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most parts of the rescue blocks are identical, so maybe merge them:

  rescue StandardError => e
    # In-progress sub-resource deletions aren't failures: re-raise so the root job re-enqueues and waits.
    raise if e.is_a?(SubResourceError) && e.any_in_progress?

    update_last_operation_with_failure(e.message) unless service_instance.operation_in_progress?
    raise e
  end

Comment thread app/jobs/v3/recursive_delete_app_job.rb Outdated
Comment on lines +13 to +15
def initialize(app_guid, user_audit_info)
super()
@app_guid = app_guid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would favor the usage of resource_guid here as well (consistent with RecursiveDeleteServiceInstanceJob and the RootJobMixin).

end
end

describe 'DeleteAppErrorTranslatorJob' do

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this context should be restored and just use the new VCAP::CloudController::SubResourceError class.

Comment thread spec/unit/jobs/enqueuer_spec.rb
Comment thread app/jobs/enqueuer.rb Outdated
Comment on lines +12 to +26
@@ -21,14 +23,26 @@ def enqueue(job, run_at: nil, priority_increment: nil)
end

def enqueue_pollable(job, existing_guid: nil, run_at: nil, priority_increment: nil, preserve_priority: false)
wrapped_job = PollableJobWrapper.new(job, existing_guid:)
wrapped_job = PollableJobWrapper.new(job, existing_guid:, root_job_guid:)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of accessing a variable that is set by a sub-class the following would be easier to read:

class Enqueuer
  def enqueue_pollable(job, existing_guid: nil, run_at: nil, priority_increment: nil, preserve_priority: false)
    wrapped_job = PollableJobWrapper.new(job, existing_guid:, root_job_guid: current_root_job_guid)
  end

  private

  def current_root_job_guid
    nil
  end
class GenericEnqueuer < Enqueuer
  def current_root_job_guid
    @root_job_guid
  end
end

johha added 2 commits August 4, 2026 16:25
- 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
@johha
johha force-pushed the recursive-app-service-delete branch 2 times, most recently from d472e26 to c52da16 Compare August 5, 2026 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants