Documentation
¶
Overview ¶
Package jobfx connects job.Runner to Uber Fx. Applications using job directly remain independent of Fx.
Module provides a Runner from job.Config, job.Func and optional Options. It never runs work or installs a work OnStart hook. Schedulers and applications with their own lifecycle can keep using Module with ordinary fx.New.
For one-shot processes, MakeApp accepts Fx options and New accepts an Application such as *confx.App. Both are lazy. App.Start handles CLI before constructing Fx, starts dependencies, runs one job, then calls an optional job.Handler. The caller must defer App.Stop and decide when to exit the process. See ExampleMakeApp.
Startup, observation and cleanup default to 30, 10 and 30 seconds respectively; App.WithConfig replaces these settings. job.Config.Timeout still controls work. Observation and cleanup preserve context values with independent cancellation. All timeouts are cooperative: hooks, work and handlers must honor their contexts.
Normal completion calls Fx.Stop directly without Fx.Run or a synthetic shutdown signal. SIGINT, SIGTERM and Fx shutdown requests cancel startup/work. Business failures are recorded in Result.Job; CLI and startup errors are returned separately. Cleanup errors do not change Result.ExitCode, so failed telemetry cleanup does not automatically request a retry of successful business work.
Options is one ordered slice rather than an unordered Fx value group. Options passed to Module come first, then container options; middleware accumulates in that order, with the first wrapper outermost.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrAppUsed means Start or cloning was attempted after the first Start/Stop. ErrAppUsed = errors.New("application has already been used") // ErrAppBusy means Stop overlapped Start or another Stop. ErrAppBusy = errors.New("application is busy") // ErrApplicationIsNil rejects a nil or typed nil composition. ErrApplicationIsNil = errors.New("application is nil") // ErrContextIsNil rejects a nil Start context. ErrContextIsNil = errors.New("context is nil") // ErrInvalidTimeout rejects a negative lifecycle timeout. ErrInvalidTimeout = errors.New("timeout must not be negative") )
Functions ¶
func Module ¶
Module provides a *job.Runner built from the container. It runs nothing: see the package documentation for the sequence a one-shot process owns.
Options come from two places and both are applied, in one defined order: the static ones passed here first, then Options from the container, each in the order it was written. Static first is what puts a recovery middleware given to Module outside everything the container adds, which is where it can still catch a panic from those wrappers.
Types ¶
type App ¶ added in v0.2.0
type App struct {
// contains filtered or unexported fields
}
App owns one synchronous job attempt and its Fx dependencies. Do not copy it. Start may be called once. Stop must be deferred by the caller, even on errors or panics. Stop during Start returns ErrAppBusy; cancel the Start context to interrupt work, then Stop after Start returns. Concurrent Stop also returns ErrAppBusy. Sequential Stop calls return the original cleanup error. The zero value rejects Start with ErrApplicationIsNil. User panics propagate.
func MakeApp ¶ added in v0.2.0
MakeApp stores ordinary Fx options without CLI support. Options are copied; their referenced values remain caller-owned. Include Module explicitly.
Example ¶
ExampleMakeApp shows one job attempt after an optional ownership decision. App owns startup, work and observation; the caller defers cleanup and decides the process exit code. Work never runs inside an Fx hook.
package main
import (
"context"
"fmt"
"log/slog"
"time"
"go.uber.org/fx"
"github.com/uchaloop/job"
"github.com/uchaloop/job/assignment"
"github.com/uchaloop/job/middleware/recovery"
"github.com/uchaloop/jobfx"
)
// ExampleMakeApp shows one job attempt after an optional ownership decision.
// App owns startup, work and observation; the caller defers cleanup and decides
// the process exit code. Work never runs inside an Fx hook.
func main() {
fmt.Println("exit code:", runOnce())
}
func runOnce() int {
// The launcher passes the scheduled point in - a Kubernetes CronJob through
// its annotation, a Linux wrapper through an argument. A pod that starts
// late must not take it from the clock, or it would decide as a later point
// than the one it was meant to serve.
scheduledAt := time.Date(2026, 9, 19, 4, 0, 0, 0, time.UTC)
rotation, err := assignment.MakeRotation(assignment.Config{
Clusters: []string{"el", "xc", "dm"},
Current: "el",
Period: 4 * time.Hour,
})
if err != nil {
slog.Error("configure rotation", "error", err)
return 1
}
decision, err := rotation.Decide(assignment.Invocation{ScheduledAt: scheduledAt})
if err != nil {
slog.Error("decide owner", "error", err)
return 1
}
if !decision.IsExecutable {
// Another cluster owns this point. Nothing ran, and that is a clean
// finish - the decision itself is what observability records, because
// no Result will ever be produced for it.
slog.Info("point belongs elsewhere", "slot", decision.Slot, "owner", decision.Owner)
return 0
}
app := jobfx.MakeApp(
fx.Supply(job.Config{Timeout: 4 * time.Minute}),
fx.Supply(slog.Default()),
fx.Provide(func() job.Func {
return func(ctx context.Context) (int64, error) {
// One bounded batch of application work.
return 3, ctx.Err()
}
}),
fx.Provide(func(log *slog.Logger) jobfx.Options {
return jobfx.Options{
job.WithMiddleware(recovery.MakeMiddleware(recovery.WithLogger(log))),
}
}),
jobfx.Module(),
fx.Provide(func(log *slog.Logger) job.Handler {
return job.HandlerFunc(func(_ context.Context, result job.Result) {
log.Info("attempt completed",
"outcome", result.Outcome,
"processed", result.Processed,
"duration", result.Duration,
)
})
}),
fx.NopLogger,
)
defer func() {
if err := app.Stop(); err != nil {
slog.Error("stop dependencies", "error", err)
}
}()
result, err := app.Start(context.Background())
if err != nil {
slog.Error("start application", "error", err)
return 1
}
return result.ExitCode()
}
Output: exit code: 0
func New ¶ added in v0.2.0
func New(application Application) *App
New stores a lazy application. It does not parse CLI, build Fx or load config. Nil applications are rejected at Start. New is an agreed naming exception. Include Module in the composition exactly once; it is not added implicitly.
func (*App) Start ¶ added in v0.2.0
Start handles process CLI first, then starts dependencies, runs the job once and delivers its result to an optional job.Handler. Business failures are in Result.Job, not the returned error. CLI/build/start failures have no Job. Observation preserves context values but has an independent timeout. Start does not call os.Exit, Fx.Run or Shutdowner on normal job completion.
func (*App) Stop ¶ added in v0.2.0
Stop releases dependencies with an independent cleanup budget and joins the signal bridge. Stop before Start is safe and prevents subsequent Start. Cleanup errors are returned and cached; they do not alter the job outcome.
func (*App) WithConfig ¶ added in v0.2.0
WithConfig returns an independent unused App with replacement settings. The composition is shared. Cloning an already used App produces an App whose Start returns ErrAppUsed. Validation is deferred to Start to preserve laziness.
type AppConfig ¶ added in v0.2.0
type AppConfig struct {
StartupTimeout time.Duration
ObserveTimeout time.Duration
CleanupTimeout time.Duration
}
AppConfig controls lifecycle budgets, independently of job.Config.Timeout. Zero values select 30s startup, 10s observation and 30s cleanup. Negative values are rejected by Start. Cancellation remains cooperative.
type Application ¶ added in v0.2.0
type Application interface {
FX() fx.Option
HandleCLI(args []string, output io.Writer) (isHandled bool, err error)
}
Application supplies lazy composition and CLI handling. *confx.App implements this interface without jobfx depending on confx. Implementations must allow reuse when an unused App is cloned with WithConfig.
type Options ¶
Options is the ordered set of job options an application builds from the container. Provide it once, and the order inside it is the order applied:
fx.Provide(func(log *slog.Logger) jobfx.Options {
return jobfx.Options{job.WithMiddleware(recovery.MakeMiddleware(recovery.WithLogger(log)))}
})
A set that needs nothing from the container can be supplied outright with fx.Supply(jobfx.Options{...}), though such options can also go straight to Module.