Skip to content

gather: read multiple byte ranges with one call, fixes #211 - #231

Merged
ThomasWaldmann merged 2 commits into
borgbackup:mainfrom
ThomasWaldmann:gather-211
Sep 26, 2026
Merged

ThomasWaldmann merged 2 commits into
borgbackup:mainfrom
ThomasWaldmann:gather-211

Conversation

@ThomasWaldmann

@ThomasWaldmann ThomasWaldmann commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Fixes #211.

Store.gather(sources, namespace=, deleted=) / BackendBase.gather(sources) take (name, offset, size) tuples (same format as defrag) and return the contents of all ranges concatenated, in the order given. A short read raises ReadRangeError; the caller knows the sizes it requested, so it can split the result (e.g. into memoryview slices).

  • BackendBase.gather: one partial load per range, works for all backends. defrag is now implemented on top of it (gather, then store the new item) - a behavior-neutral refactor, the existing defrag tests pass unchanged.
  • Sources validation: validate_sources checks each tuple and returns a new list, which the callers use. So a generator works, too: validating consumes it, iterating over the original sources again would silently yield nothing.
  • posixfs: the only backend that overrides gather, to check the read permission of all sources before reading anything. defrag only checks the target permission itself now, the sources are checked by gather. This also fixes which names are checked: defrag checked namespace + name, but the names it gets already include the namespace, so it checked a nonexistent path (only the parent-directory fallback made that pass).
  • rest: POST /?cmd=gather with the sources as JSON body; the server gathers with one roundtrip and answers with the concatenated bytes (application/octet-stream, 416 on a short read). The client keeps its own server-side defrag (building it on the client's gather would download and re-upload the data).
  • Store.gather: ranges of items in a cached namespace (writethrough / mirror) are served like load does it, all other ranges go to the backend with one call. New stats: gather_calls/time/volume, backend_gather_calls/volume.
  • Zero-size ranges are skipped in the base implementation (rclone rejects an empty range request).
  • Docs + changelog (0.7.0: new API and new REST command).

Separate commit, a fix for an existing bug that gather also hits:

  • caching: partial loads with a negative offset from the primary backend. In a cached namespace, a partial load that loads the whole item from the primary backend (mirror mode, or a writethrough cache miss) sliced value[offset:offset + size]. With a negative offset, that end index is wrong, e.g. offset=-3, size=3 gave b"" instead of the last 3 bytes. A writethrough cache hit was correct, so the result depended on the cache state. Now a negative offset is made absolute before slicing.

Tests: store (nesting, soft-deleted, short read, invalid sources, generator sources, stats keys), all backends, cache interaction, negative offsets in both cache modes, posixfs permissions, REST server (raw HTTP) and REST client.

🤖 Generated with Claude Code

@ThomasWaldmann
ThomasWaldmann force-pushed the gather-211 branch 4 times, most recently from b129214 to 8c24d4d Compare September 25, 2026 22:46
ThomasWaldmann and others added 2 commits September 26, 2026 11:00
Store.gather(sources, namespace=, deleted=) / BackendBase.gather(sources) take a list of
(name, offset, size) tuples (like defrag) and return the contents of all ranges concatenated,
in the order given. A short read raises ReadRangeError, the caller splits the result knowing
the sizes it requested.

- BackendBase.gather: one partial load per range, works for all backends.
  defrag is now implemented on top of it (gather, then store the new item).
- posixfs: checks the read permission of all sources before reading anything.
  defrag only checks the target permission itself, the sources are checked by gather.
- rest: POST /?cmd=gather with the sources as JSON body, the server gathers with one
  roundtrip and answers with the concatenated bytes (application/octet-stream).
  A server that does not know the command gives a clear error.
- Store.gather: ranges of items in a cached namespace are served like load does it,
  all other ranges go to the backend with one gather call. Stats: gather_calls/time/volume,
  backend_gather_calls/volume.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ckend

In a cached namespace, a partial load that has to load the whole item from the
primary backend (mirror mode, or a cache miss in writethrough mode) sliced the
range out of the item as value[offset:offset + size]. With a negative offset,
that end index is wrong, e.g. offset=-3, size=3 gave value[-3:0] == b"" instead
of the last 3 bytes. A writethrough cache hit was correct, so the result depended
on the cache state. gather has the same issue for ranges of cached items.

Make a negative offset absolute (relative to the item size) before slicing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ThomasWaldmann
ThomasWaldmann merged commit 0d1923e into borgbackup:main Sep 26, 2026
9 checks passed
@ThomasWaldmann
ThomasWaldmann deleted the gather-211 branch September 26, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

implement gather

1 participant