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:

shouldAbortPush makes the per-Read decision in two regimes:

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

// 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",
        }
    }
}