60 lines
1.8 KiB
Go
60 lines
1.8 KiB
Go
package routing
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
)
|
|
|
|
// ── Fallback Runner ─────────────────────────
|
|
|
|
// DispatchFunc is called by the fallback runner to attempt a request
|
|
// against a specific candidate. Returns nil on success, error on failure.
|
|
// The caller is responsible for setting up SSE headers, streaming, etc.
|
|
// This is NOT the actual dispatch — it's the "try this candidate" callback.
|
|
type DispatchFunc func(candidate Candidate) error
|
|
|
|
// RunWithFallback tries candidates in order until one succeeds.
|
|
// Returns the candidate that succeeded, the decision (updated with fallback
|
|
// depth), and any error if all candidates fail.
|
|
//
|
|
// maxRetries controls how many fallback attempts are made beyond the first.
|
|
// maxRetries=0 means only try the first candidate (no fallback).
|
|
// maxRetries=-1 means try all candidates.
|
|
func RunWithFallback(candidates []Candidate, decision *Decision, maxRetries int, dispatch DispatchFunc) (Candidate, *Decision, error) {
|
|
if len(candidates) == 0 {
|
|
return Candidate{}, decision, fmt.Errorf("no routing candidates available")
|
|
}
|
|
|
|
// Determine how many candidates to try
|
|
limit := 1 // just the first
|
|
if maxRetries < 0 {
|
|
limit = len(candidates) // try all
|
|
} else if maxRetries > 0 {
|
|
limit = 1 + maxRetries
|
|
if limit > len(candidates) {
|
|
limit = len(candidates)
|
|
}
|
|
}
|
|
|
|
var lastErr error
|
|
for i := 0; i < limit; i++ {
|
|
c := candidates[i]
|
|
err := dispatch(c)
|
|
if err == nil {
|
|
// Success
|
|
if decision != nil {
|
|
decision.Selected = c.ConfigID
|
|
decision.FallbackDepth = i
|
|
}
|
|
return c, decision, nil
|
|
}
|
|
|
|
lastErr = err
|
|
if i < limit-1 {
|
|
log.Printf("[routing] candidate %s (%s) failed: %v — trying next", c.ConfigID, c.ProviderID, err)
|
|
}
|
|
}
|
|
|
|
return Candidate{}, decision, fmt.Errorf("all %d routing candidates failed; last error: %w", limit, lastErr)
|
|
}
|