test_datasource_pools.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. """Opt-in acceptance tests against isolated PostgreSQL and MySQL sources."""
  2. import os
  3. import threading
  4. from concurrent.futures import ThreadPoolExecutor
  5. import pytest
  6. from sqlalchemy import text
  7. from sqlalchemy.engine import make_url
  8. from app.config.config import datasource_pool_settings
  9. from app.core.data_source.adapters import adapter_for
  10. from app.core.data_source.errors import (
  11. DataSourceNotFound,
  12. DataSourceReadOnlyViolation,
  13. )
  14. from app.core.data_source.manager import DataSourceConnectionManager
  15. from app.core.data_source.models import (
  16. DataSourceCredential,
  17. DataSourceDefinition,
  18. )
  19. from app.core.data_source.pool_registry import PoolRegistry
  20. SOURCE_URL_ENVIRONMENTS = (
  21. ("postgresql", "TEST_SOURCE_POSTGRES_URL"),
  22. ("mysql", "TEST_SOURCE_MYSQL_URL"),
  23. )
  24. pytestmark = pytest.mark.integration
  25. class DefinitionRepository:
  26. def __init__(self, definition):
  27. self._definitions = {definition.uid: definition}
  28. def get(self, uid):
  29. return self._definitions.get(uid)
  30. def delete(self, uid):
  31. self._definitions.pop(uid, None)
  32. class CredentialRepository:
  33. def __init__(self, credential):
  34. self.credential = credential
  35. def get_active(self, _session, _uid, _version):
  36. return self.credential
  37. class FakeClock:
  38. def __init__(self):
  39. self._value = 1000.0
  40. self._lock = threading.Lock()
  41. def __call__(self):
  42. with self._lock:
  43. return self._value
  44. def advance(self, seconds):
  45. with self._lock:
  46. self._value += seconds
  47. def _source_url(database_type, environment_name):
  48. raw = os.environ.get(environment_name)
  49. if not raw:
  50. pytest.skip(
  51. f"set {environment_name} to run the isolated {database_type} test"
  52. )
  53. return make_url(raw)
  54. def _manager(database_type, environment_name):
  55. url = _source_url(database_type, environment_name)
  56. uid = (
  57. "01900000-0000-7000-8000-000000000101"
  58. if database_type == "postgresql"
  59. else "01900000-0000-7000-8000-000000000102"
  60. )
  61. definition = DataSourceDefinition(
  62. uid=uid,
  63. name_en=f"acceptance-{database_type}",
  64. database_type=database_type,
  65. host=url.host,
  66. port=url.port,
  67. database=url.database,
  68. credential_ref=uid,
  69. credential_version=1,
  70. pool_size=2,
  71. max_overflow=3,
  72. )
  73. credential = DataSourceCredential(
  74. username=url.username,
  75. password=url.password,
  76. )
  77. definitions = DefinitionRepository(definition)
  78. clock = FakeClock()
  79. settings = datasource_pool_settings()
  80. registry = PoolRegistry(settings, clock=clock)
  81. manager = DataSourceConnectionManager(
  82. definitions=definitions,
  83. credentials=CredentialRepository(credential),
  84. platform_session=lambda: object(),
  85. adapter_resolver=adapter_for,
  86. registry=registry,
  87. settings_resolver=datasource_pool_settings,
  88. clock=clock,
  89. )
  90. return uid, definitions, clock, registry, manager
  91. @pytest.mark.parametrize(
  92. ("database_type", "environment_name"),
  93. SOURCE_URL_ENVIRONMENTS,
  94. )
  95. def test_real_source_pool_is_bounded_reused_evicted_and_recreated(
  96. database_type,
  97. environment_name,
  98. ):
  99. uid, definitions, clock, registry, manager = _manager(
  100. database_type,
  101. environment_name,
  102. )
  103. def read_customers(_index):
  104. with manager.connect(uid, purpose="dataflow_read") as connection:
  105. return tuple(
  106. connection.execute(
  107. text(
  108. "SELECT customer_name "
  109. "FROM acceptance_customers ORDER BY id"
  110. )
  111. ).scalars()
  112. )
  113. with ThreadPoolExecutor(max_workers=25) as executor:
  114. results = list(executor.map(read_customers, range(100)))
  115. assert set(results) == {("Alpha", "Beta")}
  116. status = registry.snapshot()[0]
  117. assert status.pool_size == 2
  118. assert status.checked_out == 0
  119. assert 1 < status.checked_out_peak <= 5
  120. assert status.query_total == 100
  121. with pytest.raises(DataSourceReadOnlyViolation):
  122. with manager.connect(uid, purpose="dataflow_read") as connection:
  123. connection.execute(
  124. text(
  125. "INSERT INTO acceptance_customers "
  126. "(id, customer_name) VALUES (999, 'Forbidden')"
  127. )
  128. )
  129. assert read_customers(100) == ("Alpha", "Beta")
  130. clock.advance(901)
  131. assert registry.evict_idle() == 1
  132. assert registry.snapshot() == []
  133. assert read_customers(101) == ("Alpha", "Beta")
  134. recreated = registry.snapshot()[0]
  135. assert recreated.connection_created_total == 1
  136. definitions.delete(uid)
  137. manager.invalidate(uid, "source deleted")
  138. with pytest.raises(DataSourceNotFound):
  139. with manager.connect(uid, purpose="dataflow_read"):
  140. pass
  141. manager.close()