|
2 | 2 | import asyncio |
3 | 3 | import gc |
4 | 4 | import math |
| 5 | +import os |
5 | 6 | import shutil |
| 7 | +import tempfile |
6 | 8 | import typing |
7 | 9 | from collections import defaultdict |
8 | 10 | from contextlib import suppress |
@@ -183,6 +185,83 @@ def __init__( |
183 | 185 | self._key_index = LRUCache(limit=self.key_index_size) |
184 | 186 | self.db_lock = asyncio.Lock() |
185 | 187 | self.rebalance_ack = False |
| 188 | + self._backup_path = os.path.join(self.path, f"{str(self.basename)}-backups") |
| 189 | + try: |
| 190 | + self._backup_engine = None |
| 191 | + if not os.path.isdir(self._backup_path): |
| 192 | + os.makedirs(self._backup_path, exist_ok=True) |
| 193 | + testfile = tempfile.TemporaryFile(dir=self._backup_path) |
| 194 | + testfile.close() |
| 195 | + except PermissionError: |
| 196 | + self.log.warning( |
| 197 | + f'Unable to make directory for path "{self._backup_path}",' |
| 198 | + f"disabling backups." |
| 199 | + ) |
| 200 | + except OSError: |
| 201 | + self.log.warning( |
| 202 | + f'Unable to create files in "{self._backup_path}",' f"disabling backups" |
| 203 | + ) |
| 204 | + else: |
| 205 | + self._backup_engine = rocksdb.BackupEngine(self._backup_path) |
| 206 | + |
| 207 | + async def backup_partition( |
| 208 | + self, tp: Union[TP, int], flush: bool = True, purge: bool = False, keep: int = 1 |
| 209 | + ) -> None: |
| 210 | + """Backup partition from this store. |
| 211 | +
|
| 212 | + This will be saved in a separate directory in the data directory called |
| 213 | + '{table-name}-backups'. |
| 214 | +
|
| 215 | + Arguments: |
| 216 | + tp: Partition to backup |
| 217 | + flush: Flush the memset before backing up the state of the table. |
| 218 | + purge: Purge old backups in the process |
| 219 | + keep: How many backups to keep after purging |
| 220 | +
|
| 221 | + This is only supported in newer versions of python-rocksdb which can read |
| 222 | + the RocksDB database using multi-process read access. |
| 223 | + See https://github.com/facebook/rocksdb/wiki/How-to-backup-RocksDB to know more. |
| 224 | + """ |
| 225 | + if self._backup_engine: |
| 226 | + partition = tp |
| 227 | + if isinstance(tp, TP): |
| 228 | + partition = tp.partition |
| 229 | + try: |
| 230 | + if flush: |
| 231 | + db = await self._try_open_db_for_partition(partition) |
| 232 | + else: |
| 233 | + db = self.rocksdb_options.open( |
| 234 | + self.partition_path(partition), read_only=True |
| 235 | + ) |
| 236 | + self._backup_engine.create_backup(db, flush_before_backup=flush) |
| 237 | + if purge: |
| 238 | + self._backup_engine.purge_old_backups(keep) |
| 239 | + except Exception: |
| 240 | + self.log.info(f"Unable to backup partition {partition}.") |
| 241 | + |
| 242 | + def restore_backup( |
| 243 | + self, tp: Union[TP, int], latest: bool = True, backup_id: int = 0 |
| 244 | + ) -> None: |
| 245 | + """Restore partition backup from this store. |
| 246 | +
|
| 247 | + Arguments: |
| 248 | + tp: Partition to restore |
| 249 | + latest: Restore the latest backup, set as False to restore a specific ID |
| 250 | + backup_id: Backup to restore |
| 251 | +
|
| 252 | + """ |
| 253 | + if self._backup_engine: |
| 254 | + partition = tp |
| 255 | + if isinstance(tp, TP): |
| 256 | + partition = tp.partition |
| 257 | + if latest: |
| 258 | + self._backup_engine.restore_latest_backup( |
| 259 | + str(self.partition_path(partition)), self._backup_path |
| 260 | + ) |
| 261 | + else: |
| 262 | + self._backup_engine.restore_backup( |
| 263 | + backup_id, str(self.partition_path(partition)), self._backup_path |
| 264 | + ) |
186 | 265 |
|
187 | 266 | def persisted_offset(self, tp: TP) -> Optional[int]: |
188 | 267 | """Return the last persisted offset. |
|
0 commit comments