Documentation
¶
Index ¶
- Variables
- func Permanent(err error) error
- func RetryAfter(err error, d time.Duration) error
- type Attacher
- type Backoff
- type Clock
- type Config
- type ConstantBackoff
- type ErrorLimit
- type ExponentialBackoff
- type Job
- type LinearBackoff
- type Metrics
- type Option
- func WithLogger(l *slog.Logger) Option
- func WithMaxWorkers(n int) Option
- func WithMetricsEnabled(bool) Optiondeprecated
- func WithMinWorkers(n int) Option
- func WithName(name string) Option
- func WithOnTaskDone(fn func(TaskResult)) Option
- func WithQueueSize(n int) Option
- func WithRecorder(r Recorder) Option
- func WithRetry(policy RetryPolicy) Option
- func WithScaleCooldown(d time.Duration) Option
- func WithScaleDownIdleDuration(d time.Duration) Option
- func WithScaleUpThreshold(pct float64) Option
- type PanicError
- type Pool
- type PoolInfo
- type Recorder
- type RejectReason
- type RetryError
- type RetryPolicy
- type ShutdownError
- type Status
- type Task
- type TaskResult
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrPoolShutdown is returned by Submit once Shutdown has started, and is the // RetryError reason when a forced shutdown aborts a backoff wait ErrPoolShutdown = errors.New("pool is shutdown") // ErrQueueFull is returned by TrySubmit when the queue has no space ErrQueueFull = errors.New("queue is full") // ErrTimeout is returned when a job submission times out. It wraps // context.DeadlineExceeded, so errors.Is matches both. ErrTimeout = fmt.Errorf("submission timeout: %w", context.DeadlineExceeded) // ErrNilJob is returned when submitting a nil job ErrNilJob = errors.New("job is nil") // ErrRetriesExhausted is the RetryError reason when a job used all its attempts ErrRetriesExhausted = errors.New("retries exhausted") // ErrRetryBudgetExceeded is the RetryError reason when the next retry would exceed MaxElapsedTime ErrRetryBudgetExceeded = errors.New("retry time budget exceeded") // ErrPermanentFailure is the RetryError reason when a job returned an error wrapped with Permanent ErrPermanentFailure = errors.New("permanent failure") // ErrInvalidConfig is returned when pool configuration is invalid ErrInvalidConfig = errors.New("invalid pool configuration") )
Functions ¶
func Permanent ¶ added in v0.1.0
Permanent marks err as not retryable: the retry engine stops immediately. Use it for failures that cannot succeed on retry, such as invalid input. Permanent(nil) returns nil.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"log/slog"
adaptivepool "github.com/iyashjayesh/go-adaptive-pool"
)
func main() {
done := make(chan adaptivepool.TaskResult, 1)
pool, err := adaptivepool.New(
adaptivepool.WithLogger(slog.New(slog.DiscardHandler)),
adaptivepool.WithRetry(adaptivepool.RetryPolicy{MaxAttempts: 5}),
adaptivepool.WithOnTaskDone(func(r adaptivepool.TaskResult) { done <- r }),
)
if err != nil {
panic(err)
}
defer func() { _ = pool.Shutdown(context.Background()) }()
errInvalid := errors.New("invalid payload")
_ = pool.Submit(context.Background(), func(context.Context) error {
return adaptivepool.Permanent(errInvalid)
})
r := <-done
fmt.Println(r.Status, r.Attempts)
fmt.Println(errors.Is(r.Err, adaptivepool.ErrPermanentFailure), errors.Is(r.Err, errInvalid))
}
Output: permanent 1 true true
Types ¶
type Attacher ¶ added in v0.1.0
Attacher is implemented by recorders that export gauges or need registration. New calls Attach before starting the pool, and an error fails New. Shutdown calls Detach once every worker has exited, after which the pool reports no more events. stats can be read at any time to report current values such as queue depth.
type Backoff ¶ added in v0.1.0
Backoff computes the delay before a retry. retry starts at 1 for the first retry. Implementations must be safe for concurrent use; the built-in ones are stateless values.
type Config ¶
type Config struct {
// contains filtered or unexported fields
}
Config holds the configuration for the pool
type ConstantBackoff ¶ added in v0.1.0
ConstantBackoff waits the same Delay before every retry.
type ErrorLimit ¶ added in v0.1.0
ErrorLimit limits how many attempts may fail with errors matching Err.
type ExponentialBackoff ¶ added in v0.1.0
ExponentialBackoff waits Base * Multiplier^(retry-1). A zero Multiplier defaults to 1.5.
type LinearBackoff ¶ added in v0.1.0
LinearBackoff waits Base * retry: Base, 2*Base, 3*Base, ...
type Metrics ¶
type Metrics interface {
// QueueDepth returns the current number of jobs in the queue
QueueDepth() int
// ActiveWorkers returns the current number of active workers
ActiveWorkers() int
// BusyWorkers returns the number of workers currently executing a job
BusyWorkers() int
// JobsProcessed returns the total number of jobs processed
JobsProcessed() int64
// JobsFailed returns the number of processed jobs that ended with an error or panic
JobsFailed() int64
// JobsRejected returns the total number of jobs rejected
JobsRejected() int64
// JobsDropped returns the number of queued jobs a forced shutdown discarded without running
JobsDropped() int64
// JobsRetried returns the number of processed jobs that needed at least one retry
JobsRetried() int64
// RetryAttempts returns the total number of retries across all jobs
RetryAttempts() int64
// AvgJobLatency returns the average job execution time
AvgJobLatency() time.Duration
// AvgQueueWait returns the average time jobs spent queued before execution
AvgQueueWait() time.Duration
}
Metrics provides observability into pool behavior
type Option ¶
type Option func(*Config)
Option is a functional option for configuring the pool
func WithLogger ¶ added in v0.1.0
WithLogger sets the structured logger used by the pool. Default: slog.Default()
func WithMaxWorkers ¶
WithMaxWorkers sets the maximum number of workers Default: runtime.NumCPU()
func WithMetricsEnabled
deprecated
func WithMinWorkers ¶
WithMinWorkers sets the minimum number of workers Default: 1
func WithName ¶ added in v0.1.0
WithName names the pool. The name is attached to every log record and passed to recorders in PoolInfo, so multiple pools in one process can be told apart. Default: "" (unnamed)
func WithOnTaskDone ¶ added in v0.1.0
func WithOnTaskDone(fn func(TaskResult)) Option
WithOnTaskDone sets a hook called exactly once per executed task, whatever the outcome, and once per queued task dropped by a forced shutdown. It lets application code react to results (dead-letter queues, status updates, tracing) since Submit returns before the task runs.
The hook runs synchronously on the worker goroutine, so keep it fast and hand off heavy work. A panic in the hook is recovered and logged.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"log/slog"
adaptivepool "github.com/iyashjayesh/go-adaptive-pool"
)
func main() {
done := make(chan adaptivepool.TaskResult, 1)
pool, err := adaptivepool.New(
adaptivepool.WithLogger(slog.New(slog.DiscardHandler)),
adaptivepool.WithOnTaskDone(func(r adaptivepool.TaskResult) { done <- r }),
)
if err != nil {
panic(err)
}
defer func() { _ = pool.Shutdown(context.Background()) }()
_ = pool.SubmitTask(context.Background(), adaptivepool.Task{
ID: "order-8812",
Kind: "send_email",
Run: func(context.Context) error { return errors.New("smtp: connection refused") },
})
r := <-done
fmt.Printf("%s %s %s: %v\n", r.Kind, r.ID, r.Status, r.Err)
}
Output: send_email order-8812 error: smtp: connection refused
func WithQueueSize ¶
WithQueueSize sets the bounded queue capacity Default: 1000
func WithRecorder ¶ added in v0.1.0
WithRecorder sends pool events to r, e.g. a Prometheus or OpenTelemetry recorder. The pool's own counters behind Metrics() are always kept. Default: none
func WithRetry ¶ added in v0.1.0
func WithRetry(policy RetryPolicy) Option
WithRetry enables retries for every job submitted to the pool. Default: disabled
Example ¶
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
adaptivepool "github.com/iyashjayesh/go-adaptive-pool"
)
func main() {
done := make(chan adaptivepool.TaskResult, 1)
pool, err := adaptivepool.New(
adaptivepool.WithLogger(slog.New(slog.DiscardHandler)),
adaptivepool.WithRetry(adaptivepool.RetryPolicy{
MaxAttempts: 5,
Backoff: adaptivepool.ExponentialBackoff{Base: time.Millisecond},
MaxDelay: 10 * time.Millisecond,
Jitter: true,
}),
adaptivepool.WithOnTaskDone(func(r adaptivepool.TaskResult) { done <- r }),
)
if err != nil {
panic(err)
}
defer func() { _ = pool.Shutdown(context.Background()) }()
calls := 0
_ = pool.Submit(context.Background(), func(context.Context) error {
calls++
if calls < 3 {
return errors.New("temporarily unavailable")
}
return nil
})
r := <-done
fmt.Println(r.Status, "after", r.Attempts, "attempts")
}
Output: success after 3 attempts
func WithScaleCooldown ¶
WithScaleCooldown sets the minimum duration between scaling operations Default: 5s
func WithScaleDownIdleDuration ¶
WithScaleDownIdleDuration sets the idle duration before scaling down workers Default: 30s
func WithScaleUpThreshold ¶
WithScaleUpThreshold sets the queue utilization percentage (0.0 to 1.0) that triggers scale-up Default: 0.7 (70%)
type PanicError ¶ added in v0.1.0
PanicError is the error recorded when a job panics. Stack holds the goroutine stack captured at the time of the panic.
func (*PanicError) Error ¶ added in v0.1.0
func (e *PanicError) Error() string
type Pool ¶
type Pool interface {
// Submit submits a job to the pool for execution. If the queue is full
// it blocks until there is space, ctx is done or the pool shuts down.
// ctx is also the context the job runs with; use SubmitTask with
// Task.Context to give the job a different one.
//
// It returns ErrNilJob for a nil job, ErrPoolShutdown once Shutdown has
// started, ErrTimeout if ctx's deadline passes while waiting, and
// context.Canceled if ctx is cancelled while waiting. A ctx that is
// already done is rejected the same way, without queueing, unless
// Task.Context is set.
Submit(ctx context.Context, job Job) error
// SubmitTask is like Submit but carries a task ID and kind for logs and
// metrics, and an optional execution context separate from ctx. It
// returns the same errors as Submit.
SubmitTask(ctx context.Context, task Task) error
// TrySubmit is like SubmitTask but never waits: if the queue is full it
// returns ErrQueueFull. ctx is only the job's context, used when
// task.Context is nil.
TrySubmit(ctx context.Context, task Task) error
// Shutdown gracefully shuts down the pool
// Waits for in-flight jobs to complete within the context timeout
// If ctx ends first, jobs still queued are dropped without running, each
// reported with StatusShutdown, and it returns a *ShutdownError.
// It may be called more than once: later calls return the first call's
// result, or ctx's error if ctx ends before the shutdown does
Shutdown(ctx context.Context) error
// Metrics returns the pool metrics
Metrics() Metrics
}
Pool represents a worker pool that can execute jobs concurrently
type Recorder ¶ added in v0.1.0
type Recorder interface {
// JobSubmitted is called when a job is accepted into the queue
JobSubmitted(kind string)
// JobRejected is called when a submission is rejected
JobRejected(kind string, reason RejectReason)
// JobCompleted is called once per executed job with its final status,
// number of attempts, time spent queued and execution time
JobCompleted(kind string, status Status, attempts int, queueWait, duration time.Duration)
// JobDropped is called once per queued job that a forced shutdown
// discarded without running it
JobDropped(kind string)
// ScaledUp is called when the pool adds workers
ScaledUp(added int)
}
Recorder receives pool events, decoupling the pool from any metrics backend. See the promrecorder and otelrecorder packages for Prometheus and OpenTelemetry implementations.
Kinds are normalized before being passed in: an empty kind is reported as "default". Methods run on submitting and worker goroutines, so implementations must be safe for concurrent use and fast. A panic in a method is recovered and logged once, so a faulty recorder can't crash the pool.
type RejectReason ¶ added in v0.1.0
type RejectReason string
RejectReason explains why a submission was rejected
const ( // RejectShutdown means the pool was shutting down RejectShutdown RejectReason = "shutdown" // RejectTimeout means the submit context's deadline passed while waiting for queue space RejectTimeout RejectReason = "timeout" // RejectCanceled means the submit context was canceled while waiting for queue space RejectCanceled RejectReason = "canceled" // RejectQueueFull means TrySubmit found the queue full RejectQueueFull RejectReason = "queue_full" )
type RetryError ¶ added in v0.1.0
type RetryError struct {
// Reason explains why retrying stopped
Reason error
// Attempts is the number of times the job ran
Attempts int
// Err is the error returned by the last attempt
Err error
}
RetryError is the final error of a job that was retried and gave up. errors.Is matches both the stop Reason (e.g. ErrRetriesExhausted or context.Canceled) and the job's last error.
func (*RetryError) Error ¶ added in v0.1.0
func (e *RetryError) Error() string
func (*RetryError) Unwrap ¶ added in v0.1.0
func (e *RetryError) Unwrap() []error
Unwrap returns the stop reason and the last attempt's error
type RetryPolicy ¶ added in v0.1.0
type RetryPolicy struct {
// MaxAttempts is the maximum number of times a job runs, including the
// first attempt. Required and must be >= 1.
MaxAttempts int
// Backoff computes the wait before each retry.
// Default: ExponentialBackoff{Base: 100ms}
Backoff Backoff
// MaxDelay caps any single wait, including RetryAfter hints, so a job
// cannot hold a worker for an unbounded time. Zero means no cap.
MaxDelay time.Duration
// Jitter draws each wait uniformly from [0, delay), after MaxDelay, to
// spread out retries of jobs that failed together.
Jitter bool
// MaxElapsedTime bounds a job's total time across attempts and waits.
// Retrying stops if the next wait would exceed it; a running attempt is
// never interrupted. Zero means no budget.
MaxElapsedTime time.Duration
// ErrorLimits caps attempts for specific errors. Failures are matched
// with errors.Is against each limit in order; the first match counts.
// MaxAttempts still applies to the job as a whole.
ErrorLimits []ErrorLimit
// Clock is the time source used for backoff waits. Override it in tests.
// Default: the system clock
Clock Clock
}
RetryPolicy configures how failed jobs are retried. Retries run inline on the worker; errors are retried unless wrapped with Permanent or the job's context is done, and panics are never retried. A job that stops retrying fails with a *RetryError.
A graceful Shutdown lets retries finish; a forced one aborts backoff waits.
type ShutdownError ¶ added in v0.1.0
type ShutdownError struct {
// Dropped is the number of queued jobs that were discarded without running
Dropped int
// Err is the error of Shutdown's context
Err error
}
ShutdownError is returned by Shutdown when its context ended before the queue drained. errors.Is matches the context's error.
func (*ShutdownError) Error ¶ added in v0.1.0
func (e *ShutdownError) Error() string
func (*ShutdownError) Unwrap ¶ added in v0.1.0
func (e *ShutdownError) Unwrap() error
Unwrap returns the context's error
type Status ¶ added in v0.1.0
type Status string
Status describes how a task finished. The same values are used for the "status" metric label and log attribute.
const ( // StatusSuccess means the job returned nil StatusSuccess Status = "success" // StatusError means the job returned an error while its context was still active StatusError Status = "error" // StatusPanic means the job panicked; the error is a *PanicError StatusPanic Status = "panic" // StatusRetriesExhausted means the job failed on every allowed attempt StatusRetriesExhausted Status = "retries_exhausted" // StatusBudgetExceeded means retrying stopped because of MaxElapsedTime StatusBudgetExceeded Status = "budget_exceeded" // StatusPermanent means the job returned an error wrapped with Permanent, // with or without retries StatusPermanent Status = "permanent" // StatusShutdown means a forced pool shutdown aborted retrying, or dropped // the task from the queue before it ran (Attempts is 0) StatusShutdown Status = "shutdown" // StatusCanceled means the job failed after its context was done StatusCanceled Status = "canceled" )
type Task ¶ added in v0.1.0
type Task struct {
// ID identifies a single task (e.g. an order or request ID). It appears in
// logs and TaskResult only, never in metric labels, so it may be unique.
ID string
// Kind groups tasks of the same type (e.g. "send_email") and is used as a
// metric label. Keep the set of kinds small and bounded.
Kind string
// Run is the work to execute.
Run Job
// Context is the context Run executes with. When nil, the ctx passed to
// SubmitTask is used for both waiting for queue space and execution. Set it
// so a submit deadline doesn't cancel the job; see ExamplePool_SubmitTask.
Context context.Context
}
Task is a Job annotated with identity used for observability.
type TaskResult ¶ added in v0.1.0
type TaskResult struct {
ID string
Kind string
// QueueWait is the time between submission and the start of execution
QueueWait time.Duration
// Duration is the total execution time, including any retries
Duration time.Duration
// Attempts is the number of times the job was run
Attempts int
Status Status
// Err is the final error, nil on success
Err error
}
TaskResult describes how a task finished. It is passed to the OnTaskDone hook.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
batch_processor
command
Package main provides a batch processing example using the adaptive pool.
|
Package main provides a batch processing example using the adaptive pool. |
|
comparison
command
Package main provides a simple benchmark comparison tool for the adaptive pool.
|
Package main provides a simple benchmark comparison tool for the adaptive pool. |
|
http_server
command
Package main provides an HTTP server example using the adaptive pool.
|
Package main provides an HTTP server example using the adaptive pool. |
|
one_million_simulator/naive
command
Package main provides a naive simulator that spawns a goroutine for every task.
|
Package main provides a naive simulator that spawns a goroutine for every task. |
|
one_million_simulator/with_pool
command
Package main provides a 1 Million RPS simulator to demonstrate the adaptive pool's behavior.
|
Package main provides a 1 Million RPS simulator to demonstrate the adaptive pool's behavior. |
|
otelrecorder
module
|
|
|
promrecorder
module
|