Repository navigation
fix(b2): return the upload error from Writer.ReadFrom instead of context canceled - #252
Merged
Merged
Conversation
Pin delayed source reads that resume after copyContext returns and Writer.ReadFrom has been cancelled and closed. Part of #229
A cancelled copy could return while its source Read was still blocked. When that Read resumed, the copy goroutine could write to a Writer that the caller had already closed, causing the slice-bounds panic seen in CI run 40925001975. Guard destination writes and mark the guard stopped before returning on cancellation. Waiting for the copy goroutine is not an option because the source Read may block forever. Preserve the closer path that closes the body and waits for its copy to finish. Closes #229
…ReadFrom cancellation Address the panel's nits on #229: the regression test compared the error text, so wrapping errCopyAbandoned would break it while the guard still works; and ReadFrom's godoc did not say that a Read already in progress on a non-seekable source can outlive a cancelled call.
…context-stray-write
The comment said nothing a pending Read returns is written, but the guard only stops writes once copyContext returns: a Write already in progress, with data read before the cancellation, can still complete. Say what the code guarantees: after ReadFrom returns, nothing more from r reaches the Writer.
…context-stray-write
…context-stray-write
Hold cancel_large_file or WithCancelOnError callbacks behind channels after producer and worker upload failures. ReadFrom must return on cancellation before those channels are released, and the callback must receive the cancellation request result (#229).
Keep the existing Writer error path and release the copy guard only on terminal write errors. No buffer writes follow that hook, and the stopped check still gates every later write, so no source bytes can reach the destination after a cancelled copy returns. Release the error-state mutex before cancel_large_file and errf as well, so a producer observing a worker failure does not wait behind cleanup. Close joins its existing worker wait group on error exits to preserve delivery of the cleanup result to WithCancelOnError callers. Reject a separate deferred error-recording and cleanup mechanism: reusing setErr preserves first-error ownership and callback semantics without adding parallel state. The public API and ReadFrom godoc remain unchanged. Closes #229
Hold the producer inside buffer allocation until a worker enters blocked cancellation cleanup, then release it into getErr while the copy guard is still held. This makes moving cleanup back under emux fail deterministically. Gate the worker cancellation request separately and verify that its nonblocking error callback has completed when Close returns. Removing the deferred worker wait now fails without making the callback depend on Close. Keep the existing cancellation cases and production code unchanged.
…context-stray-write
…context-stray-write
…ay-write' into fix-b2-issue-245-readfrom-masks-error
Base automatically changed from
fix-b2-issue-229-copycontext-stray-write
to
main
October 30, 2026 20:58
…from-masks-error # Conflicts: # b2/writer.go
david-santacruz-backblaze
approved these changes
Oct 30, 2026
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.
Problem
When a large-file upload fails permanently,
io.Copy(w, r)into a*b2.Writerreturnedcontext canceledinstead of the upload error.io.CopyusesWriter.ReadFrom, which copies under the Writer's context. A part failure callsWriter.setErr, which records the error and cancels that context;copyContextthen sawctx.Done()and returnedctx.Err(), masking the real failure.Close()returned the right error, but most callers checkio.Copy's.Root cause
The Writer couldn't tell "the Writer cancelled itself because an upload failed" from "the caller cancelled". A first fix that compared error types was wrong:
base.makeNetRequestflattens most transport errors (a dial or I/O error caused by the caller's cancellation) into a plainb2err, so a caller's cancellation could come back as an opaque transport error.Fix
context.WithCancelCause(b2/b2.go).setErrcancels it with the first recorded error, wrapped in an unexportedwriterCausetag.ReadFromreplaces a context error only whencontext.Causeunwraps to awriterCause, and returns the original upload error (soerrors.Is/errors.Asmatch it, as they do forClose). Anything else, including a caller's own cause fromWithCancelCause/WithTimeoutCause, leavescontext.Canceled/context.DeadlineExceededunchanged.ReadFromgodoc states both outcomes for every path (non-seekable, Resume, streaming).Verification
Tests in
b2/writer_readfrom_error_test.goand the tightenedTestWriterReturnsError(errors.Ison the injected error) cover:The deadline case starts its timer only after the controlled part upload is reached, so it doesn't race setup.
setErrcancels without the tagio.Copy error = context canceled, want the permanent upload-part failureReadFromsubstitutes any causeio.Copy error = caller-provided cancellation cause, want caller's context canceled(and deadline exceeded)ReadFromalways returns the recorded errorio.Copy error = flattened transport error, want caller's context canceled(and deadline exceeded)ReadFromnever substitutesio.Copy error = context canceled, want the permanent upload-part failureWith no B2 credentials set:
go test -race -count=50 -run ReadFrom ./b2passes, as do #236's cancellation tests at-race -count=30;go test -race -count=5 -shuffle=on ./...andgo vet ./...pass. The SDK panel reviewed two rounds; its findings (the caller-cause leak, godoc identity, deadline-test timing) are addressed.Also: fixes the
b2test build on main#247 removed the
objectForDeleteOptionTesthelper fromb2/b2_test.go, while #249'sTestObjectDeleteRejectsFolderStatus(b2/core_edges_test.go) still used it, so theb2tests stopped compiling onmainonce both merged. Commit 9d869bb inlines the same two setup linesTestObjectDeleteAllowsCopyStatususes. Test-only; merging this PR makesmaingreen again.Merge order
#236 has merged; this PR is now based on
mainand synced with it.Closes #245