Skip to content

Commit 07982b1

Browse files
Mariadb fix of dead server issue due to unrecycled sessions (#204)
* improve database session recycling * version increased * MariaDB unrecycled sessions fix to webserver database * Removed mariadbconnector
1 parent 7223161 commit 07982b1

File tree

3 files changed

+92
-58
lines changed

3 files changed

+92
-58
lines changed

wadas/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
# Date: 2024-08-14
1818
# Description: module to keep track of WADAS version
1919

20-
__version__ = "v1.0.0.a6"
20+
__version__ = "v1.0.0.a7"
2121
__dbversion__ = __version__

wadas/domain/database.py

Lines changed: 76 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ def get_engine(cls):
193193
if (DataBase.wadas_db.type == DataBase.DBTypes.SQLITE)
194194
else create_engine(
195195
DataBase.wadas_db.get_connection_string(),
196-
pool_recycle=1800, # Recycle connection every 30 minutes
197-
pool_pre_ping=True, # Check if connection is still valid
196+
pool_recycle=300, # Recycle connection every 5 minutes (under MariaDB wait_timeout)
197+
pool_pre_ping=True, # Check if connection is still valid before use
198198
)
199199
)
200200
return DataBase.wadas_db_engine
@@ -237,12 +237,18 @@ def create_session(cls, retry_count=0):
237237
if engine := cls.get_engine():
238238
# If db is not SQLite, check engine status (SQLite has no pre-existing sessions)
239239
if DataBase.wadas_db.type != DataBase.DBTypes.SQLITE:
240-
with engine.connect() as connection:
241-
if connection.invalidated:
242-
logger.warning("Connection invalidated, disposing engine...")
243-
engine.dispose()
244-
DataBase.wadas_db_engine = None # Force new engine
245-
engine = cls.get_engine()
240+
try:
241+
with engine.connect() as connection:
242+
if connection.invalidated:
243+
logger.warning("Connection invalidated, disposing engine...")
244+
engine.dispose()
245+
DataBase.wadas_db_engine = None # Force new engine
246+
engine = cls.get_engine()
247+
except (InterfaceError, SQLAlchemyOperationalError, mariadbOperationalerror):
248+
logger.warning("Pre-ping connection check failed, disposing engine...")
249+
engine.dispose()
250+
DataBase.wadas_db_engine = None
251+
engine = cls.get_engine()
246252

247253
Session = sessionmaker(bind=engine)
248254
return Session()
@@ -277,6 +283,8 @@ def get_db_uuid(cls):
277283
return result[0] if result else None
278284
except Exception:
279285
return None
286+
finally:
287+
session.close()
280288
else:
281289
return None
282290

@@ -290,6 +298,8 @@ def get_db_version(cls):
290298
return result[0] if result else None
291299
except Exception:
292300
return None
301+
finally:
302+
session.close()
293303
else:
294304
return None
295305

@@ -767,36 +777,40 @@ def remove_actuator_from_camera(cls, camera, actuator):
767777

