Skip to content

Commit 8dc0bd6

Browse files
claudiubeluDany9966
authored andcommitted
conductor: fix on-error cleanup skipped when source task cancelled-after-completion
When concurrently deploying resources on the source (deploy_source) and on the target (deploy_target), if deploy_target fails while deploy_source is still running, the conductor marks deploy_source as `CANCELLING` and sends it a cancel signal. If deploy_source finishes its work before the signal kills it, `task_completed()` is sent for a task in the `CANCELLING` state. The conductor saves the result (so source_resources, including the resource ID is saved in the DB) and marks the task `CANCELED_AFTER_COMPLETION`. The on-error cleanup scheduling logic in _advance_execution_state checks: ``` elif TASK_STATUS_COMPLETED in non_error_parents.values(): ``` `CANCELED_AFTER_COMPLETION` != `COMPLETED`, so the check is False. `DELETE_TRANSFER_SOURCE_RESOURCES` is unscheduled and the source resource is leaked. Adding `CLEANUP_TASK_TRIGGER_STATUSES`, covering both `COMPLETED` and `CANCELED_AFTER_COMPLETION`, since both mean the task ran to completion and may have created resources that need cleaning up. Use that constant in the on-error scheduling check. Adding assertions in `coriolis/tests/integration/test_failure_recovery.py`, that check in the DB that `DELETE_TRANSFER_SOURCE_RESOURCES` / `DELETE_TRANSFER_TARGET_RESOURCES` reached a completed status, and that the source /target resources were zeroed out in the action info.
1 parent 0bae38b commit 8dc0bd6

3 files changed

Lines changed: 116 additions & 11 deletions

File tree

‎coriolis/conductor/rpc/server.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2722,9 +2722,13 @@ def _start_task(task):
27222722
parent_task_statuses)
27232723
task_statuses[task.id] = _start_task(task)
27242724
# start on-error tasks only if at least one non-error
2725-
# parent task has completed successfully:
2726-
elif constants.TASK_STATUS_COMPLETED in (
2727-
non_error_parents.values()):
2725+
# parent task has completed (COMPLETED or
2726+
# CANCELED_AFTER_COMPLETION both mean the task
2727+
# finished its work and may have created resources
2728+
# that need cleaning up):
2729+
elif any(
2730+
s in constants.CLEANUP_TASK_TRIGGER_STATUSES
2731+
for s in non_error_parents.values()):
27282732
LOG.info(
27292733
"Starting on-error task '%s' as all parent "
27302734
"tasks have been finalized and at least one "

‎coriolis/constants.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@
7575
TASK_STATUS_FAILED_TO_CANCEL
7676
]
7777

