diff --git a/src/api/database/database.py b/src/api/database/database.py index f300c2fb3d0a05f3a7ea358b2fe0ad01526777eb..9139102eb6b3f954f7f60eb9d622c0fedc6e5d80 100644 --- a/src/api/database/database.py +++ b/src/api/database/database.py @@ -1,14 +1,31 @@ from threading import Condition from datetime import datetime -from typing import Callable +from typing import Callable, Literal from abc import ABC, ABCMeta, abstractmethod from contextlib import contextmanager from traceback import StackSummary, extract_stack -from api.miscellaneous import DEBUG_ENABLED +from api.miscellaneous import DEBUG_ENABLED, DIAGNOSTICS_TRACKER, DiagnosticsCounter DbValueType = int | str | bytes | datetime +_TransactionType = Literal["read", "write"] + + +def _create_diagnostics_counters(id: str) -> dict[_TransactionType, DiagnosticsCounter]: + return { + "read": DIAGNOSTICS_TRACKER.register_counter(f"database.transaction.read.{id}"), + "write": DIAGNOSTICS_TRACKER.register_counter(f"database.transaction.write.{id}") + } + + +_COUNTERS_TRANSACTION_COUNT = _create_diagnostics_counters("count") +_COUNTERS_TRANSACTION_ERRORS = _create_diagnostics_counters("errors") +_COUNTERS_TRANSACTION_ATTEMPTED_RECONNECTS = _create_diagnostics_counters("attempted_reconnects") +_COUNTERS_TRANSACTION_SUCCESSFUL_RECONNECTS = _create_diagnostics_counters("successful_reconnects") +_COUNTERS_TRANSACTION_ERRORS_AFTER_RECONNECT = _create_diagnostics_counters("errors_after_reconnect") +_COUNTERS_TRANSACTION_ABORTED_BY_USER = _create_diagnostics_counters("aborted_by_user") + class DatabaseResultRow: @@ -184,14 +201,17 @@ class DbConnectionFactory(ABC): class AbstractTransaction(ABC): __metaclass__ = ABCMeta - def __init__(self, release_connection: Callable, connection: DbConnection): + def __init__(self, type: _TransactionType, release_connection: Callable, connection: DbConnection): super().__init__() + self._type = type self._release_connection = release_connection self._closed = False self._connection = connection self._has_executed_statements = False + self._has_encountered_errors = False self._queued_statements: list[FilledStatement] = [] self._statement_results: dict[FilledStatement, DbResult] = {} + _COUNTERS_TRANSACTION_COUNT[type].trigger() def is_closed(self) -> bool: return self._closed @@ -315,15 +335,24 @@ class AbstractTransaction(ABC): try: results = self._connection.execute_statements(self._queued_statements) except DatabaseError as e: + if not self._has_encountered_errors: + _COUNTERS_TRANSACTION_ERRORS[self._type].trigger() + self._has_encountered_errors = True + if self._has_executed_statements or not self._connection.is_disconnected(force_ping=True): self.on_error_close_transaction() raise e + # All triggers after here can be triggered at most once for a single transaction because after this code block + # we have either executed statements or closed the transaction + _COUNTERS_TRANSACTION_ATTEMPTED_RECONNECTS[self._type].trigger() if not self._connection.try_reconnect(): self.on_error_close_transaction() raise e + _COUNTERS_TRANSACTION_SUCCESSFUL_RECONNECTS[self._type].trigger() try: results = self._connection.execute_statements(self._queued_statements) except Exception: + _COUNTERS_TRANSACTION_ERRORS_AFTER_RECONNECT[self._type].trigger() self.on_error_close_transaction() raise e # Raise original exception except Exception as e: @@ -355,7 +384,7 @@ class AbstractTransaction(ABC): class ReadTransaction(AbstractTransaction): def __init__(self, release_connection: Callable, connection: DbConnection): - super().__init__(release_connection, connection) + super().__init__("read", release_connection, connection) self.queue_statement(connection.get_transaction_begin_statement(False)) def execute_statement_and_close(self, @@ -421,7 +450,7 @@ class ReadTransaction(AbstractTransaction): class WriteTransaction(AbstractTransaction): def __init__(self, release_connection: Callable, connection: DbConnection): - super().__init__(release_connection, connection) + super().__init__("write", release_connection, connection) self._committed = False self.queue_statement(connection.get_transaction_begin_statement(True)) @@ -681,6 +710,7 @@ class DbConnectionPool: transaction.close() except Exception: if transaction is not None and not transaction.is_closed(): + _COUNTERS_TRANSACTION_ABORTED_BY_USER["read"].trigger() transaction.on_error_close_transaction() raise @@ -705,5 +735,6 @@ class DbConnectionPool: transaction.rollback() except Exception: if transaction is not None and not transaction.is_closed(): + _COUNTERS_TRANSACTION_ABORTED_BY_USER["write"].trigger() transaction.on_error_close_transaction() raise