Files
zeekay 5623a248ff kms: bound the read, and retry inside the bound
A KMS that answers on the second try is the common case — a pod that starts
before its network is ready, a rollout on the KMS side. A read that gives up on
the first answer turns those into a start that does not happen, on every node
at once; a read that never gives up turns them into a start that never
finishes.

Retry does both: one deadline, taken once, covering the attempt in flight as
well as the waits between attempts. Every attempt runs under a context derived
from that deadline, so a read that hangs ends at the bound rather than past it,
and the wait is trimmed to what is left of the budget so the whole budget is
spent trying.

The tests measure it: a read that never answers and a read that hangs both end
at the limit.
2026-08-21 07:03:24 -07:00

49 lines
1.5 KiB
Go

package kms
import (
"context"
"time"
)
// Retry calls read until it answers, or until limit elapses.
//
// A KMS that answers on the second try is the common case — a pod that starts
// before its network is ready, a rollout on the KMS side — and a read that
// gives up on the first answer turns those into a start that does not happen.
// A read that never gives up turns them into a start that never finishes.
// So: retry, and bound it.
//
// The bound is one deadline, taken once, and it covers the attempt in flight as
// well as the waits between attempts. Every attempt runs under a context
// derived from that deadline, so a read that hangs is cut at the bound rather
// than extending past it, and the loop stops as soon as the next wait would
// cross it. Whatever the caller does with the error, it does within limit.
//
// The wait doubles from half a second and is trimmed to what is left of the
// budget, so the whole budget is spent trying rather than abandoned because the
// next wait would have been inconvenient.
func Retry(limit time.Duration, read func(context.Context) error) error {
ctx, cancel := context.WithTimeout(context.Background(), limit)
defer cancel()
deadline, _ := ctx.Deadline()
for wait := 500 * time.Millisecond; ; wait *= 2 {
err := read(ctx)
if err == nil {
return nil
}
left := time.Until(deadline)
if left <= 0 {
return err
}
if wait > left {
wait = left
}
select {
case <-time.After(wait):
case <-ctx.Done():
return err
}
}
}