Skip to content

Commit 23337e8

Browse files
committed
introduce PackFileObjectPool to reduce file open syscalls on reading chunked blobs
1 parent 51718ec commit 23337e8

3 files changed

Lines changed: 105 additions & 20 deletions

File tree

prime_backup/action/helpers/blob_exporter.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from prime_backup import logger
1212
from prime_backup.action.helpers.chunk_io import ChunkIO
13+
from prime_backup.action.helpers.pack_reader import PackFileObjectPool
1314
from prime_backup.compressors import CompressMethod
1415
from prime_backup.compressors import Compressor
1516
from prime_backup.db.session import DbSession
@@ -171,10 +172,10 @@ def __export_to_fs_chunked(self, output_path: Path):
171172
blob_chunks = self.blob_chunks_getter.get(self.blob.id)
172173
hash_method = chunk_utils.get_hash_method()
173174

174-
with open(output_path, 'wb') as f_out:
175+
with open(output_path, 'wb') as f_out, PackFileObjectPool() as pack_file_obj_pool:
175176
for oc in blob_chunks:
176177
bypass_reader: Optional[BypassReader] = None
177-
with ChunkIO(oc.chunk).open_decompressed() as f_in:
178+
with ChunkIO(oc.chunk, pack_file_obj_pool=pack_file_obj_pool).open_decompressed() as f_in:
178179
if self.verify_blob:
179180
bypass_reader = BypassReader(f_in, calc_hash=True, hash_method=hash_method)
180181
file_utils.copy_file_obj_fast(bypass_reader, f_out, estimate_read_size=oc.chunk.raw_size)
@@ -217,18 +218,21 @@ def __export_as_reader_direct(self, reader_csm: Callable[[SupportsReadBytes], An
217218
def __export_as_reader_chunked(self, reader_csm: Callable[[SupportsReadBytes], Any]):
218219
blob_chunks = self.blob_chunks_getter.get(self.blob.id)
219220
exit_flag = False
221+
gen_yielded = False
220222

221223
def open_chunk_gen() -> Generator[_OpenedChunk, None, None]:
222224
hash_method = chunk_utils.get_hash_method()
223225
for oc in blob_chunks:
224-
with ChunkIO(oc.chunk).open_decompressed() as f_in:
226+
with ChunkIO(oc.chunk, pack_file_obj_pool=pack_file_obj_pool).open_decompressed() as f_in:
225227
try:
226228
peek_reader = _PeekReader(f_in, 32 * 1024)
227229
peek_reader.peek()
228230
except Exception as e:
229231
self.logger.error(f'Failed to peek-read chunk {oc.chunk.hash} in pack entry {oc.chunk.pack_entry.pack_id}@{oc.chunk.pack_entry.offset} for {oc}: {e}')
230232
raise
231233

234+
nonlocal gen_yielded
235+
gen_yielded = True
232236
if self.verify_blob:
233237
def verify_callback(ck: ChunkInfo, br: BypassReader):
234238
self.__verify_exported_chunk(ck, br.get_read_len(), br.get_hash())
@@ -242,14 +246,16 @@ def verify_callback(ck: ChunkInfo, br: BypassReader):
242246
if exit_flag:
243247
break
244248

245-
chunk_gen = open_chunk_gen()
246-
try:
247-
reader_csm(_CombinedChunksReader(chunk_gen))
248-
finally:
249-
# ensure possible opened chunk reader is closed
250-
exit_flag = True
251-
with contextlib.suppress(StopIteration):
252-
next(chunk_gen)
249+
with PackFileObjectPool() as pack_file_obj_pool:
250+
chunk_gen = open_chunk_gen()
251+
try:
252+
reader_csm(_CombinedChunksReader(chunk_gen))
253+
finally:
254+
# ensure possible opened ChunkIO is closed
255+
exit_flag = True
256+
if gen_yielded:
257+
with contextlib.suppress(StopIteration):
258+
next(chunk_gen)
253259

254260
def __verify_exported_blob(self, written_size: int, written_hash: str):
255261
self.__verify_exported_data(lambda: 'blob', self.blob.raw_size, self.blob.hash, written_size, written_hash)

prime_backup/action/helpers/chunk_io.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import contextlib
2-
from typing import Generator, Tuple
2+
from typing import Generator, Tuple, Optional
33

4-
from prime_backup.action.helpers.pack_reader import PackReader
4+
from prime_backup.action.helpers.pack_reader import PackReader, PackFileObjectPool
55
from prime_backup.compressors import Compressor
66
from prime_backup.types.chunk_info import ChunkInfo
77
from prime_backup.utils.bypass_io import BypassReader
88
from prime_backup.utils.io_types import SupportsReadBytes, SupportsReadAndSeek
99

1010

1111
class ChunkIO:
12-
def __init__(self, chunk: ChunkInfo):
12+
def __init__(self, chunk: ChunkInfo, *, pack_file_obj_pool: Optional[PackFileObjectPool] = None):
1313
self.chunk = chunk
14+
self.pack_file_obj_pool = pack_file_obj_pool
1415

1516
def __get_pack_id(self) -> int:
1617
if self.chunk.pack_entry.pack_id <= 0:
@@ -19,7 +20,12 @@ def __get_pack_id(self) -> int:
1920

2021
@contextlib.contextmanager
2122
def open_raw(self) -> Generator[SupportsReadAndSeek, None, None]:
22-
with PackReader.open_entry(self.__get_pack_id(), self.chunk.pack_entry.offset, self.chunk.stored_size) as reader:
23+
with PackReader.open_entry(
24+
pack_id=self.__get_pack_id(),
25+
offset=self.chunk.pack_entry.offset,
26+
length=self.chunk.stored_size,
27+
file_obj_pool=self.pack_file_obj_pool
28+
) as reader:
2329
yield reader
2430

2531
@contextlib.contextmanager

prime_backup/action/helpers/pack_reader.py

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,81 @@
1+
import collections
12
import contextlib
23
import os
3-
from typing import BinaryIO, Generator
4+
import threading
5+
from typing import BinaryIO, Generator, Optional, List
46

57
from typing_extensions import Final
68

79
from prime_backup.utils import pack_utils
810
from prime_backup.utils.io_types import SupportsReadAndSeek
911

1012

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+
1179
class PackEntryReader:
1280
def __init__(self, file_obj: BinaryIO, offset: int, length: int):
1381
self.__file_obj: Final[BinaryIO] = file_obj
@@ -52,7 +120,12 @@ def seek(self, offset: int, whence: int = 0):
52120
class PackReader:
53121
@classmethod
54122
@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

Comments
 (0)