feat(cloud-store): define repository contract
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
## 2. Repository Contract And Migrations
|
||||
|
||||
- [ ] 2.1 Define the cloud repository contract for hosts, device snapshots, plugins, tasks, attempts, leases, reservations, and transactional assignment operations.
|
||||
- [x] 2.1 Define the cloud repository contract for hosts, device snapshots, plugins, tasks, attempts, leases, reservations, and transactional assignment operations.
|
||||
- [ ] 2.2 Implement SQLAlchemy models and a repository adapter that preserves existing `CloudStore` observable behavior.
|
||||
- [ ] 2.3 Add PostgreSQL and SQLite database URL support with engine/session lifecycle owned by the cloud application.
|
||||
- [ ] 2.4 Add Alembic configuration and a baseline migration that preserves existing host, device, task, and plugin data while adding lease/attempt/result fields.
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Literal, Protocol
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cloud.plugins import PluginManifest
|
||||
from cloud.pool import HostRegistration, PooledDevice
|
||||
from cloud.scheduler import ScheduledTask
|
||||
|
||||
|
||||
AttemptStatus = Literal["assigned", "dispatched", "done", "failed", "expired"]
|
||||
TerminalTaskStatus = Literal["done", "failed"]
|
||||
ResultRecordStatus = Literal["recorded", "already_recorded", "conflict"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskAttemptRecord:
|
||||
task_id: str
|
||||
attempt: int
|
||||
lease_id: str
|
||||
host_id: str
|
||||
device_id: str
|
||||
status: AttemptStatus
|
||||
lease_expires_at: datetime
|
||||
created_at: datetime
|
||||
completed_at: datetime | None = None
|
||||
failure_reason: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LeasedAssignment:
|
||||
task_id: str
|
||||
attempt: int
|
||||
lease_id: str
|
||||
lease_expires_at: datetime
|
||||
host_id: str
|
||||
device_id: str
|
||||
goal: str | None
|
||||
workflow_definition_id: str | None
|
||||
|
||||
|
||||
class CloudRepository(Protocol):
|
||||
"""Persistence port for cloud state and atomic scheduling operations."""
|
||||
|
||||
def upsert_host(
|
||||
self,
|
||||
host_id: str,
|
||||
*,
|
||||
address: str | None,
|
||||
last_seen_at: datetime,
|
||||
) -> None: ...
|
||||
|
||||
def replace_host_devices(
|
||||
self,
|
||||
host_id: str,
|
||||
devices: list[PooledDevice],
|
||||
) -> None: ...
|
||||
|
||||
def list_hosts(self) -> list[HostRegistration]: ...
|
||||
|
||||
def get_host(self, host_id: str) -> HostRegistration | None: ...
|
||||
|
||||
def list_devices(self) -> list[PooledDevice]: ...
|
||||
|
||||
def get_device(self, device_id: str) -> PooledDevice | None: ...
|
||||
|
||||
def enqueue_task(self, task: ScheduledTask) -> None: ...
|
||||
|
||||
def list_queued_tasks(self) -> list[ScheduledTask]: ...
|
||||
|
||||
def get_task(self, task_id: str) -> ScheduledTask | None: ...
|
||||
|
||||
def update_task(
|
||||
self,
|
||||
task_id: str,
|
||||
*,
|
||||
status: str | None = None,
|
||||
assigned_device_id: str | None = None,
|
||||
assigned_host_id: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
def count_queued_tasks(self) -> int: ...
|
||||
|
||||
def save_plugin(self, manifest: PluginManifest, *, wired: bool) -> None: ...
|
||||
|
||||
def list_plugins(self) -> list[tuple[PluginManifest, bool]]: ...
|
||||
|
||||
def get_plugin(self, name: str) -> tuple[PluginManifest, bool] | None: ...
|
||||
|
||||
def list_reserved_device_ids(self, *, now: datetime) -> set[str]: ...
|
||||
|
||||
def assign_task(
|
||||
self,
|
||||
*,
|
||||
task_id: str,
|
||||
host_id: str,
|
||||
device_id: str,
|
||||
lease_id: str,
|
||||
lease_expires_at: datetime,
|
||||
now: datetime,
|
||||
) -> LeasedAssignment | None: ...
|
||||
|
||||
def claim_assignment(
|
||||
self,
|
||||
*,
|
||||
host_id: str,
|
||||
now: datetime,
|
||||
) -> LeasedAssignment | None: ...
|
||||
|
||||
def renew_lease(
|
||||
self,
|
||||
*,
|
||||
task_id: str,
|
||||
attempt: int,
|
||||
lease_id: str,
|
||||
host_id: str,
|
||||
lease_expires_at: datetime,
|
||||
now: datetime,
|
||||
) -> bool: ...
|
||||
|
||||
def record_task_result(
|
||||
self,
|
||||
*,
|
||||
task_id: str,
|
||||
attempt: int,
|
||||
lease_id: str,
|
||||
host_id: str,
|
||||
status: TerminalTaskStatus,
|
||||
failure_reason: str | None,
|
||||
completed_at: datetime,
|
||||
) -> ResultRecordStatus: ...
|
||||
|
||||
def reap_expired_leases(
|
||||
self,
|
||||
*,
|
||||
now: datetime,
|
||||
max_attempts: int,
|
||||
) -> list[str]: ...
|
||||
|
||||
def list_task_attempts(self, task_id: str) -> list[TaskAttemptRecord]: ...
|
||||
|
||||
def health_check(self) -> None: ...
|
||||
|
||||
def close(self) -> None: ...
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import get_protocol_members
|
||||
|
||||
from cloud.repository import CloudRepository, LeasedAssignment, TaskAttemptRecord
|
||||
|
||||
|
||||
def test_cloud_repository_exposes_crud_and_atomic_lease_operations() -> None:
|
||||
members = get_protocol_members(CloudRepository)
|
||||
|
||||
assert {
|
||||
"upsert_host",
|
||||
"replace_host_devices",
|
||||
"list_hosts",
|
||||
"list_devices",
|
||||
"enqueue_task",
|
||||
"get_task",
|
||||
"save_plugin",
|
||||
"assign_task",
|
||||
"claim_assignment",
|
||||
"renew_lease",
|
||||
"record_task_result",
|
||||
"reap_expired_leases",
|
||||
"list_task_attempts",
|
||||
"health_check",
|
||||
"close",
|
||||
} <= members
|
||||
|
||||
|
||||
def test_repository_transfer_records_are_immutable() -> None:
|
||||
assert TaskAttemptRecord.__dataclass_params__.frozen is True
|
||||
assert LeasedAssignment.__dataclass_params__.frozen is True
|
||||
Reference in New Issue
Block a user