sidekiq

package module
v0.0.0-...-8e687c1 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 11 Imported by: 0

README

go-ruby-sidekiq/sidekiq

sidekiq — go-ruby-sidekiq

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the core engine of the sidekiq background-job framework — the client that enqueues jobs, the job/worker option model, the scheduled-job poller and a basic server/processor, all backed by Redis. It writes the exact same Redis data structures a real Sidekiq process does, so a real Sidekiq server can consume jobs this package enqueues, and this package can process jobs a real Sidekiq client wrote — the payloads are byte-compatible.

It is the Sidekiq backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-redis, go-ruby-regexp and go-ruby-marshal.

What it is — and isn't. Enqueue, scheduling, retry backoff and the dead-set machinery are fully deterministic and live here as pure Go. The two interpreter-/host-dependent pieces are seams: a job's perform body is Ruby, injected as a Perform function; and the Redis socket is a go-redis client the caller supplies. Time, the 24-hex job id and retry jitter are injectable too, so the whole engine runs deterministically under test with no real Redis server, no sleeps and no background goroutines.

Features

  • Byte-compatible payloadClient.Push writes the documented Sidekiq job hash {"retry","queue","class","args","jid","created_at","enqueued_at"} (jid = 24 hex chars, SecureRandom.hex(12); timestamps = float epoch seconds, Time.now.to_f) LPUSH'd to queue:<name>, with the queue name SADD'd to the queues set — exactly as Sidekiq's client does.
  • Bulk enqueueClient.PushBulk mirrors Sidekiq::Client#push_bulk.
  • Worker optionsClient.Worker(class, WorkerOptions{Queue, Retry}) models sidekiq_options, exposing PerformAsync (enqueue now), PerformAt and PerformIn (ZADD to the schedule sorted-set scored by run-at time).
  • Scheduler / pollerClient.EnqueueScheduledJobs moves due jobs from the schedule and retry sorted-sets onto their queues, claiming each with ZREM so concurrent pollers never double-enqueue. Deterministic against an injected clock; no sleeps, no goroutine.
  • Server / processorClient.NewProcessor(perform, queues...) BRPOPs a job, decodes it and invokes the Perform seam, then does Sidekiq's bookkeeping: bumps stat:processed / stat:failed, and on failure schedules a retry (ZADD retry) with Sidekiq's exact backoff count⁴ + 15 + jitter, or moves the job to the dead set (trimmed to 10 000 jobs / 6 months) once attempts are exhausted. retry_count, failed_at, retried_at, error_class and error_message are recorded on the payload exactly as Sidekiq records them.
  • StatsClient.Stats snapshots the Sidekiq::Stats counters and set sizes (processed, failed, enqueued, scheduled, retries, dead).
  • CGO-free and validated on all six supported 64-bit targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) across Linux, macOS and Windows.

Usage

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/go-ruby-sidekiq/sidekiq"
	"github.com/redis/go-redis/v9"
)

func main() {
	ctx := context.Background()
	rdb := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})
	c := sidekiq.New(rdb)

	// Enqueue via a worker (models a class with sidekiq_options).
	mailer := c.Worker("EmailJob", sidekiq.WorkerOptions{Queue: "mailers", Retry: 3})
	jid, _ := mailer.PerformAsync(ctx, "user@example.com")
	fmt.Println("enqueued", jid)

	// Schedule for later (ZADD to the schedule set).
	mailer.PerformIn(ctx, 5*time.Minute, "later@example.com")

	// Move due scheduled/retry jobs onto their queues (call on a cadence).
	c.EnqueueScheduledJobs(ctx)

	// Run jobs. The perform body is the host seam (Ruby, in go-embedded-ruby).
	perform := func(class string, args []any) error {
		fmt.Printf("run %s%v\n", class, args)
		return nil // return an error to trigger retry/dead handling
	}
	p := c.NewProcessor(perform, "mailers", "default")
	p.ProcessOne(ctx) // or p.Run(ctx) to loop until the context is cancelled
}

API

func New(rdb redis.Cmdable, opts ...Option) *Client

