Getting Started¶
This page walks you from zero to a running, horizontally-scalable task queue. You will register a task, start a worker, dispatch messages, and observe them being processed.
Requirements¶
| Requirement | Detail |
|---|---|
| Go | 1.24.9 or newer (see go.mod). The module uses recent generics and stdlib features. |
| Redis | 6.0 minimum. 6.2+ recommended -- XPENDING's IDLE filter (used for efficient pending reclaims) needs >= 6.2; older versions fall back to client-side filtering. |
| Network | Each TaskRunner instance needs TCP reachability to Redis. No peer-to-peer networking between instances -- they coordinate entirely through Redis. |
No Redis for local dev?
The test suite (and the examples' tests) use
miniredis, an in-process Redis
compatible server. You do not need a real Redis to run go test ./....
Installation¶
TaskRunner ships its dependencies vendored in vendor/, so if your module
is in vendor mode (GOFLAGS=-mod=vendor) it will just work. Otherwise go mod
will resolve them normally.
The five-minute quickstart¶
This is the canonical "process a queue" program. It registers one task, starts the runner, dispatches 100 messages, and then shuts down cleanly.
package main
import (
"context"
"fmt"
"strconv"
"sync"
"time"
"github.com/redis/go-redis/v9"
"github.com/soroosh-tanzadeh/taskrunner/redisstream"
"github.com/soroosh-tanzadeh/taskrunner/runner"
)
func main() {
rdb := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379", DB: 0, PoolSize: 50})
// 1. Build the queue backend (Redis Streams, the shipped implementation).
queue := redisstream.NewRedisStreamMessageQueueWithOptions(
rdb,
redisstream.WithPrefix("example"),
redisstream.WithQueue("tasks"),
redisstream.WithReClaimDelay(30*time.Second),
redisstream.WithDeleteOnAck(true),
)
// 2. Build the runner.
tr := runner.NewTaskRunner(runner.TaskRunnerConfig{
BatchSize: 10,
ConsumerGroup: "example",
ConsumersPrefix: "default",
NumWorkers: 8,
NumFetchers: 4,
LongQueueHook: func(s runner.Stats) { fmt.Printf("%+v\n", s) },
LongQueueThreshold: 30 * time.Second,
}, rdb, queue)
// 3. Register the task and its handler.
tr.RegisterTask(&runner.Task{
Name: "exampletask",
MaxRetry: 5,
Action: func(ctx context.Context, payload any) error {
fmt.Printf("Hello from example task %v\n", payload)
return nil
},
})
// 4. Start the runner in the background. Start() blocks until ctx is canceled.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
wg := sync.WaitGroup{}
wg.Add(1)
go func() { defer wg.Done(); _ = tr.Start(ctx) }()
// 5. Dispatch work.
for i := 0; i < 100; i++ {
_ = tr.Dispatch(context.Background(), "exampletask", strconv.Itoa(i))
}
time.Sleep(2 * time.Second)
cancel() // graceful shutdown
wg.Wait()
}
What just happened?¶
redisstream.NewRedisStreamMessageQueueWithOptionswires a Redis Streams queue.WithDeleteOnAck(true)deletes the stream entry after processing so the stream never grows unbounded.WithReClaimDelay(30s)is the visibility timeout -- a message that has not been acked within 30s becomes eligible for reclaim by another consumer.NewTaskRunnerconstructs the engine. It does not connect to Redis eagerly; the first network call happens whenStart()runs.RegisterTaskadds the task to an in-memory registry keyed by name. The registry is asafemap.SafeMap, read on everyDispatchand every worker invocation.Start(ctx)spins upNumFetchersfetcher goroutines, anantsworker pool ofNumWorkers, the replication registrar, the timing-aggregation loop, and leader election. It blocks untilctxis canceled.Dispatchlooks up the task, builds aTaskMessage, JSON-marshals it, andXADDs it to the stream. Workers pick it up viaXREADGROUP.
Running multiple instances¶
Horizontal scaling is automatic -- just run the same binary more than once,
pointed at the same Redis and the same ConsumerGroup. Redis Streams
consumer-group semantics ensure each message is delivered to exactly one
consumer in the group.
# Terminal 1
REDIS_ADDR=127.0.0.1:6379 ./myapp
# Terminal 2 -- shares the load
REDIS_ADDR=127.0.0.1:6379 ./myapp
One of the instances will win leader election. Only the leader runs the delayed scheduler, computes timing stats, and publishes worker-tuning capacity. Followers apply the leader's published capacity. The workload (message processing) is shared across all instances.
Next steps¶
- Architecture -- the fetcher/worker split, the message lifecycle, and how heartbeats prevent double-processing.
- Delayed Scheduling -- run jobs in the
future with
DispatchDelayedandScheduleFor. - Unique Jobs -- prevent duplicate enqueues.
- Worker Tuning -- let the leader autoscale the worker pool based on predicted wait time.
- Configuration Reference -- every field, defaults, and tuning advice.