Abort uploads early once projected to exceed the target body limit · Entire
Abort uploads early once projected to exceed the target body limit
19606ad→main·
Soph·2mo ago·4 files·+230 added/-4 removed
Layered on top of the streaming pack observer from the previous commit. The observer now consults a caller-supplied aborter on every Read; when it returns true, subsequent Reads surface ErrPackUploadAborted and the upload is short-circuited. The bootstrap loop wires this in via a new selfImposedBudget local:
- initialised from p.TargetMaxPack
- ratchets down each time we observe a smaller server-side cutoff (parsed limit from a 413, or — more commonly with reverse proxies that don't announce the limit in their 4xx body — the bytes that flowed before the connection was cut)
- persists across attempts so each retry starts with the tightest known ceiling
shouldAbortPush makes the per-Read decision in two regimes:
header parsed AND at least one full object observed: project final size as bytesSent × totalObjects ÷ objectsSent; abort if that projection exceeds 95% of budget. Catches front-loaded packs (cli-checkpoints-shape) at ~10% of the upload instead of the full ~limit.
header still in flight or zero objects observed: fall back to a flat bytesSent ≥ 95% × budget threshold. Covers the "no parser signal yet but we know we must cut by N bytes" case from a prior 413.
A minBytesBeforeAbort floor (8 MiB) keeps the projection from firing on the header alone; an aborted-early flag lets the push failure path distinguish self-cut from server-cut so the user-facing notice and slog line read sensibly in both cases.
Combined with the calibration heuristic from soph/smart-subdivision, each subsequent attempt costs ~5% of the previous attempt's wasted upload instead of ~100% — for the 500 MiB Cloudflare cap that's ~25 MiB per failed retry instead of 500 MiB.
Sessions
4d50f20d0358View transcript
Changes
4
internal/strategy/bootstrap
- Mbootstrap.go+84/-4
- Mbootstrap_test.go+68
- Mpack_observer.go+44
- Mpack_observer_test.go+34
// p.TargetMaxPack — saving an entire ~limit-sized wasted upload.
calibratedBytesPerObject := int64(estimatedBytesPerObject)
// selfImposedBudget tracks the byte ceiling we use for mid-stream
// abort. Initialised from the user-supplied target limit; ratchets
down each time we observe a smaller server-side cutoff (parsed
413 limit or, more commonly with reverse proxies that don't
announce the limit in the response, the bytes we managed to send
before the connection was cut). Re-used across attempts so every
failed push refines the budget.
selfImposedBudget := p.TargetMaxPack
for _, batch := range batches {
if batch.subsumed {
cmds := []gitproto.PushCommand{{
cmds := convert.PlansToPushCommands(stagePlans)
observer := newPackStreamObserver(packReader)
if selfImposedBudget > 0 {
budget := selfImposedBudget
observer.SetAborter(func(bytesSent, objectsSent, totalObjects int64) bool {
return shouldAbortPush(bytesSent, objectsSent, totalObjects, budget)
})
}
pushErr := p.TargetPusher.PushPack(ctx, cmds, observer)
sentBytes := observer.Bytes()
objectsSent := observer.ObjectsSent()
totalObjects := observer.TotalObjects()
abortedEarly := observer.Aborted()
if pushErr != nil {
_ = packReader.Close()
sizeIssue := abortedEarly || isTargetBodyLimitError(pushErr)
p.log(
"bootstrap batch push failed",
"branch", batch.Plan.TargetRef.String(),
"batch", idx+1,
"object_count", packObjectCount,
"objects_sent", objectsSent,
"total_objects_in_pack", totalObjects,
"will_subdivide", isTargetBodyLimitError(pushErr) && len(batch.chain) > 0,
"aborted_early", abortedEarly,
"will_subdivide", sizeIssue && len(batch.chain) > 0,
"error", pushErr.Error())
if isTargetBodyLimitError(pushErr) && len(batch.chain) > 0 {
limit := p.TargetMaxPack
if parsed := targetBodyLimit(pushErr); parsed > 0 {
limit = parsed
} else if abortedEarly && selfImposedBudget > 0 {
limit = selfImposedBudget
}
// Calibrate before subdividing. The new value carries
// over to the next iteration's pre-flight check, so a
// ...
}
}
}
// shouldAbortPush decides whether an in-flight push has crossed the // "we are clearly going to overshoot the budget" threshold. Two // regimes: // // - Pack header has been parsed (totalObjects > 0) AND at least one // object has fully gone through (objectsSent > 0): project the // final pack size as bytesSent × totalObjects ÷ objectsSent and // abort if that projection exceeds budget × safety. // // - Header not yet observed or no full object yet: fall back to a // simple bytesSent ≥ budget × safety check. Catches the common // "we have a budget from a prior 413, just don't send past it // again" case before the parser has anything to say. // // Returns false until bytesSent crosses minBytesBeforeAbort so the // pack header alone never triggers an abort. func shouldAbortPush(bytesSent, objectsSent, totalObjects, budget int64) bool { if budget <= 0 || bytesSent < minBytesBeforeAbort { return false } const safety = 95 // percent of budget at which we cut threshold := budget * safety / 100 if objectsSent > 0 && totalObjects > 0 { projected := bytesSent * totalObjects / objectsSent return projected > threshold } return bytesSent > threshold }
// observedSubdivisionFactor estimates how many sub-packs a rejected // push should be split into based on bytes actually transmitted before // the server cut us off.
```go
func TestShouldAbortPush(t *testing.T) {
t.Parallel()
const cap500 = 500 * 1024 * 1024
cases := []struct {
name string
bytesSent int64
objectsSent int64
totalObjects int64
budget int64
want bool
}{
{
name: "no budget never aborts",
budget: 0, bytesSent: 1 << 30, want: false,
},
{
name: "tiny upload below floor never aborts even at full budget",
bytesSent: 1024, budget: cap500, want: false,
},
{
name: "projection under threshold proceeds",
bytesSent: 50 * 1024 * 1024,
objectsSent: 25, totalObjects: 100,
budget: cap500, want: false,
},
{
name: "front-loaded projection trips abort",
bytesSent: 50 * 1024 * 1024,
objectsSent: 5, totalObjects: 100,
budget: cap500, want: true,
},
}
}
func TestObservedSubdivisionFactor(t *testing.T) {
t.Parallel()
cases := []struct {
name string
bytesSent int64
objectsSent int64
totalObjects int64
budget int64
want bool
}{
{
name: "example case",
}
}
}