feat: --append / --append-verify tail resume #272

Closed
TapTap wants to merge 0 commits from feat/p3-append into dev
Owner

Design

Tail-only resume model. When the per-file STATUS_CHECK finds an existing
destination file that is shorter than the source, an append mode stops the
normal delta/full transfer. The receiver sends a new STATUS_APPEND frame with
the resume offset (the retained prefix length); the sender transmits ONLY the
tail (STATUS_APPEND_DATA). The receiver rebuilds the full file in memory
(prefix + tail) and installs it through the normal atomic store engine, so
--inplace, --partial/--partial-dir, --delay-updates, --backup,
--existing/--ignore-existing/--update and delete-manifest behaviour are all
unchanged. The result is always byte-identical to the source when the prefix
matches.

Prefix-verify model. --append sends the tail without content-verifying the
retained prefix (rsync parity): a wrong prefix is kept, so the result is
prefix+tail and can differ from the source — this documented risk is the
plain-append safety statement. --append-verify first exchanges the source
prefix xxHash64 (STATUS_APPEND_SIG); the receiver compares it to the retained
prefix and replies STATUS_APPEND_OK (send tail) or STATUS_NEXT (mismatch →
the sender falls back to a clean full transfer), so a mismatched prefix is
never silently appended to and the result is always byte-exact.

Protocol bump. New frames STATUS_APPEND / STATUS_APPEND_SIG / STATUS_APPEND_OK / STATUS_APPEND_DATA change the wire, so PROTOCOL_VERSION
was bumped 2.9.0 → 2.10.0 (src/shared/config.h; peers must match). The
append/append_verify booleans already crossed the wire.

CLI / incompatibilities (verified against rsync). Both flags imply
--incremental (the handshake needs it) and are rejected up front with
-s (chunk serialization) and --whole-file (never a silent full transfer).
When both spellings are given --append-verify wins. --append+--inplace
works (FastSync still reconstructs+renames, which is safer than rsync's in-place
write: an interrupted resume never leaves a partial/corrupt destination).

Tests

  • CLI unit: acceptance (parse + imply incremental + validate), rejection of
    -s and --whole-file, removal from the unimplemented reject list.
  • Config wire round-trip for append/append_verify.
  • Unit: append_resume_eligible / append_tail_length pure resume math.
  • Integration (tests/integration/test_append.py): matching-prefix resume is
    byte-identical AND tail-only (wire bytes << source size, via a byte-counting
    proxy); plain --append with a wrong prefix keeps prefix+tail (parity);
    --append-verify with a wrong prefix falls back to a byte-exact full transfer;
    --append + --inplace and --append -m.

Verified: full unit suite (27/27), full integration suite (250 passed, 10
skipped, 1 xpassed; no regressions), CI-identical clang-format (v9 image) and
cppcheck (exit 0).

Docs

RSYNC_COMPAT.md rows 86–87 (--append, --append-verify) flipped to
Implemented with precise Notes plus a Phase-3 append-wave notes block. The
Summary count line is deliberately untouched.

## Design **Tail-only resume model.** When the per-file `STATUS_CHECK` finds an existing destination file that is **shorter** than the source, an append mode stops the normal delta/full transfer. The receiver sends a new `STATUS_APPEND` frame with the resume offset (the retained prefix length); the sender transmits ONLY the tail (`STATUS_APPEND_DATA`). The receiver rebuilds the full file in memory (prefix + tail) and installs it through the **normal atomic store engine**, so `--inplace`, `--partial`/`--partial-dir`, `--delay-updates`, `--backup`, `--existing`/`--ignore-existing`/`--update` and delete-manifest behaviour are all unchanged. The result is always byte-identical to the source when the prefix matches. **Prefix-verify model.** `--append` sends the tail without content-verifying the retained prefix (**rsync parity**): a wrong prefix is kept, so the result is `prefix+tail` and can differ from the source — this documented risk is the plain-append safety statement. `--append-verify` first exchanges the source prefix xxHash64 (`STATUS_APPEND_SIG`); the receiver compares it to the retained prefix and replies `STATUS_APPEND_OK` (send tail) or `STATUS_NEXT` (mismatch → the sender falls back to a **clean full transfer**), so a mismatched prefix is **never** silently appended to and the result is always byte-exact. **Protocol bump.** New frames `STATUS_APPEND / STATUS_APPEND_SIG / STATUS_APPEND_OK / STATUS_APPEND_DATA` change the wire, so `PROTOCOL_VERSION` was bumped **2.9.0 → 2.10.0** (src/shared/config.h; peers must match). The `append`/`append_verify` booleans already crossed the wire. **CLI / incompatibilities (verified against rsync).** Both flags imply `--incremental` (the handshake needs it) and are rejected up front with `-s` (chunk serialization) and `--whole-file` (never a silent full transfer). When both spellings are given `--append-verify` wins. `--append`+`--inplace` works (FastSync still reconstructs+renames, which is safer than rsync's in-place write: an interrupted resume never leaves a partial/corrupt destination). ## Tests - **CLI unit**: acceptance (parse + imply incremental + validate), rejection of `-s` and `--whole-file`, removal from the unimplemented reject list. - **Config wire round-trip** for `append`/`append_verify`. - **Unit**: `append_resume_eligible` / `append_tail_length` pure resume math. - **Integration** (`tests/integration/test_append.py`): matching-prefix resume is byte-identical AND tail-only (wire bytes << source size, via a byte-counting proxy); plain `--append` with a wrong prefix keeps `prefix+tail` (parity); `--append-verify` with a wrong prefix falls back to a byte-exact full transfer; `--append` + `--inplace` and `--append -m`. Verified: full unit suite (27/27), full integration suite (250 passed, 10 skipped, 1 xpassed; no regressions), CI-identical clang-format (v9 image) and cppcheck (exit 0). ## Docs `RSYNC_COMPAT.md` rows 86–87 (`--append`, `--append-verify`) flipped to Implemented with precise Notes plus a Phase-3 append-wave notes block. The **Summary** count line is deliberately untouched.
TapTap added 3 commits 2026-09-07 15:45:34 +02:00
Implement rsync's append modes: when an existing destination file is SHORTER
than the source, the receiver negotiates a resume offset and only the tail is
transferred; the full file (retained prefix + tail) is rebuilt and installed
through the normal atomic store path, so the result is byte-identical to the
source whenever the prefix matches.

