Skip to content

Commit c51369f

Browse files
authored
Improve error handling for already deleted resources (#4352)
* Improve error handling for already deleted resources If a user triggers two DELETE requests for the same resource within a few milliseconds two deletion jobs are created. If both jobs are be executed in parallel it might happen that one of them fails with a `Sequel::NoExistingObject` error. This change prevents this by catching those errors but only if the error message originates from the current model/ table. Therefore it is ensured that similar error messages when e.g. deleting sub resources still result in job failures. The same applies if a user unbind routes but also deletes the route itself within a very short time period. For this case the route model now also catches `Sequel::NoExistingObject` errors related to route bindings. Additionally, the error handling in the DeleteActionJob was improved to catch errors when the actions itself don't handle them. * Ensure `delete` returns empty error Otherwise it might return a dataset. * ensure array is not frozen
1 parent c29f30b commit c51369f

File tree

7 files changed

+80
-5
lines changed

7 files changed

+80
-5
lines changed

app/actions/app_delete.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def delete(apps, record_event: true)
5252
end
5353
logger.info("Deleted app: #{app.guid}")
5454
end
55+
56+
[]
5557
end
5658

5759
def delete_without_event(apps)

app/actions/organization_delete.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ def delete(org_dataset)
3535
Repositories::OrganizationEventRepository.new.record_organization_delete_request(org, @user_audit_info, { recursive: true })
3636
end
3737
end
38+
39+
[]
3840
end
3941

4042
def timeout_error(dataset)

app/actions/v2/organization_delete.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def delete(org_dataset)
2121
org.destroy
2222
end
2323
end
24+
[]
2425
end
2526

2627
def timeout_error(dataset)

app/jobs/delete_action_job.rb

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,23 @@ def perform
1717
logger.info("Deleting model class '#{model_class}' with guid '#{resource_guid}'")
1818

1919
dataset = model_class.where(guid: resource_guid)
20-
if delete_action_can_return_warnings?
21-
errors, warnings = delete_action.delete(dataset)
22-
else
23-
errors = delete_action.delete(dataset)
20+
errors = []
21+
22+
begin
23+
if delete_action_can_return_warnings?
24+
errors, warnings = delete_action.delete(dataset)
25+
else
26+
errors = delete_action.delete(dataset)
27+
end
28+
rescue StandardError => e
29+
errors << e
2430
end
2531

26-
raise errors.first unless errors&.empty?
32+
# Ignore errors if the target resource has already been deleted (e.g., by a parallel job)
33+
quoted_table_name = model_class.db.quote_identifier(model_class.table_name)
34+
errors.reject! { |err| err.is_a?(Sequel::NoExistingObject) && err.message.include?("DELETE FROM #{quoted_table_name}") } unless errors.frozen?
35+
36+
raise errors.first unless errors.empty?
2737

2838
warnings
2939
end

app/models/runtime/route.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ def internal_route_vip_range
263263

264264
def destroy_route_bindings
265265
errors = RouteBindingDelete.new.delete(route_binding_dataset)
266+
267+
quoted_table_name = RouteBinding.db.quote_identifier(RouteBinding.table_name)
268+
errors.reject! { |e| e.is_a?(Sequel::NoExistingObject) && e.message.include?("DELETE FROM #{quoted_table_name}") }
269+
266270
raise errors.first unless errors.empty?
267271
end
268272

spec/unit/jobs/delete_action_job_spec.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
module VCAP::CloudController
44
module Jobs
5+
RSpec.shared_examples 'a delete action handling external deletion' do
6+
before do
7+
allow_any_instance_of(resource.class).to receive(:destroy).and_wrap_original do |original_method, *args|
8+
Sequel::Model.db.run("DELETE FROM #{resource.class.table_name} WHERE id = #{resource.id}") # Simulate external deletion
9+
original_method.call(*args)
10+
end
11+
end
12+
13+
it 'still attempts to delete the resource even if it was already deleted externally' do
14+
expect { delete_job.perform }.not_to raise_error
15+
end
16+
end
17+
518
RSpec.describe DeleteActionJob, job_context: :worker do
619
let(:user) { User.make(admin: true) }
720
let(:delete_action) { instance_double(SpaceDelete, delete: []) }
@@ -127,6 +140,32 @@ module Jobs
127140
expect(job.resource_guid).to eq(space.guid)
128141
end
129142
end
143+
144+
context 'when the resource is deleted externally before destroy' do
145+
it_behaves_like 'a delete action handling external deletion' do
146+
let(:resource) { PackageModel.make }
147+
let(:delete_action) { PackageDelete.new(nil) }
148+
let(:delete_job) { DeleteActionJob.new(PackageModel, resource.guid, delete_action) }
149+
end
150+
151+
it_behaves_like 'a delete action handling external deletion' do
152+
let(:resource) { Space.make }
153+
let(:delete_action) { SpaceDelete.new(nil, nil) }
154+
let(:delete_job) { DeleteActionJob.new(Space, resource.guid, delete_action) }
155+
end
156+
157+
it_behaves_like 'a delete action handling external deletion' do
158+
let(:resource) { Route.make }
159+
let(:delete_action) { RouteDeleteAction.new(nil) }
160+
let(:delete_job) { DeleteActionJob.new(Route, resource.guid, delete_action) }
161+
end
162+
163+
it_behaves_like 'a delete action handling external deletion' do
164+
let(:resource) { User.make }
165+
let(:delete_action) { UserDeleteAction.new }
166+
let(:delete_job) { DeleteActionJob.new(User, resource.guid, delete_action) }
167+
end
168+
end
130169
end
131170
end
132171
end

spec/unit/models/runtime/route_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,6 +1284,23 @@ module VCAP::CloudController
12841284
expect(process.reload.routes[0]).to eq route
12851285
end
12861286
end
1287+
1288+
context 'when route_binding is deleted externally before destroy' do
1289+
before do
1290+
allow_any_instance_of(ServiceKeyDelete).to receive(:delete_service_binding).and_wrap_original do |original_method, *args|
1291+
service_binding = args.first
1292+
service_binding.destroy
1293+
original_method.call(*args)
1294+
end
1295+
end
1296+
1297+
it 'does not raise a Sequel::NoExistingObject error' do
1298+
route_binding = RouteBinding.make
1299+
route = route_binding.route
1300+
stub_unbind(route_binding)
1301+
expect { route.destroy }.not_to raise_error
1302+
end
1303+
end
12871304
end
12881305
end
12891306

0 commit comments

Comments
 (0)