Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions driver/kubernetes/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
stderrors "errors"
"fmt"
"math/rand"
"net"
"strings"
"syscall"
Expand Down Expand Up @@ -389,9 +390,37 @@ func isTransientConnectionError(err error) bool {
return false
}

// calculateBackoff calculates the delay for the given attempt with exponential backoff.
// calculateBackoff calculates the delay for the given attempt with exponential
// backoff and additive jitter, drawing from [d, 2d] capped by maxDelay, where d
// is the exponential value for the attempt. The exponential value is the floor
// rather than the midpoint, so a retry is never issued sooner than the schedule
// would have on its own.
Comment on lines +393 to +397

@chagui chagui Aug 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: doc comment is much longer than this package's convention and duplicates the caller's documentation. I'd leave the rationale behind the retries to tryWithBackoff and focus on calculateBackoff contract and non-obvious implementation choice (floor vs. midpoint).

See http://go.dev/doc/comment

Doc comments should not explain internal details such as the algorithm used in the current implementation. Those are best left to comments inside the function body.

Suggested change
// calculateBackoff calculates the delay for the given attempt with exponential
// backoff and additive jitter, drawing from [d, 2d] capped by maxDelay, where d
// is the exponential value for the attempt. The exponential value is the floor
// rather than the midpoint, so a retry is never issued sooner than the schedule
// would have on its own.
// calculateBackoff returns a randomized exponential backoff delay for attempt,
// never exceeding maxDelay.

(and remove everything below this to keep it to 2 lines)

//
// The exponential component alone is a pure function of the attempt number, so
// every builder retrying the same condition waits for exactly the same durations.
// That matters for the case this backoff exists to handle: CSR approval lagging
// node readiness is a cluster-wide event, so concurrent builds scheduled onto
// newly-ready nodes hit the transient TLS error at the same moment and would
// then retry in unison, concentrating load on the API server while it is already
// working through the approval backlog.
Comment on lines +401 to +405

@chagui chagui Aug 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicates tryWithBackoff (few lines above: https://github.com/1991santhu/buildx/blob/fix%2Fkubernetes-dial-backoff-jitter/driver/kubernetes/driver.go#L316-L318) docs, attaching the TLS/CSR narrative to calculateBackoff doesn't really make sense here because calculateBackoff only needs to justify its own local choice (floor vs. midpoint).

//
// The jitter is added to the interval rather than centred on it, so a retry is
// never issued sooner than the exponential schedule intended. Centring it would
// let the first retry fire after baseDelay/2, undercutting a configured minimum
// while the API server is still working through the CSR backlog. The extra is
// bounded by the remaining headroom so the result never exceeds maxDelay.
func calculateBackoff(attempt int, baseDelay, maxDelay time.Duration) time.Duration {
return min(time.Duration(1<<uint(attempt))*baseDelay, maxDelay)
d := min(time.Duration(1<<uint(attempt))*baseDelay, maxDelay)

extra := d
if headroom := maxDelay - d; headroom < extra {
extra = headroom
}
if extra <= 0 {
return d
}

return d + time.Duration(rand.Int63n(int64(extra)+1)) // #nosec G404 -- no strong randomness required for retry jitter
Comment on lines +413 to +423

@chagui chagui Aug 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: took me some time to understand the function, I think we could simplify the code and use more explicit names to improve readability:

Suggested change
d := min(time.Duration(1<<uint(attempt))*baseDelay, maxDelay)
extra := d
if headroom := maxDelay - d; headroom < extra {
extra = headroom
}
if extra <= 0 {
return d
}
return d + time.Duration(rand.Int63n(int64(extra)+1)) // #nosec G404 -- no strong randomness required for retry jitter
delay := min(time.Duration(1<<uint(attempt))*baseDelay, maxDelay)
jitterRange := min(delay, maxDelay-delay) // never exceed maxDelay
if jitterRange <= 0 {
return delay
}
// Floored at delay so jitter never issues a retry sooner than
// plain exponential backoff would have.
return delay + time.Duration(rand.Int63n(int64(jitterRange)+1)) // #nosec G404 -- no strong randomness required for retry jitter

}

func (d *Driver) Client(ctx context.Context, opts ...client.ClientOpt) (*client.Client, error) {
Expand Down
58 changes: 58 additions & 0 deletions driver/kubernetes/driver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package kubernetes

import (
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestCalculateBackoffNeverShorterThanSchedule(t *testing.T) {
const (
baseDelay = 500 * time.Millisecond
maxDelay = 10 * time.Second
)

for attempt := range 6 {
schedule := min(time.Duration(1<<uint(attempt))*baseDelay, maxDelay)
ceiling := min(2*schedule, maxDelay)

for range 500 {
got := calculateBackoff(attempt, baseDelay, maxDelay)
require.GreaterOrEqual(t, got, schedule,
"attempt %d must never wait less than the exponential schedule", attempt)
require.LessOrEqual(t, got, ceiling,
"attempt %d must not exceed twice the schedule, nor maxDelay", attempt)
}
}
}

func TestCalculateBackoffRespectsMaxDelay(t *testing.T) {
const (
baseDelay = 500 * time.Millisecond
maxDelay = 10 * time.Second
)

for range 500 {
require.LessOrEqual(t, calculateBackoff(20, baseDelay, maxDelay), maxDelay,
"a large attempt count must stay capped at maxDelay")
}
}

func TestCalculateBackoffVaries(t *testing.T) {
const (
baseDelay = 500 * time.Millisecond
maxDelay = 10 * time.Second
)

seen := make(map[time.Duration]struct{})
for range 500 {
seen[calculateBackoff(3, baseDelay, maxDelay)] = struct{}{}
}

// A deterministic implementation returns a single value. The range at
// attempt 3 is two seconds wide, so one distinct value across 500 draws

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: at attempt 3, d is 4s, so the additive-jitter range is [4s, 8s], four seconds wide not two. I think this comment is left over from the earlier equal-jitter implementation. The test itself looks correct.

Suggested change
// attempt 3 is two seconds wide, so one distinct value across 500 draws
// attempt 3 is four seconds wide, so one distinct value across 500 draws

// would not be chance.
require.Greater(t, len(seen), 1,
"backoff must vary so concurrent builders do not retry in lockstep")
}
Loading