mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 04:40:57 +00:00
fix(cloud): enforce instance capacity ceilings
This commit is contained in:
@@ -121,6 +121,43 @@ async def test_postgresql_manager_applies_explicit_bounded_pool_options(monkeypa
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cloud_postgresql_manager_applies_bounded_server_timeouts(monkeypatch) -> None:
|
||||
captured_options = None
|
||||
|
||||
def create_engine(_url, **options):
|
||||
nonlocal captured_options
|
||||
captured_options = options
|
||||
return object()
|
||||
|
||||
monkeypatch.setattr(postgresql.sqlalchemy_asyncio, 'create_async_engine', create_engine)
|
||||
ap = SimpleNamespace(
|
||||
instance_config=SimpleNamespace(
|
||||
data={
|
||||
'database': {
|
||||
'postgresql': {
|
||||
'statement_timeout_ms': 45_000,
|
||||
'lock_timeout_ms': 4_000,
|
||||
'idle_in_transaction_session_timeout_ms': 55_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
manager = postgresql.PostgreSQLDatabaseManager(ap)
|
||||
manager.persistence_mode = 'cloud_runtime'
|
||||
await manager.initialize()
|
||||
|
||||
assert captured_options['connect_args'] == {
|
||||
'server_settings': {
|
||||
'statement_timeout': '45000',
|
||||
'lock_timeout': '4000',
|
||||
'idle_in_transaction_session_timeout': '55000',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
('name', 'value'),
|
||||
@@ -128,8 +165,12 @@ async def test_postgresql_manager_applies_explicit_bounded_pool_options(monkeypa
|
||||
('pool_size', 0),
|
||||
('pool_size', True),
|
||||
('max_overflow', -1),
|
||||
('pool_size', 101),
|
||||
('max_overflow', 101),
|
||||
('pool_timeout_seconds', 0),
|
||||
('pool_timeout_seconds', 301),
|
||||
('pool_recycle_seconds', '1800'),
|
||||
('pool_recycle_seconds', 86401),
|
||||
],
|
||||
)
|
||||
async def test_postgresql_manager_rejects_invalid_pool_options(name, value) -> None:
|
||||
@@ -139,6 +180,45 @@ async def test_postgresql_manager_rejects_invalid_pool_options(name, value) -> N
|
||||
await postgresql.PostgreSQLDatabaseManager(ap).initialize()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_postgresql_manager_rejects_combined_pool_capacity_above_hard_ceiling() -> None:
|
||||
ap = SimpleNamespace(
|
||||
instance_config=SimpleNamespace(
|
||||
data={
|
||||
'database': {
|
||||
'postgresql': {
|
||||
'pool_size': 60,
|
||||
'max_overflow': 41,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match=r'pool_size \+ max_overflow'):
|
||||
await postgresql.PostgreSQLDatabaseManager(ap).initialize()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
('name', 'value'),
|
||||
[
|
||||
('statement_timeout_ms', 0),
|
||||
('statement_timeout_ms', 300_001),
|
||||
('lock_timeout_ms', 60_001),
|
||||
('idle_in_transaction_session_timeout_ms', True),
|
||||
('idle_in_transaction_session_timeout_ms', 300_001),
|
||||
],
|
||||
)
|
||||
async def test_cloud_postgresql_manager_rejects_unsafe_server_timeouts(name, value) -> None:
|
||||
ap = SimpleNamespace(instance_config=SimpleNamespace(data={'database': {'postgresql': {name: value}}}))
|
||||
|
||||
with pytest.raises(ValueError, match=rf'database\.postgresql\.{name}'):
|
||||
manager = postgresql.PostgreSQLDatabaseManager(ap)
|
||||
manager.persistence_mode = 'cloud_runtime'
|
||||
await manager.initialize()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_postgresql_manager_rejects_non_postgresql_url_without_echoing_secret() -> None:
|
||||
ap = SimpleNamespace(
|
||||
|
||||
@@ -26,6 +26,7 @@ from langbot.pkg.persistence.tenant_uow import (
|
||||
CrossScopeTransactionError,
|
||||
PersistenceScopeKind,
|
||||
ScopedSessionTransactionError,
|
||||
TenantScopedAsyncSession,
|
||||
TenantScopedSyncSession,
|
||||
TenantScopeRequiredError,
|
||||
TenantUnitOfWork,
|
||||
@@ -37,6 +38,33 @@ from langbot.pkg.persistence.tenant_uow import (
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
async def test_tenant_uow_reports_pool_timeout_during_transaction_admission(monkeypatch) -> None:
|
||||
engine = create_async_engine('sqlite+aiosqlite:///:memory:')
|
||||
pool_timeouts = 0
|
||||
|
||||
def record_pool_timeout() -> None:
|
||||
nonlocal pool_timeouts
|
||||
pool_timeouts += 1
|
||||
|
||||
async def fail_transaction_start(self, capability):
|
||||
del self, capability
|
||||
raise sa.exc.TimeoutError('pool exhausted')
|
||||
|
||||
monkeypatch.setattr(TenantScopedAsyncSession, '_start_owned_transaction', fail_transaction_start)
|
||||
try:
|
||||
with pytest.raises(sa.exc.TimeoutError, match='pool exhausted'):
|
||||
async with TenantUnitOfWork(
|
||||
engine,
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
on_pool_timeout=record_pool_timeout,
|
||||
):
|
||||
pass
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
assert pool_timeouts == 1
|
||||
|
||||
|
||||
def _on_conflict_statement(*, update_value, update_key='value', index_element=None):
|
||||
table = sa.table('conflict_rows', sa.column('id'), sa.column('value'))
|
||||
if index_element is None:
|
||||
|
||||
Reference in New Issue
Block a user