Reuse cached checksums in the capped full_sync pass - #2165
Conversation
|
Reviewed the changes. No issues found. The size-tracking implementation is correct: |
ce4b6b7 to
eac45f6
Compare
|
Re-reviewed the current diff (capped-sync checksum-cache with covered sizes). No issues found.
|
|
Here's some thoughts on an improvement from me and Claude 🤖 At That bites in two routine cases. Any A file only picks up a size once a write invalidates its entry and the uncapped pass recomputes it, so actively written files Treating a sizeless entry as a miss fixes it. Beside # The hash for `path` only if the entry records how many bytes it covers.
# A sizeless entry is deliberately a miss: recomputing records the size,
# which is what makes the entry reusable by a later capped pass.
def sized_hash?(path) : Bytes?
if entry = @checksums[path]?
entry.hash if entry.size
end
endand then at cached_hash = @file_index.shared do |_files, checksums|
if caps
checksums.hash_for?(path, caps[path]? || 0i64)
else
checksums.sized_hash?(path)
end
endNothing else needs to change since Passing sizes at the |
49749f2 to
e8991ce
Compare
The capped full_sync pass bypassed the checksum cache entirely and re-hashed every file from disk while holding @lock, stalling all replicated operations for the duration. Checksums entries now record how many bytes each hash covers, so the capped pass reuses a cached hash exactly when it covers the requested cut, which is every file not written to between the two passes. Comparing against the file's current size instead would race local writes that haven't invalidated the cache yet. The persisted format gains an optional size field; old checksums.sha1 files still parse, their entries just never match a sized lookup. Fixes #2162 Co-authored-by: Lucas Polesello <lucas.c4d@gmail.com>
Keeps the on-disk line format of a serialized Entry in one place instead of splitting it between the writer and the parser in restore. Written bytes are unchanged, so existing checksums.sha1 files stay readable.
This reverts commit c2901f0.
The compare loop, file_from_socket and the close-time digest finalization now store how many bytes their hashes cover, so a node promoted to leader can reuse them in the capped sync pass instead of re-hashing every file under the replication lock.
append and set now always take the covered size; only entries restored from an old-format checksums.sha1 lack one. Lookups return the whole Entry (hash plus covered size) and each caller decides what coverage it accepts: the capped sync pass demands an exact match, the uncapped pass takes any recorded coverage and recomputes sizeless entries outside the lock so they gain a size the capped pass can trust. Without that healing, every entry on an upgraded or promoted node stays sizeless indefinitely and the capped pass keeps re-hashing all untouched files under the replication lock on each follower join.
e8991ce to
f8b4ed9
Compare
Fixes #2162.
Builds on @lukas8219's 80cd199, which spotted the capped pass never using the checksum cache. That commit is cherry-picked here with a corrected reuse condition on top: it compared the digest's byte length against the caps hash, so it could never be true and the capped pass still recomputed everything.
During the final full_sync pass the leader hashes every file up to a snapshotted cut while holding the lock that all replicated operations need, so publishing stalls until the pass is done. The checksum cache was bypassed because entries did not record how many bytes a hash covers, so a cached hash could not be trusted for a capped size.
Every checksum write now records the covered byte count, on the leader as well as on the follower, so a size may only be recorded by code that just hashed exactly that many bytes. Lookups hand out the whole entry, hash and size together, and the caller decides whether the recorded coverage fits its use. The capped pass reuses a cached hash exactly when it covers the requested cut, which is every file not written to between the two passes, so it no longer re-reads tens of thousands of unchanged files under the lock. Comparing the cap against the file's current size instead would race local writes that have not invalidated the cache yet, which is why the covered size is stored rather than inferred.
The size is persisted in checksums.sha1 as "hash size *path". Files written by older versions parse as entries without a size; the uncapped pass recomputes those and records sizes, so an old cache heals in one pass and the capped pass never trusts a sizeless entry.
Specs: the reuse spec deletes the file from disk before the capped pass, so the hash can only come from the cache, and it fails without this change. Further specs cover recompute on cap mismatch, healing of old-format entries, size persistence across append/store/restore, and that follower-written checksums match both content and size on disk.