Documentation
¶
Overview ¶
Package maintenance controls admission while existing work retains its normal resource ownership and cancellation rules. It owns no servers or goroutines.
Index ¶
- Constants
- Variables
- func NewSecret() (string, error)
- func Publish(ctx context.Context, gate *Gate, store Store, state State) error
- func Refresh(ctx context.Context, gate *Gate, store Store) error
- func Watch(ctx context.Context, gate *Gate, store Store, interval time.Duration, ...) error
- func WithContext(ctx context.Context, gate *Gate) context.Context
- type Gate
- func (g *Gate) Admit() error
- func (g *Gate) Apply(state State) error
- func (g *Gate) Bypass(ctx context.Context, cookie string, now time.Time) bool
- func (g *Gate) Drain()
- func (g *Gate) Exempts(method, path string, ip netip.Addr) bool
- func (g *Gate) IssueBypass(ctx context.Context, candidate string, now time.Time) (string, time.Time, bool)
- func (g *Gate) Mode() Mode
- func (g *Gate) Policy() Policy
- func (g *Gate) Set(paused bool) error
- func (g *Gate) State() State
- func (g *Gate) Wait(ctx context.Context) error
- type Mode
- type Policy
- type Rule
- type SecretDigest
- type State
- type Store
Constants ¶
const ( // MaxRules bounds configured plus shared exemption rules separately. MaxRules = 64 // MaxAllowedPrefixes bounds configured plus shared network allow lists separately. MaxAllowedPrefixes = 64 // MaxMessageBytes bounds the operator message rendered in maintenance responses. MaxMessageBytes = 512 // MaxRetryAfter bounds the advertised Retry-After interval. MaxRetryAfter = 24 * time.Hour // DefaultBypassTTL is the lifetime of a secret bypass cookie. DefaultBypassTTL = 12 * time.Hour // MaxBypassTTL bounds configured bypass cookie lifetimes. MaxBypassTTL = 7 * 24 * time.Hour // BypassCookie names the signed cookie that admits a browser holding the secret. BypassCookie = "foundry_maintenance" )
const ( // DefaultPollInterval is how often an instance reloads the shared state. DefaultPollInterval = 5 * time.Second // MinPollInterval and MaxPollInterval bound shared-state polling. MinPollInterval = 100 * time.Millisecond MaxPollInterval = 10 * time.Minute )
Variables ¶
var ErrDraining = safeerror.New("application is draining")
var ErrMaintenance = safeerror.New("application is in maintenance mode")
Functions ¶
func Publish ¶
Publish saves shared state and then applies it to the local gate, so the issuing process observes the same record as polling instances.
func Refresh ¶
Refresh loads the shared record once and applies it. An absent record leaves the local gate unchanged, so configured initial maintenance survives until an operator first saves shared state. Draining gates ignore the record.
func Watch ¶
func Watch(ctx context.Context, gate *Gate, store Store, interval time.Duration, report func(error)) error
Watch refreshes the gate every interval until ctx ends and then returns nil. A failed load retains the last applied state; report receives the first failure after each success (it may be nil) so a store outage is visible without logging every poll. Transient failures never stop the application.
func WithContext ¶
WithContext carries the application's admission gate. Kernels derive request, job and command contexts from the application runtime, so every admission check shares one gate even when observability is disabled. A nil gate clears an inherited gate and represents normal serving.
Types ¶
type Gate ¶
type Gate struct {
// contains filtered or unexported fields
}
Gate is application-owned and concurrency-safe. Pausing admission is reversible; Drain is terminal. A nil optional gate represents normal serving. Admission checks do not cancel existing work or revoke acquired resources. Do not copy a Gate after first use. The zero value is ready to use and has an empty Policy; New validates and snapshots configured exemptions.
func FromContext ¶
FromContext returns the carried gate, or nil (normal serving) when absent.
func (*Gate) Apply ¶
Apply replaces the operator state, pausing or resuming admission. A shared store applies the same record on every instance. Drain remains terminal.
func (*Gate) Drain ¶
func (g *Gate) Drain()
Drain permanently stops admission and wakes waiting workers. It is idempotent.
func (*Gate) Exempts ¶
Exempts reports whether a paused gate admits this request through a configured or shared rule or allowed network. It is false unless the gate is paused, so exemptions never reopen a draining application.
func (*Gate) IssueBypass ¶
func (g *Gate) IssueBypass(ctx context.Context, candidate string, now time.Time) (string, time.Time, bool)
IssueBypass signs a bypass cookie when a paused gate's shared secret matches candidate. The cookie lifetime comes from the configured Policy.
func (*Gate) Set ¶
Set pauses or resumes admission. Existing work continues under its original lifetime; application shutdown must still drain the owning kernels/resources.
type Policy ¶
type Policy struct {
Exempt []Rule
Allow []netip.Prefix
BypassTTL time.Duration
// Keys seal bypass cookies so every instance sharing them accepts one;
// typically the application key ring. Without keys a cookie is valid only
// on the instance that issued it.
Keys *encryption.Keyring
}
Policy is deployment configuration retained by one Gate. It never changes after construction; shared operator State adds its own rules and networks.
type Rule ¶
type Rule struct {
Method string `json:"method,omitempty"`
Path string `json:"path"`
Prefix bool `json:"prefix,omitempty"`
}
Rule exempts matching requests from a paused gate. Method is an uppercase HTTP method or empty for every method. Path is a canonical absolute path; Prefix also matches every descendant segment ("/status" matches "/status/db", never "/statuses"). Exemptions never bypass Draining.
func ParseRule ¶
ParseRule reads the operator text form "[METHOD ]/path[/*]". A trailing "/*" selects prefix matching ("/*" alone exempts every path).
type SecretDigest ¶
type SecretDigest [32]byte
SecretDigest is the SHA-256 digest of a bypass secret. Stores retain only the digest; the plaintext secret is shown once to the operator who created it.
func DigestSecret ¶
func DigestSecret(secret string) (SecretDigest, error)
DigestSecret validates a bypass secret: 16 to 128 URL-safe characters (letters, digits, '-' and '_'), usable as one request path segment.
func (SecretDigest) IsZero ¶
func (d SecretDigest) IsZero() bool
IsZero reports whether the digest is all zero bytes.
func (SecretDigest) LogValue ¶
func (SecretDigest) LogValue() slog.Value
LogValue returns a fixed log label without exposing the digest.
func (SecretDigest) MarshalText ¶
func (d SecretDigest) MarshalText() ([]byte, error)
MarshalText encodes the digest as lowercase hexadecimal, or empty text for the zero value.
func (*SecretDigest) UnmarshalText ¶
func (d *SecretDigest) UnmarshalText(text []byte) error
UnmarshalText decodes lowercase hexadecimal, treating empty text as the zero digest.
type State ¶
type State struct {
Down bool
RetryAfter time.Duration
Message string
Secret SecretDigest
Allow []netip.Prefix
Exempt []Rule
Since time.Time
}
State is the operator-controlled maintenance record shared by a fleet. Down pauses admission. RetryAfter is advertised in whole seconds; Message is an operator-supplied public text. Allow and Exempt add to the configured Policy.
func (State) MarshalJSON ¶
MarshalJSON is the versioned shared-store representation. It contains the secret digest, never the secret itself.
func (State) MatchesSecret ¶
MatchesSecret compares a candidate secret with the stored digest in constant time.
func (*State) UnmarshalJSON ¶
UnmarshalJSON decodes a bounded versioned maintenance record and replaces the receiver only after validation.
type Store ¶
type Store interface {
// Load loads maintenance state and reports whether a record exists.
Load(context.Context) (State, bool, error)
// Save persists the supplied maintenance state.
Save(context.Context, State) error
}
Store shares one operator State across a fleet, for example through a Redis or PostgreSQL cache store. Load reports found=false before any State was saved. Implementations must honor context cancellation and bound their I/O. Admission never reads the store: each instance applies polled records to its local Gate, which is the cache consulted on every request, job and command.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package cachestore shares maintenance state through an existing configured cache store.
|
Package cachestore shares maintenance state through an existing configured cache store. |