"""Clock implementation using different backend""" from __future__ import annotations import logging from abc import ABC, abstractmethod from time import monotonic_ns from typing import TYPE_CHECKING, Awaitable, Union if TYPE_CHECKING: from psycopg_pool import ConnectionPool logger = logging.getLogger(__name__) class AbstractClock(ABC): """Clock that return timestamp for `now`""" @abstractmethod def now(self) -> Union[int, Awaitable[int]]: """Get time as of now, in milliseconds""" @staticmethod def _get_monotonic_ms() -> int: """Get monotonic time in milliseconds""" return monotonic_ns() // 1_000_000 class MonotonicClock(AbstractClock): def now(self) -> int: """Get monotonic time in milliseconds""" return self._get_monotonic_ms() class MonotonicAsyncClock(AbstractClock): """Monotonic Async Clock, meant for testing only""" async def now(self) -> int: """Get monotonic time in milliseconds""" return self._get_monotonic_ms() class PostgresClock(AbstractClock): """Get timestamp using Postgres as remote clock backend""" def __init__(self, pool: "ConnectionPool"): self.pool = pool def now(self) -> int: """Get current time in milliseconds using Postgres. Falls back to local time if the DB query fails for any reason. """ # used `clock_timestamp` instead of `current_timestamp`, # because we want the actual current time query = "SELECT (extract(epoch FROM clock_timestamp()) * 1000)::bigint" try: with ( self.pool.connection() as connection, # the cursor should be explicitly closed to avoid # potential resource leaks. E.g. psycopg3, cursors # should either be used with a context manager or # closed explicitly after use. connection.cursor() as cursor, ): cursor.execute(query) row = cursor.fetchone() if row is None: msg = "Postgres time query returned no rows" raise RuntimeError(msg) return row[0] except Exception: logger.exception("Postgres time query failed, falling back to local clock") # fallback to local monotonic time in milliseconds return self._get_monotonic_ms()