Documentation
ΒΆ
Index ΒΆ
- Variables
- func GetJSON[T any](c *Client, ctx context.Context, url string) (T, error)
- func Permanent(err error) error
- func PostJSON[T any](c *Client, ctx context.Context, url string, body any) (T, error)
- type Backoff
- type BulkheadConfig
- type BulkheadPolicy
- type CircuitBreakerPolicy
- func (cb *CircuitBreakerPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)
- func (cb *CircuitBreakerPolicy) State() State
- func (cb *CircuitBreakerPolicy) WithHooks(h Hooks) *CircuitBreakerPolicy
- func (cb *CircuitBreakerPolicy) WithLogger(l *slog.Logger) *CircuitBreakerPolicy
- func (cb *CircuitBreakerPolicy) WithName(name string) *CircuitBreakerPolicy
- type CircuitConfig
- type Client
- func (c *Client) Close() error
- func (c *Client) Do(req *http.Request) (*http.Response, error)
- func (c *Client) DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error)
- func (c *Client) Get(ctx context.Context, url string) (*http.Response, error)
- func (c *Client) Post(ctx context.Context, url, contentType string, body io.Reader) (*http.Response, error)
- func (c *Client) RoundTrip(req *http.Request) (*http.Response, error)
- func (c *Client) Transport() http.RoundTripper
- type Hooks
- type Option
- func AggressiveConfig() []Option
- func ConservativeConfig() []Option
- func DefaultConfig() []Option
- func ProductionConfig() []Option
- func WithBulkhead(cfg BulkheadConfig) Option
- func WithCircuitBreaker(cfg CircuitConfig) Option
- func WithDebug() Option
- func WithHTTPClient(hc *http.Client) Option
- func WithHooks(h Hooks) Option
- func WithLogger(l *slog.Logger) Option
- func WithPolicy(p Policy) Option
- func WithRateLimit(cfg RateLimitConfig) Option
- func WithRequestID(header string) Option
- func WithRequestIDPolicy(p *RequestIDPolicy) Option
- func WithRetry(cfg RetryConfig) Option
- func WithTimeout(cfg TimeoutConfig) Option
- type PermanentError
- type Policy
- type PolicyFunc
- type RateLimitConfig
- type RateLimitPolicy
- type RequestError
- type RequestIDPolicy
- type RetryConfig
- type RetryPolicy
- type State
- type TimeoutConfig
- type TimeoutPolicy
Examples ΒΆ
Constants ΒΆ
This section is empty.
Variables ΒΆ
var ( ErrCircuitOpen = errors.New("ambatukam: circuit breaker is open") ErrMaxRetries = errors.New("ambatukam: max retries exceeded") ErrNilRequest = errors.New("ambatukam: nil request") ErrTimeout = errors.New("ambatukam: per-attempt timeout exceeded") ErrBulkheadFull = errors.New("ambatukam: bulkhead full") ErrRateLimited = errors.New("ambatukam: rate limited") )
Functions ΒΆ
func GetJSON ΒΆ
GetJSON performs a GET request and decodes the response body as JSON into T.
If the response status is 4xx or 5xx, returns an error wrapping the status code. If decoding fails, returns an error wrapping the underlying decode error.
All configured policies (retry, circuit, bulkhead, rate limit, timeout) apply normally to this request.
The returned *RequestError (on non-2xx) always has Attempts=1 since this is the final response after any retries configured on the Client.
Example ΒΆ
ExampleGetJSON demonstrates the typed JSON helper.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"name":"alice","age":30}`)
}))
defer srv.Close()
client := ambatukam.New()
defer client.Close()
u, err := ambatukam.GetJSON[testUser](client, context.Background(), srv.URL)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(u.Name, u.Age)
Output: alice 30
func Permanent ΒΆ
Permanent wraps err to mark it as non-retryable.
Example ΒΆ
ExamplePermanent demonstrates marking an error as non-retryable.
package main
import (
"errors"
"fmt"
"github.com/farhanturu/ambatukam-go"
)
func main() {
err := ambatukam.Permanent(errors.New("do not retry me"))
fmt.Println(err)
}
Output: permanent: do not retry me
func PostJSON ΒΆ
PostJSON marshals body as JSON, performs a POST with content-type "application/json", and decodes the response body as JSON into T.
If the response status is 4xx or 5xx, returns an error wrapping the status code. If decoding fails, returns an error wrapping the underlying decode error.
All configured policies apply normally.
The returned *RequestError (on non-2xx) always has Attempts=1 since this is the final response after any retries configured on the Client.
Example ΒΆ
ExamplePostJSON demonstrates the typed JSON POST helper.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(201)
fmt.Fprint(w, `{"name":"bob","age":25}`)
}))
defer srv.Close()
client := ambatukam.New()
defer client.Close()
out, err := ambatukam.PostJSON[testUser](client, context.Background(), srv.URL, testUser{Name: "alice", Age: 30})
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(out.Name, out.Age)
Output: bob 25
Types ΒΆ
type Backoff ΒΆ
Backoff produces the delay to wait before the next retry attempt. attempt is 0-indexed: attempt=0 is the delay before the first retry (i.e., after the initial request failed).
func ConstantBackoff ΒΆ
ConstantBackoff returns a Backoff that always returns d.
func ExponentialBackoff ΒΆ
ExponentialBackoff returns a Backoff that grows exponentially from initial up to max, with symmetric jitter of Β±20% applied per delay.
func LinearBackoff ΒΆ
LinearBackoff returns a Backoff that grows linearly: initial + step*attempt, capped at max.
type BulkheadConfig ΒΆ
type BulkheadConfig struct {
MaxConcurrent uint32
// MaxQueue is the number of requests that can wait when MaxConcurrent is reached.
// Set to 0 to disable queueing (fail-fast on capacity).
MaxQueue uint32
// QueueTimeout is how long a queued request waits for a slot before
// returning ErrBulkheadFull.
//
// Special behavior: if MaxQueue > 0 and QueueTimeout == 0, requests wait
// up to 1 second as a safety net (avoiding unbounded waits). To get
// fail-fast behavior, set MaxQueue = 0 instead.
QueueTimeout time.Duration
}
type BulkheadPolicy ΒΆ
type BulkheadPolicy struct {
// contains filtered or unexported fields
}
BulkheadPolicy caps the number of in-flight requests reaching the next policy (typically the HTTP transport / downstream). Additional requests either wait on a bounded queue (up to cfg.MaxQueue) or fail fast with ErrBulkheadFull when the queue is full or queueing is disabled.
Implementation notes:
- The semaphore `sem` is a buffered channel of struct{} with capacity cfg.MaxConcurrent. Sending into it is non-blocking when a slot is free; it would block once the bulkhead is full.
- The `queue` channel is used as a non-blocking admission gate: the Execute path briefly inserts and removes a marker to test whether there is queue capacity left, then waits on the semaphore for up to cfg.QueueTimeout (or 1s as a safety net when timeout is 0).
- inFlight and denied counters are atomic for observability via InFlight() / Denied().
func NewBulkhead ΒΆ
func NewBulkhead(cfg BulkheadConfig) *BulkheadPolicy
NewBulkhead constructs a BulkheadPolicy. Zero-valued cfg.MaxConcurrent defaults to runtime.NumCPU()*2 (floored at 1) so the policy is always usable out of the box.
Example ΒΆ
ExampleNewBulkhead demonstrates configuring a bulkhead.
package main
import (
"fmt"
"time"
"github.com/farhanturu/ambatukam-go"
)
func main() {
cfg := ambatukam.BulkheadConfig{
MaxConcurrent: 5,
MaxQueue: 10,
QueueTimeout: 100 * time.Millisecond,
}
client := ambatukam.New(ambatukam.WithBulkhead(cfg))
defer client.Close()
fmt.Println("max concurrent:", cfg.MaxConcurrent)
}
Output: max concurrent: 5
func (*BulkheadPolicy) Denied ΒΆ
func (b *BulkheadPolicy) Denied() uint64
Denied returns the total count of denied requests since startup (observability).
func (*BulkheadPolicy) Execute ΒΆ
func (b *BulkheadPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)
Execute runs a request through the bulkhead. Admitted requests call next. Denied requests return (nil, ErrBulkheadFull) (wrapped with "queue timeout" when the wait timed out).
func (*BulkheadPolicy) InFlight ΒΆ
func (b *BulkheadPolicy) InFlight() uint32
InFlight returns the current count of in-flight requests (observability).
func (*BulkheadPolicy) WithLogger ΒΆ
func (b *BulkheadPolicy) WithLogger(l *slog.Logger) *BulkheadPolicy
WithLogger sets a non-nil logger on the policy.
type CircuitBreakerPolicy ΒΆ
type CircuitBreakerPolicy struct {
// contains filtered or unexported fields
}
CircuitBreakerPolicy is a circuit breaker resilience policy.
State machine:
- Closed: consecutive failures are counted via cb.failures. When the counter reaches cfg.FailureThreshold, the breaker transitions to Open and records the time the open window began.
- Open: every request fails fast with ErrCircuitOpen. When the open window has elapsed (cfg.OpenDuration), the next request triggers the Open->HalfOpen transition and is admitted as a trial.
- HalfOpen: up to cfg.HalfOpenMaxReqs trial requests are admitted. A successful trial closes the breaker. A failed trial re-opens the breaker.
Concurrency:
- cb.mu protects state, openedAt, halfOpenPermits, halfOpenInFlight. It is never held across next(ctx, req).
- cb.failures is read atomically (fast Closed hot path) and reset to 0 atomically on every success in Closed state and on the HalfOpen->Closed transition.
- cb.generation is a monotonic counter incremented on every state transition. A request entering HalfOpen records the generation; on response the result is only applied if the breaker is still on the same generation. Otherwise the breaker has moved on and the trial is discarded (no stale writes).
func NewCircuitBreaker ΒΆ
func NewCircuitBreaker(cfg CircuitConfig) *CircuitBreakerPolicy
NewCircuitBreaker constructs a CircuitBreakerPolicy. Zero-valued config fields are filled with the package defaults via DefaultCircuitConfig: FailureThreshold=5, OpenDuration=30s, HalfOpenMaxReqs=1.
Example ΒΆ
ExampleNewCircuitBreaker demonstrates configuring the circuit breaker.
package main
import (
"fmt"
"time"
"github.com/farhanturu/ambatukam-go"
)
func main() {
cfg := ambatukam.DefaultCircuitConfig()
cfg.FailureThreshold = 3
cfg.OpenDuration = 10 * time.Second
client := ambatukam.New(ambatukam.WithCircuitBreaker(cfg))
defer client.Close()
fmt.Println("failure threshold:", cfg.FailureThreshold)
}
Output: failure threshold: 3
func (*CircuitBreakerPolicy) Execute ΒΆ
func (cb *CircuitBreakerPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)
Execute runs a single request through the breaker. A request denied by the breaker returns (nil, ErrCircuitOpen) without invoking next. Admitted requests call next and the result is routed through shouldTrip/onSuccess/ onFailure.
func (*CircuitBreakerPolicy) State ΒΆ
func (cb *CircuitBreakerPolicy) State() State
State returns the current breaker state. Safe to call concurrently.
func (*CircuitBreakerPolicy) WithHooks ΒΆ
func (cb *CircuitBreakerPolicy) WithHooks(h Hooks) *CircuitBreakerPolicy
WithHooks installs user callbacks fired on circuit state changes. Only OnStateChange is used by this policy.
func (*CircuitBreakerPolicy) WithLogger ΒΆ
func (cb *CircuitBreakerPolicy) WithLogger(l *slog.Logger) *CircuitBreakerPolicy
WithLogger sets a non-nil logger on the policy.
func (*CircuitBreakerPolicy) WithName ΒΆ
func (cb *CircuitBreakerPolicy) WithName(name string) *CircuitBreakerPolicy
WithName sets the breaker identifier reported in OnStateChange callbacks. An empty name leaves the existing name unchanged (default: "default").
type CircuitConfig ΒΆ
type CircuitConfig struct {
FailureThreshold uint32
OpenDuration time.Duration
HalfOpenMaxReqs uint32
ShouldTrip func(resp *http.Response, err error) bool
}
func DefaultCircuitConfig ΒΆ
func DefaultCircuitConfig() CircuitConfig
DefaultCircuitConfig returns the circuit breaker configuration used when none is provided. Callers can extend these defaults programmatically.
type Client ΒΆ
type Client struct {
// contains filtered or unexported fields
}
func NewDefaultClient ΒΆ
func NewDefaultClient() *Client
NewDefaultClient is a convenience constructor: New(DefaultConfig()...).
Example ΒΆ
ExampleNewDefaultClient demonstrates the one-call production-default client.
package main
import (
"fmt"
"github.com/farhanturu/ambatukam-go"
)
func main() {
client := ambatukam.NewDefaultClient()
defer client.Close()
fmt.Println("ok")
}
Output: ok
func (*Client) DoWithContext ΒΆ
DoWithContext is like Do but lets the caller supply the request context. It is equivalent to Do(req.WithContext(ctx)).
func (*Client) Get ΒΆ
Example ΒΆ
ExampleClient_Get demonstrates a basic GET request with all three policies.
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/farhanturu/ambatukam-go"
)
func main() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"hello":"world"}`)
}))
defer srv.Close()
client := ambatukam.New(
ambatukam.WithTimeout(ambatukam.TimeoutConfig{Timeout: 2 * time.Second}),
ambatukam.WithRetry(ambatukam.DefaultRetryConfig()),
ambatukam.WithCircuitBreaker(ambatukam.DefaultCircuitConfig()),
)
defer client.Close()
resp, err := client.Get(context.Background(), srv.URL)
if err != nil {
fmt.Println("error:", err)
return
}
defer resp.Body.Close()
fmt.Println("status:", resp.StatusCode)
}
Output: status: 200
func (*Client) Post ΒΆ
func (c *Client) Post(ctx context.Context, url, contentType string, body io.Reader) (*http.Response, error)
Example ΒΆ
ExampleClient_Post demonstrates a POST with a JSON body.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"github.com/farhanturu/ambatukam-go"
)
func main() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
fmt.Println("server received:", string(body))
w.WriteHeader(http.StatusCreated)
}))
defer srv.Close()
client := ambatukam.New()
defer client.Close()
resp, err := client.Post(
context.Background(),
srv.URL+"/users",
"application/json",
io.NopCloser(strings.NewReader(`{"name":"alice"}`)),
)
if err != nil {
fmt.Println("error:", err)
return
}
defer resp.Body.Close()
fmt.Println("status:", resp.StatusCode)
}
Output: server received: {"name":"alice"} status: 201
func (*Client) RoundTrip ΒΆ
RoundTrip implements http.RoundTripper. It enables using an amba *Client as the Transport of any *http.Client, including third-party libraries that accept only http.RoundTripper.
Equivalent to c.Do(req).
Example ΒΆ
ExampleClient_RoundTrip shows how to use an amba *Client as the Transport of a standard *http.Client.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/farhanturu/ambatukam-go"
)
func main() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
defer srv.Close()
amba := ambatukam.New(ambatukam.WithRetry(ambatukam.RetryConfig{MaxRetries: 2}))
defer amba.Close()
hc := &http.Client{Transport: amba.Transport()}
resp, err := hc.Get(srv.URL)
if err != nil {
fmt.Println("error:", err)
return
}
defer resp.Body.Close()
fmt.Println("status:", resp.StatusCode)
}
Output: status: 200
func (*Client) Transport ΒΆ
func (c *Client) Transport() http.RoundTripper
Transport returns an http.RoundTripper that delegates to this Client. Convenient for `&http.Client{Transport: ambaClient.Transport()}`.
type Hooks ΒΆ
type Hooks struct {
// BeforeRequest is called immediately before each HTTP attempt.
// It can mutate the request (e.g. add Authorization header) or return
// an error to abort the attempt. Returning an error short-circuits
// the retry loop with that error.
BeforeRequest func(req *http.Request) error
// AfterResponse is called after each HTTP attempt completes (success or failure).
// It is purely observational; its return value is ignored. Use it for logging,
// metrics emission, or response inspection.
AfterResponse func(req *http.Request, resp *http.Response, err error)
// OnRetry is called between retry attempts, before the backoff sleep.
// `attempt` is the 0-indexed attempt number that just completed (the next
// attempt will be `attempt+1`). `nextDelay` is the planned sleep duration.
OnRetry func(req *http.Request, attempt int, nextDelay time.Duration)
// OnStateChange is called when a circuit breaker changes state.
// `name` is the breaker's identifier (defaults to "default" if not set).
// `from` and `to` are the old and new states (e.g. StateClosed β StateOpen).
OnStateChange func(name string, from, to State)
}
Hooks are user callbacks invoked at key lifecycle points. All fields are optional. Any nil callback is skipped.
type Option ΒΆ
type Option func(*Client)
func AggressiveConfig ΒΆ
func AggressiveConfig() []Option
AggressiveConfig returns a strict, fast-fail configuration for protecting fragile downstream services. Lower retry count, lower circuit threshold, short timeouts.
func ConservativeConfig ΒΆ
func ConservativeConfig() []Option
ConservativeConfig returns a generous configuration for critical services that must not fail. Many retries, slow trip, larger timeouts.
func DefaultConfig ΒΆ
func DefaultConfig() []Option
DefaultConfig returns a sensible production-default *Client configuration. Use with New() to get a fully-configured Client:
client := ambatukam.New(ambatukam.DefaultConfig()...)
Defaults:
- Retry: 3 attempts, exponential 100ms..5s, jitter 0.2
- Circuit: 5 failures to trip, 30s open duration, 1 half-open probe
- Timeout: 30s per attempt
- Bulkhead: runtime.NumCPU()*4 concurrent, no queue (fail fast)
- RateLimit: disabled (use WithRateLimit to enable)
- No request ID (use WithRequestID to enable)
- No hooks (use WithHooks to enable)
func ProductionConfig ΒΆ
func ProductionConfig() []Option
ProductionConfig returns a balanced default configuration suitable for most production services. Use with New():
client := ambatukam.New(ambatukam.ProductionConfig()...)
Tuning: 3 retries, 30s timeout, 5-failure circuit, NumCPU*4 concurrent.
func WithBulkhead ΒΆ
func WithBulkhead(cfg BulkheadConfig) Option
func WithCircuitBreaker ΒΆ
func WithCircuitBreaker(cfg CircuitConfig) Option
func WithDebug ΒΆ
func WithDebug() Option
WithDebug enables verbose DEBUG-level logging via slog to stderr. All policy decisions (retries, circuit transitions, rate-limit waits) are logged.
Equivalent to WithLogger(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))).
func WithHTTPClient ΒΆ
Example ΒΆ
ExampleWithHTTPClient demonstrates swapping in a custom *http.Client.
package main
import (
"fmt"
"net/http"
"time"
"github.com/farhanturu/ambatukam-go"
)
func main() {
client := ambatukam.New(
ambatukam.WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
)
defer client.Close()
fmt.Println("custom http.Client")
}
Output: custom http.Client
func WithHooks ΒΆ
WithHooks installs user callbacks invoked at key request lifecycle points (before each attempt, after each response, between retries, and on circuit breaker state changes). Any nil field in Hooks is skipped.
Hooks are propagated to registered retry and circuit breaker policies inside New(), so declaration order does not matter β WithHooks can be passed before or after WithRetry/WithCircuitBreaker with the same effect.
func WithLogger ΒΆ
func WithPolicy ΒΆ
func WithRateLimit ΒΆ
func WithRateLimit(cfg RateLimitConfig) Option
func WithRequestID ΒΆ
WithRequestID enables automatic X-Request-ID generation/propagation. Pass an empty string to use the default header name.
func WithRequestIDPolicy ΒΆ
func WithRequestIDPolicy(p *RequestIDPolicy) Option
WithRequestIDPolicy registers a custom-configured RequestIDPolicy. Use this when you need to set a custom header name or generator (see RequestIDPolicy.WithHeader, RequestIDPolicy.WithGenerator).
func WithRetry ΒΆ
func WithRetry(cfg RetryConfig) Option
func WithTimeout ΒΆ
func WithTimeout(cfg TimeoutConfig) Option
type PermanentError ΒΆ
type PermanentError struct{ Err error }
PermanentError signals that a retry must NOT be attempted for this error. Wrap any error with Permanent() to mark it as non-retryable; the retry policy will propagate it immediately without further attempts.
func (*PermanentError) Error ΒΆ
func (e *PermanentError) Error() string
func (*PermanentError) Unwrap ΒΆ
func (e *PermanentError) Unwrap() error
type Policy ΒΆ
type Policy interface {
Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)
}
func Chain ΒΆ
Chain composes policies so the FIRST argument is OUTERMOST. Execution order: P0.Execute -> P1.Execute -> ... -> Pn.Execute -> next. Each policy sees the request before passing to next, and sees the result/error after.
The returned Policy delegates Execute by threading the supplied `next` into the innermost policy, so when used via WithPolicy(Chain(...)) the Client.Do chain builder's terminal (c.hc.Do) reaches the last policy in the chain.
Example ΒΆ
ExampleChain demonstrates manual composition of policies.
package main
import (
"fmt"
"time"
"github.com/farhanturu/ambatukam-go"
)
func main() {
client := ambatukam.New(ambatukam.WithPolicy(ambatukam.Chain(
ambatukam.NewTimeout(ambatukam.TimeoutConfig{Timeout: 2 * time.Second}),
ambatukam.NewRetry(ambatukam.DefaultRetryConfig()),
ambatukam.NewCircuitBreaker(ambatukam.DefaultCircuitConfig()),
)))
defer client.Close()
fmt.Println("chained: ok")
}
Output: chained: ok
type PolicyFunc ΒΆ
func (PolicyFunc) Execute ΒΆ
func (f PolicyFunc) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)
Execute makes PolicyFunc satisfy the Policy interface so a bare function can be used wherever a Policy is expected (e.g. WithPolicy or as a return value from Chain). The function is invoked directly; `next` is intentionally ignored β a PolicyFunc is treated as a terminal behavior.
type RateLimitConfig ΒΆ
type RateLimitConfig struct {
// Rate is the tokens per second. Special values:
// Rate == 0 (default): rate limiting is disabled β all requests pass through.
// Rate < 0: deny all requests (fail-closed).
// Rate > 0: normal token-bucket behavior.
Rate float64
// Burst is the bucket capacity (default 1 if 0).
Burst uint32
// WaitTimeout is how long to wait for a token when none is available.
// 0 = fail fast; >0 = wait up to this long.
WaitTimeout time.Duration
}
RateLimitConfig configures a token-bucket rate limiter policy.
Rate is the steady-state token replenishment rate in tokens per second. A non-positive Rate denies every request immediately with ErrRateLimited.
Burst is the bucket capacity β the maximum number of tokens that can be consumed back-to-back without replenishment. Zero is replaced with 1 inside NewRateLimit.
WaitTimeout controls behaviour when no token is available. Zero means fail fast (the request returns ErrRateLimited immediately). A positive value means the policy waits up to that duration for a token to become available; if the wait would exceed WaitTimeout the request is denied.
Note: because the bucket is checked, then released, then re-checked between waits, this policy is best-effort under high concurrency. For strict admission control, consider a dedicated library. The race window is bounded by WaitTimeout: a goroutine that releases the mutex with a small remaining deficit and then sleeps WaitTimeout may find another goroutine has already consumed the freshly-refilled token, in which case it loops and tries again.
type RateLimitPolicy ΒΆ
type RateLimitPolicy struct {
// contains filtered or unexported fields
}
RateLimitPolicy is a token-bucket rate limiter.
State machine (per policy instance):
- The bucket holds `tokens` (a float) and is refilled continuously at `cfg.Rate` tokens per second up to a cap of `cfg.Burst`.
- On every Execute call the bucket is refilled based on elapsed wall time since `lastRefill`, then the policy either consumes one token and forwards the request, or β depending on `cfg.WaitTimeout` β either fails fast with ErrRateLimited or sleeps up to the time required for one token to be available (capped by WaitTimeout).
Concurrency:
- `mu` serialises refill + token consumption. It is never held across `next(ctx, req)` β the mutex is released before the HTTP call runs, so the rate limiter does not throttle concurrency, only throughput.
- There is an inherent best-effort race between releasing the mutex and the next refill: a goroutine that wakes after a short deficit can find another goroutine has already consumed the freshly available token. The waiter then loops and tries again. The race window is bounded by WaitTimeout. This is acceptable for typical HTTP rate limiting; for strict admission control use a dedicated library such as golang.org/x/time/rate.
func NewRateLimit ΒΆ
func NewRateLimit(cfg RateLimitConfig) *RateLimitPolicy
NewRateLimit constructs a RateLimitPolicy.
Zero-valued fields are normalised:
- cfg.Burst == 0 β cfg.Burst = 1
- cfg.WaitTimeout < 0 β cfg.WaitTimeout = 0
cfg.Rate semantics:
- cfg.Rate == 0 β disabled: every request passes through unchanged.
- cfg.Rate < 0 β closedAll: every request returns ErrRateLimited.
- cfg.Rate > 0 β normal token-bucket behaviour.
Example ΒΆ
ExampleNewRateLimit demonstrates building a rate-limit policy directly.
package main
import (
"fmt"
"github.com/farhanturu/ambatukam-go"
)
func main() {
client := ambatukam.New(ambatukam.WithRateLimit(ambatukam.RateLimitConfig{
Rate: 10,
Burst: 5,
}))
defer client.Close()
fmt.Println("rate limited client ready")
}
Output: rate limited client ready
func (*RateLimitPolicy) Execute ΒΆ
func (r *RateLimitPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)
Execute runs a request through the rate limiter.
Behaviour:
- disabled (Rate == 0): the request is forwarded unchanged.
- closedAll (Rate < 0): every request returns ErrRateLimited immediately.
- Bucket has a token: the token is consumed and the request is forwarded.
- Bucket is empty, WaitTimeout == 0: returns ErrRateLimited immediately.
- Bucket is empty, WaitTimeout > 0: sleeps up to WaitTimeout (capped by the actual time-to-token), then retries. Honours ctx cancellation.
func (*RateLimitPolicy) WithLogger ΒΆ
func (r *RateLimitPolicy) WithLogger(l *slog.Logger) *RateLimitPolicy
WithLogger sets a non-nil logger on the policy.
type RequestError ΒΆ
RequestError captures full context about a failed request: method, URL, status code, attempt count, and the underlying error.
func (*RequestError) Error ΒΆ
func (e *RequestError) Error() string
func (*RequestError) Unwrap ΒΆ
func (e *RequestError) Unwrap() error
type RequestIDPolicy ΒΆ
type RequestIDPolicy struct {
// contains filtered or unexported fields
}
RequestIDPolicy adds or propagates a request ID header on every request. Useful for distributed tracing and log correlation across services.
func NewRequestIDPolicy ΒΆ
func NewRequestIDPolicy() *RequestIDPolicy
NewRequestIDPolicy returns a RequestIDPolicy that uses the default "X-Request-ID" header and an internal hex-encoded random generator. Use WithHeader and WithGenerator to customise.
Example ΒΆ
ExampleNewRequestIDPolicy demonstrates registering a request-ID policy.
package main
import (
"fmt"
"github.com/farhanturu/ambatukam-go"
)
func main() {
client := ambatukam.New(ambatukam.WithRequestIDPolicy(ambatukam.NewRequestIDPolicy()))
defer client.Close()
fmt.Println("request ID policy enabled")
}
Output: request ID policy enabled
func (*RequestIDPolicy) Execute ΒΆ
func (r *RequestIDPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)
Execute sets the configured request ID header on req when not already present, then invokes next.
func (*RequestIDPolicy) WithGenerator ΒΆ
func (r *RequestIDPolicy) WithGenerator(gen func() string) *RequestIDPolicy
WithGenerator overrides the ID generator (e.g., for UUID v7). Nil generators are ignored.
func (*RequestIDPolicy) WithHeader ΒΆ
func (r *RequestIDPolicy) WithHeader(name string) *RequestIDPolicy
WithHeader overrides the header name (default "X-Request-ID"). Empty names are ignored to keep a sane default.
type RetryConfig ΒΆ
type RetryConfig struct {
MaxRetries int
InitialBackoff time.Duration
MaxBackoff time.Duration
Multiplier float64
Jitter float64
Backoff Backoff
ShouldRetry func(resp *http.Response, err error) bool
}
func DefaultRetryConfig ΒΆ
func DefaultRetryConfig() RetryConfig
DefaultRetryConfig returns the retry configuration used when none is provided. Callers can extend these defaults programmatically.
type RetryPolicy ΒΆ
type RetryPolicy struct {
// contains filtered or unexported fields
}
RetryPolicy retries failed requests with configurable backoff. The request body is buffered once before any attempt so each retry can re-read it safely (safe POST retry).
func NewRetry ΒΆ
func NewRetry(cfg RetryConfig) *RetryPolicy
NewRetry constructs a RetryPolicy. Zero-valued cfg fields are filled with the package defaults via applyRetryDefaults. Defaults: MaxRetries floored at 0, backoff defaults to exponential 100ms..5s with multiplier 2, jitter clamped to [0,1].
Example ΒΆ
ExampleNewRetry demonstrates configuring the retry policy.
package main
import (
"fmt"
"time"
"github.com/farhanturu/ambatukam-go"
)
func main() {
cfg := ambatukam.DefaultRetryConfig()
cfg.MaxRetries = 5
cfg.Backoff = ambatukam.ExponentialBackoff(50*time.Millisecond, time.Second, 2.0)
client := ambatukam.New(ambatukam.WithRetry(cfg))
defer client.Close()
fmt.Println("max retries:", cfg.MaxRetries)
}
Output: max retries: 5
func (*RetryPolicy) Execute ΒΆ
func (r *RetryPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)
Execute runs the request through next, retrying transient failures per config.
func (*RetryPolicy) WithHooks ΒΆ
func (r *RetryPolicy) WithHooks(h Hooks) *RetryPolicy
WithHooks installs user callbacks fired during the retry lifecycle. Each non-nil callback is invoked at the appropriate point: BeforeRequest before each attempt (errors abort the loop), AfterResponse after each attempt, OnRetry between attempts before the backoff sleep.
func (*RetryPolicy) WithLogger ΒΆ
func (r *RetryPolicy) WithLogger(l *slog.Logger) *RetryPolicy
WithLogger sets a non-nil logger on the policy.
type TimeoutConfig ΒΆ
type TimeoutPolicy ΒΆ
type TimeoutPolicy struct {
// contains filtered or unexported fields
}
TimeoutPolicy applies a per-attempt deadline to the request passed to its `next` PolicyFunc. If the deadline fires before the downstream call returns, the error returned to the caller is wrapped with ErrTimeout so callers can distinguish a per-attempt timeout from other failures (e.g. parent context cancellation).
A non-positive timeout disables the policy entirely.
func NewTimeout ΒΆ
func NewTimeout(cfg TimeoutConfig) *TimeoutPolicy
NewTimeout constructs a TimeoutPolicy from a TimeoutConfig.
Example ΒΆ
ExampleNewTimeout demonstrates configuring the per-attempt timeout.
package main
import (
"fmt"
"time"
"github.com/farhanturu/ambatukam-go"
)
func main() {
client := ambatukam.New(
ambatukam.WithTimeout(ambatukam.TimeoutConfig{Timeout: 500 * time.Millisecond}),
)
defer client.Close()
fmt.Println("timeout: ok")
}
Output: timeout: ok
func (*TimeoutPolicy) Execute ΒΆ
func (t *TimeoutPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)
Execute runs the downstream call under a child context with the configured timeout. If the timeout fires (child context's Err() is DeadlineExceeded) while the parent context is still healthy, the returned error is wrapped with ErrTimeout. If the parent context is already canceled (or its own deadline already fired), the underlying error is returned unchanged so the caller can distinguish between a per-attempt timeout and an outer cancel.
The child context is attached to the request via req.WithContext so the downstream HTTP transport observes the per-attempt deadline (the transport reads from req.Context, not from any ctx argument the caller passes).