fix(parity): ⚠️ residual burn-down + review fixes (protocol 2.27.0) #301

Merged
TapTap merged 17 commits from feat/parity-fixes into dev 2026-09-18 22:19:26 +02:00
Owner

Parity residual burn-down (Tiers 1–3) + review fixes

Stacks on the differential-gate PR; once that merges, the gate merge drops out of this diff.

Stats / delete / output (fix/parity-stats)

  • --delete-delay counts only actual removals; --stats gains the (reg/dir/link/special) breakdown and counts only transferred regulars; --progress prints the leading ./ line and correct to-chk; %C uses the selected transfer checksum; checksum_digest_file supports md4/sha1/none. --out-format reclassified ❌ (%b/delta-%c are protocol-specific).

Options / output lines (fix/parity-options, protocol 2.26.0 → 2.27.0, new report_deletes bool)

  • --bwlimit parses/throttles exactly like rsync 3.4.1; --ignore-errors matches rsync (skip unreadable subdirs, IO-error deletion suppression, exit 23); --info=name/flist/del/remove/nonreg/progress emit rsync-format lines. -M over daemon/TCP and receiver-side protect/risk reclassified ❌.

Filesystem / selection (fix/parity-fs)

  • -d/--dirs recreates empty directories; -R --no-implied-dirs --files-from creates the missing implied parent (was a false ✅); --iconv reproduces rsync's push direction. --temp-dir (absolute), basis dirs, --delay-updates, --fuzzy, --dry-run reclassified ❌ with reproduced residuals.

Review-wave fixes

  • BLOCKER: uninitialized PipelineContextSender.delete_suppressed (silently dropped the delete keep-set under -m).
  • Receiver no longer retains every deleted path unless report_deletes; pipeline-create leak; --bwlimit overflow UB; dead LOG_INFO_BACKUP; duplicated path helper; lstat for empty-dir metadata; --delete-delay budget claim corrected (row → ⚠️); --fuzzy overclaim softened; xdist fixture collisions; non-privileged --ignore-errors unit test.

Matrix: 111 ✅ / 13 ⚠️ / 33 ❌ = 157.

Verified: unit 44/44, ASan 44/44, full integration 810 passed, differential gate strict 50 passed, clang-format + cppcheck clean, delete stress 5× green.

