CARLOS rastrillo docs

๐Ÿค– jobs

amadan.net/rastrillo/rastrillo/jobs

Background work you can watch: Start runs a function in a goroutine and hands back an id a status page can poll.

Background jobs is the guide.

In-memory, on purpose

Your app is single-process and a restart kills the goroutine, so a persisted row would only persist a lie. A job is a goroutine, and a deploy ends it mid-flight. Make jobs idempotent, and put work that has to survive a restart in eventlog.

New, Start and Get

func New(logger *slog.Logger) *Jobs
func (j *Jobs) Start(owner, name, location string, fn func(ctx context.Context, progress func(string)) error) (Job, error)
func (j *Jobs) Get(id, owner string) (Job, bool)

Build one at boot. The zero value is not usable.

owner is the session Subject, a string, because magic-link subjects are emails and password subjects are numeric strings. Key your own job-related rows the same way.

fn's error text reaches the owner, so write it for them. A panic inside fn becomes Failed rather than taking the process down.

Get answers only the owner. A wrong owner and an unknown id are indistinguishable, the same 404 rule scope enforces for rows.

The bounds

Start returns ErrOwnerBusy past four running jobs per owner. Flash your own copy; the framework does not choose your wording.

A job still running after fifteen minutes is marked Failed and stops counting against the limit. Its context expires at the same moment, so a well-behaved fn stops too.

What no bound can do is kill a goroutine. An fn that ignores its context runs invisibly until the process restarts โ€” it just no longer blocks its owner. Honour ctx.

Job and Status

type Job struct {
	ID         string
	Owner      string
	Name       string
	Status     Status
	Progress   string
	Err        string
	Location   string
	StartedAt  time.Time
	FinishedAt time.Time
}

Status is Running, Done or Failed. There is no "queued": Start runs the goroutine immediately.

Location is where the owner lands when the job finishes; empty keeps them on the status page. Progress is the latest text fn passed to its progress callback, empty until it sets one.

The handlers

func NewHandlers(cfg Config) (*Handlers, error)

type Config struct {
	Jobs           *Jobs
	Render         func(w http.ResponseWriter, r *http.Request, d PageData)
	RenderFragment func(w http.ResponseWriter, r *http.Request, d PageData)
}

All three are required and NewHandlers errors otherwise.

Handler Route Behaviour
Handlers.StatusPage GET /jobs/{id} full page; 303 to Location when done
Handlers.Fragment GET /jobs/{id}/fragment the partial alone; 204 + Rastrillo-Location when done
Handlers.Events GET /jobs/{id}/events Server-Sent Events

Mount all three inside the sess.Require group.

RenderFragment has to draw the partial on its own, without the layout, or the layout nests inside itself on the next poll.

PageData

type PageData struct {
	Job          Job
	FragmentPath string
	EventsPath   string
	PollSeconds  int
}

FragmentPath goes in data-poll, and PollSeconds feeds data-poll-every and the <noscript> meta refresh. Emit that meta only while the job is running, or a failed page refreshes forever.

EventsPath is opt-in. Put it in data-poll-push and a supporting browser upgrades to server push; ignore it and you get plain polling.

The stream

Events sends update, done and gone events with : ping heartbeats every 15 seconds, per-write deadlines, and a bounded stream lifetime. It is one-way, and the shim falls back to timer polling on its own, permanently, if the stream fails.

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.