Spaces:
Sleeping
Sleeping
File size: 5,728 Bytes
be85b16 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
"""
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',
]
|