Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
10 changes: 6 additions & 4 deletions openhands/automation/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
async_sessionmaker,
create_async_engine,
)
from sqlalchemy.pool import NullPool

from openhands.automation.config import ServiceSettings, get_config

Expand Down Expand Up @@ -153,7 +154,10 @@ def _create_sqlite_engine(db_url: str) -> EngineResult:

SQLite configuration notes:
- Uses aiosqlite driver for async support
- No connection pooling (SQLite handles this internally)
- Uses NullPool to disable SQLAlchemy's connection pooling;
without this, the default AsyncAdaptedQueuePool silently applies
a 5-connection pool with 10 overflow and 30 s timeout, causing
500 errors under concurrent fan-out (see issue #347).
- check_same_thread=False required for async usage
"""
# Ensure the URL uses aiosqlite driver
Expand All @@ -162,10 +166,8 @@ def _create_sqlite_engine(db_url: str) -> EngineResult:

engine = create_async_engine(
db_url,
# SQLite-specific settings
connect_args={"check_same_thread": False},
# No pooling for SQLite - it handles this internally
pool_pre_ping=True,
poolclass=NullPool,
)
logger.info("Created SQLite engine: %s", db_url.split("?")[0])
return EngineResult(engine=engine, is_sqlite=True)
Expand Down
13 changes: 13 additions & 0 deletions tests/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ def test_absolute_path(self):
result = _create_sqlite_engine("sqlite+aiosqlite:////data/automations.db")
assert result.is_sqlite is True

@pytest.mark.asyncio
async def test_uses_null_pool(self):
"""SQLite engine uses NullPool to disable connection pooling.

Without NullPool, SQLAlchemy defaults to AsyncAdaptedQueuePool
which silently imposes a 5-connection pool with 30s timeout (#347).
"""
from sqlalchemy.pool import NullPool

result = _create_sqlite_engine("sqlite+aiosqlite:///:memory:")
assert isinstance(result.engine.pool, NullPool)
await result.dispose()


class TestEngineResult:
"""Tests for EngineResult dataclass."""
Expand Down
Loading