feat(host-agent): stop assignment execution collaboratively on cancellation
- LeaseGuard gains an is_cancellation convenience property - ActiveAssignmentRunner marks the lease lost with a cancellation reason when a renewal response reports cancel_requested - AssignmentExecutor threads stop_reason through to TaskRunner/ WorkflowRunner and maps a cancellation-flavored stop to AssignmentExecutionResult.status = "cancelled" instead of "failed" - AssignmentProcessor forwards a three-way done/cancelled/failed status when reporting the terminal result - Add/extend tests across lease, assignment, processor, and client
This commit is contained in:
@@ -78,6 +78,39 @@ def test_goal_assignment_preserves_runtime_failure_reason() -> None:
|
||||
assert result.failure_reason == "planner unavailable"
|
||||
|
||||
|
||||
def test_goal_assignment_maps_cancellation_stop_to_cancelled_status() -> None:
|
||||
# First should_stop() call is Executor.execute()'s pre-flight check (must pass
|
||||
# through so the runner is actually invoked); the runner's own loop then stops.
|
||||
calls = {"count": 0}
|
||||
|
||||
def should_stop() -> bool:
|
||||
calls["count"] += 1
|
||||
return calls["count"] > 1
|
||||
|
||||
class FakeTaskRunner:
|
||||
def run(self, task: Task, *, should_stop=None, stop_reason=None) -> Task:
|
||||
assert should_stop is not None and should_stop()
|
||||
assert stop_reason is not None
|
||||
task.status = "cancelled"
|
||||
task.failure_reason = stop_reason()
|
||||
return task
|
||||
|
||||
factories = ExecutionFactories(
|
||||
task_runner_factory=lambda: FakeTaskRunner(), # type: ignore[arg-type,return-value]
|
||||
workflow_runner_factory=lambda: object(), # type: ignore[arg-type,return-value]
|
||||
workflow_store=object(), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
result = AssignmentExecutor(factories).execute(
|
||||
_assignment(),
|
||||
should_stop=should_stop,
|
||||
stop_reason=lambda: "cancellation requested by control plane",
|
||||
)
|
||||
|
||||
assert result.status == "cancelled"
|
||||
assert result.failure_reason == "cancellation requested by control plane"
|
||||
|
||||
|
||||
def test_workflow_assignment_loads_and_executes_definition() -> None:
|
||||
definition = object()
|
||||
calls: list[tuple[object, str]] = []
|
||||
@@ -109,6 +142,44 @@ def test_workflow_assignment_loads_and_executes_definition() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_workflow_assignment_maps_cancellation_stop_to_cancelled_status() -> None:
|
||||
calls = {"count": 0}
|
||||
|
||||
def should_stop() -> bool:
|
||||
calls["count"] += 1
|
||||
return calls["count"] > 1
|
||||
|
||||
class FakeWorkflowStore:
|
||||
def get_definition(self, definition_id: str):
|
||||
return object() if definition_id == "workflow-a" else None
|
||||
|
||||
class FakeWorkflowRunner:
|
||||
def run(self, loaded_definition, device_id: str, *, should_stop=None, stop_reason=None):
|
||||
assert should_stop is not None and should_stop()
|
||||
assert stop_reason is not None
|
||||
return SimpleNamespace(
|
||||
id="run-a", status="cancelled", failure_reason=stop_reason()
|
||||
)
|
||||
|
||||
factories = ExecutionFactories(
|
||||
task_runner_factory=lambda: object(), # type: ignore[arg-type,return-value]
|
||||
workflow_runner_factory=lambda: FakeWorkflowRunner(), # type: ignore[arg-type,return-value]
|
||||
workflow_store=FakeWorkflowStore(), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
result = AssignmentExecutor(factories).execute(
|
||||
_assignment(goal=None, workflow_definition_id="workflow-a"),
|
||||
should_stop=should_stop,
|
||||
stop_reason=lambda: "cancellation requested by control plane",
|
||||
)
|
||||
|
||||
assert result.status == "cancelled"
|
||||
assert result.metadata == {
|
||||
"workflow_run_id": "run-a",
|
||||
"workflow_status": "cancelled",
|
||||
}
|
||||
|
||||
|
||||
def test_unknown_workflow_fails_without_running() -> None:
|
||||
class FakeWorkflowStore:
|
||||
def get_definition(self, definition_id: str):
|
||||
|
||||
Reference in New Issue
Block a user