// options (all injectable for deterministic testing)
func WithClock(func() time.Time) Option   // created_at / enqueued_at / scoring
func WithRand(io.Reader) Option            // 24-hex jid entropy
func WithJitter(func(count int) int) Option // retry jitter (seconds)
func WithMaxRetries(int) Option            // default max attempts (25)

// client
func (c *Client) Push(ctx, Item) (jid string, err error)
func (c *Client) PushBulk(ctx, Item, argsList [][]any) (jids []string, err error)
func (c *Client) Worker(class string, opts WorkerOptions) *Worker
func (c *Client) EnqueueScheduledJobs(ctx) (moved int, err error)
func (c *Client) NewProcessor(perform Perform, queues ...string) *Processor
func (c *Client) Stats(ctx) (Stats, error)

// worker (models sidekiq_options + perform_async / perform_at / perform_in)
func (w *Worker) PerformAsync(ctx, args ...any) (jid string, err error)
func (w *Worker) PerformAt(ctx, t time.Time, args ...any) (jid string, err error)
func (w *Worker) PerformIn(ctx, d time.Duration, args ...any) (jid string, err error)

// processor (the server)
type Perform func(class string, args []any) error
func (p *Processor) SetTimeout(d time.Duration)
func (p *Processor) ProcessOne(ctx) (processed bool, err error)
func (p *Processor) Run(ctx) error

// carry a Ruby exception class name through a failed job's error
type ClassedError interface { error; ErrorClass() string }

The Perform seam is the one inherently interpreter-dependent piece — a worker's perform method is Ruby — so a host (for example go-embedded-ruby) dispatches (class, args) to the matching Ruby body. To record the true Ruby exception class on a retry, the returned error may implement ClassedError; otherwise the Go type name is used.

Tests & coverage

The suite runs entirely against miniredis — a pure-Go, in-process Redis started per test and closed in t.Cleanup, so there is no external Redis server and no leaked goroutines. miniredis is strictly test-scoped (imported only from _test.go); the sole runtime dependency is the go-redis client. Time, randomness and retry jitter are injected, so payload byte-exactness, scheduled→enqueue promotion and process→success/retry/dead transitions are all asserted deterministically, with every Redis transport-error branch driven via a go-redis fault hook.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

CGO-free, gofmt + go vet clean, -race clean, 100% line coverage, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three OSes (Linux, macOS, Windows).

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-sidekiq/sidekiq authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package sidekiq is a pure-Go (no cgo) reimplementation of the core engine of the Ruby Sidekiq background-job framework: the client that enqueues jobs, the job/worker option model, the scheduled-job poller and a basic server/processor — backed by Redis.

It writes the exact same Redis data structures a real Sidekiq process does, so a real Sidekiq server can consume jobs this package enqueues and vice versa. The job payload is the documented Sidekiq JSON hash (class, args, queue, jid, created_at, enqueued_at, retry) LPUSH'd to queue:<name> with the queue name SADD'd to the queues set; scheduled jobs are ZADD'd to the schedule sorted-set keyed by their run-at timestamp; retries land in the retry sorted-set and exhausted jobs in the dead set — all byte-compatible with Sidekiq.

