Skip to content

Commit 09c3852

Browse files
committed
improve _CombinedChunksReader verification calls
1 parent 23337e8 commit 09c3852

2 files changed

Lines changed: 88 additions & 20 deletions

File tree

prime_backup/action/helpers/blob_exporter.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import contextlib
21
import dataclasses
32
import functools
43
import threading
@@ -93,10 +92,12 @@ def __init__(self, chunks_gen: Generator[_OpenedChunk, None, None]):
9392
self.idx = 0
9493
self.chunks_gen = chunks_gen
9594
self.current: Optional[_OpenedChunk] = None
95+
self.current_read_len = 0
96+
self.current_verified = False
9697
self.reach_end = False
9798

9899
def read(self, length: int = -1) -> bytes:
99-
if self.reach_end:
100+
if self.reach_end or length == 0:
100101
return b''
101102
if self.current is None:
102103
if not self.__switch_to_next():
@@ -116,16 +117,33 @@ def read(self, length: int = -1) -> bytes:
116117
logger.get().error(f'Failed to read {to_read} bytes from chunk {self.current.offset_chunk}: {e}')
117118
raise
118119
total_read += len(buf)
120+
self.current_read_len += len(buf)
119121
results.append(buf)
120-
if to_read == -1 or len(buf) < to_read:
121-
self.current.verify_callback()
122+
if self.current_read_len >= self.current.offset_chunk.chunk.raw_size:
123+
self.__verify_current()
122124
if not self.__switch_to_next():
123125
break
126+
elif to_read == -1 or len(buf) < to_read:
127+
raise EOFError('chunk {} exhausted after reading {} bytes, expected {}'.format(
128+
self.current.offset_chunk, self.current_read_len, self.current.offset_chunk.chunk.raw_size,
129+
))
124130
return b''.join(results)
125131

132+
def close(self):
133+
self.chunks_gen.close()
134+
self.current = None
135+
self.reach_end = True
136+
137+
def __verify_current(self):
138+
if self.current is not None and not self.current_verified:
139+
self.current.verify_callback()
140+
self.current_verified = True
141+
126142
def __switch_to_next(self) -> bool:
127143
try:
128144
self.current = next(self.chunks_gen)
145+
self.current_read_len = 0
146+
self.current_verified = False
129147
return True
130148
except StopIteration:
131149
self.reach_end = True
@@ -217,8 +235,6 @@ def __export_as_reader_direct(self, reader_csm: Callable[[SupportsReadBytes], An
217235

218236
def __export_as_reader_chunked(self, reader_csm: Callable[[SupportsReadBytes], Any]):
219237
blob_chunks = self.blob_chunks_getter.get(self.blob.id)
220-
exit_flag = False
221-
gen_yielded = False
222238

223239
def open_chunk_gen() -> Generator[_OpenedChunk, None, None]:
224240
hash_method = chunk_utils.get_hash_method()
@@ -231,8 +247,6 @@ def open_chunk_gen() -> Generator[_OpenedChunk, None, None]:
231247
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}')
232248
raise
233249

234-
nonlocal gen_yielded
235-
gen_yielded = True
236250
if self.verify_blob:
237251
def verify_callback(ck: ChunkInfo, br: BypassReader):
238252
self.__verify_exported_chunk(ck, br.get_read_len(), br.get_hash())
@@ -242,20 +256,13 @@ def verify_callback(ck: ChunkInfo, br: BypassReader):
242256
else:
243257
yield _OpenedChunk(oc, peek_reader, lambda: None)
244258

245-
nonlocal exit_flag
246-
if exit_flag:
247-
break
248-
249259
with PackFileObjectPool() as pack_file_obj_pool:
250260
chunk_gen = open_chunk_gen()
261+
reader = _CombinedChunksReader(chunk_gen)
251262
try:
252-
reader_csm(_CombinedChunksReader(chunk_gen))
263+
reader_csm(reader)
253264
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)
265+
reader.close()
259266

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

