kyu

package module
v1.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 24 Imported by: 0

README

kyu

Go PostgreSQL Redis Prometheus License

A distributed job queue library for Go, backed by PostgreSQL and Redis.

Grafana Dashboard kyu jobs visualized on Grafana

kyu is a Go-native distributed job queue whose primary design concern is that it be operable in production: dead-letter management, a live dashboard, and a CLI ship with the queue rather than as external tooling. PostgreSQL is the durable source of truth for every job, attempt, and error, while Redis exists only as the low-latency priority index that decides what runs next — a deliberate split that uses each backend for what it does best instead of either one doing everything.

PostgreSQL is the source of truth - every job, its full history, retry count, and error message are persisted there. Redis acts as the priority queue - workers pop job IDs from a sorted set and fetch the full record from Postgres to process. Jobs survive a Redis restart because nothing is lost if the sorted set is cleared.


Contents

How it works

Kyu runs five concurrent subsystems once you call Start.

The worker pool pops job IDs from a Redis sorted set, fetches the full job record from Postgres, runs the registered handler, then updates the job status. Failed jobs with retries remaining are re-queued with exponential backoff. Failed jobs with no retries left are marked dead (see Dead letter queue).

Jobs are claimed with optimistic locking. When a worker picks up a job it stamps the row running with its locked_by identity, and every transition out of that state - completed, failed, or dead - is an update guarded by the same locked_by value. If a worker's claim was lost because another worker reaped or re-claimed the job, its write is ignored rather than clobbering the current owner's state.

The scheduler ticks every SchedulerInterval and queries Postgres for scheduled or failed jobs whose time has arrived, pushing their IDs back into Redis.

The stale reaper ticks every ReaperInterval and resets any job stuck in the running state longer than StaleJobTimeout. This handles workers that crashed mid-job.

The orphan reaper ticks every OrphanCheckInterval and re-queues pending jobs that are missing from Redis. This covers jobs popped off the queue by a worker that crashed before it could mark them running (and jobs left behind if the Redis sorted set was ever cleared).

The metrics server exposes a Prometheus /metrics endpoint on MetricsPort.


Scope

kyu's boundaries are decisions, not gaps.

Handlers are Go functions compiled into the consumer's binary. There is no handler DSL, no sidecar runtime, and no sandbox layer. This is deliberate: handlers share the same toolchain, type system, and deployment pipeline as the application that enqueues jobs, and kyu takes on no protocol or security surface for executing untrusted code.

kyu is a job queue, not a workflow orchestrator. There is no job-dependency graph and no DAG engine. Jobs are independent units of work that may run concurrently; retries and backoff apply per job. A sequence of dependent steps is expressed in application code, with each handler enqueuing the next job in the chain when it completes.


Installation

The kyu library is a Go module; the kyu serve CLI ships with it.

As a library (embed the queue in your Go program):

go get github.com/codetesla51/kyu

As a CLI (installs the kyu binary on your GOBIN):

go install github.com/codetesla51/kyu/cmd/kyu@latest

As a binary (from the GitHub Releases page, built by CI from the version tag):

curl -sL https://github.com/codetesla51/kyu/releases/latest/download/kyu-linux-amd64 -o kyu
chmod +x kyu && ./kyu serve

Replace kyu-linux-amd64 with kyu-linux-arm64, kyu-darwin-amd64, kyu-darwin-arm64, or kyu-windows-amd64.exe for other platforms. A multi-arch Linux image is also published on GHCR: ghcr.io/codetesla51/kyu:v1.0.0.

Requires PostgreSQL and Redis. The jobs table and indexes are created by the embedded goose migrations, which run automatically on the first Connect call. If you manage the schema yourself, set DisableAutoMigrate: true (see Config). Queries are generated by sqlc from queries/.


Quick start

package main

import (
    "context"
    "log"
    "os/signal"
    "syscall"

    "github.com/codetesla51/kyu"
)

func main() {
    q := kyu.New(kyu.Config{
        DSN:         "postgres://user:pass@localhost:5432/mydb?sslmode=disable",
        RedisAddr:   "localhost:6380",
        Workers:     5,
        MetricsPort: 9090,
    })

    q.Register("send_email", func(ctx context.Context, payload string) error {
        log.Printf("sending email: %s", payload)
        return nil
    })

    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    if err := q.Connect(ctx); err != nil {
        log.Fatal(err)
    }

    if err := q.Start(ctx); err != nil {
        log.Fatal(err)
    }
}

Start blocks. On SIGINT or SIGTERM the context is cancelled, workers finish their current jobs, the scheduler and metrics server shut down cleanly, and Start returns.

The required call order is: New -> Register -> Connect -> Enqueue / Start. Connect must be called before Enqueue or Start - both require an open database and Redis connection.