- --append: sends the tail without content-verifying the retained prefix
  (rsync parity; the documented prefix-trust risk).
- --append-verify: verifies the retained prefix against the source's prefix
  xxHash64 before appending and, on a mismatch, falls back to a clean full
  transfer (never a corrupt prefix+tail blend).

New wire frames STATUS_APPEND / STATUS_APPEND_SIG / STATUS_APPEND_OK /
STATUS_APPEND_DATA; PROTOCOL_VERSION bumped 2.9.0 -> 2.10.0 (peers must match).
Both flags imply --incremental and are incompatible with -s (chunk
serialization) and --whole-file (rejected up front). Respects --inplace,
--partial/--partial-dir and --delay-updates via the shared store engine.
- CLI: --append/--append-verify acceptance (parse + imply --incremental,
  validate) and incompatibility rejection with -s and --whole-file; both
  removed from the unimplemented reject list.
- Config: on-the-wire append/append_verify round-trip.
- Unit: append_resume_eligible / append_tail_length pure resume math.
- Integration (test_append.py): matching-prefix resume is byte-identical and
  tail-only (wire bytes << source size); --append with a wrong prefix keeps
  prefix+tail (rsync parity) while --append-verify detects the mismatch and
  falls back to a byte-exact full transfer; --append with --inplace and -m.
docs: mark --append / --append-verify implemented in RSYNC_COMPAT
CI / lint (pull_request) Successful in 49s
CI / sanitizers (undefined) (pull_request) Successful in 50s
CI / sanitizers (address) (pull_request) Successful in 51s
CI / fuzz-build (pull_request) Successful in 17s
CI / coverage (pull_request) Successful in 42s
CI / valgrind (pull_request) Successful in 36s
CI / build-and-test (pull_request) Successful in 12m52s
bfb3a531e9
Flip both rows to Implemented with precise Notes covering the tail-resume
model, the prefix-verify semantics (and the plain-append rsync-parity safety
statement), the new wire frames, and the PROTOCOL_VERSION 2.9.0 -> 2.10.0 bump.
Add a Phase-3 append-wave implementation-notes block. The Summary count line
is deliberately left untouched.
Author
Owner

Merged into dev via local merge (2FA blocks server-side merge). missing-args 681d7a8, append f99e6e1, checksum-choice 4c98744, recount 829e760. Independent c-reviews: missing-args REQUEST CHANGES (absent-parent no-op blocker fixed); append REQUEST CHANGES (blocker verified already-handled + tested, sentinel guard for streamed/-f sources); checksum-choice APPROVE WITH NITS (xxh3 alias removed, digest length pinned). dev CI run #494: all jobs success. Closing without server merge.

Merged into dev via local merge (2FA blocks server-side merge). missing-args 681d7a8, append f99e6e1, checksum-choice 4c98744, recount 829e760. Independent c-reviews: missing-args REQUEST CHANGES (absent-parent no-op blocker fixed); append REQUEST CHANGES (blocker verified already-handled + tested, sentinel guard for streamed/-f sources); checksum-choice APPROVE WITH NITS (xxh3 alias removed, digest length pinned). dev CI run #494: all jobs success. Closing without server merge.
TapTap closed this pull request 2026-09-07 20:25:59 +02:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: TapTap/FastSync#272