manager.py 5.2 KB

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