mirror of
https://github.com/acaloiaro/neoq
synced 2026-07-21 18:29:08 +00:00
132 lines
3.2 KiB
Go
132 lines
3.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/acaloiaro/neoq/internal"
|
|
"github.com/acaloiaro/neoq/jobs"
|
|
)
|
|
|
|
const (
|
|
DefaultHandlerDeadline = 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
|
|
Deadline 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)
|
|
}
|
|
}
|
|
|
|
// Deadline configures handlers with a time deadline for every executed job
|
|
// The deadline is the amount of time that can be spent executing the handler's Func
|
|
// when a deadline is exceeded, the job is failed and enters its retry phase
|
|
func Deadline(d time.Duration) Option {
|
|
return func(h *Handler) {
|
|
h.Deadline = 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 one fewer threads than CPUs
|
|
if h.Concurrency == 0 {
|
|
Concurrency(runtime.NumCPU() - 1)(&h)
|
|
}
|
|
|
|
// always set a job deadline if none is set
|
|
if h.Deadline == 0 {
|
|
Deadline(DefaultHandlerDeadline)(&h)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Exec executes handler functions with a concrete time deadline
|
|
func Exec(ctx context.Context, handler Handler) (err error) {
|
|
deadlineCtx, cancel := context.WithDeadline(ctx, time.Now().Add(handler.Deadline))
|
|
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 <-deadlineCtx.Done():
|
|
ctxErr := deadlineCtx.Err()
|
|
if errors.Is(ctxErr, context.DeadlineExceeded) {
|
|
err = fmt.Errorf("job exceeded its %s deadline: %w", handler.Deadline, ctxErr)
|
|
} else if errors.Is(ctxErr, context.Canceled) {
|
|
err = ctxErr
|
|
} else {
|
|
err = fmt.Errorf("job failed to process: %w", ctxErr)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// JobFromContext fetches the job from a context if the job context variable is already set
|
|
func JobFromContext(ctx context.Context) (j *jobs.Job, err error) {
|
|
var ok bool
|
|
if j, ok = ctx.Value(internal.JobCtxVarKey).(*jobs.Job); ok {
|
|
return
|
|
}
|
|
|
|
return nil, ErrContextHasNoJob
|
|
}
|