Conversation
`fal files upload` sized its progress task in parts, so a 100 MB file - ten 10 MB parts uploaded by ten concurrent workers - had nothing to advance until those parts all landed at the end. The bar sat near 0% for the whole transfer and then jumped to done. A full-file MD5 pass ran before the first byte went out, adding a second phase with no advancement at all. Progress is now sized in bytes and advanced as each part body is handed to the transport, and the digest is folded into the reader thread the upload already runs, so verifying the etag costs no extra read and no silent wait. The small (non-multipart) branch advances the same way while its request body is written. `BaseMultipartUpload.upload_file` gains an `on_bytes_uploaded` callback and a `content_md5` property; `on_part_complete` and existing callers are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Symptom
Uploading a large file with
fal files uploadlooks hung: the progress bar sits at or near 0% for the entire transfer, then jumps straight to done. Nothing is actually wrong with the upload — the bar just has no way to move.Why the bar could not move
Two things compounded in
projects/fal/src/fal/files.py:_put_file_multipartcreated the task withtotal=num_partsand advanced one unit per completed part. WithMULTIPART_CHUNK_SIZE = 10 MBandMULTIPART_MAX_CONCURRENCY = 10, a 100 MB file is exactly 10 parts uploaded by 10 concurrent workers. All ten are in flight at once sharing the same link, so they finish at roughly the same moment — the bar has nothing to report until the transfer is essentially over._compute_md5(lpath)read the whole file while the task displayed"Calculating checksum..."withcompleted=0. On a large file or a slow disk that is a second silent, apparently-stalled phase before the silent upload phase.The non-multipart branch (files under the 10 MB threshold) had the same shape:
total=1, advanced once after the whole blocking request returned.What changed
projects/fal/src/fal/upload.pyProgressFileReader— a read-through view of a binary stream that reports its absolute offset after each read. httpx rewinds a multipart body before resending it, so reporting an offset rather than a delta keeps a retried part from double-counting._BytesUploadedTracker— folds concurrent per-part offsets into one cumulative total, recomputed from each part's latest offset rather than accumulated, for the same reason.BaseMultipartUpload.upload_filegainson_bytes_uploaded(total_bytes), fired as each part's body is handed to the transport.on_part_completeis untouched, and both are optional.upload_filealready runs, and exposed asBaseMultipartUpload.content_md5. The file is read once instead of twice, and the hashing overlaps the upload rather than preceding it.projects/fal/src/fal/files.py_put_file_multipartsizes the task in bytes (total=size) and drives it fromon_bytes_uploaded. The"Calculating checksum..."phase is gone because there is no longer a separate checksum pass.multipart.content_md5.content_md5isNoneuntil a call has read the file through, so a set etag with no digest raises rather than silently skipping verification (covered by a test).ProgressFileReader, so the bar advances while the request body is written.DownloadColumnso movement is visible even when the percentage rounds to the same integer.Trade-off worth knowing
upload_filenow always computes the MD5, including forAppFileMultipartUpload(app file sync), which previously did not. It rides along with a read the upload has to do anyway and overlaps the network, so the cost is small; the alternative was a flag that every caller would have to thread through.How to test
Reproduce the symptom and verify the fix
tools/-free repro used here: a fake backend over a single shared 25 MB/s pipe drained at 64 KiB granularity, so ten concurrent parts progress together and finish together — the real-world shape. It drivesFalFileSystem._put_file_multipartwith a realrich.progress.Progressand samples the bar every 250 ms. No network, no credentials.demo_progress.py
Before (
main, commit335261eb) — 0% for three of the four seconds, reaching 10% before the upload completes:After (this branch) — tracks the transfer, same 4.0 s wall time:
New tests
test_upload_progress.pyruns the realBaseMultipartUploadagainst anhttpx.MockTransport, which drains the request body, so the byte callbacks fire from the actual multipart encoder rather than from a stub. It covers monotonicity, the exact final total, behaviour under concurrency, a retried part not inflating the count, the digest matchinghashlib.md5of the file, the empty-file path,on_part_completeback-compat, and reader-thread error propagation leaving no digest behind.test_files_upload_progress.pydrivesFalFileSystemagainst a realrich.progress.Progresssubclass that records every reported position, so the assertions are against rich's own task state rather than a mock's call log.This replaces
test_files_checksum_progress.py, which asserted the old parts-based task and the"Calculating checksum..."phase that no longer exists.Full unit suite
Lint / types
Not verified
fal files uploadagainst a real backend. It needs credentials and a live storage backend, and uploading a 100 MB file to production storage purely to watch a progress bar is not a reasonable test. Every result above comes from fake transports. Worth a manualfal files uploadof a >10 MB file on a real account before release.task.percentagefrom aProgressconstructed withdisable=Truerather than capturing rendered frames, so the newDownloadColumnlayout has not been seen in a terminal.AppFileMultipartUpload(app file sync) was only checked by the existing unit tests. It picks up the always-on MD5 computation and was not exercised against a real deploy.pytestran on Python 3.12 only; CI covers 3.10 through 3.14 plus Windows.🤖 Generated with Claude Code