-
Notifications
You must be signed in to change notification settings - Fork 53
🐛 Avoid creating unneeded extra DB connections, and add a semaphore to limit concurrent operations #878
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
🐛 Avoid creating unneeded extra DB connections, and add a semaphore to limit concurrent operations #878
Changes from 3 commits
01d8717
7e484e5
69d58d5
88d02fa
980f54a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,7 +14,9 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import BaseModel, model_validator | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from sqlalchemy import URL, and_, make_url, or_, select | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from sqlalchemy.ext.asyncio import async_sessionmaker | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from sqlalchemy.exc import DBAPIError, IntegrityError, OperationalError | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from sqlalchemy.exc import TimeoutError as SQLAlchemyTimeoutError | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker | ||||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| from kai_mcp_solution_server.analyzer_types import ExtendedIncident | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from kai_mcp_solution_server.ast_diff.parser import Language, extract_ast_info | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -26,6 +28,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| DBSolution, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| DBViolation, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| get_async_engine, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| kill_idle_connections, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from kai_mcp_solution_server.db.python_objects import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| SolutionFile, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -36,6 +39,54 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def with_db_recovery(func): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Decorator to execute database operations with automatic recovery on connection errors. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Uses a semaphore to limit concurrent DB operations and prevent pool exhaustion. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Implements exponential backoff with retry on connection errors. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def wrapper(kai_ctx: KaiSolutionServerContext, *args, **kwargs): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _SharedResources.db_semaphore is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise RuntimeError("Database semaphore not initialized") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Semaphore ensures we don't overwhelm the connection pool | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async with _SharedResources.db_semaphore: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| max_retries = 3 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| base_delay = 0.1 # 100ms base delay | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| for attempt in range(max_retries): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return await func(kai_ctx, *args, **kwargs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| except IntegrityError: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| except SQLAlchemyTimeoutError: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if attempt < max_retries - 1: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| delay = base_delay * (2**attempt) # Exponential backoff | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Connection pool timeout (attempt {attempt + 1}), retrying in {delay}s..." | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await asyncio.sleep(delay) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _recover_from_db_error() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log(f"Connection pool exhausted after {max_retries} attempts") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| except (DBAPIError, OperationalError) as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+72
to
+83
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refresh per-request context after recovering from pool timeouts. On except SQLAlchemyTimeoutError:
if attempt < max_retries - 1:
delay = base_delay * (2**attempt) # Exponential backoff
log(
f"Connection pool timeout (attempt {attempt + 1}), retrying in {delay}s..."
)
await asyncio.sleep(delay)
- await _recover_from_db_error()
+ await _recover_from_db_error()
+ await kai_ctx.create()
else:
log(f"Connection pool exhausted after {max_retries} attempts")
raise📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if attempt < max_retries - 1: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| delay = base_delay * (2**attempt) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Database error (attempt {attempt + 1}): {e}, retrying in {delay}s..." | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await asyncio.sleep(delay) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _recover_from_db_error() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await kai_ctx.create() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log(f"Database error after {max_retries} attempts, giving up") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return wrapper | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| class SolutionServerSettings(BaseSettings): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_config = SettingsConfigDict(env_prefix="kai_") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -108,37 +159,109 @@ def validate_db_dsn(cls, data: Any) -> Any: | |||||||||||||||||||||||||||||||||||||||||||||||||||
| return data | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| class KaiSolutionServerContext: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self, settings: SolutionServerSettings) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.settings = settings | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.lock = asyncio.Lock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| class _SharedResources: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Global shared resources initialized once at module level.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine: AsyncEngine | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| session_maker: async_sessionmaker | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model: BaseChatModel | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| initialization_lock: asyncio.Lock | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| initialized: bool = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Semaphore to limit concurrent DB operations (prevent connection pool exhaustion) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| db_semaphore: asyncio.Semaphore | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| max_concurrent_ops: int = 80 # Allow up to 80 concurrent DB operations | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def _initialize_shared_resources() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Initialize shared resources once, protected by a lock.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _SharedResources.initialization_lock is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _SharedResources.initialization_lock = asyncio.Lock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| async with _SharedResources.initialization_lock: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _SharedResources.initialized: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def create(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from kai_mcp_solution_server.db.dao import Base | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.engine = await get_async_engine(self.settings.db_dsn) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Initializing shared database engine and model (once for all connections)..." | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| settings = SolutionServerSettings() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Initialize semaphore to limit concurrent DB operations | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _SharedResources.db_semaphore = asyncio.Semaphore( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _SharedResources.max_concurrent_ops | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"DB operation semaphore initialized with limit: {_SharedResources.max_concurrent_ops}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Ensure tables exist (safe - only creates if not already there) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async with self.engine.begin() as conn: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Initialize database engine | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _SharedResources.engine = await get_async_engine(settings.db_dsn) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async with _SharedResources.engine.begin() as conn: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await conn.run_sync(Base.metadata.create_all) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.session_maker = async_sessionmaker( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| bind=self.engine, expire_on_commit=False | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _SharedResources.session_maker = async_sessionmaker( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| bind=_SharedResources.engine, expire_on_commit=False | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.model: BaseChatModel | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.settings.llm_params is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Initialize model | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if settings.llm_params is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("LLM parameters must be provided in the settings.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif self.settings.llm_params.get("model") == "fake": | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| llm_params = self.settings.llm_params.copy() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif settings.llm_params.get("model") == "fake": | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| llm_params = settings.llm_params.copy() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| llm_params.pop("model", None) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "responses" not in llm_params: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| llm_params["responses"] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fake response", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.model = FakeListChatModel(**llm_params) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| llm_params["responses"] = ["fake response"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _SharedResources.model = FakeListChatModel(**llm_params) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.model = init_chat_model(**self.settings.llm_params) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _SharedResources.model = init_chat_model(**settings.llm_params) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| _SharedResources.initialized = True | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log("Shared resources initialized successfully") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def _recover_from_db_error() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Recover from database errors by killing idle connections or recreating engine.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _SharedResources.engine is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log("Recovering from database error - killing idle connections...") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await kill_idle_connections(_SharedResources.engine) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log("Successfully killed idle connections") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log(f"Failed to kill idle connections: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log("Disposing and recreating engine...") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _SharedResources.engine.dispose() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _SharedResources.initialized = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _initialize_shared_resources() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| class KaiSolutionServerContext: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Per-connection context that references shared resources.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self, settings: SolutionServerSettings) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.settings = settings | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.lock = asyncio.Lock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # References to shared resources (set in create()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.engine: AsyncEngine | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.session_maker: async_sessionmaker | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.model: BaseChatModel | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def create(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Initialize shared resources if needed and reference them.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _initialize_shared_resources() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _SharedResources.engine is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise RuntimeError("Database engine failed to initialize") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _SharedResources.session_maker is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise RuntimeError("Session maker failed to initialize") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _SharedResources.model is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise RuntimeError("Model failed to initialize") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| log(f"Connection using shared engine: {id(_SharedResources.engine)}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.engine = _SharedResources.engine | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.session_maker = _SharedResources.session_maker | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.model = _SharedResources.model | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @asynccontextmanager | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -157,15 +280,8 @@ async def kai_solution_server_lifespan( | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield ctx | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| log(f"Error in lifespan: {traceback.format_exc()}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise e | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Clean up database connections when client disconnects | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "ctx" in locals() and hasattr(ctx, "engine"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log("Disposing database engine...") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await ctx.engine.dispose() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log("Database engine disposed") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcp: FastMCP[KaiSolutionServerContext] = FastMCP( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -178,6 +294,7 @@ class CreateIncidentResult(BaseModel): | |||||||||||||||||||||||||||||||||||||||||||||||||||
| solution_id: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @with_db_recovery | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def create_incident( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| kai_ctx: KaiSolutionServerContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| client_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -265,6 +382,7 @@ async def tool_create_multiple_incidents( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| return results | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @with_db_recovery | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def create_solution( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| kai_ctx: KaiSolutionServerContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| client_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -394,6 +512,7 @@ async def tool_create_solution( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @with_db_recovery | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def generate_hint_v1( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| kai_ctx: KaiSolutionServerContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| client_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -458,6 +577,7 @@ async def generate_hint_v1( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| await session.flush() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @with_db_recovery | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def generate_hint_v2( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| kai_ctx: KaiSolutionServerContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| client_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -536,6 +656,7 @@ async def generate_hint_v2( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| await session.flush() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @with_db_recovery | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def generate_hint_v3( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| kai_ctx: KaiSolutionServerContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| client_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -626,6 +747,7 @@ async def generate_hint_v3( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| await session.flush() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @with_db_recovery | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def delete_solution( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| kai_ctx: KaiSolutionServerContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| client_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -665,6 +787,7 @@ class GetBestHintResult(BaseModel): | |||||||||||||||||||||||||||||||||||||||||||||||||||
| hint_id: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @with_db_recovery | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def get_best_hint( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| kai_ctx: KaiSolutionServerContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ruleset_name: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -727,6 +850,7 @@ class SuccessRateMetric(BaseModel): | |||||||||||||||||||||||||||||||||||||||||||||||||||
| unknown_solutions: int = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @with_db_recovery | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def get_success_rate( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| kai_ctx: KaiSolutionServerContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| violation_ids: list[ViolationID], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -809,6 +933,7 @@ async def tool_get_success_rate( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @with_db_recovery | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def accept_file( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| kai_ctx: KaiSolutionServerContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| client_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -897,6 +1022,7 @@ async def tool_accept_file( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @with_db_recovery | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def reject_file( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| kai_ctx: KaiSolutionServerContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| client_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard Postgres-only termination and return affected count
Kill query is Postgres-specific; on SQLite/MySQL it will fail. Also, returning how many sessions were terminated is useful for observability.
📝 Committable suggestion