## Parity residual burn-down (Tiers 1–3) + review fixes Stacks on the differential-gate PR; once that merges, the gate merge drops out of this diff. **Stats / delete / output** (`fix/parity-stats`) - `--delete-delay` counts only actual removals; `--stats` gains the `(reg/dir/link/special)` breakdown and counts only transferred regulars; `--progress` prints the leading `./` line and correct `to-chk`; `%C` uses the selected transfer checksum; `checksum_digest_file` supports md4/sha1/none. `--out-format` reclassified ❌ (`%b`/delta-`%c` are protocol-specific). **Options / output lines** (`fix/parity-options`, protocol 2.26.0 → **2.27.0**, new `report_deletes` bool) - `--bwlimit` parses/throttles exactly like rsync 3.4.1; `--ignore-errors` matches rsync (skip unreadable subdirs, IO-error deletion suppression, exit 23); `--info=name/flist/del/remove/nonreg/progress` emit rsync-format lines. `-M` over daemon/TCP and receiver-side `protect`/`risk` reclassified ❌. **Filesystem / selection** (`fix/parity-fs`) - `-d/--dirs` recreates empty directories; `-R --no-implied-dirs --files-from` creates the missing implied parent (was a false ✅); `--iconv` reproduces rsync's push direction. `--temp-dir` (absolute), basis dirs, `--delay-updates`, `--fuzzy`, `--dry-run` reclassified ❌ with reproduced residuals. **Review-wave fixes** - BLOCKER: uninitialized `PipelineContextSender.delete_suppressed` (silently dropped the delete keep-set under `-m`). - Receiver no longer retains every deleted path unless `report_deletes`; pipeline-create leak; `--bwlimit` overflow UB; dead `LOG_INFO_BACKUP`; duplicated path helper; `lstat` for empty-dir metadata; `--delete-delay` budget claim corrected (row → ⚠️); `--fuzzy` overclaim softened; xdist fixture collisions; non-privileged `--ignore-errors` unit test. Matrix: **111 ✅ / 13 ⚠️ / 33 ❌ = 157**. Verified: unit 44/44, ASan 44/44, full integration 810 passed, differential gate strict 50 passed, clang-format + cppcheck clean, delete stress 5× green.
TapTap added 18 commits 2026-09-18 22:15:51 +02:00
test(parity): add differential rsync-parity CI gate
CI / lint (pull_request) Successful in 1m42s
CI / parity-full (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
CI / parity-fast (pull_request) Successful in 19s
CI / build-and-test (pull_request) Successful in 48s
b705fb807f
Add tests/integration/test_differential_parity.py plus a shared
parity_harness.py and a data-driven parity_caveats.py allowlist.  The gate
runs real rsync 3.4.1 and FastSync over the same corpora, compares the
destination trees (paths, hashes, symlink targets, modes, hard-link
grouping) and the normalized -i/--stats/--out-format output, and fails on
any difference not listed in the allowlist.  Stale allowlist entries warn
(or fail under FASTSYNC_PARITY_STRICT=1) so the residual list shrinks.

Register parity/parity_ci markers and wire a fast PR job (parity_ci) plus a
push-only full job (parity, strict) into .gitea/workflows/ci.yaml.
Document the gate and the allowlist workflow in tests/integration/README.md.
- --delete-delay: count/track only entries actually removed; a directory
  refilled before commit (ENOTEMPTY) no longer inflates Number of deleted
  files or the --max-delete budget (unit + integration + rsync differential).
- --stats: per-type Number of files breakdown; only stored regular files
  count as transferred; transferred/literal byte totals and Total file size
  (symlink target lengths) now match rsync for whole-file transfers.
- --progress: print the leading ./ root line and include the root entry in
  the to-chk denominator (single-file output byte-identical to rsync).
- --out-format %C: use the selected transfer checksum and render every
  algorithm exactly like rsync; checksum_digest_file gains md4/sha1/none.
  Reclassify --out-format to Divergent (protocol-specific %b/delta-%c).
- Docs: RSYNC_COMPAT tally 107/25/24, HANDOFF update. No wire change.
- Recursive scans emit a directory entry for every traversed directory that
  produced no transferred child, so empty source dirs (and dirs emptied by
  filtering) are recreated like rsync; -m prunes them, --files-from/--list-only
  never emit implicit dirs.
- A directory entry now replaces a destination regular file (rsync removes the
  non-directory) instead of aborting; confined to the secure parent fd.
- -R --no-implied-dirs --files-from: stop refusing a listed file whose parent
  is not listed; create the implied parent with default attributes (no source
  metadata is captured for it), matching rsync 3.4.1.
- Differential gate: drop min_size/empty_dirs_recursive/dirs_plain allowlist
  entries (dirs_plain now hands FastSync the same trailing-slash source as
  rsync); add differential test for the files-from implied parent.
- Docs: -d row -> Parity, tally 108/24/24.

No wire change (PROTOCOL_VERSION stays 2.26.0).
- --iconv now matches rsync's push direction: the destination charset is the
  client spec's REMOTE half, so a default receiver writes wire names verbatim;
  a server's own --iconv LOCAL overrides it (daemon charset analog). Updated
  unit + integration tests and added a default-server differential gate case.
- Empty-directory emission is gated behind a new ScannerOptions.emit_empty_dirs
  set only by the real sender, so low-level scanner helpers keep the historical
  file-only list.
- --temp-dir reclassified to Divergent: relative dirs match rsync exactly
  (resolved under the destination), but an absolute path is deliberately
  rejected by the confined receiver; differential test added.
- Docs/tally: 109 Parity / 22 Caveat / 25 Divergent.
- --bwlimit: faithful port of rsync 3.4.1 parse_size_arg (default KiB/s,
  binary K/M/G/T/P, decimal KB/MB, KiB/MiB, decimals, 0 = unlimited, 512-byte
  floor, (size+512)/1024 quantization).  Unit tests + docs.
- --info: wire del/remove/name/flist/nonreg/progress to real FastSync events in
  rsync's line format (deleting PATH, sender removed NAME, name lines,
  'sending incremental file list', skipping non-regular file "NAME"); name no
  longer aliases copy; --info=progress drives the progress path and report_stats.
- --ignore-errors: match rsync's default -- a source I/O error skips deletion
  unless --ignore-errors, while the readable tree still transfers and the run
  exits 23.  Covers all delete timings and both send paths.
Each row's exact residual reproduced against rsync 3.4.1:
- basis dirs (--compare/copy/link-dest): FastSync xxHash-verifies a basis hit
  while rsync --size-only installs the wrong same-size basis content.
- --delay-updates: the fixed .fastsync-stage name wipes an unrelated
  destination entry of that name even without --delete; rsync leaves it.
- --fuzzy: deterministic name/size heuristic (10x window), not rsync's matcher.
- --dry-run: would-delete report over-reports the updated file and an
  excluded-but-protected extra, and ordering differs.
