quillmark ⌥

Getting started

Installation

quillmark requires Go 1.22+. Add it to your project:

go get codeberg.org/quillmarkhq/quillmark@latest

Usage

The example below creates a queue with a couple of options and uses it.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"codeberg.org/quillmarkhq/quillmark"
)

func main() {
	ctx := context.Background()

	q, err := quillmark.New(quillmark.Options{
		DatabaseURL: "postgres://localhost/app",
		Concurrency: 10,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer q.Close()

	n, err := q.Enqueue(ctx, "send_email", []byte("{"to":"ada@example.com"}"))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n)

	n, err := q.Schedule(ctx, "cleanup", 30 * time.Second)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n)
}

Jobs and handlers

A job has a kind, a JSON payload and some bookkeeping. A handler is registered for each kind and receives the decoded payload.

Keep handlers idempotent: a job may run more than once if a worker dies after doing the work but before marking it complete.

Enqueueing

Use enqueue() to add a job. Pass a transaction to make the job part of a larger unit of work.

Jobs become visible to workers only after the transaction commits, so a worker never sees a job whose data is not there yet.

Retries

When a handler fails, the job is rescheduled with exponential backoff. After the maximum number of attempts it is moved to the dead state and kept for inspection.

Return a permanent error from a handler to skip the remaining attempts.

Running workers

Workers poll for available jobs and lock them with FOR UPDATE SKIP LOCKED, so any number of workers can run side by side.

Set the concurrency per queue to match the resources a handler needs.

Shutting down

On shutdown, workers stop fetching new jobs and wait for running ones up to a timeout. Jobs still running after that are released and picked up again later.