Bandwidth throttling: --bwlimit <KB/s> with token bucket #15
Reference in New Issue
Block a user
Delete Branch "bwlimit"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
New --bwlimit flag that limits transfer rate using a token bucket algorithm.
(64 KB max chunk size, nanosleep-based deficit compensation)
All 7 unit tests and 31 integration tests pass with zero warnings.
Code Review: Bandwidth throttling
Token bucket in
send_n_datais a clean approach. Below are the issues I found.CRITICAL
1.
__threadbreaks bwlimit for multithreaded transfersprotocol.c:10-12io_bwlimit,bw_tokens, andbw_last_refillare all declared__thread(thread-local storage). The main thread callsio_set_bwlimit()during CLI parsing, setting its own thread-local copy. But in the multithreaded client path (-m),send_chunks_multithreadedruns in a separate thread created viathrd_create. That thread has its own independentio_bwlimit = 0from the static initializer — it never sees the value set by the main thread.Result:
--bwlimitis silently ignored whenever-mis used.Fix: change the three bw variables from
__threadto plainstatic. This works for both models:fork()copies the entire address space, so each child gets its own copy of static variables — no sharing issue.send_n_data, so there's no contention onbw_tokens/bw_last_refill— no mutex needed.BUGS
2.
--bwlimit 0is accepted but does nothingclient_cli.c:132strtoullparses0fine,io_set_bwlimit(0)setsio_bwlimit = 0, andbw_throttleimmediately returns onio_bwlimit == 0. The user gets a log message "Set bandwidth limit to 0 KB/s" but no throttling happens. Should validatekbps > 0and error on0.3.
nanosleepEINTR not handledprotocol.c:49If a signal (e.g., SIGCHLD in the fork model) interrupts
nanosleep, it returns early with the remaining time inrmtp(which is NULL here). The throttle under-sleeps, resulting in slightly higher bandwidth usage. For a file transfer tool this is minor, but passing a non-NULLrmtpand retrying would be more correct.DESIGN
4. Token bucket cap is
io_bwlimit(1 second) — prevents burst accumulationprotocol.c:39if (bw_tokens > (long long)io_bwlimit) bw_tokens = (long long)io_bwlimit;caps tokens at 1 second worth. This means after a long idle period (e.g., while the server processes metadata), the client can only burst 1 second of data. This is standard token bucket behavior and is fine, but worth documenting — some users expect idle time to "bank" bandwidth for large transfers.5. Bwlimit only applies to sends, not receives
bw_throttleis called only insend_n_data.receive_n_datais not throttled. This is correct (the client is the sender), but should be documented. If the server ever needs to send data back (e.g., for a pull mode), it won't be throttled.MINOR
6. Test doesn't verify bandwidth limiting
test.pyThe test runs a transfer with
--bwlimit 10240and checks the exit code. It doesn't verify the transfer was actually slower. A more robust test would compare transfer time against a no-bwlimit baseline and assert a minimum duration.7. Merge conflict with TLS PR
Both this PR and the TLS PR modify
send_n_data. TLS addsSSL_writeas an alternative towrite(). Bwlimit adds chunk capping before thewrite()call. When both are merged, the chunk cap needs to apply toSSL_writetoo. Not a bug in either PR alone, but worth coordinating.Summary
Must-fix before merging:
__threadfrom bwlimit variables (bug #1 — bwlimit broken with-m)--bwlimitvalue > 0 (bug #2)Nice-to-have:
3. Handle
nanosleepEINTR (issue #3)4. Strengthen test to verify actual throttling (issue #6)
Re-review of
3dcaf0bAll three issues from the previous review are fixed:
__threadremoved from bwlimit variables —io_bwlimit,bw_tokens,bw_last_refillare now plainstatic. Works correctly in both fork model (server) and thread model (client) since only one thread at a time callssend_n_data.--bwlimit 0validation — clean error message, exits early.nanosleepEINTR handling — proper retry loop withremainingtimespec.No new issues introduced. LGTM.
PR Review Summary
Verdict: FAIL — 1 critical issue must be fixed before merge
Files reviewed: 3
Issues found: 6 (1 critical, 4 warning, 1 style)
CRITICAL Issue
[1]
src/shared/protocol.c:14-16/src/client/client_cli.c:132— throttling ignored in multithreaded modeio_bwlimit,bw_tokens, andbw_last_refillare all__thread(thread-local). Whenmain()callsio_set_bwlimit(), it sets only the main thread's copy. Whensend_files_multithreaded()spawns worker threads, each new thread gets its own copy initialized to0. Thebw_throttle()check (if (io_bwlimit == 0) return;) means throttling is completely disabled in the sender worker thread.A user running
fastsync -m --bwlimit 10240 ...gets zero throttling.Fix options:
bwlimitfield toConfig, and callio_set_bwlimit(config->bwlimit)at the top ofsend_chunks_multithreaded()Warnings
[2]
src/shared/protocol.c:20— bw_tokens initialized to 0 (no burst tolerance)bw_tokensstarts at 0, so the first write always sleeps. Standard token bucket implementations initialize tokens to the max rate (full bucket) to allow an initial burst.Fix: Change to
bw_tokens = (long long)io_bwlimit;so the bucket starts full.[3]
src/client/client_cli.c:133— no overflow check on kbps * 1024strtoullwithNULLendptr silently returns 0 for non-numeric input. If the user provides a value >ULLONG_MAX / 1024, the multiplication wraps.Fix: Use
strtoull(argv[++i], &end, 10)and validateendand range.[4]
src/shared/protocol.c:43— nanosleep return value not checkedIf
nanosleepis interrupted by a signal, the actual sleep is shorter, allowing more data through than the rate limit intends. A retry loop would be more correct.[5]
src/shared/protocol.c:39— potential signed overflowCasting
io_bwlimit(unsigned long long) tolong longis UB if it exceedsLLONG_MAX. Consider usingunsigned long longforbw_tokens.Style
[6]
src/shared/protocol.c:39,42,46— Repeated cast ofio_bwlimittolong long. Usingunsigned long longforbw_tokenswith a separate signed variable for deficit computation would be cleaner.The critical issue is the most important: the
__threadstorage silently disables bandwidth limiting for all multithreaded transfers. This is a functional bug that users will hit immediately when combining-mwith--bwlimit.Re-review after commit
3dcaf0bVerdict: PASS — Critical issue fixed
Critical — resolved
io_bwlimit,bw_tokens,bw_last_refillare no longer__thread✅Throttling now works correctly in multithreaded mode.
Additional fixes
nanosleepnow handlesEINTRwith retry loop ✅--bwlimitvalidates input > 0 ✅Remaining warnings (non-blocking)
bw_tokensstill initialized to 0 — first write always sleeps. Consider initializing toio_bwlimitfor burst tolerance.strtoullwithNULLendptr — non-numeric input silently returns 0 (now caught by >0 check), but overflow from huge values still possible. Consider using&endand validating.(long long)io_bwlimitcast could overflow if rate > LLONG_MAX. Consider usingunsigned long longforbw_tokens.These are minor and non-blocking. Safe to merge.