ref #6: [feat] add impl in memory session repository

This commit is contained in:
Azamat 2026-04-02 13:12:34 +03:00
parent 87c789b7fe
commit 33ebcb1a82
4 changed files with 120 additions and 8 deletions

View file

@ -1,24 +1,34 @@
from collections.abc import Mapping
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from pathlib import Path
import docker
from docker import DockerClient
from adapter.config.loader import load_config
from adapter.config.model import AppConfig
from adapter.docker.runtime import DockerSandboxRuntime
from adapter.observability.factory import build_observability
from adapter.observability.runtime import ObservabilityRuntime
from domain.user import User
from repository.sandbox_session import InMemorySandboxSessionRepository
from repository.user import InMemoryUserRepository
from usecase.interface import Clock
from usecase.sandbox import CreateSandbox
from usecase.user import GetUser
@dataclass(frozen=True, slots=True)
class AppRepositories:
user: InMemoryUserRepository
sandbox_session: InMemorySandboxSessionRepository
@dataclass(frozen=True, slots=True)
class AppUsecases:
get_user: GetUser
create_sandbox: CreateSandbox
@dataclass(slots=True)
@ -27,16 +37,29 @@ class AppContainer:
observability: ObservabilityRuntime
repositories: AppRepositories
usecases: AppUsecases
_docker_client: DockerClient = field(repr=False)
_is_shutdown: bool = field(default=False, init=False, repr=False)
def shutdown(self) -> None:
if self._is_shutdown:
return
try:
self.observability.shutdown()
finally:
self._is_shutdown = True
self._is_shutdown = True
errors: list[Exception] = []
for action in (self._docker_client.close, self.observability.shutdown):
try:
action()
except Exception as exc:
errors.append(exc)
if errors:
raise ExceptionGroup('shutdown failed', errors)
class SystemClock(Clock):
def now(self) -> datetime:
return datetime.now(tz=UTC)
def build_container(
@ -54,17 +77,32 @@ def build_container(
)
observability = build_observability(app_config)
docker_client: DockerClient = docker.from_env()
clock = SystemClock()
user_repository = InMemoryUserRepository(
observability.tracer, [User(id='123', email='aza@gglamer.ru', name='gglamer')]
)
repositories = AppRepositories(user=user_repository)
sandbox_repository = InMemorySandboxSessionRepository()
sandbox_runtime = DockerSandboxRuntime(app_config.sandbox, docker_client)
repositories = AppRepositories(
user=user_repository,
sandbox_session=sandbox_repository,
)
usecases = AppUsecases(
get_user=GetUser(
repository=user_repository,
logger=observability.logger,
tracer=observability.tracer,
)
),
create_sandbox=CreateSandbox(
repository=sandbox_repository,
runtime=sandbox_runtime,
clock=clock,
logger=observability.logger,
ttl=timedelta(seconds=app_config.sandbox.ttl_seconds),
),
)
return AppContainer(
@ -72,4 +110,5 @@ def build_container(
observability=observability,
repositories=repositories,
usecases=usecases,
_docker_client=docker_client,
)

View file

@ -0,0 +1,28 @@
from datetime import datetime
from domain.sandbox import SandboxSession
from usecase.interface import SandboxSessionRepository
class InMemorySandboxSessionRepository(SandboxSessionRepository):
def __init__(self) -> None:
self._sessions_by_chat_id: dict[str, SandboxSession] = {}
def get_active_by_chat_id(self, chat_id: str) -> SandboxSession | None:
return self._sessions_by_chat_id.get(chat_id)
def list_expired(self, now: datetime) -> list[SandboxSession]:
return [
session
for session in self._sessions_by_chat_id.values()
if session.expires_at <= now
]
def save(self, session: SandboxSession) -> None:
self._sessions_by_chat_id[session.chat_id] = session
def delete(self, session_id: str) -> None:
for chat_id, session in tuple(self._sessions_by_chat_id.items()):
if session.session_id == session_id:
del self._sessions_by_chat_id[chat_id]
return

View file

@ -64,7 +64,7 @@
### M04. In-memory session repository и usecase `CreateSandbox`
- Субагент: `feature-developer`
- Статус: pending
- Статус: completed
- Зависимости: `M01`, `M02`, `M03`
- Commit required: no
- Scope: реализовать in-memory registry активных sandbox-сессий и usecase создания sandbox с логикой reuse по `chat_id`

View file

@ -1,5 +1,6 @@
from dataclasses import dataclass
from datetime import timedelta
from uuid import uuid4
from domain.sandbox import SandboxSession
from usecase.interface import Clock, Logger, SandboxRuntime, SandboxSessionRepository
@ -26,7 +27,47 @@ class CreateSandbox:
self._ttl = ttl
def execute(self, command: CreateSandboxCommand) -> SandboxSession:
raise NotImplementedError
now = self._clock.now()
session = self._repository.get_active_by_chat_id(command.chat_id)
if session is not None and session.expires_at > now:
self._logger.info(
'sandbox_reused',
attrs={
'chat_id': command.chat_id,
'session_id': session.session_id,
'container_id': session.container_id,
},
)
return session
if session is not None:
self._logger.info(
'sandbox_replaced',
attrs={
'chat_id': command.chat_id,
'session_id': session.session_id,
'container_id': session.container_id,
},
)
self._runtime.stop(session.container_id)
self._repository.delete(session.session_id)
new_session = self._runtime.create(
session_id=_new_session_id(),
chat_id=command.chat_id,
expires_at=now + self._ttl,
)
self._repository.save(new_session)
self._logger.info(
'sandbox_created',
attrs={
'chat_id': command.chat_id,
'session_id': new_session.session_id,
'container_id': new_session.container_id,
},
)
return new_session
class CleanupExpiredSandboxes:
@ -44,3 +85,7 @@ class CleanupExpiredSandboxes:
def execute(self) -> list[SandboxSession]:
raise NotImplementedError
def _new_session_id() -> str:
return uuid4().hex