Repository navigation
Keep the link busy until the end of a transfer: shared download budget, file-level streams, GCS ranged reads - #1601
Merged
Conversation
…wnload_*) One GCS read stream has a fixed ceiling, so when a download is down to its last large part the link is mostly idle. allow_multipart_download covers this only partly: it buffers the whole object into a temporary file before extraction starts (extra disk equal to the object size per file in flight), and it is bypassed whenever download_max_bytes_per_second is set (#1377). Add an opt-in ranged streaming reader: - objects >= gcs.ranged_download_min_size are read as gcs.ranged_download_concurrency concurrent NewRangeReader requests of gcs.ranged_download_chunk_size bytes and handed to the existing decompress/untar pipeline strictly in order; no temporary file - memory is bounded per reader (a window of `concurrency` chunks) and process-wide (gcs.ranged_download_max_buffers chunk buffers shared by all readers), so at the tail the last large file gets the whole budget - chunk indexes are claimed in order by goroutines already holding both slots, so readers sharing the budget cannot deadlock - each range borrows its own pooled client for the duration of the request, so ranges are not multiplexed onto one HTTP/2 connection - compatible with download_max_bytes_per_second: the limiter paces the consumer and the prefetch stays bounded - disabled by default (ranged_download_concurrency: 0)
During download the tables are dispatched in metadata order, at most download_concurrency at a time, and each table runs its parts with its own download_concurrency limit. When only one table is left it can use only its own slots, so the tail of a download drains with download_concurrency streams while the slots of finished tables stay idle. - general.download_transfer_concurrency: one semaphore per Download() shared by every part/archive/required-part transfer of all tables; the per-table part limit is raised to the budget, so the semaphore alone bounds the total. 0 (default) = download_concurrency^2, the current worst case, so no extra connections; negative = legacy per-table limit - general.download_table_order: largest_first (default) dispatches big tables first, so they are not left to drain alone at the end; metadata keeps the old order. Only the dispatch order changes. The cost is the sum of per-part sizes (2.8.0+), TotalBytes for older backups Ported from the patch in #1591 by @minguyen9988. Fixes #1591 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With compression_format: none UploadPath, DownloadPath and DownloadPathWithManifest move the files of one part one by one, so on high-latency remote storage per-file round trips dominate a wide part. general.file_transfer_concurrency (default 1 = sequential) lets one path transfer use up to N streams. The caller goroutine is the first stream and holds its own slot of the transfer budget; every extra stream runs only while it can take a free slot of the same budget without blocking (TryAcquire), so the total number of streams never exceeds the budget: download_transfer_concurrency for download, upload_concurrency^2 for upload (upload parts now take a slot too when the option is enabled). Extra streams just use the slots left idle by other parts, typically at the tail of a transfer, and the product parts x files is bounded as requested in the #1454 review. The first error is recorded before the other streams are canceled, so it is not hidden by `context canceled`. The integration SFTP config enables it with 4 streams. Fixes #1454 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A recursive FTP Walk keeps one pooled connection while its callbacks borrow another one. With a shared budget a single table can now run download_concurrency^2 part transfers, which can exhaust the pool of 4*ftp.concurrency connections and hang; limit the budget to 2*ftp.concurrency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coverage Report for CI Build 37576468474Coverage increased (+0.1%) to 69.593%Details
Uncovered Changes
Coverage Regressions26 previously-covered lines in 9 files lost coverage.
Coverage Stats
💛 - Coveralls |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1591, fixes #1454, fixes #1592
All three issues are about the same thing: keeping the link busy until the end of a transfer. #1454 and #1591 share one mechanism: the global transfer budget from #1591 is the "product bounded by a global semaphore" that the #1454 review asked for. #1592 is a separate GCS storage-layer change. It is opt-in, kept as the author's own commit, and can be dropped from this PR on its own.
Commits
minguyen9988:fix-gcs-ranged-streaming-download)gcs.ranged_download_min_sizeare read asgcs.ranged_download_concurrencyconcurrentNewRangeReaderrequests and fed to decompression in order.download_max_bytes_per_second.ranged_download_max_buffers * ranged_download_chunk_size.general.download_transfer_concurrency: onesemaphore.WeightedperDownload(), shared by every part, archive and required-part transfer of all tables.0(default) =download_concurrency^2, the same connection maximum as today. Negative = legacy per-table limit.general.download_table_order:largest_first(default) ormetadata. Only the dispatch order changes; restore order is not affected.general.file_transfer_concurrency(default1= current sequential behavior) is a worker count inside the existingUploadPath/DownloadPath/DownloadPathWithManifest. No new*Parallelcopies.TryAcquireon the same budget succeeds, so they never block and never raise the total above the budget. They only use slots left idle by other parts, mostly at the tail.download_transfer_concurrencyfor download,upload_concurrency^2for upload. When the option is enabled, upload parts take a slot too.context canceledcannot hide it (a unit test caught this).compression_format: noneis affected.gcs.ranged_download_*settings and the testflows default-config snapshot.Walkholds one pooled connection while its callbacks borrow another.download_concurrency^2transfers and exhaust the pool of4*ftp.concurrencyconnections.2*ftp.concurrency.The storage functions in commit 3 are re-indented into closures, so review with
git diff -w.Tests
TotalBytesfallback).UploadPath/DownloadPath/DownloadPathWithManifestwith file workers: peak streams equalmin(workers, budget), content and byte counts are exact, slots are released, and the first error is not masked. All pass under-race.make testpasses.compression_format: none) now setsfile_transfer_concurrency: 4.largest_firstand the auto budget are on by default, so every download test also exercises them.Open points for review
largest_firstdefault changes the dispatch order for everyone. It is harmless, but it is a behavior change.restore_remote --streamingcallsdownloadTableDatadirectly, so none of the three new download settings apply there.upload_by_part: false, required-part goroutines indownloadDiffPartstake a slot before waiting on the shared per-archivenamedLock. This costs throughput, not correctness.namedLockis not unlocked on a download error.ranged_download_min_size, which is at least 64 MiB.gcs.allow_multipart_downloadinstead of adding new settings? The author offered both options in GCS download: one large archive is always a single stream at the tail; allow_multipart_download needs a temp file and is bypassed by download_max_bytes_per_second - ranged streaming reader, benchmark and branch #1592.🤖 Generated with Claude Code