""" DB Backup Service - Intelligent database backup to Google Drive Features: - Async lock to prevent concurrent uploads - Debouncing to skip frequent uploads - Non-blocking background execution - Force option for critical backups (e.g., shutdown) """ import asyncio import logging from datetime import datetime, timedelta from typing import Optional logger = logging.getLogger(__name__) class BackupService: """ Intelligent database backup service. Prevents concurrent uploads and excessive backup frequency using: - Async lock (one upload at a time) - Debouncing (minimum interval between uploads) - Non-blocking skip (don't wait if upload in progress) """ def __init__( self, drive_service, min_interval_seconds: int = 30 ): """ Initialize backup service. Args: drive_service: DriveService instance for actual uploads min_interval_seconds: Minimum seconds between uploads (debouncing) """ self.drive_service = drive_service self.min_interval = timedelta(seconds=min_interval_seconds) # Async lock to prevent concurrent uploads self._upload_lock = asyncio.Lock() # Track last upload time for debouncing self._last_upload_time: Optional[datetime] = None logger.info(f"BackupService initialized (min interval: {min_interval_seconds}s)") async def backup_async(self, force: bool = False) -> bool: """ Backup database to Google Drive (async, non-blocking). Args: force: If True, skip debouncing and upload immediately (useful for critical backups like shutdown) Returns: True if backup was performed, False if skipped """ # Non-blocking check: if upload already in progress, skip if self._upload_lock.locked(): logger.info("📦 Backup already in progress, skipping this request") return False # Debouncing check: skip if uploaded recently (unless forced) if not force and self._last_upload_time: time_since_last = datetime.utcnow() - self._last_upload_time if time_since_last < self.min_interval: logger.info( f"📦 Backup skipped - last backup was {time_since_last.seconds}s ago " f"(min interval: {self.min_interval.seconds}s)" ) return False # Try to acquire lock (should succeed since we checked above) async with self._upload_lock: try: # Perform actual upload using DriveService loop = asyncio.get_event_loop() await loop.run_in_executor( None, self.drive_service.upload_db ) # Update last upload time self._last_upload_time = datetime.utcnow() logger.info("✅ Database backup completed successfully") return True except Exception as e: logger.error(f"❌ Database backup failed: {e}") # Don't update last_upload_time on failure # This allows retry on next request return False def backup_sync(self, force: bool = False) -> bool: """ Synchronous backup (for non-async contexts). Args: force: If True, skip debouncing Returns: True if backup was performed, False if skipped """ # Check if we should skip (debouncing) if not force and self._last_upload_time: time_since_last = datetime.utcnow() - self._last_upload_time if time_since_last < self.min_interval: logger.info( f"📦 Backup skipped - last backup was {time_since_last.seconds}s ago" ) return False try: # Perform upload self.drive_service.upload_db() self._last_upload_time = datetime.utcnow() logger.info("✅ Database backup completed successfully (sync)") return True except Exception as e: logger.error(f"❌ Database backup failed (sync): {e}") return False def get_stats(self) -> dict: """Get backup statistics.""" return { "last_backup_time": self._last_upload_time.isoformat() if self._last_upload_time else None, "upload_in_progress": self._upload_lock.locked(), "min_interval_seconds": self.min_interval.seconds, } # Global instance (initialized in app startup) _backup_service: Optional[BackupService] = None def get_backup_service() -> BackupService: """Get the global backup service instance.""" if _backup_service is None: raise RuntimeError("BackupService not initialized. Call initialize_backup_service() first.") return _backup_service def initialize_backup_service(drive_service, min_interval_seconds: int = 30) -> BackupService: """ Initialize the global backup service instance. Args: drive_service: DriveService instance min_interval_seconds: Minimum seconds between backups Returns: BackupService instance """ global _backup_service _backup_service = BackupService(drive_service, min_interval_seconds) return _backup_service __all__ = [ 'BackupService', 'get_backup_service', 'initialize_backup_service', ]