78+
# Statuses that indicate a non-error parent task completed its work and
79+
# may have created resources that on-error cleanup tasks must destroy.
80+
# CANCELED_AFTER_COMPLETION means the task ran to completion (result saved,
81+
# resources created) but was marked cancelled because a cancellation was
82+
# already in flight when the completion arrived.
83+
CLEANUP_TASK_TRIGGER_STATUSES = [
84+
TASK_STATUS_COMPLETED,
85+
TASK_STATUS_CANCELED_AFTER_COMPLETION,
86+
]
87+
7888
FINALIZED_TASK_STATUSES = [
7989
TASK_STATUS_COMPLETED,
8090
TASK_STATUS_ERROR,

‎coriolis/tests/integration/test_failure_recovery.py‎

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,123 @@
44
"""
55
Integration test for failure handling and cleanup.
66
7-
Patches the active import provider's deploy_replica_target_resources to raise
8-
an exception, triggers a transfer execution, and asserts that:
9-
1. The execution reaches ERROR status.
10-
2. Cleanup tasks (delete_replica_source_resources) ran so the replicator
11-
process is no longer alive.
7+
Tests two symmetrical error-path scenarios:
8+
9+
- deploy_replica_target_resources is delayed then raises -> source task
10+
completes first (creating real source resources) -> execution errors ->
11+
source cleanup (DELETE_TRANSFER_SOURCE_RESOURCES) must run,
12+
source_resources zeroed out.
13+
14+
- deploy_replica_source_resources is delayed then raises -> target task
15+
completes first (creating real target resources) -> execution errors ->
16+
destination cleanup (DELETE_TRANSFER_TARGET_RESOURCES) must run,
17+
target_resources zeroed out.
1218
1319
Must be run as root; requires the scsi_debug kernel module.
1420
"""
1521

22+
import time
1623
from unittest import mock
1724

25+
from coriolis import constants
26+
from coriolis.db import api as db_api
1827
from coriolis.tests.integration import base
1928

2029

2130
class TransferFailureIntegrationTest(base.ReplicaIntegrationTestBase):
2231
"""Error path and resource cleanup."""
2332

24-
def test_error_status_on_provider_failure(self):
25-
"""Execution reaches ERROR when target resource deployment fails."""
33+
def _assertResourcesCleaned(self, execution_id, task_type, resource_key):
34+
ctxt = self._get_db_context()
35+
execution = db_api.get_tasks_execution(ctxt, execution_id)
36+
37+
cleanup_task = next(
38+
(t for t in execution.tasks if t.task_type == task_type),
39+
None,
40+
)
41+
self.assertIsNotNone(
42+
cleanup_task,
43+
"%s task not found in execution %s" % (task_type, execution_id),
44+
)
45+
46+
self.assertIn(
47+
cleanup_task.status,
48+
constants.CLEANUP_TASK_TRIGGER_STATUSES,
49+
"%s task did not complete (status: %s); %s may have leaked"
50+
% (task_type, cleanup_task.status, resource_key),
51+
)
52+
53+
action = db_api.get_action(
54+
ctxt, execution.action_id, include_task_info=True)
55+
for instance in execution.action.instances:
56+
self.assertIsNone(
57+
action.info.get(instance, {}).get(resource_key),
58+
"%s not cleared for instance '%s' after cleanup task completed"
59+
% (resource_key, instance),
60+
)
61+
62+
def assertSourceResourcesCleaned(self, execution_id):
63+
self._assertResourcesCleaned(
64+
execution_id,
65+
constants.TASK_TYPE_DELETE_TRANSFER_SOURCE_RESOURCES,
66+
"source_resources",
67+
)
68+
69+
def assertTargetResourcesCleaned(self, execution_id):
70+
self._assertResourcesCleaned(
71+
execution_id,
72+
constants.TASK_TYPE_DELETE_TRANSFER_TARGET_RESOURCES,
73+
"target_resources",
74+
)
75+
76+
def test_source_resources_cleaned_on_target_failure(self):
77+
"""Source cleanup runs when target resource deployment fails.
78+
79+
deploy_replica_target_resources is delayed so that
80+
deploy_replica_source_resources has time to finish (creating real
81+
source resources) before the target task eventually raises. The
82+
conductor must schedule DELETE_TRANSFER_SOURCE_RESOURCES and zero out
83+
source_resources in the action info.
84+
"""
2685
injected_error = Exception("injected target resource failure")
2786

87+
def _slow_then_fail(self_provider, *args, **kwargs):
88+
time.sleep(15)
89+
raise injected_error
90+
2891
with mock.patch.object(
2992
self._harness.imp_provider_class,
3093
"deploy_replica_target_resources",
31-
side_effect=injected_error,
94+
_slow_then_fail,
3295
):
3396
execution = self._client.transfer_executions.create(
3497
self._transfer.id, shutdown_instances=False)
3598
self.assertExecutionErrored(execution.id)
99+
100+
self.assertSourceResourcesCleaned(execution.id)
101+
102+
def test_target_resources_cleaned_on_source_failure(self):
103+
"""Target cleanup runs when source resource deployment fails.
104+
105+
deploy_replica_source_resources is delayed so that
106+
deploy_replica_target_resources has time to finish (creating real
107+
target resources) before the source task eventually raises. The
108+
conductor must schedule DELETE_TRANSFER_TARGET_RESOURCES and zero out
109+
target_resources in the action info.
110+
"""
111+
injected_error = Exception("injected source resource failure")
112+
113+
def _slow_then_fail(self_provider, *args, **kwargs):
114+
time.sleep(15)
115+
raise injected_error
116+
117+
with mock.patch.object(
118+
self._harness.exp_provider_class,
119+
"deploy_replica_source_resources",
120+
_slow_then_fail,
121+
):
122+
execution = self._client.transfer_executions.create(
123+
self._transfer.id, shutdown_instances=False)
124+
self.assertExecutionErrored(execution.id)
125+
126+
self.assertTargetResourcesCleaned(execution.id)

0 commit comments

Comments
 (0)