Files
carpibord/obd2_client/src/handlers/storage_handler.py

229 lines
6.8 KiB
Python

"""
Storage Handler - persists OBD2 readings to SQLite.
Implements batching for efficient database writes.
"""
import threading
import time
from typing import Dict, Any, List, Optional
from datetime import datetime
from .base import BaseHandler, OBD2Reading
from ..storage.storage import get_storage, Storage
from ..logger import get_logger
logger = get_logger(__name__)
class StorageHandler(BaseHandler):
"""
Handler that saves OBD2 readings to SQLite.
Features:
- Configurable batch size for efficient writes
- Automatic flush on interval
- Session management integration
- Thread-safe batching
"""
def __init__(
self,
enabled: bool = True,
batch_size: int = 50,
flush_interval: float = 1.0,
db_path: Optional[str] = None,
auto_session: bool = True,
):
"""
Initialize storage handler.
Args:
enabled: Whether handler is active
batch_size: Number of readings to batch before write
flush_interval: Maximum time before flushing batch (seconds)
db_path: Path to SQLite database
auto_session: Automatically start session on first reading
"""
super().__init__(name="storage", enabled=enabled)
self._storage: Optional[Storage] = None
self._batch_size = batch_size
self._flush_interval = flush_interval
self._db_path = db_path
self._auto_session = auto_session
self._batch: List[Dict[str, Any]] = []
self._batch_lock = threading.Lock()
self._last_flush_time = time.time()
self._saved_count = 0
self._batch_count = 0
self._session_started = False
def initialize(self) -> bool:
"""Initialize the handler."""
try:
self._storage = get_storage(db_path=self._db_path)
self._initialized = True
logger.info(
f"Storage handler initialized: batch_size={self._batch_size}, "
f"flush_interval={self._flush_interval}s"
)
return True
except Exception as e:
logger.error(f"Failed to initialize storage handler: {e}")
return False
def _ensure_session(self) -> None:
"""Ensure a session is started."""
if self._auto_session and not self._session_started:
if self._storage and not self._storage.get_current_session_id():
self._storage.start_session()
self._session_started = True
def handle(self, reading: OBD2Reading) -> bool:
"""
Add reading to batch.
Args:
reading: OBD2Reading to save
Returns:
True if reading was accepted
"""
if not self._initialized or not self.enabled:
return False
self._ensure_session()
with self._batch_lock:
self._batch.append({
"pid": reading.pid,
"name": reading.name,
"value": reading.value,
"unit": reading.unit,
"timestamp": reading.timestamp.isoformat(),
"session_id": reading.session_id,
})
should_flush = (
len(self._batch) >= self._batch_size or
(time.time() - self._last_flush_time) >= self._flush_interval
)
if should_flush:
self._do_flush()
return True
def handle_batch(self, readings: List[OBD2Reading]) -> int:
"""
Add multiple readings to batch.
Args:
readings: List of OBD2Reading objects
Returns:
Number of readings accepted
"""
if not self._initialized or not self.enabled:
return 0
self._ensure_session()
with self._batch_lock:
for reading in readings:
self._batch.append({
"pid": reading.pid,
"name": reading.name,
"value": reading.value,
"unit": reading.unit,
"timestamp": reading.timestamp.isoformat(),
"session_id": reading.session_id,
})
should_flush = (
len(self._batch) >= self._batch_size or
(time.time() - self._last_flush_time) >= self._flush_interval
)
if should_flush:
self._do_flush()
return len(readings)
def _do_flush(self) -> None:
"""Internal flush - must be called with lock held."""
if not self._storage or not self._batch:
return
batch_to_save = self._batch
self._batch = []
self._last_flush_time = time.time()
try:
saved = self._storage.save_readings_batch(batch_to_save)
self._saved_count += saved
self._batch_count += 1
logger.debug(f"Flushed {saved} readings (batch #{self._batch_count})")
except Exception as e:
logger.error(f"Failed to flush readings: {e}")
# Put back failed batch
self._batch = batch_to_save + self._batch
def flush(self) -> None:
"""Flush pending readings to database."""
with self._batch_lock:
self._do_flush()
def shutdown(self) -> None:
"""Shutdown the handler."""
self.flush()
if self._storage and self._session_started:
self._storage.end_session()
logger.info(
f"Storage handler shutdown: saved={self._saved_count}, "
f"batches={self._batch_count}"
)
self._initialized = False
def get_stats(self) -> Dict[str, Any]:
"""Get handler statistics."""
storage_stats = {}
if self._storage:
storage_stats = self._storage.get_stats()
with self._batch_lock:
pending = len(self._batch)
return {
"name": self.name,
"enabled": self.enabled,
"initialized": self._initialized,
"saved_count": self._saved_count,
"batch_count": self._batch_count,
"pending_in_batch": pending,
"batch_size": self._batch_size,
"flush_interval": self._flush_interval,
**storage_stats,
}
@property
def storage(self) -> Optional[Storage]:
"""Get the Storage instance."""
return self._storage
def start_session(self) -> Optional[str]:
"""Manually start a new session."""
if self._storage:
session_id = self._storage.start_session()
self._session_started = True
return session_id
return None
def end_session(self) -> None:
"""Manually end current session."""
if self._storage:
self._storage.end_session()
self._session_started = False