ref #6: [feat] add impl in memory session repository
This commit is contained in:
parent
87c789b7fe
commit
33ebcb1a82
4 changed files with 120 additions and 8 deletions
|
|
@ -1,24 +1,34 @@
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import docker
|
||||||
|
from docker import DockerClient
|
||||||
|
|
||||||
from adapter.config.loader import load_config
|
from adapter.config.loader import load_config
|
||||||
from adapter.config.model import AppConfig
|
from adapter.config.model import AppConfig
|
||||||
|
from adapter.docker.runtime import DockerSandboxRuntime
|
||||||
from adapter.observability.factory import build_observability
|
from adapter.observability.factory import build_observability
|
||||||
from adapter.observability.runtime import ObservabilityRuntime
|
from adapter.observability.runtime import ObservabilityRuntime
|
||||||
from domain.user import User
|
from domain.user import User
|
||||||
|
from repository.sandbox_session import InMemorySandboxSessionRepository
|
||||||
from repository.user import InMemoryUserRepository
|
from repository.user import InMemoryUserRepository
|
||||||
|
from usecase.interface import Clock
|
||||||
|
from usecase.sandbox import CreateSandbox
|
||||||
from usecase.user import GetUser
|
from usecase.user import GetUser
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class AppRepositories:
|
class AppRepositories:
|
||||||
user: InMemoryUserRepository
|
user: InMemoryUserRepository
|
||||||
|
sandbox_session: InMemorySandboxSessionRepository
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class AppUsecases:
|
class AppUsecases:
|
||||||
get_user: GetUser
|
get_user: GetUser
|
||||||
|
create_sandbox: CreateSandbox
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
|
|
@ -27,16 +37,29 @@ class AppContainer:
|
||||||
observability: ObservabilityRuntime
|
observability: ObservabilityRuntime
|
||||||
repositories: AppRepositories
|
repositories: AppRepositories
|
||||||
usecases: AppUsecases
|
usecases: AppUsecases
|
||||||
|
_docker_client: DockerClient = field(repr=False)
|
||||||
_is_shutdown: bool = field(default=False, init=False, repr=False)
|
_is_shutdown: bool = field(default=False, init=False, repr=False)
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
if self._is_shutdown:
|
if self._is_shutdown:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
self._is_shutdown = True
|
||||||
self.observability.shutdown()
|
errors: list[Exception] = []
|
||||||
finally:
|
|
||||||
self._is_shutdown = True
|
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(
|
def build_container(
|
||||||
|
|
@ -54,17 +77,32 @@ def build_container(
|
||||||
)
|
)
|
||||||
|
|
||||||
observability = build_observability(app_config)
|
observability = build_observability(app_config)
|
||||||
|
docker_client: DockerClient = docker.from_env()
|
||||||
|
clock = SystemClock()
|
||||||
|
|
||||||
user_repository = InMemoryUserRepository(
|
user_repository = InMemoryUserRepository(
|
||||||
observability.tracer, [User(id='123', email='aza@gglamer.ru', name='gglamer')]
|
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(
|
usecases = AppUsecases(
|
||||||
get_user=GetUser(
|
get_user=GetUser(
|
||||||
repository=user_repository,
|
repository=user_repository,
|
||||||
logger=observability.logger,
|
logger=observability.logger,
|
||||||
tracer=observability.tracer,
|
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(
|
return AppContainer(
|
||||||
|
|
@ -72,4 +110,5 @@ def build_container(
|
||||||
observability=observability,
|
observability=observability,
|
||||||
repositories=repositories,
|
repositories=repositories,
|
||||||
usecases=usecases,
|
usecases=usecases,
|
||||||
|
_docker_client=docker_client,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
28
repository/sandbox_session.py
Normal file
28
repository/sandbox_session.py
Normal 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
|
||||||
2
tasks.md
2
tasks.md
|
|
@ -64,7 +64,7 @@
|
||||||
### M04. In-memory session repository и usecase `CreateSandbox`
|
### M04. In-memory session repository и usecase `CreateSandbox`
|
||||||
|
|
||||||
- Субагент: `feature-developer`
|
- Субагент: `feature-developer`
|
||||||
- Статус: pending
|
- Статус: completed
|
||||||
- Зависимости: `M01`, `M02`, `M03`
|
- Зависимости: `M01`, `M02`, `M03`
|
||||||
- Commit required: no
|
- Commit required: no
|
||||||
- Scope: реализовать in-memory registry активных sandbox-сессий и usecase создания sandbox с логикой reuse по `chat_id`
|
- Scope: реализовать in-memory registry активных sandbox-сессий и usecase создания sandbox с логикой reuse по `chat_id`
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from domain.sandbox import SandboxSession
|
from domain.sandbox import SandboxSession
|
||||||
from usecase.interface import Clock, Logger, SandboxRuntime, SandboxSessionRepository
|
from usecase.interface import Clock, Logger, SandboxRuntime, SandboxSessionRepository
|
||||||
|
|
@ -26,7 +27,47 @@ class CreateSandbox:
|
||||||
self._ttl = ttl
|
self._ttl = ttl
|
||||||
|
|
||||||
def execute(self, command: CreateSandboxCommand) -> SandboxSession:
|
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:
|
class CleanupExpiredSandboxes:
|
||||||
|
|
@ -44,3 +85,7 @@ class CleanupExpiredSandboxes:
|
||||||
|
|
||||||
def execute(self) -> list[SandboxSession]:
|
def execute(self) -> list[SandboxSession]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def _new_session_id() -> str:
|
||||||
|
return uuid4().hex
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue