Skip to content

Commit 899c9ec

Browse files
committed
fix: chunk store_load over RPC to avoid msgpack BufferFull
Large repositories can have a chunks index exceeding the msgpack unpacker buffer size, causing BufferFull errors over SSH/RPC. Instead of loading the entire value in one RPC call, add two new server-side methods (store_get_size, store_load_chunk) and override store_load in RemoteRepository to fetch data in MAX_DATA_SIZE pieces, reassembling on the client side. Fixes #8440
1 parent e3a2c9b commit 899c9ec

4 files changed

Lines changed: 36 additions & 6 deletions

File tree

src/borg/archiver/create_cmd.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -576,8 +576,7 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser):
576576
from ._common import process_epilog
577577
from ._common import define_exclusion_group
578578

579-
create_epilog = process_epilog(
580-
"""
579+
create_epilog = process_epilog("""
581580
This command creates a backup archive containing all files found while recursively
582581
traversing all specified paths. Paths are added to the archive as they are given,
583582
which means that if relative paths are desired, the command must be run from the correct
@@ -783,8 +782,7 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser):
783782
784783
Borg supports paths with the slashdot hack to strip path prefixes here also.
785784
So, be careful not to unintentionally trigger that.
786-
"""
787-
)
785+
""")
788786

789787
subparser = ArgumentParser(parents=[common_parser], description=self.do_create.__doc__, epilog=create_epilog)
790788
subparsers.add_subcommand("create", subparser, help="create a backup")

src/borg/helpers/parseformat.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,11 +300,24 @@ def ChunkerParams(s):
300300
def FilesCacheMode(s):
301301
ENTRIES_MAP = dict(ctime="c", mtime="m", size="s", inode="i", rechunk="r", disabled="d")
302302
VALID_MODES = ("cis", "ims", "cs", "ms", "cr", "mr", "d", "s") # letters in alpha order
303+
WIN32_INVALID_MODES = ("cis", "cs", "cr") # modes containing ctime, invalid on Windows
303304
if s in VALID_MODES:
305+
if is_win32 and s in WIN32_INVALID_MODES:
306+
raise ArgumentTypeError(
307+
"ctime is not supported in files-cache mode on Windows "
308+
"(ctime means file creation time on Windows, not inode change time). "
309+
"Use an mtime-based mode instead."
310+
)
304311
return s
305312
entries = set(s.strip().split(","))
306313
if not entries <= set(ENTRIES_MAP):
307314
raise ArgumentTypeError("cache mode must be a comma-separated list of: %s" % ",".join(sorted(ENTRIES_MAP)))
315+
if is_win32 and "ctime" in entries:
316+
raise ArgumentTypeError(
317+
"ctime is not supported in files-cache mode on Windows "
318+
"(ctime means file creation time on Windows, not inode change time). "
319+
"Use an mtime-based mode instead."
320+
)
308321
short_entries = {ENTRIES_MAP[entry] for entry in entries}
309322
mode = "".join(sorted(short_entries))
310323
if mode not in VALID_MODES:

src/borg/remote.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ class RepositoryServer: # pragma: no cover
161161
"put_manifest",
162162
"store_list",
163163
"store_load",
164+
"store_load_chunk",
165+
"store_get_size",
164166
"store_store",
165167
"store_delete",
166168
"store_move",
@@ -1049,9 +1051,16 @@ def put_manifest(self, data):
10491051
def store_list(self, name, *, deleted=False):
10501052
"""actual remoting is done via self.call in the @api decorator"""
10511053

1052-
@api(since=parse_version("2.0.0b8"))
10531054
def store_load(self, name):
1054-
"""actual remoting is done via self.call in the @api decorator"""
1055+
# chunked fetch to avoid msgpack BufferFull on large repositories
1056+
total_size = self.call("store_get_size", {"name": name})
1057+
data = bytearray()
1058+
offset = 0
1059+
while offset < total_size:
1060+
chunk = self.call("store_load_chunk", {"name": name, "offset": offset, "size": MAX_DATA_SIZE})
1061+
data += chunk
1062+
offset += len(chunk)
1063+
return bytes(data)
10551064

10561065
@api(since=parse_version("2.0.0b8"))
10571066
def store_store(self, name, value):

src/borg/repository.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,16 @@ def store_store(self, name, value):
560560
self._lock_refresh()
561561
return self.store.store(name, value)
562562

563+
def store_get_size(self, name):
564+
self._lock_refresh()
565+
data = self.store.load(name)
566+
return len(data)
567+
568+
def store_load_chunk(self, name, offset, size):
569+
self._lock_refresh()
570+
data = self.store.load(name)
571+
return data[offset : offset + size]
572+
563573
def store_delete(self, name, *, deleted=False):
564574
self._lock_refresh()
565575
return self.store.delete(name, deleted=deleted)

0 commit comments

Comments
 (0)