feat: TTL enforcement and operational hardening (SAND-WP-0009)
Add TTL parser, expires_at on create, extend_ttl and expire/reap APIs, activity-core integration doc, repo classification, registry refresh, HTTP parity, and 69 tests.
This commit is contained in:
parent
b58191b23e
commit
df658e7ef9
20 changed files with 913 additions and 39 deletions
77
src/sandboxer/lifecycle/expire.py
Normal file
77
src/sandboxer/lifecycle/expire.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
"""TTL and idle-reap expiry candidate selection."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from typing import Literal
|
||||
|
||||
from sandboxer.lifecycle.store import SandboxStore
|
||||
from sandboxer.lifecycle.ttl import is_idle_expired, is_past_expiry
|
||||
from sandboxer.models import SandboxState, SandboxStatus
|
||||
from sandboxer.profiles.loader import load_profile
|
||||
|
||||
ExpireReason = Literal["ttl", "idle"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpireCandidate:
|
||||
sandbox_id: str
|
||||
profile_id: str
|
||||
reason: ExpireReason
|
||||
expires_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
_LIVE_STATES = frozenset({SandboxState.READY, SandboxState.ACTIVE})
|
||||
|
||||
|
||||
def find_expire_candidates(
|
||||
store: SandboxStore,
|
||||
*,
|
||||
now: datetime | None = None,
|
||||
) -> list[ExpireCandidate]:
|
||||
ref = now or datetime.now(UTC)
|
||||
candidates: list[ExpireCandidate] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
for status in store.list_all():
|
||||
if status.state not in _LIVE_STATES:
|
||||
continue
|
||||
|
||||
if is_past_expiry(status.expires_at, now=ref):
|
||||
candidates.append(
|
||||
ExpireCandidate(
|
||||
sandbox_id=status.sandbox_id,
|
||||
profile_id=status.profile_id,
|
||||
reason="ttl",
|
||||
expires_at=status.expires_at,
|
||||
)
|
||||
)
|
||||
seen.add(status.sandbox_id)
|
||||
continue
|
||||
|
||||
try:
|
||||
profile = load_profile(status.profile_id)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
|
||||
if is_idle_expired(status.updated_at, profile.ttl.idle_reap, now=ref):
|
||||
candidates.append(
|
||||
ExpireCandidate(
|
||||
sandbox_id=status.sandbox_id,
|
||||
profile_id=status.profile_id,
|
||||
reason="idle",
|
||||
updated_at=status.updated_at,
|
||||
)
|
||||
)
|
||||
seen.add(status.sandbox_id)
|
||||
|
||||
return sorted(candidates, key=lambda c: c.sandbox_id)
|
||||
|
||||
|
||||
def apply_expired_state(status: SandboxStatus, *, now: datetime | None = None) -> SandboxStatus:
|
||||
ref = now or datetime.now(UTC)
|
||||
status.state = SandboxState.EXPIRED
|
||||
status.updated_at = ref
|
||||
return status
|
||||
Loading…
Add table
Add a link
Reference in a new issue