| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- """Opt-in acceptance tests against isolated PostgreSQL and MySQL sources."""
- import os
- import threading
- from concurrent.futures import ThreadPoolExecutor
- import pytest
- from sqlalchemy import text
- from sqlalchemy.engine import make_url
- from app.config.config import datasource_pool_settings
- from app.core.data_source.adapters import adapter_for
- from app.core.data_source.errors import (
- DataSourceNotFound,
- DataSourceReadOnlyViolation,
- )
- from app.core.data_source.manager import DataSourceConnectionManager
- from app.core.data_source.models import (
- DataSourceCredential,
- DataSourceDefinition,
- )
- from app.core.data_source.pool_registry import PoolRegistry
- SOURCE_URL_ENVIRONMENTS = (
- ("postgresql", "TEST_SOURCE_POSTGRES_URL"),
- ("mysql", "TEST_SOURCE_MYSQL_URL"),
- )
- pytestmark = pytest.mark.integration
- class DefinitionRepository:
- def __init__(self, definition):
- self._definitions = {definition.uid: definition}
- def get(self, uid):
- return self._definitions.get(uid)
- def delete(self, uid):
- self._definitions.pop(uid, None)
- class CredentialRepository:
- def __init__(self, credential):
- self.credential = credential
- def get_active(self, _session, _uid, _version):
- return self.credential
- class FakeClock:
- def __init__(self):
- self._value = 1000.0
- self._lock = threading.Lock()
- def __call__(self):
- with self._lock:
- return self._value
- def advance(self, seconds):
- with self._lock:
- self._value += seconds
- def _source_url(database_type, environment_name):
- raw = os.environ.get(environment_name)
- if not raw:
- pytest.skip(
- f"set {environment_name} to run the isolated {database_type} test"
- )
- return make_url(raw)
- def _manager(database_type, environment_name):
- url = _source_url(database_type, environment_name)
- uid = (
- "01900000-0000-7000-8000-000000000101"
- if database_type == "postgresql"
- else "01900000-0000-7000-8000-000000000102"
- )
- definition = DataSourceDefinition(
- uid=uid,
- name_en=f"acceptance-{database_type}",
- database_type=database_type,
- host=url.host,
- port=url.port,
- database=url.database,
- credential_ref=uid,
- credential_version=1,
- pool_size=2,
- max_overflow=3,
- )
- credential = DataSourceCredential(
- username=url.username,
- password=url.password,
- )
- definitions = DefinitionRepository(definition)
- clock = FakeClock()
- settings = datasource_pool_settings()
- registry = PoolRegistry(settings, clock=clock)
- manager = DataSourceConnectionManager(
- definitions=definitions,
- credentials=CredentialRepository(credential),
- platform_session=lambda: object(),
- adapter_resolver=adapter_for,
- registry=registry,
- settings_resolver=datasource_pool_settings,
- clock=clock,
- )
- return uid, definitions, clock, registry, manager
- @pytest.mark.parametrize(
- ("database_type", "environment_name"),
- SOURCE_URL_ENVIRONMENTS,
- )
- def test_real_source_pool_is_bounded_reused_evicted_and_recreated(
- database_type,
- environment_name,
- ):
- uid, definitions, clock, registry, manager = _manager(
- database_type,
- environment_name,
- )
- def read_customers(_index):
- with manager.connect(uid, purpose="dataflow_read") as connection:
- return tuple(
- connection.execute(
- text(
- "SELECT customer_name "
- "FROM acceptance_customers ORDER BY id"
- )
- ).scalars()
- )
- with ThreadPoolExecutor(max_workers=25) as executor:
- results = list(executor.map(read_customers, range(100)))
- assert set(results) == {("Alpha", "Beta")}
- status = registry.snapshot()[0]
- assert status.pool_size == 2
- assert status.checked_out == 0
- assert 1 < status.checked_out_peak <= 5
- assert status.query_total == 100
- with pytest.raises(DataSourceReadOnlyViolation):
- with manager.connect(uid, purpose="dataflow_read") as connection:
- connection.execute(
- text(
- "INSERT INTO acceptance_customers "
- "(id, customer_name) VALUES (999, 'Forbidden')"
- )
- )
- assert read_customers(100) == ("Alpha", "Beta")
- clock.advance(901)
- assert registry.evict_idle() == 1
- assert registry.snapshot() == []
- assert read_customers(101) == ("Alpha", "Beta")
- recreated = registry.snapshot()[0]
- assert recreated.connection_created_total == 1
- definitions.delete(uid)
- manager.invalidate(uid, "source deleted")
- with pytest.raises(DataSourceNotFound):
- with manager.connect(uid, purpose="dataflow_read"):
- pass
- manager.close()
|