2026-09-23 16:48:36 +00:00
|
|
|
// Package generation coordinates model attempts, backend permits, and bounded
|
|
|
|
|
// built-in HTTP retries for one generation request.
|
|
|
|
|
package generation
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"math/rand/v2"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-11-02 14:38:00 +00:00
|
|
|
"go.maximumdirect.net/promptkit/internal/capacity"
|
|
|
|
|
"go.maximumdirect.net/promptkit/internal/domain"
|
|
|
|
|
"go.maximumdirect.net/promptkit/internal/llm"
|
|
|
|
|
"go.maximumdirect.net/promptkit/internal/observation"
|
2026-09-23 16:48:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
initialBackoff = 500 * time.Millisecond
|
|
|
|
|
maximumBackoff = 8 * time.Second
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Coordinator implements the internal model client using the existing backend
|
|
|
|
|
// capacity manager. Exactly one of builtIn and injected is selected.
|
|
|
|
|
type Coordinator struct {
|
|
|
|
|
manager *capacity.Manager
|
|
|
|
|
builtIn *llm.OpenAICompatibleClient
|
|
|
|
|
injected llm.Client
|
|
|
|
|
now func() time.Time
|
|
|
|
|
jitter func(time.Duration) time.Duration
|
|
|
|
|
wait func(context.Context, time.Duration) error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewBuiltIn selects the built-in HTTP retry path.
|
|
|
|
|
func NewBuiltIn(manager *capacity.Manager, client *llm.OpenAICompatibleClient) *Coordinator {
|
|
|
|
|
return &Coordinator{manager: manager, builtIn: client, now: time.Now, jitter: randomJitter, wait: waitContext}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewInjected selects one opaque client dispatch with no automatic retry.
|
|
|
|
|
func NewInjected(manager *capacity.Manager, client llm.Client) *Coordinator {
|
|
|
|
|
return &Coordinator{manager: manager, injected: client, now: time.Now, jitter: randomJitter, wait: waitContext}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExhaustedError retains the final eligible status failure and the number of
|
|
|
|
|
// attempts actually dispatched.
|
|
|
|
|
type ExhaustedError struct {
|
|
|
|
|
attempts uint64
|
|
|
|
|
cause error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *ExhaustedError) Error() string {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return "transient model retries exhausted"
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("transient model retries exhausted after %d attempts", e.attempts)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *ExhaustedError) Unwrap() error {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return e.cause
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *ExhaustedError) Attempts() uint64 {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
return e.attempts
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate executes either one opaque client call or a frozen built-in HTTP
|
|
|
|
|
// sequence with one permit per actual dispatch.
|
|
|
|
|
func (c *Coordinator) Generate(ctx context.Context, req domain.GenerateRequest) (*domain.GenerateResponse, error) {
|
2026-10-13 16:56:56 +00:00
|
|
|
response, err, _ := c.generate(ctx, req, false)
|
|
|
|
|
return response, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GenerateWithEvidence runs the built-in retry path and returns its factual
|
|
|
|
|
// attempt evidence alongside any response retained before cancellation.
|
|
|
|
|
func (c *Coordinator) GenerateWithEvidence(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
req domain.GenerateRequest,
|
|
|
|
|
) (*domain.GenerateResponse, error, GenerationEvidence) {
|
|
|
|
|
return c.generate(ctx, req, true)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 13:48:16 +00:00
|
|
|
// PreparationError identifies a built-in request preparation failure.
|
|
|
|
|
type PreparationError struct{ Cause error }
|
|
|
|
|
|
|
|
|
|
func (e *PreparationError) Error() string { return e.Cause.Error() }
|
|
|
|
|
func (e *PreparationError) Unwrap() error { return e.Cause }
|
|
|
|
|
|
2026-10-13 16:56:56 +00:00
|
|
|
func (c *Coordinator) generate(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
req domain.GenerateRequest,
|
|
|
|
|
captureEvidence bool,
|
|
|
|
|
) (*domain.GenerateResponse, error, GenerationEvidence) {
|
2026-09-23 16:48:36 +00:00
|
|
|
if c.injected != nil {
|
2026-10-13 17:05:40 +00:00
|
|
|
return c.generateInjected(ctx, req, captureEvidence)
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
|
|
|
|
prepared, err := c.builtIn.PrepareGeneration(req)
|
|
|
|
|
if err != nil {
|
2026-11-05 13:48:16 +00:00
|
|
|
return nil, &PreparationError{Cause: err}, GenerationEvidence{PreparationFailed: true}
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-13 16:56:56 +00:00
|
|
|
evidence := GenerationEvidence{}
|
2026-10-13 20:27:46 +00:00
|
|
|
if captureEvidence {
|
|
|
|
|
evidence.InitialEndpoint = prepared.InitialEndpoint()
|
|
|
|
|
}
|
2026-10-13 16:56:56 +00:00
|
|
|
var usage observation.UsageAccumulator
|
|
|
|
|
finish := func(response *domain.GenerateResponse, err error) (*domain.GenerateResponse, error, GenerationEvidence) {
|
|
|
|
|
if captureEvidence {
|
|
|
|
|
evidence.Usage = usage.Observed()
|
|
|
|
|
}
|
|
|
|
|
return response, err, evidence
|
|
|
|
|
}
|
2026-09-23 16:48:36 +00:00
|
|
|
var attempts, retriesUsed uint64
|
2026-09-23 17:05:31 +00:00
|
|
|
recorder := observation.FromContext(ctx)
|
|
|
|
|
generation := req.Generation
|
|
|
|
|
if generation == 0 {
|
|
|
|
|
generation = 1
|
|
|
|
|
}
|
2026-09-23 16:48:36 +00:00
|
|
|
for {
|
|
|
|
|
if err := ctx.Err(); err != nil {
|
2026-10-13 16:56:56 +00:00
|
|
|
return finish(nil, err)
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
2026-09-23 17:05:31 +00:00
|
|
|
ordinal := attempts + 1
|
2026-11-05 14:25:07 +00:00
|
|
|
result := c.execute(ctx, req.Target.BackendID, prepared, captureEvidence)
|
|
|
|
|
if result.Dispatched {
|
2026-09-23 16:48:36 +00:00
|
|
|
attempts++
|
|
|
|
|
}
|
2026-11-05 14:27:28 +00:00
|
|
|
recordAttempt(result, false, captureEvidence, &evidence, &usage, recorder, generation, ordinal)
|
2026-11-05 14:25:07 +00:00
|
|
|
if result.Outcome.Err == nil {
|
2026-09-23 17:05:31 +00:00
|
|
|
if err := ctx.Err(); err != nil {
|
2026-10-13 16:56:56 +00:00
|
|
|
if captureEvidence {
|
2026-11-05 14:25:07 +00:00
|
|
|
return finish(result.Outcome.Response, err)
|
2026-10-13 16:56:56 +00:00
|
|
|
}
|
|
|
|
|
return finish(nil, err)
|
2026-09-23 17:05:31 +00:00
|
|
|
}
|
2026-11-05 14:25:07 +00:00
|
|
|
return finish(result.Outcome.Response, nil)
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
2026-11-05 14:25:07 +00:00
|
|
|
if !result.Dispatched || !eligibleStatus(result.Outcome) {
|
2026-10-13 16:56:56 +00:00
|
|
|
if captureEvidence {
|
2026-11-05 14:25:07 +00:00
|
|
|
return finish(result.Outcome.Response, result.Outcome.Err)
|
2026-10-13 16:56:56 +00:00
|
|
|
}
|
2026-11-05 14:25:07 +00:00
|
|
|
return finish(nil, result.Outcome.Err)
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
|
|
|
|
if retriesUsed == req.TransientRetries {
|
|
|
|
|
if retriesUsed == 0 {
|
2026-11-05 14:25:07 +00:00
|
|
|
return finish(nil, result.Outcome.Err)
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
2026-11-05 14:25:07 +00:00
|
|
|
return finish(nil, &ExhaustedError{attempts: attempts, cause: result.Outcome.Err})
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
|
|
|
|
if err := ctx.Err(); err != nil {
|
2026-10-13 16:56:56 +00:00
|
|
|
return finish(nil, err)
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
|
|
|
|
retryOrdinal := retriesUsed + 1
|
2026-11-05 14:25:07 +00:00
|
|
|
delay := retryDelay(c.backoff(retryOrdinal), result.Outcome.RetryNotBefore, c.now())
|
2026-09-23 17:05:31 +00:00
|
|
|
waitStart := c.now()
|
|
|
|
|
waitErr := c.wait(ctx, delay)
|
2026-10-13 16:56:56 +00:00
|
|
|
waitDuration := elapsed(waitStart, c.now())
|
|
|
|
|
if captureEvidence {
|
|
|
|
|
evidence.RetryDelays = append(evidence.RetryDelays, waitDuration)
|
|
|
|
|
}
|
2026-09-23 17:05:31 +00:00
|
|
|
if recorder != nil {
|
|
|
|
|
recorder.Delay(observation.DelayData{Generation: generation, Attempt: attempts,
|
2026-10-13 16:56:56 +00:00
|
|
|
Elapsed: waitDuration, Failure: ClassifyFailure(waitErr)})
|
2026-09-23 17:05:31 +00:00
|
|
|
}
|
|
|
|
|
if waitErr != nil {
|
2026-10-13 16:56:56 +00:00
|
|
|
return finish(nil, waitErr)
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
|
|
|
|
if err := ctx.Err(); err != nil {
|
2026-10-13 16:56:56 +00:00
|
|
|
return finish(nil, err)
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
|
|
|
|
retriesUsed++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 14:27:28 +00:00
|
|
|
func (c *Coordinator) generateInjected(ctx context.Context, req domain.GenerateRequest, captureEvidence bool) (*domain.GenerateResponse, error, GenerationEvidence) {
|
2026-10-13 17:05:40 +00:00
|
|
|
evidence := GenerationEvidence{}
|
|
|
|
|
var usage observation.UsageAccumulator
|
2026-09-23 17:05:31 +00:00
|
|
|
generation := req.Generation
|
|
|
|
|
if generation == 0 {
|
|
|
|
|
generation = 1
|
|
|
|
|
}
|
2026-11-05 14:27:28 +00:00
|
|
|
result := c.executeInjected(ctx, req)
|
|
|
|
|
recordAttempt(result, true, captureEvidence, &evidence, &usage, observation.FromContext(ctx), generation, 1)
|
|
|
|
|
if captureEvidence {
|
|
|
|
|
evidence.Usage = usage.Observed()
|
|
|
|
|
}
|
|
|
|
|
if result.Outcome.Err != nil {
|
|
|
|
|
return nil, result.Outcome.Err, evidence
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
2026-09-23 17:05:31 +00:00
|
|
|
if err := ctx.Err(); err != nil {
|
2026-10-13 17:05:40 +00:00
|
|
|
if captureEvidence {
|
2026-11-05 14:27:28 +00:00
|
|
|
return result.Outcome.Response, err, evidence
|
2026-10-13 17:05:40 +00:00
|
|
|
}
|
2026-11-05 14:27:28 +00:00
|
|
|
return nil, err, evidence
|
2026-09-23 17:05:31 +00:00
|
|
|
}
|
2026-11-05 14:27:28 +00:00
|
|
|
return result.Outcome.Response, nil, evidence
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *Coordinator) executeInjected(ctx context.Context, req domain.GenerateRequest) attemptResult {
|
|
|
|
|
result := attemptResult{StartedAt: c.now()}
|
|
|
|
|
release, err := c.manager.Acquire(ctx, req.Target.BackendID)
|
|
|
|
|
result.CapacityWait = c.capacityWait(req.Target.BackendID, result.StartedAt)
|
2026-09-23 17:05:31 +00:00
|
|
|
if err != nil {
|
2026-11-05 14:27:28 +00:00
|
|
|
result.Outcome.Err = err
|
|
|
|
|
return result
|
2026-09-23 17:05:31 +00:00
|
|
|
}
|
2026-11-05 14:27:28 +00:00
|
|
|
defer release()
|
2026-09-23 16:48:36 +00:00
|
|
|
if err := ctx.Err(); err != nil {
|
2026-11-05 14:27:28 +00:00
|
|
|
result.Outcome.Err = err
|
|
|
|
|
return result
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
2026-11-05 14:27:28 +00:00
|
|
|
start := c.now()
|
|
|
|
|
result.Dispatched = true
|
|
|
|
|
requested := req.StructuredOutput != nil
|
|
|
|
|
result.Outcome.StructuredOutputRequested = &requested
|
|
|
|
|
result.Outcome.Response, result.Outcome.Err = c.injected.Generate(ctx, req)
|
|
|
|
|
result.RequestDuration = elapsed(start, c.now())
|
|
|
|
|
if result.Outcome.Err == nil && result.Outcome.Response == nil {
|
|
|
|
|
result.Outcome.Err = llm.ErrMalformedResponse
|
|
|
|
|
}
|
|
|
|
|
if result.Outcome.Err == nil {
|
|
|
|
|
result.Outcome.Usage = result.Outcome.Response.Usage
|
|
|
|
|
result.Outcome.UsagePresence = result.Outcome.Response.UsagePresence
|
|
|
|
|
}
|
|
|
|
|
return result
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
|
|
|
|
|
2026-11-05 14:25:07 +00:00
|
|
|
type attemptResult struct {
|
|
|
|
|
Outcome llm.AttemptOutcome
|
|
|
|
|
Dispatched bool
|
|
|
|
|
CapacityWait time.Duration
|
|
|
|
|
RequestDuration time.Duration
|
|
|
|
|
StartedAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 14:27:28 +00:00
|
|
|
// recordAttempt runs after the attempt has released its permit, including when
|
|
|
|
|
// the recorder calls application code. Injected clients expose no HTTP facts.
|
|
|
|
|
func recordAttempt(result attemptResult, injected, capture bool, evidence *GenerationEvidence, usage *observation.UsageAccumulator, recorder *observation.Recorder, generation, ordinal uint64) {
|
|
|
|
|
outcome := result.Outcome
|
|
|
|
|
failure := ClassifyFailure(outcome.Err)
|
|
|
|
|
if capture {
|
|
|
|
|
attempt := AttemptEvidence{
|
|
|
|
|
StartedAt: result.StartedAt.UTC(), Dispatched: result.Dispatched,
|
|
|
|
|
CapacityWait: result.CapacityWait, RequestDuration: result.RequestDuration,
|
|
|
|
|
Failure: failure,
|
|
|
|
|
}
|
|
|
|
|
if injected {
|
|
|
|
|
attempt.ResponseReceived = outcome.Response != nil
|
|
|
|
|
attempt.GenerationCompleted = outcome.Err == nil && outcome.Response != nil
|
|
|
|
|
if result.Dispatched {
|
|
|
|
|
attempt.CapacityWait = 0
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
attempt.ResponseReceived = outcome.Evidence.ResponseReceived
|
|
|
|
|
attempt.HTTP = cloneHTTPEvidence(outcome.Evidence)
|
|
|
|
|
attempt.ResponseDecoded = outcome.ResponseDecoded
|
|
|
|
|
attempt.GenerationCompleted = outcome.GenerationCompleted
|
|
|
|
|
}
|
|
|
|
|
if outcome.StructuredOutputRequested != nil {
|
|
|
|
|
requested := *outcome.StructuredOutputRequested
|
|
|
|
|
attempt.StructuredOutputRequested = &requested
|
|
|
|
|
}
|
|
|
|
|
if result.Dispatched {
|
|
|
|
|
usage.Add(outcome.Usage, outcome.UsagePresence)
|
|
|
|
|
var single observation.UsageAccumulator
|
|
|
|
|
single.Add(outcome.Usage, outcome.UsagePresence)
|
|
|
|
|
attempt.Usage = single.Observed()
|
|
|
|
|
}
|
|
|
|
|
evidence.Attempts = append(evidence.Attempts, attempt)
|
|
|
|
|
}
|
|
|
|
|
if recorder != nil {
|
|
|
|
|
recorder.Attempt(observation.AttemptData{Generation: generation, Attempt: ordinal,
|
|
|
|
|
Dispatched: result.Dispatched, CapacityWait: result.CapacityWait, GenerationDuration: result.RequestDuration,
|
|
|
|
|
Outcome: attemptOutcome(outcome.Err), Failure: failure, StatusCode: outcome.StatusCode,
|
|
|
|
|
Usage: outcome.Usage, UsagePresence: outcome.UsagePresence})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 14:25:07 +00:00
|
|
|
func (c *Coordinator) execute(ctx context.Context, backendID string, prepared *llm.PreparedGeneration, captureEvidence bool) attemptResult {
|
|
|
|
|
result := attemptResult{StartedAt: c.now()}
|
2026-09-23 16:48:36 +00:00
|
|
|
release, err := c.manager.Acquire(ctx, backendID)
|
2026-11-05 14:25:07 +00:00
|
|
|
result.CapacityWait = c.capacityWait(backendID, result.StartedAt)
|
2026-09-23 16:48:36 +00:00
|
|
|
if err != nil {
|
2026-11-05 14:25:07 +00:00
|
|
|
result.Outcome.Err = err
|
|
|
|
|
return result
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
|
|
|
|
defer release()
|
|
|
|
|
if err := ctx.Err(); err != nil {
|
2026-11-05 14:25:07 +00:00
|
|
|
result.Outcome.Err = err
|
|
|
|
|
return result
|
2026-09-23 17:05:31 +00:00
|
|
|
}
|
|
|
|
|
dispatchStart := c.now()
|
2026-10-13 16:56:56 +00:00
|
|
|
if captureEvidence {
|
2026-11-05 14:25:07 +00:00
|
|
|
result.Outcome = prepared.ExecuteWithEvidence(ctx)
|
|
|
|
|
result.Dispatched = result.Outcome.Evidence.DispatchInvoked
|
2026-10-13 16:56:56 +00:00
|
|
|
} else {
|
2026-11-05 14:25:07 +00:00
|
|
|
result.Outcome = prepared.Execute(ctx)
|
|
|
|
|
result.Dispatched = true
|
2026-10-13 16:56:56 +00:00
|
|
|
}
|
2026-11-05 14:25:07 +00:00
|
|
|
result.RequestDuration = elapsed(dispatchStart, c.now())
|
|
|
|
|
return result
|
2026-09-23 17:05:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *Coordinator) capacityWait(backendID string, start time.Time) time.Duration {
|
|
|
|
|
if !c.manager.Limited(backendID) {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
return elapsed(start, c.now())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func elapsed(start, end time.Time) time.Duration {
|
|
|
|
|
if d := end.Sub(start); d > 0 {
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func attemptOutcome(err error) domain.ExecutionOutcome {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return domain.ExecutionOutcomeSuccess
|
|
|
|
|
}
|
|
|
|
|
return domain.ExecutionOutcomeError
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 16:56:56 +00:00
|
|
|
// ClassifyFailure maps an execution error to the shared content-free category.
|
|
|
|
|
func ClassifyFailure(err error) domain.ExecutionFailureKind {
|
2026-09-23 17:05:31 +00:00
|
|
|
if err == nil {
|
|
|
|
|
return domain.ExecutionFailureNone
|
|
|
|
|
}
|
|
|
|
|
if errors.Is(err, context.Canceled) {
|
|
|
|
|
return domain.ExecutionFailureCanceled
|
|
|
|
|
}
|
|
|
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
|
|
|
return domain.ExecutionFailureDeadline
|
|
|
|
|
}
|
|
|
|
|
if errors.Is(err, llm.ErrUnexpectedStatus) {
|
|
|
|
|
return domain.ExecutionFailureProviderStatus
|
|
|
|
|
}
|
|
|
|
|
if errors.Is(err, llm.ErrTransport) {
|
|
|
|
|
return domain.ExecutionFailureTransport
|
|
|
|
|
}
|
|
|
|
|
if errors.Is(err, llm.ErrMalformedResponse) {
|
|
|
|
|
return domain.ExecutionFailureMalformedResponse
|
|
|
|
|
}
|
|
|
|
|
if errors.Is(err, llm.ErrInvalidRequest) {
|
|
|
|
|
return domain.ExecutionFailureInvalidRequest
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
2026-09-23 17:05:31 +00:00
|
|
|
return domain.ExecutionFailureGeneration
|
2026-09-23 16:48:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func eligibleStatus(outcome llm.AttemptOutcome) bool {
|
|
|
|
|
if !errors.Is(outcome.Err, llm.ErrUnexpectedStatus) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
switch outcome.StatusCode {
|
|
|
|
|
case 429, 500, 502, 503, 504:
|
|
|
|
|
return true
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *Coordinator) backoff(ordinal uint64) time.Duration {
|
|
|
|
|
base := initialBackoff
|
|
|
|
|
for step := uint64(1); step < ordinal && base < maximumBackoff; step++ {
|
|
|
|
|
base *= 2
|
|
|
|
|
}
|
|
|
|
|
half := base / 2
|
|
|
|
|
return half + c.jitter(half)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func retryDelay(local time.Duration, notBefore, now time.Time) time.Duration {
|
|
|
|
|
if providerDelay := notBefore.Sub(now); providerDelay > local {
|
|
|
|
|
return providerDelay
|
|
|
|
|
}
|
|
|
|
|
return local
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func randomJitter(max time.Duration) time.Duration {
|
|
|
|
|
return time.Duration(rand.Int64N(int64(max) + 1))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func waitContext(ctx context.Context, delay time.Duration) error {
|
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
timer := time.NewTimer(delay)
|
|
|
|
|
defer timer.Stop()
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return ctx.Err()
|
|
|
|
|
case <-timer.C:
|
|
|
|
|
return ctx.Err()
|
|
|
|
|
}
|
|
|
|
|
}
|