|
| 1 | +import collections |
1 | 2 | import contextlib |
2 | 3 | import os |
3 | | -from typing import BinaryIO, Generator |
| 4 | +import threading |
| 5 | +from typing import BinaryIO, Generator, Optional, List |
4 | 6 |
|
5 | 7 | from typing_extensions import Final |
6 | 8 |
|
7 | 9 | from prime_backup.utils import pack_utils |
8 | 10 | from prime_backup.utils.io_types import SupportsReadAndSeek |
9 | 11 |
|
10 | 12 |
|
| 13 | +class PackFileObjectPool: |
| 14 | + def __init__(self, max_size: int = 4): |
| 15 | + if max_size <= 0: |
| 16 | + raise ValueError('max_size should be positive, got {}'.format(max_size)) |
| 17 | + self.__max_size = max_size |
| 18 | + self.__files: 'collections.OrderedDict[int, BinaryIO]' = collections.OrderedDict() |
| 19 | + self.__lock = threading.Lock() |
| 20 | + self.__closed = False |
| 21 | + |
| 22 | + def __enter__(self) -> 'PackFileObjectPool': |
| 23 | + return self |
| 24 | + |
| 25 | + def __exit__(self, exc_type, exc_val, exc_tb): |
| 26 | + self.close() |
| 27 | + |
| 28 | + @classmethod |
| 29 | + def __close_files(cls, files: List[BinaryIO]): |
| 30 | + errors: List[Exception] = [] |
| 31 | + for file in files: |
| 32 | + try: |
| 33 | + file.close() |
| 34 | + except Exception as e: |
| 35 | + errors.append(e) |
| 36 | + if len(errors) > 0: |
| 37 | + raise Exception(f'Failed to close {len(errors)} files: {errors}') |
| 38 | + |
| 39 | + @contextlib.contextmanager |
| 40 | + def acquire(self, pack_id: int) -> Generator[BinaryIO, None, None]: |
| 41 | + with self.__lock: |
| 42 | + if self.__closed: |
| 43 | + raise RuntimeError('file object pool is closed') |
| 44 | + existing_file: Optional[BinaryIO] = self.__files.pop(pack_id, None) |
| 45 | + |
| 46 | + file: BinaryIO |
| 47 | + if existing_file is None: |
| 48 | + file = open(pack_utils.get_pack_path(pack_id), 'rb') |
| 49 | + else: |
| 50 | + file = existing_file |
| 51 | + |
| 52 | + try: |
| 53 | + yield file |
| 54 | + finally: |
| 55 | + to_close_files: List[BinaryIO] = [] |
| 56 | + with self.__lock: |
| 57 | + if self.__closed: |
| 58 | + to_close_files.append(file) |
| 59 | + else: |
| 60 | + old_file = self.__files.pop(pack_id, None) |
| 61 | + self.__files[pack_id] = file |
| 62 | + self.__files.move_to_end(pack_id) |
| 63 | + |
| 64 | + if old_file is not None and old_file is not file: |
| 65 | + to_close_files.append(old_file) |
| 66 | + while len(self.__files) > self.__max_size: |
| 67 | + _, old_file = self.__files.popitem(last=False) |
| 68 | + to_close_files.append(old_file) |
| 69 | + self.__close_files(to_close_files) |
| 70 | + |
| 71 | + def close(self): |
| 72 | + with self.__lock: |
| 73 | + self.__closed = True |
| 74 | + files = list(self.__files.values()) |
| 75 | + self.__files.clear() |
| 76 | + self.__close_files(files) |
| 77 | + |
| 78 | + |
11 | 79 | class PackEntryReader: |
12 | 80 | def __init__(self, file_obj: BinaryIO, offset: int, length: int): |
13 | 81 | self.__file_obj: Final[BinaryIO] = file_obj |
@@ -52,7 +120,12 @@ def seek(self, offset: int, whence: int = 0): |
52 | 120 | class PackReader: |
53 | 121 | @classmethod |
54 | 122 | @contextlib.contextmanager |
55 | | - def open_entry(cls, pack_id: int, offset: int, length: int) -> Generator[SupportsReadAndSeek, None, None]: |
56 | | - pack_path = pack_utils.get_pack_path(pack_id) |
57 | | - with open(pack_path, 'rb') as fh: |
58 | | - yield PackEntryReader(fh, offset, length) |
| 123 | + def open_entry(cls, pack_id: int, offset: int, length: int, *, file_obj_pool: Optional[PackFileObjectPool] = None) -> Generator[SupportsReadAndSeek, None, None]: |
| 124 | + with contextlib.ExitStack() as es: |
| 125 | + file: BinaryIO |
| 126 | + if file_obj_pool is None: |
| 127 | + pack_path = pack_utils.get_pack_path(pack_id) |
| 128 | + file = es.enter_context(open(pack_path, 'rb')) |
| 129 | + else: |
| 130 | + file = es.enter_context(file_obj_pool.acquire(pack_id)) |
| 131 | + yield PackEntryReader(file, offset, length) |
0 commit comments