Enqueuing jobs

Jobs are enqueued independently of Start - you can enqueue from a separate service, an HTTP handler, or anywhere you have access to the Queue.

// run immediately
jobID, err := q.Enqueue(ctx, "send_email", `{"to":"user@example.com"}`, kyu.EnqueueOptions{
    MaxRetries: 3,
    Priority:   1,
})

// run after 1 minute
at := time.Now().Add(1 * time.Minute)
jobID, err := q.Enqueue(ctx, "send_email", `{"to":"user@example.com"}`, kyu.EnqueueOptions{
    MaxRetries:  3,
    Priority:    0,
    ScheduledAt: &at,   // pointer so nil means "no schedule, run now"
    TimeOut:     10 * time.Second,
})

// high priority - processed before lower priority jobs
jobID, err := q.Enqueue(ctx, "process_payment", `{"order_id":"123"}`, kyu.EnqueueOptions{
    MaxRetries: 5,
    Priority:   10, // higher score = picked up first
})

ScheduledAt is a pointer because nil means "run immediately" and a real value means "run at this time". A plain time.Time cannot represent the absence of a value.

Priority maps directly to the Redis sorted set score. Workers always pop the highest score first, so higher numbers are processed before lower ones.

Enqueue multiple jobs atomically - a single COPY insert into Postgres and a single ZADD into Redis:

ids, err := q.EnqueueMany(ctx, []kyu.EnqueueRequest{
    {JobType: "send_email", Payload: `{"to":"a@example.com"}`, Options: kyu.EnqueueOptions{MaxRetries: 3, Priority: 1}},
    {JobType: "send_email", Payload: `{"to":"b@example.com"}`, Options: kyu.EnqueueOptions{MaxRetries: 3, Priority: 2}},
    {JobType: "process_payment", Payload: `{"order_id":"123"}`, Options: kyu.EnqueueOptions{MaxRetries: 5, Priority: 10}},
})

IDs are returned in the same order as the input requests.


Config

kyu.Config{
    // Required
    DSN:       "postgres://user:pass@localhost:5432/db?sslmode=disable",
    RedisAddr: "localhost:6380",

    // Worker pool
    Workers: 5, // number of concurrent goroutines processing jobs

    // Queue
    QueueName: "kyu:default", // Redis sorted set key - use different names to isolate queues

    // Metrics
    MetricsPort: 9090, // set to 0 to disable

    // Stale job reaper
    // A job stuck in "running" beyond this duration is reset to "pending"
    // and re-queued. This handles crashed workers.
    StaleJobTimeout: 10 * time.Minute,

    // Loop intervals. All default to sane values if left zero.
    SchedulerInterval:    5 * time.Second, // promotes scheduled/failed jobs whose time arrived
    ReaperInterval:       1 * time.Minute, // scans for stale running jobs
    OrphanCheckInterval: 1 * time.Minute,  // re-queues pending jobs missing from Redis

    // Completion callbacks (optional)
    // POSTs a JSON body {job_id, status, payload, error} to this URL whenever
    // a job completes. Empty disables callbacks.
    CallbackURL: "https://hooks.example.com/job-done",

    // Postgres connection pool
    MaxOpenConns:    25,
    MaxIdleConns:    25,
    ConnMaxLifetime: 5 * time.Minute,

    // Migrations (optional)
    // kyu applies its embedded goose migrations on Connect by default. Set to
    // true if you manage the kyu schema yourself (e.g. with your own migration
    // tool against the same database) to skip that step.
    DisableAutoMigrate: false,

    Logger: log.Default(),
}

All fields have defaults. kyu.New(kyu.Config{}) connects to local Postgres and Redis with 5 workers.

A note on auto-migrations: kyu shares the database's goose version table when it migrates. If the same database is managed by another goose-based project, the two migration histories can conflict. Either give kyu its own database or set DisableAutoMigrate: true and apply kyu's migrations yourself (they live in db/goose_migrations/).

If you are running multiple applications against the same Redis instance, set a unique QueueName per application. Workers compete for any job in their queue - two apps sharing the same queue name will process each other's jobs.


Registering handlers

Handlers receive the context and the payload string you passed at enqueue time. Return an error to trigger a retry (if retries remain) or mark the job dead (if none remain). Because handlers are plain Go functions compiled into the binary (see Scope), they share the application's toolchain and deployment lifecycle.

q.Register("send_email", func(ctx context.Context, payload string) error {
    var data struct {
        To      string `json:"to"`
        Subject string `json:"subject"`
    }
    if err := json.Unmarshal([]byte(payload), &data); err != nil {
        return err // will retry
    }
    return sendEmail(ctx, data.To, data.Subject)
})

