mirror of
https://github.com/acaloiaro/neoq
synced 2026-07-21 10:12:18 +00:00
119 lines
2.8 KiB
Go
119 lines
2.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
DefaultHandlerTimeout = 30 * time.Second
|
|
)
|
|
|
|
var (
|
|
ErrContextHasNoJob = errors.New("context has no Job")
|
|
ErrNoHandlerForQueue = errors.New("no handler for queue")
|
|
ErrNoProcessorForQueue = errors.New("no processor configured for queue")
|
|
)
|
|
|
|
// Func is a function that Handlers execute for every Job on a queue
|
|
type Func func(ctx context.Context) error
|
|
|
|
// Handler handles jobs on a queue
|
|
type Handler struct {
|
|
Handle Func
|
|
Concurrency int
|
|
JobTimeout time.Duration
|
|
QueueCapacity int64
|
|
}
|
|
|
|
// Option is function that sets optional configuration for Handlers
|
|
type Option func(w *Handler)
|
|
|
|
// WithOptions sets one or more options on handler
|
|
func (h *Handler) WithOptions(opts ...Option) {
|
|
for _, opt := range opts {
|
|
opt(h)
|
|
}
|
|
}
|
|
|
|
// JobTimeout configures handlers with a time deadline for every executed job
|
|
// The timeout is the amount of time that can be spent executing the handler's Func
|
|
// when a timeout is exceeded, the job fails and enters its retry phase
|
|
func JobTimeout(d time.Duration) Option {
|
|
return func(h *Handler) {
|
|
h.JobTimeout = d
|
|
}
|
|
}
|
|
|
|
// Concurrency configures Neoq handlers to process jobs concurrently
|
|
// the default concurrency is the number of (v)CPUs on the machine running Neoq
|
|
func Concurrency(c int) Option {
|
|
return func(h *Handler) {
|
|
h.Concurrency = c
|
|
}
|
|
}
|
|
|
|
// MaxQueueCapacity configures Handlers to enforce a maximum capacity on the queues that it handles
|
|
// queues that have reached capacity cause Enqueue() to block until the queue is below capacity
|
|
func MaxQueueCapacity(capacity int64) Option {
|
|
return func(h *Handler) {
|
|
h.QueueCapacity = capacity
|
|
}
|
|
}
|
|
|
|
// New creates a new queue handler
|
|
func New(f Func, opts ...Option) (h Handler) {
|
|
h = Handler{
|
|
Handle: f,
|
|
}
|
|
|
|
h.WithOptions(opts...)
|
|
|
|
// default to running on as many goroutines as there are CPUs
|
|
if h.Concurrency == 0 {
|
|
Concurrency(runtime.NumCPU())(&h)
|
|
}
|
|
|
|
// always set a job timeout if none is set
|
|
if h.JobTimeout == 0 {
|
|
JobTimeout(DefaultHandlerTimeout)(&h)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Exec executes handler functions with a concrete timeout
|
|
func Exec(ctx context.Context, handler Handler) (err error) {
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, handler.JobTimeout)
|
|
defer cancel()
|
|
|
|
var errCh = make(chan error, 1)
|
|
var done = make(chan bool)
|
|
go func(ctx context.Context) {
|
|
errCh <- handler.Handle(ctx)
|
|
done <- true
|
|
}(ctx)
|
|
|
|
select {
|
|
case <-done:
|
|
err = <-errCh
|
|
if err != nil {
|
|
err = fmt.Errorf("job failed to process: %w", err)
|
|
}
|
|
|
|
case <-timeoutCtx.Done():
|
|
ctxErr := timeoutCtx.Err()
|
|
if errors.Is(ctxErr, context.DeadlineExceeded) {
|
|
err = fmt.Errorf("job exceeded its %s timeout: %w", handler.JobTimeout, ctxErr)
|
|
} else if errors.Is(ctxErr, context.Canceled) {
|
|
err = ctxErr
|
|
} else {
|
|
err = fmt.Errorf("job failed to process: %w", ctxErr)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|