Coverage for postrfp/shared/init/dbconfig.py: 98%
49 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-22 21:34 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-22 21:34 +0000
1import time
2import logging
4from sqlalchemy.exc import OperationalError, ProgrammingError
5from sqlalchemy import event, text
6from sqlalchemy.engine import Connection, Engine, create_engine
8from postrfp import conf
9from postrfp.shared.exceptions import TimezoneError
11log = logging.getLogger(__name__)
14TEST_CONNECTION_STMT = "SELECT COUNT(*) FROM projects"
15DELAY_INTERVAL = 10
18def set_connection_timezone(dbapi_connection, _connrecord, tz: str = "+0:00"):
19 """
20 Sets the 'sesson time_zone' for the given connection
21 """
22 log.info("Connection setting timezone to UTC for db session")
23 dbapi_connection.cursor().execute(f"SET session time_zone='{tz}' ")
26def _check_connection_timezone(conn: Connection):
27 """
28 Check that the session (connection) timezone is UTC
29 """
30 tz_diff = conn.execute(
31 text("SELECT TIME_TO_SEC(TIMEDIFF(NOW(),utc_timestamp()))")
32 ).scalar_one()
34 if int(tz_diff) != 0:
35 # This should never be able to happen, because the connection hook
36 # sets the timezone to UTC. But if it does, raise an error.
37 log.error(f"Timezone difference detected: {tz_diff} seconds. ")
39 session_tz = conn.execute(text("SELECT @@SESSION.TIME_ZONE")).scalar_one()
40 global_tz_rows = conn.execute(text("SELECT @@GLOBAL.TIME_ZONE")).all()
42 global_tz = global_tz_rows[0][0]
44 raise TimezoneError(
45 f"Timezone difference detected: {tz_diff} seconds. "
46 f" Session Timezone: {session_tz}, Global (server) Timezone: {global_tz} "
47 "Unable to proceed with the connection."
48 )
51def check_connection(engine: Engine):
52 """
53 Checks that:
54 1 the database connection is live
55 2 the database has a table named 'projects'
56 3 the timezone of the DB session is the same as thaf of the local server
57 """
58 for attempt in ("first", "second", "third"):
59 log.info(f"{attempt} attempt at connecting to the DB")
60 conn = None
61 try:
62 conn = engine.connect()
63 conn.execute(text(TEST_CONNECTION_STMT))
65 _check_connection_timezone(conn)
67 # Success: exit the loop. Finally will close the connection.
68 break
70 except (ProgrammingError, OperationalError) as oe:
71 if attempt == "third":
72 if "unknown database" in str(oe).lower():
73 log.warning(
74 f"Database doesn't exist at {engine.url}, quitting: {oe}"
75 )
76 else:
77 log.error(
78 "Giving up database connection after three attempts: %s", oe
79 )
80 raise oe
82 else:
83 log.warning(
84 f"DB Connection failed. Will try again after {DELAY_INTERVAL} seconds..."
85 )
86 time.sleep(DELAY_INTERVAL)
88 finally:
89 if conn is not None:
90 conn.close()
93def build_engine(echo=False) -> Engine:
94 if conf.CONF is None:
95 raise Exception(
96 "--!!--postrfp not configured. Call sysconfig.configure_postrfp()--"
97 )
99 dsn = conf.CONF.sqlalchemy_dsn
100 engine = create_engine(
101 dsn,
102 echo=echo,
103 pool_recycle=3600,
104 execution_options={"isolation_level": "REPEATABLE READ"},
105 )
107 event.listen(engine, "connect", set_connection_timezone)
109 log.info("Created sqlalchemy engine with %s" % dsn)
110 check_connection(engine)
112 return engine