Documentation
¶
Overview ¶
Package resilience provides circuit breaker, rate limiting, and query timeout utilities.
Index ¶
- func CircuitBreakerMiddleware(cb *gobreaker.CircuitBreaker) func(http.Handler) http.Handler
- func IsOpen(cb *gobreaker.CircuitBreaker) bool
- func NewQueryBreaker(name string) *gobreaker.CircuitBreaker
- func NewQueryBreakerWith(name string, isFailure func(error) bool) *gobreaker.CircuitBreaker
- func RateLimit(tl *TenantLimiter) func(http.Handler) http.Handler
- func RateLimitMiddleware(tl *TenantLimiter, tier Tier) func(http.Handler) http.Handler
- func WithQueryTimeout(ctx context.Context, fn func(ctx context.Context) error) error
- type RateLimitConfig
- type TenantLimiter
- type Tier
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CircuitBreakerMiddleware ¶
CircuitBreakerMiddleware wraps a handler with a gobreaker circuit breaker. When the circuit is open it returns 503 with Retry-After: 8. Failures are counted when the handler writes a 5xx response.
func IsOpen ¶
func IsOpen(cb *gobreaker.CircuitBreaker) bool
IsOpen is the hot-path state check — an O(1) mutex read with zero allocations. Call this before executing a query to decide whether to serve from cache or return 503.
func NewQueryBreaker ¶
func NewQueryBreaker(name string) *gobreaker.CircuitBreaker
NewQueryBreaker returns a circuit breaker tuned for PostgreSQL query protection.
Opens when ≥10 requests have a ≥60% failure rate. Transitions open→half-open after 8 s; allows 2 probe requests in half-open. Every non-nil error counts as a failure — the gobreaker default. Production callers use NewQueryBreakerWith, which decides what a failure IS.
func NewQueryBreakerWith ¶ added in v0.1.10
func NewQueryBreakerWith(name string, isFailure func(error) bool) *gobreaker.CircuitBreaker
NewQueryBreakerWith is NewQueryBreaker with an explicit definition of failure: isFailure(err) reports whether a non-nil error means the database could not serve the request. When nil, every error counts.
WHY (ENG-49, MOTOR-TIPO-JSON-S1). The breaker exists to shed load when PostgreSQL is DOWN. With the default "every error is a failure", a unique violation, an unknown column (a plain 422), a class-22 value, a driver encode error — all produced by CLIENT INPUT, none an outage — were counted, and six 422s in a row opened the breaker: every write of the process (every tenant of the app) answered 503 for 8 s, renewably, to any caller with `create` on any resource. A statement the database REJECTED is proof the database is up. pkg/db passes the SAME predicate that already decides the 503 (timeouts, connection failures, class 08/53/57P0x), so "counted by the breaker" and "answered 503" can never disagree.
func RateLimit ¶
func RateLimit(tl *TenantLimiter) func(http.Handler) http.Handler
RateLimit enforces a configured TenantLimiter's per-tenant policy. It is the tier-less entry point used by the server: the limiter already carries its RPS/Burst config, so the tier argument is irrelevant and passed as TierPro.
func RateLimitMiddleware ¶
RateLimitMiddleware returns a chi-compatible middleware that enforces per-tenant rate limits. Tenants not found in context are skipped (e.g., health checks). Returns 429 Too Many Requests when the token bucket is empty.
Types ¶
type RateLimitConfig ¶
type RateLimitConfig struct {
RPS float64 // sustained requests per second, per tenant
Burst int // bucket capacity for short spikes
}
RateLimitConfig sets an explicit per-tenant token-bucket policy, independent of subscription tier. Used to wire the limiter from environment configuration.
type TenantLimiter ¶
type TenantLimiter struct {
// contains filtered or unexported fields
}
TenantLimiter provides per-tenant token-bucket rate limiting. Each tenant gets an independent limiter keyed by tenantID. With a nil cfg the burst size equals the tier rate (1-second burst capacity); with cfg set, every tenant uses cfg.RPS / cfg.Burst and the tier is ignored.
Past maxLimiters distinct tenants, additional (unknown) tenants share a single overflow bucket: memory stays bounded and the rate limit still applies.
func NewConfiguredLimiter ¶
func NewConfiguredLimiter(cfg RateLimitConfig) *TenantLimiter
NewConfiguredLimiter creates a TenantLimiter that applies cfg to every tenant.
func NewTenantLimiter ¶
func NewTenantLimiter() *TenantLimiter
NewTenantLimiter creates an empty tier-based TenantLimiter.