q.Register("process_payment", func(ctx context.Context, payload string) error {
    // respect context cancellation for long-running work
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }
    return chargeCard(payload)
})

Handlers are safe to register concurrently. Registering the same job type twice overwrites the first handler.


Middleware

Middleware wraps every job execution regardless of type. Middlewares are applied in registration order - the first registered is the outermost wrapper. In the example below, the logging middleware runs first, then timing, so the log line appears before the duration line.

// order: logging wraps timing wraps handler
q.Use(loggingMiddleware)  // outermost
q.Use(timingMiddleware)   // inner
// logging
q.Use(func(ctx context.Context, jobType, payload string, next func() error) error {
    log.Printf("job started: %s", jobType)
    err := next()
    if err != nil {
        log.Printf("job failed: %s: %v", jobType, err)
    }
    return err
})

// timing
q.Use(func(ctx context.Context, jobType, payload string, next func() error) error {
    start := time.Now()
    err := next()
    log.Printf("job=%s duration=%s", jobType, time.Since(start))
    return err
})

// panic recovery
q.Use(func(ctx context.Context, jobType, payload string, next func() error) error {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("job panicked: %s: %v", jobType, r)
        }
    }()
    return next()
})

Job lifecycle

pending
   |
   |-- (scheduler promotes to Redis when scheduled_at is reached)
   |
   `-- running
          |
          |-- completed       handler returned nil
          |
          |-- failed          handler returned error, retries remain
          |      `-- re-enqueued with exponential backoff (1s, 2s, 4s, ...)
          |
          |-- dead            handler returned error, no retries left
          |
          `-- cancelled       CancelJob was called before the job ran

Failed jobs use exponential backoff between retries - a job that has failed once waits 1 second, twice waits 2 seconds, three times waits 4 seconds, and so on. The scheduler picks them back up once their scheduled_at arrives.

Jobs table columns:

Column Description
id UUID, primary key
job_type matches the name passed to Register
payload arbitrary string passed to the handler
status pending, running, failed, completed, dead, cancelled
priority higher score = picked up first
scheduled_at job will not run until this time
max_retries maximum retry attempts
retry_count number of attempts so far
error_message last error returned by the handler
locked_by which worker is running it, e.g. worker-3
locked_at when the worker locked it
completed_at when it finished successfully

Inspecting jobs

// get a single job by ID
job, err := q.Inspect(ctx, jobID)
log.Printf("status=%s retries=%d error=%s", job.Status, job.RetryCount, job.ErrorMessage)

// get all jobs that exhausted their retries
dead, err := q.DeadJobs(ctx)
for _, j := range dead {
    log.Printf("dead: id=%s type=%s attempts=%d error=%s",
        j.ID, j.JobType, j.RetryCount, j.ErrorMessage)
}

// cancel a job that hasn't started yet
// works on: pending, scheduled, failed
// has no effect once a job is running
err := q.CancelJob(ctx, jobID)

Dead letter queue

Jobs that exhaust all retries are marked dead and stay persisted so you can inspect, retry, or purge them.

// list every dead job
dead, err := q.ListDead(ctx)
for _, j := range dead {
    log.Printf("dead: id=%s type=%s error=%s", j.ID, j.JobType, j.ErrorMessage)
}

// inspect a single dead job by ID
job, err := q.InspectDead(ctx, jobID)

// retry one job - resets retry_count, clears the error, and re-enqueues
// it with a fresh set of retries so it can run again immediately
err := q.Retry(ctx, jobID)

// retry every dead job, re-enqueuing them all on the given Redis queue
n, err := q.RetryAllDead(ctx, "kyu:retry-backlog")

// purge a dead job (soft delete - sets deleted_at)
err := q.DeleteDead(ctx, jobID)

Retry and RetryAllDead restore jobs to pending and push them back onto a queue with their original priority. RetryAllDead returns the number of jobs retried.

RetryAllDead takes the target queue as an argument. Retrying onto a separate queue, such as kyu:retry-backlog, isolates replayed jobs from live traffic: they are only processed by workers listening on that queue, so the replay can run on a dedicated retry worker or during a controlled maintenance window.


Operations

Runtime controls, health checks, and introspection for a running queue.

// stop the worker pool from popping new jobs; in-flight jobs finish first
q.Pause()
// let the pool pop again
q.Resume()
if q.IsPaused() { /* ... */ }

// health check - verifies both Postgres and Redis are reachable
if err := q.Ping(ctx); err != nil {
    log.Fatal(err)
}

Pause is per-process state: it stops this Queue instance's workers, while other processes sharing the queue keep working.

// point-in-time job counts plus this queue's Redis depth
stats, err := q.Stats(ctx)
log.Printf("pending=%d running=%d dead=%d depth=%d",
    stats.Pending, stats.Running, stats.Dead, stats.QueueDepth)

// list jobs, most recent first, filtered and paginated
jobs, err := q.ListJobs(ctx, kyu.JobFilter{Status: "failed", Limit: 50})
jobs, err = q.ListJobs(ctx, kyu.JobFilter{JobType: "send_email", Limit: 100, Offset: 100})

// sorted names of every registered job type
types := q.JobTypes()

// read-only config summary
info := q.Info()

// worker pool state and size
workers := q.Workers()
n := q.WorkerCount()
name := q.QueueName()

JobFilter matches any combination of Status and JobType; a zero Limit returns the 100 most recent matching jobs, and Offset pages through longer lists. The status counts in QueueStats are global to the database (all queues share the jobs table), while QueueDepth is the number of IDs waiting in this queue's Redis sorted set.

// reset a failed or cancelled job back to pending and re-enqueue it
err := q.Reset(ctx, jobID)

// soft-delete any job by ID (also removes it from the pending queue)
err := q.Delete(ctx, jobID)

// soft-delete every job in one status; returns the number affected
n, err := q.Purge(ctx, "completed")

Delete and Purge set deleted_at (soft delete) so rows disappear from listings but stay in the table. Reset applies to failed and cancelled jobs; dead jobs use Retry instead (see Dead letter queue). Purge accepts one of: pending, running, completed, failed, scheduled, cancelled, dead.


Completion callbacks

Set Config.CallbackURL and kyu POSTs a JSON webhook whenever a job completes:

{"job_id":"...","status":"completed","payload":"...","error":""}

The request is fire-and-forget (sent in a goroutine, 10s timeout) and failures are only logged, so callbacks never slow down or break job processing. An empty CallbackURL disables callbacks entirely.


RunOnce (cron mode)

RunOnce drains the current queue and returns instead of running a persistent loop. Use it when you want an external scheduler (cron, Kubernetes CronJob) to control when work happens rather than running workers continuously.

if err := q.Connect(ctx); err != nil {
    log.Fatal(err)
}
// processes everything currently in Redis, then returns
if err := q.RunOnce(ctx); err != nil {
    log.Fatal(err)
}

Metrics

When MetricsPort is set, a Prometheus /metrics endpoint is available on that port. Each Queue instance uses its own private Prometheus registry so multiple instances in the same process do not conflict.

Metric Type Description
kyu_jobs_total counter total jobs ever submitted
kyu_jobs_processed_total counter vec completed jobs, labelled by status
kyu_job_failures_total counter vec failures, labelled by job_type
kyu_jobs_dead_total counter jobs that exhausted all retries
kyu_queue_depth gauge jobs currently waiting in Redis

Prometheus scrape config:

scrape_configs:
  - job_name: kyu
    static_configs:
      - targets: ["localhost:9090"]

CLI

cmd/kyu builds a small CLI that runs a self-contained instance and manages jobs against any kyu queue.

go run ./cmd/kyu --help
Command Description
kyu serve Run workers, scheduler, reapers, metrics, and the web dashboard. This is the default command, so bare kyu also works.
kyu enqueue <type> [payload] Enqueue a job and print its ID. Flags: --priority, --retries, --schedule (RFC3339), --timeout, --count.
kyu inspect <id> Print the details of a job. Add --dead to look it up in the dead letter queue.
kyu version Print the kyu version and Go runtime.

Connection settings are persistent flags (--dsn, --redis-addr, --redis-password, --queue), each with a matching environment variable. For example:

# spin up a queue with the web dashboard
kyu serve --queue my:queue --workers 8

# enqueue a job for it from anywhere
kyu enqueue send_email '{"to":"user@example.com"}' --retries 3 --priority 5

# see what happened to it
kyu inspect <job_id>

kyu serve is intended for exploring the system and for simple deployments. Production workloads embed the library in their own binary and register real handlers with Register; the demo handlers that ship with serve are examples, not production handlers.

Dashboard

A web UI for monitoring and managing a running kyu queue. kyu serve starts it by default on the address given by --dashboard-addr, and it can also be embedded in any Go program via the dashboard package: http.Handle("/", dashboard.Handler(q)). The UI and its JSON API are embedded in the binary, so there is nothing to install beyond a Postgres and Redis reachable from the machine running it.

kyu dashboard in action

kyu serve --dsn "$DATABASE_URL" --redis-addr localhost:6380

The dashboard connects to the default local stack (postgres://localhost:5432/kyu, localhost:6380) and serves on :8080. Every setting can be a flag or an environment variable:

Variable Default Flag Purpose
DASHBOARD_ADDR :8080 --dashboard-addr HTTP listen address for the UI
DATABASE_URL postgres://localhost:5432/kyu?... --dsn Postgres DSN
REDIS_ADDR localhost:6379 --redis-addr Redis address
REDIS_PASSWORD (empty) --redis-password Redis password
KYU_QUEUE kyu:default --queue Redis queue key this instance owns
KYU_WORKERS 4 --workers number of worker goroutines
KYU_METRICS_PORT 9090 --metrics-port Prometheus metrics port
KYU_STALE_TIMEOUT 30s --stale-timeout stale running-job reset threshold
KYU_ORPHAN_INTERVAL 30s --orphan-interval orphan reaper tick interval

Features:

  • Live overview - stats, per-status totals, queue depth, and a jobs stream pushed over SSE; pause/resume to inspect a point in time.
  • Jobs - searchable and filterable by status and job type, with a canvas and a table view, paginated (25/50/100 per page, or "Load more" to page through more than the live snapshot).
  • Dead letter queue - inspect dead jobs, retry individually or all at once, or purge.
  • Workers - see each worker's busy state and current job.
  • Create - enqueue jobs straight from the UI (type, JSON payload, priority, max retries, schedule).
  • Tools - purge jobs by status.

kyu serve registers a small set of demo handlers so the dashboard has work to show: order_created, send_email, failing_job (fails every run to exercise retries and the dead letter queue), and flaky_job (fails twice then succeeds). Enqueuing any other job type will exhaust retries and land in the dead letter queue with unknown job type - register your own handlers in your own binary (see Registering handlers) or embed dashboard.Handler(q) in your server.


Benchmarks

Measured on an Intel Core i5-6300U (4 cores, 2.4GHz).

BenchmarkRegister               ~52 ns/op     0 B/op    0 allocs/op
BenchmarkExecute                ~950 ns/op  320 B/op    5 allocs/op
BenchmarkExecuteWithMiddleware  ~1.2 µs/op  480 B/op    7 allocs/op
BenchmarkExecuteParallel        ~600 ns/op  320 B/op    5 allocs/op

Job dispatch runs in under one microsecond. Zero allocations on Register. Each additional middleware layer costs one closure allocation (~160 bytes). Under parallel load the registry mutex shows no measurable contention. In practice throughput is bounded by Postgres write latency and Redis round-trip time, not by the dispatch path.

Load testing (Barrage)

Load tested with Barrage, a cross-layer HTTP/Postgres/Redis load tester. On a single untuned Postgres instance, Kyu sustains ~750 writes/sec with 100% success and zero job loss (DB mean ~166ms, p99 ~1s); Redis never became a bottleneck in any run (p99 <70ms even at Postgres's worst). Clean final result — 60s run, 20s ramp, concurrency 50, at 300 enqueues/s, 900 DB ops/s, 1500 Redis cmds/s:

Runner Requests Success Rate P50 P95 P99 Max
HTTP POST /api/jobs 15,000 100% 251.5/s 52ms 169ms 275ms 1.57s
Postgres 44,992 100% 749.9/s 96ms 547ms 966ms 3.37s
Redis 74,999 100% 1250.0/s 3.7ms 21.7ms 41.9ms 377ms

Barrage run result

The journey surfaced two non-obvious findings worth knowing before you deploy at nontrivial concurrency:

  • The connection ceiling bites before the write path. Stock Postgres (max_connections=100) against a large app pool + direct DB clients fails first with too many clients errors and looks like a throughput problem. Raise max_connections to match pool sizing (KYU_MAX_OPEN_CONNS).
  • An index was costing writes. A partial (status) WHERE deleted_at IS NULL index showed no read benefit under this write-heavy test shape and was pure write tax on every INSERT/UPDATE — removing it (plus clearing test bloat) produced a measured ~3× latency improvement (DB p99 2999ms→966ms, mean 431ms→166ms; HTTP p99 579ms→275ms) at identical load.

The full investigation log — every run, the pool-ceiling diagnosis, the index EXPLAIN ANALYZE work, and the clean before/after isolation — is in benchmarks/README.md. Reproducible config: benchmarks/load_test.yaml. Full HTML reports: report.html at the repo root (Barrage's live output, the most recent run) and benchmarks/reports/run4-clean.html (archived copy of that final clean run).

Honesty note: this measures cross-layer latency/throughput correlation under simultaneous combined load — not a single-job causal trace from enqueue to completion.


Docker Compose

A docker-compose.yml is included with Postgres, Redis, Prometheus, Grafana, and the kyu serve CLI running the demo handlers with the web dashboard on port 8080. Grafana is provisioned automatically on startup (grafana/provisioning) with a Prometheus datasource and a pre-built dashboard covering queue depth, job throughput, failure rates by job type, goroutine count, and memory usage. Default Grafana login is admin/admin.

docker compose up --build

Then open the dashboard at localhost:8080 and enqueue a job from the Create form or with kyu enqueue send_email '{"to":"demo@example.com"}'.

Service Address
Dashboard localhost:8080
Prometheus localhost:9090
Grafana localhost:3000
Postgres localhost:5432
Redis localhost:6380

Running tests

Unit tests - no infrastructure required:

go test -short ./...

Integration tests - requires Postgres on 5432 and Redis on 6380.

Before running, update the DSN and Redis address in kyu_unit_test.go to match your local setup (both the unit and integration tests live in this single file). The default credentials in the test file are for local development only.

go test ./...

Benchmarks:

go test -bench=. -benchmem -count=3

Documentation

Overview

Package kyu is an importable distributed job queue library backed by PostgreSQL (persistence) and Redis (queue/priority sorted-set).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// DSN is the PostgreSQL connection string.
	// No default; must be provided.
	DSN string

	// RedisAddr is the Redis host:port address.
	// Default: "localhost:6379"
	RedisAddr string

	// RedisPassword is the password for the Redis server.
	// Default: ""
	RedisPassword string

	// Workers is the number of concurrent worker goroutines.
	// Default: 5
	Workers int

	// MetricsPort is the port the Prometheus /metrics HTTP server listens on.
	// Set to 0 to disable the metrics server.
	// Default: 9090
	MetricsPort int

	// Logger is used for internal diagnostic messages.
	// Defaults to the standard library logger when nil.
	Logger *log.Logger

	// StaleJobTimeout is the duration after which a running job is considered stale
	// and can be retried by another worker.
	// Default: 5 minutes
	StaleJobTimeout time.Duration

	// SchedulerInterval is how often the scheduler promotes scheduled and
	// failed jobs whose time has arrived back into the queue.
	// Default: 5 seconds
	SchedulerInterval time.Duration

	// ReaperInterval is how often the stale reaper scans for jobs stuck in
	// the running state longer than StaleJobTimeout and resets them.
	// Default: 1 minute
	ReaperInterval time.Duration

	// QueueName is the Redis key for the job queue.
	// Default: "kyu:default"
	QueueName string

	// CallbackURL is an optional HTTP endpoint that kyu calls (POST with a
	// JSON body containing job_id/status/payload) whenever a job completes.
	// If empty, no callbacks are sent.
	CallbackURL string

	// MaxOpenConns is the maximum number of open database connections.
	// Default: 25
	MaxOpenConns int

	// MaxIdleConns is the maximum number of idle database connections.
	// Default: 25
	MaxIdleConns int

	// ConnMaxLifetime is the maximum lifetime of a database connection.
	// Default: 5 minutes
	ConnMaxLifetime time.Duration

	// OrphanCheckInterval is how often the orphaned job checker runs to find
	// pending jobs that were popped from Redis but never made it to the database.
	// Default: 1 minute
	OrphanCheckInterval time.Duration

	// DisableAutoMigrate skips the embedded goose migrations that normally run
	// on Connect. Set to true if you manage the kyu schema yourself (e.g. via
	// your own migration tool against the same database).
	// Default: false (migrations run automatically)
	DisableAutoMigrate bool
}

Config holds all tunable parameters for a Queue. Zero-value fields fall back to sensible defaults applied by New.

type EnqueueOptions

type EnqueueOptions struct {
	// Priority is the job priority. Higher values indicate higher priority.
	// Jobs with higher priority are processed before lower priority jobs.
	// Default: 0
	Priority int

	// MaxRetries is the maximum number of times to retry a failed job.
	// Default: 0 (no retries)
	MaxRetries int

	// ScheduledAt is the time at which the job should be executed.
	// If nil or in the past, the job is enqueued immediately.
	ScheduledAt *time.Time

	// TimeOut is the maximum time the job handler can run before being cancelled.
	// Default: 30 seconds
	TimeOut *time.Duration
}

type EnqueueRequest added in v1.0.3

type EnqueueRequest struct {
	// JobType is the registered job type to run.
	JobType string
	// Payload is the opaque payload passed to the job handler.
	Payload string
	// Options is the per-job enqueue configuration.
	Options EnqueueOptions
}

EnqueueRequest describes a single job for batch enqueuing.

type JobFilter added in v1.0.3

type JobFilter struct {
	// Status restricts to a single status: pending, running, completed,
	// failed, scheduled, cancelled, or dead. Empty matches every status.
	Status string
	// JobType restricts to a single registered job type. Empty matches all.
	JobType string
	// Limit caps the number of returned jobs (most recent first). 0 = default 100.
	Limit int
	// Offset skips the first Offset matching jobs, enabling pagination.
	// Combine with Limit to page through a long list. 0 = no offset.
	Offset int
}

JobFilter filters job listings. Empty fields match anything; Limit <= 0 defaults to the 100 most recent matching jobs.

type Middleware

type Middleware func(ctx context.Context, jobtype, payload string, next func() error) error

Middleware is a function that wraps job execution. It receives the job context, type, payload, and a next function to call the handler. Use Middleware to add logging, metrics, or other cross-cutting concerns.

type Queue

type Queue struct {
	// contains filtered or unexported fields
}

Queue is the main handle for the kyu job queue library. Create one with New, register handlers with Register, then call Start.

func New

func New(cfg Config) *Queue

New creates a new Queue with the given configuration. Defaults are applied for any zero-value fields; see Config for details. New does NOT open any connections — that happens inside Connect and Start.

func (*Queue) CancelJob

func (q *Queue) CancelJob(ctx context.Context, id string) error

CancelJob cancels a pending, scheduled, or failed job by setting its status to cancelled.

func (*Queue) Connect

func (q *Queue) Connect(ctx context.Context) error

func (*Queue) DeadJobs

func (q *Queue) DeadJobs(ctx context.Context) ([]kyudb.Job, error)

DeadJobs returns all jobs that have exhausted all retries and are marked as dead.

func (*Queue) Delete added in v1.0.3

func (q *Queue) Delete(ctx context.Context, id string) error

Delete soft-deletes any job (of any status) by ID. It also removes the ID from the pending Redis set so it is never dispatched. Returns an error if the job does not exist. For dead-letter jobs specifically, see DeleteDead.

func (*Queue) DeleteDead added in v1.0.3

func (q *Queue) DeleteDead(ctx context.Context, id string) error

DeleteDead soft-deletes a single dead job (sets deleted_at) so it disappears from all listings. It returns an error if no dead job exists with the ID.

func (*Queue) Enqueue

func (q *Queue) Enqueue(ctx context.Context, jobType, payload string, opts EnqueueOptions) (string, error)

Enqueue adds a new job to the queue.

func (*Queue) EnqueueMany added in v1.0.3

func (q *Queue) EnqueueMany(ctx context.Context, jobs []EnqueueRequest) ([]string, error)

EnqueueMany batches job creation: all jobs are inserted into Postgres with a single COPY statement and pushed onto the Redis queue with a single ZADD. IDs are returned in the same order as the input. Like Enqueue, the Postgres insert happens first; if the Redis push then fails, the jobs stay persisted as pending and are recovered by the orphan reaper.

func (*Queue) Info added in v1.0.3

func (q *Queue) Info() QueueInfo

Info returns a read-only summary of the queue configuration.

func (*Queue) Inspect

func (q *Queue) Inspect(ctx context.Context, id string) (kyudb.Job, error)

Inspect returns a job by its ID.

func (*Queue) InspectDead added in v1.0.3

func (q *Queue) InspectDead(ctx context.Context, id string) (kyudb.Job, error)

InspectDead returns a single dead job by ID. It returns an error if no dead job exists with the given ID.

func (*Queue) IsPaused added in v1.0.3

func (q *Queue) IsPaused() bool

IsPaused reports whether the worker pool is currently paused.

func (*Queue) JobTypes added in v1.0.3

func (q *Queue) JobTypes() []string

JobTypes returns the sorted names of every registered job type.

func (*Queue) ListDead added in v1.0.3

func (q *Queue) ListDead(ctx context.Context) ([]kyudb.Job, error)

ListDead returns all jobs currently in the dead state. It is an alias for DeadJobs kept for a friendlier name.

func (*Queue) ListJobs added in v1.0.3

func (q *Queue) ListJobs(ctx context.Context, filter JobFilter) ([]kyudb.Job, error)

ListJobs returns jobs matching the filter, most recent first. Use JobFilter.Limit and JobFilter.Offset to page through long lists.

func (*Queue) Pause added in v1.0.3

func (q *Queue) Pause()

Pause tells the worker pool to stop popping new jobs. In-flight jobs finish; queued jobs wait until Resume is called. Pause is per-process state - it only affects this Queue instance.

func (*Queue) Ping added in v1.0.3

func (q *Queue) Ping(ctx context.Context) error

Ping verifies that both PostgreSQL and Redis are reachable. Useful for health checks.

func (*Queue) Purge added in v1.0.3

func (q *Queue) Purge(ctx context.Context, status string) (int64, error)

Purge soft-deletes every job in the given status and returns how many rows were affected. Status must be one of: pending, running, completed, failed, scheduled, cancelled, dead.

func (*Queue) QueueName added in v1.0.3

func (q *Queue) QueueName() string

QueueName returns the Redis sorted set key this queue processes.

func (*Queue) Register

func (q *Queue) Register(jobType string, handler func(ctx context.Context, payload string) error)

Register associates a handler function with a named job type. Register is safe to call concurrently and may be called before or after Start.

func (*Queue) Reset added in v1.0.3

func (q *Queue) Reset(ctx context.Context, id string) error

Reset returns a failed or cancelled job to the pending state and re-enqueues it so it can be processed again. Dead jobs should use Retry instead. Returns an error if the job does not exist or is not in a resettable state.

func (*Queue) Resume added in v1.0.3

func (q *Queue) Resume()

Resume allows the worker pool to pop jobs again after a Pause.

func (*Queue) Retry added in v1.0.3

func (q *Queue) Retry(ctx context.Context, id string) error

Retry moves a single dead job back into the pending state, clears its retry counter and error, and re-enqueues it on the queue so it can run again with a fresh set of retries. It returns an error if the job does not exist or is not dead.

func (*Queue) RetryAllDead added in v1.0.3

func (q *Queue) RetryAllDead(ctx context.Context, queue string) (int, error)

RetryAllDead resets every dead job back to pending, clears retry counters and errors, and re-enqueues them all on the given Redis queue. It returns the number of jobs retried.

func (*Queue) RunOnce

func (q *Queue) RunOnce(ctx context.Context) error

RunOnce runs all registered workers once to process any pending jobs. It blocks until all workers complete. Call Connect() before RunOnce().

func (*Queue) Start

func (q *Queue) Start(ctx context.Context) error

Start blocks until the provided context is cancelled, at which point it performs a graceful shutdown and returns any accumulated error. If multiple subsystems fail, only the first error is returned.

func (*Queue) Stats added in v1.0.3

func (q *Queue) Stats(ctx context.Context) (QueueStats, error)

Stats returns a point-in-time snapshot of job counts and queue depth. See QueueStats for the exact semantics.

func (*Queue) Use

func (q *Queue) Use(mw Middleware)

Use registers a middleware function to be called for every job execution.

func (*Queue) WorkerCount added in v1.0.3

func (q *Queue) WorkerCount() int

WorkerCount returns the configured number of workers in the pool.

func (*Queue) Workers added in v1.0.3

func (q *Queue) Workers() []WorkerInfo

Workers returns a snapshot of the worker pool, one entry per configured worker, with its busy state.

type QueueInfo added in v1.0.3

type QueueInfo struct {
	QueueName           string        `json:"queue_name"`
	Workers             int           `json:"workers"`
	MetricsPort         int           `json:"metrics_port"`
	CallbackEnabled     bool          `json:"callback_enabled"`
	StaleJobTimeout     time.Duration `json:"stale_job_timeout"`
	SchedulerInterval   time.Duration `json:"scheduler_interval"`
	ReaperInterval      time.Duration `json:"reaper_interval"`
	OrphanCheckInterval time.Duration `json:"orphan_check_interval"`
}

QueueInfo is a read-only summary of how this queue is configured.

type QueueStats added in v1.0.3

type QueueStats struct {
	Pending    int64 `json:"pending"`
	Running    int64 `json:"running"`
	Completed  int64 `json:"completed"`
	Failed     int64 `json:"failed"`
	Scheduled  int64 `json:"scheduled"`
	Cancelled  int64 `json:"cancelled"`
	Dead       int64 `json:"dead"`
	Total      int64 `json:"total"`
	QueueDepth int64 `json:"queue_depth"`
}

QueueStats is a point-in-time snapshot of job counts. Because all kyu queues in a database share the same jobs table, the status counts are global to the database, while QueueDepth is the number of IDs waiting in this queue's Redis sorted set.

type WorkerInfo added in v1.0.3

type WorkerInfo struct {
	// ID is the worker's stable identifier, e.g. "worker-3".
	ID string `json:"id"`
	// Busy is true while the worker is executing a job.
	Busy bool `json:"busy"`
}

WorkerInfo describes one worker in this process's worker pool.

Directories

Path Synopsis
cmd
example command
Command example runs a self-contained, end-to-end kyu workflow that you can watch live on the web dashboard.
Command example runs a self-contained, end-to-end kyu workflow that you can watch live on the web dashboard.
kyu command
Command kyu is a small CLI for running and managing a kyu queue.
Command kyu is a small CLI for running and managing a kyu queue.
Package dashboard embeds the kyu web UI and serves it next to a JSON API backed by a live kyu.Queue.
Package dashboard embeds the kyu web UI and serves it next to a JSON API backed by a live kyu.Queue.
db

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL