Documentation
¶
Overview ¶
Package upstream is a backend pool for proxies, tunnels and load balancers: it picks a backend for each connection or request, tracks backend health actively and passively, and reports the state of every backend.
The pieces compose but also stand alone:
- A Balancer builds a Picker from Candidate values and needs no pool: RoundRobin, WeightedRoundRobin, Random, PowerOfTwo, LeastConn and ConsistentHash (a hash ring with optional bounded loads), or any implementation of the interface. Candidates carry in-flight counts, a moving average latency and the pool's backend, so custom balancers can route by load, latency or the backend's value; a Rebuilder updates its Picker incrementally.
- An OutlierDetector is passive health checking for one backend: consecutive failures or a failure rate eject it, with exponential backoff, and half-open trials let it back in.
- Package health runs active health checks on any set of targets and drives pools with them; package uphttp adds an HTTP check, a per-backend transport cache and an http.RoundTripper over a pool; package upnet dials the backends of a pool. All three find a backend's address the same way, with AddressOf: a string value, or a value that implements Addresser.
- A Pool combines them: it holds the members, routes picks through the balancer to eligible backends, counts requests in flight through a Lease, feeds outcomes to an outlier detector per backend, takes active health reports with Pool.ReportCheck, drains and removes backends gracefully, and announces every change as an Event.
Picking ¶
Pick, PickKey and PickHash return a Lease, which must be released exactly once with the request's Outcome; Lease.Report records the outcome early for leases that live long, such as connections, and Lease.Observe feeds the backend's latency. Pool.Do picks, runs a function and releases, retrying failures on other backends as RetryConfig allows, within a retry budget; a Request does the same for leases that outlive a function. Picks are lock-free and do not allocate: they read an immutable routing view that changes rebuild and publish atomically, so updates never disturb picks in flight. Updates that change no routing rebuild nothing, and rebuilds that overlap are coalesced. Pool.SetConfig changes the balancer, health, retry and fallback settings of a running pool.
Target.Priority puts members in tiers: picks go to the most preferred tier with a member that can take them, balanced within it, which makes active-backup pools. Target.MaxInFlight caps a member's leases, and Config.Overflow lets picks go on to the next tier when a tier is full.
Health ¶
A backend is eligible when it is not draining, not marked offline with Pool.MarkOffline, not reported unhealthy by active checks and not ejected by passive checks; each of these vetoes picks on its own. A half-open backend takes up to OutlierConfig.HalfOpenRequests trial requests at a time. When no backend is eligible, Pick fails with ErrUnavailable, or, with FallbackAll, picks among all members that are neither draining nor offline; panic mode also covers too few eligible members (Config.PanicThreshold) and eligible members that all refuse a pick. OutlierConfig.MaxEjectionPercent keeps passive health checking from ejecting too much of a pool.
State changes are edge-triggered: a backend that keeps failing is reported down once.
Concurrency ¶
All types are safe for concurrent use. No lock is held while calling a Balancer, a Clock, a probe or a subscriber, so callbacks may call back into the pool. The pool rebuilds its view on the goroutine that changed it, at most twice per change; a rebuild left over after that runs on a short-lived goroutine of the pool, which builds nothing once Close has begun. A Balancer that panics during a build does not reach any caller: the pool keeps its last view, emits EventBalancerPanic and builds again on its next change. The only timers the pool owns are those ending ejections and those stepping slow starts, which Close stops. No Close in this module waits; types that own goroutines have a Done channel for callers that need to wait for them.
Example ¶
package main
import (
"fmt"
"github.com/djylb/upstream"
)
func main() {
pool := upstream.New[string](upstream.Config{
Balancer: upstream.PowerOfTwo(),
Outlier: upstream.OutlierConfig{ConsecutiveFailures: 5},
})
defer pool.Close()
err := pool.Sync([]upstream.Target[string]{
{ID: "a", Value: "10.0.0.1:8080", Weight: 2},
{ID: "b", Value: "10.0.0.2:8080"},
})
if err != nil {
fmt.Println(err)
return
}
lease, err := pool.Pick()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("in flight:", lease.Backend().InFlight())
// Connect to lease.Value() here, then report how it went.
lease.Release(upstream.Success)
fmt.Println("in flight:", pool.Status()[0].InFlight+pool.Status()[1].InFlight)
}
Output: in flight: 1 in flight: 0
Index ¶
- Constants
- Variables
- func AddressOf[T any](v T) (addr string, ok bool)
- func Addressable[T any]() bool
- func WithOutcome(err error, o Outcome) error
- type Addresser
- type Backend
- func (b *Backend[T]) ID() string
- func (b *Backend[T]) InFlight() int64
- func (b *Backend[T]) Latency() time.Duration
- func (b *Backend[T]) MaxInFlight() int
- func (b *Backend[T]) Priority() int
- func (b *Backend[T]) State() State
- func (b *Backend[T]) Status() Status
- func (b *Backend[T]) Value() T
- func (b *Backend[T]) Weight() int
- type Balancer
- type BalancerPanicError
- type Candidate
- type CheckResult
- type Clock
- type Config
- type Drain
- type Event
- type EventKind
- type Fallback
- type HashConfig
- type Health
- type Lease
- type Outcome
- type OutlierConfig
- type OutlierDetector
- func (d *OutlierDetector) Admit() (Ticket, bool)
- func (d *OutlierDetector) Close()
- func (d *OutlierDetector) Record(t Ticket, o Outcome)
- func (d *OutlierDetector) Reset()
- func (d *OutlierDetector) SetConfig(cfg OutlierConfig)
- func (d *OutlierDetector) State() OutlierState
- func (d *OutlierDetector) Status() OutlierStatus
- type OutlierState
- type OutlierStatus
- type Picker
- type Pool
- func (p *Pool[T]) Available() int
- func (p *Pool[T]) Backend(id string) (*Backend[T], bool)
- func (p *Pool[T]) Backends() []*Backend[T]
- func (p *Pool[T]) Clock() Clock
- func (p *Pool[T]) Close()
- func (p *Pool[T]) Do(ctx context.Context, fn func(*Backend[T]) error) error
- func (p *Pool[T]) DoKey(ctx context.Context, key string, fn func(*Backend[T]) error) error
- func (p *Pool[T]) Done() <-chan struct{}
- func (p *Pool[T]) Drain(id string) (*Drain, bool)
- func (p *Pool[T]) Len() int
- func (p *Pool[T]) MarkOffline(b *Backend[T], cause error) bool
- func (p *Pool[T]) MarkOnline(b *Backend[T]) bool
- func (p *Pool[T]) Pick() (Lease[T], error)
- func (p *Pool[T]) PickHash(hash uint64) (Lease[T], error)
- func (p *Pool[T]) PickKey(key string) (Lease[T], error)
- func (p *Pool[T]) Put(t Target[T]) error
- func (p *Pool[T]) Remove(id string) (*Drain, bool)
- func (p *Pool[T]) ReportCheck(b *Backend[T], h Health, r CheckResult) bool
- func (p *Pool[T]) Request(key string) Request[T]
- func (p *Pool[T]) ResetOutlier(id string) bool
- func (p *Pool[T]) Resume(id string) bool
- func (p *Pool[T]) SetConfig(cfg Config) error
- func (p *Pool[T]) Status() []Status
- func (p *Pool[T]) Subscribe(fn func(Event[T])) (cancel func())
- func (p *Pool[T]) Sync(targets []Target[T]) error
- type Rebuilder
- type Request
- type RetryConfig
- type State
- type Status
- type Target
- type Ticket
Examples ¶
Constants ¶
const MaxWeight = math.MaxInt32
MaxWeight is the largest Target weight. It keeps the products of loads and weights that balancers compare far from overflowing.
Variables ¶
var ( // ErrConsecutiveFailures means the backend failed // OutlierConfig.ConsecutiveFailures requests in a row. ErrConsecutiveFailures = errors.New("upstream: too many consecutive failures") // ErrFailureRate means the share of failed requests in the window // reached OutlierConfig.FailurePercent. ErrFailureRate = errors.New("upstream: failure rate too high") // ErrTrialFailed means a half-open trial request failed. ErrTrialFailed = errors.New("upstream: half-open trial failed") )
Causes recorded when an OutlierDetector ejects its backend.
var ( // ErrNoBackends means the pool has no members. ErrNoBackends = errors.New("upstream: no backends") // unhealthy, ejected, draining, offline, out of half-open trial slots // or, for a retry, already tried. ErrSaturated is an ErrUnavailable // whose cause is capacity. ErrUnavailable = errors.New("upstream: no backend available") // ErrSaturated means that members that could take the pick refused it // because they were at their Target.MaxInFlight: the pick reached a // full tier and Config.Overflow is off, or every tier it could go on to // was full or unavailable too. It clears as leases are released, so it // calls for backpressure rather than for an alarm. // errors.Is(ErrSaturated, ErrUnavailable) holds. ErrSaturated = fmt.Errorf("upstream: backends at capacity: %w", ErrUnavailable) // ErrClosed means the pool was closed. ErrClosed = errors.New("upstream: pool closed") // ErrInvalidTarget means a Target has an empty or duplicate ID or a // weight out of range. ErrInvalidTarget = errors.New("upstream: invalid target") )
Errors returned by Pool.
var ( // ErrRetryLimit means the request has tried RetryConfig.Attempts // backends. ErrRetryLimit = errors.New("upstream: retry limit reached") // ErrRetryBudget means the pool's retry budget is spent. ErrRetryBudget = errors.New("upstream: retry budget exhausted") )
Errors returned by Request.Next when it declines a retry.
var ErrDrainCanceled = errors.New("upstream: drain canceled")
ErrDrainCanceled is returned by Drain.Wait when Resume ended the drain before the backend's leases were all released.
var ErrNoAddress = errors.New("upstream: backend value has no address")
ErrNoAddress means a backend value has no network address: it is neither a string nor an Addresser, and no function was given to derive one. Packages health, upnet and uphttp return it for such pools.
var ErrOffline = errors.New("upstream: backend offline")
ErrOffline is the Err of an EventStateChanged to StateOffline when Pool.MarkOffline was given no cause.
Functions ¶
func AddressOf ¶
AddressOf returns the address of a backend value: the value itself if its type is a string type, or the result of its Address method if it is an Addresser. ok is false for other values and for a nil Addresser.
func Addressable ¶
Addressable reports whether every value of type T has an address for AddressOf: T is a string type, or T implements Addresser. Packages that derive addresses check it once, up front, and fail with ErrNoAddress.
func WithOutcome ¶
WithOutcome returns an error that wraps err and that OutcomeOf maps to o, so that a function run by Pool.Do can say how its error reflects on the backend: for example Neutral for a request the client got wrong, which is neither counted against the backend nor retried. It returns nil if err is nil.
Types ¶
type Addresser ¶
type Addresser interface {
// Address returns the backend's address, as for net.Dial.
Address() string
}
An Addresser is a backend value that knows its network address, usually a host and port. Pools whose values carry more than an address, such as a struct with a zone or labels, implement it so that health checks, dialers and HTTP transports find the address without a function of their own.
type Backend ¶
type Backend[T any] struct { // contains filtered or unexported fields }
A Backend is one member of a Pool: a Target as the pool currently routes to it. Changing a target's weight or value in the pool creates a new Backend that shares the old one's counters and health state, so leases keep the version they were issued with; an update that changes neither keeps the Backend.
func (*Backend[T]) InFlight ¶
InFlight returns the number of leases on the backend not yet released.
func (*Backend[T]) Latency ¶
Latency returns the moving average of the latencies reported with Lease.Observe, or zero before the first one.
func (*Backend[T]) MaxInFlight ¶
MaxInFlight returns the backend's cap on leases in flight, or zero if it has none.
type Balancer ¶
type Balancer interface {
// Build returns a Picker for candidates, which is never empty. The
// Picker may keep the slice; the caller does not modify it afterwards.
Build(candidates []Candidate) Picker
}
A Balancer builds Pickers for sets of candidates. A Pool builds one Picker per tier (Target.Priority) and calls Build for a tier when its candidates change: when members join, leave or change weight, go down or come back, are ejected or restored, and at the steps of a slow start; a tier whose candidates did not change keeps its Picker. Calls never overlap for one pool and never happen while the pool holds a lock; picks use the previous Picker until Build returns. If Build or Rebuild panics, the pool recovers, keeps the previous Picker, reports an EventBalancerPanic and calls the Balancer again on its next change. A Balancer that can update a Picker more cheaply than it builds one also implements Rebuilder.
Balancers can be used without a Pool: build a Picker from candidates and call Pick.
func ConsistentHash ¶
func ConsistentHash(cfg HashConfig) Balancer
ConsistentHash returns a Balancer that maps keys to candidates on a hash ring.
Each candidate owns ring points derived from its ID, in number proportional to its weight, and a key belongs to the first point at or after the key's hash. Adding or removing a candidate therefore moves only the keys of the ring segments it gains or loses, about 1/n of all keys, and every process that uses the same IDs maps a key to the same candidate. Retries walk the ring to the next distinct candidates, which are the candidates the key would move to if the earlier ones left.
Pick hashes the key it is given again, so any 64-bit key works. Picks do not allocate, except retries (attempts above 0) on rings of more than 256 candidates, which need a scratch bitset. The Balancer is a Rebuilder: when members come and go, a Pool updates the ring instead of building it again.
Example ¶
package main
import (
"fmt"
"github.com/djylb/upstream"
)
func main() {
ring := upstream.ConsistentHash(upstream.HashConfig{}).Build([]upstream.Candidate{
{ID: "cache-1"}, {ID: "cache-2"}, {ID: "cache-3"},
})
key := uint64(12345)
primary := ring.Pick(key, 0)
backup := ring.Pick(key, 1) // where the key goes if the primary leaves
fmt.Println(primary != backup)
}
Output: true
func LeastConn ¶
func LeastConn() Balancer
LeastConn returns a Balancer that picks the candidate with the fewest requests in flight relative to its weight, breaking ties at random. Each pick scans every candidate. A retry (attempt above 0) picks at random so that a candidate that cannot take the request is not chosen again.
func ParseBalancer ¶
ParseBalancer returns the built-in Balancer with the given name, as its String method writes it: "round-robin", "weighted-round-robin", "random", "power-of-two", "least-conn" or "consistent-hash" (with the default HashConfig). It suits configuration files; build a ConsistentHash with options in code.
Example ¶
package main
import (
"fmt"
"github.com/djylb/upstream"
)
func main() {
// Balancers and fallbacks by name, as a configuration file gives them.
b, err := upstream.ParseBalancer("least-conn")
if err != nil {
fmt.Println(err)
return
}
f, _ := upstream.ParseFallback("all")
pool := upstream.New[string](upstream.Config{Balancer: b, Fallback: f})
defer pool.Close()
fmt.Println(b, f)
}
Output: least-conn all
func PowerOfTwo ¶
func PowerOfTwo() Balancer
PowerOfTwo returns a Balancer that samples two distinct candidates at random and picks the one with fewer requests in flight relative to its weight. It approaches least-connections balancing at constant cost and avoids the herding that strict least-connections shows when many clients see the same idle backend.
func Random ¶
func Random() Balancer
Random returns a Balancer that picks candidates at random in proportion to their weights, using the alias method so each pick takes constant time.
func RoundRobin ¶
func RoundRobin() Balancer
RoundRobin returns a Balancer that cycles through the candidates in order and ignores weights. Each Picker starts at a random position so that many pools do not hit the same backend in step.
func WeightedRoundRobin ¶
func WeightedRoundRobin() Balancer
WeightedRoundRobin returns a Balancer that implements smooth weighted round-robin: over each cycle every candidate is picked in proportion to its weight, and its picks are spread through the cycle instead of bunched together. For weights 5, 1, 1 a cycle is a a b a c a a.
The cycle is precomputed, which makes Pick a single atomic add, when it is short enough: the sum of the weights divided by their greatest common divisor, times the number of candidates, at most 1<<22. Longer cycles, which take extreme weight ratios, fall back to weighted random picks: proportions then hold on average rather than exactly in every cycle, and picks stay lock-free.
type BalancerPanicError ¶
type BalancerPanicError struct {
// Value is the value passed to panic.
Value any
// Stack is the stack of the panicking goroutine, as runtime/debug.Stack
// formats it.
Stack []byte
}
A BalancerPanicError is the Err of an EventBalancerPanic: what a Balancer's Build or Rebuild panicked with, and where.
func (*BalancerPanicError) Error ¶
func (e *BalancerPanicError) Error() string
func (*BalancerPanicError) Unwrap ¶
func (e *BalancerPanicError) Unwrap() error
Unwrap returns Value if it is an error, such as a runtime.Error.
type Candidate ¶
type Candidate struct {
// InFlight counts the backend's requests in flight. Load-aware
// balancers read it; nil means no load information, read as zero.
InFlight *atomic.Int64
// Latency is the backend's moving average latency in nanoseconds, as
// reported with Lease.Observe; zero means no sample yet, and nil no
// latency information. Latency-aware balancers read it at pick time.
Latency *atomic.Int64
// Meta is whatever the caller attaches to the candidate. A Pool sets
// it to the *Backend[T], so that a balancer can route by the
// backend's value, such as its zone or labels, or by its Status.
Meta any
// ID identifies the backend. Consistent hashing places backends by ID,
// so equal IDs map keys to the same place in every process.
ID string
// Weight is the backend's relative capacity. Values below 1 mean 1.
Weight int
}
A Candidate is a balancer's view of one backend.
Example ¶
// Balancers work without a pool.
var loadA, loadB atomic.Int64
loadA.Store(5)
p := LeastConn().Build([]Candidate{
{ID: "a", Weight: 1, InFlight: &loadA},
{ID: "b", Weight: 1, InFlight: &loadB},
})
fmt.Println(p.Pick(0, 0))
Output: 1
type CheckResult ¶
type CheckResult struct {
// Time is when the check started.
Time time.Time
// Err is why the check failed, or nil.
Err error
// Addr is the address that answered, if the check reported it, such as
// the IP address a host name resolved to.
Addr string
// Latency is the time the target took to answer.
Latency time.Duration
}
CheckResult is the result of one active health check. Package health embeds it in its own results, so a pool keeps everything a check saw except the checker's thresholds.
type Clock ¶
type Clock interface {
// Now returns the current time.
Now() time.Time
// AfterFunc arranges for f to run in its own goroutine once d has
// elapsed, and returns a function that cancels the call.
AfterFunc(d time.Duration, f func()) (stop func() bool)
// WithTimeout returns a copy of ctx that ends d from now on this Clock.
WithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc)
}
A Clock tells the time and schedules callbacks. The pool, the outlier detector and the health checker read time only through a Clock, so tests and simulations can substitute a fake one.
Implementations must be safe for concurrent use. AfterFunc must call f in its own goroutine, as time.AfterFunc does; f may start before AfterFunc returns, for example when d is not positive, and callers in this module allow for that. The returned stop function reports whether it prevented f from running, like time.Timer.Stop.
WithTimeout behaves like context.WithTimeout on the Clock's time: the returned context's Deadline reports Now()+d (or the parent's deadline if that is sooner), and once the Clock reaches it the context is canceled with Err and Cause both context.DeadlineExceeded. Every timeout in this module comes from it, so probes and transports see a real deadline.
type Config ¶
type Config struct {
// Balancer chooses backends. Nil means WeightedRoundRobin.
Balancer Balancer
// Clock is the time source. Nil means SystemClock.
Clock Clock
// Equal reports whether two target values mean the same backend, so
// that Sync and Put keep a member whose weight and value did not
// change: same Backend, no EventUpdated, no rebuild. Nil compares
// values with ==, through reflection, when they are comparable, and
// treats other values, such as slices, as always changed.
Equal func(a, b any) bool
// Outlier configures passive health checking, which is disabled by
// default.
Outlier OutlierConfig
// Retry configures how Do, DoKey and Request retry on other
// backends. There are no retries by default.
Retry RetryConfig
// SlowStart, if positive, ramps up the traffic of a backend that joins
// the pool or becomes healthy again: its weight grows linearly from a
// small fraction to its full value over SlowStart, so a cold backend is
// not flooded. The pool rebuilds its view 16 times over the ramp. With
// ConsistentHash the ramp moves keys to the backend gradually. Slow
// start works through weights within a tier, so it has no effect with
// RoundRobin, which ignores weights, on a backend alone in its tier, or
// between tiers.
SlowStart time.Duration
// PanicThreshold, from 1 to 100, makes a pool with FallbackAll panic
// while fewer than this percentage of its members that are not
// draining are eligible, instead of only when none is: a few survivors
// would otherwise take the whole load and fail in turn. Zero panics
// only when no member is eligible.
PanicThreshold int
// Fallback says what to do when too few backends are eligible.
Fallback Fallback
// Overflow lets a pick move to the next tier when every member of its
// tier that it may use is at its Target.MaxInFlight. Without Overflow
// such a pick fails with ErrSaturated, so that a lower tier, such as
// a costly backup, takes traffic only when the tier above is down, not
// when it is busy. Members that are down, ejected, out of half-open
// trial slots or already tried by the request always pass the pick on.
Overflow bool
}
Config configures a Pool. The zero value is a pool with smooth weighted round-robin, no passive health checking, no slow start, no retries and FallbackNone. Pool.SetConfig changes everything but the Clock while the pool runs.
type Drain ¶
type Drain struct {
// contains filtered or unexported fields
}
A Drain follows a backend that takes no new picks until its leases in flight are released.
func (*Drain) Canceled ¶
Canceled reports whether Resume ended the drain before the backend's leases were all released. It is meaningful once Done is closed, and false before.
type Event ¶
type Event[T any] struct { // Err is the cause of an EventStateChanged to StateUnhealthy (the last // check error) or to StateEjected or StateHalfOpen (the ejection // cause), the *BalancerPanicError of an EventBalancerPanic, and nil // otherwise. Err error // Backend is the member concerned; nil for EventBalancerPanic. Backend *Backend[T] // Kind says what happened. Kind EventKind // From and To are the backend's states before and after the event. From, To State }
An Event reports a change in a Pool. State changes are edge-triggered: a backend that keeps failing produces one event when it goes down, not one per failure.
type EventKind ¶
type EventKind uint8
EventKind says what an Event reports.
const ( // EventAdded reports a new member. EventAdded EventKind = iota // EventUpdated reports a member replaced with a new weight or value; // Backend is the new version. Updates that change neither emit none. EventUpdated // EventRemoved reports a member that left the pool. EventRemoved // EventStateChanged reports a change of a member's State. EventStateChanged // EventBalancerPanic reports that the Balancer panicked while the pool // built its view. Err is the *BalancerPanicError; Backend is nil. The // pool keeps routing with the view it published before, and builds // again on its next change. EventBalancerPanic )
func (EventKind) MarshalText ¶
MarshalText encodes the kind as its name.
func (*EventKind) UnmarshalText ¶
UnmarshalText decodes a kind from its name.
type Fallback ¶
type Fallback uint8
Fallback says what a Pool does when too few backends are eligible.
const ( // FallbackNone makes Pick fail with ErrUnavailable. FallbackNone Fallback = iota // FallbackAll enters panic mode: Pick chooses among all members that // are not draining, ignoring active and passive health. A health signal // that marks every backend down is more often wrong than right, and // degraded service beats none. The pool panics when no member is // eligible, when fewer than Config.PanicThreshold percent of the // members that are not draining are eligible, and, for a single pick, // when every eligible member refuses it, as half-open members out of // trial slots do. Leases picked in panic mode still report outcomes. FallbackAll )
func ParseFallback ¶
ParseFallback returns the Fallback named s: "none" or "all".
func (Fallback) MarshalText ¶
MarshalText encodes the fallback as its name.
func (*Fallback) UnmarshalText ¶
UnmarshalText decodes a fallback from its name, as ParseFallback does.
type HashConfig ¶
type HashConfig struct {
// VirtualNodes is the average number of ring points per candidate. More
// points spread keys more evenly at the cost of memory and build time.
// The default is 160.
VirtualNodes int
// LoadFactor enables bounded loads when above zero: a candidate that
// already carries LoadFactor times its weighted share of the requests
// in flight is passed over for the next one on the ring. Values below
// 1 mean 1. Bounded loads keep a hot key from overloading one backend
// but cost a scan of the in-flight counts on every pick.
LoadFactor float64
}
HashConfig configures ConsistentHash.
type Health ¶
type Health uint8
Health is the verdict of active health checking on a backend.
func (Health) MarshalText ¶
MarshalText encodes the verdict as its name.
func (*Health) UnmarshalText ¶
UnmarshalText decodes a verdict from its name.
type Lease ¶
type Lease[T any] struct { // contains filtered or unexported fields }
A Lease is a backend picked for one request or connection. It holds one of the backend's in-flight slots until Release returns it with the request's outcome.
Every lease returned without error must be released exactly once, or the backend's in-flight count stays raised and a drain never completes; Pool.Do, Pool.DoKey and Request.Do do this for the caller. Release zeroes the lease, so releasing the same variable again does nothing. Copies of a lease share one slot: release only one of them. Making every copy idempotent would need state shared by the copies, one allocation per pick, so the pool instead detects what it can without cost: releasing a copy after another copy has been released panics if the backend has no slot left to return, like a negative sync.WaitGroup counter; if other leases are in flight, the extra release is not detected and frees one of their slots.
func (*Lease[T]) Observe ¶
Observe records a latency sample of the backend, such as the time to a response's first byte or to an established connection, for latency-aware balancers: it feeds the moving average that Candidate.Latency, Backend.Latency and Status.Latency report. Observe may be called any number of times before Release, and does nothing on a released or zero Lease.
func (*Lease[T]) Release ¶
Release returns the lease's slot and records the outcome for passive health checking, unless Report already recorded one. It does nothing on a released or zero Lease. If the outcome ejects or restores the backend, Release also rebuilds the pool's view and may deliver events to subscribers on the calling goroutine; the slot is returned first, so a panicking subscriber cannot leak it.
func (*Lease[T]) Report ¶
Report records the outcome for passive health checking now, while the lease keeps its in-flight slot. Use it when the outcome is known long before the lease ends, as for a connection that was established and then stays open: a half-open backend's trial ends with the report instead of with the connection. Only the first Report counts; Release then returns the slot and ignores its outcome. Report does nothing on a released or zero Lease.
Example ¶
package main
import (
"fmt"
"github.com/djylb/upstream"
)
func main() {
pool := upstream.New[string](upstream.Config{Balancer: upstream.LeastConn()})
defer pool.Close()
_ = pool.Sync([]upstream.Target[string]{{ID: "relay", Value: "10.0.0.1:9000"}})
lease, _ := pool.Pick()
// The connection is up: report the outcome now and keep the in-flight
// slot until the connection closes.
lease.Report(upstream.Success)
s := pool.Status()[0]
fmt.Println(s.InFlight, s.Successes)
lease.Release(upstream.Neutral) // the reported outcome stands
s = pool.Status()[0]
fmt.Println(s.InFlight, s.Successes, s.Neutrals)
}
Output: 1 1 0 1 0
type Outcome ¶
type Outcome uint8
Outcome classifies a finished request or connection for passive health checking.
const ( // Success means the backend served the request. It resets the // consecutive-failure count. Success Outcome = iota // Failure means the backend failed the request: it refused the // connection, timed out or answered with an error the caller blames on // the backend. Failure // Neutral means the result says nothing about the backend, for example // because the client went away. It counts towards neither successes nor // failures. Neutral )
func OutcomeOf ¶
OutcomeOf maps an error to an Outcome: nil is Success, an error from WithOutcome has the outcome given there, an error wrapping context.Canceled is Neutral, and any other error is Failure. context.DeadlineExceeded is a Failure because a backend that does not answer in time is the usual cause.
func (Outcome) MarshalText ¶
MarshalText encodes the outcome as its name.
func (*Outcome) UnmarshalText ¶
UnmarshalText decodes an outcome from its name.
type OutlierConfig ¶
type OutlierConfig struct {
// Window is the span over which FailurePercent is measured. The
// default is 10s. The rate is a sliding estimate over the current and
// the previous window.
Window time.Duration
// BaseEjection is the first ejection time. The default is 30s.
BaseEjection time.Duration
// MaxEjection caps the ejection time. The default is 5m; a value below
// BaseEjection is raised to it.
MaxEjection time.Duration
// ConsecutiveFailures ejects the backend after this many failures in a
// row. Zero disables the rule.
ConsecutiveFailures int
// FailurePercent ejects the backend when at least this percentage of
// the requests in Window failed, once MinRequests requests have been
// seen. Zero disables the rule; values above 100 mean 100.
FailurePercent int
// MinRequests is the request volume the failure-rate rule needs before
// it judges the backend. The default is 20.
MinRequests int
// HalfOpenRequests is the number of trial requests a half-open backend
// admits at the same time. The default is 1.
HalfOpenRequests int
// HalfOpenSuccesses is the number of successful trials that restore a
// half-open backend. The default is 1.
HalfOpenSuccesses int
// MaxEjectionPercent, from 1 to 100, caps the share of a pool's members
// that passive health checking may hold ejected or half-open at once,
// so that a fault shared by every backend, such as a database they all
// depend on, cannot eject the whole pool. One member may always be
// ejected. A failure that would eject a member beyond the cap leaves it
// in rotation and is counted in OutlierStatus.Refused. Zero means no
// cap. It applies to the detectors of a Pool, not to an
// OutlierDetector on its own.
MaxEjectionPercent int
}
OutlierConfig configures passive health checking, which judges a backend by the outcomes of real traffic. The zero value disables it; set ConsecutiveFailures, FailurePercent or both to enable it. Zero durations and counts take the defaults documented on each field.
An ejected backend receives no traffic for an ejection time that starts at BaseEjection and doubles with every ejection that is not followed by a recovery, up to MaxEjection. When the time is up the backend becomes half-open: it admits at most HalfOpenRequests concurrent trial requests. HalfOpenSuccesses successful trials restore it and reset the ejection time; one failed trial ejects it again.
func (OutlierConfig) Enabled ¶
func (c OutlierConfig) Enabled() bool
Enabled reports whether the configuration enables passive health checking.
type OutlierDetector ¶
type OutlierDetector struct {
// contains filtered or unexported fields
}
An OutlierDetector tracks the passive health of one backend. Callers ask it for admission before each request and report each request's outcome afterwards. It is safe for concurrent use. Admit and Record are lock-free apart from state transitions, which are rare.
The detector owns one timer, which ends an ejection. Close stops it.
Example ¶
package main
import (
"errors"
"fmt"
"net"
"github.com/djylb/upstream"
)
func main() {
// Passive health checking for a single connection target, without a
// pool.
d := upstream.NewOutlierDetector(upstream.OutlierConfig{ConsecutiveFailures: 2}, nil, nil)
defer d.Close()
dial := func() error {
t, ok := d.Admit()
if !ok {
return errors.New("backend ejected")
}
_, err := net.Dial("tcp", "invalid address")
d.Record(t, upstream.OutcomeOf(err))
return err
}
for range 3 {
_ = dial()
}
fmt.Println(d.State(), d.Status().Ejections)
}
Output: ejected 1
func NewOutlierDetector ¶
func NewOutlierDetector(cfg OutlierConfig, clock Clock, onChange func()) *OutlierDetector
NewOutlierDetector returns a detector for one backend that reads time from clock; nil means SystemClock. onChange, if not nil, is called after every state transition, without any lock held, from the goroutine that caused the transition: one calling Record, Reset or Close, or the detector's timer goroutine. Calls may overlap, so onChange should read State instead of assuming an order.
A detector whose configuration is not Enabled admits everything and never ejects.
func (*OutlierDetector) Admit ¶
func (d *OutlierDetector) Admit() (Ticket, bool)
Admit reports whether the backend may take a new request now. A healthy backend always admits; an ejected one never does; a half-open one admits while fewer than HalfOpenRequests trials are in flight. When Admit returns true the caller must pass the Ticket to Record exactly once.
func (*OutlierDetector) Close ¶
func (d *OutlierDetector) Close()
Close stops the detector's timer. The detector keeps its current state and still answers Admit and Record, but it no longer changes state. Close is idempotent.
func (*OutlierDetector) Record ¶
func (d *OutlierDetector) Record(t Ticket, o Outcome)
Record reports the outcome of a request admitted with t.
func (*OutlierDetector) Reset ¶
func (d *OutlierDetector) Reset()
Reset clears any ejection and the ejection backoff, as if the backend had just recovered. It is meant for operators overriding the detector.
func (*OutlierDetector) SetConfig ¶
func (d *OutlierDetector) SetConfig(cfg OutlierConfig)
SetConfig replaces the detector's configuration. The current state and any ejection in progress stay; the new thresholds and times apply from the next request and the next ejection. A configuration that is not Enabled also clears any ejection, as Reset does.
func (*OutlierDetector) State ¶
func (d *OutlierDetector) State() OutlierState
State returns the current passive health state.
func (*OutlierDetector) Status ¶
func (d *OutlierDetector) Status() OutlierStatus
Status returns a snapshot of the detector.
type OutlierState ¶
type OutlierState uint32
OutlierState is the passive health state of a backend.
const ( // OutlierOK means the backend is not ejected. OutlierOK OutlierState = iota // OutlierEjected means the backend is ejected and admits no traffic // until its ejection time is up. OutlierEjected // OutlierHalfOpen means the ejection time is up and the backend admits // a limited number of trial requests. OutlierHalfOpen )
func (OutlierState) MarshalText ¶
func (s OutlierState) MarshalText() ([]byte, error)
MarshalText encodes the state as its name.
func (OutlierState) String ¶
func (s OutlierState) String() string
String returns the name of the state.
func (*OutlierState) UnmarshalText ¶
func (s *OutlierState) UnmarshalText(b []byte) (err error)
UnmarshalText decodes a state from its name.
type OutlierStatus ¶
type OutlierStatus struct {
// EjectedUntil is when the current ejection ends; it is zero unless
// State is OutlierEjected.
EjectedUntil time.Time
// Cause is why the backend was last ejected; it is nil while State is
// OutlierOK.
Cause error
// Ejections counts every ejection since the detector was created.
Ejections uint64
// Refused counts the failures that would have ejected the backend but
// that OutlierConfig.MaxEjectionPercent refused.
Refused uint64
// ConsecutiveFailures is the current run of failures.
ConsecutiveFailures uint32
// State is the passive health state.
State OutlierState
}
OutlierStatus is a snapshot of an OutlierDetector.
type Picker ¶
type Picker interface {
// Pick returns the index of the chosen candidate. key is the request's
// hash for key-affine balancers, or a random number when the caller
// has no key. attempt is 0 for the first choice; when the caller
// cannot use the chosen backend it asks again with attempt 1, 2 and so
// on, and a Picker that is deterministic for a key should then return
// other candidates.
Pick(key uint64, attempt int) int
}
A Picker chooses among the candidates it was built from. It must be safe for concurrent use and should not allocate.
type Pool ¶
type Pool[T any] struct { // contains filtered or unexported fields }
A Pool is a set of backends with a balancer, health state and accounting. Pick and Release are lock-free and do not allocate; updates to membership and health build a new immutable routing view and publish it atomically, so picks in progress are never disturbed.
A backend is eligible for picks when it is not draining, not reported down by active checks and not ejected by passive checks; a half-open backend is eligible for its trial requests. When too few backends are eligible, Config.Fallback decides.
Rebuilds are coalesced: an update that comes while another goroutine builds the view leaves the next build to that goroutine and returns. A method that changes the pool has published its change when it returns, unless such a build is in progress, which then publishes it. Removals, drains and health verdicts take effect for picks at once either way, because a pick checks the backend's state itself; only new members and changed weights wait for the view.
The pool starts no goroutines. It owns the passive health timers and the slow-start timer, which Close stops. Pick never locks; Release locks only when its outcome ejects or restores a backend, to rebuild the view.
Locking: mu guards membership and settings and is never held while calling a Balancer, a Clock or an event subscriber. Inside mu the pool may take qmu (the event queue) and an OutlierDetector's mutex to read its status; neither calls out while held. rampMu (the slow-start timer) is taken alone.
func (*Pool[T]) Available ¶
Available returns the number of members that are eligible for new picks, as the routing view sees them: not draining, not offline, not down and not ejected, in every tier. A half-open member counts although it takes only its trial requests, and a full one although it takes no pick until a lease is released. Available is zero while the pool is empty or closed and in panic mode, when Pick, under FallbackAll, still picks among members that are not eligible; a readiness check that should fail in panic mode tests Available() > 0.
func (*Pool[T]) Clock ¶
Clock returns the pool's time source, for packages that measure the latencies they observe on its leases.
func (*Pool[T]) Close ¶
func (p *Pool[T]) Close()
Close stops the passive health and slow-start timers and ends all subscriptions: no subscriber call starts once Close has begun, and one in progress may finish. Close does not wait: a timer that has already fired may still finish its own bookkeeping, but runs no Balancer, Clock or subscriber code, and a rebuild already in progress publishes nothing. Close may be called from anywhere, including a subscriber or a Balancer. Picks fail with ErrClosed once Close returns and updates are refused; leases already issued may still be released. Close is idempotent.
func (*Pool[T]) Do ¶
Do picks a backend, calls fn with it and releases the lease with OutcomeOf of fn's error, also when fn panics, in which case the outcome is Failure and the panic continues. When the outcome is Failure and ctx is not done, Do retries on another backend as Config.Retry allows. It returns nil once fn succeeds, fn's last error if fn failed, and otherwise ctx's or the first pick's error.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/djylb/upstream"
)
func main() {
pool := upstream.New[string](upstream.Config{
Retry: upstream.RetryConfig{Attempts: 3, Budget: 0.2},
})
defer pool.Close()
_ = pool.Sync([]upstream.Target[string]{
{ID: "primary", Value: "10.0.0.1:8080"},
{ID: "backup", Value: "10.0.0.2:8080", Priority: 1},
})
// Do releases each lease with the outcome of the function's error and
// retries a failure on a backend it has not tried.
err := pool.Do(context.Background(), func(b *upstream.Backend[string]) error {
if b.ID() == "primary" {
return errors.New("connection refused")
}
fmt.Println("served by", b.ID())
return nil
})
fmt.Println(err)
fmt.Println("primary failures:", pool.Status()[0].Failures)
}
Output: served by backup <nil> primary failures: 1
func (*Pool[T]) Done ¶
func (p *Pool[T]) Done() <-chan struct{}
Done returns a channel that is closed when Close is called. The pool starts no goroutines but the short-lived one that finishes a rebuild a committer handed off, which builds nothing once Close has begun, so nothing of it starts user code after that, and helpers attached to the pool, such as a health checker started with health.Watch, stop with it.
func (*Pool[T]) Drain ¶
Drain stops new picks of the backend with the given ID while it stays a member, for maintenance or before removal. The returned Drain is done when the backend has no leases in flight, or canceled when Resume ends the drain first. Draining a backend that is already draining returns the same Drain. Drain reports false if there is no such member.
func (*Pool[T]) MarkOffline ¶
MarkOffline takes b out of rotation on the caller's word, for example because the session the backend is reached through dropped, until MarkOnline. It is one of three inputs that each veto picks: an offline backend, one that active checks report down and one that passive checks ejected take no new picks, and MarkOnline lifts only its own veto. While several apply, the backend's State is the first in the order StateDraining, StateOffline, StateUnhealthy, StateEjected, StateHalfOpen. Unlike the health verdicts, the mark also holds in panic mode: FallbackAll never picks an offline backend, which also does not count towards Config.PanicThreshold.
Taking b offline emits EventStateChanged with cause as its Err, or ErrOffline if cause is nil, and rebuilds the view like any other change; marking an offline backend again only replaces the cause, which the next event reports. The mark follows the member's ID across updates. A call for a backend that has been removed, or replaced by a new member with the same ID, is ignored; MarkOffline reports whether b was a current member of an open pool.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/djylb/upstream"
)
func main() {
pool := upstream.New[string](upstream.Config{})
defer pool.Close()
_ = pool.Sync([]upstream.Target[string]{{ID: "edge-1", Value: "edge-1"}})
defer pool.Subscribe(func(ev upstream.Event[string]) {
if ev.Kind == upstream.EventStateChanged {
fmt.Println(ev.Backend.ID(), ev.From, "->", ev.To, ev.Err)
}
})()
// The session the backend is reached through dropped, and came back.
b, _ := pool.Backend("edge-1")
pool.MarkOffline(b, errors.New("session closed"))
_, err := pool.Pick()
fmt.Println(err)
pool.MarkOnline(b)
}
Output: edge-1 healthy -> offline session closed upstream: no backend available edge-1 offline -> healthy <nil>
func (*Pool[T]) MarkOnline ¶
MarkOnline lifts the mark of MarkOffline; a backend that comes back online ramps up under Config.SlowStart. Like MarkOffline, it ignores backends that are no longer members, and reports whether b was a current member of an open pool.
func (*Pool[T]) Pick ¶
Pick leases a backend chosen by the balancer without a key. Key-affine balancers pick at random.
func (*Pool[T]) PickKey ¶
PickKey leases a backend chosen for key, such as a client address or a session ID. With ConsistentHash the same key keeps mapping to the same backend while it is eligible; other balancers ignore the key. An empty key carries no affinity: PickKey("") is Pick.
Example ¶
package main
import (
"fmt"
"github.com/djylb/upstream"
)
func main() {
pool := upstream.New[string](upstream.Config{Balancer: upstream.ConsistentHash(upstream.HashConfig{})})
defer pool.Close()
_ = pool.Sync([]upstream.Target[string]{{ID: "a"}, {ID: "b"}, {ID: "c"}})
first, _ := pool.PickKey("198.51.100.7")
again, _ := pool.PickKey("198.51.100.7")
fmt.Println("sticky:", first.Backend() == again.Backend())
first.Release(upstream.Success)
again.Release(upstream.Success)
}
Output: sticky: true
func (*Pool[T]) Put ¶
Put adds a target, or replaces the member with the same ID, keeping its position, health state and counters. It emits EventAdded or EventUpdated; a Put that changes none of the weight, priority, cap and value of a member does nothing.
func (*Pool[T]) Remove ¶
Remove removes the backend with the given ID gracefully: new picks avoid it at once, and leases already issued finish normally. The returned Drain is done when the last of them is released. Remove reports false if there is no such member.
Example ¶
package main
import (
"fmt"
"github.com/djylb/upstream"
)
func main() {
pool := upstream.New[string](upstream.Config{})
defer pool.Close()
_ = pool.Sync([]upstream.Target[string]{{ID: "old"}, {ID: "new"}})
lease, _ := pool.PickKey("")
for lease.Backend().ID() != "old" {
lease.Release(upstream.Neutral)
lease, _ = pool.PickKey("")
}
drained, _ := pool.Remove("old")
select {
case <-drained.Done():
default:
fmt.Println("waiting for the last lease")
}
lease.Release(upstream.Success)
<-drained.Done()
fmt.Println("drained")
}
Output: waiting for the last lease drained
func (*Pool[T]) ReportCheck ¶
func (p *Pool[T]) ReportCheck(b *Backend[T], h Health, r CheckResult) bool
ReportCheck records the result of an active health check of b and sets its health. A health checker calls it after every check; the checker, not the pool, applies rise and fall thresholds. HealthDown takes the backend out of rotation; HealthUp and HealthUnknown keep it in, so a change between them only updates the health and rebuilds nothing. Reports for a backend that has been removed, or replaced by a new member with the same ID, are ignored; ReportCheck reports whether b was a current member of an open pool.
func (*Pool[T]) Request ¶
Request returns the picker for one request routed by key; an empty key carries no affinity, as with PickKey.
func (*Pool[T]) ResetOutlier ¶
ResetOutlier clears the passive health ejection of the backend with the given ID, if any. It reports false if there is no such member.
func (*Pool[T]) Resume ¶
Resume ends a drain started by Drain; the Drain reports Canceled unless it had already completed. Resume reports false if there is no such member.
func (*Pool[T]) SetConfig ¶
SetConfig replaces the pool's configuration, except Clock, keeping its members with their counters, health and leases. A new Balancer builds a new view from scratch; the same Balancer, such as the same pointer or an equal ConsistentHash value, goes on rebuilding its Pickers; new outlier settings apply to the next request of every member, and disabling passive health checking clears all ejections; new retry settings apply to the next Request.Next. It returns ErrClosed if the pool is closed.
func (*Pool[T]) Subscribe ¶
Subscribe calls fn for every later event, starting with an EventAdded for each current member, and returns a function that ends the subscription.
Events are delivered one at a time in the order they happened, never while the pool holds a lock, on the goroutine that caused them or on one already delivering events. fn may call any Pool method; events that call causes are delivered after fn returns. fn should be quick, because the goroutine delivering events may be one releasing a lease. An event may be delivered slightly before picks observe the change it reports. After cancel returns, fn may still be running for an event already in progress but is not called again.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/djylb/upstream"
)
func main() {
pool := upstream.New[string](upstream.Config{})
defer pool.Close()
_ = pool.Sync([]upstream.Target[string]{{ID: "a"}})
cancel := pool.Subscribe(func(ev upstream.Event[string]) {
fmt.Println(ev.Kind, ev.Backend.ID(), ev.From, "->", ev.To, ev.Err)
})
defer cancel()
a, _ := pool.Backend("a")
refused := errors.New("refused")
// Repeated failures report one transition.
pool.ReportCheck(a, upstream.HealthDown, upstream.CheckResult{Err: refused})
pool.ReportCheck(a, upstream.HealthDown, upstream.CheckResult{Err: refused})
pool.ReportCheck(a, upstream.HealthUp, upstream.CheckResult{})
}
Output: added a healthy -> healthy <nil> state-changed a healthy -> unhealthy refused state-changed a unhealthy -> healthy <nil>
func (*Pool[T]) Sync ¶
Sync replaces the pool's membership with targets, in order. Members whose ID is in targets keep their health state and counters and take the new weight and value; new IDs join as healthy; members missing from targets are removed gracefully: they take no new picks while their leases finish. Sync emits EventRemoved, then EventAdded or EventUpdated for each target that is new or changed, in order. A Sync that changes nothing, order included, does nothing. On error nothing changes.
type Rebuilder ¶
type Rebuilder interface {
Balancer
// Rebuild returns a Picker for candidates, which is never empty. prev
// was returned by Build or Rebuild of this Balancer and may still be
// in use by picks: Rebuild must not modify it.
Rebuild(prev Picker, candidates []Candidate) Picker
}
A Rebuilder is a Balancer that derives a Picker from the previous one, for balancers whose Build is expensive, such as hash rings. A Pool calls Rebuild instead of Build when it has a Picker that the same Balancer built. The result must be what Build would return for candidates; Rebuild only saves work, such as by keeping the parts of prev whose candidates did not change.
type Request ¶
type Request[T any] struct { // contains filtered or unexported fields }
A Request picks distinct backends for one request: the first Next is a pick like PickKey, and every later Next is a retry that leases a backend this Request has not returned before, as the pool's RetryConfig allows. It reads the pool's current routing view and retry settings on every call, so a retry avoids backends that went down meanwhile.
Do runs the whole retry loop; Pool.Do is Do with a function of the backend. Use Next directly for loops that Do cannot express. A Request is for one goroutine. It does not allocate for up to four attempts.
Example ¶
package main
import (
"fmt"
"github.com/djylb/upstream"
)
func main() {
pool := upstream.New[string](upstream.Config{Retry: upstream.RetryConfig{Attempts: 2}})
defer pool.Close()
_ = pool.Sync([]upstream.Target[string]{{ID: "a"}, {ID: "b"}})
// A Request leases distinct backends for one request: each Next
// leases a backend not tried before, as the retry settings allow.
req := pool.Request("client-key")
first, _ := req.Next()
first.Release(upstream.Failure)
second, _ := req.Next()
fmt.Println(first.Backend() == nil, second.Backend() != nil)
second.Release(upstream.Success)
_, err := req.Next()
fmt.Println(err)
}
Output: true true upstream: retry limit reached
func (*Request[T]) Do ¶
Do calls fn with a lease from Next, again with a new lease for as long as fn asks for a retry, ctx is not done and Next allows one. fn owns its lease: it may release it, or hand it on with Lease.Take, as for a connection that outlives fn. A lease fn leaves unreleased is released when fn returns, with OutcomeOf of fn's error, or with Failure if fn panics, in which case the panic goes on.
Do returns fn's error once fn asks for no retry. When a retry is declined, it returns fn's last error, or, if that was nil, ctx's error or the pick's; so does it when no lease could be picked at all. Do never returns nil unless fn's last call did.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/djylb/upstream"
)
func main() {
pool := upstream.New[string](upstream.Config{Retry: upstream.RetryConfig{Attempts: 2}})
defer pool.Close()
_ = pool.Sync([]upstream.Target[string]{{ID: "a"}, {ID: "b"}})
// Do runs the retry loop for leases that outlive it, such as
// connections: Take hands the lease of the attempt that worked on.
var kept upstream.Lease[string]
req := pool.Request("")
err := req.Do(context.Background(), func(l *upstream.Lease[string]) (retry bool, err error) {
if req.Tried() == 1 {
return true, errors.New("refused") // released as a Failure, then retried
}
kept = l.Take()
return false, nil
})
fmt.Println(err, req.Tried(), kept.Backend() != nil)
kept.Release(upstream.Success)
}
Output: <nil> 2 true
func (*Request[T]) Next ¶
Next leases the next backend. The first call fails like PickHash; a retry fails with ErrRetryLimit after RetryConfig.Attempts leases, with ErrRetryBudget when the budget is spent, with ErrUnavailable when every backend that takes traffic has been tried, and with ErrSaturated when those left are full. Release the previous lease, usually with Failure, before or after calling Next.
type RetryConfig ¶
type RetryConfig struct {
// Window is the span over which the budget counts requests and
// retries. The default is 10s. The counts are a sliding estimate over
// the current and the previous window.
Window time.Duration
// Budget, if positive, is the largest ratio of retries to requests,
// such as 0.2 for one retry per five requests. Zero means no budget:
// only Attempts bounds retries.
Budget float64
// Attempts is the most backends one request tries, the first one
// included. Zero and one mean no retries.
Attempts int
// MinRetries is the number of retries per Window that the budget
// allows whatever the request rate, so that a pool with little
// traffic can still retry. Zero means 10; a negative value means none.
MinRetries int
}
RetryConfig configures retries on other backends, by Pool.Do, Pool.DoKey and Request. The zero value allows no retries.
A retry budget bounds the extra load that retries put on the backends when many requests fail at once: within any Window, retries may number at most MinRetries plus Budget times the requests started.
type State ¶
type State uint8
State is the routing state of a backend, combining its membership, the caller's availability input, its active health and its passive health. When several conditions hold, the state is the first that applies in this order: removed, draining, offline, unhealthy, ejected, half-open, healthy.
const ( // StateHealthy means the backend takes new traffic. StateHealthy State = iota // StateHalfOpen means the backend's ejection is over and it takes a // limited number of trial requests. StateHalfOpen // StateEjected means passive health checking ejected the backend. StateEjected // StateUnhealthy means active health checks report the backend down. StateUnhealthy // StateDraining means the backend takes no new traffic but its leases // may still be in flight. StateDraining // StateRemoved means the backend is no longer a member of its pool. StateRemoved // StateOffline means the caller marked the backend unavailable with // Pool.MarkOffline, for example because the session it is reached // through dropped. StateOffline )
func (State) MarshalText ¶
MarshalText encodes the state as its name.
func (*State) UnmarshalText ¶
UnmarshalText decodes a state from its name.
type Status ¶
type Status struct {
// LastCheck is the latest active health check result; its Time is zero
// before the first check.
LastCheck CheckResult
// Outlier is the passive health snapshot. Its State is always
// OutlierOK when passive health checking is disabled.
Outlier OutlierStatus
// ID identifies the backend.
ID string
// Weight is the backend's weight.
Weight int
// Priority is the backend's tier: lower values are preferred.
Priority int
// MaxInFlight is the backend's cap on leases in flight; zero means
// none.
MaxInFlight int
// InFlight is the number of leases not yet released.
InFlight int64
// Latency is the moving average of the latencies reported with
// Lease.Observe, or zero before the first one.
Latency time.Duration
// Successes, Failures and Neutrals count the outcomes of leases, as
// reported by Lease.Report or Lease.Release. Together with InFlight
// they count every pick: each lease is in flight until released and
// has one outcome.
Successes uint64
Failures uint64
Neutrals uint64
// State is the routing state.
State State
// Health is the latest active health verdict, whatever the State.
Health Health
// Draining reports whether the backend is draining or removed.
Draining bool
// Offline reports whether Pool.MarkOffline marked the backend
// unavailable, whatever the State.
Offline bool
}
Status is a snapshot of one backend.
type Target ¶
type Target[T any] struct { // ID identifies the backend. It must be unique within the pool. Health // state and counters follow the ID across updates, so give a backend a // new ID when its value stops meaning the same server. ID string // Weight is the backend's relative capacity within its tier, from 0 to // MaxWeight. Zero means 1; to stop sending traffic to a backend, drain // it. Weight int // Priority is the backend's tier. Picks go to the tier with the lowest // Priority that has an eligible member, balanced among that tier's // members, and move to the next tier only when no member of the tier // can take the pick; see Config.Overflow for tiers that are full. // Members of different priorities make an active-backup setup. The // default, 0, puts every member in one tier. Priority int // MaxInFlight, if positive, caps the backend's leases in flight: a // backend at its cap refuses picks, in panic mode too, until a lease // is released. Zero means no cap. The in-flight count may read above // the cap for a moment while a pick that lost a race for the last slot // backs out; the leases never exceed it. MaxInFlight int // Value is what callers connect to, such as an address. Value T }
A Target describes a backend to a Pool.
Example (Priority) ¶
package main
import (
"fmt"
"github.com/djylb/upstream"
)
func main() {
// Two primaries share the load; the backup takes traffic when both are
// down, and, with Overflow, when both are at their MaxInFlight.
pool := upstream.New[string](upstream.Config{Overflow: true})
defer pool.Close()
_ = pool.Sync([]upstream.Target[string]{
{ID: "primary-1", Value: "10.0.0.1:8080", MaxInFlight: 1},
{ID: "primary-2", Value: "10.0.0.2:8080", MaxInFlight: 1},
{ID: "backup", Value: "10.0.0.9:8080", Priority: 1},
})
var leases []upstream.Lease[string]
for range 3 {
l, err := pool.Pick()
if err != nil {
fmt.Println(err)
return
}
leases = append(leases, l)
}
fmt.Println("third pick:", leases[2].Backend().ID())
for i := range leases {
leases[i].Release(upstream.Success)
}
}
Output: third pick: backup
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package health runs active health checks: it probes targets on jittered intervals, applies rise and fall thresholds, measures latency and classifies failures, and reports results and transitions.
|
Package health runs active health checks: it probes targets on jittered intervals, applies rise and fall thresholds, measures latency and classifies failures, and reports results and transitions. |
|
internal
|
|
|
fakeclock
Package fakeclock provides a manually advanced clock for tests.
|
Package fakeclock provides a manually advanced clock for tests. |
|
Package uphttp holds the HTTP-specific parts of upstream: an HTTP health check, a cache of per-backend transports and an http.RoundTripper that balances requests over a pool.
|
Package uphttp holds the HTTP-specific parts of upstream: an HTTP health check, a cache of per-backend transports and an http.RoundTripper that balances requests over a pool. |
|
Package upnet connects to the backends of an upstream.Pool: a Dialer picks a backend, dials it, retries on another backend when the dial fails, and holds the backend's lease for as long as the connection is open, so that in-flight counts, least-connections balancing and drains follow real connections.
|
Package upnet connects to the backends of an upstream.Pool: a Dialer picks a backend, dials it, retries on another backend when the dial fails, and holds the backend's lease for as long as the connection is open, so that in-flight counts, least-connections balancing and drains follow real connections. |