-
Notifications
You must be signed in to change notification settings - Fork 53
🐛 Fix DB connection pool getting exhausted #862
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
Changes from 5 commits
c85c725
02641db
17e6fd6
4ad87a5
1a0942c
20bd53a
f20601b
76df1f1
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 |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| name: MCP Server Stress Test | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'kai_mcp_solution_server/**' | ||
| - '.github/workflows/stress-test-mcp-server.yml' | ||
| push: | ||
| branches: | ||
| - main | ||
| paths: | ||
| - 'kai_mcp_solution_server/**' | ||
| - '.github/workflows/stress-test-mcp-server.yml' | ||
| workflow_dispatch: | ||
| inputs: | ||
| num_clients: | ||
| description: 'Number of concurrent clients to test' | ||
| required: false | ||
| default: '100' | ||
|
|
||
| jobs: | ||
| stress-test-postgres: | ||
| name: Stress Test with PostgreSQL | ||
| runs-on: ubuntu-latest | ||
|
|
||
| services: | ||
| postgres: | ||
| image: postgres:16 | ||
| env: | ||
| POSTGRES_USER: kai_user | ||
| POSTGRES_PASSWORD: kai_password | ||
| POSTGRES_DB: kai_test_db | ||
| options: >- | ||
| --health-cmd pg_isready | ||
| --health-interval 10s | ||
| --health-timeout 5s | ||
| --health-retries 5 | ||
| ports: | ||
| - 5432:5432 | ||
|
|
||
| defaults: | ||
| run: | ||
| shell: bash | ||
| working-directory: ./kai_mcp_solution_server | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Python 3.12 | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.12" | ||
|
|
||
| - name: Install the latest version of uv | ||
| uses: astral-sh/setup-uv@v6 | ||
| with: | ||
| version: "latest" | ||
|
|
||
| - name: Install dependencies | ||
| run: | | ||
| uv sync | ||
| uv pip install pytest-asyncio psycopg2-binary asyncpg | ||
| - name: Run stress test with PostgreSQL backend | ||
| env: | ||
| 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' }} | ||
| 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 & | ||
| SERVER_PID=$! | ||
| # Wait for server to be ready | ||
| echo "Waiting for server to start..." | ||
| for i in {1..30}; do | ||
| if curl -s http://localhost:8000/ > /dev/null 2>&1; then | ||
| echo "Server is ready!" | ||
| break | ||
| fi | ||
| if [ $i -eq 30 ]; then | ||
| echo "Server failed to start in 30 seconds" | ||
| kill $SERVER_PID || true | ||
| exit 1 | ||
| fi | ||
| echo -n "." | ||
| sleep 1 | ||
| done | ||
| # Run the stress test | ||
| echo "" | ||
| echo "Testing with $NUM_CONCURRENT_CLIENTS concurrent clients against PostgreSQL" | ||
| make test-stress | ||
| TEST_RESULT=$? | ||
| # Stop the server | ||
| echo "Stopping MCP server..." | ||
| kill $SERVER_PID || true | ||
| exit $TEST_RESULT | ||
| timeout-minutes: 10 | ||
|
|
||
| - name: Check PostgreSQL connection count | ||
| if: always() | ||
| run: | | ||
| PGPASSWORD=kai_password psql -h localhost -U kai_user -d kai_test_db -c \ | ||
| "SELECT count(*), state FROM pg_stat_activity GROUP BY state;" | ||
|
Comment on lines
+104
to
+109
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. 🛠️ Refactor suggestion Install psql before querying pg_stat_activity Ubuntu runners may lack psql; this step can fail the job. Add this step before “Check PostgreSQL connection count”: - name: Install PostgreSQL client
run: |
sudo apt-get update
sudo apt-get install -y postgresql-client🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -86,7 +86,7 @@ run-local: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cd $(PROJECT_ROOT) && KAI_DB_DSN='$(KAI_DB_DSN)' KAI_LLM_PARAMS='$(KAI_LLM_PARAMS)' uv run python -m kai_mcp_solution_server --transport streamable-http --host 0.0.0.0 --port 8000 --mount-path=$(MOUNT_PATH) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Run with Podman for testing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Run with Podman for testing (flexible - any database via KAI_DB_DSN) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .PHONY: run-podman | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run-podman: build | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @echo "Running MCP solution server in Podman..." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -96,7 +96,57 @@ run-podman: build | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -e KAI_LLM_PARAMS='$(KAI_LLM_PARAMS)' \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -e KAI_DB_DSN='$(KAI_DB_DSN)' \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $(if $(PODMAN_ARGS),$(PODMAN_ARGS),) \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --name kai-mcp-solution-server $(IMAGE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --name kai-mcp-solution-server $(IMAGE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Convenience target: Run with SQLite | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .PHONY: podman-sqlite | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| podman-sqlite: build | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @echo "Running MCP solution server with SQLite..." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @if [ -z "$(KAI_LLM_PARAMS)" ]; then echo "Error: KAI_LLM_PARAMS is required"; exit 1; fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| podman run --rm -it -p 8000:8000 \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -e MOUNT_PATH='$(MOUNT_PATH)' \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -e KAI_LLM_PARAMS='$(KAI_LLM_PARAMS)' \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -e KAI_DB_DSN='sqlite+aiosqlite:///data/kai.db' \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $(if $(OPENAI_API_KEY),-e OPENAI_API_KEY='$(OPENAI_API_KEY)',) \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $(if $(ANTHROPIC_API_KEY),-e ANTHROPIC_API_KEY='$(ANTHROPIC_API_KEY)',) \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $(if $(AZURE_OPENAI_API_KEY),-e AZURE_OPENAI_API_KEY='$(AZURE_OPENAI_API_KEY)',) \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $(if $(AZURE_OPENAI_ENDPOINT),-e AZURE_OPENAI_ENDPOINT='$(AZURE_OPENAI_ENDPOINT)',) \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $(if $(GOOGLE_API_KEY),-e GOOGLE_API_KEY='$(GOOGLE_API_KEY)',) \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $(if $(AWS_ACCESS_KEY_ID),-e AWS_ACCESS_KEY_ID='$(AWS_ACCESS_KEY_ID)',) \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $(if $(AWS_SECRET_ACCESS_KEY),-e AWS_SECRET_ACCESS_KEY='$(AWS_SECRET_ACCESS_KEY)',) \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $(if $(AWS_REGION),-e AWS_REGION='$(AWS_REGION)',) \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $(if $(OLLAMA_HOST),-e OLLAMA_HOST='$(OLLAMA_HOST)',) \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -v kai-sqlite-data:/data:Z \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --name kai-mcp-sqlite $(IMAGE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Convenience target: Run with SQLite | |
| .PHONY: podman-sqlite | |
| podman-sqlite: build | |
| @echo "Running MCP solution server with SQLite..." | |
| @if [ -z "$(KAI_LLM_PARAMS)" ]; then echo "Error: KAI_LLM_PARAMS is required"; exit 1; fi | |
| podman run --rm -it -p 8000:8000 \ | |
| -e MOUNT_PATH='$(MOUNT_PATH)' \ | |
| -e KAI_LLM_PARAMS='$(KAI_LLM_PARAMS)' \ | |
| -e KAI_DB_DSN='sqlite+aiosqlite:///data/kai.db' \ | |
| $(if $(OPENAI_API_KEY),-e OPENAI_API_KEY='$(OPENAI_API_KEY)',) \ | |
| $(if $(ANTHROPIC_API_KEY),-e ANTHROPIC_API_KEY='$(ANTHROPIC_API_KEY)',) \ | |
| $(if $(AZURE_OPENAI_API_KEY),-e AZURE_OPENAI_API_KEY='$(AZURE_OPENAI_API_KEY)',) \ | |
| $(if $(AZURE_OPENAI_ENDPOINT),-e AZURE_OPENAI_ENDPOINT='$(AZURE_OPENAI_ENDPOINT)',) \ | |
| $(if $(GOOGLE_API_KEY),-e GOOGLE_API_KEY='$(GOOGLE_API_KEY)',) \ | |
| $(if $(AWS_ACCESS_KEY_ID),-e AWS_ACCESS_KEY_ID='$(AWS_ACCESS_KEY_ID)',) \ | |
| $(if $(AWS_SECRET_ACCESS_KEY),-e AWS_SECRET_ACCESS_KEY='$(AWS_SECRET_ACCESS_KEY)',) \ | |
| $(if $(AWS_REGION),-e AWS_REGION='$(AWS_REGION)',) \ | |
| $(if $(OLLAMA_HOST),-e OLLAMA_HOST='$(OLLAMA_HOST)',) \ | |
| -v kai-sqlite-data:/data:Z \ | |
| --name kai-mcp-sqlite $(IMAGE) | |
| # Convenience target: Run with SQLite | |
| .PHONY: podman-sqlite | |
| podman-sqlite: build | |
| @echo "Running MCP solution server with SQLite..." | |
| @if [ -z "$(KAI_LLM_PARAMS)" ]; then echo "Error: KAI_LLM_PARAMS is required"; exit 1; fi | |
| podman run --rm -it -p 8000:8000 \ | |
| -e MOUNT_PATH='$(MOUNT_PATH)' \ | |
| -e KAI_LLM_PARAMS='$(KAI_LLM_PARAMS)' \ | |
| -e KAI_DB_DSN='sqlite+aiosqlite:///data/kai.db' \ | |
| $(if $(OPENAI_API_KEY),-e OPENAI_API_KEY,) \ | |
| $(if $(ANTHROPIC_API_KEY),-e ANTHROPIC_API_KEY,) \ | |
| $(if $(AZURE_OPENAI_API_KEY),-e AZURE_OPENAI_API_KEY,) \ | |
| $(if $(AZURE_OPENAI_ENDPOINT),-e AZURE_OPENAI_ENDPOINT,) \ | |
| $(if $(GOOGLE_API_KEY),-e GOOGLE_API_KEY,) \ | |
| $(if $(AWS_ACCESS_KEY_ID),-e AWS_ACCESS_KEY_ID,) \ | |
| $(if $(AWS_SECRET_ACCESS_KEY),-e AWS_SECRET_ACCESS_KEY,) \ | |
| $(if $(AWS_REGION),-e AWS_REGION,) \ | |
| $(if $(OLLAMA_HOST),-e OLLAMA_HOST,) \ | |
| -v kai-sqlite-data:/data:Z \ | |
| --name kai-mcp-sqlite $(IMAGE) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| #!/usr/bin/env python | ||
| """ | ||
| Database management script for KAI MCP Solution Server. | ||
|
|
||
| This script provides database management commands that should be run manually, | ||
| such as dropping all tables or initializing the database schema. | ||
| """ | ||
|
|
||
| import argparse | ||
| import asyncio | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| # Add parent directory to path to import server modules | ||
| sys.path.insert(0, str(Path(__file__).parent.parent / "src")) | ||
|
|
||
| from kai_mcp_solution_server.db.dao import Base, drop_everything, get_async_engine | ||
| from kai_mcp_solution_server.server import SolutionServerSettings | ||
|
|
||
|
|
||
| async def drop_all_tables(settings: SolutionServerSettings) -> None: | ||
| """Drop all database tables.""" | ||
| print(f"Connecting to database: {settings.db_dsn}") | ||
|
|
||
| # Confirm with user | ||
| response = input( | ||
| "⚠️ WARNING: This will DROP ALL TABLES in the database. Are you sure? (yes/N): " | ||
| ) | ||
| if response.lower() != "yes": | ||
| print("Aborted.") | ||
| return | ||
|
|
||
| engine = await get_async_engine(settings.db_dsn) | ||
|
|
||
| async with engine.begin() as conn: | ||
| print("Dropping all tables...") | ||
| await conn.run_sync(drop_everything) | ||
| print("✅ All tables dropped successfully.") | ||
|
|
||
| await engine.dispose() | ||
|
|
||
|
|
||
| async def reset_database(settings: SolutionServerSettings) -> None: | ||
| """Drop all tables and recreate them (full reset).""" | ||
| print(f"Connecting to database: {settings.db_dsn}") | ||
|
|
||
| # Confirm with user | ||
| response = input( | ||
| "⚠️ WARNING: This will RESET the entire database (drop and recreate all tables). Are you sure? (yes/N): " | ||
| ) | ||
| if response.lower() != "yes": | ||
| print("Aborted.") | ||
| return | ||
|
|
||
| engine = await get_async_engine(settings.db_dsn) | ||
|
|
||
| async with engine.begin() as conn: | ||
| print("Dropping all tables...") | ||
| await conn.run_sync(drop_everything) | ||
| print("Creating tables...") | ||
| await conn.run_sync(Base.metadata.create_all) | ||
| print("✅ Database reset successfully.") | ||
|
|
||
| await engine.dispose() | ||
|
|
||
|
|
||
| async def main(): | ||
| parser = argparse.ArgumentParser( | ||
| description="Manage KAI MCP Solution Server database", | ||
| formatter_class=argparse.RawDescriptionHelpFormatter, | ||
| epilog=""" | ||
| Examples: | ||
| # Drop all tables (DANGEROUS - deletes all data) | ||
| python scripts/manage_db.py drop | ||
|
|
||
| # Reset database (drop and recreate all tables) | ||
| python scripts/manage_db.py reset | ||
|
|
||
| # Use a specific database | ||
| KAI_DB_DSN="postgresql://user:pass@localhost/dbname" python scripts/manage_db.py reset | ||
|
|
||
| # Skip confirmation prompts (use with caution!) | ||
| python scripts/manage_db.py reset --force | ||
|
|
||
| Note: The server automatically creates tables on startup, so there's no need for | ||
| an 'init' command. Use 'reset' if you need fresh tables. | ||
| """, | ||
| ) | ||
|
|
||
| parser.add_argument("command", choices=["drop", "reset"], help="Command to execute") | ||
|
|
||
| parser.add_argument( | ||
| "--force", | ||
| action="store_true", | ||
| help="Skip confirmation prompts (use with caution!)", | ||
| ) | ||
|
|
||
| args = parser.parse_args() | ||
|
|
||
| # Load settings from environment | ||
| try: | ||
| settings = SolutionServerSettings() | ||
| except Exception as e: | ||
| print(f"❌ Error loading settings: {e}") | ||
| print("\nMake sure to set required environment variables:") | ||
| print(" - KAI_DB_DSN: Database connection string") | ||
| print(' - KAI_LLM_PARAMS: LLM configuration (can be \'{"model": "fake"}\')') | ||
| sys.exit(1) | ||
|
|
||
| # Override confirmation if --force is used | ||
| if args.force: | ||
| # Monkey-patch input to always return "yes" | ||
| import builtins | ||
|
|
||
| builtins.input = lambda _: "yes" | ||
|
|
||
| try: | ||
| if args.command == "drop": | ||
| await drop_all_tables(settings) | ||
| elif args.command == "reset": | ||
| await reset_database(settings) | ||
| except Exception as e: | ||
| print(f"❌ Error: {e}") | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(main()) |
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.
🛠️ Refactor suggestion
Harden server lifecycle: fail-fast, trap, and wait on exit
Prevents orphaned background server and ensures proper failure propagation.
Apply this diff:
📝 Committable suggestion
🤖 Prompt for AI Agents