tests/test_pack_storage.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dataclasses
22
from io import BytesIO
33
from pathlib import Path
4-
from typing import Dict, Generator, Optional
4+
from typing import Dict, Generator, List, Optional
55

66
import pytest
77

@@ -11,6 +11,7 @@
1111
from prime_backup.action.delete_backup_file_action import DeleteBackupFileAction
1212
from prime_backup.action.export_backup_action_tar import ExportBackupToTarAction
1313
from prime_backup.action.get_pack_action import GetPackByFileNamePrefixAction, GetPackByIdAction
14+
from prime_backup.action.helpers.blob_exporter import _CombinedChunksReader, _OpenedChunk
1415
from prime_backup.action.helpers.chunk_io import ChunkIO
1516
from prime_backup.action.helpers.pack_reader import PackEntryReader
1617
from prime_backup.action.helpers.pack_writer import PackWriter
@@ -28,7 +29,7 @@
2829
from prime_backup.db.session import DbSession
2930
from prime_backup.exceptions import PackFileNameNotUnique
3031
from prime_backup.types.backup_info import BackupInfo
31-
from prime_backup.types.chunk_info import ChunkInfo
32+
from prime_backup.types.chunk_info import ChunkInfo, OffsetChunkInfo
3233
from prime_backup.types.chunk_method import ChunkMethod
3334
from prime_backup.types.hash_method import HashMethod
3435
from prime_backup.types.operator import Operator
@@ -317,6 +318,66 @@ def test_pack_entry_reader_seek_uses_entry_relative_offsets() -> None:
317318
reader.seek(0, 12345)
318319

319320

321+
def test_combined_chunks_reader_verifies_exactly_consumed_final_chunk() -> None:
322+
verified_chunks: List[int] = []
323+
generator_closed = False
324+
325+
def make_opened_chunk(chunk_id: int, data: bytes) -> _OpenedChunk:
326+
chunk = ChunkInfo(
327+
id=chunk_id,
328+
hash=str(chunk_id),
329+
compress=CompressMethod.plain,
330+
raw_size=len(data),
331+
stored_size=len(data),
332+
pack_entry=PackEntryLocation(chunk_id, 0),
333+
)
334+
return _OpenedChunk(
335+
OffsetChunkInfo(0, chunk),
336+
BytesIO(data),
337+
lambda: verified_chunks.append(chunk_id),
338+
)
339+
340+
def opened_chunks() -> Generator[_OpenedChunk, None, None]:
341+
nonlocal generator_closed
342+
try:
343+
yield make_opened_chunk(1, b'ab')
344+
yield make_opened_chunk(2, b'cd')
345+
finally:
346+
generator_closed = True
347+
348+
reader = _CombinedChunksReader(opened_chunks())
349+
assert reader.read(4) == b'abcd'
350+
assert verified_chunks == [1, 2]
351+
352+
reader.close()
353+
assert verified_chunks == [1, 2]
354+
assert generator_closed
355+
356+
357+
def test_combined_chunks_reader_raises_eof_error_on_short_chunk() -> None:
358+
chunk = ChunkInfo(
359+
id=1,
360+
hash='short',
361+
compress=CompressMethod.plain,
362+
raw_size=4,
363+
stored_size=2,
364+
pack_entry=PackEntryLocation(1, 0),
365+
)
366+
def opened_chunks() -> Generator[_OpenedChunk, None, None]:
367+
yield _OpenedChunk(
368+
OffsetChunkInfo(0, chunk),
369+
BytesIO(b'ab'),
370+
lambda: None,
371+
)
372+
373+
reader = _CombinedChunksReader(opened_chunks())
374+
375+
with pytest.raises(EOFError, match='exhausted after reading 2 bytes, expected 4'):
376+
reader.read()
377+
378+
reader.close()
379+
380+
320381
def test_scan_unknown_pack_files_keeps_known_derived_pack_file_names(env: PackStorageEnv) -> None:
321382
__create_backup()
322383

0 commit comments

Comments
 (0)