Documentation
¶
Overview ¶
FILENAME: datastore.go
FILENAME: errors.go
FILENAME: job.go
FILENAME: lease.go
FILENAME: limiter.go
FILENAME: local_lease.go
FILENAME: local_store.go
FILENAME: options.go
FILENAME: redis_lease.go
FILENAME: redis_store.go
Index ¶
- Variables
- func RedisStateKey(limiterID string) string
- type Datastore
- type Job
- type Lease
- type LeaseDatastore
- type Limiter
- func (l *Limiter) QueueLen() int
- func (l *Limiter) Running() int
- func (l *Limiter) Schedule(task func() (interface{}, error)) (interface{}, error)
- func (l *Limiter) ScheduleContext(ctx context.Context, task func() (interface{}, error)) (interface{}, error)
- func (l *Limiter) ScheduleWithOptions(task func() (interface{}, error), priority, weight int) (interface{}, error)
- func (l *Limiter) ScheduleWithOptionsContext(ctx context.Context, task func() (interface{}, error), priority, weight int) (interface{}, error)
- func (l *Limiter) Stop() error
- func (l *Limiter) Wrap(fn func() (interface{}, error)) func() (interface{}, error)
- type LocalState
- type LocalStore
- func (ls *LocalStore) Acquire(_ context.Context, limiterID string, weight int, opts Options) (*Lease, time.Duration, error)
- func (ls *LocalStore) Disconnect() error
- func (ls *LocalStore) RegisterDone(limiterID string, weight int) error
- func (ls *LocalStore) Release(_ context.Context, lease *Lease) error
- func (ls *LocalStore) Renew(_ context.Context, lease *Lease) error
- func (ls *LocalStore) Request(limiterID string, weight int, opts Options) (canRun bool, waitTime time.Duration, err error)
- type Options
- type PanicError
- type PriorityQueue
- func (pq *PriorityQueue) IsEmpty() bool
- func (pq PriorityQueue) Len() int
- func (pq PriorityQueue) Less(i, j int) bool
- func (pq *PriorityQueue) Peek() *Job
- func (pq *PriorityQueue) Pop() interface{}
- func (pq *PriorityQueue) PopJob() *Job
- func (pq *PriorityQueue) Push(x interface{})
- func (pq *PriorityQueue) PushJob(job *Job)
- func (pq *PriorityQueue) Remove(job *Job) bool
- func (pq PriorityQueue) Swap(i, j int)
- type RedisKeyLayout
- type RedisStore
- func (rs *RedisStore) Acquire(ctx context.Context, limiterID string, weight int, opts Options) (*Lease, time.Duration, error)
- func (rs *RedisStore) Close() error
- func (rs *RedisStore) Disconnect() error
- func (rs *RedisStore) RegisterDone(limiterID string, weight int) error
- func (rs *RedisStore) Release(ctx context.Context, lease *Lease) error
- func (rs *RedisStore) Renew(ctx context.Context, lease *Lease) error
- func (rs *RedisStore) Request(limiterID string, weight int, opts Options) (canRun bool, waitTime time.Duration, err error)
- type SchedPolicy
Constants ¶
This section is empty.
Variables ¶
var ( // ErrStoreClosed is returned when attempting to use a closed store. ErrStoreClosed = errors.New("store is closed") // ErrMissingID is returned when a limiter ID is required but not provided. ErrMissingID = errors.New("limiter ID is required") // ErrInvalidWeight is returned when a job weight is invalid. ErrInvalidWeight = errors.New("job weight must be positive") // ErrWeightExceedsMax is returned when a job can never fit within the configured limit. ErrWeightExceedsMax = errors.New("job weight exceeds max concurrent limit") // ErrNilTask is returned when attempting to schedule a nil task. ErrNilTask = errors.New("task must not be nil") // ErrTaskPanic is returned when a scheduled task panics. ErrTaskPanic = errors.New("task panicked") // ErrInvalidID is returned when a limiter ID is malformed. ErrInvalidID = errors.New("limiter ID is invalid") // ErrInvalidMaxConcurrent is returned when MaxConcurrent is negative. Only // zero means unlimited; a negative value is a configuration mistake and // would otherwise silently disable the concurrency limit. ErrInvalidMaxConcurrent = errors.New("MaxConcurrent must not be negative") // ErrInvalidMinTime is returned when MinTime is negative. A negative value // would otherwise silently disable the minimum spacing between jobs. ErrInvalidMinTime = errors.New("MinTime must not be negative") // ErrInvalidMaxQueueSize is returned when MaxQueueSize is negative. Only // zero means unbounded. ErrInvalidMaxQueueSize = errors.New("MaxQueueSize must not be negative") // ErrInvalidRetryInterval is returned when RetryInterval is negative. ErrInvalidRetryInterval = errors.New("RetryInterval must not be negative") // ErrInvalidLeaseTTL is returned when LeaseTTL is negative. ErrInvalidLeaseTTL = errors.New("LeaseTTL must not be negative") // ErrInvalidSchedPolicy is returned when SchedPolicy is not a known policy. ErrInvalidSchedPolicy = errors.New("SchedPolicy is not a known scheduling policy") // ErrNilClient is returned when a Redis store is constructed without a // client. It unwraps to ErrStoreClosed, both because a store with no client // can never serve a request and so that code written against the previous // behavior — which returned ErrStoreClosed here — keeps working. ErrNilClient error = nilClientError{} // ErrQueueFull is returned when the queue has reached MaxQueueSize. ErrQueueFull = errors.New("limiter queue is full") // ErrLimiterConfigMismatch is returned by a LeaseDatastore when the // admission-relevant configuration supplied for a limiter ID disagrees with // the configuration already recorded for it. Sharing an ID with different // MaxConcurrent, MinTime or LeaseTTL values makes the effective distributed // policy depend on which process reaches the store first, so it is rejected // rather than silently resolved. Errors wrapping it carry both // configurations. ErrLimiterConfigMismatch = errors.New("limiter configuration does not match the configuration already registered for this ID") )
var ( // ErrLeaseLost is returned when renewing or releasing a lease that the // store no longer holds, because it expired or was already released. ErrLeaseLost = errors.New("lease is no longer held") // ErrNilLease is returned when a nil lease is passed to Renew or Release. ErrNilLease = errors.New("lease must not be nil") )
Lease errors.
Functions ¶
func RedisStateKey ¶ added in v1.1.0
RedisStateKey returns the key the legacy Request/RegisterDone path uses for a limiter ID. Like RedisKeys it is exported for operational inspection.
Types ¶
type Datastore ¶
type Datastore interface {
// Request checks if a job can run according to the limiter's rules.
// It must return whether the job can run now, and if not, a suggested wait time.
Request(limiterID string, weight int, opts Options) (canRun bool, waitTime time.Duration, err error)
// RegisterDone informs the store that a job has finished.
//
// It must not shorten the lifetime of state that Request sized to cover a
// MinTime window: spacing is measured from when a job started, so the record
// of that start has to outlive the job.
RegisterDone(limiterID string, weight int) error
// Disconnect cleans up any connections.
Disconnect() error
}
Datastore defines the interface for state management.
It is the original contract, built around a single shared counter, and it cannot express "this job is still running" — see LeaseDatastore, which the limiter prefers when a store implements it. The methods here take no context, so cancellation guarantees are weaker: the limiter can only wait for a Request or RegisterDone call to return.
type Job ¶
type Job struct {
Task func() (interface{}, error)
Priority int
Weight int
// contains filtered or unexported fields
}
Job represents a function to be executed by the Limiter.
type Lease ¶ added in v1.1.0
type Lease struct {
// Token uniquely identifies this reservation. Release and Renew act on the
// token, so a stale release from a job whose lease already expired cannot
// decrement a newer job's reservation.
Token string
// LimiterID is the limiter the capacity belongs to.
LimiterID string
// Weight is the capacity held.
Weight int
// TTL is the window each renewal grants. Renew reuses it so extending a
// lease never silently changes how long a crashed holder would keep the
// capacity.
TTL time.Duration
// ExpiresAt is when the store will reclaim this lease unless it is renewed.
// It is the store's clock, not the caller's.
ExpiresAt time.Time
}
Lease is a reservation of capacity held by one job. Unlike a shared counter, each lease is individually identified, so releasing one cannot disturb another, and an expired lease can be reclaimed without guessing how much weight it accounted for.
type LeaseDatastore ¶ added in v1.1.0
type LeaseDatastore interface {
Datastore
// Acquire reserves weight for limiterID. When capacity is unavailable it
// returns a nil lease and, if the wait is bounded (a MinTime window),
// how long to wait before retrying.
//
// A distributed implementation may require every instance sharing a
// limiterID to agree on the admission-relevant configuration —
// MaxConcurrent, MinTime and LeaseTTL — and report a disagreement as an
// error matching ErrLimiterConfigMismatch. RedisStore does; LocalStore has
// no other process to disagree with.
Acquire(ctx context.Context, limiterID string, weight int, opts Options) (lease *Lease, retryAfter time.Duration, err error)
// Renew extends a lease's expiry. It returns ErrLeaseLost if the lease has
// already expired or been released, which means the capacity has been
// handed to someone else and the caller is now over the limit.
//
// Renewal must not disturb rate-spacing history: MinTime is measured from
// when a job started, and that fact outlives the reservation.
Renew(ctx context.Context, lease *Lease) error
// Release returns a lease's capacity. Releasing an unknown or expired lease
// is not an error: the store has already reclaimed it, and reporting a
// failure would only invite a retry that cannot help.
//
// Like Renew, it must leave rate-spacing history alone.
Release(ctx context.Context, lease *Lease) error
}
LeaseDatastore is a Datastore that tracks individual reservations rather than a single shared counter.
A counter cannot express "this job is still running": the only way to keep state from leaking after a crash is to expire it, and expiring a counter while a job is still running lets another job start over the limit. Per-lease expiry with renewal separates "the holder is gone" from "the holder is slow".
A store may implement this alongside Datastore; the limiter uses the lease path when available and falls back to Request/RegisterDone otherwise.
Every method takes a context, and the limiter passes one that is cancelled when Limiter.Stop begins, so an implementation that blocks — on a network round trip, a lock, or a queue — must observe it. Cancellation guarantees are therefore stronger here than on the legacy Datastore methods, which take no context at all and can only be waited out.
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter manages job scheduling and rate limiting.
func NewLimiter ¶
NewLimiter creates a new Limiter instance.
func (*Limiter) QueueLen ¶ added in v1.1.0
QueueLen returns how many jobs are waiting for capacity. It is a point-in-time reading, useful for monitoring queue depth against Options.MaxQueueSize.
func (*Limiter) Running ¶ added in v1.1.0
Running returns the total weight of jobs currently executing. Unweighted jobs count as 1 each, so this is the job count in the common case.
func (*Limiter) ScheduleContext ¶ added in v1.1.0
func (l *Limiter) ScheduleContext(ctx context.Context, task func() (interface{}, error)) (interface{}, error)
ScheduleContext submits a job and blocks until it completes or ctx is done. If ctx ends while the job is still queued, the job is removed from the queue and ctx.Err() is returned; a job that has already started is left to run to completion, since the limiter cannot interrupt a task function.
func (*Limiter) ScheduleWithOptions ¶
func (l *Limiter) ScheduleWithOptions(task func() (interface{}, error), priority, weight int) (interface{}, error)
ScheduleWithOptions submits a job with custom priority and weight.
func (*Limiter) ScheduleWithOptionsContext ¶ added in v1.1.0
func (l *Limiter) ScheduleWithOptionsContext(ctx context.Context, task func() (interface{}, error), priority, weight int) (interface{}, error)
ScheduleWithOptionsContext is ScheduleContext with a custom priority and weight.
func (*Limiter) Stop ¶
Stop stops the limiter, cancels queued jobs and waits for running jobs to finish. It is safe to call concurrently and repeatedly: every caller blocks until shutdown has completed and receives the same error.
Shutdown cancels the context the limiter passes to a LeaseDatastore, so a store blocked inside Acquire or Renew is unblocked rather than holding Stop open. Releases are exempt and get a bounded context of their own: capacity still has to be handed back. A legacy Datastore takes no context, so its Request and RegisterDone calls can only be waited out.
The datastore is only disconnected if the limiter owns it (see Options.CloseDatastoreOnStop). An injected datastore stays usable so that other limiters, or other parts of the application sharing the same Redis client, are unaffected.
Stop must not be called from inside a scheduled task; doing so deadlocks because Stop waits for that task to finish.
type LocalState ¶
type LocalState struct {
// contains filtered or unexported fields
}
LocalState holds the state for a single limiter.
type LocalStore ¶
type LocalStore struct {
// contains filtered or unexported fields
}
LocalStore is an in-memory implementation of Datastore and LeaseDatastore.
func NewLocalStore ¶
func NewLocalStore() *LocalStore
NewLocalStore creates a new LocalStore instance.
func (*LocalStore) Acquire ¶ added in v1.1.0
func (ls *LocalStore) Acquire(_ context.Context, limiterID string, weight int, opts Options) (*Lease, time.Duration, error)
Acquire reserves capacity and returns a renewable lease. It implements LeaseDatastore with the same semantics as RedisStore, so switching between local and distributed mode does not change observable behavior.
func (*LocalStore) Disconnect ¶
func (ls *LocalStore) Disconnect() error
Disconnect cleans up any connections.
func (*LocalStore) RegisterDone ¶
func (ls *LocalStore) RegisterDone(limiterID string, weight int) error
RegisterDone informs the store that a job has finished.
func (*LocalStore) Release ¶ added in v1.1.0
func (ls *LocalStore) Release(_ context.Context, lease *Lease) error
Release returns a lease's capacity. It implements LeaseDatastore. Releasing an already-reclaimed lease succeeds: only this token is removed, so a stale release cannot disturb a newer holder.
type Options ¶
type Options struct {
ID string // A unique ID for the limiter, required for Redis mode.
MaxConcurrent int // Max number of jobs running at once. 0 means unlimited.
MinTime time.Duration // Minimum time between jobs. 0 means no spacing.
Datastore Datastore // Optional datastore for clustering. Defaults to local if nil.
// CloseDatastoreOnStop transfers ownership of an injected Datastore to the
// limiter, so Limiter.Stop disconnects it. It defaults to false because a
// datastore — and the Redis client inside it — is typically shared with
// other limiters and other parts of the application, and stopping one
// limiter must not break them. A datastore the limiter creates for itself
// is always closed on Stop regardless of this setting.
CloseDatastoreOnStop bool
// SchedPolicy controls how weighted jobs compete for capacity.
// Defaults to SchedStrict.
SchedPolicy SchedPolicy
// RetryInterval is how often the scheduler re-checks a distributed
// datastore that refused capacity. Defaults to 10ms. It has no effect on an
// idle limiter, which does not wake at all.
RetryInterval time.Duration
// MaxQueueSize caps how many jobs may wait in the queue. Scheduling beyond
// it returns ErrQueueFull, which keeps an overloaded producer from growing
// the queue without bound. 0 means unbounded.
MaxQueueSize int
// LeaseTTL is how long a capacity reservation survives without renewal,
// when the datastore implements LeaseDatastore. It bounds how long a
// crashed process can hold capacity; the limiter renews every LeaseTTL/3
// while a job runs, so a long-running job is not affected. Defaults to 30s,
// clamped to a 1s minimum.
LeaseTTL time.Duration
// OnError receives errors that have no caller to return them to — most
// importantly a failure to hand capacity back to the datastore, which
// otherwise leaves capacity reserved with no visibility. It is called from
// limiter goroutines, so it must be safe for concurrent use and must not
// block or call back into the limiter.
OnError func(error)
}
Options holds the configuration for a Limiter.
type PanicError ¶ added in v1.1.0
type PanicError struct {
// Value is whatever was passed to panic.
Value interface{}
// Stack is the stack trace captured where the panic was recovered.
Stack []byte
}
PanicError carries the value a task panicked with and the stack trace captured at the point of recovery, so a panic in a scheduled task can be diagnosed without the goroutine's stack being lost.
It matches errors.Is(err, ErrTaskPanic), so existing checks keep working:
if errors.Is(err, gothrottle.ErrTaskPanic) { ... }
Use errors.As to reach the stack:
var perr *gothrottle.PanicError
if errors.As(err, &perr) { log.Print(perr.Stack) }
func (*PanicError) Error ¶ added in v1.1.0
func (e *PanicError) Error() string
func (*PanicError) Unwrap ¶ added in v1.1.0
func (e *PanicError) Unwrap() error
Unwrap makes errors.Is(err, ErrTaskPanic) report true.
type PriorityQueue ¶
type PriorityQueue []*Job
PriorityQueue implements heap.Interface and holds Jobs.
func NewPriorityQueue ¶
func NewPriorityQueue() *PriorityQueue
NewPriorityQueue creates a new priority queue.
func (*PriorityQueue) IsEmpty ¶
func (pq *PriorityQueue) IsEmpty() bool
IsEmpty returns true if the queue is empty.
func (PriorityQueue) Len ¶
func (pq PriorityQueue) Len() int
func (PriorityQueue) Less ¶
func (pq PriorityQueue) Less(i, j int) bool
func (*PriorityQueue) Peek ¶ added in v1.1.0
func (pq *PriorityQueue) Peek() *Job
Peek returns the highest priority job without removing it, or nil when the queue is empty.
func (*PriorityQueue) Pop ¶
func (pq *PriorityQueue) Pop() interface{}
func (*PriorityQueue) PopJob ¶
func (pq *PriorityQueue) PopJob() *Job
PopJob removes and returns the highest priority job.
func (*PriorityQueue) Push ¶
func (pq *PriorityQueue) Push(x interface{})
func (*PriorityQueue) PushJob ¶
func (pq *PriorityQueue) PushJob(job *Job)
PushJob adds a job to the priority queue.
func (*PriorityQueue) Remove ¶ added in v1.1.0
func (pq *PriorityQueue) Remove(job *Job) bool
Remove takes a specific job out of the queue. It reports whether the job was still queued, which lets a caller distinguish "cancelled before it ran" from "already dispatched".
func (PriorityQueue) Swap ¶
func (pq PriorityQueue) Swap(i, j int)
type RedisKeyLayout ¶ added in v1.1.0
type RedisKeyLayout struct {
// Leases is a hash of lease token to reserved weight.
Leases string
// Expirations is a sorted set of lease token to expiry, in microseconds of
// Redis server time.
Expirations string
// LastStart holds the microsecond timestamp of the most recent admission,
// which is what MinTime spacing is measured from. It is written only on a
// successful acquisition and is absent when MinTime is zero.
LastStart string
// Config records the MaxConcurrent, MinTime and LeaseTTL every instance
// sharing this ID must agree on.
Config string
}
RedisKeyLayout names the Redis keys one limiter ID's lease state occupies. It is exported for operational use — inspecting live state, or clearing a limiter that will never run again — not because the layout is part of the throttling contract.
func RedisKeys ¶ added in v1.1.0
func RedisKeys(limiterID string) RedisKeyLayout
RedisKeys returns the keys RedisStore uses for a limiter ID.
All four share one Redis Cluster hash tag so the multi-key Lua scripts stay within a single hash slot. The tag is a hash of the limiter ID rather than the ID itself, because an ID containing braces would otherwise choose its own tag.
type RedisStore ¶
type RedisStore struct {
// contains filtered or unexported fields
}
RedisStore is a Redis-based implementation of Datastore and LeaseDatastore.
func NewRedisStore ¶
func NewRedisStore(client redis.UniversalClient) (*RedisStore, error)
NewRedisStore creates a new RedisStore instance.
The parameter is go-redis's UniversalClient, which *redis.Client satisfies, so existing call sites are unchanged. *redis.ClusterClient and *redis.Ring satisfy it too; see the package documentation on Redis Cluster for what is and is not supported there.
func (*RedisStore) Acquire ¶ added in v1.1.0
func (rs *RedisStore) Acquire(ctx context.Context, limiterID string, weight int, opts Options) (*Lease, time.Duration, error)
Acquire reserves capacity and returns a renewable lease. It implements LeaseDatastore.
Every instance sharing a limiter ID must supply the same MaxConcurrent, MinTime and LeaseTTL. The first acquisition records them; a later one that disagrees is refused with an error matching ErrLimiterConfigMismatch rather than silently applying whichever policy arrived last.
func (*RedisStore) Close ¶ added in v1.1.0
func (rs *RedisStore) Close() error
Close disconnects the store and additionally closes the underlying *redis.Client. Use it only when this store is the sole owner of the client; Disconnect leaves the client open for other users.
func (*RedisStore) Disconnect ¶
func (rs *RedisStore) Disconnect() error
Disconnect releases this store's resources. The *redis.Client passed to NewRedisStore was created by the caller and stays open: other stores, limiters or application components may still be using it. Callers close the client themselves when they are done with it.
func (*RedisStore) RegisterDone ¶
func (rs *RedisStore) RegisterDone(limiterID string, weight int) error
RegisterDone informs the store that a job has finished.
func (*RedisStore) Release ¶ added in v1.1.0
func (rs *RedisStore) Release(ctx context.Context, lease *Lease) error
Release returns a lease's capacity. It implements LeaseDatastore.
type SchedPolicy ¶ added in v1.1.0
type SchedPolicy int
SchedPolicy selects what the scheduler does when the highest priority queued job does not fit in the currently available capacity.
const ( // SchedStrict waits for the highest priority job to fit. A heavy job holds // the queue, so priority is never inverted, at the cost of leaving // capacity idle (head-of-line blocking). SchedStrict SchedPolicy = iota // SchedBestFit lets lighter, lower priority jobs use capacity the head job // cannot fill yet. Throughput improves, but a heavy high priority job can // be overtaken by lighter work. SchedBestFit )