You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Here, implement "resumable" jobs, which are jobs that can checkpoint
their progress so that in case they have to stop early, they're picked
up from a point that lets them skip work that's already been done. This
is especially useful for long running jobs that are at risk of being
interrupted from something like a deploy.
Here's roughly the shape of the API, with the same normal `Work`
function that all jobs implement, and with a series of `ResumableStep`
calls within, each of which take a name for the step and function
representing it:
func (w *ResumableWorker) Work(ctx context.Context, job *river.Job[ResumableArgs]) error {
river.ResumableStep(ctx, "step1", func(ctx context.Context) error {
fmt.Println("Step 1")
return nil
})
river.ResumableStep(ctx, "step2", func(ctx context.Context) error {
fmt.Println("Step 2")
return nil
})
river.ResumableStep(ctx, "step3", func(ctx context.Context) error {
fmt.Println("Step 3")
return nil
})
return nil
}
We also provide a cursor API for more granularity. This lets a step set
an arbitrary cursor value periodically as it's doing something like
looping over records in a set:
river.ResumableStepCursor(ctx, "process_ids", func(ctx context.Context, cursor ResumableCursor) error {
for _, id := range job.Args.IDs {
if id <= cursor.LastProcessedID {
continue
}
fmt.Printf("Processed %d\n", id)
if err := river.ResumableSetCursor(ctx, ResumableCursor{LastProcessedID: id}); err != nil {
return err
}
}
return nil
})
The function is `ResumableStepCursor[TCursor any]` where `TCursor` can
be defined arbitrarily by the user. This could be a simple scalar value
representing an ID, or a more complex `struct` value containing multiple
IDs, enabling nested loops that set inner and outer IDs at the same time.
`ResumableStep` and `ResumableStepCursor` steps can be freely
intermingled, and multiple `ResumableStepCursor` steps with different
cursor types are supported. Cursors must be JSON marshable because
they're stored to a job's metadata.
Lastly, we provide `ResumableSetStepTx` and `ResumableSetStepCursorTx`
for cases where a transaction guarantee is necessary. Normally,
resumable step and cursor are set as a job's being completed, but
there's a chance this is never called in case of sudden failure.
`ResumableSetStepTx` (and its cursor version) is available to durably
persist a step at the cost of an extra database operation similar to how
`JobCompleteTx` does the same for job completion.
One neat aspect the implementation here is that I was able to make it
entirely middleware-only. So all the resumable job logic goes in an
internal `resumableMiddleware` that's included in all clients by
default. This is kind of nice because it keeps its code highly modular
and will hopefully act as a template for future features.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
8
8
## [Unreleased]
9
9
10
+
### Added
11
+
12
+
- Added "resumable jobs" that can be broken down into multiple steps and with a step persisted after it finishes that lets them skip work that's already been done. This is particularly useful for long running jobs that may experience a cancellation (like in the event of a deploy) during the span of their run. [PR #1226](https://github.com/riverqueue/river/pull/1226).
0 commit comments