ref #3: [feat] add context and tasks for master-service
This commit is contained in:
parent
f0c4988b44
commit
7b3f82e805
10 changed files with 137 additions and 5 deletions
2
Makefile
2
Makefile
|
|
@ -31,7 +31,7 @@ test:
|
|||
uv run pytest -v
|
||||
|
||||
lint:
|
||||
uv run ruff check .
|
||||
uv run ruff check . --fix
|
||||
|
||||
typecheck:
|
||||
uv run mypy .
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
19
docs/006-mvp-docker-sandbox-orchestration.md
Normal file
19
docs/006-mvp-docker-sandbox-orchestration.md
Normal 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.
|
||||
|
|
@ -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
21
domain/sandbox.py
Normal 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
|
||||
2
tasks.md
2
tasks.md
|
|
@ -32,7 +32,7 @@
|
|||
### M01. ADR и минимальный sandbox scaffolding
|
||||
|
||||
- Исполнитель: `primary-agent`
|
||||
- Статус: pending
|
||||
- Статус: completed
|
||||
- Зависимости: нет
|
||||
- Commit required: no
|
||||
- Scope: зафиксировать MVP-решение в ADR и создать минимальные сущности, ошибки и usecase-контракты для sandbox orchestration
|
||||
|
|
|
|||
|
|
@ -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
46
usecase/sandbox.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue