Skip to content

Hooks

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:

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 provides InsertMany, 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.