mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
816ee85fb9
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
2.8 KiB
Python
70 lines
2.8 KiB
Python
"""Integration test for the asyncpg engine-dispose race (issue #831 / #877).
|
|
|
|
On the Postgres backend (``postgresql+asyncpg``), the "open async engine -> work
|
|
-> await engine.dispose()" cycle could crash with
|
|
``IndexError: pop from an empty deque`` raised from SQLAlchemy's async dispose
|
|
machinery as it races event-loop teardown. This made Postgres + file watcher
|
|
unusable (container restart loop).
|
|
|
|
These tests exercise the real create-engine -> query -> dispose cycle against a
|
|
live Postgres instance (testcontainers) for many iterations. The race is
|
|
timing-dependent, so we loop enough to be meaningful and assert clean
|
|
completion. They are skipped on SQLite, which is unaffected.
|
|
|
|
Run with:
|
|
BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest \
|
|
test-int/test_postgres_dispose_cycle.py -q
|
|
"""
|
|
|
|
import pytest
|
|
from sqlalchemy import text
|
|
|
|
from basic_memory import db
|
|
from basic_memory.config import DatabaseBackend
|
|
from basic_memory.db import DatabaseType, _create_postgres_engine
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_query_dispose_cycle_is_clean(app_config, db_backend):
|
|
"""Repeatedly create -> query -> dispose an asyncpg engine without crashing."""
|
|
if db_backend != "postgres":
|
|
pytest.skip("Postgres-specific test - asyncpg dispose race does not affect SQLite")
|
|
|
|
db_url = DatabaseType.get_db_url(app_config.database_path, DatabaseType.POSTGRES, app_config)
|
|
|
|
# Loop enough iterations to make the timing-dependent dispose race observable.
|
|
for _ in range(50):
|
|
engine = _create_postgres_engine(db_url, app_config)
|
|
async with engine.connect() as conn:
|
|
result = await conn.execute(text("SELECT 1"))
|
|
assert result.scalar() == 1
|
|
# This dispose is the call site that raced loop teardown in #831/#877.
|
|
await engine.dispose()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_shutdown_db_dispose_is_clean(app_config, config_manager, db_backend):
|
|
"""Repeated get_or_create_db -> query -> shutdown_db cycles complete cleanly.
|
|
|
|
Exercises the shielded ``shutdown_db`` teardown path against asyncpg. The
|
|
autouse ``cleanup_global_db_after_test`` fixture in conftest restores module
|
|
state afterwards.
|
|
"""
|
|
if db_backend != "postgres":
|
|
pytest.skip("Postgres-specific test - asyncpg dispose race does not affect SQLite")
|
|
|
|
assert app_config.database_backend == DatabaseBackend.POSTGRES
|
|
|
|
for _ in range(25):
|
|
engine, session_maker = await db.get_or_create_db(
|
|
app_config.database_path,
|
|
db_type=DatabaseType.POSTGRES,
|
|
ensure_migrations=False,
|
|
config=app_config,
|
|
)
|
|
async with db.scoped_session(session_maker) as session:
|
|
result = await session.execute(text("SELECT 1"))
|
|
assert result.scalar() == 1
|
|
# Shielded dispose - must not surface the asyncpg deque race.
|
|
await db.shutdown_db()
|