From 123576e37aa646c3119c5f4f750c32fd0cbe4a33 Mon Sep 17 00:00:00 2001 From: Stuart Inglis Date: Mon, 20 Jul 2026 14:04:03 +1200 Subject: [PATCH] checksum: keep the link alive while hashing a file (issue #839) With --checksum the sender hashes every regular file while it builds the file list (flist.c calls file_checksum() from make_file()), and nothing is sent to the peer for as long as that takes. On a large file the far end therefore sees a silent link and aborts once --timeout elapses: rsync -a --checksum --timeout=3 rsync://host/mod/ dst/ [Receiver] io timeout after 4 seconds -- exiting rsync error: timeout in data send/receive (code 30) A single 2 GiB file reproduces it here (~4.3s to hash). The same transfer without --checksum, or with a --timeout longer than the hash, is fine, so it is purely the unbroken silence while hashing. Note it is driven by the time to hash one file, not by how many there are: 500,000 small files take 4.6s to hash in total and do *not* trip it, because list entries keep streaming out and the link never goes quiet. rsync already has the right primitive for this. maybe_send_keepalive() sends a MSG_NOOP and self-throttles on allowed_lull, which set_io_timeout() computes as half of --timeout -- exactly the "send something every half the threshold" interval this needs, at whatever timeout the user picked. The generator, sender and receiver already call it during their long stretches; the file-list hashing path simply never did. Tick it as we hash. file_checksum() had six copies of the same chunk loop (one per digest), so rather than add the call six times -- and leave a seventh digest free to forget it -- the loop is now a single CHECKSUM_FEED() macro carrying the tick. It takes the update as an expression because the digests have differing signatures; a function-pointer iterator would need a wrapper per digest and could not express the pre-27 MD4 tail, so each caller keeps its own trailing partial chunk handling unchanged. The tick is throttled by bytes hashed rather than fired per piece: the MD4 loop runs in CSUM_CHUNK (64) byte pieces, so a per-piece tick would consult the clock 16.7 million times per GiB -- 512x more often than the other digests. That measured +2.6% on an MD4 pass here (1.95s -> 2.00s for 1 GiB, matching the 2.9ns vDSO time() call), and would be far worse where time() is a real syscall. Consulting it once per CHUNK_SIZE of hashing puts every digest on the same footing and the cost back in the noise (1.94s). Verified digest-identical to the unpatched build in both directions (master sender vs patched receiver and vice versa) over files at the chunk boundaries -- 0, 1, 63, 64, 65, 32767, 32768, 32769, 65537 and multi-chunk sizes -- for every checksum-choice this build offers, with a flipped byte as a positive control. generate_and_send_sums() was checked for the same pattern and does not need it: it writes each block sum as it computes, so that link never goes quiet. --- checksum.c | 54 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/checksum.c b/checksum.c index 24e46bfbb..67b15a78d 100644 --- a/checksum.c +++ b/checksum.c @@ -37,6 +37,7 @@ extern int am_server; extern int whole_file; +extern int allowed_lull; extern int checksum_seed; extern int protocol_version; extern int proper_seed_order; @@ -398,6 +399,41 @@ void get_checksum2(char *buf, int32 len, char *sum) } } +/* Hashing a large file sends nothing to the peer for as long as it takes, so a + * --checksum run over a big file looks like a dead link and the far end aborts + * once --timeout elapses. Tick the keep-alive as we go; maybe_send_keepalive() + * itself only acts once allowed_lull (half of --timeout) has passed. + * + * `n` is the size of the piece just hashed, and we only consult the clock once + * per CHUNK_SIZE of it. The MD4 loop feeds us CSUM_CHUNK (64) bytes at a time, + * so ticking per piece would call time() 512x more often for MD4 than for the + * other digests -- 16.7 million times per GiB, which is cheap on a vDSO clock + * and decidedly not on a platform where time() is a real syscall. */ +static void checksum_keepalive(int32 n) +{ + static int32 until_tick = CHUNK_SIZE; + + if (!allowed_lull) + return; + if ((until_tick -= n) > 0) + return; + until_tick = CHUNK_SIZE; + maybe_send_keepalive(time(NULL), MSK_ALLOW_FLUSH); +} + +/* Feed the mapped file to a digest in `chunk`-sized pieces. UPDATE is an + * expression using _p/_n for the piece; the digests have differing signatures, + * so an expression is what lets one loop serve all of them. Each caller keeps + * its own trailing-partial-chunk handling, which is not identical across + * digests (see the pre-27 MD4 case below). */ +#define CHECKSUM_FEED(chunk, UPDATE) \ + for (i = 0; i + (chunk) <= len; i += (chunk)) { \ + const char *_p = map_ptr(buf, i, (chunk)); \ + int32 _n = (chunk); \ + UPDATE; \ + checksum_keepalive(_n); \ + } + void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum) { struct map_struct *buf; @@ -421,8 +457,7 @@ void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum) EVP_DigestInit_ex(evp, file_sum_evp_md, NULL); - for (i = 0; i + CHUNK_SIZE <= len; i += CHUNK_SIZE) - EVP_DigestUpdate(evp, (uchar *)map_ptr(buf, i, CHUNK_SIZE), CHUNK_SIZE); + CHECKSUM_FEED(CHUNK_SIZE, EVP_DigestUpdate(evp, (uchar *)_p, _n)); remainder = (int32)(len - i); if (remainder > 0) @@ -440,8 +475,7 @@ void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum) XXH64_reset(state, 0); - for (i = 0; i + CHUNK_SIZE <= len; i += CHUNK_SIZE) - XXH64_update(state, (uchar *)map_ptr(buf, i, CHUNK_SIZE), CHUNK_SIZE); + CHECKSUM_FEED(CHUNK_SIZE, XXH64_update(state, (uchar *)_p, _n)); remainder = (int32)(len - i); if (remainder > 0) @@ -459,8 +493,7 @@ void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum) XXH3_64bits_reset(state); - for (i = 0; i + CHUNK_SIZE <= len; i += CHUNK_SIZE) - XXH3_64bits_update(state, (uchar *)map_ptr(buf, i, CHUNK_SIZE), CHUNK_SIZE); + CHECKSUM_FEED(CHUNK_SIZE, XXH3_64bits_update(state, (uchar *)_p, _n)); remainder = (int32)(len - i); if (remainder > 0) @@ -477,8 +510,7 @@ void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum) XXH3_128bits_reset(state); - for (i = 0; i + CHUNK_SIZE <= len; i += CHUNK_SIZE) - XXH3_128bits_update(state, (uchar *)map_ptr(buf, i, CHUNK_SIZE), CHUNK_SIZE); + CHECKSUM_FEED(CHUNK_SIZE, XXH3_128bits_update(state, (uchar *)_p, _n)); remainder = (int32)(len - i); if (remainder > 0) @@ -495,8 +527,7 @@ void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum) md5_begin(&m5); - for (i = 0; i + CHUNK_SIZE <= len; i += CHUNK_SIZE) - md5_update(&m5, (uchar *)map_ptr(buf, i, CHUNK_SIZE), CHUNK_SIZE); + CHECKSUM_FEED(CHUNK_SIZE, md5_update(&m5, (uchar *)_p, _n)); remainder = (int32)(len - i); if (remainder > 0) @@ -513,8 +544,7 @@ void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum) mdfour_begin(&m); - for (i = 0; i + CSUM_CHUNK <= len; i += CSUM_CHUNK) - mdfour_update(&m, (uchar *)map_ptr(buf, i, CSUM_CHUNK), CSUM_CHUNK); + CHECKSUM_FEED(CSUM_CHUNK, mdfour_update(&m, (uchar *)_p, _n)); /* Prior to version 27 an incorrect MD4 checksum was computed * by failing to call mdfour_tail() for block sizes that