feat: rsync delete timing (--delete-before/--delete-during/--delete-after/--delete-delay) #268

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

Implements real rsync deletion-timing so the flags actually select when extra-file deletion happens.

Design

Deletion stays derived only from the transmitted keep-set manifest, uses the symlink-safe bounded walker, and skips the .fastsync-stage dir. Timing is picked purely from the config on both ends; the STATUS_MANIFEST frame is now self-delimiting/position-independent.

  • Early modes (--delete-before, --delete-during/--del): sender does a full path-only pre-scan and transmits the keep-set BEFORE any file data; receiver deletes extras and acks STATUS_OK; the sender only streams once deletion committed (or aborts). Delete-before/during therefore delete even if a later transfer phase fails (destructive by definition, like rsync).
  • Commit modes (plain --delete, --delete-after, --delete-delay): manifest closes the data stream; deletion commits only after STATUS_FINISHED proves the whole transfer succeeded. This is also the default under plain --delete (not rsync's delete-during default) to preserve FastSync's commit-style safety.
  • -m: late deletion is committed by server.c only after the disk-writer thread has drained (fixes a delete-vs-in-flight-temp-file race); early deletion runs in the receive thread before any data is enqueued.
  • Every timing flag implies --delete; at most one timing flag allowed; conflicts rejected on both ends.

Wire / protocol

Two new config booleans (delete_during, delete_delay) are serialized alongside the existing delete_before/delete_after; PROTOCOL_VERSION bumped 2.7.0 -> 2.8.0 (peers must match exactly, enforced). No new STATUS codes.

Defaults

Plain --delete keeps today's behavior = delete-after (commit) semantics.

Documented divergences from rsync

  • FastSync streams in one directory scan with no per-directory generator pass, so --delete-during cannot interleave deletions per-directory; it selects the same early engine mode as --delete-before (identical observable success/failure behaviour).
  • --delete-delay converges with --delete-after (FastSync never snapshots the destination during data flow); same end-of-transfer commit & safety.
  • rsync's --delete default is delete-during; FastSync's default stays delete-after.
  • Early keep-set is a pre-scan snapshot; a source file that appears between pre-scan and the data pass is still sent but was not protected from deletion.

Tests

  • Unit: CLI acceptance of each flag (+ --del alias) with use_delete implied; rejection of conflicting timings / timing with --no-delete; config wire round-trips for the new fields; receiver validation rejects two timings; config_delete_timing_early() mapping.
  • Integration (TCP, single + -m): every flag removes extras on success; early modes delete a destination file blocking a nested write so the transfer then succeeds, whereas plain --delete/--delete-after/--delete-delay keep everything intact and fail (commit-style); early timing completes (no delete) when the server refuses deletion.
  • 27/27 unit groups pass; 151 feature + 26 other integration tests pass; strict-warnings build clean; clang-format 18 + cppcheck clean (CI image).
Implements real rsync deletion-timing so the flags actually select when extra-file deletion happens. ## Design Deletion stays derived only from the transmitted keep-set manifest, uses the symlink-safe bounded walker, and skips the .fastsync-stage dir. Timing is picked purely from the config on both ends; the STATUS_MANIFEST frame is now self-delimiting/position-independent. - Early modes (--delete-before, --delete-during/--del): sender does a full path-only pre-scan and transmits the keep-set BEFORE any file data; receiver deletes extras and acks STATUS_OK; the sender only streams once deletion committed (or aborts). Delete-before/during therefore delete even if a later transfer phase fails (destructive by definition, like rsync). - Commit modes (plain --delete, --delete-after, --delete-delay): manifest closes the data stream; deletion commits only after STATUS_FINISHED proves the whole transfer succeeded. This is also the default under plain --delete (not rsync's delete-during default) to preserve FastSync's commit-style safety. - -m: late deletion is committed by server.c only after the disk-writer thread has drained (fixes a delete-vs-in-flight-temp-file race); early deletion runs in the receive thread before any data is enqueued. - Every timing flag implies --delete; at most one timing flag allowed; conflicts rejected on both ends. ## Wire / protocol Two new config booleans (delete_during, delete_delay) are serialized alongside the existing delete_before/delete_after; PROTOCOL_VERSION bumped 2.7.0 -> 2.8.0 (peers must match exactly, enforced). No new STATUS codes. ## Defaults Plain --delete keeps today's behavior = delete-after (commit) semantics. ## Documented divergences from rsync - FastSync streams in one directory scan with no per-directory generator pass, so --delete-during cannot interleave deletions per-directory; it selects the same early engine mode as --delete-before (identical observable success/failure behaviour). - --delete-delay converges with --delete-after (FastSync never snapshots the destination during data flow); same end-of-transfer commit & safety. - rsync's --delete default is delete-during; FastSync's default stays delete-after. - Early keep-set is a pre-scan snapshot; a source file that appears between pre-scan and the data pass is still sent but was not protected from deletion. ## Tests - Unit: CLI acceptance of each flag (+ --del alias) with use_delete implied; rejection of conflicting timings / timing with --no-delete; config wire round-trips for the new fields; receiver validation rejects two timings; config_delete_timing_early() mapping. - Integration (TCP, single + -m): every flag removes extras on success; early modes delete a destination file blocking a nested write so the transfer then succeeds, whereas plain --delete/--delete-after/--delete-delay keep everything intact and fail (commit-style); early timing completes (no delete) when the server refuses deletion. - 27/27 unit groups pass; 151 feature + 26 other integration tests pass; strict-warnings build clean; clang-format 18 + cppcheck clean (CI image).
TapTap added 3 commits 2026-09-06 18:54:32 +02:00
Deletion timing is now real and selected by the four rsync flags plus the
plain --delete default. Wire protocol bumps to 2.8.0: two new config
booleans (delete_during, delete_delay) are serialized and validated, joining
the existing delete_before/delete_after.

- Early modes (--delete-before, --delete-during/--del): the sender pre-scans
  the whole tree (paths only), transmits the keep-set manifest BEFORE any
  file data, and the receiver removes extras and acks STATUS_OK; the sender
  only streams data after the deletion committed. Deletion is thus performed
  even if a later transfer phase fails (rsync delete-before/during are
  destructive by definition). FastSync streams in a single scan so it cannot
  interleave per-directory like rsync delete-during; --delete-during selects
  the same engine mode as --delete-before (documented divergence).
- Late/commit modes (plain --delete, --delete-after, --delete-delay): the
  manifest closes the data stream and deletion is committed only after
  STATUS_FINISHED proves the whole transfer succeeded, preserving FastSync's
  commit-style safety. --delete-delay converges with --delete-after because
  FastSync never snapshots the destination during data flow (documented).
- The STATUS_MANIFEST frame is now self-delimiting and position-independent.
  Single-threaded receivers delete before the success frame; the -m receiver
  hands the keep-set to server.c, which commits the deletion only after the
  disk writer thread has drained (fixes a delete-vs-in-flight-temp race).
- Every timing flag implies --delete; at most one timing flag is allowed.
- Each timing flag implies --delete, matching rsync; conflicts are rejected.
- CLI: each timing flag (+ --del alias) accepted and implies --delete;
  conflicting timings and a timing with --no-delete are rejected.
- Config: delete_during/delete_delay survive config_send/config_receive;
  two simultaneous timings are rejected by the receiver-side validation;
  config_delete_timing_early() mapping is unit-tested.
- Integration (TCP, single- and multithreaded): every flag removes extras on
  a successful transfer; early modes (--delete-before/--delete-during/--del)
  delete before data is applied so a destination file blocking a nested
  write is removed and the transfer succeeds, while plain --delete /
  --delete-after / --delete-delay keep it and fail with every extra intact
  (commit-style). Early timing also completes (without deleting) when the
  server refuses deletion.
docs: mark rsync delete-timing family implemented
CI / lint (pull_request) Successful in 26s
CI / sanitizers (address) (pull_request) Successful in 39s
CI / fuzz-build (pull_request) Successful in 16s
CI / sanitizers (undefined) (pull_request) Successful in 37s
CI / coverage (pull_request) Successful in 34s
CI / valgrind (pull_request) Successful in 37s
CI / build-and-test (pull_request) Successful in 6m22s
36c2c04910
RSYNC_COMPAT.md: flip --delete-before, --del/--delete-during, --delete-delay
and --delete-after to Implemented with precise notes (default-under--delete,
safety model, 2.7.0 -> 2.8.0 protocol bump, exact divergences from rsync).
README option tables list the new flags and the delete-after default.
TapTap added 4 commits 2026-09-06 19:35:37 +02:00
The late/commit path keeps the received keep-set in a local list until
STATUS_FINISHED. Error exits after it was parked (STATUS_ABORT, a failing
receive_status / non-FINISHED status, a second manifest frame, or a later
file/chunk/store failure) previously dropped the only reference and leaked up
to ~16 MB of path strings + pointer array per connection. Both failure labels
now discard the parked list exactly once; the successful FINISHED path still
hands ownership to *pending_manifest (the -m caller) without freeing it.
The receiver performs the whole bounded deletion walk (up to
MAX_SERVER_DELETE_COUNT unlinks) before answering the delete-before/during
manifest, so its STATUS_OK reply can take far longer than the default 60 s
per-message receive window. Waiting with the default would make the sender
abort AFTER the deletion had already committed on the receiver. Add a timed
receive variant (receive_status_timed / protocol_receive_n_data_timed) and use
it for the early-manifest ACK with a 1 h explicit deadline; connection errors
and EOF still abort immediately.
- Unit (leak guards): drive receiver_process_pending() past a parked keep-set
  into STATUS_ABORT, EOF, and a second manifest frame; each must return -1 with
  no manifest handed out. Verified leak-free under ASan.
- Unit: receive_status_timed reads a status and fails cleanly on EOF.
- Integration: test_late_flags_commit_only_after_success now parametrizes the
  -m path, proving a failed -m late-timing run preserves every extra and that
  the deferred manifest is dropped (never applied) when the writer fails.
docs: clarify --del alias, early keep-set caps, ACK wait, --no-delete conflict
CI / lint (pull_request) Successful in 25s
CI / sanitizers (undefined) (pull_request) Successful in 41s
CI / sanitizers (address) (pull_request) Successful in 42s
CI / fuzz-build (pull_request) Successful in 16s
CI / coverage (pull_request) Successful in 33s
CI / valgrind (pull_request) Successful in 36s
CI / build-and-test (pull_request) Successful in 6m51s
02679fe335
Usage help now gives --delete-during a complete description with --del on its
own line, and notes that timing flags imply --delete while timing+--no-delete
is rejected regardless of argument order. RSYNC_COMPAT.md documents: the
receiver's MAX_MANIFEST_ENTRIES/MAX_MANIFEST_BYTES caps now abort an early-mode
run before any data (previously only the deletion step failed), the extended
early-delete ACK deadline, and the order-independent flag-conflict policy.
Author
Owner

Merged into dev via local merge (2FA blocks server-side merge). Commits: delete-timing 265b1e7 (rsync delete timing, protocol 2.8.0) + basis-dest 01a5a93. Independent c-review APPROVE WITH NITS / REQUEST CHANGES; all findings fixed and re-verified. dev CI run #479: all jobs success (lint, build-and-test, sanitizers address+undefined, fuzz-build, coverage, valgrind). Closing without server merge.

Merged into dev via local merge (2FA blocks server-side merge). Commits: delete-timing 265b1e7 (rsync delete timing, protocol 2.8.0) + basis-dest 01a5a93. Independent c-review APPROVE WITH NITS / REQUEST CHANGES; all findings fixed and re-verified. dev CI run #479: all jobs success (lint, build-and-test, sanitizers address+undefined, fuzz-build, coverage, valgrind). Closing without server merge.
TapTap closed this pull request 2026-09-06 20:20:20 +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#268