Files
Nexus/server/infrastructure/database/ssh_session_repo.py
T
Your Name 99201f48fd fix: MissingGreenlet error — convert all middleware to pure ASGI
Root cause: BaseHTTPMiddleware.call_next() runs the endpoint in a
separate async task, which breaks SQLAlchemy's greenlet context.
After session.commit(), the connection is released to the pool.
When session.refresh() tries to acquire a new connection for a
SELECT, it fails with MissingGreenlet because the greenlet spawn
context is not available in the call_next() task.

Fix (two-part):
1. Convert all 4 middleware classes from BaseHTTPMiddleware to pure
   ASGI middleware — eliminates call_next() entirely so the entire
   request chain runs in the same async context.
2. Set expire_on_commit=False on the session factory — after commit,
   objects retain their in-memory values instead of being expired.
   This removes the need for session.refresh() entirely.

All session.refresh() calls removed from 11 repository files.
With expire_on_commit=False, post-commit attribute access no longer
triggers lazy loads that would also fail with MissingGreenlet.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 15:24:15 +08:00

83 lines
3.1 KiB
Python

"""Nexus — SSH Session & Command Log Repository (Async SQLAlchemy)"""
from typing import Optional, List
from datetime import datetime, timezone, timedelta
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from server.domain.models import SshSession, CommandLog
class SshSessionRepositoryImpl:
def __init__(self, session: AsyncSession):
self.session = session
async def get_by_id(self, session_id: str) -> Optional[SshSession]:
result = await self.session.execute(select(SshSession).where(SshSession.id == session_id))
return result.scalar_one_or_none()
async def get_active_by_admin(self, admin_id: int) -> List[SshSession]:
result = await self.session.execute(
select(SshSession)
.where(SshSession.admin_id == admin_id, SshSession.status == "active")
.order_by(SshSession.started_at.desc())
)
return list(result.scalars().all())
async def get_by_server(self, server_id: int, limit: int = 50) -> List[SshSession]:
result = await self.session.execute(
select(SshSession)
.where(SshSession.server_id == server_id)
.order_by(SshSession.started_at.desc())
.limit(limit)
)
return list(result.scalars().all())
async def create(self, ssh_session: SshSession) -> SshSession:
self.session.add(ssh_session)
await self.session.commit()
return ssh_session
async def close_session(self, session_id: str) -> Optional[SshSession]:
ssh_session = await self.get_by_id(session_id)
if ssh_session:
ssh_session.status = "closed"
ssh_session.closed_at = datetime.now(timezone.utc)
await self.session.commit()
return ssh_session
class CommandLogRepositoryImpl:
def __init__(self, session: AsyncSession):
self.session = session
async def get_by_session(self, session_id: str, limit: int = 200) -> List[CommandLog]:
result = await self.session.execute(
select(CommandLog)
.where(CommandLog.session_id == session_id)
.order_by(CommandLog.created_at.asc())
.limit(limit)
)
return list(result.scalars().all())
async def get_by_server(self, server_id: int, limit: int = 200) -> List[CommandLog]:
result = await self.session.execute(
select(CommandLog)
.where(CommandLog.server_id == server_id)
.order_by(CommandLog.created_at.desc())
.limit(limit)
)
return list(result.scalars().all())
async def create(self, log: CommandLog) -> CommandLog:
self.session.add(log)
await self.session.commit()
return log
async def count_by_admin(self, admin_id: int, hours: int = 24) -> int:
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
result = await self.session.execute(
select(func.count(CommandLog.id))
.where(CommandLog.admin_id == admin_id, CommandLog.created_at >= cutoff)
)
return result.scalar_one() or 0