mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-19 05:47:48 +00:00
fix(retry): retries-after-first semantics, ctx-aware backoff, hardened classification
Critical: attempts now counts retries performed AFTER the first try
(default 1 -> two tries total), not the total try count. The prior
reading made retry:{enabled:true}'s own documented default perform zero
retries. (*retryPolicy).totalTries() is the single conversion point,
clamping any attempts<1 to 1 so do()'s loop bound can never compute <=0
and skip calling call() entirely (no (nil,nil) return path).
Backoff waits are now ctx-aware: sleepFn func(time.Duration) is replaced
by waitFn func(context.Context, time.Duration) bool, whose production
implementation (defaultWaitFn) selects between time.After(d) and
ctx.Done(). A context canceled mid-wait aborts the remaining retries
immediately instead of blocking out the full backoff.
Also: classify a request-build failure (invalid method/URL) as
non-transient via errRequestBuildFailed, since retrying an identically
malformed request only wastes attempts; drop the drain cap to a
dedicated 64KiB constant instead of the 32MiB response cap; fix a
t.Fatal call from a non-test goroutine in the mid-stream-death test;
drop the misleading omitempty on Config.Retry (a struct value is never
'empty' to encoding/json); document the Retry-After >2s-falls-back-to-
backoff behavior and cover it with a direct table test.
README: add the retry config table, a RetryConfig field reference, and
a Retry section documenting the semantics. examples/kubernetes.yaml
gains a retry block alongside its other optional sections.
Gate: go test -race, gofmt, vet, yaegi-check, golangci-lint (incl a
fieldalignment fix on two new test structs) all clean. 295 subtests
pass, 0 failed. integration/ untouched.
This commit is contained in:
+11
-1
@@ -85,6 +85,16 @@ func newProviderHTTPError(resp *http.Response) *providerHTTPError {
|
||||
// with a non-2xx status.
|
||||
var errUpstream = errors.New("llmgateway: upstream request failed")
|
||||
|
||||
// errRequestBuildFailed marks an upstreamJSON failure that happened
|
||||
// before any network I/O — http.NewRequestWithContext rejected the
|
||||
// method or URL. It is chained alongside errUpstream (Go's multi-%w), so
|
||||
// an existing errors.Is(err, errUpstream) check still matches, while
|
||||
// retryPolicy's isTransient (retry.go) checks specifically for this
|
||||
// sentinel to treat it as non-transient: retrying an identically
|
||||
// malformed request produces the identical failure every time, so a
|
||||
// retry only wastes attempts.
|
||||
var errRequestBuildFailed = errors.New("llmgateway: build upstream request failed")
|
||||
|
||||
// providerAdapter is the gateway's uniform interface over one configured
|
||||
// upstream LLM provider. Task 9 (anthropic) and Task 10 (gemini) implement
|
||||
// it alongside this task's openai-type adapter; Tasks 11-12 wire a built
|
||||
@@ -188,7 +198,7 @@ func upstreamJSON(ctx context.Context, client *http.Client, method, url string,
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: build request: %w", errUpstream, err)
|
||||
return nil, fmt.Errorf("%w: %w: build request: %w", errUpstream, errRequestBuildFailed, err)
|
||||
}
|
||||
for k, vs := range hdr {
|
||||
for _, v := range vs {
|
||||
|
||||
Reference in New Issue
Block a user