Skip to content

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

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

Add async recursive delete for apps and service instances#5299
johha wants to merge 6 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.

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.

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.

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.

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 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).

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