feat(cloud-scheduler): reap expired leases

This commit is contained in:
2026-07-12 17:32:11 +08:00
parent e1af4403f5
commit c58d9e5316
3 changed files with 242 additions and 1 deletions
+188
View File
@@ -957,3 +957,191 @@ def test_stale_foreign_or_expired_result_is_rejected(
assert task.terminal_result is None
finally:
database.close()
@pytest.mark.parametrize("claimed", [False, True], ids=["assigned", "dispatched"])
def test_expired_lease_requeues_with_auditable_history(
database_url: str,
claimed: bool,
) -> None:
database = CloudDatabase(database_url)
host_id = _unique_id("requeue-host")
device_id = _unique_id("requeue-device")
task_id = _unique_id("requeue-task")
now = datetime(2026, 7, 12, 14, 0, tzinfo=UTC)
expired_at = now + timedelta(seconds=10)
reaped_at = expired_at + timedelta(seconds=1)
try:
database.repository.upsert_host(host_id, address=None, last_seen_at=now)
database.repository.replace_host_devices(
host_id,
[_device(device_id, host_id)],
)
database.repository.enqueue_task(
ScheduledTask(
id=task_id,
goal="retry after expiry",
workflow_definition_id=None,
constraints=TaskConstraints(),
created_at=now,
)
)
database.repository.assign_task(
task_id=task_id,
host_id=host_id,
device_id=device_id,
lease_id="expired-attempt-1",
lease_expires_at=expired_at,
now=now,
)
if claimed:
database.repository.claim_assignment(host_id=host_id, now=now)
reaped_task_ids = database.repository.reap_expired_leases(
now=reaped_at,
max_attempts=2,
)
assert task_id in reaped_task_ids
task = database.repository.get_task(task_id)
assert task is not None
assert task.status == "queued"
assert task.attempt_count == 1
assert task.assigned_host_id is None
assert task.assigned_device_id is None
assert task.lease_id is None
assert task.lease_expires_at is None
assert task.failure_reason is None
attempts = database.repository.list_task_attempts(task_id)
assert len(attempts) == 1
assert attempts[0].status == "expired"
assert attempts[0].failure_reason == "lease expired"
assert attempts[0].completed_at == reaped_at
assert device_id not in database.repository.list_reserved_device_ids(
now=reaped_at
)
second_assignment = database.repository.assign_task(
task_id=task_id,
host_id=host_id,
device_id=device_id,
lease_id="active-attempt-2",
lease_expires_at=reaped_at + timedelta(minutes=1),
now=reaped_at,
)
assert second_assignment is not None
assert second_assignment.attempt == 2
assert [
attempt.status
for attempt in database.repository.list_task_attempts(task_id)
] == ["expired", "assigned"]
finally:
database.close()
def test_expired_lease_fails_at_attempt_limit(database_url: str) -> None:
database = CloudDatabase(database_url)
host_id = _unique_id("limit-host")
device_id = _unique_id("limit-device")
task_id = _unique_id("limit-task")
now = datetime(2026, 7, 12, 15, 0, tzinfo=UTC)
expired_at = now + timedelta(seconds=10)
reaped_at = expired_at + timedelta(seconds=1)
try:
database.repository.upsert_host(host_id, address=None, last_seen_at=now)
database.repository.replace_host_devices(
host_id,
[_device(device_id, host_id)],
)
database.repository.enqueue_task(
ScheduledTask(
id=task_id,
goal="fail after expiry",
workflow_definition_id=None,
constraints=TaskConstraints(),
created_at=now,
)
)
database.repository.assign_task(
task_id=task_id,
host_id=host_id,
device_id=device_id,
lease_id="final-attempt",
lease_expires_at=expired_at,
now=now,
)
reaped_task_ids = database.repository.reap_expired_leases(
now=reaped_at,
max_attempts=1,
)
assert task_id in reaped_task_ids
task = database.repository.get_task(task_id)
assert task is not None
assert task.status == "failed"
assert task.failure_reason == "lease expired after 1 attempts"
assert task.lease_id is None
assert task.lease_expires_at is None
attempts = database.repository.list_task_attempts(task_id)
assert attempts[0].status == "expired"
assert device_id not in database.repository.list_reserved_device_ids(
now=reaped_at
)
assert (
database.repository.reap_expired_leases(
now=reaped_at,
max_attempts=1,
)
== []
)
finally:
database.close()
def test_unexpired_lease_is_not_reaped(database_url: str) -> None:
database = CloudDatabase(database_url)
host_id = _unique_id("active-reaper-host")
device_id = _unique_id("active-reaper-device")
task_id = _unique_id("active-reaper-task")
now = datetime(2026, 7, 12, 16, 0, tzinfo=UTC)
try:
database.repository.upsert_host(host_id, address=None, last_seen_at=now)
database.repository.replace_host_devices(
host_id,
[_device(device_id, host_id)],
)
database.repository.enqueue_task(
ScheduledTask(
id=task_id,
goal="stay active",
workflow_definition_id=None,
constraints=TaskConstraints(),
created_at=now,
)
)
database.repository.assign_task(
task_id=task_id,
host_id=host_id,
device_id=device_id,
lease_id="active-reaper-lease",
lease_expires_at=now + timedelta(minutes=1),
now=now,
)
assert (
database.repository.reap_expired_leases(
now=now + timedelta(seconds=30),
max_attempts=2,
)
== []
)
task = database.repository.get_task(task_id)
assert task is not None
assert task.status == "assigned"
assert device_id in database.repository.list_reserved_device_ids(now=now)
finally:
database.close()