Documentation
¶
Overview ¶
Package lb implements cadish's load-balancing primitives: a pool of backends (an Upstream) that picks one backend per request according to a balancing policy, keeps the eligible set healthy with active probes and passive ejection, and tracks pod/IP churn behind dns:// and k8s:// targets without a reload. These are SELF-CONTAINED algorithms; the server wires them in a later milestone (M5b).
An Upstream IS an origin.Origin ¶
Upstream implements origin.Origin (Fetch(ctx, *Request) (*Response, error)), so the server treats a load-balanced pool exactly like any other origin: a `route @x -> web` makes `web` (an Upstream) the origin for matching requests. The streaming/ownership contract from the origin package is honored verbatim — Upstream delegates the per-backend fetch to an httporigin-style client and returns that origin.Response UNCHANGED (no body buffering, caller owns/closes the body).
The routing-key integration seam (what M5b MUST feed) ¶
Two policies need a routing key the server computes per request: `sticky` (hash a cookie value or client IP so a user pins to one backend) and `shard_by key` (hash an arbitrary key). The server already computes this value via the `{sticky}` normalizer. Rather than widen origin.Request (which is backend-agnostic and shared by every origin), the key travels in the context:
ctx = lb.WithRoutingKey(ctx, stickyValue) // server computes stickyValue resp, err := upstream.Fetch(ctx, req) // Upstream reads it back
Upstream.Fetch calls RoutingKey(ctx) to read it. The mapping by policy:
- round_robin / least_conn — routing key IGNORED.
- sticky — routing key REQUIRED; absent/empty ⇒ Upstream falls back to round_robin for that request (documented, not an error).
- shard_by url — routing key IGNORED; the request Key (URL path) is the hash input instead.
- shard_by key — routing key REQUIRED (same fallback as sticky).
This is the ONLY thing the server must wire beyond constructing the Upstream and calling Start(ctx): compute the sticky/shard key and attach it with WithRoutingKey before Fetch. origin.Request and origin.Origin are unchanged.
Index ¶
- Variables
- func HealthKeyword(s string) bool
- func MaxWindow() int
- func ParseALPNArg(d *cadishfile.Directive) ([]string, error)
- func ParseHTTPReuseArg(d *cadishfile.Directive) error
- func ParseResolveArg(d *cadishfile.Directive) (time.Duration, []string, error)
- func ParseSNIArg(d *cadishfile.Directive) (string, error)
- func ParseTLSInsecureArg(d *cadishfile.Directive) error
- func RoutingKey(ctx context.Context) (key string, ok bool)
- func ValidateExpectToken(s string) bool
- func WithExcludeBaseURL(ctx context.Context, baseURL string) context.Context
- func WithRoutingKey(ctx context.Context, key string) context.Context
- type BackendHealth
- type Config
- type Doer
- type Endpoint
- type EndpointResolver
- type HealthSpec
- type HostHeaderPolicy
- type Option
- func WithClock(now func() time.Time) Option
- func WithEndpointResolver(r EndpointResolver) Option
- func WithOriginFactory(f OriginFactory) Option
- func WithPassiveEjection(threshold int, dur time.Duration) Option
- func WithProbeDoer(d Doer) Option
- func WithResolveInterval(d time.Duration) Option
- func WithResolver(r Resolver) Option
- type OriginFactory
- type Policy
- type Resolver
- type Ring
- type ShardKey
- type StickySpec
- type Target
- type TargetScheme
- type Timeouts
- type Upstream
- func (u *Upstream) AnyHealthy() bool
- func (u *Upstream) Endpoints() []string
- func (u *Upstream) Fetch(ctx context.Context, req *origin.Request) (*origin.Response, error)
- func (u *Upstream) Fingerprint() string
- func (u *Upstream) HasK8sBackend() bool
- func (u *Upstream) HealthSnapshot() UpstreamHealth
- func (u *Upstream) Inflight() int64
- func (u *Upstream) Name() string
- func (u *Upstream) Owner(key string, healthyOnly bool) (string, bool)
- func (u *Upstream) Policy() Policy
- func (u *Upstream) ResolveUpgrade(ctx context.Context, req *origin.Request) (origin.UpgradeTarget, error)
- func (u *Upstream) Start(ctx context.Context)
- type UpstreamHealth
Constants ¶
This section is empty.
Variables ¶
var ErrNoBackend = errors.New("lb: no eligible backend")
ErrNoBackend is returned by Fetch when no eligible backend could serve the request (all unhealthy, ejected, at capacity, or already tried). The origin chain above an Upstream treats it as a connection-class failure (StatusOf ⇒ 0) and may fall through to another origin.
Functions ¶
func HealthKeyword ¶
HealthKeyword reports whether a token is a `health` sub-key. Exported so the `cadish check` linter can find where a variadic `expect` list ends and validate exactly the tokens the runtime parser consumes (check≡run).
func MaxWindow ¶
func MaxWindow() int
MaxWindow is the largest accepted health `window N` sample count. Exported so the `cadish check` linter rejects an absurd window at lint time with the SAME bound the runtime parser enforces in parseHealth (check≡run) — otherwise check passes a config that would drive a ~2GB-per-backend allocation and fail at `cadish run`.
func ParseALPNArg ¶ added in v0.2.1
func ParseALPNArg(d *cadishfile.Directive) ([]string, error)
ParseALPNArg parses an `alpn <proto>…` directive into its protocol list (TLSVERIFY, `alpn http/1.1`). It requires at least one non-empty protocol token and rejects an accidental matcher ref (`@name`). Shared by lint and runtime so `cadish check` validates the same tokens the runtime parser consumes (check≡run).
func ParseHTTPReuseArg ¶
func ParseHTTPReuseArg(d *cadishfile.Directive) error
ParseHTTPReuseArg parses an `http_reuse never` directive (gap H6). ONLY `never` is accepted (owner decision 2026-06-24 — keep the surface minimal; the implicit default is connection reuse). Any other keyword — including HAProxy's safe/aggressive/always — is a positioned error naming the supported set. It returns nil on the valid `never` form; the caller sets DisableReuse.
func ParseResolveArg ¶ added in v0.2.1
ParseResolveArg parses the per-upstream `resolve [<interval>] [nameserver <ip:port>…]` directive (RESOLVER, sound subset). Accepted forms:
resolve 10s resolve nameserver 10.134.8.94:53 resolve 10s nameserver 10.134.8.94:53 10.134.8.95:53
The optional leading token is a duration (the dynamic re-resolution interval); the optional `nameserver` keyword introduces one or more `ip:port` DNS servers the pool dials instead of the system resolver. At least one of the two must be present (a bare `resolve` is an error). Shared by lint and runtime so `cadish check` validates exactly the tokens the runtime parser consumes (check≡run).
func ParseSNIArg ¶
func ParseSNIArg(d *cadishfile.Directive) (string, error)
ParseSNIArg parses an `sni <server-name>` directive into its single hostname argument (gap H6). It takes exactly one non-empty host token; an accidental matcher ref (`@name`) is rejected so a typo doesn't silently advertise a bogus SNI. The validity check mirrors a fixed `host_header VALUE`.
func ParseTLSInsecureArg ¶ added in v0.2.1
func ParseTLSInsecureArg(d *cadishfile.Directive) error
ParseTLSInsecureArg validates a bare `tls_insecure` flag (TLSVERIFY, HAProxy `ssl verify none`). It takes NO arguments; any token is a positioned error. It returns nil on the valid bare form; the caller sets Insecure. Shared by lint and runtime so `cadish check` rejects `tls_insecure foo` (check≡run).
func RoutingKey ¶
RoutingKey reads the routing key attached by WithRoutingKey. ok is false (and key is "") when none was attached or it was empty.
func ValidateExpectToken ¶
ValidateExpectToken reports whether s is a valid `health … expect` acceptance — an exact status code (100–599) or a status class `Nxx` (1≤N≤5). It is the same predicate the runtime parser enforces via parseExpectToken, exported so the `cadish check` linter rejects a malformed `expect` token (e.g. `6xx`, `999`, `foo`) at lint time instead of only at load (check≡run).
func WithExcludeBaseURL ¶ added in v0.2.2
WithExcludeBaseURL marks one backend baseURL the pool must NEVER select for this request, even when the ring/policy would otherwise land on it. PeerOrigin uses it so a read-through is never dialed back to SELF: self stays in the ownership ring (so ownership stays consistent cluster-wide) but is excluded from the actual routing decision. This closes the health-flap race where pickRing walks clockwise onto self between the peerorigin self-guard's point-in-time Owner() read and Fetch's pick — a self-dial that would coalesce against the in-flight leader and stall (F-B2).
func WithRoutingKey ¶
WithRoutingKey returns a child context carrying key as the per-request routing key consumed by the sticky / shard_by-key policies. The server computes key from the `{sticky}` normalizer (a cookie value, the client IP, …) and attaches it before calling Upstream.Fetch. An empty key is treated as "no key".
Types ¶
type BackendHealth ¶
type BackendHealth struct {
ID string `json:"id"`
BaseURL string `json:"base_url"`
// Healthy is the active health-FSM verdict (true when no health spec).
Healthy bool `json:"healthy"`
// Ejected is true while the backend is passively ejected (consecutive
// connection/5xx failures over the passive threshold).
Ejected bool `json:"ejected"`
// InFlight is the current in-flight request count.
InFlight int64 `json:"inflight"`
// ConsecFail is the current consecutive-failure count feeding ejection.
ConsecFail int `json:"consec_fail"`
}
BackendHealth is one backend's current health/capacity state.
type Config ¶
type Config struct {
// Name is the upstream/cluster name (the directive's first arg).
Name string
// Kind is "upstream" or "cluster" (informational).
Kind string
// Backends are the `to` targets, in source order. At least one is required.
Backends []Target
// Policy is the balancing policy (default RoundRobin).
Policy Policy
// Sticky is the sticky spec (non-nil only for Policy == Sticky).
Sticky *StickySpec
// Shard selects what Shard policy hashes (ShardNone unless Policy == Shard).
Shard ShardKey
// Health is the active-probe spec; nil disables active health checking (all
// backends are then always eligible).
Health *HealthSpec
// Timeouts are per-backend transport timeouts (zero ⇒ library defaults).
Timeouts Timeouts
// HostHeader is the upstream Host-header policy applied to every backend's
// httporigin (backlog #11). The zero value is httporigin.HostPreserve (forward
// the client Host) — the default. The config layer fills it from the
// `host_header` directive; the default origin factory passes it to
// httporigin.WithHostPolicy.
HostHeader HostHeaderPolicy
// SNI is the TLS ClientHello server name advertised to every HTTPS backend's
// httporigin (gap H6, `sni <server-name>`). Empty ⇒ Go's default (the dialed
// host) — the zero value leaves the datapath unchanged. The config layer fills
// it from the `sni` directive; the default origin factory passes it to
// httporigin.WithSNI.
SNI string
// DisableReuse disables backend connection reuse for every backend's httporigin
// (gap H6, `http_reuse never`). False ⇒ keep-alive on (the default, datapath
// unchanged). The config layer fills it from `http_reuse never`; the default
// origin factory passes it to httporigin.WithDisableKeepAlives.
DisableReuse bool
// Insecure disables origin TLS verification for every backend (TLSVERIFY,
// `tls_insecure` = HAProxy `ssl verify none`). False ⇒ full verification (the
// secure default, datapath unchanged). Mutually exclusive with CAFile (enforced
// by the config layer). Applied to BOTH the origin factory and the health probe.
Insecure bool
// CAFile is the `ca_file <path>` source for RootCAs, kept for the pool
// fingerprint (a *x509.CertPool is not stably hashable). "" ⇒ system roots. The
// config layer loads + validates the PEM at `cadish check` time into RootCAs.
CAFile string
// RootCAs is the per-upstream verification pool loaded from CAFile (TLSVERIFY,
// `ca_file`). nil ⇒ system roots (the default). Applied to BOTH the origin
// factory and the health probe transport. The config layer fills it.
RootCAs *x509.CertPool
// CAPEMHash is a stable content hash (hex sha256) of the loaded ca_file PEM bytes,
// folded into the pool fingerprint so a CA rotation IN PLACE (same path, new bytes)
// forces a fresh pool across a hot reload instead of transplanting the old RootCAs.
// "" ⇒ no ca_file (system roots), the byte-for-byte default. The config layer fills
// it from the bytes it already reads in loadCAFilePEM; *x509.CertPool itself is not
// stably hashable, so this hash stands in for it.
CAPEMHash string
// ALPN pins the origin TLS ALPN protocol list for every backend (TLSVERIFY,
// `alpn http/1.1`). Empty ⇒ Go's default (h2 auto-upgrade preserved). The config
// layer fills it; the origin factory passes it to httporigin.WithALPN.
ALPN []string
// ResolveInterval overrides how often dns:// / k8s:// targets are re-resolved
// (the inline `resolve <interval>` knob). 0 ⇒ defaultResolveInterval (30s), the
// byte-for-byte default. The config layer fills it from the `resolve` directive;
// lb.New promotes it onto the pool's re-resolution ticker.
ResolveInterval time.Duration
// Nameservers are the DNS servers (ip:port) to query for dns:// targets (the
// inline `resolve nameserver <ip:port>…` knob). Empty ⇒ the system resolver
// (net.DefaultResolver), the byte-for-byte default. When set, lb.New builds a
// per-pool PreferGo *net.Resolver that dials these servers in order.
Nameservers []string
// MaxConns caps concurrent in-flight requests per backend (0 = unlimited).
MaxConns int
// Replicas is the consistent-hash virtual-node count per backend (0 ⇒
// defaultReplicas). Exposed mainly for tests.
Replicas int
// Pos is the directive's source position.
Pos cadishfile.Pos
}
Config is a plain, fully-described upstream pool. Build it with ParseUpstream / ParseCluster from a Cadishfile directive, or by hand in tests.
func ParseCluster ¶
func ParseCluster(d *cadishfile.Directive) (Config, error)
ParseCluster builds a Config from a `cluster NAME { ... }` directive block. A cluster is a peer pool — semantically the cache-sharding case — and accepts exactly the same inner directives as ParseUpstream. (It does not force a shard policy; configure `shard_by url` explicitly, as the canonical example does.)
func ParseUpstream ¶
func ParseUpstream(d *cadishfile.Directive) (Config, error)
ParseUpstream builds a Config from an `upstream NAME { ... }` directive block. It validates structure and values, returning a positioned *cadishfile.ParseError on the first problem. Recognized inner directives:
to URL... (repeatable; ≥1 required) policy round_robin|least_conn|sticky|shard (or: lb POLICY) sticky by cookie NAME [else client_ip] (⇒ Policy sticky) sticky by client_ip (⇒ Policy sticky) shard_by url|key (⇒ Policy shard) health METHOD PATH expect CODE interval D window N threshold T timeout [connect D] [first_byte D] [between_bytes D] max_conns N replicas N (consistent-hash vnodes; tests)
Policy defaults to round_robin, or is inferred from a sticky/shard_by line when no explicit policy/lb directive is present.
type Doer ¶
Doer is the minimal HTTP client surface the active prober needs. *http.Client satisfies it; tests inject a fake so probing never touches the network.
type Endpoint ¶
Endpoint is one concrete ready pod address for a k8s:// target — both the IP and the resolved (numeric) port. Named ports are resolved to their number by the EndpointResolver, so the lb pool always works with numbers.
type EndpointResolver ¶
type EndpointResolver interface {
// ResolveEndpoints returns the current ready endpoints for service in namespace,
// resolving the requested port (numeric passthrough or named -> number). An error
// leaves the pool's previous endpoint set in place.
ResolveEndpoints(ctx context.Context, service, namespace, port string) ([]Endpoint, error)
// Watch registers onChange to be invoked whenever this (service, namespace)'s
// endpoints change, so the pool re-resolves within sub-second of pod churn rather
// than on the periodic timer. The pool calls it exactly once per k8s backend at
// Start; callers should not register the same onChange repeatedly, as each
// registration is independent (implementations need not dedupe).
//
// Watch returns a cancel func that DEREGISTERS the registration (FIX 4). The pool
// calls every cancel when its context is cancelled — when a k8s:// pool is rebuilt
// (fingerprint change) the old pool's listeners must be removed, or the dead
// *Upstream stays pinned via a never-removed listener (unbounded memory + O(N)
// fan-out growth over a long-lived controller). cancel is idempotent; an
// implementation that has nothing to deregister may return nil.
Watch(service, namespace string, onChange func()) (cancel func())
}
EndpointResolver resolves a k8s:// target to its current ready endpoints and lets a pool subscribe to change events (Approach C: informer + warm cache + event poke). internal/k8s implements it; tests inject a fake. It is distinct from the DNS Resolver because a k8s target needs the (service, namespace, port) triple and returns ip:port pairs (named ports already resolved), not bare addresses.
type HealthSpec ¶
type HealthSpec struct {
Method string // GET, HEAD, …
Path string // request path, e.g. "/"
// ExpectCode is the single accepted status for the single-int back-compat
// form (`expect 301`); 0 when the list/class form is used. Kept for callers
// and the config fingerprint; Matches is the source of truth.
ExpectCode int
// ExpectCodes are exact accepted statuses (`expect 200 301`).
ExpectCodes []int
// ExpectClasses are accepted status classes by leading digit (e.g. 2 for 2xx,
// 3 for 3xx) from the class form (`expect 2xx 3xx`).
ExpectClasses []int
Interval time.Duration // time between probes
Window int // sliding window size (probes considered)
Threshold int // successes→up / failures→down within the window
}
HealthSpec configures the active health prober: `health METHOD PATH expect CODE… interval D window N threshold T`.
`expect` accepts one or more acceptances: an exact status (`expect 301`), a list (`expect 200 301`), or a class form (`expect 2xx`, `expect 2xx 3xx`). A probe counts as a success when the response status matches ANY acceptance.
func ParseHealthSpec ¶
func ParseHealthSpec(d *cadishfile.Directive) (*HealthSpec, error)
ParseHealthSpec parses a `health METHOD PATH expect CODE interval D window N threshold T` directive into a HealthSpec. Exported so the cluster layer reuses the same active-health probe configuration as upstream pools.
func (*HealthSpec) Matches ¶
func (h *HealthSpec) Matches(code int) bool
Matches reports whether an HTTP status code counts as a healthy probe under this spec: it matches the single back-compat code, any listed exact code, or any accepted status class (2xx/3xx/…).
type HostHeaderPolicy ¶
type HostHeaderPolicy struct {
// Policy is the host-header mode (preserve / origin / fixed).
Policy httporigin.HostPolicy
// Value is the fixed Host for httporigin.HostFixed; ignored otherwise.
Value string
}
HostHeaderPolicy is the upstream Host-header policy for a pool's backends (backlog #11). It mirrors httporigin's policy so lb.Config can carry it without every caller importing httporigin. The zero value is "preserve" (Policy == httporigin.HostPreserve, the default).
type Option ¶
type Option func(*Upstream)
Option configures an Upstream.
func WithClock ¶
WithClock overrides the time source (tests use a fake clock to drive passive ejection windows deterministically).
func WithEndpointResolver ¶
func WithEndpointResolver(r EndpointResolver) Option
WithEndpointResolver injects the Kubernetes EndpointResolver used for k8s:// targets (internal/k8s supplies the real one; tests inject a fake). Absent it, a k8s:// pool resolves to no endpoints.
func WithOriginFactory ¶
func WithOriginFactory(f OriginFactory) Option
WithOriginFactory overrides how per-backend origins are built (tests return an in-memory or httptest-backed origin instead of a live httporigin).
func WithPassiveEjection ¶
WithPassiveEjection overrides the passive-ejection tuning (consecutive-failure threshold and ejection duration).
func WithProbeDoer ¶
WithProbeDoer overrides the HTTP client used by active health probes (tests inject a fake Doer so probing never hits the network).
func WithResolveInterval ¶
WithResolveInterval overrides the dynamic re-resolution interval (default 30s).
func WithResolver ¶
WithResolver overrides the DNS resolver for dns:// / k8s:// targets (tests inject a deterministic fake).
type OriginFactory ¶
OriginFactory builds the per-backend origin.Origin used to fetch from one resolved endpoint. The default builds an httporigin.Origin whose HTTP client honors the upstream's connect/first_byte timeouts. Tests inject a factory that returns an in-memory or httptest-backed origin.
type Policy ¶
type Policy int
Policy is a backend-selection strategy.
const ( // RoundRobin rotates evenly over the healthy backends (the default). RoundRobin Policy = iota // LeastConn picks the healthy backend with the fewest in-flight requests. LeastConn // Sticky pins a routing key (cookie value / client IP, supplied by the // caller via WithRoutingKey) to one backend via a consistent-hash ring; if // that backend is unhealthy the key rehashes to the next healthy backend. Sticky // Shard consistent-hashes the shard key (the request URL for ShardURL, or // the routing key for ShardKey) so a key maps to one backend — the // peer-cache-sharding case. Shard )
type Resolver ¶
type Resolver interface {
// Resolve returns the current addresses for host. An error leaves the
// previously-known endpoint set in place (resolution failures must not blow
// away a working backend set).
Resolve(ctx context.Context, host string) ([]string, error)
}
Resolver resolves a hostname to a set of addresses (IPv4/IPv6 literals). The stdlib net resolver satisfies it via netResolver; tests inject a fake so dynamic resolution is deterministic and offline.
type Ring ¶
type Ring struct {
// contains filtered or unexported fields
}
Ring is an exported, immutable consistent-hash ring over a set of string ids. It wraps the same Ketama-style ring the sticky/shard policies use internally, so cluster ownership routing (internal/cluster) hashes a cache key onto the SAME ring topology as upstream sharding — one well-tested implementation, no reinvention. A Ring is safe for concurrent Lookup; membership changes build a new Ring (NewRing).
func NewRing ¶
NewRing builds a Ring placing each id at `replicas` virtual nodes (<=0 ⇒ the package default, 160). Duplicate ids are de-duplicated; the result is deterministic for a given (ids, replicas).
func (*Ring) Lookup ¶
Lookup returns the id that owns key, walking clockwise past any id rejected by eligible (nil ⇒ all eligible) until an eligible id is found. ok is false when the ring is empty or no id is eligible. Walking past ineligible owners — rather than rebuilding the ring — is what makes a dead node's keys rehash only to the next node while every other key stays put.
type StickySpec ¶
type StickySpec struct {
// Source is "cookie" or "client_ip".
Source string
// Cookie is the cookie name when Source == "cookie".
Cookie string
// Fallback is the else-source ("client_ip" or "cookie"); empty if none.
Fallback string
// FallbackCookie is the cookie name when Fallback == "cookie".
FallbackCookie string
}
StickySpec records how the SERVER should derive the routing key for a Sticky upstream. The lb package does not read cookies or client IPs itself — the key arrives via WithRoutingKey — but parsing and exposing the spec lets the server (and `cadish check`) know what to compute for `{sticky}`.
type Target ¶
type Target struct {
// Raw is the original token, e.g. "http://h:80", "dns://svc:8080".
Raw string
// Scheme is how endpoints are discovered.
Scheme TargetScheme
// ConnScheme is the wire scheme used to reach an endpoint ("http"/"https").
// For static targets it is the URL's scheme; for dns/k8s it defaults to
// "http".
ConnScheme string
// Host is the hostname (static: the URL host; dns/k8s: the name to resolve).
Host string
// Port is the port string (may be empty for static, where the URL governs).
Port string
// Path is an optional base path for static targets (preserved and prefixed
// onto request keys by httporigin).
Path string
// Service and Namespace are the parsed parts of a k8s:// target's
// `service.namespace` host (populated only when Scheme == SchemeK8s). Meaning
// lives here in lb, not in the semantics-free cadishfile AST.
Service string
Namespace string
Pos cadishfile.Pos
}
Target is one `to` backend address. A static target is a single endpoint; a dns/k8s target expands into one endpoint per resolved address.
func ParseTarget ¶
func ParseTarget(tok string, pos cadishfile.Pos) (Target, error)
ParseTarget parses one `to`-style backend token (http://, https://, dns://, k8s://, or a bare host:port ⇒ http) into a Target, with a positioned error on a bad token. Exported so the cluster layer (internal/cluster) reuses the exact same target syntax + validation as upstream pools, rather than reinventing it.
type TargetScheme ¶
type TargetScheme int
TargetScheme classifies a backend target by how its endpoints are discovered.
const ( // SchemeStatic is an http://host:port / https://host:port target: a single // endpoint whose DNS is resolved by the HTTP client per request. SchemeStatic TargetScheme = iota // SchemeDNS is a dns://host:port target: the host is re-resolved (A/AAAA) // periodically and each address becomes an endpoint. SchemeDNS // SchemeK8s is a k8s://service.namespace:port target: resolved against the // Kubernetes API (EndpointSlices) to the live ready pod IP:port set, over which // the pool runs its own LB. Resolution is supplied by an injected // EndpointResolver (internal/k8s); absent one, the pool has no endpoints. SchemeK8s )
func (TargetScheme) String ¶
func (s TargetScheme) String() string
String renders the scheme keyword.
type Timeouts ¶
type Timeouts struct {
// Connect bounds dial+TLS establishment.
Connect time.Duration
// FirstByte bounds the wait for response headers after the request is sent.
FirstByte time.Duration
// BetweenBytes is the body-stall budget: the maximum gap between
// progress-making reads of the origin response body. It IS enforced — the
// server stamps it onto the Response and the idle-timeout reader aborts a
// stream that stalls longer than this (taking the stricter of this and the
// global -idle-timeout). See internal/server handler.go / idlereader.go.
BetweenBytes time.Duration
}
Timeouts are per-backend transport timeouts.
type Upstream ¶
type Upstream struct {
// contains filtered or unexported fields
}
Upstream is a named pool of backends that implements origin.Origin. It selects one healthy backend per request according to its policy, delegates the fetch, and records the outcome for passive ejection. Construct with New; call Start to run background health probing and dynamic re-resolution.
func New ¶
New builds an Upstream from cfg. It validates the config, then performs an initial synchronous resolution so static pools are immediately usable (dynamic pools begin populating here too; transient resolution failures are tolerated and retried by Start). New itself does not start background goroutines — call Start(ctx) for active health checks and periodic re-resolution.
func (*Upstream) AnyHealthy ¶ added in v0.2.1
AnyHealthy reports whether the pool currently has at least one ELIGIBLE backend — one whose health FSM is up AND which is not passively ejected — i.e. Varnish's nbsrv()>0. It is the O(1)-ish, no-dial liveness signal behind the `upstream_healthy NAME…` matcher (the AWS /aws-health-check probe): it reads only the maintained health/ejection state, never opening a connection or running a probe. It takes the same cheap read snapshot the picker uses, then short-circuits on the first live backend under that backend's own mutex (the same lock the fetch path already takes), so it is race-clean and never blocks the datapath.
func (*Upstream) Endpoints ¶
Endpoints returns the current backend base URLs (a snapshot). Order is not significant. Used by the cluster layer to learn the live peer set.
func (*Upstream) Fetch ¶
Fetch implements origin.Origin. It selects an eligible backend per the policy, delegates the fetch, and fails over to another eligible backend on a connection error, a 5xx, or a capacity miss — retrying until a backend succeeds, a definitive answer arrives (200/206/404/other 4xx), or no eligible backend remains (ErrNoBackend). The streaming response is returned UNCHANGED except for a transparent close hook that releases the backend's inflight/capacity accounting when the caller closes the body.
func (*Upstream) Fingerprint ¶
Fingerprint returns this pool's content fingerprint (see Config.fingerprint). It is the cross-package seam the config layer uses to diff pools across a reload: two pools with the same Name and the same Fingerprint are the same steady upstream and can be transplanted (kept running) rather than rebuilt.
func (*Upstream) HasK8sBackend ¶
HasK8sBackend reports whether any backend target is a k8s:// endpoint, i.e. one resolved through the injected/shared EndpointResolver (a Kubernetes client) rather than DNS or a fixed address. Zero-downtime reload uses it to avoid transplanting a pool onto a config whose owned k8s client is about to be torn down.
func (*Upstream) HealthSnapshot ¶
func (u *Upstream) HealthSnapshot() UpstreamHealth
HealthSnapshot returns a copied, immutable view of the pool's current backends and their health. It takes the same read snapshot the picker uses, then copies each backend's state under its own mutex, so it is race-clean and never blocks the datapath beyond a brief per-backend lock already on the fetch path.
func (*Upstream) Inflight ¶
Inflight returns the number of requests this pool is currently serving (a fetch that has acquired a backend and whose response body the caller has not yet closed). The server uses it to drain a REMOVED pool gracefully: stop sending new requests (the routing swap already did that) but wait for in-flight requests to finish before cancelling the pool's context. Safe for concurrent use.
func (*Upstream) Owner ¶
Owner returns the base URL of the backend that owns key on the consistent-hash ring, walking clockwise past ineligible (unhealthy / ejected) backends so the result is the node a sharded key currently lives on. ok is false when the pool is empty or no backend is eligible. This is the read-side of the SAME ring the Shard policy routes on; the cluster layer uses it to decide owner-vs-self for ownership routing (#8) while peer fetches go through Fetch on the same pool.
When healthyOnly is false, eligibility is ignored and the raw ring owner is returned (used to learn the topology's intended owner even when it is down, so the caller can decide strict-vs-degraded fallback itself).
func (*Upstream) ResolveUpgrade ¶ added in v0.2.1
func (u *Upstream) ResolveUpgrade(ctx context.Context, req *origin.Request) (origin.UpgradeTarget, error)
ResolveUpgrade implements origin.Upgrader for a load-balanced pool: it picks ONE eligible backend (honoring the pool's health FSM / passive ejection — the same `pick` the Fetch path uses, so an unhealthy or ejected backend is skipped) and delegates to that backend's per-upstream origin so the upgrade tunnel reuses the backend's own transport (SNI / keepalive / TLS-verify). It returns origin.ErrNoUpgradeBackend when the pool currently has no eligible backend (the edge equivalent of ErrNoBackend on the Fetch path). The tunnel runs OFF the Fetch path, so pool inflight accounting is intentionally not incremented here — a live hijacked connection is owned by the server's ReverseProxy, not a trackedBody.
type UpstreamHealth ¶
type UpstreamHealth struct {
Name string `json:"name"`
Policy string `json:"policy"`
Backends []BackendHealth `json:"backends"`
}
UpstreamHealth is a point-in-time view of an Upstream pool and its backends, for observability (the admin dashboard / Prometheus export). It exposes only immutable, copied values — never the live backend pointers — so a reader cannot mutate pool state.