Repository navigation
fix(x): make transport failure injection race-free, honor the RoundTripper contract and bound window sizes - #251
Merged
Conversation
david-santacruz-backblaze
suggested changes
Oct 30, 2026
david-santacruz-backblaze
left a comment
There was a problem hiding this comment.
Mate, please review following suggestions for this pr
x/transport/transport.go,RoundTrip,o.status > 0branch:io.Copy(io.Discard, req.Body)is unbounded and ignoresreq.Context().Response(500)with anio.Pipebody that is never written or closed blocks past 2s. #176 item 2 covers pipe bodies. Closereq.Bodywithout draining, or drain in a goroutine that stops onctx.Done().x/transport/failures_test.go: add a case with anio.Pipebody and a timeout. It should fail on the current code and pass after the fix.- Split out the
chore: satisfy static analysiscommit and theioutil.NopClosertoio.NopCloserswap, or note them in the PR description. Neither is in #176. - Note in the PR description that
Stallnow closesreq.Bodyon cancel. #176 only asks forctx.Err(). - Change
Window.Newtowindow.Newin the changelog fragment and state that it panics whenresolution <= 0. - Add tests for
MatchPathSubstring,FailureRateat 0 and 1, andStallwith a live context. #176 asks for each option to be covered.
Contributor
Author
|
@david-santacruz-backblaze Thanks, all six are addressed (c548485, c3e1103, 3071ff3, 4d054b8), and the branch is synced with main:
|
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
The exported helpers under
x/are used for fault testing, including by blazer's own integration tests. #176 lists five problems:transport.Triggerwrites the failure rate from a goroutine whileRoundTripreads it, which is a data race under-race. Withcontext.Background()the goroutine never exits.Response(status)) neither read nor closereq.Body, and return a nilHeader, which breaks thehttp.RoundTrippercontract.Stallstill sends the request after the request context ends.window.Newdivides by zero whensize < resolution(on first use) orresolution == 0(insideNew).internal/retry(used by every API call) andx/transporthad no tests.Fix
x/transport:atomic.Uint64holding the float64 bits).Triggerusescontext.AfterFuncinstead of a goroutine, so nothing outlives the context.req.Bodywithout draining it (draining could block forever on a pipe that is never written), and setHeader,ProtoandContentLength.Stallreturnsctx.Err()when the context ends first, without calling the wrapped transport. It also closes the request body in that case, which Fix the x/transport data race and RoundTripper contract, the x/window divide-by-zero, and add tests #176 didn't ask for but theRoundTrippercontract requires once the request isn't forwarded. Its godoc says so.x/window:Newpanics with a documented message when a finite window hasresolution <= 0, liketime.NewTicker. No signature change.size < resolution.sweepcompares absolute time-slot indices instead of array indices. The SDK panel found that with one bucket, the modulo comparison was always equal, so a window polled more often than its resolution never expired events.x/transportoption is covered (Trigger,Response/Body,Stallwith a cancelled and a live context,MatchPathSubstring,FailureRateat 0 and 1, an unwrittenio.Pipebody), inx/transport/failures_test.go,x/window/new_bounds_test.goandinternal/retry/retry_test.go, in new files so they don't collide with test: add credential-free tests for calls only covered by live tests #231 and test: tighten minor sync, simulator and window test assertions #233.Verification
Each break was applied alone, failed its test, then passed after revert:
sweepback to the modulo comparisonReduce after resolution boundary = 1, want nilinteger divide by zeroresolution <= 0check removedNew panic = runtime error: integer divide by zero, want documented resolution errorTriggerback to a goroutineTrigger(context.Background()) added goroutines: before=2 after=34request body closed=false reads=0, want drained and closedHeaderStallsends after the context endsstalled RoundTrip error = <nil>, want context.CanceledBackoff(31s) = 1m3.3s, want capped below 32sBackoff(1s) = 1.03s, want [2.02s, 2.06s)io.Copy)synthetic RoundTrip did not return while pipe body is unwrittenMatchPathSubstringmatches every pathnon-matching response = … StatusCode:500 …, want wrapped responseFailureRate(0)treated as 1response = … StatusCode:500 …, want wrapped responseStallwith a live context skips the wrapped transportwrapped RoundTripper was not called after the stallWith no B2 credentials set:
go test -race -count=20 ./internal/retry/... ./x/...,go test -race -count=5 -shuffle=on ./...,go vet ./...andmake cipass. The live tests that useStall(base/integration_test.go) still compile; their intent is unchanged, because the request was cancelled anyway.Housekeeping in this PR
Two small changes outside #176's list, kept here because they touch the same files:
18558a7("chore: satisfy static analysis"): lint fixes the new tests needed.ioutil.NopCloser→io.NopCloserinx/transport/transport.go(io/ioutilis deprecated); no behavior change.Deferred (pre-existing, not changed here): with an already-cancelled context,
Triggerfollowed by a laterFailureRateoption leaves the final rate depending on option order.Closes #176