mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 04:40:57 +00:00
138 lines
4.8 KiB
Python
138 lines
4.8 KiB
Python
"""PostgreSQL integration coverage for durable workspace quota locking.
|
|
|
|
Run with TEST_POSTGRES_URL=postgresql+asyncpg://... pytest ...
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
import uuid
|
|
|
|
import pytest
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
|
|
from langbot.pkg.cloud import quotas as quota_module
|
|
from langbot.pkg.cloud.quotas import WorkspaceQuota, WorkspaceQuotaExceededError, require_resource_capacity
|
|
|
|
|
|
pytestmark = [pytest.mark.integration, pytest.mark.slow, pytest.mark.asyncio]
|
|
|
|
|
|
class _Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
class _Workspace(_Base):
|
|
__tablename__ = 'quota_integration_workspaces'
|
|
|
|
uuid: Mapped[str] = mapped_column(sa.String(36), primary_key=True)
|
|
|
|
|
|
class _Resource(_Base):
|
|
__tablename__ = 'quota_integration_resources'
|
|
|
|
uuid: Mapped[str] = mapped_column(sa.String(36), primary_key=True)
|
|
workspace_uuid: Mapped[str] = mapped_column(
|
|
sa.String(36),
|
|
sa.ForeignKey('quota_integration_workspaces.uuid', ondelete='CASCADE'),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def quota_postgres(monkeypatch):
|
|
url = os.environ.get('TEST_POSTGRES_URL')
|
|
if not url:
|
|
pytest.skip('TEST_POSTGRES_URL not set')
|
|
if url.startswith('postgresql://'):
|
|
url = url.replace('postgresql://', 'postgresql+asyncpg://', 1)
|
|
|
|
engine = create_async_engine(url, pool_size=5, max_overflow=0)
|
|
monkeypatch.setattr(quota_module.persistence_workspace, 'Workspace', _Workspace)
|
|
async with engine.begin() as connection:
|
|
await connection.run_sync(_Base.metadata.drop_all)
|
|
await connection.run_sync(_Base.metadata.create_all)
|
|
try:
|
|
yield url, engine
|
|
finally:
|
|
async with engine.begin() as connection:
|
|
await connection.run_sync(_Base.metadata.drop_all)
|
|
await engine.dispose()
|
|
|
|
|
|
async def test_workspace_row_lock_is_atomic_isolated_and_survives_pool_restart(quota_postgres) -> None:
|
|
url, engine = quota_postgres
|
|
workspace_a = str(uuid.uuid4())
|
|
workspace_b = str(uuid.uuid4())
|
|
quota = WorkspaceQuota(limit=1, requires_transaction_lock=True)
|
|
sessions = async_sessionmaker(engine, expire_on_commit=False)
|
|
|
|
async with engine.begin() as connection:
|
|
await connection.execute(sa.insert(_Workspace), [{'uuid': workspace_a}, {'uuid': workspace_b}])
|
|
|
|
lock_acquired = asyncio.Event()
|
|
release_first = asyncio.Event()
|
|
|
|
async def admit(workspace_uuid: str, *, hold: bool = False) -> None:
|
|
async with sessions() as session:
|
|
async with session.begin():
|
|
await require_resource_capacity(
|
|
session.execute,
|
|
workspace_uuid=workspace_uuid,
|
|
model=_Resource,
|
|
quota=quota,
|
|
resource_name='resources',
|
|
)
|
|
if hold:
|
|
lock_acquired.set()
|
|
await release_first.wait()
|
|
await session.execute(
|
|
sa.insert(_Resource).values(uuid=str(uuid.uuid4()), workspace_uuid=workspace_uuid)
|
|
)
|
|
|
|
first = asyncio.create_task(admit(workspace_a, hold=True))
|
|
await asyncio.wait_for(lock_acquired.wait(), timeout=2)
|
|
same_workspace = asyncio.create_task(admit(workspace_a))
|
|
other_workspace = asyncio.create_task(admit(workspace_b))
|
|
|
|
await asyncio.wait_for(other_workspace, timeout=2)
|
|
assert not same_workspace.done(), 'same-workspace transaction bypassed SELECT FOR UPDATE'
|
|
|
|
release_first.set()
|
|
await first
|
|
with pytest.raises(WorkspaceQuotaExceededError, match=r'Maximum number of resources \(1\) reached'):
|
|
await same_workspace
|
|
|
|
async with sessions() as session:
|
|
counts = dict(
|
|
(
|
|
await session.execute(
|
|
sa.select(_Resource.workspace_uuid, sa.func.count())
|
|
.group_by(_Resource.workspace_uuid)
|
|
.order_by(_Resource.workspace_uuid)
|
|
)
|
|
).all()
|
|
)
|
|
assert counts == {workspace_a: 1, workspace_b: 1}
|
|
|
|
await engine.dispose()
|
|
restarted_engine = create_async_engine(url, pool_size=2, max_overflow=0)
|
|
restarted_sessions = async_sessionmaker(restarted_engine, expire_on_commit=False)
|
|
try:
|
|
async with restarted_sessions() as session:
|
|
async with session.begin():
|
|
with pytest.raises(WorkspaceQuotaExceededError):
|
|
await require_resource_capacity(
|
|
session.execute,
|
|
workspace_uuid=workspace_a,
|
|
model=_Resource,
|
|
quota=quota,
|
|
resource_name='resources',
|
|
)
|
|
finally:
|
|
await restarted_engine.dispose()
|