mirror of
https://github.com/hibiken/asynq.git
synced 2025-09-17 20:30:06 +08:00
Compare commits
39 Commits
dependabot
...
sohail/rel
Author | SHA1 | Date | |
---|---|---|---|
![]() |
117f009f17 | ||
![]() |
3530465e1a | ||
![]() |
917d6f7a7d | ||
![]() |
261e813576 | ||
![]() |
02c6dae7eb | ||
![]() |
5cfcb71139 | ||
![]() |
c78e7b0ccd | ||
![]() |
b4db174032 | ||
![]() |
39f1d8c3e6 | ||
![]() |
e70de721b8 | ||
![]() |
6c06ad7e45 | ||
![]() |
a676d3d2fa | ||
![]() |
ef0d32965f | ||
![]() |
f16f9ac440 | ||
![]() |
63f7cb7b17 | ||
![]() |
04b3a3475d | ||
![]() |
013190b824 | ||
![]() |
1e102a5392 | ||
![]() |
e1a8a366a6 | ||
![]() |
c8c8adfaa6 | ||
![]() |
03f4799712 | ||
![]() |
3f4e211a3b | ||
![]() |
0655c569f5 | ||
![]() |
95a0768ae0 | ||
![]() |
f4b56498f2 | ||
![]() |
ae478d5b22 | ||
![]() |
ff7ef48463 | ||
![]() |
b1e13893ff | ||
![]() |
0dc670d7d8 | ||
![]() |
461d922616 | ||
![]() |
5daa3c52ed | ||
![]() |
d04888e748 | ||
![]() |
174008843d | ||
![]() |
2b632b93d5 | ||
![]() |
b35b559d40 | ||
![]() |
8df0bfa583 | ||
![]() |
b25d10b61d | ||
![]() |
38f7499b71 | ||
![]() |
9e548fc097 |
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -3,13 +3,20 @@ name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] Description of the bug"
|
||||
labels: bug
|
||||
assignees: hibiken
|
||||
|
||||
assignees:
|
||||
- hibiken
|
||||
- kamikazechaser
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS: [e.g. MacOS, Linux]
|
||||
- `asynq` package version [e.g. v0.25.0]
|
||||
- Redis/Valkey version
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior (Code snippets if applicable):
|
||||
1. Setup background processing ...
|
||||
@@ -22,9 +29,5 @@ A clear and concise description of what you expected to happen.
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS: [e.g. MacOS, Linux]
|
||||
- Version of `asynq` package [e.g. v1.0.0]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -3,7 +3,9 @@ name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE REQUEST] Description of the feature request"
|
||||
labels: enhancement
|
||||
assignees: hibiken
|
||||
assignees:
|
||||
- hibiken
|
||||
- kamikazechaser
|
||||
|
||||
---
|
||||
|
||||
|
5
.github/dependabot.yaml
vendored
5
.github/dependabot.yaml
vendored
@@ -18,4 +18,7 @@ updates:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "pr-deps"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
12
.github/workflows/benchstat.yml
vendored
12
.github/workflows/benchstat.yml
vendored
@@ -18,9 +18,9 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
go-version: 1.23.x
|
||||
- name: Benchmark
|
||||
run: go test -run=^$ -bench=. -count=5 -timeout=60m ./... | tee -a new.txt
|
||||
- name: Upload Benchmark
|
||||
@@ -42,9 +42,9 @@ jobs:
|
||||
with:
|
||||
ref: master
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
go-version: 1.23.x
|
||||
- name: Benchmark
|
||||
run: go test -run=^$ -bench=. -count=5 -timeout=60m ./... | tee -a old.txt
|
||||
- name: Upload Benchmark
|
||||
@@ -60,9 +60,9 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
go-version: 1.23.x
|
||||
- name: Install benchstat
|
||||
run: go get -u golang.org/x/perf/cmd/benchstat
|
||||
- name: Download Incoming
|
||||
|
22
.github/workflows/build.yml
vendored
22
.github/workflows/build.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
go-version: [1.20.x, 1.21.x]
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
runs-on: ${{ matrix.os }}
|
||||
services:
|
||||
redis:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
go-version: [1.20.x, 1.21.x]
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
runs-on: ${{ matrix.os }}
|
||||
services:
|
||||
redis:
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
@@ -67,3 +67,17 @@ jobs:
|
||||
- name: Test tools module
|
||||
run: cd tools && go test -race -v ./... && cd ..
|
||||
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.61
|
||||
|
28
CHANGELOG.md
28
CHANGELOG.md
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.25.0] - 2024-10-29
|
||||
|
||||
### Upgrades
|
||||
- Minumum go version is set to 1.22 (PR: https://github.com/hibiken/asynq/pull/925)
|
||||
- Internal protobuf package is upgraded to address security advisories (PR: https://github.com/hibiken/asynq/pull/925)
|
||||
- Most packages are upgraded
|
||||
- CI/CD spec upgraded
|
||||
|
||||
### Added
|
||||
- `IsPanicError` function is introduced to support catching of panic errors when processing tasks (PR: https://github.com/hibiken/asynq/pull/491)
|
||||
- `JanitorInterval` and `JanitorBatchSize` are added as Server options (PR: https://github.com/hibiken/asynq/pull/715)
|
||||
- `NewClientFromRedisClient` is introduced to allow reusing an existing redis client (PR: https://github.com/hibiken/asynq/pull/742)
|
||||
- `TaskCheckInterval` config option is added to specify the interval between checks for new tasks to process when all queues are empty (PR: https://github.com/hibiken/asynq/pull/694)
|
||||
- `Ping` method is added to Client, Server and Scheduler ((PR: https://github.com/hibiken/asynq/pull/585))
|
||||
- `RevokeTask` error type is introduced to prevent a task from being retried or archived (PR: https://github.com/hibiken/asynq/pull/882)
|
||||
- `SentinelUsername` is added as a redis config option (PR: https://github.com/hibiken/asynq/pull/924)
|
||||
- Some jitter is introduced to improve latency when fetching jobs in the processor (PR: https://github.com/hibiken/asynq/pull/868)
|
||||
- Add task enqueue command to the CLI (PR: https://github.com/hibiken/asynq/pull/918)
|
||||
- Add a map cache (concurrent safe) to keep track of queues that ultimately reduces redis load when enqueuing tasks (PR: https://github.com/hibiken/asynq/pull/946)
|
||||
|
||||
### Fixes
|
||||
- Archived tasks that are trimmed should now be deleted (PR: https://github.com/hibiken/asynq/pull/743)
|
||||
- Fix lua script when listing task messages with an expired lease (PR: https://github.com/hibiken/asynq/pull/709)
|
||||
- Fix potential context leaks due to cancellation not being called (PR: https://github.com/hibiken/asynq/pull/926)
|
||||
- Misc documentation fixes
|
||||
- Misc test fixes
|
||||
|
||||
|
||||
## [0.24.1] - 2023-05-01
|
||||
|
||||
### Changed
|
||||
|
6
Makefile
6
Makefile
@@ -4,4 +4,8 @@ proto: internal/proto/asynq.proto
|
||||
protoc -I=$(ROOT_DIR)/internal/proto \
|
||||
--go_out=$(ROOT_DIR)/internal/proto \
|
||||
--go_opt=module=github.com/hibiken/asynq/internal/proto \
|
||||
$(ROOT_DIR)/internal/proto/asynq.proto
|
||||
$(ROOT_DIR)/internal/proto/asynq.proto
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
golangci-lint run
|
||||
|
10
README.md
10
README.md
@@ -37,7 +37,6 @@ Task queues are used as a mechanism to distribute work across multiple machines.
|
||||
- [Flexible handler interface with support for middlewares](https://github.com/hibiken/asynq/wiki/Handler-Deep-Dive)
|
||||
- [Ability to pause queue](/tools/asynq/README.md#pause) to stop processing tasks from the queue
|
||||
- [Periodic Tasks](https://github.com/hibiken/asynq/wiki/Periodic-Tasks)
|
||||
- [Support Redis Cluster](https://github.com/hibiken/asynq/wiki/Redis-Cluster) for automatic sharding and high availability
|
||||
- [Support Redis Sentinels](https://github.com/hibiken/asynq/wiki/Automatic-Failover) for high availability
|
||||
- Integration with [Prometheus](https://prometheus.io/) to collect and visualize queue metrics
|
||||
- [Web UI](#web-ui) to inspect and remote-control queues and tasks
|
||||
@@ -45,16 +44,19 @@ Task queues are used as a mechanism to distribute work across multiple machines.
|
||||
|
||||
## Stability and Compatibility
|
||||
|
||||
**Status**: The library is currently undergoing **heavy development** with frequent, breaking API changes.
|
||||
**Status**: The library relatively stable and is currently undergoing **moderate development** with less frequent breaking API changes.
|
||||
|
||||
> ☝️ **Important Note**: Current major version is zero (`v0.x.x`) to accommodate rapid development and fast iteration while getting early feedback from users (_feedback on APIs are appreciated!_). The public API could change without a major version update before `v1.0.0` release.
|
||||
|
||||
### Redis Cluster Compatibility
|
||||
|
||||
Some of the lua scripts in this library may not be compatible with Redis Cluster.
|
||||
|
||||
## Sponsoring
|
||||
If you are using this package in production, **please consider sponsoring the project to show your support!**
|
||||
|
||||
## Quickstart
|
||||
|
||||
Make sure you have Go installed ([download](https://golang.org/dl/)). Latest two Go versions are supported (See https://go.dev/dl).
|
||||
Make sure you have Go installed ([download](https://golang.org/dl/)). The **last two** Go versions are supported (See https://go.dev/dl).
|
||||
|
||||
Initialize your project by creating a folder and then running `go mod init github.com/your/repo` ([learn more](https://blog.golang.org/using-go-modules)) inside the folder. Then install Asynq library with the [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command:
|
||||
|
||||
|
4
asynq.go
4
asynq.go
@@ -316,6 +316,9 @@ type RedisFailoverClientOpt struct {
|
||||
// https://redis.io/topics/sentinel.
|
||||
SentinelAddrs []string
|
||||
|
||||
// Redis sentinel username.
|
||||
SentinelUsername string
|
||||
|
||||
// Redis sentinel password.
|
||||
SentinelPassword string
|
||||
|
||||
@@ -364,6 +367,7 @@ func (opt RedisFailoverClientOpt) MakeRedisClient() interface{} {
|
||||
return redis.NewFailoverClient(&redis.FailoverOptions{
|
||||
MasterName: opt.MasterName,
|
||||
SentinelAddrs: opt.SentinelAddrs,
|
||||
SentinelUsername: opt.SentinelUsername,
|
||||
SentinelPassword: opt.SentinelPassword,
|
||||
Username: opt.Username,
|
||||
Password: opt.Password,
|
||||
|
@@ -55,7 +55,7 @@ func BenchmarkEndToEndSimple(b *testing.B) {
|
||||
}
|
||||
b.StartTimer() // end setup
|
||||
|
||||
srv.Start(HandlerFunc(handler))
|
||||
_ = srv.Start(HandlerFunc(handler))
|
||||
wg.Wait()
|
||||
|
||||
b.StopTimer() // begin teardown
|
||||
@@ -117,7 +117,7 @@ func BenchmarkEndToEnd(b *testing.B) {
|
||||
}
|
||||
b.StartTimer() // end setup
|
||||
|
||||
srv.Start(HandlerFunc(handler))
|
||||
_ = srv.Start(HandlerFunc(handler))
|
||||
wg.Wait()
|
||||
|
||||
b.StopTimer() // begin teardown
|
||||
@@ -174,7 +174,7 @@ func BenchmarkEndToEndMultipleQueues(b *testing.B) {
|
||||
}
|
||||
b.StartTimer() // end setup
|
||||
|
||||
srv.Start(HandlerFunc(handler))
|
||||
_ = srv.Start(HandlerFunc(handler))
|
||||
wg.Wait()
|
||||
|
||||
b.StopTimer() // begin teardown
|
||||
@@ -215,7 +215,7 @@ func BenchmarkClientWhileServerRunning(b *testing.B) {
|
||||
handler := func(ctx context.Context, t *Task) error {
|
||||
return nil
|
||||
}
|
||||
srv.Start(HandlerFunc(handler))
|
||||
_ = srv.Start(HandlerFunc(handler))
|
||||
|
||||
b.StartTimer() // end setup
|
||||
|
||||
|
33
client.go
33
client.go
@@ -10,11 +10,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
"github.com/hibiken/asynq/internal/errors"
|
||||
"github.com/hibiken/asynq/internal/rdb"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// A Client is responsible for scheduling tasks.
|
||||
@@ -25,15 +25,26 @@ import (
|
||||
// Clients are safe for concurrent use by multiple goroutines.
|
||||
type Client struct {
|
||||
broker base.Broker
|
||||
// When a Client has been created with an existing Redis connection, we do
|
||||
// not want to close it.
|
||||
sharedConnection bool
|
||||
}
|
||||
|
||||
// NewClient returns a new Client instance given a redis connection option.
|
||||
func NewClient(r RedisConnOpt) *Client {
|
||||
c, ok := r.MakeRedisClient().(redis.UniversalClient)
|
||||
redisClient, ok := r.MakeRedisClient().(redis.UniversalClient)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("asynq: unsupported RedisConnOpt type %T", r))
|
||||
}
|
||||
return &Client{broker: rdb.NewRDB(c)}
|
||||
client := NewClientFromRedisClient(redisClient)
|
||||
client.sharedConnection = false
|
||||
return client
|
||||
}
|
||||
|
||||
// NewClientFromRedisClient returns a new instance of Client given a redis.UniversalClient
|
||||
// Warning: The underlying redis connection pool will not be closed by Asynq, you are responsible for closing it.
|
||||
func NewClientFromRedisClient(c redis.UniversalClient) *Client {
|
||||
return &Client{broker: rdb.NewRDB(c), sharedConnection: true}
|
||||
}
|
||||
|
||||
type OptionType int
|
||||
@@ -150,9 +161,9 @@ func (t deadlineOption) Value() interface{} { return time.Time(t) }
|
||||
// TTL duration must be greater than or equal to 1 second.
|
||||
//
|
||||
// Uniqueness of a task is based on the following properties:
|
||||
// - Task Type
|
||||
// - Task Payload
|
||||
// - Queue Name
|
||||
// - Task Type
|
||||
// - Task Payload
|
||||
// - Queue Name
|
||||
func Unique(ttl time.Duration) Option {
|
||||
return uniqueOption(ttl)
|
||||
}
|
||||
@@ -307,6 +318,9 @@ var (
|
||||
|
||||
// Close closes the connection with redis.
|
||||
func (c *Client) Close() error {
|
||||
if c.sharedConnection {
|
||||
return fmt.Errorf("redis connection is shared so the Client can't be closed through asynq")
|
||||
}
|
||||
return c.broker.Close()
|
||||
}
|
||||
|
||||
@@ -405,6 +419,11 @@ func (c *Client) EnqueueContext(ctx context.Context, task *Task, opts ...Option)
|
||||
return newTaskInfo(msg, state, opt.processAt, nil), nil
|
||||
}
|
||||
|
||||
// Ping performs a ping against the redis connection.
|
||||
func (c *Client) Ping() error {
|
||||
return c.broker.Ping()
|
||||
}
|
||||
|
||||
func (c *Client) enqueue(ctx context.Context, msg *base.TaskMessage, uniqueTTL time.Duration) error {
|
||||
if uniqueTTL > 0 {
|
||||
return c.broker.EnqueueUnique(ctx, msg, uniqueTTL)
|
||||
@@ -414,7 +433,7 @@ func (c *Client) enqueue(ctx context.Context, msg *base.TaskMessage, uniqueTTL t
|
||||
|
||||
func (c *Client) schedule(ctx context.Context, msg *base.TaskMessage, t time.Time, uniqueTTL time.Duration) error {
|
||||
if uniqueTTL > 0 {
|
||||
ttl := t.Add(uniqueTTL).Sub(time.Now())
|
||||
ttl := time.Until(t.Add(uniqueTTL))
|
||||
return c.broker.ScheduleUnique(ctx, msg, t, ttl)
|
||||
}
|
||||
return c.broker.Schedule(ctx, msg, t)
|
||||
|
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
h "github.com/hibiken/asynq/internal/testutil"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func TestClientEnqueueWithProcessAtOption(t *testing.T) {
|
||||
@@ -143,11 +144,7 @@ func TestClientEnqueueWithProcessAtOption(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientEnqueue(t *testing.T) {
|
||||
r := setup(t)
|
||||
client := NewClient(getRedisConnOpt(t))
|
||||
defer client.Close()
|
||||
|
||||
func testClientEnqueue(t *testing.T, client *Client, r redis.UniversalClient) {
|
||||
task := NewTask("send_email", h.JSON(map[string]interface{}{"to": "customer@gmail.com", "from": "merchant@example.com"}))
|
||||
now := time.Now()
|
||||
|
||||
@@ -478,6 +475,24 @@ func TestClientEnqueue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientEnqueue(t *testing.T) {
|
||||
r := setup(t)
|
||||
client := NewClient(getRedisConnOpt(t))
|
||||
defer client.Close()
|
||||
testClientEnqueue(t, client, r)
|
||||
}
|
||||
|
||||
func TestClientFromRedisClientEnqueue(t *testing.T) {
|
||||
r := setup(t)
|
||||
redisClient := getRedisConnOpt(t).MakeRedisClient().(redis.UniversalClient)
|
||||
client := NewClientFromRedisClient(redisClient)
|
||||
testClientEnqueue(t, client, r)
|
||||
err := client.Close()
|
||||
if err == nil {
|
||||
t.Error("client.Close() should have failed because of a shared client but it didn't")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientEnqueueWithGroupOption(t *testing.T) {
|
||||
r := setup(t)
|
||||
client := NewClient(getRedisConnOpt(t))
|
||||
@@ -541,11 +556,11 @@ func TestClientEnqueueWithGroupOption(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "With Group and ProcessIn options",
|
||||
desc: "With Group and ProcessAt options",
|
||||
task: task,
|
||||
opts: []Option{
|
||||
Group("mygroup"),
|
||||
ProcessIn(30 * time.Minute),
|
||||
ProcessAt(now.Add(30 * time.Minute)),
|
||||
},
|
||||
wantInfo: &TaskInfo{
|
||||
Queue: "default",
|
||||
@@ -1158,7 +1173,7 @@ func TestClientEnqueueUniqueWithProcessAtOption(t *testing.T) {
|
||||
}
|
||||
|
||||
gotTTL := r.TTL(context.Background(), base.UniqueKey(base.DefaultQueueName, tc.task.Type(), tc.task.Payload())).Val()
|
||||
wantTTL := tc.at.Add(tc.ttl).Sub(time.Now())
|
||||
wantTTL := time.Until(tc.at.Add(tc.ttl))
|
||||
if !cmp.Equal(wantTTL.Seconds(), gotTTL.Seconds(), cmpopts.EquateApprox(0, 1)) {
|
||||
t.Errorf("TTL = %v, want %v", gotTTL, wantTTL)
|
||||
continue
|
||||
|
13
go.mod
13
go.mod
@@ -1,18 +1,17 @@
|
||||
module github.com/hibiken/asynq
|
||||
|
||||
go 1.20
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/golang/protobuf v1.5.3
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/redis/go-redis/v9 v9.4.0
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/spf13/cast v1.5.1
|
||||
github.com/spf13/cast v1.7.0
|
||||
go.uber.org/goleak v1.3.0
|
||||
golang.org/x/sys v0.16.0
|
||||
golang.org/x/time v0.3.0
|
||||
google.golang.org/protobuf v1.31.0
|
||||
golang.org/x/sys v0.26.0
|
||||
golang.org/x/time v0.7.0
|
||||
google.golang.org/protobuf v1.35.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
39
go.sum
39
go.sum
@@ -1,39 +1,42 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
||||
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
|
||||
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@@ -120,7 +120,9 @@ func (h *heartbeater) start(wg *sync.WaitGroup) {
|
||||
for {
|
||||
select {
|
||||
case <-h.done:
|
||||
h.broker.ClearServerState(h.host, h.pid, h.serverID)
|
||||
if err := h.broker.ClearServerState(h.host, h.pid, h.serverID); err != nil {
|
||||
h.logger.Errorf("Failed to clear server state: %v", err)
|
||||
}
|
||||
h.logger.Debug("Heartbeater done")
|
||||
timer.Stop()
|
||||
return
|
||||
|
19
inspector.go
19
inspector.go
@@ -10,16 +10,19 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
"github.com/hibiken/asynq/internal/errors"
|
||||
"github.com/hibiken/asynq/internal/rdb"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Inspector is a client interface to inspect and mutate the state of
|
||||
// queues and tasks.
|
||||
type Inspector struct {
|
||||
rdb *rdb.RDB
|
||||
// When an Inspector has been created with an existing Redis connection, we do
|
||||
// not want to close it.
|
||||
sharedConnection bool
|
||||
}
|
||||
|
||||
// New returns a new instance of Inspector.
|
||||
@@ -28,13 +31,25 @@ func NewInspector(r RedisConnOpt) *Inspector {
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("inspeq: unsupported RedisConnOpt type %T", r))
|
||||
}
|
||||
inspector := NewInspectorFromRedisClient(c)
|
||||
inspector.sharedConnection = false
|
||||
return inspector
|
||||
}
|
||||
|
||||
// NewInspectorFromRedisClient returns a new instance of Inspector given a redis.UniversalClient
|
||||
// Warning: The underlying redis connection pool will not be closed by Asynq, you are responsible for closing it.
|
||||
func NewInspectorFromRedisClient(c redis.UniversalClient) *Inspector {
|
||||
return &Inspector{
|
||||
rdb: rdb.NewRDB(c),
|
||||
rdb: rdb.NewRDB(c),
|
||||
sharedConnection: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the connection with redis.
|
||||
func (i *Inspector) Close() error {
|
||||
if i.sharedConnection {
|
||||
return fmt.Errorf("redis connection is shared so the Inspector can't be closed through asynq")
|
||||
}
|
||||
return i.rdb.Close()
|
||||
}
|
||||
|
||||
|
@@ -22,11 +22,7 @@ import (
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func TestInspectorQueues(t *testing.T) {
|
||||
r := setup(t)
|
||||
defer r.Close()
|
||||
inspector := NewInspector(getRedisConnOpt(t))
|
||||
|
||||
func testInspectorQueues(t *testing.T, inspector *Inspector, r redis.UniversalClient) {
|
||||
tests := []struct {
|
||||
queues []string
|
||||
}{
|
||||
@@ -52,7 +48,21 @@ func TestInspectorQueues(t *testing.T) {
|
||||
t.Errorf("Queues() = %v, want %v; (-want, +got)\n%s", got, tc.queues, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectorQueues(t *testing.T) {
|
||||
r := setup(t)
|
||||
defer r.Close()
|
||||
inspector := NewInspector(getRedisConnOpt(t))
|
||||
testInspectorQueues(t, inspector, r)
|
||||
}
|
||||
|
||||
func TestInspectorFromRedisClientQueues(t *testing.T) {
|
||||
r := setup(t)
|
||||
defer r.Close()
|
||||
redisClient := getRedisConnOpt(t).MakeRedisClient().(redis.UniversalClient)
|
||||
inspector := NewInspectorFromRedisClient(redisClient)
|
||||
testInspectorQueues(t, inspector, r)
|
||||
}
|
||||
|
||||
func TestInspectorDeleteQueue(t *testing.T) {
|
||||
|
@@ -14,16 +14,16 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/hibiken/asynq/internal/errors"
|
||||
pb "github.com/hibiken/asynq/internal/proto"
|
||||
"github.com/hibiken/asynq/internal/timeutil"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// Version of asynq library and CLI.
|
||||
const Version = "0.24.1"
|
||||
const Version = "0.25.0"
|
||||
|
||||
// DefaultQueueName is the queue name used if none are specified by user.
|
||||
const DefaultQueueName = "default"
|
||||
@@ -379,10 +379,8 @@ func EncodeServerInfo(info *ServerInfo) ([]byte, error) {
|
||||
for q, p := range info.Queues {
|
||||
queues[q] = int32(p)
|
||||
}
|
||||
started, err := ptypes.TimestampProto(info.Started)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
started := timestamppb.New(info.Started)
|
||||
|
||||
return proto.Marshal(&pb.ServerInfo{
|
||||
Host: info.Host,
|
||||
Pid: int32(info.PID),
|
||||
@@ -406,10 +404,8 @@ func DecodeServerInfo(b []byte) (*ServerInfo, error) {
|
||||
for q, p := range pbmsg.GetQueues() {
|
||||
queues[q] = int(p)
|
||||
}
|
||||
startTime, err := ptypes.Timestamp(pbmsg.GetStartTime())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
startTime := pbmsg.GetStartTime()
|
||||
|
||||
return &ServerInfo{
|
||||
Host: pbmsg.GetHost(),
|
||||
PID: int(pbmsg.GetPid()),
|
||||
@@ -418,7 +414,7 @@ func DecodeServerInfo(b []byte) (*ServerInfo, error) {
|
||||
Queues: queues,
|
||||
StrictPriority: pbmsg.GetStrictPriority(),
|
||||
Status: pbmsg.GetStatus(),
|
||||
Started: startTime,
|
||||
Started: startTime.AsTime(),
|
||||
ActiveWorkerCount: int(pbmsg.GetActiveWorkerCount()),
|
||||
}, nil
|
||||
}
|
||||
@@ -441,14 +437,9 @@ func EncodeWorkerInfo(info *WorkerInfo) ([]byte, error) {
|
||||
if info == nil {
|
||||
return nil, fmt.Errorf("cannot encode nil worker info")
|
||||
}
|
||||
startTime, err := ptypes.TimestampProto(info.Started)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deadline, err := ptypes.TimestampProto(info.Deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
startTime := timestamppb.New(info.Started)
|
||||
deadline := timestamppb.New(info.Deadline)
|
||||
|
||||
return proto.Marshal(&pb.WorkerInfo{
|
||||
Host: info.Host,
|
||||
Pid: int32(info.PID),
|
||||
@@ -468,14 +459,9 @@ func DecodeWorkerInfo(b []byte) (*WorkerInfo, error) {
|
||||
if err := proto.Unmarshal(b, &pbmsg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
startTime, err := ptypes.Timestamp(pbmsg.GetStartTime())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deadline, err := ptypes.Timestamp(pbmsg.GetDeadline())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
startTime := pbmsg.GetStartTime()
|
||||
deadline := pbmsg.GetDeadline()
|
||||
|
||||
return &WorkerInfo{
|
||||
Host: pbmsg.GetHost(),
|
||||
PID: int(pbmsg.GetPid()),
|
||||
@@ -484,8 +470,8 @@ func DecodeWorkerInfo(b []byte) (*WorkerInfo, error) {
|
||||
Type: pbmsg.GetTaskType(),
|
||||
Payload: pbmsg.GetTaskPayload(),
|
||||
Queue: pbmsg.GetQueue(),
|
||||
Started: startTime,
|
||||
Deadline: deadline,
|
||||
Started: startTime.AsTime(),
|
||||
Deadline: deadline.AsTime(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -519,14 +505,9 @@ func EncodeSchedulerEntry(entry *SchedulerEntry) ([]byte, error) {
|
||||
if entry == nil {
|
||||
return nil, fmt.Errorf("cannot encode nil scheduler entry")
|
||||
}
|
||||
next, err := ptypes.TimestampProto(entry.Next)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prev, err := ptypes.TimestampProto(entry.Prev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next := timestamppb.New(entry.Next)
|
||||
prev := timestamppb.New(entry.Prev)
|
||||
|
||||
return proto.Marshal(&pb.SchedulerEntry{
|
||||
Id: entry.ID,
|
||||
Spec: entry.Spec,
|
||||
@@ -544,22 +525,17 @@ func DecodeSchedulerEntry(b []byte) (*SchedulerEntry, error) {
|
||||
if err := proto.Unmarshal(b, &pbmsg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next, err := ptypes.Timestamp(pbmsg.GetNextEnqueueTime())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prev, err := ptypes.Timestamp(pbmsg.GetPrevEnqueueTime())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next := pbmsg.GetNextEnqueueTime()
|
||||
prev := pbmsg.GetPrevEnqueueTime()
|
||||
|
||||
return &SchedulerEntry{
|
||||
ID: pbmsg.GetId(),
|
||||
Spec: pbmsg.GetSpec(),
|
||||
Type: pbmsg.GetTaskType(),
|
||||
Payload: pbmsg.GetTaskPayload(),
|
||||
Opts: pbmsg.GetEnqueueOptions(),
|
||||
Next: next,
|
||||
Prev: prev,
|
||||
Next: next.AsTime(),
|
||||
Prev: prev.AsTime(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -578,10 +554,7 @@ func EncodeSchedulerEnqueueEvent(event *SchedulerEnqueueEvent) ([]byte, error) {
|
||||
if event == nil {
|
||||
return nil, fmt.Errorf("cannot encode nil enqueue event")
|
||||
}
|
||||
enqueuedAt, err := ptypes.TimestampProto(event.EnqueuedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enqueuedAt := timestamppb.New(event.EnqueuedAt)
|
||||
return proto.Marshal(&pb.SchedulerEnqueueEvent{
|
||||
TaskId: event.TaskID,
|
||||
EnqueueTime: enqueuedAt,
|
||||
@@ -595,13 +568,10 @@ func DecodeSchedulerEnqueueEvent(b []byte) (*SchedulerEnqueueEvent, error) {
|
||||
if err := proto.Unmarshal(b, &pbmsg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enqueuedAt, err := ptypes.Timestamp(pbmsg.GetEnqueueTime())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enqueuedAt := pbmsg.GetEnqueueTime()
|
||||
return &SchedulerEnqueueEvent{
|
||||
TaskID: pbmsg.GetTaskId(),
|
||||
EnqueuedAt: enqueuedAt,
|
||||
EnqueuedAt: enqueuedAt.AsTime(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -737,7 +707,7 @@ type Broker interface {
|
||||
ReclaimStaleAggregationSets(qname string) error
|
||||
|
||||
// Task retention related method
|
||||
DeleteExpiredCompletedTasks(qname string) error
|
||||
DeleteExpiredCompletedTasks(qname string, batchSize int) error
|
||||
|
||||
// Lease related methods
|
||||
ListLeaseExpired(cutoff time.Time, qnames ...string) ([]*TaskMessage, error)
|
||||
|
@@ -4,14 +4,13 @@
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.25.0
|
||||
// protoc v3.17.3
|
||||
// protoc-gen-go v1.34.2
|
||||
// protoc v3.19.6
|
||||
// source: asynq.proto
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
@@ -26,10 +25,6 @@ const (
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// This is a compile-time assertion that a sufficiently up-to-date version
|
||||
// of the legacy proto package is being used.
|
||||
const _ = proto.ProtoPackageIsVersion4
|
||||
|
||||
// TaskMessage is the internal representation of a task with additional
|
||||
// metadata fields.
|
||||
// Next ID: 15
|
||||
@@ -739,7 +734,7 @@ func file_asynq_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_asynq_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_asynq_proto_goTypes = []interface{}{
|
||||
var file_asynq_proto_goTypes = []any{
|
||||
(*TaskMessage)(nil), // 0: asynq.TaskMessage
|
||||
(*ServerInfo)(nil), // 1: asynq.ServerInfo
|
||||
(*WorkerInfo)(nil), // 2: asynq.WorkerInfo
|
||||
@@ -769,7 +764,7 @@ func file_asynq_proto_init() {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_asynq_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
file_asynq_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*TaskMessage); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
@@ -781,7 +776,7 @@ func file_asynq_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_asynq_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
file_asynq_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*ServerInfo); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
@@ -793,7 +788,7 @@ func file_asynq_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_asynq_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
file_asynq_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*WorkerInfo); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
@@ -805,7 +800,7 @@ func file_asynq_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_asynq_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
file_asynq_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*SchedulerEntry); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
@@ -817,7 +812,7 @@ func file_asynq_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_asynq_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
||||
file_asynq_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*SchedulerEnqueueEvent); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
|
@@ -829,6 +829,7 @@ const (
|
||||
// KEYS[6] -> asynq:{<qname>}:failed:<yyyy-mm-dd>
|
||||
// KEYS[7] -> asynq:{<qname>}:processed
|
||||
// KEYS[8] -> asynq:{<qname>}:failed
|
||||
// KEYS[9] -> asynq:{<qname>}:t:
|
||||
// -------
|
||||
// ARGV[1] -> task ID
|
||||
// ARGV[2] -> updated base.TaskMessage value
|
||||
@@ -845,8 +846,22 @@ if redis.call("ZREM", KEYS[3], ARGV[1]) == 0 then
|
||||
return redis.error_reply("NOT FOUND")
|
||||
end
|
||||
redis.call("ZADD", KEYS[4], ARGV[3], ARGV[1])
|
||||
redis.call("ZREMRANGEBYSCORE", KEYS[4], "-inf", ARGV[4])
|
||||
redis.call("ZREMRANGEBYRANK", KEYS[4], 0, -ARGV[5])
|
||||
local old = redis.call("ZRANGE", KEYS[4], "-inf", ARGV[4], "BYSCORE")
|
||||
if #old > 0 then
|
||||
for _, id in ipairs(old) do
|
||||
redis.call("DEL", KEYS[9] .. id)
|
||||
end
|
||||
redis.call("ZREM", KEYS[4], unpack(old))
|
||||
end
|
||||
|
||||
local extra = redis.call("ZRANGE", KEYS[4], 0, -ARGV[5])
|
||||
if #extra > 0 then
|
||||
for _, id in ipairs(extra) do
|
||||
redis.call("DEL", KEYS[9] .. id)
|
||||
end
|
||||
redis.call("ZREM", KEYS[4], unpack(extra))
|
||||
end
|
||||
|
||||
redis.call("HSET", KEYS[1], "msg", ARGV[2], "state", "archived")
|
||||
local n = redis.call("INCR", KEYS[5])
|
||||
if tonumber(n) == 1 then
|
||||
@@ -889,6 +904,7 @@ func (r *RDB) Archive(ctx context.Context, msg *base.TaskMessage, errMsg string)
|
||||
base.FailedKey(msg.Queue, now),
|
||||
base.ProcessedTotalKey(msg.Queue),
|
||||
base.FailedTotalKey(msg.Queue),
|
||||
base.TaskKeyPrefix(msg.Queue),
|
||||
}
|
||||
argv := []interface{}{
|
||||
msg.ID,
|
||||
@@ -1217,7 +1233,7 @@ redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", ARGV[1])
|
||||
return redis.status_reply("OK")
|
||||
`)
|
||||
|
||||
// ReclaimStateAggregationSets checks for any stale aggregation sets in the given queue, and
|
||||
// ReclaimStaleAggregationSets checks for any stale aggregation sets in the given queue, and
|
||||
// reclaim tasks in the stale aggregation set by putting them back in the group.
|
||||
func (r *RDB) ReclaimStaleAggregationSets(qname string) error {
|
||||
var op errors.Op = "RDB.ReclaimStaleAggregationSets"
|
||||
@@ -1241,9 +1257,7 @@ return table.getn(ids)`)
|
||||
|
||||
// DeleteExpiredCompletedTasks checks for any expired tasks in the given queue's completed set,
|
||||
// and delete all expired tasks.
|
||||
func (r *RDB) DeleteExpiredCompletedTasks(qname string) error {
|
||||
// Note: Do this operation in fix batches to prevent long running script.
|
||||
const batchSize = 100
|
||||
func (r *RDB) DeleteExpiredCompletedTasks(qname string, batchSize int) error {
|
||||
for {
|
||||
n, err := r.deleteExpiredCompletedTasks(qname, batchSize)
|
||||
if err != nil {
|
||||
|
@@ -2002,7 +2002,6 @@ func TestArchive(t *testing.T) {
|
||||
}
|
||||
errMsg := "SMTP server not responding"
|
||||
|
||||
// TODO(hibiken): add test cases for trimming
|
||||
tests := []struct {
|
||||
active map[string][]*base.TaskMessage
|
||||
lease map[string][]base.Z
|
||||
@@ -2171,6 +2170,163 @@ func TestArchive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveTrim(t *testing.T) {
|
||||
r := setup(t)
|
||||
defer r.Close()
|
||||
now := time.Now()
|
||||
r.SetClock(timeutil.NewSimulatedClock(now))
|
||||
|
||||
t1 := &base.TaskMessage{
|
||||
ID: uuid.NewString(),
|
||||
Type: "send_email",
|
||||
Payload: nil,
|
||||
Queue: "default",
|
||||
Retry: 25,
|
||||
Retried: 25,
|
||||
Timeout: 1800,
|
||||
}
|
||||
t2 := &base.TaskMessage{
|
||||
ID: uuid.NewString(),
|
||||
Type: "reindex",
|
||||
Payload: nil,
|
||||
Queue: "default",
|
||||
Retry: 25,
|
||||
Retried: 0,
|
||||
Timeout: 3000,
|
||||
}
|
||||
errMsg := "SMTP server not responding"
|
||||
|
||||
maxArchiveSet := make([]base.Z, 0)
|
||||
for i := 0; i < maxArchiveSize-1; i++ {
|
||||
maxArchiveSet = append(maxArchiveSet, base.Z{Message: &base.TaskMessage{
|
||||
ID: uuid.NewString(),
|
||||
Type: "generate_csv",
|
||||
Payload: nil,
|
||||
Queue: "default",
|
||||
Retry: 25,
|
||||
Retried: 0,
|
||||
Timeout: 60,
|
||||
}, Score: now.Add(-time.Hour + -time.Second*time.Duration(i)).Unix()})
|
||||
}
|
||||
|
||||
wantMaxArchiveSet := make([]base.Z, 0)
|
||||
// newly archived task should be at the front
|
||||
wantMaxArchiveSet = append(wantMaxArchiveSet, base.Z{Message: h.TaskMessageWithError(*t1, errMsg, now), Score: now.Unix()})
|
||||
// oldest task should be dropped from the set
|
||||
wantMaxArchiveSet = append(wantMaxArchiveSet, maxArchiveSet[:len(maxArchiveSet)-1]...)
|
||||
|
||||
tests := []struct {
|
||||
toArchive map[string][]*base.TaskMessage
|
||||
lease map[string][]base.Z
|
||||
archived map[string][]base.Z
|
||||
wantArchived map[string][]base.Z
|
||||
}{
|
||||
{ // simple, 1 to be archived, 1 already archived, both are in the archive set
|
||||
toArchive: map[string][]*base.TaskMessage{
|
||||
"default": {t1},
|
||||
},
|
||||
lease: map[string][]base.Z{
|
||||
"default": {
|
||||
{Message: t1, Score: now.Add(10 * time.Second).Unix()},
|
||||
},
|
||||
},
|
||||
archived: map[string][]base.Z{
|
||||
"default": {
|
||||
{Message: t2, Score: now.Add(-time.Hour).Unix()},
|
||||
},
|
||||
},
|
||||
wantArchived: map[string][]base.Z{
|
||||
"default": {
|
||||
{Message: h.TaskMessageWithError(*t1, errMsg, now), Score: now.Unix()},
|
||||
{Message: t2, Score: now.Add(-time.Hour).Unix()},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // 1 to be archived, 1 already archived but past expiry, only the newly archived task should be left
|
||||
toArchive: map[string][]*base.TaskMessage{
|
||||
"default": {t1},
|
||||
},
|
||||
lease: map[string][]base.Z{
|
||||
"default": {
|
||||
{Message: t1, Score: now.Add(10 * time.Second).Unix()},
|
||||
},
|
||||
},
|
||||
archived: map[string][]base.Z{
|
||||
"default": {
|
||||
{Message: t2, Score: now.Add(-time.Hour * 24 * (archivedExpirationInDays + 1)).Unix()},
|
||||
},
|
||||
},
|
||||
wantArchived: map[string][]base.Z{
|
||||
"default": {
|
||||
{Message: h.TaskMessageWithError(*t1, errMsg, now), Score: now.Unix()},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // 1 to be archived, maxArchiveSize in archive set, archive set should be trimmed back to maxArchiveSize and newly archived task should be in the set
|
||||
toArchive: map[string][]*base.TaskMessage{
|
||||
"default": {t1},
|
||||
},
|
||||
lease: map[string][]base.Z{
|
||||
"default": {
|
||||
{Message: t1, Score: now.Add(10 * time.Second).Unix()},
|
||||
},
|
||||
},
|
||||
archived: map[string][]base.Z{
|
||||
"default": maxArchiveSet,
|
||||
},
|
||||
wantArchived: map[string][]base.Z{
|
||||
"default": wantMaxArchiveSet,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
h.FlushDB(t, r.client) // clean up db before each test case
|
||||
h.SeedAllActiveQueues(t, r.client, tc.toArchive)
|
||||
h.SeedAllLease(t, r.client, tc.lease)
|
||||
h.SeedAllArchivedQueues(t, r.client, tc.archived)
|
||||
|
||||
for _, tasks := range tc.toArchive {
|
||||
for _, target := range tasks {
|
||||
err := r.Archive(context.Background(), target, errMsg)
|
||||
if err != nil {
|
||||
t.Errorf("(*RDB).Archive(%v, %v) = %v, want nil", target, errMsg, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for queue, want := range tc.wantArchived {
|
||||
gotArchived := h.GetArchivedEntries(t, r.client, queue)
|
||||
|
||||
if diff := cmp.Diff(want, gotArchived, h.SortZSetEntryOpt, zScoreCmpOpt, timeCmpOpt); diff != "" {
|
||||
t.Errorf("mismatch found in %q after calling (*RDB).Archive: (-want, +got):\n%s", base.ArchivedKey(queue), diff)
|
||||
}
|
||||
|
||||
// check that only keys present in the archived set are in rdb
|
||||
vals := r.client.Keys(context.Background(), base.TaskKeyPrefix(queue)+"*").Val()
|
||||
if len(vals) != len(gotArchived) {
|
||||
t.Errorf("len of keys = %v, want %v", len(vals), len(gotArchived))
|
||||
return
|
||||
}
|
||||
|
||||
for _, val := range vals {
|
||||
found := false
|
||||
for _, entry := range gotArchived {
|
||||
if strings.Contains(val, entry.Message.ID) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("key %v not found in archived set (it was orphaned by the archive trim)", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestForwardIfReadyWithGroup(t *testing.T) {
|
||||
r := setup(t)
|
||||
defer r.Close()
|
||||
@@ -2542,8 +2698,8 @@ func TestDeleteExpiredCompletedTasks(t *testing.T) {
|
||||
h.FlushDB(t, r.client)
|
||||
h.SeedAllCompletedQueues(t, r.client, tc.completed)
|
||||
|
||||
if err := r.DeleteExpiredCompletedTasks(tc.qname); err != nil {
|
||||
t.Errorf("DeleteExpiredCompletedTasks(%q) failed: %v", tc.qname, err)
|
||||
if err := r.DeleteExpiredCompletedTasks(tc.qname, 100); err != nil {
|
||||
t.Errorf("DeleteExpiredCompletedTasks(%q, 100) failed: %v", tc.qname, err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -3050,7 +3206,7 @@ func TestCancelationPubSub(t *testing.T) {
|
||||
publish := []string{"one", "two", "three"}
|
||||
|
||||
for _, msg := range publish {
|
||||
r.PublishCancelation(msg)
|
||||
_ = r.PublishCancelation(msg)
|
||||
}
|
||||
|
||||
// allow for message to reach subscribers.
|
||||
|
@@ -11,8 +11,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var errRedisDown = errors.New("testutil: redis is down")
|
||||
@@ -145,13 +145,13 @@ func (tb *TestBroker) ForwardIfReady(qnames ...string) error {
|
||||
return tb.real.ForwardIfReady(qnames...)
|
||||
}
|
||||
|
||||
func (tb *TestBroker) DeleteExpiredCompletedTasks(qname string) error {
|
||||
func (tb *TestBroker) DeleteExpiredCompletedTasks(qname string, batchSize int) error {
|
||||
tb.mu.Lock()
|
||||
defer tb.mu.Unlock()
|
||||
if tb.sleeping {
|
||||
return errRedisDown
|
||||
}
|
||||
return tb.real.DeleteExpiredCompletedTasks(qname)
|
||||
return tb.real.DeleteExpiredCompletedTasks(qname, batchSize)
|
||||
}
|
||||
|
||||
func (tb *TestBroker) ListLeaseExpired(cutoff time.Time, qnames ...string) ([]*base.TaskMessage, error) {
|
||||
|
15
janitor.go
15
janitor.go
@@ -27,13 +27,17 @@ type janitor struct {
|
||||
|
||||
// average interval between checks.
|
||||
avgInterval time.Duration
|
||||
|
||||
// number of tasks to be deleted when janitor runs to delete the expired completed tasks.
|
||||
batchSize int
|
||||
}
|
||||
|
||||
type janitorParams struct {
|
||||
logger *log.Logger
|
||||
broker base.Broker
|
||||
queues []string
|
||||
interval time.Duration
|
||||
logger *log.Logger
|
||||
broker base.Broker
|
||||
queues []string
|
||||
interval time.Duration
|
||||
batchSize int
|
||||
}
|
||||
|
||||
func newJanitor(params janitorParams) *janitor {
|
||||
@@ -43,6 +47,7 @@ func newJanitor(params janitorParams) *janitor {
|
||||
done: make(chan struct{}),
|
||||
queues: params.queues,
|
||||
avgInterval: params.interval,
|
||||
batchSize: params.batchSize,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +78,7 @@ func (j *janitor) start(wg *sync.WaitGroup) {
|
||||
|
||||
func (j *janitor) exec() {
|
||||
for _, qname := range j.queues {
|
||||
if err := j.broker.DeleteExpiredCompletedTasks(qname); err != nil {
|
||||
if err := j.broker.DeleteExpiredCompletedTasks(qname, j.batchSize); err != nil {
|
||||
j.logger.Errorf("Failed to delete expired completed tasks from queue %q: %v",
|
||||
qname, err)
|
||||
}
|
||||
|
@@ -26,11 +26,13 @@ func TestJanitor(t *testing.T) {
|
||||
defer r.Close()
|
||||
rdbClient := rdb.NewRDB(r)
|
||||
const interval = 1 * time.Second
|
||||
const batchSize = 100
|
||||
janitor := newJanitor(janitorParams{
|
||||
logger: testLogger,
|
||||
broker: rdbClient,
|
||||
queues: []string{"default", "custom"},
|
||||
interval: interval,
|
||||
logger: testLogger,
|
||||
broker: rdbClient,
|
||||
queues: []string{"default", "custom"},
|
||||
interval: interval,
|
||||
batchSize: batchSize,
|
||||
})
|
||||
|
||||
now := time.Now()
|
||||
|
@@ -7,7 +7,6 @@ package asynq
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -79,13 +78,13 @@ type PeriodicTaskConfig struct {
|
||||
|
||||
func (c *PeriodicTaskConfig) hash() string {
|
||||
h := sha256.New()
|
||||
io.WriteString(h, c.Cronspec)
|
||||
io.WriteString(h, c.Task.Type())
|
||||
_, _ = h.Write([]byte(c.Cronspec))
|
||||
_, _ = h.Write([]byte(c.Task.Type()))
|
||||
h.Write(c.Task.Payload())
|
||||
opts := stringifyOptions(c.Opts)
|
||||
sort.Strings(opts)
|
||||
for _, opt := range opts {
|
||||
io.WriteString(h, opt)
|
||||
_, _ = h.Write([]byte(opt))
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
@@ -32,6 +32,7 @@ func (p *FakeConfigProvider) GetConfigs() ([]*PeriodicTaskConfig, error) {
|
||||
}
|
||||
|
||||
func TestNewPeriodicTaskManager(t *testing.T) {
|
||||
redisConnOpt := getRedisConnOpt(t)
|
||||
cfgs := []*PeriodicTaskConfig{
|
||||
{Cronspec: "* * * * *", Task: NewTask("foo", nil)},
|
||||
{Cronspec: "* * * * *", Task: NewTask("bar", nil)},
|
||||
@@ -43,14 +44,14 @@ func TestNewPeriodicTaskManager(t *testing.T) {
|
||||
{
|
||||
desc: "with provider and redisConnOpt",
|
||||
opts: PeriodicTaskManagerOpts{
|
||||
RedisConnOpt: RedisClientOpt{Addr: ":6379"},
|
||||
RedisConnOpt: redisConnOpt,
|
||||
PeriodicTaskConfigProvider: &FakeConfigProvider{cfgs: cfgs},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "with sync option",
|
||||
opts: PeriodicTaskManagerOpts{
|
||||
RedisConnOpt: RedisClientOpt{Addr: ":6379"},
|
||||
RedisConnOpt: redisConnOpt,
|
||||
PeriodicTaskConfigProvider: &FakeConfigProvider{cfgs: cfgs},
|
||||
SyncInterval: 5 * time.Minute,
|
||||
},
|
||||
@@ -58,7 +59,7 @@ func TestNewPeriodicTaskManager(t *testing.T) {
|
||||
{
|
||||
desc: "with scheduler option",
|
||||
opts: PeriodicTaskManagerOpts{
|
||||
RedisConnOpt: RedisClientOpt{Addr: ":6379"},
|
||||
RedisConnOpt: redisConnOpt,
|
||||
PeriodicTaskConfigProvider: &FakeConfigProvider{cfgs: cfgs},
|
||||
SyncInterval: 5 * time.Minute,
|
||||
SchedulerOpts: &SchedulerOpts{
|
||||
@@ -74,37 +75,33 @@ func TestNewPeriodicTaskManager(t *testing.T) {
|
||||
t.Errorf("%s; NewPeriodicTaskManager returned error: %v", tc.desc, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPeriodicTaskManagerError(t *testing.T) {
|
||||
cfgs := []*PeriodicTaskConfig{
|
||||
{Cronspec: "* * * * *", Task: NewTask("foo", nil)},
|
||||
{Cronspec: "* * * * *", Task: NewTask("bar", nil)},
|
||||
}
|
||||
tests := []struct {
|
||||
desc string
|
||||
opts PeriodicTaskManagerOpts
|
||||
}{
|
||||
{
|
||||
desc: "without provider",
|
||||
opts: PeriodicTaskManagerOpts{
|
||||
RedisConnOpt: RedisClientOpt{Addr: ":6379"},
|
||||
t.Run("error", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
opts PeriodicTaskManagerOpts
|
||||
}{
|
||||
{
|
||||
desc: "without provider",
|
||||
opts: PeriodicTaskManagerOpts{
|
||||
RedisConnOpt: redisConnOpt,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "without redisConOpt",
|
||||
opts: PeriodicTaskManagerOpts{
|
||||
PeriodicTaskConfigProvider: &FakeConfigProvider{cfgs: cfgs},
|
||||
{
|
||||
desc: "without redisConOpt",
|
||||
opts: PeriodicTaskManagerOpts{
|
||||
PeriodicTaskConfigProvider: &FakeConfigProvider{cfgs: cfgs},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
_, err := NewPeriodicTaskManager(tc.opts)
|
||||
if err == nil {
|
||||
t.Errorf("%s; NewPeriodicTaskManager did not return error", tc.desc)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
_, err := NewPeriodicTaskManager(tc.opts)
|
||||
if err == nil {
|
||||
t.Errorf("%s; NewPeriodicTaskManager did not return error", tc.desc)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeriodicTaskConfigHash(t *testing.T) {
|
||||
|
52
processor.go
52
processor.go
@@ -8,7 +8,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
@@ -181,7 +181,8 @@ func (p *processor) exec() {
|
||||
// Sleep to avoid slamming redis and let scheduler move tasks into queues.
|
||||
// Note: We are not using blocking pop operation and polling queues instead.
|
||||
// This adds significant load to redis.
|
||||
time.Sleep(p.taskCheckInterval)
|
||||
jitter := rand.N(p.taskCheckInterval)
|
||||
time.Sleep(p.taskCheckInterval/2 + jitter)
|
||||
<-p.sema // release token
|
||||
return
|
||||
case err != nil:
|
||||
@@ -261,7 +262,8 @@ func (p *processor) requeue(l *base.Lease, msg *base.TaskMessage) {
|
||||
// If lease is not valid, do not write to redis; Let recoverer take care of it.
|
||||
return
|
||||
}
|
||||
ctx, _ := context.WithDeadline(context.Background(), l.Deadline())
|
||||
ctx, cancel := context.WithDeadline(context.Background(), l.Deadline())
|
||||
defer cancel()
|
||||
err := p.broker.Requeue(ctx, msg)
|
||||
if err != nil {
|
||||
p.logger.Errorf("Could not push task id=%s back to queue: %v", msg.ID, err)
|
||||
@@ -283,7 +285,8 @@ func (p *processor) markAsComplete(l *base.Lease, msg *base.TaskMessage) {
|
||||
// If lease is not valid, do not write to redis; Let recoverer take care of it.
|
||||
return
|
||||
}
|
||||
ctx, _ := context.WithDeadline(context.Background(), l.Deadline())
|
||||
ctx, cancel := context.WithDeadline(context.Background(), l.Deadline())
|
||||
defer cancel()
|
||||
err := p.broker.MarkAsComplete(ctx, msg)
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Could not move task id=%s type=%q from %q to %q: %+v",
|
||||
@@ -304,7 +307,8 @@ func (p *processor) markAsDone(l *base.Lease, msg *base.TaskMessage) {
|
||||
// If lease is not valid, do not write to redis; Let recoverer take care of it.
|
||||
return
|
||||
}
|
||||
ctx, _ := context.WithDeadline(context.Background(), l.Deadline())
|
||||
ctx, cancel := context.WithDeadline(context.Background(), l.Deadline())
|
||||
defer cancel()
|
||||
err := p.broker.Done(ctx, msg)
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Could not remove task id=%s type=%q from %q err: %+v", msg.ID, msg.Type, base.ActiveKey(msg.Queue), err)
|
||||
@@ -323,20 +327,23 @@ func (p *processor) markAsDone(l *base.Lease, msg *base.TaskMessage) {
|
||||
// the task should not be retried and should be archived instead.
|
||||
var SkipRetry = errors.New("skip retry for the task")
|
||||
|
||||
// RevokeTask is used as a return value from Handler.ProcessTask to indicate that
|
||||
// the task should not be retried or archived.
|
||||
var RevokeTask = errors.New("revoke task")
|
||||
|
||||
func (p *processor) handleFailedMessage(ctx context.Context, l *base.Lease, msg *base.TaskMessage, err error) {
|
||||
if p.errHandler != nil {
|
||||
p.errHandler.HandleError(ctx, NewTask(msg.Type, msg.Payload), err)
|
||||
}
|
||||
if !p.isFailureFunc(err) {
|
||||
// retry the task without marking it as failed
|
||||
p.retry(l, msg, err, false /*isFailure*/)
|
||||
return
|
||||
}
|
||||
if msg.Retried >= msg.Retry || errors.Is(err, SkipRetry) {
|
||||
switch {
|
||||
case errors.Is(err, RevokeTask):
|
||||
p.logger.Warnf("revoke task id=%s", msg.ID)
|
||||
p.markAsDone(l, msg)
|
||||
case msg.Retried >= msg.Retry || errors.Is(err, SkipRetry):
|
||||
p.logger.Warnf("Retry exhausted for task id=%s", msg.ID)
|
||||
p.archive(l, msg, err)
|
||||
} else {
|
||||
p.retry(l, msg, err, true /*isFailure*/)
|
||||
default:
|
||||
p.retry(l, msg, err, p.isFailureFunc(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +352,8 @@ func (p *processor) retry(l *base.Lease, msg *base.TaskMessage, e error, isFailu
|
||||
// If lease is not valid, do not write to redis; Let recoverer take care of it.
|
||||
return
|
||||
}
|
||||
ctx, _ := context.WithDeadline(context.Background(), l.Deadline())
|
||||
ctx, cancel := context.WithDeadline(context.Background(), l.Deadline())
|
||||
defer cancel()
|
||||
d := p.retryDelayFunc(msg.Retried, e, NewTask(msg.Type, msg.Payload))
|
||||
retryAt := time.Now().Add(d)
|
||||
err := p.broker.Retry(ctx, msg, retryAt, e.Error(), isFailure)
|
||||
@@ -367,7 +375,8 @@ func (p *processor) archive(l *base.Lease, msg *base.TaskMessage, e error) {
|
||||
// If lease is not valid, do not write to redis; Let recoverer take care of it.
|
||||
return
|
||||
}
|
||||
ctx, _ := context.WithDeadline(context.Background(), l.Deadline())
|
||||
ctx, cancel := context.WithDeadline(context.Background(), l.Deadline())
|
||||
defer cancel()
|
||||
err := p.broker.Archive(ctx, msg, e.Error())
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Could not move task id=%s from %q to %q", msg.ID, base.ActiveKey(msg.Queue), base.ArchivedKey(msg.Queue))
|
||||
@@ -404,8 +413,7 @@ func (p *processor) queues() []string {
|
||||
names = append(names, qname)
|
||||
}
|
||||
}
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
r.Shuffle(len(names), func(i, j int) { names[i], names[j] = names[j], names[i] })
|
||||
rand.Shuffle(len(names), func(i, j int) { names[i], names[j] = names[j], names[i] })
|
||||
return uniq(names, len(p.queueConfig))
|
||||
}
|
||||
|
||||
@@ -415,21 +423,19 @@ func (p *processor) queues() []string {
|
||||
func (p *processor) perform(ctx context.Context, task *Task) (err error) {
|
||||
defer func() {
|
||||
if x := recover(); x != nil {
|
||||
errMsg := string(debug.Stack())
|
||||
|
||||
p.logger.Errorf("recovering from panic. See the stack trace below for details:\n%s", errMsg)
|
||||
p.logger.Errorf("recovering from panic. See the stack trace below for details:\n%s", string(debug.Stack()))
|
||||
_, file, line, ok := runtime.Caller(1) // skip the first frame (panic itself)
|
||||
if ok && strings.Contains(file, "runtime/") {
|
||||
// The panic came from the runtime, most likely due to incorrect
|
||||
// map/slice usage. The parent frame should have the real trigger.
|
||||
_, file, line, ok = runtime.Caller(2)
|
||||
}
|
||||
|
||||
var errMsg string
|
||||
// Include the file and line number info in the error, if runtime.Caller returned ok.
|
||||
if ok {
|
||||
err = fmt.Errorf("panic [%s:%d]: %v", file, line, x)
|
||||
errMsg = fmt.Sprintf("panic [%s:%d]: %v", file, line, x)
|
||||
} else {
|
||||
err = fmt.Errorf("panic: %v", x)
|
||||
errMsg = fmt.Sprintf("panic: %v", x)
|
||||
}
|
||||
err = &errors.PanicError{
|
||||
ErrMsg: errMsg,
|
||||
|
@@ -295,6 +295,7 @@ func TestProcessorRetry(t *testing.T) {
|
||||
|
||||
errMsg := "something went wrong"
|
||||
wrappedSkipRetry := fmt.Errorf("%s:%w", errMsg, SkipRetry)
|
||||
wrappedRevokeTask := fmt.Errorf("%s:%w", errMsg, RevokeTask)
|
||||
|
||||
tests := []struct {
|
||||
desc string // test description
|
||||
@@ -312,7 +313,7 @@ func TestProcessorRetry(t *testing.T) {
|
||||
pending: []*base.TaskMessage{m1, m2, m3, m4},
|
||||
delay: time.Minute,
|
||||
handler: HandlerFunc(func(ctx context.Context, task *Task) error {
|
||||
return fmt.Errorf(errMsg)
|
||||
return errors.New(errMsg)
|
||||
}),
|
||||
wait: 2 * time.Second,
|
||||
wantErrMsg: errMsg,
|
||||
@@ -346,6 +347,32 @@ func TestProcessorRetry(t *testing.T) {
|
||||
wantArchived: []*base.TaskMessage{m1, m2},
|
||||
wantErrCount: 2, // ErrorHandler should still be called with SkipRetry error
|
||||
},
|
||||
{
|
||||
desc: "Should revoke task",
|
||||
pending: []*base.TaskMessage{m1, m2},
|
||||
delay: time.Minute,
|
||||
handler: HandlerFunc(func(ctx context.Context, task *Task) error {
|
||||
return RevokeTask // return RevokeTask without wrapping
|
||||
}),
|
||||
wait: 2 * time.Second,
|
||||
wantErrMsg: RevokeTask.Error(),
|
||||
wantRetry: []*base.TaskMessage{},
|
||||
wantArchived: []*base.TaskMessage{},
|
||||
wantErrCount: 2, // ErrorHandler should still be called with RevokeTask error
|
||||
},
|
||||
{
|
||||
desc: "Should revoke task (with error wrapping)",
|
||||
pending: []*base.TaskMessage{m1, m2},
|
||||
delay: time.Minute,
|
||||
handler: HandlerFunc(func(ctx context.Context, task *Task) error {
|
||||
return wrappedRevokeTask
|
||||
}),
|
||||
wait: 2 * time.Second,
|
||||
wantErrMsg: wrappedRevokeTask.Error(),
|
||||
wantRetry: []*base.TaskMessage{},
|
||||
wantArchived: []*base.TaskMessage{},
|
||||
wantErrCount: 2, // ErrorHandler should still be called with RevokeTask error
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
41
scheduler.go
41
scheduler.go
@@ -10,11 +10,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
"github.com/hibiken/asynq/internal/log"
|
||||
"github.com/hibiken/asynq/internal/rdb"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
@@ -43,15 +43,27 @@ type Scheduler struct {
|
||||
// to avoid using cron.EntryID as the public API of
|
||||
// the Scheduler.
|
||||
idmap map[string]cron.EntryID
|
||||
// When a Scheduler has been created with an existing Redis connection, we do
|
||||
// not want to close it.
|
||||
sharedConnection bool
|
||||
}
|
||||
|
||||
// NewScheduler returns a new Scheduler instance given the redis connection option.
|
||||
// The parameter opts is optional, defaults will be used if opts is set to nil
|
||||
func NewScheduler(r RedisConnOpt, opts *SchedulerOpts) *Scheduler {
|
||||
c, ok := r.MakeRedisClient().(redis.UniversalClient)
|
||||
redisClient, ok := r.MakeRedisClient().(redis.UniversalClient)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("asynq: unsupported RedisConnOpt type %T", r))
|
||||
}
|
||||
scheduler := NewSchedulerFromRedisClient(redisClient, opts)
|
||||
scheduler.sharedConnection = false
|
||||
return scheduler
|
||||
}
|
||||
|
||||
// NewSchedulerFromRedisClient returns a new instance of Scheduler given a redis.UniversalClient
|
||||
// The parameter opts is optional, defaults will be used if opts is set to nil.
|
||||
// Warning: The underlying redis connection pool will not be closed by Asynq, you are responsible for closing it.
|
||||
func NewSchedulerFromRedisClient(c redis.UniversalClient, opts *SchedulerOpts) *Scheduler {
|
||||
if opts == nil {
|
||||
opts = &SchedulerOpts{}
|
||||
}
|
||||
@@ -72,7 +84,7 @@ func NewScheduler(r RedisConnOpt, opts *SchedulerOpts) *Scheduler {
|
||||
id: generateSchedulerID(),
|
||||
state: &serverState{value: srvStateNew},
|
||||
logger: logger,
|
||||
client: NewClient(r),
|
||||
client: NewClientFromRedisClient(c),
|
||||
rdb: rdb.NewRDB(c),
|
||||
cron: cron.New(cron.WithLocation(loc)),
|
||||
location: loc,
|
||||
@@ -261,8 +273,12 @@ func (s *Scheduler) Shutdown() {
|
||||
s.wg.Wait()
|
||||
|
||||
s.clearHistory()
|
||||
s.client.Close()
|
||||
s.rdb.Close()
|
||||
if err := s.client.Close(); err != nil {
|
||||
s.logger.Errorf("Failed to close redis client connection: %v", err)
|
||||
}
|
||||
if !s.sharedConnection {
|
||||
s.rdb.Close()
|
||||
}
|
||||
s.logger.Info("Scheduler stopped")
|
||||
}
|
||||
|
||||
@@ -273,7 +289,9 @@ func (s *Scheduler) runHeartbeater() {
|
||||
select {
|
||||
case <-s.done:
|
||||
s.logger.Debugf("Scheduler heatbeater shutting down")
|
||||
s.rdb.ClearSchedulerEntries(s.id)
|
||||
if err := s.rdb.ClearSchedulerEntries(s.id); err != nil {
|
||||
s.logger.Errorf("Failed to clear the scheduler entries: %v", err)
|
||||
}
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
@@ -320,3 +338,14 @@ func (s *Scheduler) clearHistory() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ping performs a ping against the redis connection.
|
||||
func (s *Scheduler) Ping() error {
|
||||
s.state.mu.Lock()
|
||||
defer s.state.mu.Unlock()
|
||||
if s.state.value == srvStateClosed {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.rdb.Ping()
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
"github.com/hibiken/asynq/internal/testutil"
|
||||
@@ -58,6 +59,7 @@ func TestSchedulerRegister(t *testing.T) {
|
||||
|
||||
r := setup(t)
|
||||
|
||||
// Tests for new redis connection.
|
||||
for _, tc := range tests {
|
||||
scheduler := NewScheduler(getRedisConnOpt(t), nil)
|
||||
if _, err := scheduler.Register(tc.cronspec, tc.task, tc.opts...); err != nil {
|
||||
@@ -75,6 +77,28 @@ func TestSchedulerRegister(t *testing.T) {
|
||||
t.Errorf("mismatch found in queue %q: (-want,+got)\n%s", tc.queue, diff)
|
||||
}
|
||||
}
|
||||
|
||||
r = setup(t)
|
||||
|
||||
// Tests for existing redis connection.
|
||||
for _, tc := range tests {
|
||||
redisClient := getRedisConnOpt(t).MakeRedisClient().(redis.UniversalClient)
|
||||
scheduler := NewSchedulerFromRedisClient(redisClient, nil)
|
||||
if _, err := scheduler.Register(tc.cronspec, tc.task, tc.opts...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := scheduler.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(tc.wait)
|
||||
scheduler.Shutdown()
|
||||
|
||||
got := testutil.GetPendingMessages(t, r, tc.queue)
|
||||
if diff := cmp.Diff(tc.want, got, testutil.IgnoreIDOpt); diff != "" {
|
||||
t.Errorf("mismatch found in queue %q: (-want,+got)\n%s", tc.queue, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchedulerWhenRedisDown(t *testing.T) {
|
||||
@@ -90,7 +114,7 @@ func TestSchedulerWhenRedisDown(t *testing.T) {
|
||||
|
||||
// Connect to non-existent redis instance to simulate a redis server being down.
|
||||
scheduler := NewScheduler(
|
||||
RedisClientOpt{Addr: ":9876"},
|
||||
RedisClientOpt{Addr: ":9876"}, // no Redis listening to this port.
|
||||
&SchedulerOpts{EnqueueErrorHandler: errorHandler},
|
||||
)
|
||||
|
||||
|
107
server.go
107
server.go
@@ -9,7 +9,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -37,6 +37,9 @@ type Server struct {
|
||||
logger *log.Logger
|
||||
|
||||
broker base.Broker
|
||||
// When a Server has been created with an existing Redis connection, we do
|
||||
// not want to close it.
|
||||
sharedConnection bool
|
||||
|
||||
state *serverState
|
||||
|
||||
@@ -103,7 +106,7 @@ type Config struct {
|
||||
// If BaseContext is nil, the default is context.Background().
|
||||
// If this is defined, then it MUST return a non-nil context
|
||||
BaseContext func() context.Context
|
||||
|
||||
|
||||
// TaskCheckInterval specifies the interval between checks for new tasks to process when all queues are empty.
|
||||
//
|
||||
// If unset, zero or a negative value, the interval is set to 1 second.
|
||||
@@ -239,6 +242,17 @@ type Config struct {
|
||||
//
|
||||
// If unset or nil, the group aggregation feature will be disabled on the server.
|
||||
GroupAggregator GroupAggregator
|
||||
|
||||
// JanitorInterval specifies the average interval of janitor checks for expired completed tasks.
|
||||
//
|
||||
// If unset or zero, default interval of 8 seconds is used.
|
||||
JanitorInterval time.Duration
|
||||
|
||||
// JanitorBatchSize specifies the number of expired completed tasks to be deleted in one run.
|
||||
//
|
||||
// If unset or zero, default batch size of 100 is used.
|
||||
// Make sure to not put a big number as the batch size to prevent a long-running script.
|
||||
JanitorBatchSize int
|
||||
}
|
||||
|
||||
// GroupAggregator aggregates a group of tasks into one before the tasks are passed to the Handler.
|
||||
@@ -386,9 +400,8 @@ func toInternalLogLevel(l LogLevel) log.Level {
|
||||
// DefaultRetryDelayFunc is the default RetryDelayFunc used if one is not specified in Config.
|
||||
// It uses exponential back-off strategy to calculate the retry delay.
|
||||
func DefaultRetryDelayFunc(n int, e error, t *Task) time.Duration {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
// Formula taken from https://github.com/mperham/sidekiq.
|
||||
s := int(math.Pow(float64(n), 4)) + 15 + (r.Intn(30) * (n + 1))
|
||||
s := int(math.Pow(float64(n), 4)) + 15 + (rand.IntN(30) * (n + 1))
|
||||
return time.Duration(s) * time.Second
|
||||
}
|
||||
|
||||
@@ -408,15 +421,28 @@ const (
|
||||
defaultDelayedTaskCheckInterval = 5 * time.Second
|
||||
|
||||
defaultGroupGracePeriod = 1 * time.Minute
|
||||
|
||||
defaultJanitorInterval = 8 * time.Second
|
||||
|
||||
defaultJanitorBatchSize = 100
|
||||
)
|
||||
|
||||
// NewServer returns a new Server given a redis connection option
|
||||
// and server configuration.
|
||||
func NewServer(r RedisConnOpt, cfg Config) *Server {
|
||||
c, ok := r.MakeRedisClient().(redis.UniversalClient)
|
||||
redisClient, ok := r.MakeRedisClient().(redis.UniversalClient)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("asynq: unsupported RedisConnOpt type %T", r))
|
||||
}
|
||||
server := NewServerFromRedisClient(redisClient, cfg)
|
||||
server.sharedConnection = false
|
||||
return server
|
||||
}
|
||||
|
||||
// NewServerFromRedisClient returns a new instance of Server given a redis.UniversalClient
|
||||
// and server configuration
|
||||
// Warning: The underlying redis connection pool will not be closed by Asynq, you are responsible for closing it.
|
||||
func NewServerFromRedisClient(c redis.UniversalClient, cfg Config) *Server {
|
||||
baseCtxFn := cfg.BaseContext
|
||||
if baseCtxFn == nil {
|
||||
baseCtxFn = context.Background
|
||||
@@ -547,11 +573,26 @@ func NewServer(r RedisConnOpt, cfg Config) *Server {
|
||||
interval: healthcheckInterval,
|
||||
healthcheckFunc: cfg.HealthCheckFunc,
|
||||
})
|
||||
|
||||
janitorInterval := cfg.JanitorInterval
|
||||
if janitorInterval == 0 {
|
||||
janitorInterval = defaultJanitorInterval
|
||||
}
|
||||
|
||||
janitorBatchSize := cfg.JanitorBatchSize
|
||||
if janitorBatchSize == 0 {
|
||||
janitorBatchSize = defaultJanitorBatchSize
|
||||
}
|
||||
if janitorBatchSize > defaultJanitorBatchSize {
|
||||
logger.Warnf("Janitor batch size of %d is greater than the recommended batch size of %d. "+
|
||||
"This might cause a long-running script", janitorBatchSize, defaultJanitorBatchSize)
|
||||
}
|
||||
janitor := newJanitor(janitorParams{
|
||||
logger: logger,
|
||||
broker: rdb,
|
||||
queues: qnames,
|
||||
interval: 8 * time.Second,
|
||||
logger: logger,
|
||||
broker: rdb,
|
||||
queues: qnames,
|
||||
interval: janitorInterval,
|
||||
batchSize: janitorBatchSize,
|
||||
})
|
||||
aggregator := newAggregator(aggregatorParams{
|
||||
logger: logger,
|
||||
@@ -563,18 +604,19 @@ func NewServer(r RedisConnOpt, cfg Config) *Server {
|
||||
groupAggregator: cfg.GroupAggregator,
|
||||
})
|
||||
return &Server{
|
||||
logger: logger,
|
||||
broker: rdb,
|
||||
state: srvState,
|
||||
forwarder: forwarder,
|
||||
processor: processor,
|
||||
syncer: syncer,
|
||||
heartbeater: heartbeater,
|
||||
subscriber: subscriber,
|
||||
recoverer: recoverer,
|
||||
healthchecker: healthchecker,
|
||||
janitor: janitor,
|
||||
aggregator: aggregator,
|
||||
logger: logger,
|
||||
broker: rdb,
|
||||
sharedConnection: true,
|
||||
state: srvState,
|
||||
forwarder: forwarder,
|
||||
processor: processor,
|
||||
syncer: syncer,
|
||||
heartbeater: heartbeater,
|
||||
subscriber: subscriber,
|
||||
recoverer: recoverer,
|
||||
healthchecker: healthchecker,
|
||||
janitor: janitor,
|
||||
aggregator: aggregator,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,6 +632,10 @@ func NewServer(r RedisConnOpt, cfg Config) *Server {
|
||||
// One exception to this rule is when ProcessTask returns a SkipRetry error.
|
||||
// If the returned error is SkipRetry or an error wraps SkipRetry, retry is
|
||||
// skipped and the task will be immediately archived instead.
|
||||
//
|
||||
// Another exception to this rule is when ProcessTask returns a RevokeTask error.
|
||||
// If the returned error is RevokeTask or an error wraps RevokeTask, the task
|
||||
// will not be retried or archived.
|
||||
type Handler interface {
|
||||
ProcessTask(context.Context, *Task) error
|
||||
}
|
||||
@@ -702,7 +748,9 @@ func (srv *Server) Shutdown() {
|
||||
srv.heartbeater.shutdown()
|
||||
srv.wg.Wait()
|
||||
|
||||
srv.broker.Close()
|
||||
if !srv.sharedConnection {
|
||||
srv.broker.Close()
|
||||
}
|
||||
srv.logger.Info("Exiting")
|
||||
}
|
||||
|
||||
@@ -714,7 +762,7 @@ func (srv *Server) Shutdown() {
|
||||
func (srv *Server) Stop() {
|
||||
srv.state.mu.Lock()
|
||||
if srv.state.value != srvStateActive {
|
||||
// Invalid calll to Stop, server can only go from Active state to Stopped state.
|
||||
// Invalid call to Stop, server can only go from Active state to Stopped state.
|
||||
srv.state.mu.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -725,3 +773,16 @@ func (srv *Server) Stop() {
|
||||
srv.processor.stop()
|
||||
srv.logger.Info("Processor stopped")
|
||||
}
|
||||
|
||||
// Ping performs a ping against the redis connection.
|
||||
//
|
||||
// This is an alternative to the HealthCheckFunc available in the Config object.
|
||||
func (srv *Server) Ping() error {
|
||||
srv.state.mu.Lock()
|
||||
defer srv.state.mu.Unlock()
|
||||
if srv.state.value == srvStateClosed {
|
||||
return nil
|
||||
}
|
||||
|
||||
return srv.broker.Ping()
|
||||
}
|
||||
|
@@ -14,22 +14,12 @@ import (
|
||||
"github.com/hibiken/asynq/internal/rdb"
|
||||
"github.com/hibiken/asynq/internal/testbroker"
|
||||
"github.com/hibiken/asynq/internal/testutil"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
// https://github.com/go-redis/redis/issues/1029
|
||||
ignoreOpt := goleak.IgnoreTopFunction("github.com/redis/go-redis/v9/internal/pool.(*ConnPool).reaper")
|
||||
defer goleak.VerifyNone(t, ignoreOpt)
|
||||
|
||||
redisConnOpt := getRedisConnOpt(t)
|
||||
c := NewClient(redisConnOpt)
|
||||
defer c.Close()
|
||||
srv := NewServer(redisConnOpt, Config{
|
||||
Concurrency: 10,
|
||||
LogLevel: testLogLevel,
|
||||
})
|
||||
|
||||
func testServer(t *testing.T, c *Client, srv *Server) {
|
||||
// no-op handler
|
||||
h := func(ctx context.Context, task *Task) error {
|
||||
return nil
|
||||
@@ -53,18 +43,55 @@ func TestServer(t *testing.T) {
|
||||
srv.Shutdown()
|
||||
}
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
// https://github.com/go-redis/redis/issues/1029
|
||||
ignoreOpt := goleak.IgnoreTopFunction("github.com/redis/go-redis/v9/internal/pool.(*ConnPool).reaper")
|
||||
defer goleak.VerifyNone(t, ignoreOpt)
|
||||
|
||||
redisConnOpt := getRedisConnOpt(t)
|
||||
c := NewClient(redisConnOpt)
|
||||
defer c.Close()
|
||||
srv := NewServer(redisConnOpt, Config{
|
||||
Concurrency: 10,
|
||||
LogLevel: testLogLevel,
|
||||
})
|
||||
|
||||
testServer(t, c, srv)
|
||||
}
|
||||
|
||||
func TestServerFromRedisClient(t *testing.T) {
|
||||
// https://github.com/go-redis/redis/issues/1029
|
||||
ignoreOpt := goleak.IgnoreTopFunction("github.com/redis/go-redis/v9/internal/pool.(*ConnPool).reaper")
|
||||
defer goleak.VerifyNone(t, ignoreOpt)
|
||||
|
||||
redisConnOpt := getRedisConnOpt(t)
|
||||
redisClient := redisConnOpt.MakeRedisClient().(redis.UniversalClient)
|
||||
c := NewClientFromRedisClient(redisClient)
|
||||
srv := NewServerFromRedisClient(redisClient, Config{
|
||||
Concurrency: 10,
|
||||
LogLevel: testLogLevel,
|
||||
})
|
||||
|
||||
testServer(t, c, srv)
|
||||
|
||||
err := c.Close()
|
||||
if err == nil {
|
||||
t.Error("client.Close() should have failed because of a shared client but it didn't")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerRun(t *testing.T) {
|
||||
// https://github.com/go-redis/redis/issues/1029
|
||||
ignoreOpt := goleak.IgnoreTopFunction("github.com/redis/go-redis/v9/internal/pool.(*ConnPool).reaper")
|
||||
defer goleak.VerifyNone(t, ignoreOpt)
|
||||
|
||||
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{LogLevel: testLogLevel})
|
||||
srv := NewServer(getRedisConnOpt(t), Config{LogLevel: testLogLevel})
|
||||
|
||||
done := make(chan struct{})
|
||||
// Make sure server exits when receiving TERM signal.
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
|
||||
_ = syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
@@ -83,7 +110,7 @@ func TestServerRun(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerErrServerClosed(t *testing.T) {
|
||||
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{LogLevel: testLogLevel})
|
||||
srv := NewServer(getRedisConnOpt(t), Config{LogLevel: testLogLevel})
|
||||
handler := NewServeMux()
|
||||
if err := srv.Start(handler); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -96,7 +123,7 @@ func TestServerErrServerClosed(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerErrNilHandler(t *testing.T) {
|
||||
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{LogLevel: testLogLevel})
|
||||
srv := NewServer(getRedisConnOpt(t), Config{LogLevel: testLogLevel})
|
||||
err := srv.Start(nil)
|
||||
if err == nil {
|
||||
t.Error("Starting server with nil handler: (*Server).Start(nil) did not return error")
|
||||
@@ -105,7 +132,7 @@ func TestServerErrNilHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerErrServerRunning(t *testing.T) {
|
||||
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{LogLevel: testLogLevel})
|
||||
srv := NewServer(getRedisConnOpt(t), Config{LogLevel: testLogLevel})
|
||||
handler := NewServeMux()
|
||||
if err := srv.Start(handler); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -126,7 +153,7 @@ func TestServerWithRedisDown(t *testing.T) {
|
||||
}()
|
||||
r := rdb.NewRDB(setup(t))
|
||||
testBroker := testbroker.NewTestBroker(r)
|
||||
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{LogLevel: testLogLevel})
|
||||
srv := NewServer(getRedisConnOpt(t), Config{LogLevel: testLogLevel})
|
||||
srv.broker = testBroker
|
||||
srv.forwarder.broker = testBroker
|
||||
srv.heartbeater.broker = testBroker
|
||||
|
@@ -1,4 +1,4 @@
|
||||
//go:build linux || bsd || darwin
|
||||
//go:build linux || dragonfly || freebsd || netbsd || openbsd || darwin
|
||||
|
||||
package asynq
|
||||
|
||||
|
@@ -25,7 +25,7 @@ To view details on any command, use `asynq help <command> <subcommand>`.
|
||||
- `asynq dash`
|
||||
- `asynq stats`
|
||||
- `asynq queue [ls inspect history rm pause unpause]`
|
||||
- `asynq task [ls cancel delete archive run delete-all archive-all run-all]`
|
||||
- `asynq task [ls cancel delete archive run deleteall archiveall runall]`
|
||||
- `asynq server [ls]`
|
||||
|
||||
### Global flags
|
||||
|
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -369,7 +370,12 @@ func createRDB() *rdb.RDB {
|
||||
return rdb.NewRDB(c)
|
||||
}
|
||||
|
||||
// createRDB creates a Inspector instance using flag values and returns it.
|
||||
// createClient creates a Client instance using flag values and returns it.
|
||||
func createClient() *asynq.Client {
|
||||
return asynq.NewClient(getRedisConnOpt())
|
||||
}
|
||||
|
||||
// createInspector creates a Inspector instance using flag values and returns it.
|
||||
func createInspector() *asynq.Inspector {
|
||||
return asynq.NewInspector(getRedisConnOpt())
|
||||
}
|
||||
@@ -456,3 +462,37 @@ func isPrintable(data []byte) bool {
|
||||
}
|
||||
return !isAllSpace
|
||||
}
|
||||
|
||||
// Helper to turn a command line flag into a duration
|
||||
func getDuration(cmd *cobra.Command, arg string) time.Duration {
|
||||
durationStr, err := cmd.Flags().GetString(arg)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(durationStr)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return duration
|
||||
}
|
||||
|
||||
// Helper to turn a command line flag into a time
|
||||
func getTime(cmd *cobra.Command, arg string) time.Time {
|
||||
timeStr, err := cmd.Flags().GetString(arg)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
timeVal, err := time.Parse(time.RFC3339, timeStr)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return timeVal
|
||||
}
|
||||
|
@@ -53,6 +53,24 @@ func init() {
|
||||
taskRunCmd.MarkFlagRequired("queue")
|
||||
taskRunCmd.MarkFlagRequired("id")
|
||||
|
||||
taskCmd.AddCommand(taskEnqueueCmd)
|
||||
taskEnqueueCmd.Flags().StringP("type_name", "t", "", "type name to enqueue the task as (required)")
|
||||
taskEnqueueCmd.Flags().StringP("payload", "l", "", "payload to enqueue (required)")
|
||||
// The following are the various OptionTypes; if not specified we won't pass them so that composeOptions()
|
||||
// can apply its own defaults
|
||||
taskEnqueueCmd.Flags().Int("retry", 0, "maximum retries")
|
||||
taskEnqueueCmd.Flags().String("queue", "", "queue to enqueue the task to")
|
||||
taskEnqueueCmd.Flags().String("id", "", "id to enqueue the task as")
|
||||
taskEnqueueCmd.Flags().String("timeout", "", "timeout for the task (how long it can run); must be parseable as a time.Duration")
|
||||
taskEnqueueCmd.Flags().String("deadline", "", "deadline for the task; must be in RFC3339 format")
|
||||
taskEnqueueCmd.Flags().String("unique", "", "unique period for the task (duration within which it is guaranteed to be unique); must be parseable as a time.Duration")
|
||||
taskEnqueueCmd.Flags().String("process_at", "", "process at time for the task; must be in RFC3339 format")
|
||||
taskEnqueueCmd.Flags().String("process_in", "", "process in window for the task; must be parseable as a time.Duration")
|
||||
taskEnqueueCmd.Flags().String("retention", "", "retention window for the task; must be parseable as a time.Duration")
|
||||
taskEnqueueCmd.Flags().String("group", "", "group for the task")
|
||||
taskEnqueueCmd.MarkFlagRequired("type_name")
|
||||
taskEnqueueCmd.MarkFlagRequired("payload")
|
||||
|
||||
taskCmd.AddCommand(taskArchiveAllCmd)
|
||||
taskArchiveAllCmd.Flags().StringP("queue", "q", "", "queue to which the tasks belong (required)")
|
||||
taskArchiveAllCmd.Flags().StringP("state", "s", "", "state of the tasks; one of { pending | aggregating | scheduled | retry } (required)")
|
||||
@@ -151,6 +169,16 @@ var taskRunCmd = &cobra.Command{
|
||||
$ asynq task run --queue=myqueue --id=f1720682-f5a6-4db1-8953-4f48ae541d0f`),
|
||||
}
|
||||
|
||||
var taskEnqueueCmd = &cobra.Command{
|
||||
Use: "enqueue --type_name=footype --payload=barpayload",
|
||||
Short: "Enqueue a task",
|
||||
Args: cobra.NoArgs,
|
||||
Run: taskEnqueue,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq task enqueue -t footype -l barpayload
|
||||
$ asynq task enqueue -t footask -l barpayload --retry 3 --id f1720682-f5a6-4db1-8953-4f48ae541d0f --queue bazqueue --timeout 100s --deadline 2024-12-14T01:23:45Z --unique 100s --process_at 2024-12-14T01:22:05Z --process_in 100s --retention 5h --group baygroup`),
|
||||
}
|
||||
|
||||
var taskArchiveAllCmd = &cobra.Command{
|
||||
Use: "archiveall --queue=<queue> --state=<state>",
|
||||
Short: "Archive all tasks in the given state",
|
||||
@@ -521,6 +549,95 @@ func taskRun(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("task is now pending")
|
||||
}
|
||||
|
||||
func taskEnqueue(cmd *cobra.Command, args []string) {
|
||||
typeName, err := cmd.Flags().GetString("type_name")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
payload, err := cmd.Flags().GetString("payload")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// For all of the optional flags, we need to explicitly check whether they were set or
|
||||
// not; for consistency we want to use the defaults set in composeOptions() rather than
|
||||
// the ones in the flag definitions.
|
||||
opts := []asynq.Option{}
|
||||
if cmd.Flags().Changed("retry") {
|
||||
retry, err := cmd.Flags().GetInt("retry")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, asynq.MaxRetry(retry))
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("queue") {
|
||||
queue, err := cmd.Flags().GetString("queue")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, asynq.Queue(queue))
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("id") {
|
||||
id, err := cmd.Flags().GetString("id")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, asynq.TaskID(id))
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("timeout") {
|
||||
opts = append(opts, asynq.Timeout(getDuration(cmd, "timeout")))
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("deadline") {
|
||||
opts = append(opts, asynq.Deadline(getTime(cmd, "deadline")))
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("unique") {
|
||||
opts = append(opts, asynq.Unique(getDuration(cmd, "unique")))
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("process_at") {
|
||||
opts = append(opts, asynq.ProcessAt(getTime(cmd, "process_at")))
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("process_in") {
|
||||
opts = append(opts, asynq.ProcessIn(getDuration(cmd, "process_in")))
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("retention") {
|
||||
opts = append(opts, asynq.Retention(getDuration(cmd, "retention")))
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("group") {
|
||||
group, err := cmd.Flags().GetString("group")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, asynq.Group(group))
|
||||
}
|
||||
|
||||
c := createClient()
|
||||
task := asynq.NewTask(typeName, []byte(payload), opts...)
|
||||
|
||||
taskInfo, err := c.Enqueue(task)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Enqueued task %s to queue %s\n", taskInfo.ID, taskInfo.Queue)
|
||||
}
|
||||
|
||||
func taskArchiveAll(cmd *cobra.Command, args []string) {
|
||||
qname, err := cmd.Flags().GetString("queue")
|
||||
if err != nil {
|
||||
@@ -653,3 +770,4 @@ func taskRunAll(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
fmt.Printf("%d tasks are now pending\n", n)
|
||||
}
|
||||
|
||||
|
10
tools/go.mod
10
tools/go.mod
@@ -1,10 +1,10 @@
|
||||
module github.com/hibiken/asynq/tools
|
||||
|
||||
go 1.20
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/gdamore/tcell/v2 v2.5.1
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/hibiken/asynq v0.24.1
|
||||
@@ -31,8 +31,8 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.11 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/pelletier/go-toml v1.2.0 // indirect
|
||||
@@ -45,7 +45,7 @@ require (
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.0.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
|
26
tools/go.sum
26
tools/go.sum
@@ -58,9 +58,10 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
@@ -176,20 +177,22 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
@@ -266,6 +269,7 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
@@ -388,7 +392,6 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -398,7 +401,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -414,8 +416,10 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
4
x/go.mod
4
x/go.mod
@@ -1,9 +1,9 @@
|
||||
module github.com/hibiken/asynq/x
|
||||
|
||||
go 1.20
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/hibiken/asynq v0.24.1
|
||||
github.com/prometheus/client_golang v1.11.1
|
||||
github.com/redis/go-redis/v9 v9.3.0
|
||||
|
11
x/go.sum
11
x/go.sum
@@ -10,8 +10,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -20,6 +22,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
@@ -49,10 +52,11 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw=
|
||||
github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
@@ -66,9 +70,11 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -105,6 +111,7 @@ github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
|
Reference in New Issue
Block a user