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
|
uv run pytest -v
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
uv run ruff check .
|
uv run ruff check . --fix
|
||||||
|
|
||||||
typecheck:
|
typecheck:
|
||||||
uv run mypy .
|
uv run mypy .
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
||||||
|
|
||||||
from adapter.config.loader import load_config
|
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.dependencies import APP_CONFIG_STATE, APP_CONTAINER_STATE
|
||||||
from adapter.http.fastapi.middleware import register_middleware
|
from adapter.http.fastapi.middleware import register_middleware
|
||||||
from adapter.http.fastapi.routers.v1.router import router as v1_router
|
from adapter.http.fastapi.routers.v1.router import router as v1_router
|
||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
API_V1_PREFIX = '/api/v1'
|
API_V1_PREFIX = '/api/v1'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from adapter.di.container import AppContainer
|
|
||||||
from fastapi import Depends, Request
|
from fastapi import Depends, Request
|
||||||
|
|
||||||
|
from adapter.di.container import AppContainer
|
||||||
from usecase.user import GetUser
|
from usecase.user import GetUser
|
||||||
|
|
||||||
APP_CONTAINER_STATE = 'container'
|
APP_CONTAINER_STATE = 'container'
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request, Response
|
||||||
|
|
||||||
from adapter.config.model import AppConfig
|
from adapter.config.model import AppConfig
|
||||||
from adapter.http.fastapi.dependencies import get_container
|
from adapter.http.fastapi.dependencies import get_container
|
||||||
from fastapi import FastAPI, Request, Response
|
|
||||||
|
|
||||||
|
|
||||||
def register_middleware(app: FastAPI, config: AppConfig) -> None:
|
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:
|
def __init__(self, email: str) -> None:
|
||||||
super().__init__('user_conflict')
|
super().__init__('user_conflict')
|
||||||
self.email = email
|
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
|
### M01. ADR и минимальный sandbox scaffolding
|
||||||
|
|
||||||
- Исполнитель: `primary-agent`
|
- Исполнитель: `primary-agent`
|
||||||
- Статус: pending
|
- Статус: completed
|
||||||
- Зависимости: нет
|
- Зависимости: нет
|
||||||
- Commit required: no
|
- Commit required: no
|
||||||
- Scope: зафиксировать MVP-решение в ADR и создать минимальные сущности, ошибки и usecase-контракты для sandbox orchestration
|
- Scope: зафиксировать MVP-решение в ADR и создать минимальные сущности, ошибки и usecase-контракты для sandbox orchestration
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
from datetime import datetime
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Protocol, TypeAlias
|
from typing import Protocol, TypeAlias
|
||||||
|
|
||||||
|
from domain.sandbox import SandboxSession
|
||||||
from domain.user import User
|
from domain.user import User
|
||||||
|
|
||||||
AttrValue: TypeAlias = str | int | float | bool
|
AttrValue: TypeAlias = str | int | float | bool
|
||||||
|
|
@ -16,6 +18,32 @@ class UserRepository(Protocol):
|
||||||
def save(self, user: User) -> None: ...
|
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):
|
class Logger(Protocol):
|
||||||
def debug(self, message: str, attrs: Attrs | None = None) -> None: ...
|
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