ref #3: [feat] add context and tasks for master-service

This commit is contained in:
Azamat 2026-04-02 12:04:47 +03:00
parent f0c4988b44
commit 7b3f82e805
10 changed files with 137 additions and 5 deletions

View file

@ -31,7 +31,7 @@ test:
uv run pytest -v
lint:
uv run ruff check .
uv run ruff check . --fix
typecheck:
uv run mypy .

View file

@ -1,5 +1,6 @@
from collections.abc import Callable
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from adapter.config.loader import load_config
@ -8,7 +9,6 @@ from adapter.di.container import AppContainer, build_container
from adapter.http.fastapi.dependencies import APP_CONFIG_STATE, APP_CONTAINER_STATE
from adapter.http.fastapi.middleware import register_middleware
from adapter.http.fastapi.routers.v1.router import router as v1_router
from fastapi import FastAPI
API_V1_PREFIX = '/api/v1'

View file

@ -1,7 +1,8 @@
from typing import cast
from adapter.di.container import AppContainer
from fastapi import Depends, Request
from adapter.di.container import AppContainer
from usecase.user import GetUser
APP_CONTAINER_STATE = 'container'

View file

@ -1,8 +1,9 @@
from time import perf_counter
from fastapi import FastAPI, Request, Response
from adapter.config.model import AppConfig
from adapter.http.fastapi.dependencies import get_container
from fastapi import FastAPI, Request, Response
def register_middleware(app: FastAPI, config: AppConfig) -> None:

View file

@ -0,0 +1,19 @@
# 006 MVP Docker Sandbox Orchestration
Context
- The service needs a first MVP for sandbox orchestration behind `/api/v1/create`.
- The first version must stay small, avoid auth, and preserve clean architecture boundaries.
Decision
- Use Docker as the outer runtime adapter for sandbox start and stop operations.
- Keep sandbox entities and errors in `domain/` and orchestration ports in `usecase/`.
- Use an in-memory session repository for the MVP instead of a central database.
- Keep one active sandbox per `chat_id` and reuse it until TTL expiry.
- Set default sandbox TTL to 300 seconds.
- Mount chat storage as `rw`, dependencies as `ro`, and lambda-tools as `ro`.
- Run expired sandbox cleanup as an in-process background loop in the HTTP app lifecycle.
Consequences
- Inner layers stay free from Docker and FastAPI details.
- The MVP is single-instance oriented and not yet suitable for multi-node coordination.
- Repository and runtime can be replaced later without changing usecase contracts.

View file

@ -16,3 +16,19 @@ class UserConflictError(UserError):
def __init__(self, email: str) -> None:
super().__init__('user_conflict')
self.email = email
class SandboxError(DomainError):
pass
class SandboxStartError(SandboxError):
def __init__(self, chat_id: str) -> None:
super().__init__('sandbox_start_failed')
self.chat_id = chat_id
class SandboxAlreadyRunningError(SandboxError):
def __init__(self, chat_id: str) -> None:
super().__init__('sandbox_already_running')
self.chat_id = chat_id

21
domain/sandbox.py Normal file
View file

@ -0,0 +1,21 @@
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
class SandboxStatus(str, Enum):
STARTING = 'starting'
RUNNING = 'running'
STOPPING = 'stopping'
STOPPED = 'stopped'
FAILED = 'failed'
@dataclass(frozen=True, slots=True)
class SandboxSession:
session_id: str
chat_id: str
container_id: str
status: SandboxStatus
created_at: datetime
expires_at: datetime

View file

@ -32,7 +32,7 @@
### M01. ADR и минимальный sandbox scaffolding
- Исполнитель: `primary-agent`
- Статус: pending
- Статус: completed
- Зависимости: нет
- Commit required: no
- Scope: зафиксировать MVP-решение в ADR и создать минимальные сущности, ошибки и usecase-контракты для sandbox orchestration

View file

@ -1,7 +1,9 @@
from collections.abc import Mapping
from datetime import datetime
from types import TracebackType
from typing import Protocol, TypeAlias
from domain.sandbox import SandboxSession
from domain.user import User
AttrValue: TypeAlias = str | int | float | bool
@ -16,6 +18,32 @@ class UserRepository(Protocol):
def save(self, user: User) -> None: ...
class SandboxSessionRepository(Protocol):
def get_active_by_chat_id(self, chat_id: str) -> SandboxSession | None: ...
def list_expired(self, now: datetime) -> list[SandboxSession]: ...
def save(self, session: SandboxSession) -> None: ...
def delete(self, session_id: str) -> None: ...
class SandboxRuntime(Protocol):
def create(
self,
*,
session_id: str,
chat_id: str,
expires_at: datetime,
) -> SandboxSession: ...
def stop(self, container_id: str) -> None: ...
class Clock(Protocol):
def now(self) -> datetime: ...
class Logger(Protocol):
def debug(self, message: str, attrs: Attrs | None = None) -> None: ...

46
usecase/sandbox.py Normal file
View file

@ -0,0 +1,46 @@
from dataclasses import dataclass
from datetime import timedelta
from domain.sandbox import SandboxSession
from usecase.interface import Clock, Logger, SandboxRuntime, SandboxSessionRepository
@dataclass(frozen=True, slots=True)
class CreateSandboxCommand:
chat_id: str
class CreateSandbox:
def __init__(
self,
repository: SandboxSessionRepository,
runtime: SandboxRuntime,
clock: Clock,
logger: Logger,
ttl: timedelta,
) -> None:
self._repository = repository
self._runtime = runtime
self._clock = clock
self._logger = logger
self._ttl = ttl
def execute(self, command: CreateSandboxCommand) -> SandboxSession:
raise NotImplementedError
class CleanupExpiredSandboxes:
def __init__(
self,
repository: SandboxSessionRepository,
runtime: SandboxRuntime,
clock: Clock,
logger: Logger,
) -> None:
self._repository = repository
self._runtime = runtime
self._clock = clock
self._logger = logger
def execute(self) -> list[SandboxSession]:
raise NotImplementedError