Guide 6 of 12
Background jobs
Jobs are opt-in, but not a one-time choice: add River at new time with --jobs, or later, the moment you generate the first job.
bogie new blog --jobs
This runs River on the same Postgres the app already has: no Redis, a transactional enqueue, typed args. It adds a worker role, app/jobs, and g job.
Adding jobs to an app that has none
bogie g job NAME on an app made without --jobs adds jobs first, as if --jobs had been passed to new, then generates the job as usual:
bogie g job send_welcome
note this app has no app/jobs yet; adding --jobs
updated AGENTS.md
updated Dockerfile
updated Makefile
updated app/application.go
create app/jobs/jobs.go
create app/jobs/jobs_test.go
updated app/models/store.go
updated app/services/migrate/migrate.go
updated bin/ci
updated config/deploy.production.yml
updated config/deploy.yml
updated main.go
insert main.go: "worker": runWorker,
insert app/application.go: import "blog/app/jobs"
updated bogie.toml: jobs = true
run go mod tidy
create app/jobs/send_welcome.go
create app/jobs/send_welcome_test.go
insert app/jobs/jobs.go: registered += add(workers, &SendWelcomeWorker{Log: log})
It is the same trick as bogie app:update: render the app twice, once without jobs and once with, and merge the difference in. A file you have already edited merges rather than being overwritten; a real overlap leaves conflict markers, the same as app:update. Two refusals guard it, for the same reasons app:update has them:
- A dirty git tree, since this touches several files in one diff:
bogie: generate job: enable jobs: the git tree has uncommitted changes; commit or stash them, since this touches several files in one diff - An app not yet on this Bogie's version, so the merge's base is rendered from the right templates and no unrelated template change shows up as noise in what should be a jobs-only diff:
bogie: generate job: enable jobs: this app is at bogie 0.1.0 and this Bogie is 0.1.1; run `bogie app:update` first
Once jobs are on, every later g job just writes the job and registers it, the same as an app made with --jobs from the start.
Writing one
bogie g job send_welcome
create app/jobs/send_welcome.go
create app/jobs/send_welcome_test.go
insert app/jobs/jobs.go: registered += add(workers, &SendWelcomeWorker{Log: log})
type SendWelcomeArgs struct {
// ID string `json:"id"`
}
func (SendWelcomeArgs) Kind() string { return "send_welcome" }
type SendWelcomeWorker struct {
river.WorkerDefaults[SendWelcomeArgs]
Log *slog.Logger
}
func (w *SendWelcomeWorker) Work(ctx context.Context, job *river.Job[SendWelcomeArgs]) error {
w.Log.InfoContext(ctx, "send_welcome: not implemented yet", "job", job.ID)
return nil
}
An Args type with a Kind, and a Worker with Work(ctx, job). Args are stored as JSON in the job row, so keep them to ids and small values; the worker loads whatever else it needs. Never change Kind once jobs of it have been queued: a queued job of the old kind would have no worker to run it. Dependencies go on the worker as fields, the smallest interface it needs (rule 2), passed on the registration line.
The marker
The generator registers the worker above // bogie:jobs in app/jobs/jobs.go, the same idempotent, checked-before-written insertion every generator uses:
registered += add(workers, &SendWelcomeWorker{Log: log})
// bogie:jobs
Enqueuing
From anywhere that holds the application, including inside a transaction so the job commits with the row it depends on or not at all:
app.Jobs.Insert(ctx, jobs.SendWelcomeArgs{}, nil)
// or, so the enqueue commits with the row:
app.Jobs.InsertTx(ctx, tx, jobs.SendWelcomeArgs{}, nil)
Running the worker
bogie worker
Or make worker. It is its own process, separate from bogie server, so a backlog in the queue never slows a request. In development bogie worker runs under Air and builds to its own binary, so it rebuilds and restarts on save independently of the server; make worker is one boot, no reload. River's own schema (the tables it needs to track jobs) is applied by migrate, through rivermigrate, not copied into db/migrate: it arrives the same way the app's own migrations do, at boot or with bogie db:migrate.
Removing one
bogie d job send_welcome
Removes the file and the registration line.