Hooks are functions that can be injected into the job lifecycle in various places, extending River's core functionality with custom code. Hooks are a similar concept to middleware, except their invocations finish immediately instead of wrapping an inner call.
rivertype.Hook
is a trivial interface implemented by embedding river.HookDefaults
:
type logHook struct { river.HookDefaults}
Hook operations
Hooks have no effect until they implement one or more of hook operation interfaces. The hook above could be made to log on job inserts by implementing rivertype.HookInsertBegin
:
func (*logHook) InsertBegin(ctx context.Context, params *JobInsertParams) error { fmt.Printf("inserting job with kind %q\n", params.Kind) return nil}
While InsertBegin
only logs in this example, it's also allowed to use its params
pointer to modify job insert parameters before insertion takes place.
logHook
could be extended to also log on job work by implementing rivertype.HookWorkBegin
:
func (*logHook) WorkBegin(ctx context.Context, job *JobRow) error { fmt.Printf("working job with kind %q\n", job.Kind) return nil}
List of all hook operations
Full list of hook operations interfaces:
rivertype.HookInsertBegin
: Invoked before a job is inserted.rivertype.HookWorkBegin
: Invoked before a job is worked.
Configuring hooks
A global set of hooks that run for every job are configurable on a River client:
riverClient, err := river.NewClient(riverpgxv5.New(dbPool), &river.Config{ // Order is significant. Hooks: []rivertype.Hook{ &BothInsertAndWorkBeginHook{}, &InsertBeginHook{}, &WorkBeginHook{}, },})
The effect of each hook in the list will depend on the operation interfaces it implements. For example, implementing rivertype.HookInsertBegin
will invoke the hook before a job is inserted, and implementing rivertype.HookWorkBegin
will invoke it before a job is worked. Hooks may implement multiple operations. Hooks implementing no operations will be have no functional effect.
Order in the list is significant, with hooks that appear first running before hooks that appear later.
Per-job hooks
Job args may implement JobArgsWithHooks
to provide hooks for their specific job kind:
type JobWithHooksArgs struct{}
func (JobWithHooksArgs) Kind() string { return "job_with_hooks" }
func (JobWithHooksArgs) Hooks() []rivertype.Hook { // Order is significant. return []rivertype.Hook{ &JobWithHooksBothInsertAndWorkBeginHook{}, &JobWithHooksInsertBeginHook{}, &JobWithHooksWorkBeginHook{}, }}
Hooks()
is only invoked once the first time a job is inserted or worked and from then on its value is memoized. In observance of this behavior, hooks should not vary based on job arg contents.
See the JobArgsHooks
example for complete code.
Testing interface compliance
Each configured hook is checked against the hook operation interfaces before being run, and because the trivial nature of rivertype.Hook
provides little in the way of type safety, it's an easy mistake to make to not have implemented a desired operation quite right (e.g. a return value is accidentally left off). To protect against this possibility, it's recommended that interface compliance is checked in code using a trivial assignment:
var ( _ rivertype.HookInsertBegin = &logHook{} _ rivertype.HookWorkBegin = &logHook{})
logHook
unexpectedly failing to implement rivertype.HookInsertBegin
would be caught early because it'd cause a compilation failure.
Differences from middleware
Hooks are a similar concept to middleware except that they're invoked and finish immediately instead of wrapping an inner call. This leads to some important considerations for their use:
- It's useless for them to modify context because any changes are popped right back off the stack again, making them unsuitable for uses where context needs to last for the duration of an operation. For example, OpenTelemetry traces are added to context, and would therefore have to be implemented in a middleware instead.
- Similarly, they're not suitable for anything else that'd require doing work around an operation, like timing how long it took to occur.
- Because they return immediately, they can operate more granularly than middleware. Hooks provide
InsertBegin
, which is invoked for every inserted job. Middleware providesInsertMany
, which is invoked for every inserted job batch. - Because they return immediately, they don't accumulate an extra frame to the stack. This has the benefit in keeping stack traces shallower, making them easier to read and reason about.
Because they operate more granulary and don't go on the stack, generally prefer the use of hooks over middleware, and fall back to middleware in cases where hooks are too restrictive.