keypool

package
v0.4.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 28, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package keypool implements per-key circuit-breaker state and configurable Pool selection over healthy keys. State is persisted in pkg/state under "secret_health:<keyHash>" (circuit records), "pool_rr:<poolName>" (round-robin counters), and "pool_lru:<poolName>:<keyHash>" (LRU timestamps).

Supported selection strategies (KeySelection):

  • "prioritized" (default) — always pick the first healthy key in declaration order.
  • "round-robin" — distribute traffic evenly using a counter.
  • "least-recently-used" — pick the key with the oldest last-used timestamp.

The KeySelection enum lives in this package (not policy) so policy can import keypool without creating a cycle when its Service composes Selector + Limiter at runtime.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoHealthyKeys = errors.New("keypool: no healthy keys in pool")

Functions

func ClearCircuit

func ClearCircuit(ctx context.Context, store kv.Store, keyHash string) error

ClearCircuit deletes the circuit-breaker record for a key from the KV store. This is best-effort: if the DEL fails the error is returned to the caller, who should log a warning but not fail the outer operation.

Use this when a key is permanently deleted from the catalog so that orphaned secret_health:* keys do not accumulate in Redis indefinitely (R-8).

Types

type CircuitRecord

type CircuitRecord struct {
	State          CircuitState   `json:"state"`
	OpenUntil      time.Time      `json:"open_until,omitempty"`
	BackoffStep    int            `json:"backoff_step"`
	LastTransition time.Time      `json:"last_transition"`
	Indefinite     bool           `json:"indefinite,omitempty"`
	Reason         CooldownReason `json:"reason,omitempty"`
}

CircuitRecord is the per-key circuit-breaker state stored under "secret_health:<keyHash>" in pkg/state.

type CircuitState

type CircuitState int

CircuitState describes the current health of a key.

const (
	CircuitClosed   CircuitState = iota // healthy, accepting traffic
	CircuitOpen                         // unhealthy, skip
	CircuitHalfOpen                     // single probe allowed
)

func (CircuitState) String

func (s CircuitState) String() string

String returns a stable lower-snake label for the circuit state, suitable for logs and API responses.

type CooldownReason

type CooldownReason string

CooldownReason describes why a key was placed into cooldown (circuit open). The empty string means unknown / legacy record written before this field existed.

const (
	ReasonUpstreamAuthFailed   CooldownReason = "upstream_auth_failed"
	ReasonUpstreamRateLimited  CooldownReason = "upstream_rate_limited"
	ReasonUpstreamServerError  CooldownReason = "upstream_server_error"
	ReasonUpstreamNetworkError CooldownReason = "upstream_network_error"
	ReasonLocalRateLimited     CooldownReason = "local_rl_exhausted"
)

type FailureKind

type FailureKind int

FailureKind classifies the upstream failure for circuit-breaker transitions.

const (
	FailureAuth           FailureKind = iota // 401/403 → open indefinitely
	FailureRateLimitShort                    // 429 with Retry-After ≤ 5s → stay closed
	FailureRateLimitLong                     // 429 with Retry-After > 5s → open for that duration
	FailureServerError                       // 5xx → exponential backoff
	FailureNetwork                           // net/timeout (post-connect) → treat as 5xx
	// FailureUpstreamUnreachable is a dial-phase failure (connection refused,
	// no route, DNS, TLS handshake): the connection was never established, so
	// it is a property of the host/baseURL, not the key. It NEVER trips a key
	// breaker — the pipeline retries the same host with backoff and reports an
	// unreachable status. Distinguishes a misconfigured baseURL from a bad key.
	FailureUpstreamUnreachable
)

type KeySelection

type KeySelection string

KeySelection is the algorithm a Selector uses to pick from healthy candidates. Persisted as a string in Policy.Spec.KeySelection.

const (
	// KeySelectionPrioritized drains keys in declaration order — the
	// first healthy key in the candidate set wins. Default.
	KeySelectionPrioritized KeySelection = "prioritized"
	// KeySelectionRoundRobin rotates evenly across healthy keys via a
	// per-scope counter.
	KeySelectionRoundRobin KeySelection = "round-robin"
	// KeySelectionLeastRecentlyUsed prefers the key whose last successful
	// use was furthest in the past.
	KeySelectionLeastRecentlyUsed KeySelection = "least-recently-used"
)

type Selector

type Selector struct {
	// contains filtered or unexported fields
}

Selector picks HostKeys from Pools and tracks per-key circuit-breaker state.

func New

func New(s kv.Store, log *slog.Logger, clock func() time.Time, rng *rand.Rand) *Selector

New constructs a Selector. clock and rng may be nil. When rng is nil, a new rand seeded from time.Now().UnixNano() is used.

func (*Selector) Pick

func (s *Selector) Pick(ctx context.Context, scope string, algo KeySelection, keys []*hostkey.HostKey, exclude ...[]*hostkey.HostKey) (*hostkey.HostKey, error)

Pick returns a healthy HostKey from the candidate set. scope is the kv key tag (typically the owning Policy's Meta.Name) used for the round-robin counter and LRU timestamps. algo is the selection strategy; empty falls back to KeySelectionPrioritized.

exclude is an optional list of keys to skip even if healthy (e.g. for retry-with-exclusion in future callers). Pass nil to skip the check.

Open keys past their OpenUntil are auto-transitioned to HalfOpen and become eligible. Concurrent Picks may both pick the same half-open key; the caller's RecordSuccess/RecordFailure resolves the outcome (acceptable).

func (*Selector) PickWithExclude

func (s *Selector) PickWithExclude(ctx context.Context, scope string, algo KeySelection, keys []*hostkey.HostKey, exclude []*hostkey.HostKey) (*hostkey.HostKey, error)

PickWithExclude is a convenience wrapper around Pick that accepts an explicit exclude list. Callers that always have an exclude slice can use this to avoid the variadic syntax.

func (*Selector) ReadCircuit

func (s *Selector) ReadCircuit(ctx context.Context, keyHash string) (CircuitRecord, bool)

ReadCircuit returns the stored circuit-breaker record for a key and whether a record actually exists in the state store. A missing or undecodable record yields a default-closed record with found=false — i.e. the key has never failed and is assumed healthy. This is a read-only accessor for the admin plane; it does not auto-transition expired-open records the way Pick does.

func (*Selector) RecordFailure

func (s *Selector) RecordFailure(ctx context.Context, keyHash string, kind FailureKind, retryAfter time.Duration)

RecordFailure transitions according to kind. retryAfter is honoured only for RateLimit kinds.

func (*Selector) RecordLocalRateLimit

func (s *Selector) RecordLocalRateLimit(ctx context.Context, keyHash string, retryAfter time.Duration)

RecordLocalRateLimit cools down a key because our own rate-limit rule fired (KeyQuotaExhausted from pkg/ratelimit). Distinct from upstream-driven cooldowns: no backoff escalation, no half-open probe — the duration is deterministic from Retry-After.

Used by the pipeline's post-Pick Reserve path (issue #89, future PR).

func (*Selector) RecordSuccess

func (s *Selector) RecordSuccess(ctx context.Context, keyHash string)

RecordSuccess transitions a key to CircuitClosed and resets backoff.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL