Documentation
¶
Overview ¶
Package job runs one attempt at a piece of work and reports how it went.
It is the execution half of a scheduled job, with no opinion about when the next attempt happens. A short-lived process started by a Kubernetes CronJob runs one attempt and exits; github.com/uchaloop/beat runs the same work repeatedly inside a long-lived process. Both use the same Runner, so the work, its middleware, its timeout and its classification are written once.
runner, err := job.MakeRunner(
job.Config{Timeout: 3 * time.Minute},
func(ctx context.Context) (int64, error) {
return queue.ProcessBatch(ctx, 1000)
},
job.WithMiddleware(recovery.Middleware()),
)
result := runner.Run(ctx)
What one attempt means ¶
Runner.Run calls the chain at most once, synchronously, and waits for it to return. It never retries, never starts a goroutine of its own and never calls a Handler: the caller decides what to do with the Result and when to try again. Two attempts never overlap because the caller does not overlap them.
The timeout is cooperative ¶
Config.Timeout bounds the work context. Like every deadline in Go it cancels; it cannot interrupt a running function. A Func must return when its context is done, and must wait for its own goroutines first. One that ignores cancellation holds its caller for as long as it likes, and no Result is reported until it returns.
Result.Outcome, not Result.Err, says whether an attempt succeeded: a Func cut short by the deadline may still return a nil error, and that is not a success. The runner classifies the attempt before releasing its own context, so a deadline or a cancellation is visible even when the work stayed quiet about it.
Error processing ¶
WithErrorHandler registers an optional callback for non-nil errors returned by the Func and middleware. It runs once after the work returns, with caller context values but a fresh ErrorHandlerTimeout budget (default one minute). Cancellation of the work or caller does not cancel error processing. Already-cancelled input skips both stages; a nil work error never calls it.
Result.Err and Outcome describe the work. ErrorHandlerErr separately reports callback failure or deadline expiration, even if the callback returns nil. Duration includes both stages; WorkDuration and ErrorHandlerDuration separate them. Observability Handler delivery remains the caller's responsibility. Shutdown budgets must allow error processing before closing dependencies. A noncooperative callback still blocks Run; panics propagate to the caller.
Panics ¶
The runner does not recover them. A panic propagates to whoever called Run - the honest default for work that misbehaved. To turn one into a PanicError on the Result instead, add job/middleware/recovery.
Choosing which process runs the work ¶
job/assignment is an optional, pure policy that picks one cluster to own a scheduled point when the same deployment runs in several of them. It is off unless an application asks for it, and it decides nothing about execution.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Timeout bounds the work and middleware. Zero defaults to one minute;
// negative values are invalid.
//
// The bound is cooperative: it cancels the attempt's context and cannot
// interrupt the function. A Func that ignores cancellation holds its caller
// for as long as it likes, and no outcome is reported until it returns.
Timeout time.Duration `env:"TIMEOUT" envDescription:"Cooperative timeout for work and middleware; zero uses one minute."`
// ErrorHandlerTimeout bounds error processing after the work returns.
// Zero defaults to one minute; negative values are invalid. The handler's
// context retains caller values but not its deadline or cancellation.
// This budget is additional to Timeout and matters during shutdown too.
ErrorHandlerTimeout time.Duration `env:"ERROR_HANDLER_TIMEOUT" envDescription:"Additional timeout for error handling after work returns; zero uses one minute."`
}
Config is the configuration job declares. The env tags are inert strings: an application loads them with github.com/uchaloop/confmaker and provides the filled Config. job itself never reads the environment. Register with the explicit instance name "job" to read JOB_*.
func (*Config) SetDefaults ¶
func (c *Config) SetDefaults()
SetDefaults establishes the values a deployment does not have to think about. confmaker calls it before the environment is applied, so a variable left unset keeps what is set here, and a generated .env.example carries the real default rather than a blank.
A Config built in Go by hand does not go through it, which is why MakeRunner still treats a zero Timeout as the default. Both paths apply one constant.
type ErrorHandler ¶ added in v0.2.0
ErrorHandler handles a non-nil error returned by the work and middleware. It runs synchronously with an independent, bounded context. It must respect cancellation. Its own error is recorded separately and is never handled again. Panics propagate; work recovery middleware does not cover this callback.
type Func ¶
Func is one attempt at the work. It reports how many items it processed - return 0 when the work is not item-oriented - and an error if the attempt failed. The count is expected to be non-negative; the runner preserves it as returned rather than validating or clamping it.
A Func must respect ctx: the runner bounds it with the configured timeout, and whoever owns the process cancels it on shutdown. The bound is cooperative, as every deadline in Go is: it cancels the context, it does not interrupt the running function. A Func must return when its context is done, and must wait for its own goroutines before it returns.
type Handler ¶
Handler receives a Result after an attempt. It is the observability seam of job: metrics, logging and tracing are built on top of it, so the package depends on none of them.
The Runner never calls a Handler. Whoever owns the attempt delivers its Result exactly once - a one-shot process straight from Run, a scheduler through its own richer record - so there is no hidden second delivery.
func MultiHandler ¶
MultiHandler returns a Handler that passes each Result to every handler in turn, skipping nil ones. Handlers run in order and are not isolated: a panic in one propagates to the caller, so keep them panic-free.
type HandlerFunc ¶
HandlerFunc adapts an ordinary function to a Handler, like http.HandlerFunc.
type Middleware ¶
Middleware wraps a Func to add behaviour around it - recovery, context enrichment, extra logging. It is classic Go composition: a Middleware receives the next Func and returns a Func that calls it.
A Middleware must keep the one-attempt contract: it may suppress the call to next, but must not call it twice or concurrently. Deciding when the next attempt happens is the caller's business, not a Middleware's.
type Option ¶
type Option interface {
// contains filtered or unexported methods
}
Option customizes a Runner built by MakeRunner.
func WithErrorHandler ¶ added in v0.2.0
func WithErrorHandler(handler ErrorHandler) Option
WithErrorHandler sets the optional error callback. A nil callback disables it. It is not called for skipped work or a nil work error, even on timeout.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/uchaloop/job"
)
func main() {
failure := errors.New("batch rejected")
handled := false
runner, err := job.MakeRunner(
job.Config{Timeout: time.Minute, ErrorHandlerTimeout: 10 * time.Second},
func(context.Context) (int64, error) { return 3, failure },
job.WithErrorHandler(func(ctx context.Context, err error) error {
// Replace with application-owned DLQ persistence or additional logging.
handled = errors.Is(err, failure)
return ctx.Err()
}),
)
if err != nil {
panic(err)
}
result := runner.Run(context.Background())
fmt.Println(result.Processed, result.Outcome, handled, result.ErrorHandlerErr)
}
Output: 3 error true <nil>
func WithMiddleware ¶
func WithMiddleware(mw ...Middleware) Option
WithMiddleware wraps the work. The first middleware is the outermost layer and therefore runs first, which is the order the call reads in. Nil entries are skipped, and the chain is composed once, when the Runner is built.
type Outcome ¶
type Outcome string
Outcome classifies how an attempt ended. It is the one label an observability adapter needs; Result.Err carries the detail behind it.
const ( // OutcomeOK is an attempt that finished and returned no error. OutcomeOK Outcome = "ok" // OutcomeError is an attempt that returned an error of its own. OutcomeError Outcome = "error" // OutcomePanic is an attempt whose panic the recovery middleware turned // into a *PanicError. Without that middleware a panic never reaches a // Result - it propagates to the caller. OutcomePanic Outcome = "panic" // OutcomeTimeout means the attempt context deadline expired, whether set // by Config.Timeout or by an earlier caller deadline. It is reported // even when the Func swallowed the cancellation and returned no error, // because the runner checks the context before classifying the result. // Cancellation is cooperative; the result is produced only after return. OutcomeTimeout Outcome = "timeout" // OutcomeCanceled is an attempt cut off by the caller's context - a // shutdown, most often. It is not a failure of the work, so it carries its // own label rather than adding to the error count every time a process // stops. OutcomeCanceled Outcome = "canceled" )
type PanicError ¶
PanicError wraps a value recovered from a panic in the work. The recovery middleware returns it as the attempt's error, so a handler can tell a panic from an ordinary error - Result.Outcome reads OutcomePanic - and inspect the recovered value and its stack.
The runner does not recover panics; without the recovery middleware a panic propagates to whoever called Run.
func (*PanicError) Error ¶
func (e *PanicError) Error() string
type Result ¶
type Result struct {
// Start is the wall-clock time the attempt began. It is the zero Time when
// the caller's context was already done and the work was never called.
Start time.Time
// Duration covers the whole attempt, including ErrorHandler when called.
// Observability handlers are not part of it.
Duration time.Duration
// WorkDuration measures the Func and middleware only.
WorkDuration time.Duration
// ErrorHandlerCalled distinguishes an invoked callback from an absent one,
// including callbacks that finish without advancing the clock.
ErrorHandlerCalled bool
// ErrorHandlerDuration measures the error callback, or zero when not called.
ErrorHandlerDuration time.Duration
// ErrorHandlerErr contains the callback error joined with any expiration
// of its context. Err and Outcome always describe the original work.
ErrorHandlerErr error
// Processed is the number of items the Func reported, including partial
// progress on failure. Values should be non-negative; the runner preserves
// them unchanged. Callers are responsible for supplying valid counts.
Processed int64
// Err is the error the chain returned, or nil. It stays nil when a timeout
// or a cancellation cut a Func short that chose to report no error, which
// is why Outcome and not Err decides whether an attempt succeeded.
Err error
// Outcome classifies the attempt and is authoritative.
Outcome Outcome
}
Result describes one completed attempt.
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
Runner executes one attempt per call to Run. It holds the resolved configuration and the middleware chain, built once, so a caller that runs the same work repeatedly - beat, for instance - pays for the composition once.
A Runner keeps no state between attempts. Concurrent calls to Run are the caller's business: the runner serializes nothing, so they are safe only when the Func, its middleware and ErrorHandler are. Separate runners for logical jobs are simpler than one runner guarding shared state.
func MakeRunner ¶
MakeRunner resolves cfg against opts and builds a Runner. fn is required.
func (*Runner) Run ¶
Run performs one attempt and reports how it went.
It calls the chain at most once, synchronously, on the calling goroutine, and waits for it to return. There is no retry, no goroutine of its own and no delivery to an observability Handler: the caller owns the Result. An optional ErrorHandler processes a returned error before Run returns.
The work is bounded by Timeout, and error processing by ErrorHandlerTimeout. Both deadlines cancel contexts without interrupting execution. A caller's context that is already done skips the work entirely and reports why.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package assignment picks one cluster to own a scheduled point when the same deployment runs in several of them.
|
Package assignment picks one cluster to own a scheduled point when the same deployment runs in several of them. |
|
middleware
|
|
|
recovery
Package recovery provides a job.Middleware that recovers from a panic in the work and turns it into a *job.PanicError, so one bad attempt does not take the process with it.
|
Package recovery provides a job.Middleware that recovers from a panic in the work and turns it into a *job.PanicError, so one bad attempt does not take the process with it. |