Docs: RSYNC_COMPAT tally 109/16/31; HANDOFF item 9. clang-format + cppcheck +
ASan + full suite clean.
- Wire: config frame gains report_deletes (protocol 2.26.0 -> 2.27.0); the
  receiver lists actually-removed paths in the STATUS_STATS path list, so the
  sender prints rsync's `deleting PATH` / `*deleting   PATH` lines for a real
  --delete run (and -i/out-format).  Observers threaded through the manifest,
  missing-args and per-directory delete engines; golden wire len/hash updated.
- bwlimit: throttle now paces like rsync 3.4.1 -- ~100ms burst capacity and the
  sleep is no longer credited as refill, so 4 MiB at 1024/2048 KiB/s matches
  rsync within ~4% (was ~2x too fast).
- tests/integration/test_option_parity.py: bwlimit parse matrix + throttle
  rate, --info flist/name/nonreg/del(dry+real+itemize)/remove, real-setpriv
  --ignore-errors, rsync-daemon -M forwarding evidence, filter protect/risk
  destination-only divergence pin.
- RSYNC_COMPAT.md: tally 109/21/26; --bwlimit and --ignore-errors -> Parity,
  --filter and -M -> Divergent with differential rationale, --info residual
  narrowed to the categories with no client-observable event.
- HANDOFF/README: protocol 2.27.0, option-wave summary.
# Conflicts:
#	RSYNC_COMPAT.md
- Initialize PipelineContextSender.delete_suppressed=false: an uninitialized
  true silently skipped the --delete keep-set manifest under -m/--threads,
  so destination extras were never removed.
- Allocate/install the receiver deleted-path observer only when
  report_deletes is set (--info=del / -i / --out-format under --delete), cap
  the retained list at MAX_MANIFEST_ENTRIES, and free already-created lists
  on the receiver-pipeline create failure path.
- Validate report_deletes/report_stats/report_dest_info on receive.
- Correct stale comments (config.h report_deletes, delete_plan.h deleted
  count, multiprocessing.h stats locking, utils.h observer placement).
- Tests: sender delete_suppressed init, report_deletes gating (unit), and
  -m/--delete default delete-after keep-set removal (integration).
- cli: remove UB in --bwlimit scaling (range-check the double product before
  casting, drop atoi for the +/-1 form) and add huge/boundary unit tests
- test: widen the CI throttle wall-clock band to [1.5, 4.5]s with a 2s
  cross-tolerance so a loaded runner cannot flake it
- log: drop the unused LOG_INFO_BACKUP bit; --info=backup is accepted-but-
  silent like the other rsync-only categories
- client_send: remove the duplicate delete_display_path forward declaration
- utils: add non-allocating utils_strip_transfer_root and use it from
  scanner_note_nonreg and delete_display_path (was duplicated logic)
- scanner: lstat() instead of stat() when re-reading an empty dir's metadata
- file: drop the no-op else-if and the redundant ELOOP arm in
  file_ensure_directory_secure (symlinks are refused anyway)
- format/stats: document literal_data as whole-file accurate (delta upper
  bound) instead of claiming literal bytes sent
- docs: refresh stale protocol 2.26.0 labels to 2.27.0
- --delete-delay: state that the reported count advances on actual removal
  while --max-delete is charged at plan/snapshot time (defer_add/planned).
  A new differential shows rsync instead charges on actual removals and
  recursively removes a queued directory, so the row moves to Caveat
  (matrix 111/13/33) and the residual is pinned by tests.
- Add a deterministic unit test (plan-time budget charge), a FastSync
  integration test (byte-barrier refill + --max-delete), and an rsync
  differential for a refilled deferred directory.
- Soften the --fuzzy summary: the tree is byte-exact by design, so it is
  pinned by the threshold suite, not a byte-level differential.
- README: describe what -m parity actually selects; fix the allowlist
  example to the real max_delete entry.
- xdist-safe delete-timing fixture names (timing and --threads mode).
- Renumber the duplicate HANDOFF item 9 to 10; add the missing final
  newline to test_checksum.c.
- Expose ignore_errors_allows_delete and unit-test the deletion gate
  without a privileged source directory; update the stale deleted-count
  doc comment.

No production behavior changes.
# Conflicts:
#	src/shared/delete_plan.h
#	tests/integration/test_delete_timing_parity.py
docs(agents): note FASTSYNC_UNDER_VALGRIND for manual valgrind runs
CI / lint (pull_request) Successful in 1m40s
CI / parity-full (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
CI / parity-fast (pull_request) Successful in 16s
CI / build-and-test (pull_request) Successful in 51s
3d0672a721
TapTap merged commit 596a039454 into dev 2026-09-18 22:19:26 +02:00
TapTap deleted branch feat/parity-fixes 2026-09-18 22:19:26 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: TapTap/FastSync#301