"""Public context-managed access to external business data sources.""" import time from contextlib import contextmanager from sqlalchemy.exc import DBAPIError, OperationalError, TimeoutError from app.core.data_source.adapters.base import PURPOSES from app.core.data_source.errors import ( DataSourceConfigurationInvalid, DataSourceConnectionFailed, DataSourceNotFound, DataSourcePoolTimeout, DataSourceQueryTimeout, DataSourceReadOnlyViolation, ) from app.core.data_source.models import PoolKey def _driver_error_code(error): original = getattr(error, "orig", error) code = getattr(original, "pgcode", None) if code: return str(code) args = getattr(original, "args", ()) return str(args[0]) if args else "" def _translate_query_error(error): code = _driver_error_code(error) if code in {"57014", "3024", "1317"}: return DataSourceQueryTimeout() if code in {"25006", "1792"}: return DataSourceReadOnlyViolation() return error class ManagedConnection: def __init__(self, connection, *, key, registry, clock=None): self._connection = connection self._key = key self._registry = registry self._clock = clock or time.monotonic def execute(self, statement, *args, **kwargs): started = self._clock() try: return self._connection.execute(statement, *args, **kwargs) except DBAPIError as exc: translated = _translate_query_error(exc) if translated is exc: raise raise translated from exc finally: self._registry.record_query_duration( self._key, (self._clock() - started) * 1000, ) def __getattr__(self, name): return getattr(self._connection, name) class DataSourceConnectionManager: def __init__( self, *, definitions, credentials, platform_session, adapter_resolver, registry, settings_resolver, clock=None, ): self.definitions = definitions self.credentials = credentials self.platform_session = platform_session self.adapter_resolver = adapter_resolver self.registry = registry self.settings_resolver = settings_resolver self._clock = clock or time.monotonic @contextmanager def connect(self, data_source_uid, purpose): if purpose not in PURPOSES or purpose == "connection_test": raise DataSourceConfigurationInvalid( "unsupported pooled connection purpose" ) definition = self.definitions.get(data_source_uid) if not definition: raise DataSourceNotFound() credential = self.credentials.get_active( self.platform_session(), data_source_uid, definition.credential_version, ) adapter = self.adapter_resolver(definition.database_type) settings = self.settings_resolver(definition.pool_overrides()) key = PoolKey( data_source_uid=str(data_source_uid), credential_version=int(definition.credential_version), config_fingerprint=definition.connection_fingerprint(), ) with self.registry.lease( key, lambda: adapter.create_pooled_engine( definition, credential, settings, ), ) as engine: breaker = self.registry.breaker_for(key) try: with breaker.probe_permission(): started = self._clock() connection = engine.connect() self.registry.add_checkout_wait( key, (self._clock() - started) * 1000, ) self.registry.record_connection_success(key) except TimeoutError as exc: self.registry.record_pool_timeout(key) self.registry.record_connection_failure(key) raise DataSourcePoolTimeout() from exc except (OperationalError, DBAPIError) as exc: self.registry.record_connection_failure(key) raise DataSourceConnectionFailed() from exc with connection: transaction = connection.begin() try: adapter.configure_transaction(connection, purpose) yield ManagedConnection( connection, key=key, registry=self.registry, clock=self._clock, ) if purpose == "dataflow_write": transaction.commit() else: transaction.rollback() except Exception: try: transaction.rollback() except Exception: pass raise def invalidate(self, data_source_uid, reason): self.registry.invalidate(data_source_uid, reason) def snapshot(self): return self.registry.snapshot() def close(self): self.registry.close_all()