The only piece that is inherently interpreter-dependent is the body of a job (a Worker's perform method, which is Ruby). That is modelled as an injected host seam — a Perform function — mirroring the go-ruby-* design: the deterministic enqueue/schedule/retry machinery lives here in pure Go, and the host (for example go-embedded-ruby) supplies the perform bodies. The Redis socket itself is likewise a seam: the caller supplies a go-redis client.

Time, randomness (the 24-hex jid) and retry jitter are all injectable so the whole engine runs deterministically under test with no real Redis server, no sleeps and no background goroutines.

Index

Constants

View Source
const DefaultMaxRetries = 25

DefaultMaxRetries is the number of retry attempts Sidekiq makes before a job is moved to the dead set when its retry option is true (Sidekiq's DEFAULT_MAX_RETRY_ATTEMPTS).

View Source
const DefaultQueue = "default"

DefaultQueue is the queue a job lands on when none is specified, matching Sidekiq's default.

Variables

This section is empty.

Functions

This section is empty.

Types

type ClassedError

type ClassedError interface {
	error
	// ErrorClass returns the Ruby exception class name, e.g. "RuntimeError".
	ErrorClass() string
}

ClassedError lets a host carry the Ruby exception class name of a failed job through the Go error returned by a Perform seam. When a job's perform body (Ruby) raises, the host wraps the raised exception in an error implementing this interface so the retry machinery records the true Ruby class name in the payload's error_class field, matching Sidekiq.

type Client

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

Client enqueues jobs into Redis exactly as Sidekiq's client does. It owns no socket: the caller supplies a go-redis client (a redis.Cmdable, which a *redis.Client satisfies), mirroring the go-ruby-* host-seam design. Time, randomness and retry jitter are injectable via Option for deterministic testing.

A Client is safe for concurrent use to the same extent its underlying go-redis client is.

func New

func New(rdb redis.Cmdable, opts ...Option) *Client

New builds a Client over the supplied go-redis client. With no options it is production-ready: real time, crypto/rand job ids and randomised retry jitter.

func (*Client) EnqueueScheduledJobs

func (c *Client) EnqueueScheduledJobs(ctx context.Context) (int, error)

EnqueueScheduledJobs moves every job in the schedule and retry sorted-sets whose run-at time is now due (score <= now) onto its target queue. It is the pure-Go model of Sidekiq's Scheduled::Poller#enqueue: a host calls it on whatever cadence it likes, and it advances deterministically against the Client's injected clock — no sleeps, no background goroutine. It returns the number of jobs enqueued.

func (*Client) NewProcessor

func (c *Client) NewProcessor(perform Perform, queues ...string) *Processor

NewProcessor builds a Processor that fetches from the given queues (in priority order, highest first) and runs jobs through perform. With no queues it defaults to DefaultQueue. The BRPOP wait defaults to one second and can be tuned with Processor.SetTimeout.

func (*Client) Push

func (c *Client) Push(ctx context.Context, item Item) (string, error)

Push enqueues a single job and returns its jid. A job with a zero At is pushed onto its queue immediately; a job with a non-zero At is added to the schedule set to run later. This mirrors Sidekiq::Client#push.

func (*Client) PushBulk

func (c *Client) PushBulk(ctx context.Context, item Item, argsList [][]any) ([]string, error)

PushBulk enqueues many jobs that share a class/queue/retry policy, one per entry in argsList, and returns their jids in order. It mirrors Sidekiq::Client#push_bulk. A non-zero At schedules every job for that time.

func (*Client) Stats

func (c *Client) Stats(ctx context.Context) (Stats, error)

Stats gathers a Stats snapshot from Redis.

func (*Client) Worker

func (c *Client) Worker(class string, opts WorkerOptions) *Worker

Worker returns a Worker for the named class with the given options. This mirrors declaring a class that includes Sidekiq::Job and calls sidekiq_options(...).

type Item

type Item struct {
	// Class is the worker class name whose perform will run.
	Class string
	// Args are the positional arguments passed to perform.
	Args []any
	// Queue is the target queue; empty means [DefaultQueue].
	Queue string
	// Retry is the retry policy: nil (default true), a bool, or an int max
	// attempt count.
	Retry any
	// At, when non-zero, schedules the job to run at that time (ZADD to the
	// schedule set) instead of enqueuing it immediately.
	At time.Time
	// Jid is the job id; when empty a fresh 24-hex id is generated.
	Jid string
}

Item describes a job to enqueue. Only Class is required; Queue defaults to DefaultQueue, Retry defaults to true, Jid is generated when empty, and a non-zero At schedules the job for that time rather than enqueuing it now.

type Option

type Option func(*Client)

Option configures a Client. All the injectable seams (clock, randomness, retry jitter) default to production values, so New(rdb) yields a fully working client; tests override them for determinism.

func WithClock

func WithClock(clock func() time.Time) Option

WithClock injects the time source used for created_at / enqueued_at stamps, schedule scoring and retry timing. It defaults to time.Now.

func WithJitter

func WithJitter(jitter func(count int) int) Option

WithJitter injects the retry-jitter function. Given the current retry count it returns an added delay in seconds; Sidekiq's default is a random value in [0, 30*(count+1)). It defaults to the randomised production jitter.

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries sets the default maximum retry attempts for jobs whose retry option is true. It defaults to DefaultMaxRetries.

func WithRand

func WithRand(r io.Reader) Option

WithRand injects the entropy source for generating the 24-hex job id. It defaults to crypto/rand.Reader.

type Perform

type Perform func(class string, args []any) error

Perform is the host seam that runs a job's body. Given a worker class name and its decoded arguments it executes the work and returns nil on success or an error on failure. This is the one inherently interpreter-dependent piece of Sidekiq — a worker's perform method is Ruby — so it is injected: a host such as go-embedded-ruby dispatches (class, args) to the matching Ruby perform body. To record the true Ruby exception class on a retry, the returned error may implement ClassedError.

type Processor

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

Processor is the pure-Go model of a Sidekiq server process: it fetches a job off a queue, decodes it and invokes the Perform seam, then does Sidekiq's success/failure bookkeeping (processed/failed counters, retry scheduling and the dead set). It spawns no goroutines; a host drives it by calling Processor.ProcessOne or Processor.Run.

func (*Processor) ProcessOne

func (p *Processor) ProcessOne(ctx context.Context) (processed bool, err error)

ProcessOne fetches at most one job (BRPOP across the configured queues, highest priority first) and runs it. It reports whether a job was processed; a false result with a nil error means the fetch timed out with no work. Any error from the job's perform body is handled internally (retry or dead set) and is not returned; the returned error is reserved for Redis/transport failures.

func (*Processor) Run

func (p *Processor) Run(ctx context.Context) error

Run drives the processor until ctx is cancelled, fetching and running jobs in a loop. It returns ctx.Err() when the context is done, or any Redis/transport error encountered. It runs entirely on the calling goroutine — no background goroutines are started.

func (*Processor) SetTimeout

func (p *Processor) SetTimeout(d time.Duration)

SetTimeout sets the BRPOP block duration used when waiting for a job.

type Stats

type Stats struct {
	// Processed is the lifetime count of processed jobs (stat:processed).
	Processed int64
	// Failed is the lifetime count of failed jobs (stat:failed).
	Failed int64
	// Enqueued is the total number of jobs waiting across all known queues.
	Enqueued int64
	// Scheduled, Retries and Dead are the cardinalities of the schedule, retry
	// and dead sorted-sets.
	Scheduled int64
	Retries   int64
	Dead      int64
}

Stats is a snapshot of the engine's global counters and set sizes, modelling Sidekiq::Stats: lifetime processed/failed totals, the number of jobs waiting across all queues, and the sizes of the schedule, retry and dead sets.

type Worker

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

Worker binds a worker class name and its options to a Client, exposing the familiar perform_async / perform_in / perform_at enqueue methods. It is the Go analogue of a class that includes Sidekiq::Job and calls sidekiq_options.

func (*Worker) PerformAsync

func (w *Worker) PerformAsync(ctx context.Context, args ...any) (string, error)

PerformAsync enqueues the job to run now (Sidekiq's perform_async) and returns its jid.

func (*Worker) PerformAt

func (w *Worker) PerformAt(ctx context.Context, t time.Time, args ...any) (string, error)

PerformAt schedules the job to run at time t (Sidekiq's perform_at) and returns its jid.

func (*Worker) PerformIn

func (w *Worker) PerformIn(ctx context.Context, interval time.Duration, args ...any) (string, error)

PerformIn schedules the job to run after the given interval from now (Sidekiq's perform_in). A non-positive interval enqueues immediately, exactly as Sidekiq does.

type WorkerOptions

type WorkerOptions struct {
	// Queue is the queue jobs of this worker run on; empty means [DefaultQueue].
	Queue string
	// Retry is the retry policy: nil (default true), a bool, or an int max
	// attempt count, matching sidekiq_options retry:.
	Retry any
}

WorkerOptions models a worker's sidekiq_options declaration: the per-class defaults applied to every job it enqueues. A zero value means "use the engine defaults" (queue "default", retry true).

Jump to

Keyboard shortcuts

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