Example: Delayed Scheduler¶
This example schedules tasks to run in the future. The full source is at
examples/scheduler.
package main
import (
"context"
"fmt"
"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: 2,
}, rdb, queue)
tr.RegisterTask(&runner.Task{
Name: "runLater",
MaxRetry: 3,
Action: func(ctx context.Context, payload any) error {
fmt.Printf("executed: %v\n", payload)
return nil
},
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
wg := sync.WaitGroup{}
wg.Add(2)
go func() { defer wg.Done(); _ = tr.Start(ctx) }() // (1) workers + election
go func() { defer wg.Done(); _ = tr.StartDelayedSchedule(ctx, 1000) }() // (2) scheduler
_ = tr.DispatchDelayed(context.Background(), "runLater", "I run in ~5s", 5*time.Second)
_ = tr.DispatchDelayed(context.Background(), "runLater", "I run in ~10s", 10*time.Second)
time.Sleep(12 * time.Second)
cancel()
wg.Wait()
}
Walkthrough¶
Start two loops¶
go func() { _ = tr.Start(ctx) }() // workers + election
go func() { _ = tr.StartDelayedSchedule(ctx, 1000) }() // scheduler
Startruns the fetchers, workers, replication registrar, timing aggregator, and leader election.StartDelayedSchedule(ctx, batchSize)runs the leader-polled scheduler loop. ThebatchSize(here1000) is both the page size for due jobs and the capacity of the scheduler's internal dispatch pool.
Only the leader actually dispatches due jobs, but every instance should call
StartDelayedSchedule so that a leadership failover picks up the scheduler
without intervention.
Schedule with a relative delay¶
This ZADDs a DelayedTask into taskrunner:<group>:delayed_tasks with
score = time.Now().Add(5s).Unix(). The minimum effective delay is 5s; shorter
delays return ErrDurationIsSmallerThanCheckCycle.
Or schedule for an absolute time¶
when := time.Now().Add(1 * time.Hour)
_ = tr.ScheduleFor(context.Background(), "runLater", "hourly", when)
ScheduleFor is the absolute-time counterpart to DispatchDelayed's
relative duration.
What the leader does every 5 seconds¶
sequenceDiagram
participant L as Leader
participant Z as delayed_tasks ZSET
participant S as Stream
L->>Z: ZCOUNT score in [0, now]
Z-->>L: N due
L->>Z: ZRANGEBYSCORE (paged, batchSize)
Z-->>L: due DelayedTask JSON
loop each due job
L->>S: Dispatch -> XADD
L->>Z: ZREM member
end
Because ZREM happens after XADD, a crash between the two means a job could
be dispatched twice on recovery. Keep your Action idempotent.
Run it¶
go run examples/scheduler/main.go
# executed: I run in ~5s (~5s in)
# executed: I run in ~10s (~10s in)
The test (examples/scheduler/scheduler_test.go) uses miniredis:
When to reach for delayed vs. retry¶
- Delayed = you know at dispatch time the job should run later (reminders, scheduled maintenance, backoff windows you compute yourself).
- Retry (via
MaxRetry+ the pending reclaim cycle) = the job failed and should be redelivered automatically. See Delayed Scheduling.