manager.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. """Public context-managed access to external business data sources."""
  2. import time
  3. from contextlib import contextmanager
  4. from sqlalchemy.exc import DBAPIError, OperationalError, TimeoutError
  5. from app.core.data_source.adapters.base import PURPOSES
  6. from app.core.data_source.errors import (
  7. DataSourceConfigurationInvalid,
  8. DataSourceConnectionFailed,
  9. DataSourceNotFound,
  10. DataSourcePoolTimeout,
  11. DataSourceQueryTimeout,
  12. DataSourceReadOnlyViolation,
  13. DataSourceWriteOutcomeUnknown,
  14. )
  15. from app.core.data_source.models import PoolKey
  16. def _driver_error_code(error):
  17. original = getattr(error, "orig", error)
  18. code = getattr(original, "pgcode", None)
  19. if code:
  20. return str(code)
  21. args = getattr(original, "args", ())
  22. return str(args[0]) if args else ""
  23. def _translate_query_error(error):
  24. code = _driver_error_code(error)
  25. if code in {"57014", "3024", "1317"}:
  26. return DataSourceQueryTimeout()
  27. if code in {"25006", "1792"}:
  28. return DataSourceReadOnlyViolation()
  29. return error
  30. class ManagedConnection:
  31. def __init__(self, connection, *, key, registry, clock=None):
  32. self._connection = connection
  33. self._key = key
  34. self._registry = registry
  35. self._clock = clock or time.monotonic
  36. def execute(self, statement, *args, **kwargs):
  37. started = self._clock()
  38. try:
  39. return self._connection.execute(statement, *args, **kwargs)
  40. except DBAPIError as exc:
  41. translated = _translate_query_error(exc)
  42. if translated is exc:
  43. raise
  44. raise translated from exc
  45. finally:
  46. self._registry.record_query_duration(
  47. self._key,
  48. (self._clock() - started) * 1000,
  49. )
  50. def __getattr__(self, name):
  51. return getattr(self._connection, name)
  52. class DataSourceConnectionManager:
  53. def __init__(
  54. self,
  55. *,
  56. definitions,
  57. credentials,
  58. platform_session,
  59. adapter_resolver,
  60. registry,
  61. settings_resolver,
  62. clock=None,
  63. ):
  64. self.definitions = definitions
  65. self.credentials = credentials
  66. self.platform_session = platform_session
  67. self.adapter_resolver = adapter_resolver
  68. self.registry = registry
  69. self.settings_resolver = settings_resolver
  70. self._clock = clock or time.monotonic
  71. @contextmanager
  72. def connect(self, data_source_uid, purpose):
  73. if purpose not in PURPOSES or purpose == "connection_test":
  74. raise DataSourceConfigurationInvalid(
  75. "unsupported pooled connection purpose"
  76. )
  77. definition = self.definitions.get(data_source_uid)
  78. if not definition:
  79. raise DataSourceNotFound()
  80. credential = self.credentials.get_active(
  81. self.platform_session(),
  82. data_source_uid,
  83. definition.credential_version,
  84. )
  85. adapter = self.adapter_resolver(definition.database_type)
  86. settings = self.settings_resolver(definition.pool_overrides())
  87. key = PoolKey(
  88. data_source_uid=str(data_source_uid),
  89. credential_version=int(definition.credential_version),
  90. config_fingerprint=definition.connection_fingerprint(),
  91. )
  92. with self.registry.lease(
  93. key,
  94. lambda: adapter.create_pooled_engine(
  95. definition,
  96. credential,
  97. settings,
  98. ),
  99. ) as engine:
  100. breaker = self.registry.breaker_for(key)
  101. try:
  102. with breaker.probe_permission():
  103. started = self._clock()
  104. connection = engine.connect()
  105. self.registry.add_checkout_wait(
  106. key,
  107. (self._clock() - started) * 1000,
  108. )
  109. self.registry.record_connection_success(key)
  110. except TimeoutError as exc:
  111. self.registry.record_pool_timeout(key)
  112. self.registry.record_connection_failure(key)
  113. raise DataSourcePoolTimeout() from exc
  114. except (OperationalError, DBAPIError) as exc:
  115. self.registry.record_connection_failure(key)
  116. raise DataSourceConnectionFailed() from exc
  117. with connection:
  118. transaction = connection.begin()
  119. try:
  120. adapter.configure_transaction(connection, purpose)
  121. yield ManagedConnection(
  122. connection,
  123. key=key,
  124. registry=self.registry,
  125. clock=self._clock,
  126. )
  127. if purpose == "dataflow_write":
  128. try:
  129. transaction.commit()
  130. except (OperationalError, DBAPIError) as exc:
  131. raise DataSourceWriteOutcomeUnknown() from exc
  132. else:
  133. transaction.rollback()
  134. except Exception:
  135. try:
  136. transaction.rollback()
  137. except Exception:
  138. pass
  139. raise
  140. def invalidate(self, data_source_uid, reason):
  141. self.registry.invalidate(data_source_uid, reason)
  142. def snapshot(self):
  143. return self.registry.snapshot()
  144. def close(self):
  145. self.registry.close_all()