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',
]