Example: Task Queue¶
This is the canonical "process a queue" example. It registers one task, starts
the runner, dispatches a batch of messages, lets them process, and shuts down
cleanly. The full runnable source lives at
examples/simple.
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})
queue := redisstream.NewRedisStreamMessageQueueWithOptions(
rdb,
redisstream.WithPrefix("example"),
redisstream.WithQueue("tasks"),
redisstream.WithReClaimDelay(30*time.Second),
redisstream.WithDeleteOnAck(true),
)
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)
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
},
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
wg := sync.WaitGroup{}
wg.Add(1)
go func() { defer wg.Done(); _ = tr.Start(ctx) }()
for i := 0; i < 10; i++ {
_ = tr.Dispatch(context.Background(), "exampletask", strconv.Itoa(i))
}
time.Sleep(2 * time.Second)
cancel()
wg.Wait()
}
Walkthrough¶
1. Build the queue¶
queue := redisstream.NewRedisStreamMessageQueueWithOptions(
rdb,
redisstream.WithPrefix("example"), // stream -> "example:tasks"
redisstream.WithQueue("tasks"),
redisstream.WithReClaimDelay(30*time.Second),
redisstream.WithDeleteOnAck(true),
)
deleteOnAck: true keeps the stream from growing unbounded. reClaimDelay:
30s
is the visibility timeout -- a message unacked for 30s becomes eligible for
reclaim by another consumer.
2. Build the runner¶
NewTaskRunner does no I/O. The first Redis call happens when Start runs.
3. Register the task¶
tr.RegisterTask(&runner.Task{
Name: "exampletask",
MaxRetry: 5,
Action: func(ctx context.Context, payload any) error { ... },
})
The registry is in-memory and keyed by Name. Lookups happen on every
Dispatch and every worker invocation.
4. Start the runner¶
Start blocks until ctx is canceled, so run it in a goroutine. It spins up
fetchers, the worker pool, election, and the timing aggregator.
5. Dispatch¶
Dispatch is synchronous: it XADDs and returns. Payloads are JSON-marshalled,
so pass any serializable value.
6. Shut down¶
Canceling the context triggers a graceful drain: fetchers stop, the worker pool drains for up to 30s, the bulk writer flushes, the error channel closes.
Run it¶
The accompanying test (examples/simple/simple_test.go) uses miniredis, so it
runs with no real Redis:
Variations¶
- Multiple instances: run the binary twice; Redis Streams spreads work across both automatically.
- Real work: replace the
fmt.Printfaction with your business logic -- HTTP call, DB write, file processing. Remember to keep it idempotent. - Errors: return an error from
Actionto trigger a retry; setFailedTaskHandleron the config to handle terminal failures.