Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/stress-test-mcp-server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
KAI_DB_DSN: "postgresql+asyncpg://kai_user:kai_password@localhost:5432/kai_test_db"
KAI_LLM_PARAMS: '{"model": "fake", "responses": ["Test response"]}'
MCP_SERVER_URL: "http://localhost:8000"
NUM_CONCURRENT_CLIENTS: ${{ github.event.inputs.num_clients || '100' }}
NUM_CONCURRENT_CLIENTS: ${{ github.event.inputs.num_clients || '200' }}
run: |
echo "Starting MCP server connected to PostgreSQL..."
uv run python -m kai_mcp_solution_server --transport streamable-http --host 0.0.0.0 --port 8000 &
Expand Down
1 change: 1 addition & 0 deletions kai_mcp_solution_server/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ run-podman: build
podman-postgres: build
@echo "Starting MCP solution server with PostgreSQL using podman-compose..."
@if [ -z "$(KAI_LLM_PARAMS)" ]; then echo "Error: KAI_LLM_PARAMS is required"; exit 1; fi
@cd tools/deploy && \
IMAGE=$(IMAGE) KAI_LLM_PARAMS='$(KAI_LLM_PARAMS)' MOUNT_PATH='$(MOUNT_PATH)' \
podman-compose up --force-recreate

Expand Down
20 changes: 19 additions & 1 deletion kai_mcp_solution_server/src/kai_mcp_solution_server/db/dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
String,
event,
func,
text,
)
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
Expand Down Expand Up @@ -110,6 +111,22 @@ async def ensure_tables_exist(engine: AsyncEngine) -> None:
await conn.run_sync(Base.metadata.create_all)


async def kill_idle_connections(engine: AsyncEngine) -> None:
"""Kill all idle connections from this application to the database."""
async with engine.begin() as conn:
await conn.execute(
text(
"""
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE application_name = 'kai-solution-server'
AND state = 'idle'
AND pid != pg_backend_pid()
"""
)
)

Comment on lines +114 to +136
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

-async def kill_idle_connections(engine: AsyncEngine) -> None:
-    """Kill all idle connections from this application to the database."""
-    async with engine.begin() as conn:
-        await conn.execute(
-            text(
-                """
-                SELECT pg_terminate_backend(pid)
-                FROM pg_stat_activity
-                WHERE application_name = 'kai-solution-server'
-                AND state = 'idle'
-                AND pid != pg_backend_pid()
-                """
-            )
-        )
+async def kill_idle_connections(engine: AsyncEngine) -> int:
+    """Kill all idle connections from this application to the database (Postgres only). Returns number terminated."""
+    if getattr(engine, "dialect", None) is None or engine.dialect.name != "postgresql":
+        return 0
+    async with engine.begin() as conn:
+        res = await conn.execute(
+            text(
+                """
+                SELECT pg_terminate_backend(pid)
+                FROM pg_stat_activity
+                WHERE application_name = 'kai-solution-server'
+                  AND state = 'idle'
+                  AND pid != pg_backend_pid()
+                """
+            )
+        )
+        # rowcount reflects number of rows returned/affected
+        return getattr(res, "rowcount", 0) or 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def kill_idle_connections(engine: AsyncEngine) -> None:
"""Kill all idle connections from this application to the database."""
async with engine.begin() as conn:
await conn.execute(
text(
"""
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE application_name = 'kai-solution-server'
AND state = 'idle'
AND pid != pg_backend_pid()
"""
)
)
async def kill_idle_connections(engine: AsyncEngine) -> int:
"""Kill all idle connections from this application to the database (Postgres only). Returns number terminated."""
if getattr(engine, "dialect", None) is None or engine.dialect.name != "postgresql":
return 0
async with engine.begin() as conn:
res = await conn.execute(
text(
"""
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE application_name = 'kai-solution-server'
AND state = 'idle'
AND pid != pg_backend_pid()
"""
)
)
# rowcount reflects number of rows returned/affected
return getattr(res, "rowcount", 0) or 0


async def get_async_engine(url: URL | str) -> AsyncEngine:
# Convert to string if URL object
url_str = str(url)
Expand All @@ -133,10 +150,11 @@ async def get_async_engine(url: URL | str) -> AsyncEngine:
url,
pool_size=20, # Base connections maintained in pool
max_overflow=80, # Additional connections created as needed (total max = 100)
pool_timeout=30, # Timeout waiting for a connection from pool
pool_timeout=60, # Timeout waiting for a connection from pool
pool_recycle=3600, # Recycle connections after 1 hour
pool_pre_ping=True, # Test connections before using
echo_pool=False, # Set to True for debugging connection pool
pool_reset_on_return="rollback", # Reset connections on return to pool
)

@event.listens_for(engine.sync_engine, "connect")
Expand Down
Loading
Loading