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.
Execute is the process entry point for Fx options, an Application or a configured App. It uses a background context and exits after cleanup without adding logs. Caller defers do not run on normal exit. Use the manual App API to inspect errors. WithAssignment adds cluster selection after CLI and before Fx construction, delegating ownership policy to job/assignment. A skipped cluster exits with zero without running constructors, lifecycle hooks, work or result handlers.
For manual one-shot lifecycle, MakeApp accepts Fx options and New accepts an Application such as *confx.App. Both are lazy. App.Start handles CLI before constructing Fx, checks optional assignment, 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.
Deprecated: Use github.com/uchaloop/beatfx/jobfx instead.
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") )
var ( // ErrAssignmentSourceIsNil rejects an explicitly configured nil source. ErrAssignmentSourceIsNil = errors.New("assignment source is nil") // ErrAssignmentAlreadySet rejects multiple assignment settings. ErrAssignmentAlreadySet = errors.New("assignment is already configured") )
Functions ¶
func Execute ¶ added in v0.2.1
func Execute(inputs ...any)
Execute owns a one-shot process and exits after cleanup. Pass either one or more fx.Option values, one Application (such as *confx.App), or one unused *App with custom settings. ExecuteOption values may accompany any mode. Mixed composition modes, empty composition and nil inputs exit with code 1 before startup. Assignment settings are validated after CLI. To expand []fx.Option, pass fx.Options(options...).
Execute uses context.Background and the App's signal handling. Successful CLI, skipped clusters and successful work exit with code 0. Invalid input, assignment/startup errors and unsuccessful work exit with code 1. Cleanup errors do not change that code. Execute adds no logs and intentionally discards cleanup errors; use Start/Stop to inspect errors. Configured Fx loggers and application handlers still run.
Cleanup completes before os.Exit. Caller defers do not run on normal exit. User panics propagate after deferred cleanup; Execute does not recover them. Execute is an explicitly agreed process-entry exception to the no-exit rule.
func Module ¶
Module copies the input option slice; referenced option state remains shared. It 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 lazy cluster selection before any Fx construction. 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 lazy cluster selection before any Fx construction.
// 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)
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,
).WithAssignment(func(ctx context.Context) (assignment.Config, assignment.Invocation, error) {
if err := ctx.Err(); err != nil {
return assignment.Config{}, assignment.Invocation{}, err
}
return assignment.Config{
Clusters: []string{"el", "xc", "dm"},
Current: "el",
Period: 4 * time.Hour,
}, assignment.Invocation{ScheduledAt: scheduledAt}, nil
})
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, then optional assignment before constructing Fx. A non-owner returns a decision without invoking runtime constructors or handlers. The owner 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/assignment/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) WithAssignment ¶ added in v0.2.1
func (a *App) WithAssignment(source AssignmentSource) *App
WithAssignment returns an independent unused App sharing the source callback. Repeated assignment settings are rejected after CLI, rather than overwritten. Cloning an already used App defers ErrAppUsed to Start, as WithConfig does.
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 AssignmentSource ¶ added in v0.2.1
type AssignmentSource func(context.Context) (assignment.Config, assignment.Invocation, error)
AssignmentSource supplies bootstrap parameters before Fx is constructed. It runs only after CLI handling and must honor ctx cancellation. It must not depend on the main Fx graph. ScheduledAt must identify the original scheduled invocation, including on retries; do not replace it with the current time. Source closures are shared by cloned Apps and must support that reuse. No source timeout or signal subscription is installed before Fx; sources must bound their own I/O. Execute supplies a background context during this phase.
type ExecuteOption ¶ added in v0.2.1
type ExecuteOption interface {
// contains filtered or unexported methods
}
ExecuteOption configures the App used by Execute, independently of Fx options. Options may accompany one Application, one App, or a list of fx.Option values.
func WithAssignment ¶ added in v0.2.1
func WithAssignment(source AssignmentSource) ExecuteOption
WithAssignment enables lazy cluster selection. Nil sources and duplicate settings fail after CLI handling, so help/describe remain available. This selects a cluster, not one replica; it provides no locking or failover.
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.
type Result ¶ added in v0.2.0
type Result struct {
IsCLIHandled bool
Assignment *assignment.Decision
Job *job.Result
}
Result distinguishes a completed CLI request from an attempted job. Assignment is nil when selection is disabled or CLI handled the request. Job is nil for CLI, skipped clusters, assignment errors and startup errors.