768778
if cls.get_instance():
769779
logger.debug("Removing actuator %s from camera %s in db.", actuator.id, camera.id)
770-
if cls.create_session():
771-
# Retrieve camera and actuator db instances
772-
camera_db_id = cls.get_camera_id(camera.id)
773-
actuator_db_id = cls.get_actuator_id(actuator.id)
774-
775-
stmt = delete(camera_actuator_association).where(
776-
and_(
777-
camera_actuator_association.c.actuator_id == actuator_db_id,
778-
camera_actuator_association.c.camera_id == camera_db_id,
779-
)
780+
# Retrieve camera and actuator db instances
781+
camera_db_id = cls.get_camera_id(camera.id)
782+
actuator_db_id = cls.get_actuator_id(actuator.id)
783+
784+
stmt = delete(camera_actuator_association).where(
785+
and_(
786+
camera_actuator_association.c.actuator_id == actuator_db_id,
787+
camera_actuator_association.c.camera_id == camera_db_id,
780788
)
781-
cls.run_query(stmt)
782-
else:
783-
logger.debug("Could not create db session, skipping actuator association insert.")
789+
)
790+
cls.run_query(stmt)
791+
else:
792+
logger.debug("Could not create db session, skipping actuator association insert.")
784793

785794
@classmethod
786795
def get_camera_id(cls, camera_id):
787796
"""Method to return camera database id (primary key)"""
788797

789798
if session := cls.create_session():
790-
return (
791-
session.query(ORMCamera.db_id)
792-
.filter(
793-
and_(
794-
ORMCamera.camera_id == camera_id,
795-
ORMCamera.deletion_date.is_(None), # Avoid to return id of deleted camera
799+
try:
800+
return (
801+
session.query(ORMCamera.db_id)
802+
.filter(
803+
and_(
804+
ORMCamera.camera_id == camera_id,
805+
ORMCamera.deletion_date.is_(
806+
None
807+
), # Avoid to return id of deleted camera
808+
)
796809
)
797-
)
798-
.scalar()
799-
) # Use scalar() to retrieve the value directly
810+
.scalar()
811+
) # Use scalar() to retrieve the value directly
812+
finally:
813+
session.close()
800814
else:
801815
logger.debug(
802816
"Could not get camera id %s since session has not been created.", camera_id
@@ -808,16 +822,21 @@ def get_actuator_id(cls, actuator_id):
808822
"""Method to return actuator database id (primary key)"""
809823

810824
if session := cls.create_session():
811-
return (
812-
session.query(ORMActuator.db_id)
813-
.filter(
814-
and_(
815-
ORMActuator.actuator_id == actuator_id,
816-
ORMActuator.deletion_date.is_(None), # Avoid to return id of deleted camera
825+
try:
826+
return (
827+
session.query(ORMActuator.db_id)
828+
.filter(
829+
and_(
830+
ORMActuator.actuator_id == actuator_id,
831+
ORMActuator.deletion_date.is_(
832+
None
833+
), # Avoid to return id of deleted camera
834+
)
817835
)
836+
.scalar()
818837
)
819-
.scalar()
820-
)
838+
finally:
839+
session.close()
821840
else:
822841
logger.debug(
823842
"Could not get actuator id %s since session has not been created.", actuator_id
@@ -836,16 +855,19 @@ def get_detection_event_id(cls, detection_event: DetectionEvent):
836855
return None
837856

838857
if session := cls.create_session():
839-
return (
840-
session.query(ORMDetectionEvent.db_id)
841-
.filter(
842-
and_(
843-
ORMDetectionEvent.camera_id == camera_db_id,
844-
ORMDetectionEvent.time_stamp == detection_event.time_stamp,
858+
try:
859+
return (
860+
session.query(ORMDetectionEvent.db_id)
861+
.filter(
862+
and_(
863+
ORMDetectionEvent.camera_id == camera_db_id,
864+
ORMDetectionEvent.time_stamp == detection_event.time_stamp,
865+
)
845866
)
846-
)
847-
.scalar()
848-
) # Use scalar() to retrieve the value directly
867+
.scalar()
868+
) # Use scalar() to retrieve the value directly
869+
finally:
870+
session.close()
849871
else:
850872
logger.error(
851873
"Could not retrieve detection event id as connection could not be created."
@@ -1379,7 +1401,7 @@ def create_database(self):
13791401
# Connect to MySQL server to check DB connection, credentials and db existence
13801402
test_engine = create_engine(
13811403
f"mysql+pymysql://{self.username}:{self.get_password()}@{self.host}:{self.port}",
1382-
pool_recycle=1800,
1404+
pool_recycle=300,
13831405
pool_pre_ping=True,
13841406
)
13851407

@@ -1439,7 +1461,7 @@ def create_database(self):
14391461
temp_engine = create_engine(
14401462
f"mysql+pymysql://{self.username}:{self.get_password()}"
14411463
f"@{self.host}:{self.port}",
1442-
pool_recycle=1800,
1464+
pool_recycle=300,
14431465
pool_pre_ping=True,
14441466
)
14451467

@@ -1463,7 +1485,7 @@ def create_database(self):
14631485

14641486
# Recreate engine with correct database
14651487
DataBase.wadas_db_engine = create_engine(
1466-
self.get_connection_string(), pool_recycle=1800, pool_pre_ping=True
1488+
self.get_connection_string(), pool_recycle=300, pool_pre_ping=True
14671489
)
14681490

14691491
# Create tables
@@ -1564,9 +1586,7 @@ def get_connection_string(self, create=False):
15641586
if not self.database_name and not create:
15651587
raise ValueError("Database name is required for MariaDB.")
15661588

1567-
base_string = (
1568-
f"mariadb+mariadbconnector://{self.username}:{password}@{self.host}:{self.port}"
1569-
)
1589+
base_string = f"mariadb+pymysql://{self.username}:{password}@{self.host}:{self.port}"
15701590
return base_string if create else f"{base_string}/{self.database_name}"
15711591

15721592
def create_database(self):
@@ -1578,7 +1598,7 @@ def create_database(self):
15781598
try:
15791599
# Connect to MariaDB server to check DB connection, credentials and db existence
15801600
test_engine = create_engine(
1581-
self.get_connection_string(create=True), pool_recycle=1800, pool_pre_ping=True
1601+
self.get_connection_string(create=True), pool_recycle=300, pool_pre_ping=True
15821602
)
15831603

15841604
with test_engine.connect() as conn:
@@ -1635,7 +1655,7 @@ def create_database(self):
16351655
try:
16361656
# Connect without specifying the database
16371657
temp_engine = create_engine(
1638-
self.get_connection_string(create=True), pool_recycle=1800, pool_pre_ping=True
1658+
self.get_connection_string(create=True), pool_recycle=300, pool_pre_ping=True
16391659
)
16401660

16411661
with temp_engine.connect() as conn:
@@ -1658,7 +1678,7 @@ def create_database(self):
16581678

16591679
# Recreate engine with correct database
16601680
DataBase.wadas_db_engine = create_engine(
1661-
self.get_connection_string(create=False), pool_recycle=1800, pool_pre_ping=True
1681+
self.get_connection_string(create=False), pool_recycle=300, pool_pre_ping=True
16621682
)
16631683

16641684
# Create tables

wadas_webserver/database.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,19 @@ class Database:
5656

5757
def __init__(self, connection_string):
5858
self.connection_string = connection_string
59-
self.engine = create_engine(self.get_connection_string())
59+
is_sqlite = connection_string.startswith("sqlite")
60+
self.engine = (
61+
create_engine(self.get_connection_string())
62+
if is_sqlite
63+
else create_engine(
64+
self.get_connection_string(),
65+
pool_recycle=300, # Recycle connections every 5 min (below MariaDB/MySQL wait_timeout)
66+
pool_pre_ping=True, # Validate connection before use
67+
pool_size=5, # Persistent connections kept in pool
68+
max_overflow=10, # Extra temporary connections allowed under load
69+
pool_timeout=30, # Max seconds to wait for a connection from the pool
70+
)
71+
)
6072

6173
@contextmanager
6274
def get_session(self):
@@ -65,7 +77,9 @@ def get_session(self):
6577
try:
6678
yield session
6779
except Exception:
80+
session.rollback()
6881
logger.exception("An error occurred while creating a session")
82+
raise
6983
finally:
7084
session.close()
7185

0 commit comments

Comments
 (0)