216 lines
8.6 KiB
Python
216 lines
8.6 KiB
Python
"""Local CAS lifecycle rules for KnowledgeFS control-spaces.
|
|
|
|
P1A persists lifecycle intent only. This service deliberately has no
|
|
KnowledgeFS client or task dispatcher, so deploying it cannot emit P1B remote
|
|
traffic.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Mapping
|
|
from datetime import datetime
|
|
|
|
from models.knowledge_fs import KnowledgeFSControlSpace, KnowledgeFSControlSpaceState
|
|
from repositories.knowledge_fs_control_space_repository import (
|
|
KnowledgeFSControlSpaceCASUpdate,
|
|
KnowledgeFSControlSpaceRepository,
|
|
)
|
|
from services.knowledge_fs.observability import (
|
|
KnowledgeFSControlSpaceStateMetric,
|
|
KnowledgeFSOperationalMetricsPort,
|
|
get_knowledge_fs_operational_metrics,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class KnowledgeFSControlSpaceLifecycleError(RuntimeError):
|
|
"""Base error for a rejected local control-space lifecycle operation."""
|
|
|
|
|
|
class KnowledgeFSControlSpaceNotFoundError(KnowledgeFSControlSpaceLifecycleError):
|
|
pass
|
|
|
|
|
|
class KnowledgeFSControlSpaceVersionConflictError(KnowledgeFSControlSpaceLifecycleError):
|
|
pass
|
|
|
|
|
|
class KnowledgeFSInvalidLifecycleTransitionError(KnowledgeFSControlSpaceLifecycleError):
|
|
pass
|
|
|
|
|
|
class KnowledgeFSSpaceRegistrationConflictError(KnowledgeFSControlSpaceLifecycleError):
|
|
pass
|
|
|
|
|
|
class KnowledgeFSWorkspaceDeletionBlockedError(KnowledgeFSControlSpaceLifecycleError):
|
|
def __init__(self, control_space_ids: tuple[str, ...]):
|
|
self.control_space_ids = control_space_ids
|
|
super().__init__("Workspace deletion is blocked by non-terminal KnowledgeFS control-spaces")
|
|
|
|
|
|
_ALLOWED_TRANSITIONS: Mapping[KnowledgeFSControlSpaceState, frozenset[KnowledgeFSControlSpaceState]] = {
|
|
KnowledgeFSControlSpaceState.PROVISIONING: frozenset(
|
|
{
|
|
KnowledgeFSControlSpaceState.ACTIVE,
|
|
KnowledgeFSControlSpaceState.DELETING,
|
|
KnowledgeFSControlSpaceState.ERROR,
|
|
}
|
|
),
|
|
KnowledgeFSControlSpaceState.ACTIVE: frozenset(
|
|
{KnowledgeFSControlSpaceState.DELETING, KnowledgeFSControlSpaceState.ERROR}
|
|
),
|
|
KnowledgeFSControlSpaceState.DELETING: frozenset(
|
|
{KnowledgeFSControlSpaceState.DELETED, KnowledgeFSControlSpaceState.ERROR}
|
|
),
|
|
KnowledgeFSControlSpaceState.ERROR: frozenset(
|
|
{
|
|
KnowledgeFSControlSpaceState.PROVISIONING,
|
|
KnowledgeFSControlSpaceState.ACTIVE,
|
|
KnowledgeFSControlSpaceState.DELETING,
|
|
}
|
|
),
|
|
KnowledgeFSControlSpaceState.DELETED: frozenset(),
|
|
}
|
|
|
|
|
|
class KnowledgeFSControlSpaceLifecycleService:
|
|
"""Validate lifecycle invariants and persist transitions through a CAS."""
|
|
|
|
def __init__(
|
|
self,
|
|
repository: KnowledgeFSControlSpaceRepository,
|
|
*,
|
|
metrics: KnowledgeFSOperationalMetricsPort | None = None,
|
|
) -> None:
|
|
self._repository = repository
|
|
self._metrics = metrics or get_knowledge_fs_operational_metrics()
|
|
|
|
def transition(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
control_space_id: str,
|
|
expected_resource_version: int,
|
|
new_state: KnowledgeFSControlSpaceState,
|
|
lifecycle_operation_id: str,
|
|
knowledge_space_id: str | None = None,
|
|
knowledge_space_revision: int | None = None,
|
|
last_error_code: str | None = None,
|
|
last_error_message: str | None = None,
|
|
) -> KnowledgeFSControlSpace:
|
|
control_space = self._repository.get(tenant_id=tenant_id, control_space_id=control_space_id)
|
|
if control_space is None:
|
|
raise KnowledgeFSControlSpaceNotFoundError("KnowledgeFS control-space was not found in this tenant")
|
|
if control_space.resource_version != expected_resource_version:
|
|
raise KnowledgeFSControlSpaceVersionConflictError("KnowledgeFS control-space resource version changed")
|
|
if control_space.deletion_irreversible_at is not None and new_state in {
|
|
KnowledgeFSControlSpaceState.ACTIVE,
|
|
KnowledgeFSControlSpaceState.PROVISIONING,
|
|
}:
|
|
raise KnowledgeFSInvalidLifecycleTransitionError(
|
|
"KnowledgeFS control-space deletion is irreversible and cannot be recovered"
|
|
)
|
|
if new_state not in _ALLOWED_TRANSITIONS[control_space.state]:
|
|
raise KnowledgeFSInvalidLifecycleTransitionError(
|
|
f"Cannot transition KnowledgeFS control-space from {control_space.state} to {new_state}"
|
|
)
|
|
|
|
registered_space_id = control_space.knowledge_space_id
|
|
if registered_space_id is not None and knowledge_space_id not in {None, registered_space_id}:
|
|
raise KnowledgeFSSpaceRegistrationConflictError("KnowledgeFS Space registration is immutable")
|
|
effective_space_id = knowledge_space_id or registered_space_id
|
|
if new_state is KnowledgeFSControlSpaceState.ACTIVE and effective_space_id is None:
|
|
raise KnowledgeFSSpaceRegistrationConflictError("An active control-space must have a KnowledgeFS Space ID")
|
|
if knowledge_space_revision is not None and knowledge_space_revision < control_space.knowledge_space_revision:
|
|
raise KnowledgeFSControlSpaceVersionConflictError("KnowledgeFS Space revision moved backwards")
|
|
previous_state = control_space.state
|
|
previous_updated_at = control_space.updated_at
|
|
|
|
changed = self._repository.compare_and_set_lifecycle(
|
|
KnowledgeFSControlSpaceCASUpdate(
|
|
tenant_id=tenant_id,
|
|
control_space_id=control_space_id,
|
|
expected_resource_version=expected_resource_version,
|
|
expected_state=control_space.state,
|
|
new_state=new_state,
|
|
lifecycle_operation_id=lifecycle_operation_id,
|
|
knowledge_space_id=knowledge_space_id,
|
|
knowledge_space_revision=knowledge_space_revision,
|
|
last_error_code=last_error_code,
|
|
last_error_message=last_error_message,
|
|
)
|
|
)
|
|
if not changed:
|
|
raise KnowledgeFSControlSpaceVersionConflictError("KnowledgeFS control-space changed during transition")
|
|
|
|
transitioned = self._repository.get(tenant_id=tenant_id, control_space_id=control_space_id)
|
|
if transitioned is None:
|
|
raise KnowledgeFSControlSpaceNotFoundError("KnowledgeFS control-space disappeared after transition")
|
|
self._record_state_metric(
|
|
previous_state=previous_state,
|
|
previous_updated_at=previous_updated_at,
|
|
transitioned=transitioned,
|
|
)
|
|
return transitioned
|
|
|
|
def _record_state_metric(
|
|
self,
|
|
*,
|
|
previous_state: KnowledgeFSControlSpaceState,
|
|
previous_updated_at: datetime,
|
|
transitioned: KnowledgeFSControlSpace,
|
|
) -> None:
|
|
try:
|
|
duration_seconds = max(0.0, (transitioned.updated_at - previous_updated_at).total_seconds())
|
|
self._metrics.record_control_space_state(
|
|
KnowledgeFSControlSpaceStateMetric(
|
|
duration_seconds,
|
|
previous_state.value,
|
|
transitioned.state.value,
|
|
)
|
|
)
|
|
except Exception:
|
|
logger.warning("KnowledgeFS control-space state metric export failed", exc_info=True)
|
|
|
|
def mark_deletion_irreversible(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
control_space_id: str,
|
|
lifecycle_operation_id: str,
|
|
irreversible_at: datetime,
|
|
) -> None:
|
|
"""Persist the monotonic point after which product recovery is forbidden."""
|
|
|
|
changed = self._repository.mark_deletion_irreversible(
|
|
tenant_id=tenant_id,
|
|
control_space_id=control_space_id,
|
|
lifecycle_operation_id=lifecycle_operation_id,
|
|
irreversible_at=irreversible_at,
|
|
)
|
|
if not changed:
|
|
raise KnowledgeFSControlSpaceVersionConflictError(
|
|
"KnowledgeFS control-space changed before deletion became irreversible"
|
|
)
|
|
|
|
def assert_workspace_deletion_allowed(self, *, tenant_id: str) -> None:
|
|
"""Fence tenant deletion until every control-space reaches ``deleted``."""
|
|
|
|
blocking_ids = self._repository.list_workspace_deletion_blockers(tenant_id=tenant_id)
|
|
if blocking_ids:
|
|
raise KnowledgeFSWorkspaceDeletionBlockedError(blocking_ids)
|
|
|
|
|
|
__all__ = [
|
|
"KnowledgeFSControlSpaceLifecycleError",
|
|
"KnowledgeFSControlSpaceLifecycleService",
|
|
"KnowledgeFSControlSpaceNotFoundError",
|
|
"KnowledgeFSControlSpaceVersionConflictError",
|
|
"KnowledgeFSInvalidLifecycleTransitionError",
|
|
"KnowledgeFSSpaceRegistrationConflictError",
|
|
"KnowledgeFSWorkspaceDeletionBlockedError",
|
|
]
|