River’s design makes it safe to rename job argument and worker types, but changing job Kind strings takes a few extra steps to perform safely.
Job kinds as strings
The Kind function of a job args class produces an identifier under which jobs of that type are stored to the database:
type MyArgs struct {}
func (MyArgs) Kind() string { return "stable_kind" }Inserting MyArgs would create a record in the river_job table with kind set to "stable_kind".
When working a job, River sends it to the worker registered for the kind. Renaming the Go types MyArgs and MyWorker is always safe because regardless of the new name they’re assigned, their Kind string stays the same, so River still knows where to send jobs.
Return an explicit kind from the worker class so renaming the class does not change the kind stored in the database:
class MyWorker def self.kind = "stable_kind"
def work(job) # ... endendThe class can be renamed while kind continues returning "stable_kind".
Set an explicit kind in the derive attribute to keep the stored job kind independent of the args struct’s name:
#[derive(Clone, Debug, serde::Deserialize, riverqueue::JobArgs, serde::Serialize)]#[river(kind = "stable_kind")]struct MyArgs {}The Rust type name can change without changing its kind attribute.
Give defineJob a stable kind string. The variable holding the definition can then be renamed without changing queued jobs:
const myJob = defineJob({ kind: "stable_kind" });The variable name can change without changing the persisted kind.
Existing jobs make renames unsafe
Changing a persisted kind leaves existing jobs addressed to the old worker registration. If that registration disappears, the client cannot work those jobs. The same issue applies to producers in another language that still insert the old kind.
With no existing jobs and no producers using the old name, it is safe to rename the kind directly. Otherwise, retain compatibility while the backlog and deployed applications transition.
Safely renaming job kinds
First deploy consumers that accept both old and new kinds, keeping producers on the old kind until those consumers are available everywhere. Then switch producers to the new kind. Remove the old alias only after no producer uses it and no unfinished old jobs remain. This includes scheduled jobs and retries, which can last about three weeks by default.
Job kinds can be renamed with existing jobs outstanding, but care must be taken to avoid accidental data loss.
Renaming a kind is a three step process involving the JobArgsWithKindAliases interface:
Keep the original kind and add the future name as an alias. Deploy this compatibility version to every consumer before switching producers:
type jobArgsBeingRenamed struct{}func (a jobArgsBeingRenamed) Kind() string { return "old_name" }func (a jobArgsBeingRenamed) KindAliases() []string { return []string{"new_name"} }Change the job’s
Kindto its new target name, with aKindAliasesimplementation which retains the old name, and deploy:type jobArgsBeingRenamed struct{}func (a jobArgsBeingRenamed) Kind() string { return "new_name" }func (a jobArgsBeingRenamed) KindAliases() []string { return []string{"old_name"} }New jobs are inserted as
"new_name". Existing jobs still identify as"old_name". River happily dispatches either to the appropriate worker.After all jobs inserted under
"old_name"are complete, removeKindAliases, and deploy:type jobArgsBeingRenamed struct{}func (a jobArgsBeingRenamed) Kind() string { return "new_name" }
Jobs under "old_name" may have previously errored and still be queued for eventual retry. Under the default retry policy, it’d take a full three weeks for a chronically failing job to be fully moved to discarded, and during that time they may run with "old_name".
Register the old kind as an alias for the renamed worker so it can handle jobs inserted under either name:
class NewWorker def self.kind = "new_name"
def work(job) # Work either old_name or new_name arguments. endend
workers = River::Workers.new.add(NewWorker, aliases: [:old_name])Keep the old kind in aliases while using the new kind for insertions:
#[derive(Clone, Debug, serde::Deserialize, riverqueue::JobArgs, serde::Serialize)]#[river(kind = "new_name", aliases("old_name"))]struct JobArgsBeingRenamed {}Add the previous kind to kindAliases so the registered handler accepts both old and new jobs:
const renamedJob = defineJob({ kind: "new_name", kindAliases: ["old_name"] });workers.add(renamedJob, handler);Querying or updating kind aliases
To confirm whether or not it’s safe to remove an old alias, query for jobs of a particular kind that are still eligible to be worked or retried:
SELECT count(*)FROM river_jobWHERE kind = 'old_name' AND finalized_at IS NULL;Alternatively, the process of waiting for an old kind to turn over can be expedited by updating the kind of all non-finalized (i.e those not completed or discarded) job rows:
UPDATE river_jobSET kind = 'new_name'WHERE kind = 'old_name' AND finalized_at IS NULL;The usual caveat applies in that this may be an expensive operation for enormous jobs tables, so run it with care. Depending on size, it may be advisable to update in limited batches to avoid long running queries:
WITH rename_batch AS ( SELECT * FROM river_job WHERE kind = 'old_name' AND finalized_at IS NULL LIMIT 1000)UPDATE river_jobSET kind = 'new_name'WHERE id IN ( SELECT id FROM rename_batch);