Update database connection handling to ensure string paths and set timeout

- Modified database connection calls in DBManager, FeedManager, and MessageScheduler to convert db_path to a string and set a timeout of 30 seconds, improving reliability and performance of database operations.
- Enhanced error logging to include detailed information about database path existence and permissions, aiding in debugging and ensuring smoother operation.
This commit is contained in:
agessaman
2025-12-28 12:12:15 -08:00
parent c2bd1f06e3
commit 63ed0fd73d
3 changed files with 46 additions and 25 deletions
+15 -15
View File
@@ -38,7 +38,7 @@ class DBManager:
def _init_database(self):
"""Initialize the SQLite database with required tables"""
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
# Create geocoding_cache table for weather command optimization
@@ -215,7 +215,7 @@ class DBManager:
def get_cached_geocoding(self, query: str) -> Tuple[Optional[float], Optional[float]]:
"""Get cached geocoding result for a query"""
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT latitude, longitude FROM geocoding_cache
@@ -236,7 +236,7 @@ class DBManager:
if not isinstance(cache_hours, int) or cache_hours < 1 or cache_hours > 87600: # Max 10 years
raise ValueError(f"cache_hours must be an integer between 1 and 87600, got: {cache_hours}")
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
# Use parameter binding instead of string formatting
cursor.execute('''
@@ -252,7 +252,7 @@ class DBManager:
def get_cached_value(self, cache_key: str, cache_type: str) -> Optional[str]:
"""Get cached value for a key and type"""
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT cache_value FROM generic_cache
@@ -273,7 +273,7 @@ class DBManager:
if not isinstance(cache_hours, int) or cache_hours < 1 or cache_hours > 87600: # Max 10 years
raise ValueError(f"cache_hours must be an integer between 1 and 87600, got: {cache_hours}")
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
# Use parameter binding instead of string formatting
cursor.execute('''
@@ -308,7 +308,7 @@ class DBManager:
def cleanup_expired_cache(self):
"""Remove expired cache entries from all cache tables"""
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
# Clean up geocoding cache
@@ -331,7 +331,7 @@ class DBManager:
def cleanup_geocoding_cache(self):
"""Remove expired geocoding cache entries"""
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM geocoding_cache WHERE expires_at < datetime('now')")
deleted_count = cursor.rowcount
@@ -344,7 +344,7 @@ class DBManager:
def get_database_stats(self) -> Dict[str, Any]:
"""Get database statistics"""
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
stats = {}
@@ -380,7 +380,7 @@ class DBManager:
def vacuum_database(self):
"""Optimize database by reclaiming unused space"""
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
conn.execute("VACUUM")
self.logger.info("Database vacuum completed")
except Exception as e:
@@ -398,7 +398,7 @@ class DBManager:
if not re.match(r'^[a-z_][a-z0-9_]*$', table_name):
raise ValueError(f"Invalid table name format: {table_name}")
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
# Table names cannot be parameterized, but we've validated against whitelist
cursor.execute(f'CREATE TABLE IF NOT EXISTS {table_name} ({schema})')
@@ -422,7 +422,7 @@ class DBManager:
# Extra safety: log critical action
self.logger.warning(f"CRITICAL: Dropping table '{table_name}'")
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
# Table names cannot be parameterized, but we've validated against whitelist
cursor.execute(f'DROP TABLE IF EXISTS {table_name}')
@@ -435,7 +435,7 @@ class DBManager:
def execute_query(self, query: str, params: Tuple = ()) -> List[Dict]:
"""Execute a custom query and return results as list of dictionaries"""
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(query, params)
@@ -448,7 +448,7 @@ class DBManager:
def execute_update(self, query: str, params: Tuple = ()) -> int:
"""Execute an update/insert/delete query and return number of affected rows"""
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
cursor.execute(query, params)
conn.commit()
@@ -461,7 +461,7 @@ class DBManager:
def set_metadata(self, key: str, value: str):
"""Set a metadata value for the bot"""
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT OR REPLACE INTO bot_metadata (key, value, updated_at)
@@ -474,7 +474,7 @@ class DBManager:
def get_metadata(self, key: str) -> Optional[str]:
"""Get a metadata value for the bot"""
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
cursor.execute('SELECT value FROM bot_metadata WHERE key = ?', (key,))
result = cursor.fetchone()
+16 -5
View File
@@ -11,6 +11,7 @@ import time
import hashlib
import html
import re
import os
from datetime import datetime, timezone
from typing import Dict, List, Optional, Any, Tuple
from pathlib import Path
@@ -272,7 +273,7 @@ class FeedManager:
# Query database for all processed item IDs for this feed
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT DISTINCT item_id FROM feed_activity
@@ -431,7 +432,7 @@ class FeedManager:
# Query database for all processed item IDs for this feed
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(str(self.db_path), timeout=30.0) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT DISTINCT item_id FROM feed_activity
@@ -1181,7 +1182,8 @@ class FeedManager:
"""Process queued feed messages and send them at configured intervals"""
try:
# Get all unsent messages, ordered by priority and queue time
with sqlite3.connect(self.db_path) as conn:
db_path = str(self.db_path) # Ensure string, not Path object
with sqlite3.connect(db_path, timeout=30.0) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('''
@@ -1225,7 +1227,7 @@ class FeedManager:
if success:
# Mark as sent
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(db_path, timeout=30.0) as conn:
cursor = conn.cursor()
cursor.execute('''
UPDATE feed_message_queue
@@ -1250,6 +1252,15 @@ class FeedManager:
except Exception as e:
db_path = getattr(self, 'db_path', 'unknown')
db_path_str = str(db_path) if db_path != 'unknown' else 'unknown'
self.logger.error(f"Error processing message queue: {e}")
self.logger.error(f"Database path: {db_path} (exists: {Path(db_path).exists() if isinstance(db_path, str) else False})")
if db_path_str != 'unknown':
path_obj = Path(db_path_str)
self.logger.error(f"Database path: {db_path_str} (exists: {path_obj.exists()}, readable: {os.access(db_path_str, os.R_OK) if path_obj.exists() else False}, writable: {os.access(db_path_str, os.W_OK) if path_obj.exists() else False})")
# Check parent directory permissions
if path_obj.exists():
parent = path_obj.parent
self.logger.error(f"Parent directory: {parent} (exists: {parent.exists()}, writable: {os.access(str(parent), os.W_OK) if parent.exists() else False})")
else:
self.logger.error(f"Database path: {db_path_str}")
+15 -5
View File
@@ -11,6 +11,7 @@ import datetime
import pytz
import sqlite3
import json
import os
from typing import Dict, Tuple
from pathlib import Path
@@ -260,10 +261,10 @@ class MessageScheduler:
async def _process_channel_operations(self):
"""Process pending channel operations from the web viewer"""
try:
db_path = self.bot.db_manager.db_path
db_path = str(self.bot.db_manager.db_path) # Ensure string, not Path object
# Get pending operations
with sqlite3.connect(db_path) as conn:
with sqlite3.connect(db_path, timeout=30.0) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
@@ -322,7 +323,7 @@ class MessageScheduler:
error_msg = "Failed to remove channel"
# Update operation status
with sqlite3.connect(db_path) as conn:
with sqlite3.connect(db_path, timeout=30.0) as conn:
cursor = conn.cursor()
if success:
cursor.execute('''
@@ -346,7 +347,7 @@ class MessageScheduler:
self.logger.error(f"Error processing channel operation {op_id}: {e}")
# Mark as failed
try:
with sqlite3.connect(db_path) as conn:
with sqlite3.connect(db_path, timeout=30.0) as conn:
cursor = conn.cursor()
cursor.execute('''
UPDATE channel_operations
@@ -361,5 +362,14 @@ class MessageScheduler:
except Exception as e:
db_path = getattr(self.bot.db_manager, 'db_path', 'unknown')
db_path_str = str(db_path) if db_path != 'unknown' else 'unknown'
self.logger.error(f"Error in _process_channel_operations: {e}")
self.logger.error(f"Database path: {db_path} (exists: {Path(db_path).exists() if isinstance(db_path, str) else False})")
if db_path_str != 'unknown':
path_obj = Path(db_path_str)
self.logger.error(f"Database path: {db_path_str} (exists: {path_obj.exists()}, readable: {os.access(db_path_str, os.R_OK) if path_obj.exists() else False}, writable: {os.access(db_path_str, os.W_OK) if path_obj.exists() else False})")
# Check parent directory permissions
if path_obj.exists():
parent = path_obj.parent
self.logger.error(f"Parent directory: {parent} (exists: {parent.exists()}, writable: {os.access(str(parent), os.W_OK) if parent.exists() else False})")
else:
self.logger.error(f"Database path: {db_path_str}")