mirror of
https://github.com/hibiken/asynq.git
synced 2025-04-21 08:10:19 +08:00
Compare commits
122 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c327bc40a2 | ||
|
ea0c6e93f0 | ||
|
489e21920b | ||
|
043dcfbf56 | ||
|
02907551b4 | ||
|
127fac2e90 | ||
|
106c07adaa | ||
|
1c7195ff1a | ||
|
12cbba4926 | ||
|
80479b528d | ||
|
e14c312fe3 | ||
|
ad1f587403 | ||
|
8b32b38fd5 | ||
|
96a84fac0c | ||
|
d2c207fbb8 | ||
|
1a7c61ac49 | ||
|
87375b5534 | ||
|
ffd75ebb5f | ||
|
d64fd328cb | ||
|
4f00f52c1d | ||
|
580d69e88f | ||
|
c97652d408 | ||
|
4644d37ef4 | ||
|
45c0fc6ad9 | ||
|
fd3eb86d95 | ||
|
3dbda60333 | ||
|
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 | ||
|
0a73fc6201 | ||
|
1a11a33b4f | ||
|
f0888df813 | ||
|
c2dd648a51 | ||
|
a3cca853a0 | ||
|
83df622a92 | ||
|
fdbf54eb04 | ||
|
16ec43cbca | ||
|
1e0bf88bf3 | ||
|
d0041c55a3 | ||
|
7ef0511f35 | ||
|
1ec90810db | ||
|
90188a093d | ||
|
e05f0b7196 | ||
|
c1096a0fae | ||
|
9e548fc097 | ||
|
6a7bf2ceff | ||
|
e7fa0ae865 | ||
|
fc4b6713f6 | ||
|
6b98c0bbae | ||
|
ed1ab8ee55 | ||
|
e18c0381ad | ||
|
8b422c237c | ||
|
e6f74c1c2b | ||
|
6edba6994e | ||
|
571f0d2613 | ||
|
2165ed133b | ||
|
551b0c7119 | ||
|
123d560a44 | ||
|
5bef53d1ac | ||
|
90af7749ca | ||
|
e4b8663154 | ||
|
fde294be32 | ||
|
cbb1be34ac | ||
|
6ed70adf3b | ||
|
1f42d71e9b | ||
|
f966a6c3b8 | ||
|
8b057b8767 | ||
|
c72bfef094 | ||
|
dffb78cca4 | ||
|
0275df8df4 | ||
|
cc777ebdaa | ||
|
783071c47f | ||
|
bafed907e9 | ||
|
0b8cfad703 | ||
|
c08f142b56 | ||
|
c70ff6a335 | ||
|
a04ba6411d | ||
|
d0209d9273 | ||
|
6c954c87bf | ||
|
86fe31990b | ||
|
e0e5d1ac24 | ||
|
30d409371b | ||
|
aefd276146 | ||
|
94ad9e5e74 | ||
|
5187844ca5 | ||
|
4dd2b5738a | ||
|
9116c096ec | ||
|
5c723f597e | ||
|
dd6f84c575 | ||
|
c438339c3d | ||
|
901938b0fe |
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
@ -1,12 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [hibiken] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
github: [hibiken]
|
||||
open_collective: ken-hibino
|
||||
|
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
|
||||
|
||||
---
|
||||
|
||||
|
24
.github/dependabot.yaml
vendored
Normal file
24
.github/dependabot.yaml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "pr-deps"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/tools"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "pr-deps"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/x"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "pr-deps"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
32
.github/workflows/benchstat.yml
vendored
32
.github/workflows/benchstat.yml
vendored
@ -11,20 +11,20 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
image: redis:7
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
go-version: 1.23.x
|
||||
- name: Benchmark
|
||||
run: go test -run=^$ -bench=. -count=5 -timeout=60m ./... | tee -a new.txt
|
||||
- name: Upload Benchmark
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bench-incoming
|
||||
path: new.txt
|
||||
@ -33,22 +33,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
image: redis:7
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.15.x
|
||||
go-version: 1.23.x
|
||||
- name: Benchmark
|
||||
run: go test -run=^$ -bench=. -count=5 -timeout=60m ./... | tee -a old.txt
|
||||
- name: Upload Benchmark
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bench-current
|
||||
path: old.txt
|
||||
@ -58,25 +58,25 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.15.x
|
||||
go-version: 1.23.x
|
||||
- name: Install benchstat
|
||||
run: go get -u golang.org/x/perf/cmd/benchstat
|
||||
- name: Download Incoming
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bench-incoming
|
||||
- name: Download Current
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bench-current
|
||||
- name: Benchstat Results
|
||||
run: benchstat old.txt new.txt | tee -a benchstat.txt
|
||||
- name: Upload benchstat results
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchstat
|
||||
path: benchstat.txt
|
||||
|
55
.github/workflows/build.yml
vendored
55
.github/workflows/build.yml
vendored
@ -7,20 +7,21 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
go-version: [1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x]
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
runs-on: ${{ matrix.os }}
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
image: redis:7
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
|
||||
- name: Build core module
|
||||
run: go build -v ./...
|
||||
@ -28,9 +29,6 @@ jobs:
|
||||
- name: Build x module
|
||||
run: cd x && go build -v ./... && cd ..
|
||||
|
||||
- name: Build tools module
|
||||
run: cd tools && go build -v ./... && cd ..
|
||||
|
||||
- name: Test core module
|
||||
run: go test -race -v -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
@ -41,4 +39,45 @@ jobs:
|
||||
run: go test -run=^$ -bench=. -loglevel=debug ./...
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v5
|
||||
|
||||
build-tool:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
runs-on: ${{ matrix.os }}
|
||||
services:
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
|
||||
- name: Build tools module
|
||||
run: cd tools && go build -v ./... && cd ..
|
||||
|
||||
- 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
|
||||
|
63
CHANGELOG.md
63
CHANGELOG.md
@ -7,7 +7,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.23.0] - 2022-03-11
|
||||
## [0.25.1] - 2024-12-11
|
||||
|
||||
### Upgrades
|
||||
|
||||
* Some packages
|
||||
|
||||
### Added
|
||||
|
||||
* Add `HeartbeatInterval` option to the scheduler (PR: https://github.com/hibiken/asynq/pull/956)
|
||||
* Add `RedisUniversalClient` support to periodic task manager (PR: https://github.com/hibiken/asynq/pull/958)
|
||||
* Add `--insecure` flag to CLI dash command (PR: https://github.com/hibiken/asynq/pull/980)
|
||||
* Add logging for registration errors (PR: https://github.com/hibiken/asynq/pull/657)
|
||||
|
||||
### Fixes
|
||||
- Perf: Use string concat inplace of fmt.Sprintf in hotpath (PR: https://github.com/hibiken/asynq/pull/962)
|
||||
- Perf: Init map with size (PR: https://github.com/hibiken/asynq/pull/673)
|
||||
- Fix: `Scheduler` and `PeriodicTaskManager` graceful shutdown (PR: https://github.com/hibiken/asynq/pull/977)
|
||||
- Fix: `Server` graceful shutdown on UNIX systems (PR: https://github.com/hibiken/asynq/pull/982)
|
||||
|
||||
## [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
|
||||
- Updated package version dependency for go-redis
|
||||
|
||||
## [0.24.0] - 2023-01-02
|
||||
|
||||
### Added
|
||||
- `PreEnqueueFunc`, `PostEnqueueFunc` is added in `Scheduler` and deprecated `EnqueueErrorHandler` (PR: https://github.com/hibiken/asynq/pull/476)
|
||||
|
||||
### Changed
|
||||
- Removed error log when `Scheduler` failed to enqueue a task. Use `PostEnqueueFunc` to check for errors and task actions if needed.
|
||||
- Changed log level from ERROR to WARNINING when `Scheduler` failed to record `SchedulerEnqueueEvent`.
|
||||
|
||||
## [0.23.0] - 2022-04-11
|
||||
|
||||
### Added
|
||||
|
||||
|
@ -38,7 +38,7 @@ Thank you! We'll try to respond as quickly as possible.
|
||||
## Contributing Code
|
||||
|
||||
1. Fork this repo
|
||||
2. Download your fork `git clone https://github.com/your-username/asynq && cd asynq`
|
||||
2. Download your fork `git clone git@github.com:your-username/asynq.git && cd asynq`
|
||||
3. Create your branch `git checkout -b your-branch-name`
|
||||
4. Make and commit your changes
|
||||
5. Push the branch `git push origin your-branch-name`
|
||||
|
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
|
||||
|
22
README.md
22
README.md
@ -33,10 +33,10 @@ Task queues are used as a mechanism to distribute work across multiple machines.
|
||||
- Low latency to add a task since writes are fast in Redis
|
||||
- De-duplication of tasks using [unique option](https://github.com/hibiken/asynq/wiki/Unique-Tasks)
|
||||
- Allow [timeout and deadline per task](https://github.com/hibiken/asynq/wiki/Task-Timeout-and-Cancelation)
|
||||
- Allow [aggregating group of tasks](https://github.com/hibiken/asynq/wiki/Task-aggregation) to batch multiple successive operations
|
||||
- [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
|
||||
@ -44,13 +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 accomodate 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.
|
||||
> ☝️ **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/)). Version `1.14` or higher is required.
|
||||
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:
|
||||
|
||||
@ -291,12 +297,12 @@ Asynq ships with a command line tool to inspect the state of queues and tasks.
|
||||
To install the CLI tool, run the following command:
|
||||
|
||||
```sh
|
||||
go get -u github.com/hibiken/asynq/tools/asynq
|
||||
go install github.com/hibiken/asynq/tools/asynq@latest
|
||||
```
|
||||
|
||||
Here's an example of running the `asynq stats` command:
|
||||
Here's an example of running the `asynq dash` command:
|
||||
|
||||

|
||||

|
||||
|
||||
For details on how to use the tool, refer to the tool's [README](/tools/asynq/README.md).
|
||||
|
||||
|
10
asynq.go
10
asynq.go
@ -14,7 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
)
|
||||
|
||||
@ -201,7 +201,7 @@ const (
|
||||
// Indicates that the task is processed successfully and retained until the retention TTL expires.
|
||||
TaskStateCompleted
|
||||
|
||||
// Indicates that the task is waiting in a group to be aggreated into one task.
|
||||
// Indicates that the task is waiting in a group to be aggregated into one task.
|
||||
TaskStateAggregating
|
||||
)
|
||||
|
||||
@ -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,
|
||||
@ -519,7 +523,7 @@ func parseRedisSentinelURI(u *url.URL) (RedisConnOpt, error) {
|
||||
if v, ok := u.User.Password(); ok {
|
||||
password = v
|
||||
}
|
||||
return RedisFailoverClientOpt{MasterName: master, SentinelAddrs: addrs, Password: password}, nil
|
||||
return RedisFailoverClientOpt{MasterName: master, SentinelAddrs: addrs, SentinelPassword: password}, nil
|
||||
}
|
||||
|
||||
// ResultWriter is a client interface to write result data for a task.
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hibiken/asynq/internal/log"
|
||||
@ -143,9 +143,9 @@ func TestParseRedisURI(t *testing.T) {
|
||||
{
|
||||
"redis-sentinel://:mypassword@localhost:5000,localhost:5001,localhost:5002?master=mymaster",
|
||||
RedisFailoverClientOpt{
|
||||
MasterName: "mymaster",
|
||||
SentinelAddrs: []string{"localhost:5000", "localhost:5001", "localhost:5002"},
|
||||
Password: "mypassword",
|
||||
MasterName: "mymaster",
|
||||
SentinelAddrs: []string{"localhost:5000", "localhost:5001", "localhost:5002"},
|
||||
SentinelPassword: "mypassword",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
39
client.go
39
client.go
@ -10,11 +10,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"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)
|
||||
}
|
||||
@ -237,7 +248,7 @@ func composeOptions(opts ...Option) (option, error) {
|
||||
retry: defaultMaxRetry,
|
||||
queue: base.DefaultQueueName,
|
||||
taskID: uuid.NewString(),
|
||||
timeout: 0, // do not set to deafultTimeout here
|
||||
timeout: 0, // do not set to defaultTimeout here
|
||||
deadline: time.Time{},
|
||||
processAt: time.Now(),
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -317,7 +331,7 @@ func (c *Client) Close() error {
|
||||
// The argument opts specifies the behavior of task processing.
|
||||
// If there are conflicting Option values the last one overrides others.
|
||||
// Any options provided to NewTask can be overridden by options passed to Enqueue.
|
||||
// By deafult, max retry is set to 25 and timeout is set to 30 minutes.
|
||||
// By default, max retry is set to 25 and timeout is set to 30 minutes.
|
||||
//
|
||||
// If no ProcessAt or ProcessIn options are provided, the task will be pending immediately.
|
||||
//
|
||||
@ -333,7 +347,7 @@ func (c *Client) Enqueue(task *Task, opts ...Option) (*TaskInfo, error) {
|
||||
// The argument opts specifies the behavior of task processing.
|
||||
// If there are conflicting Option values the last one overrides others.
|
||||
// Any options provided to NewTask can be overridden by options passed to Enqueue.
|
||||
// By deafult, max retry is set to 25 and timeout is set to 30 minutes.
|
||||
// By default, max retry is set to 25 and timeout is set to 30 minutes.
|
||||
//
|
||||
// If no ProcessAt or ProcessIn options are provided, the task will be pending immediately.
|
||||
//
|
||||
@ -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
|
||||
|
@ -28,7 +28,7 @@ func GetRetryCount(ctx context.Context) (n int, ok bool) {
|
||||
|
||||
// GetMaxRetry extracts maximum retry from a context, if any.
|
||||
//
|
||||
// Return value n indicates the maximum number of times the assoicated task
|
||||
// Return value n indicates the maximum number of times the associated task
|
||||
// can be retried if ProcessTask returns a non-nil error.
|
||||
func GetMaxRetry(ctx context.Context) (n int, ok bool) {
|
||||
return asynqcontext.GetMaxRetry(ctx)
|
||||
|
BIN
docs/assets/dash.gif
Normal file
BIN
docs/assets/dash.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 809 KiB |
27
go.mod
27
go.mod
@ -1,19 +1,20 @@
|
||||
module github.com/hibiken/asynq
|
||||
|
||||
go 1.14
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/go-redis/redis/v8 v8.11.2
|
||||
github.com/golang/protobuf v1.4.2
|
||||
github.com/google/go-cmp v0.5.6
|
||||
github.com/google/uuid v1.2.0
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/spf13/cast v1.3.1
|
||||
github.com/stretchr/testify v1.6.1 // indirect
|
||||
go.uber.org/goleak v0.10.0
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
|
||||
google.golang.org/protobuf v1.25.0
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
github.com/spf13/cast v1.7.0
|
||||
go.uber.org/goleak v1.3.0
|
||||
golang.org/x/sys v0.27.0
|
||||
golang.org/x/time v0.8.0
|
||||
google.golang.org/protobuf v1.35.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
)
|
||||
|
203
go.sum
203
go.sum
@ -1,177 +1,42 @@
|
||||
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w=
|
||||
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 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
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=
|
||||
github.com/go-redis/redis/v8 v8.11.2 h1:WqlSpAwz8mxDSMCvbyz1Mkiqe0LE5OY4j3lgkvu1Ts0=
|
||||
github.com/go-redis/redis/v8 v8.11.2/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4=
|
||||
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ=
|
||||
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
|
||||
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
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/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
|
||||
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190412213103-97732733099d/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-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
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/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.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/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
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
// Test goes through a few phases.
|
||||
//
|
||||
// Phase1: Simulate Server startup; Simulate starting tasks listed in startedWorkers
|
||||
// Phase2: Simluate finishing tasks listed in finishedTasks
|
||||
// Phase2: Simulate finishing tasks listed in finishedTasks
|
||||
// Phase3: Simulate Server shutdown;
|
||||
func TestHeartbeater(t *testing.T) {
|
||||
r := setup(t)
|
||||
@ -41,7 +41,7 @@ func TestHeartbeater(t *testing.T) {
|
||||
t5 := h.NewTaskMessageWithQueue("task5", nil, "custom")
|
||||
t6 := h.NewTaskMessageWithQueue("task6", nil, "default")
|
||||
|
||||
// Note: intentionally set to time less than now.Add(rdb.LeaseDuration) to test lease extention is working.
|
||||
// Note: intentionally set to time less than now.Add(rdb.LeaseDuration) to test lease extension is working.
|
||||
lease1 := h.NewLeaseWithClock(now.Add(10*time.Second), clock)
|
||||
lease2 := h.NewLeaseWithClock(now.Add(10*time.Second), clock)
|
||||
lease3 := h.NewLeaseWithClock(now.Add(10*time.Second), clock)
|
||||
|
21
inspector.go
21
inspector.go
@ -10,16 +10,19 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"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()
|
||||
}
|
||||
|
||||
@ -59,7 +74,7 @@ func (i *Inspector) Groups(queue string) ([]*GroupInfo, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GroupInfo represents a state of a group at a cerntain time.
|
||||
// GroupInfo represents a state of a group at a certain time.
|
||||
type GroupInfo struct {
|
||||
// Name of the group.
|
||||
Group string
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/google/uuid"
|
||||
@ -20,13 +19,10 @@ import (
|
||||
"github.com/hibiken/asynq/internal/rdb"
|
||||
h "github.com/hibiken/asynq/internal/testutil"
|
||||
"github.com/hibiken/asynq/internal/timeutil"
|
||||
"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) {
|
||||
@ -1138,7 +1148,7 @@ func TestInspectorListAggregatingTasks(t *testing.T) {
|
||||
tasks []*h.TaskSeedData
|
||||
allQueues []string
|
||||
allGroups map[string][]string
|
||||
groups map[string][]*redis.Z
|
||||
groups map[string][]redis.Z
|
||||
}{
|
||||
tasks: []*h.TaskSeedData{
|
||||
{Msg: m1, State: base.TaskStateAggregating},
|
||||
@ -1152,7 +1162,7 @@ func TestInspectorListAggregatingTasks(t *testing.T) {
|
||||
base.AllGroups("default"): {"group1", "group2"},
|
||||
base.AllGroups("custom"): {"group1"},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "group1"): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-30 * time.Second).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-20 * time.Second).Unix())},
|
||||
@ -3445,7 +3455,7 @@ func TestInspectorGroups(t *testing.T) {
|
||||
fixtures := struct {
|
||||
tasks []*h.TaskSeedData
|
||||
allGroups map[string][]string
|
||||
groups map[string][]*redis.Z
|
||||
groups map[string][]redis.Z
|
||||
}{
|
||||
tasks: []*h.TaskSeedData{
|
||||
{Msg: m1, State: base.TaskStateAggregating},
|
||||
@ -3458,7 +3468,7 @@ func TestInspectorGroups(t *testing.T) {
|
||||
base.AllGroups("default"): {"group1", "group2"},
|
||||
base.AllGroups("custom"): {"group1"},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "group1"): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-10 * time.Second).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-20 * time.Second).Unix())},
|
||||
|
@ -14,16 +14,16 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"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.23.0"
|
||||
const Version = "0.25.1"
|
||||
|
||||
// DefaultQueueName is the queue name used if none are specified by user.
|
||||
const DefaultQueueName = "default"
|
||||
@ -104,76 +104,76 @@ func ValidateQueueName(qname string) error {
|
||||
|
||||
// QueueKeyPrefix returns a prefix for all keys in the given queue.
|
||||
func QueueKeyPrefix(qname string) string {
|
||||
return fmt.Sprintf("asynq:{%s}:", qname)
|
||||
return "asynq:{" + qname + "}:"
|
||||
}
|
||||
|
||||
// TaskKeyPrefix returns a prefix for task key.
|
||||
func TaskKeyPrefix(qname string) string {
|
||||
return fmt.Sprintf("%st:", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "t:"
|
||||
}
|
||||
|
||||
// TaskKey returns a redis key for the given task message.
|
||||
func TaskKey(qname, id string) string {
|
||||
return fmt.Sprintf("%s%s", TaskKeyPrefix(qname), id)
|
||||
return TaskKeyPrefix(qname) + id
|
||||
}
|
||||
|
||||
// PendingKey returns a redis key for the given queue name.
|
||||
func PendingKey(qname string) string {
|
||||
return fmt.Sprintf("%spending", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "pending"
|
||||
}
|
||||
|
||||
// ActiveKey returns a redis key for the active tasks.
|
||||
func ActiveKey(qname string) string {
|
||||
return fmt.Sprintf("%sactive", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "active"
|
||||
}
|
||||
|
||||
// ScheduledKey returns a redis key for the scheduled tasks.
|
||||
func ScheduledKey(qname string) string {
|
||||
return fmt.Sprintf("%sscheduled", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "scheduled"
|
||||
}
|
||||
|
||||
// RetryKey returns a redis key for the retry tasks.
|
||||
func RetryKey(qname string) string {
|
||||
return fmt.Sprintf("%sretry", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "retry"
|
||||
}
|
||||
|
||||
// ArchivedKey returns a redis key for the archived tasks.
|
||||
func ArchivedKey(qname string) string {
|
||||
return fmt.Sprintf("%sarchived", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "archived"
|
||||
}
|
||||
|
||||
// LeaseKey returns a redis key for the lease.
|
||||
func LeaseKey(qname string) string {
|
||||
return fmt.Sprintf("%slease", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "lease"
|
||||
}
|
||||
|
||||
func CompletedKey(qname string) string {
|
||||
return fmt.Sprintf("%scompleted", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "completed"
|
||||
}
|
||||
|
||||
// PausedKey returns a redis key to indicate that the given queue is paused.
|
||||
func PausedKey(qname string) string {
|
||||
return fmt.Sprintf("%spaused", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "paused"
|
||||
}
|
||||
|
||||
// ProcessedTotalKey returns a redis key for total processed count for the given queue.
|
||||
func ProcessedTotalKey(qname string) string {
|
||||
return fmt.Sprintf("%sprocessed", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "processed"
|
||||
}
|
||||
|
||||
// FailedTotalKey returns a redis key for total failure count for the given queue.
|
||||
func FailedTotalKey(qname string) string {
|
||||
return fmt.Sprintf("%sfailed", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "failed"
|
||||
}
|
||||
|
||||
// ProcessedKey returns a redis key for processed count for the given day for the queue.
|
||||
func ProcessedKey(qname string, t time.Time) string {
|
||||
return fmt.Sprintf("%sprocessed:%s", QueueKeyPrefix(qname), t.UTC().Format("2006-01-02"))
|
||||
return QueueKeyPrefix(qname) + "processed:" + t.UTC().Format("2006-01-02")
|
||||
}
|
||||
|
||||
// FailedKey returns a redis key for failure count for the given day for the queue.
|
||||
func FailedKey(qname string, t time.Time) string {
|
||||
return fmt.Sprintf("%sfailed:%s", QueueKeyPrefix(qname), t.UTC().Format("2006-01-02"))
|
||||
return QueueKeyPrefix(qname) + "failed:" + t.UTC().Format("2006-01-02")
|
||||
}
|
||||
|
||||
// ServerInfoKey returns a redis key for process info.
|
||||
@ -188,47 +188,47 @@ func WorkersKey(hostname string, pid int, serverID string) string {
|
||||
|
||||
// SchedulerEntriesKey returns a redis key for the scheduler entries given scheduler ID.
|
||||
func SchedulerEntriesKey(schedulerID string) string {
|
||||
return fmt.Sprintf("asynq:schedulers:{%s}", schedulerID)
|
||||
return "asynq:schedulers:{" + schedulerID + "}"
|
||||
}
|
||||
|
||||
// SchedulerHistoryKey returns a redis key for the scheduler's history for the given entry.
|
||||
func SchedulerHistoryKey(entryID string) string {
|
||||
return fmt.Sprintf("asynq:scheduler_history:%s", entryID)
|
||||
return "asynq:scheduler_history:" + entryID
|
||||
}
|
||||
|
||||
// UniqueKey returns a redis key with the given type, payload, and queue name.
|
||||
func UniqueKey(qname, tasktype string, payload []byte) string {
|
||||
if payload == nil {
|
||||
return fmt.Sprintf("%sunique:%s:", QueueKeyPrefix(qname), tasktype)
|
||||
return QueueKeyPrefix(qname) + "unique:" + tasktype + ":"
|
||||
}
|
||||
checksum := md5.Sum(payload)
|
||||
return fmt.Sprintf("%sunique:%s:%s", QueueKeyPrefix(qname), tasktype, hex.EncodeToString(checksum[:]))
|
||||
return QueueKeyPrefix(qname) + "unique:" + tasktype + ":" + hex.EncodeToString(checksum[:])
|
||||
}
|
||||
|
||||
// GroupKeyPrefix returns a prefix for group key.
|
||||
func GroupKeyPrefix(qname string) string {
|
||||
return fmt.Sprintf("%sg:", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "g:"
|
||||
}
|
||||
|
||||
// GroupKey returns a redis key used to group tasks belong in the same group.
|
||||
func GroupKey(qname, gkey string) string {
|
||||
return fmt.Sprintf("%s%s", GroupKeyPrefix(qname), gkey)
|
||||
return GroupKeyPrefix(qname) + gkey
|
||||
}
|
||||
|
||||
// AggregationSetKey returns a redis key used for an aggregation set.
|
||||
func AggregationSetKey(qname, gname, setID string) string {
|
||||
return fmt.Sprintf("%s:%s", GroupKey(qname, gname), setID)
|
||||
return GroupKey(qname, gname) + ":" + setID
|
||||
}
|
||||
|
||||
// AllGroups return a redis key used to store all group keys used in a given queue.
|
||||
func AllGroups(qname string) string {
|
||||
return fmt.Sprintf("%sgroups", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "groups"
|
||||
}
|
||||
|
||||
// AllAggregationSets returns a redis key used to store all aggregation sets (set of tasks staged to be aggregated)
|
||||
// in a given queue.
|
||||
func AllAggregationSets(qname string) string {
|
||||
return fmt.Sprintf("%saggregation_sets", QueueKeyPrefix(qname))
|
||||
return QueueKeyPrefix(qname) + "aggregation_sets"
|
||||
}
|
||||
|
||||
// TaskMessage is the internal representation of a task with additional metadata fields.
|
||||
@ -375,14 +375,12 @@ func EncodeServerInfo(info *ServerInfo) ([]byte, error) {
|
||||
if info == nil {
|
||||
return nil, fmt.Errorf("cannot encode nil server info")
|
||||
}
|
||||
queues := make(map[string]int32)
|
||||
queues := make(map[string]int32, len(info.Queues))
|
||||
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),
|
||||
@ -402,14 +400,12 @@ func DecodeServerInfo(b []byte) (*ServerInfo, error) {
|
||||
if err := proto.Unmarshal(b, &pbmsg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queues := make(map[string]int)
|
||||
queues := make(map[string]int, len(pbmsg.GetQueues()))
|
||||
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,19 +568,16 @@ 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
|
||||
}
|
||||
|
||||
// Cancelations is a collection that holds cancel functions for all active tasks.
|
||||
//
|
||||
// Cancelations are safe for concurrent use by multipel goroutines.
|
||||
// Cancelations are safe for concurrent use by multiple goroutines.
|
||||
type Cancelations struct {
|
||||
mu sync.Mutex
|
||||
cancelFuncs map[string]context.CancelFunc
|
||||
@ -662,7 +632,7 @@ func NewLease(expirationTime time.Time) *Lease {
|
||||
}
|
||||
}
|
||||
|
||||
// Reset chanegs the lease to expire at the given time.
|
||||
// Reset changes the lease to expire at the given time.
|
||||
// It returns true if the lease is still valid and reset operation was successful, false if the lease had been expired.
|
||||
func (l *Lease) Reset(expirationTime time.Time) bool {
|
||||
if !l.IsValid() {
|
||||
@ -700,7 +670,7 @@ func (l *Lease) Deadline() time.Time {
|
||||
return l.expireAt
|
||||
}
|
||||
|
||||
// IsValid returns true if the lease's expieration time is in the future or equals to the current time,
|
||||
// IsValid returns true if the lease's expiration time is in the future or equals to the current time,
|
||||
// returns false otherwise.
|
||||
func (l *Lease) IsValid() bool {
|
||||
now := l.Clock.Now()
|
||||
@ -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)
|
||||
|
@ -65,7 +65,7 @@ func GetRetryCount(ctx context.Context) (n int, ok bool) {
|
||||
|
||||
// GetMaxRetry extracts maximum retry from a context, if any.
|
||||
//
|
||||
// Return value n indicates the maximum number of times the assoicated task
|
||||
// Return value n indicates the maximum number of times the associated task
|
||||
// can be retried if ProcessTask returns a non-nil error.
|
||||
func GetMaxRetry(ctx context.Context) (n int, ok bool) {
|
||||
metadata, ok := ctx.Value(metadataCtxKey).(taskMetadata)
|
||||
|
@ -161,7 +161,7 @@ func CanonicalCode(err error) Code {
|
||||
}
|
||||
|
||||
/******************************************
|
||||
Domin Specific Error Types & Values
|
||||
Domain Specific Error Types & Values
|
||||
*******************************************/
|
||||
|
||||
var (
|
||||
@ -256,6 +256,21 @@ func IsRedisCommandError(err error) bool {
|
||||
return As(err, &target)
|
||||
}
|
||||
|
||||
// PanicError defines an error when occurred a panic error.
|
||||
type PanicError struct {
|
||||
ErrMsg string
|
||||
}
|
||||
|
||||
func (e *PanicError) Error() string {
|
||||
return fmt.Sprintf("panic error cause by: %s", e.ErrMsg)
|
||||
}
|
||||
|
||||
// IsPanicError reports whether any error in err's chain is of type PanicError.
|
||||
func IsPanicError(err error) bool {
|
||||
var target *PanicError
|
||||
return As(err, &target)
|
||||
}
|
||||
|
||||
/*************************************************
|
||||
Standard Library errors package functions
|
||||
*************************************************/
|
||||
@ -263,26 +278,26 @@ func IsRedisCommandError(err error) bool {
|
||||
// New returns an error that formats as the given text.
|
||||
// Each call to New returns a distinct error value even if the text is identical.
|
||||
//
|
||||
// This function is the errors.New function from the standard libarary (https://golang.org/pkg/errors/#New).
|
||||
// It is exported from this package for import convinience.
|
||||
// This function is the errors.New function from the standard library (https://golang.org/pkg/errors/#New).
|
||||
// It is exported from this package for import convenience.
|
||||
func New(text string) error { return errors.New(text) }
|
||||
|
||||
// Is reports whether any error in err's chain matches target.
|
||||
//
|
||||
// This function is the errors.Is function from the standard libarary (https://golang.org/pkg/errors/#Is).
|
||||
// It is exported from this package for import convinience.
|
||||
// This function is the errors.Is function from the standard library (https://golang.org/pkg/errors/#Is).
|
||||
// It is exported from this package for import convenience.
|
||||
func Is(err, target error) bool { return errors.Is(err, target) }
|
||||
|
||||
// As finds the first error in err's chain that matches target, and if so, sets target to that error value and returns true.
|
||||
// Otherwise, it returns false.
|
||||
//
|
||||
// This function is the errors.As function from the standard libarary (https://golang.org/pkg/errors/#As).
|
||||
// It is exported from this package for import convinience.
|
||||
// This function is the errors.As function from the standard library (https://golang.org/pkg/errors/#As).
|
||||
// It is exported from this package for import convenience.
|
||||
func As(err error, target interface{}) bool { return errors.As(err, target) }
|
||||
|
||||
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains an Unwrap method returning error.
|
||||
// Otherwise, Unwrap returns nil.
|
||||
//
|
||||
// This function is the errors.Unwrap function from the standard libarary (https://golang.org/pkg/errors/#Unwrap).
|
||||
// It is exported from this package for import convinience.
|
||||
// This function is the errors.Unwrap function from the standard library (https://golang.org/pkg/errors/#Unwrap).
|
||||
// It is exported from this package for import convenience.
|
||||
func Unwrap(err error) error { return errors.Unwrap(err) }
|
||||
|
@ -131,6 +131,12 @@ func TestErrorPredicates(t *testing.T) {
|
||||
err: E(Op("rdb.ArchiveTask"), NotFound, &QueueNotFoundError{Queue: "default"}),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
desc: "IsPanicError should detect presence of PanicError in err's chain",
|
||||
fn: IsPanicError,
|
||||
err: E(Op("unknown"), Unknown, &PanicError{ErrMsg: "Something went wrong"}),
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
@ -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
|
||||
|
@ -10,9 +10,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
"github.com/hibiken/asynq/internal/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
@ -109,7 +109,7 @@ table.insert(res, KEYS[6])
|
||||
table.insert(res, redis.call("ZCARD", KEYS[6]))
|
||||
for i=7,10 do
|
||||
local count = 0
|
||||
local n = redis.call("GET", KEYS[i])
|
||||
local n = redis.call("GET", KEYS[i])
|
||||
if n then
|
||||
count = tonumber(n)
|
||||
end
|
||||
@ -343,7 +343,7 @@ func (r *RDB) memoryUsage(qname string) (int64, error) {
|
||||
}
|
||||
usg, err := cast.ToInt64E(res)
|
||||
if err != nil {
|
||||
return 0, errors.E(op, errors.Internal, fmt.Sprintf("could not cast script return value to int64"))
|
||||
return 0, errors.E(op, errors.Internal, "could not cast script return value to int64")
|
||||
}
|
||||
return usg, nil
|
||||
}
|
||||
@ -568,7 +568,7 @@ var groupStatsCmd = redis.NewScript(`
|
||||
local res = {}
|
||||
local group_names = redis.call("SMEMBERS", KEYS[1])
|
||||
for _, gname in ipairs(group_names) do
|
||||
local size = redis.call("ZCARD", ARGV[1] .. gname)
|
||||
local size = redis.call("ZCARD", ARGV[1] .. gname)
|
||||
table.insert(res, gname)
|
||||
table.insert(res, size)
|
||||
end
|
||||
@ -1295,7 +1295,7 @@ elseif state == "aggregating" then
|
||||
if redis.call("ZCARD", ARGV[6] .. group) == 0 then
|
||||
redis.call("SREM", KEYS[3], group)
|
||||
end
|
||||
else
|
||||
else
|
||||
if redis.call("ZREM", ARGV[5] .. state, ARGV[1]) == 0 then
|
||||
return redis.error_reply("task id not found in zset " .. tostring(ARGV[5] .. state))
|
||||
end
|
||||
@ -1349,7 +1349,7 @@ func (r *RDB) ArchiveTask(qname, id string) error {
|
||||
case -1:
|
||||
return errors.E(op, errors.FailedPrecondition, &errors.TaskAlreadyArchivedError{Queue: qname, ID: id})
|
||||
case -2:
|
||||
return errors.E(op, errors.FailedPrecondition, "cannot archive task in active state. use CancelTask instead.")
|
||||
return errors.E(op, errors.FailedPrecondition, "cannot archive task in active state. use CancelProcessing instead.")
|
||||
case -3:
|
||||
return errors.E(op, errors.NotFound, &errors.QueueNotFoundError{Queue: qname})
|
||||
default:
|
||||
@ -1490,7 +1490,7 @@ func (r *RDB) DeleteTask(qname, id string) error {
|
||||
case 0:
|
||||
return errors.E(op, errors.NotFound, &errors.TaskNotFoundError{Queue: qname, ID: id})
|
||||
case -1:
|
||||
return errors.E(op, errors.FailedPrecondition, "cannot delete task in active state. use CancelTask instead.")
|
||||
return errors.E(op, errors.FailedPrecondition, "cannot delete task in active state. use CancelProcessing instead.")
|
||||
default:
|
||||
return errors.E(op, errors.Internal, fmt.Sprintf("unexpected return value from deleteTaskCmd script: %d", n))
|
||||
}
|
||||
@ -1832,6 +1832,7 @@ func (r *RDB) RemoveQueue(qname string, force bool) error {
|
||||
if err := r.client.SRem(context.Background(), base.AllQueues, qname).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, err)
|
||||
}
|
||||
r.queuesPublished.Delete(qname)
|
||||
return nil
|
||||
case -1:
|
||||
return errors.E(op, errors.NotFound, &errors.QueueNotEmptyError{Queue: qname})
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/google/uuid"
|
||||
@ -20,6 +19,7 @@ import (
|
||||
"github.com/hibiken/asynq/internal/errors"
|
||||
h "github.com/hibiken/asynq/internal/testutil"
|
||||
"github.com/hibiken/asynq/internal/timeutil"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func TestAllQueues(t *testing.T) {
|
||||
@ -73,11 +73,11 @@ func TestCurrentStats(t *testing.T) {
|
||||
allGroups map[string][]string
|
||||
pending map[string][]string
|
||||
active map[string][]string
|
||||
scheduled map[string][]*redis.Z
|
||||
retry map[string][]*redis.Z
|
||||
archived map[string][]*redis.Z
|
||||
completed map[string][]*redis.Z
|
||||
groups map[string][]*redis.Z
|
||||
scheduled map[string][]redis.Z
|
||||
retry map[string][]redis.Z
|
||||
archived map[string][]redis.Z
|
||||
completed map[string][]redis.Z
|
||||
groups map[string][]redis.Z
|
||||
processed map[string]int
|
||||
failed map[string]int
|
||||
processedTotal map[string]int
|
||||
@ -111,7 +111,7 @@ func TestCurrentStats(t *testing.T) {
|
||||
base.ActiveKey("critical"): {},
|
||||
base.ActiveKey("low"): {},
|
||||
},
|
||||
scheduled: map[string][]*redis.Z{
|
||||
scheduled: map[string][]redis.Z{
|
||||
base.ScheduledKey("default"): {
|
||||
{Member: m3.ID, Score: float64(now.Add(time.Hour).Unix())},
|
||||
{Member: m4.ID, Score: float64(now.Unix())},
|
||||
@ -119,22 +119,22 @@ func TestCurrentStats(t *testing.T) {
|
||||
base.ScheduledKey("critical"): {},
|
||||
base.ScheduledKey("low"): {},
|
||||
},
|
||||
retry: map[string][]*redis.Z{
|
||||
retry: map[string][]redis.Z{
|
||||
base.RetryKey("default"): {},
|
||||
base.RetryKey("critical"): {},
|
||||
base.RetryKey("low"): {},
|
||||
},
|
||||
archived: map[string][]*redis.Z{
|
||||
archived: map[string][]redis.Z{
|
||||
base.ArchivedKey("default"): {},
|
||||
base.ArchivedKey("critical"): {},
|
||||
base.ArchivedKey("low"): {},
|
||||
},
|
||||
completed: map[string][]*redis.Z{
|
||||
completed: map[string][]redis.Z{
|
||||
base.CompletedKey("default"): {},
|
||||
base.CompletedKey("critical"): {},
|
||||
base.CompletedKey("low"): {},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "sms:user1"): {
|
||||
{Member: m7.ID, Score: float64(now.Add(-3 * time.Second).Unix())},
|
||||
},
|
||||
@ -205,7 +205,7 @@ func TestCurrentStats(t *testing.T) {
|
||||
base.ActiveKey("critical"): {},
|
||||
base.ActiveKey("low"): {},
|
||||
},
|
||||
scheduled: map[string][]*redis.Z{
|
||||
scheduled: map[string][]redis.Z{
|
||||
base.ScheduledKey("default"): {
|
||||
{Member: m3.ID, Score: float64(now.Add(time.Hour).Unix())},
|
||||
{Member: m4.ID, Score: float64(now.Unix())},
|
||||
@ -213,17 +213,17 @@ func TestCurrentStats(t *testing.T) {
|
||||
base.ScheduledKey("critical"): {},
|
||||
base.ScheduledKey("low"): {},
|
||||
},
|
||||
retry: map[string][]*redis.Z{
|
||||
retry: map[string][]redis.Z{
|
||||
base.RetryKey("default"): {},
|
||||
base.RetryKey("critical"): {},
|
||||
base.RetryKey("low"): {},
|
||||
},
|
||||
archived: map[string][]*redis.Z{
|
||||
archived: map[string][]redis.Z{
|
||||
base.ArchivedKey("default"): {},
|
||||
base.ArchivedKey("critical"): {},
|
||||
base.ArchivedKey("low"): {},
|
||||
},
|
||||
completed: map[string][]*redis.Z{
|
||||
completed: map[string][]redis.Z{
|
||||
base.CompletedKey("default"): {},
|
||||
base.CompletedKey("critical"): {},
|
||||
base.CompletedKey("low"): {},
|
||||
@ -250,7 +250,7 @@ func TestCurrentStats(t *testing.T) {
|
||||
},
|
||||
oldestPendingMessageEnqueueTime: map[string]time.Time{
|
||||
"default": now.Add(-15 * time.Second),
|
||||
"critical": time.Time{}, // zero value since there's no pending task in this queue
|
||||
"critical": {}, // zero value since there's no pending task in this queue
|
||||
"low": now.Add(-30 * time.Second),
|
||||
},
|
||||
paused: []string{"critical", "low"},
|
||||
@ -392,7 +392,6 @@ func TestHistoricalStats(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRedisInfo(t *testing.T) {
|
||||
@ -436,7 +435,7 @@ func TestGroupStats(t *testing.T) {
|
||||
fixtures := struct {
|
||||
tasks []*h.TaskSeedData
|
||||
allGroups map[string][]string
|
||||
groups map[string][]*redis.Z
|
||||
groups map[string][]redis.Z
|
||||
}{
|
||||
tasks: []*h.TaskSeedData{
|
||||
{Msg: m1, State: base.TaskStateAggregating},
|
||||
@ -449,7 +448,7 @@ func TestGroupStats(t *testing.T) {
|
||||
base.AllGroups("default"): {"group1", "group2"},
|
||||
base.AllGroups("custom"): {"group1"},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "group1"): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-10 * time.Second).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-20 * time.Second).Unix())},
|
||||
@ -487,7 +486,7 @@ func TestGroupStats(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
var sortGroupStatsOpt = cmp.Transformer(
|
||||
sortGroupStatsOpt := cmp.Transformer(
|
||||
"SortGroupStats",
|
||||
func(in []*GroupStat) []*GroupStat {
|
||||
out := append([]*GroupStat(nil), in...)
|
||||
@ -1509,7 +1508,6 @@ func TestListCompleted(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestListCompletedPagination(t *testing.T) {
|
||||
@ -1585,7 +1583,7 @@ func TestListAggregating(t *testing.T) {
|
||||
tasks []*h.TaskSeedData
|
||||
allQueues []string
|
||||
allGroups map[string][]string
|
||||
groups map[string][]*redis.Z
|
||||
groups map[string][]redis.Z
|
||||
}{
|
||||
tasks: []*h.TaskSeedData{
|
||||
{Msg: m1, State: base.TaskStateAggregating},
|
||||
@ -1598,7 +1596,7 @@ func TestListAggregating(t *testing.T) {
|
||||
base.AllGroups("default"): {"group1", "group2"},
|
||||
base.AllGroups("custom"): {"group3"},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "group1"): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-30 * time.Second).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-20 * time.Second).Unix())},
|
||||
@ -1665,14 +1663,14 @@ func TestListAggregatingPagination(t *testing.T) {
|
||||
tasks []*h.TaskSeedData
|
||||
allQueues []string
|
||||
allGroups map[string][]string
|
||||
groups map[string][]*redis.Z
|
||||
groups map[string][]redis.Z
|
||||
}{
|
||||
tasks: []*h.TaskSeedData{}, // will be populated below
|
||||
allQueues: []string{"default"},
|
||||
allGroups: map[string][]string{
|
||||
base.AllGroups("default"): {"mygroup"},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
groupkey: {}, // will be populated below
|
||||
},
|
||||
}
|
||||
@ -1683,7 +1681,7 @@ func TestListAggregatingPagination(t *testing.T) {
|
||||
fxt.tasks = append(fxt.tasks, &h.TaskSeedData{
|
||||
Msg: msg, State: base.TaskStateAggregating,
|
||||
})
|
||||
fxt.groups[groupkey] = append(fxt.groups[groupkey], &redis.Z{
|
||||
fxt.groups[groupkey] = append(fxt.groups[groupkey], redis.Z{
|
||||
Member: msg.ID,
|
||||
Score: float64(now.Add(-time.Duration(100-i) * time.Second).Unix()),
|
||||
})
|
||||
@ -1999,7 +1997,7 @@ func TestRunAggregatingTask(t *testing.T) {
|
||||
tasks []*h.TaskSeedData
|
||||
allQueues []string
|
||||
allGroups map[string][]string
|
||||
groups map[string][]*redis.Z
|
||||
groups map[string][]redis.Z
|
||||
}{
|
||||
tasks: []*h.TaskSeedData{
|
||||
{Msg: m1, State: base.TaskStateAggregating},
|
||||
@ -2011,7 +2009,7 @@ func TestRunAggregatingTask(t *testing.T) {
|
||||
base.AllGroups("default"): {"group1"},
|
||||
base.AllGroups("custom"): {"group1"},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "group1"): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-20 * time.Second).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-25 * time.Second).Unix())},
|
||||
@ -2324,7 +2322,6 @@ func TestRunTaskError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRunAllScheduledTasks(t *testing.T) {
|
||||
@ -2691,7 +2688,7 @@ func TestRunAllAggregatingTasks(t *testing.T) {
|
||||
tasks []*h.TaskSeedData
|
||||
allQueues []string
|
||||
allGroups map[string][]string
|
||||
groups map[string][]*redis.Z
|
||||
groups map[string][]redis.Z
|
||||
}{
|
||||
tasks: []*h.TaskSeedData{
|
||||
{Msg: m1, State: base.TaskStateAggregating},
|
||||
@ -2703,7 +2700,7 @@ func TestRunAllAggregatingTasks(t *testing.T) {
|
||||
base.AllGroups("default"): {"group1"},
|
||||
base.AllGroups("custom"): {"group2"},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "group1"): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-20 * time.Second).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-25 * time.Second).Unix())},
|
||||
@ -3001,7 +2998,7 @@ func TestArchiveAggregatingTask(t *testing.T) {
|
||||
tasks []*h.TaskSeedData
|
||||
allQueues []string
|
||||
allGroups map[string][]string
|
||||
groups map[string][]*redis.Z
|
||||
groups map[string][]redis.Z
|
||||
}{
|
||||
tasks: []*h.TaskSeedData{
|
||||
{Msg: m1, State: base.TaskStateAggregating},
|
||||
@ -3013,7 +3010,7 @@ func TestArchiveAggregatingTask(t *testing.T) {
|
||||
base.AllGroups("default"): {"group1"},
|
||||
base.AllGroups("custom"): {"group1"},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "group1"): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-20 * time.Second).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-25 * time.Second).Unix())},
|
||||
@ -3335,6 +3332,7 @@ func TestArchiveTaskError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveAllPendingTasks(t *testing.T) {
|
||||
r := setup(t)
|
||||
defer r.Close()
|
||||
@ -3485,7 +3483,7 @@ func TestArchiveAllAggregatingTasks(t *testing.T) {
|
||||
tasks []*h.TaskSeedData
|
||||
allQueues []string
|
||||
allGroups map[string][]string
|
||||
groups map[string][]*redis.Z
|
||||
groups map[string][]redis.Z
|
||||
}{
|
||||
tasks: []*h.TaskSeedData{
|
||||
{Msg: m1, State: base.TaskStateAggregating},
|
||||
@ -3497,7 +3495,7 @@ func TestArchiveAllAggregatingTasks(t *testing.T) {
|
||||
base.AllGroups("default"): {"group1"},
|
||||
base.AllGroups("custom"): {"group2"},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "group1"): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-20 * time.Second).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-25 * time.Second).Unix())},
|
||||
@ -4124,7 +4122,7 @@ func TestDeleteAggregatingTask(t *testing.T) {
|
||||
tasks []*h.TaskSeedData
|
||||
allQueues []string
|
||||
allGroups map[string][]string
|
||||
groups map[string][]*redis.Z
|
||||
groups map[string][]redis.Z
|
||||
}{
|
||||
tasks: []*h.TaskSeedData{
|
||||
{Msg: m1, State: base.TaskStateAggregating},
|
||||
@ -4136,7 +4134,7 @@ func TestDeleteAggregatingTask(t *testing.T) {
|
||||
base.AllGroups("default"): {"group1"},
|
||||
base.AllGroups("custom"): {"group1"},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "group1"): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-20 * time.Second).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-25 * time.Second).Unix())},
|
||||
@ -4758,7 +4756,7 @@ func TestDeleteAllAggregatingTasks(t *testing.T) {
|
||||
tasks []*h.TaskSeedData
|
||||
allQueues []string
|
||||
allGroups map[string][]string
|
||||
groups map[string][]*redis.Z
|
||||
groups map[string][]redis.Z
|
||||
}{
|
||||
tasks: []*h.TaskSeedData{
|
||||
{Msg: m1, State: base.TaskStateAggregating},
|
||||
@ -4770,7 +4768,7 @@ func TestDeleteAllAggregatingTasks(t *testing.T) {
|
||||
base.AllGroups("default"): {"group1"},
|
||||
base.AllGroups("custom"): {"group1"},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "group1"): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-20 * time.Second).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-25 * time.Second).Unix())},
|
||||
|
@ -9,13 +9,14 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
"github.com/hibiken/asynq/internal/errors"
|
||||
"github.com/hibiken/asynq/internal/timeutil"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
@ -26,8 +27,9 @@ const LeaseDuration = 30 * time.Second
|
||||
|
||||
// RDB is a client interface to query and mutate task queues.
|
||||
type RDB struct {
|
||||
client redis.UniversalClient
|
||||
clock timeutil.Clock
|
||||
client redis.UniversalClient
|
||||
clock timeutil.Clock
|
||||
queuesPublished sync.Map
|
||||
}
|
||||
|
||||
// NewRDB returns a new instance of RDB.
|
||||
@ -67,7 +69,7 @@ func (r *RDB) runScript(ctx context.Context, op errors.Op, script *redis.Script,
|
||||
return nil
|
||||
}
|
||||
|
||||
// Runs the given script with keys and args and retuns the script's return value as int64.
|
||||
// Runs the given script with keys and args and returns the script's return value as int64.
|
||||
func (r *RDB) runScriptWithErrorCode(ctx context.Context, op errors.Op, script *redis.Script, keys []string, args ...interface{}) (int64, error) {
|
||||
res, err := script.Run(ctx, r.client, keys, args...).Result()
|
||||
if err != nil {
|
||||
@ -112,8 +114,11 @@ func (r *RDB) Enqueue(ctx context.Context, msg *base.TaskMessage) error {
|
||||
if err != nil {
|
||||
return errors.E(op, errors.Unknown, fmt.Sprintf("cannot encode message: %v", err))
|
||||
}
|
||||
if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
if _, found := r.queuesPublished.Load(msg.Queue); !found {
|
||||
if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
}
|
||||
r.queuesPublished.Store(msg.Queue, true)
|
||||
}
|
||||
keys := []string{
|
||||
base.TaskKey(msg.Queue, msg.ID),
|
||||
@ -152,7 +157,7 @@ func (r *RDB) Enqueue(ctx context.Context, msg *base.TaskMessage) error {
|
||||
var enqueueUniqueCmd = redis.NewScript(`
|
||||
local ok = redis.call("SET", KEYS[1], ARGV[1], "NX", "EX", ARGV[2])
|
||||
if not ok then
|
||||
return -1
|
||||
return -1
|
||||
end
|
||||
if redis.call("EXISTS", KEYS[2]) == 1 then
|
||||
return 0
|
||||
@ -174,8 +179,11 @@ func (r *RDB) EnqueueUnique(ctx context.Context, msg *base.TaskMessage, ttl time
|
||||
if err != nil {
|
||||
return errors.E(op, errors.Internal, "cannot encode task message: %v", err)
|
||||
}
|
||||
if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
if _, found := r.queuesPublished.Load(msg.Queue); !found {
|
||||
if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
}
|
||||
r.queuesPublished.Store(msg.Queue, true)
|
||||
}
|
||||
keys := []string{
|
||||
msg.UniqueKey,
|
||||
@ -368,7 +376,7 @@ func (r *RDB) Done(ctx context.Context, msg *base.TaskMessage) error {
|
||||
//
|
||||
// ARGV[1] -> task ID
|
||||
// ARGV[2] -> stats expiration timestamp
|
||||
// ARGV[3] -> task exipration time in unix time
|
||||
// ARGV[3] -> task expiration time in unix time
|
||||
// ARGV[4] -> task message data
|
||||
// ARGV[5] -> max int64 value
|
||||
var markAsCompleteCmd = redis.NewScript(`
|
||||
@ -379,7 +387,7 @@ if redis.call("ZREM", KEYS[2], ARGV[1]) == 0 then
|
||||
return redis.error_reply("NOT FOUND")
|
||||
end
|
||||
if redis.call("ZADD", KEYS[3], ARGV[3], ARGV[1]) ~= 1 then
|
||||
redis.redis.error_reply("INTERNAL")
|
||||
return redis.error_reply("INTERNAL")
|
||||
end
|
||||
redis.call("HSET", KEYS[4], "msg", ARGV[4], "state", "completed")
|
||||
local n = redis.call("INCR", KEYS[5])
|
||||
@ -405,7 +413,7 @@ return redis.status_reply("OK")
|
||||
//
|
||||
// ARGV[1] -> task ID
|
||||
// ARGV[2] -> stats expiration timestamp
|
||||
// ARGV[3] -> task exipration time in unix time
|
||||
// ARGV[3] -> task expiration time in unix time
|
||||
// ARGV[4] -> task message data
|
||||
// ARGV[5] -> max int64 value
|
||||
var markAsCompleteUniqueCmd = redis.NewScript(`
|
||||
@ -416,7 +424,7 @@ if redis.call("ZREM", KEYS[2], ARGV[1]) == 0 then
|
||||
return redis.error_reply("NOT FOUND")
|
||||
end
|
||||
if redis.call("ZADD", KEYS[3], ARGV[3], ARGV[1]) ~= 1 then
|
||||
redis.redis.error_reply("INTERNAL")
|
||||
return redis.error_reply("INTERNAL")
|
||||
end
|
||||
redis.call("HSET", KEYS[4], "msg", ARGV[4], "state", "completed")
|
||||
local n = redis.call("INCR", KEYS[5])
|
||||
@ -529,8 +537,11 @@ func (r *RDB) AddToGroup(ctx context.Context, msg *base.TaskMessage, groupKey st
|
||||
if err != nil {
|
||||
return errors.E(op, errors.Unknown, fmt.Sprintf("cannot encode message: %v", err))
|
||||
}
|
||||
if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
if _, found := r.queuesPublished.Load(msg.Queue); !found {
|
||||
if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
}
|
||||
r.queuesPublished.Store(msg.Queue, true)
|
||||
}
|
||||
keys := []string{
|
||||
base.TaskKey(msg.Queue, msg.ID),
|
||||
@ -591,8 +602,11 @@ func (r *RDB) AddToGroupUnique(ctx context.Context, msg *base.TaskMessage, group
|
||||
if err != nil {
|
||||
return errors.E(op, errors.Unknown, fmt.Sprintf("cannot encode message: %v", err))
|
||||
}
|
||||
if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
if _, found := r.queuesPublished.Load(msg.Queue); !found {
|
||||
if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
}
|
||||
r.queuesPublished.Store(msg.Queue, true)
|
||||
}
|
||||
keys := []string{
|
||||
base.TaskKey(msg.Queue, msg.ID),
|
||||
@ -648,8 +662,11 @@ func (r *RDB) Schedule(ctx context.Context, msg *base.TaskMessage, processAt tim
|
||||
if err != nil {
|
||||
return errors.E(op, errors.Unknown, fmt.Sprintf("cannot encode message: %v", err))
|
||||
}
|
||||
if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
if _, found := r.queuesPublished.Load(msg.Queue); !found {
|
||||
if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
}
|
||||
r.queuesPublished.Store(msg.Queue, true)
|
||||
}
|
||||
keys := []string{
|
||||
base.TaskKey(msg.Queue, msg.ID),
|
||||
@ -707,8 +724,11 @@ func (r *RDB) ScheduleUnique(ctx context.Context, msg *base.TaskMessage, process
|
||||
if err != nil {
|
||||
return errors.E(op, errors.Internal, fmt.Sprintf("cannot encode task message: %v", err))
|
||||
}
|
||||
if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
if _, found := r.queuesPublished.Load(msg.Queue); !found {
|
||||
if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
}
|
||||
r.queuesPublished.Store(msg.Queue, true)
|
||||
}
|
||||
keys := []string{
|
||||
msg.UniqueKey,
|
||||
@ -829,6 +849,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 +866,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 +924,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,
|
||||
@ -1086,7 +1122,7 @@ const aggregationTimeout = 2 * time.Minute
|
||||
// The time for gracePeriod and maxDelay is computed relative to the time t.
|
||||
//
|
||||
// Note: It assumes that this function is called at frequency less than or equal to the gracePeriod. In other words,
|
||||
// the function only checks the most recently added task aganist the given gracePeriod.
|
||||
// the function only checks the most recently added task against the given gracePeriod.
|
||||
func (r *RDB) AggregationCheck(qname, gname string, t time.Time, gracePeriod, maxDelay time.Duration, maxSize int) (string, error) {
|
||||
var op errors.Op = "RDB.AggregationCheck"
|
||||
aggregationSetID := uuid.NewString()
|
||||
@ -1217,7 +1253,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 +1277,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 {
|
||||
@ -1284,7 +1318,10 @@ local res = {}
|
||||
local ids = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", ARGV[1])
|
||||
for _, id in ipairs(ids) do
|
||||
local key = ARGV[2] .. id
|
||||
table.insert(res, redis.call("HGET", key, "msg"))
|
||||
local v = redis.call("HGET", key, "msg")
|
||||
if v then
|
||||
table.insert(res, v)
|
||||
end
|
||||
end
|
||||
return res
|
||||
`)
|
||||
@ -1319,9 +1356,9 @@ func (r *RDB) ListLeaseExpired(cutoff time.Time, qnames ...string) ([]*base.Task
|
||||
// It returns a new expiration time if the operation was successful.
|
||||
func (r *RDB) ExtendLease(qname string, ids ...string) (expirationTime time.Time, err error) {
|
||||
expireAt := r.clock.Now().Add(LeaseDuration)
|
||||
var zs []*redis.Z
|
||||
var zs []redis.Z
|
||||
for _, id := range ids {
|
||||
zs = append(zs, &redis.Z{Member: id, Score: float64(expireAt.Unix())})
|
||||
zs = append(zs, redis.Z{Member: id, Score: float64(expireAt.Unix())})
|
||||
}
|
||||
// Use XX option to only update elements that already exist; Don't add new elements
|
||||
// TODO: Consider adding GT option to ensure we only "extend" the lease. Ceveat is that GT is supported from redis v6.2.0 or above.
|
||||
@ -1367,10 +1404,10 @@ func (r *RDB) WriteServerState(info *base.ServerInfo, workers []*base.WorkerInfo
|
||||
}
|
||||
skey := base.ServerInfoKey(info.Host, info.PID, info.ServerID)
|
||||
wkey := base.WorkersKey(info.Host, info.PID, info.ServerID)
|
||||
if err := r.client.ZAdd(ctx, base.AllServers, &redis.Z{Score: float64(exp.Unix()), Member: skey}).Err(); err != nil {
|
||||
if err := r.client.ZAdd(ctx, base.AllServers, redis.Z{Score: float64(exp.Unix()), Member: skey}).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
|
||||
}
|
||||
if err := r.client.ZAdd(ctx, base.AllWorkers, &redis.Z{Score: float64(exp.Unix()), Member: wkey}).Err(); err != nil {
|
||||
if err := r.client.ZAdd(ctx, base.AllWorkers, redis.Z{Score: float64(exp.Unix()), Member: wkey}).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "zadd", Err: err})
|
||||
}
|
||||
return r.runScript(ctx, op, writeServerStateCmd, []string{skey, wkey}, args...)
|
||||
@ -1400,7 +1437,7 @@ func (r *RDB) ClearServerState(host string, pid int, serverID string) error {
|
||||
|
||||
// KEYS[1] -> asynq:schedulers:{<schedulerID>}
|
||||
// ARGV[1] -> TTL in seconds
|
||||
// ARGV[2:] -> schedler entries
|
||||
// ARGV[2:] -> scheduler entries
|
||||
var writeSchedulerEntriesCmd = redis.NewScript(`
|
||||
redis.call("DEL", KEYS[1])
|
||||
for i = 2, #ARGV do
|
||||
@ -1423,7 +1460,7 @@ func (r *RDB) WriteSchedulerEntries(schedulerID string, entries []*base.Schedule
|
||||
}
|
||||
exp := r.clock.Now().Add(ttl).UTC()
|
||||
key := base.SchedulerEntriesKey(schedulerID)
|
||||
err := r.client.ZAdd(ctx, base.AllSchedulers, &redis.Z{Score: float64(exp.Unix()), Member: key}).Err()
|
||||
err := r.client.ZAdd(ctx, base.AllSchedulers, redis.Z{Score: float64(exp.Unix()), Member: key}).Err()
|
||||
if err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "zadd", Err: err})
|
||||
}
|
||||
@ -1431,10 +1468,10 @@ func (r *RDB) WriteSchedulerEntries(schedulerID string, entries []*base.Schedule
|
||||
}
|
||||
|
||||
// ClearSchedulerEntries deletes scheduler entries data from redis.
|
||||
func (r *RDB) ClearSchedulerEntries(scheduelrID string) error {
|
||||
func (r *RDB) ClearSchedulerEntries(schedulerID string) error {
|
||||
var op errors.Op = "rdb.ClearSchedulerEntries"
|
||||
ctx := context.Background()
|
||||
key := base.SchedulerEntriesKey(scheduelrID)
|
||||
key := base.SchedulerEntriesKey(schedulerID)
|
||||
if err := r.client.ZRem(ctx, base.AllSchedulers, key).Err(); err != nil {
|
||||
return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "zrem", Err: err})
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/google/uuid"
|
||||
@ -23,6 +22,7 @@ import (
|
||||
"github.com/hibiken/asynq/internal/errors"
|
||||
h "github.com/hibiken/asynq/internal/testutil"
|
||||
"github.com/hibiken/asynq/internal/timeutil"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// variables used for package testing.
|
||||
@ -160,6 +160,59 @@ func TestEnqueueTaskIdConflictError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnqueueQueueCache(t *testing.T) {
|
||||
r := setup(t)
|
||||
defer r.Close()
|
||||
t1 := h.NewTaskMessageWithQueue("sync1", nil, "low")
|
||||
|
||||
enqueueTime := time.Now()
|
||||
clock := timeutil.NewSimulatedClock(enqueueTime)
|
||||
r.SetClock(clock)
|
||||
|
||||
err := r.Enqueue(context.Background(), t1)
|
||||
if err != nil {
|
||||
t.Fatalf("(*RDB).Enqueue(msg) = %v, want nil", err)
|
||||
}
|
||||
|
||||
// Check queue is in the AllQueues set.
|
||||
if !r.client.SIsMember(context.Background(), base.AllQueues, t1.Queue).Val() {
|
||||
t.Fatalf("%q is not a member of SET %q", t1.Queue, base.AllQueues)
|
||||
}
|
||||
|
||||
if _, ok := r.queuesPublished.Load(t1.Queue); !ok {
|
||||
t.Fatalf("%q is not cached in queuesPublished", t1.Queue)
|
||||
}
|
||||
|
||||
t.Run("remove-queue", func(t *testing.T) {
|
||||
err := r.RemoveQueue(t1.Queue, true)
|
||||
if err != nil {
|
||||
t.Errorf("(*RDB).RemoveQueue(%q, %t) = %v, want nil", t1.Queue, true, err)
|
||||
}
|
||||
|
||||
if _, ok := r.queuesPublished.Load(t1.Queue); ok {
|
||||
t.Fatalf("%q is still cached in queuesPublished", t1.Queue)
|
||||
}
|
||||
|
||||
if r.client.SIsMember(context.Background(), base.AllQueues, t1.Queue).Val() {
|
||||
t.Fatalf("%q is a member of SET %q", t1.Queue, base.AllQueues)
|
||||
}
|
||||
|
||||
err = r.Enqueue(context.Background(), t1)
|
||||
if err != nil {
|
||||
t.Fatalf("(*RDB).Enqueue(msg) = %v, want nil", err)
|
||||
}
|
||||
|
||||
// Check queue is in the AllQueues set.
|
||||
if !r.client.SIsMember(context.Background(), base.AllQueues, t1.Queue).Val() {
|
||||
t.Fatalf("%q is not a member of SET %q", t1.Queue, base.AllQueues)
|
||||
}
|
||||
|
||||
if _, ok := r.queuesPublished.Load(t1.Queue); !ok {
|
||||
t.Fatalf("%q is not cached in queuesPublished", t1.Queue)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnqueueUnique(t *testing.T) {
|
||||
r := setup(t)
|
||||
defer r.Close()
|
||||
@ -1272,7 +1325,6 @@ func TestAddToGroupeTaskIdConflictError(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAddToGroupUnique(t *testing.T) {
|
||||
@ -1356,7 +1408,6 @@ func TestAddToGroupUnique(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAddToGroupUniqueTaskIdConflictError(t *testing.T) {
|
||||
@ -1398,7 +1449,6 @@ func TestAddToGroupUniqueTaskIdConflictError(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSchedule(t *testing.T) {
|
||||
@ -2005,7 +2055,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
|
||||
@ -2174,6 +2223,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()
|
||||
@ -2545,8 +2751,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
|
||||
}
|
||||
|
||||
@ -3053,7 +3259,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.
|
||||
@ -3122,7 +3328,7 @@ func TestAggregationCheck(t *testing.T) {
|
||||
desc string
|
||||
// initial data
|
||||
tasks []*h.TaskSeedData
|
||||
groups map[string][]*redis.Z
|
||||
groups map[string][]redis.Z
|
||||
allGroups map[string][]string
|
||||
|
||||
// args
|
||||
@ -3141,7 +3347,7 @@ func TestAggregationCheck(t *testing.T) {
|
||||
{
|
||||
desc: "with an empty group",
|
||||
tasks: []*h.TaskSeedData{},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "mygroup"): {},
|
||||
},
|
||||
allGroups: map[string][]string{
|
||||
@ -3168,7 +3374,7 @@ func TestAggregationCheck(t *testing.T) {
|
||||
{Msg: msg4, State: base.TaskStateAggregating},
|
||||
{Msg: msg5, State: base.TaskStateAggregating},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "mygroup"): {
|
||||
{Member: msg1.ID, Score: float64(now.Add(-5 * time.Minute).Unix())},
|
||||
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
|
||||
@ -3201,7 +3407,7 @@ func TestAggregationCheck(t *testing.T) {
|
||||
{Msg: msg4, State: base.TaskStateAggregating},
|
||||
{Msg: msg5, State: base.TaskStateAggregating},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "mygroup"): {
|
||||
{Member: msg1.ID, Score: float64(now.Add(-5 * time.Minute).Unix())},
|
||||
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
|
||||
@ -3235,7 +3441,7 @@ func TestAggregationCheck(t *testing.T) {
|
||||
{Msg: msg2, State: base.TaskStateAggregating},
|
||||
{Msg: msg3, State: base.TaskStateAggregating},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "mygroup"): {
|
||||
{Member: msg1.ID, Score: float64(now.Add(-5 * time.Minute).Unix())},
|
||||
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
|
||||
@ -3266,7 +3472,7 @@ func TestAggregationCheck(t *testing.T) {
|
||||
{Msg: msg4, State: base.TaskStateAggregating},
|
||||
{Msg: msg5, State: base.TaskStateAggregating},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "mygroup"): {
|
||||
{Member: msg1.ID, Score: float64(now.Add(-15 * time.Minute).Unix())},
|
||||
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
|
||||
@ -3299,7 +3505,7 @@ func TestAggregationCheck(t *testing.T) {
|
||||
{Msg: msg4, State: base.TaskStateAggregating},
|
||||
{Msg: msg5, State: base.TaskStateAggregating},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "mygroup"): {
|
||||
{Member: msg1.ID, Score: float64(now.Add(-15 * time.Minute).Unix())},
|
||||
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
|
||||
@ -3338,7 +3544,7 @@ func TestAggregationCheck(t *testing.T) {
|
||||
{Msg: msg4, State: base.TaskStateAggregating},
|
||||
{Msg: msg5, State: base.TaskStateAggregating},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "mygroup"): {
|
||||
{Member: msg1.ID, Score: float64(now.Add(-15 * time.Minute).Unix())},
|
||||
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
|
||||
@ -3371,7 +3577,7 @@ func TestAggregationCheck(t *testing.T) {
|
||||
{Msg: msg4, State: base.TaskStateAggregating},
|
||||
{Msg: msg5, State: base.TaskStateAggregating},
|
||||
},
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "mygroup"): {
|
||||
{Member: msg1.ID, Score: float64(now.Add(-15 * time.Minute).Unix())},
|
||||
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
|
||||
@ -3473,8 +3679,8 @@ func TestDeleteAggregationSet(t *testing.T) {
|
||||
desc string
|
||||
// initial data
|
||||
tasks []*h.TaskSeedData
|
||||
aggregationSets map[string][]*redis.Z
|
||||
allAggregationSets map[string][]*redis.Z
|
||||
aggregationSets map[string][]redis.Z
|
||||
allAggregationSets map[string][]redis.Z
|
||||
|
||||
// args
|
||||
ctx context.Context
|
||||
@ -3494,14 +3700,14 @@ func TestDeleteAggregationSet(t *testing.T) {
|
||||
{Msg: m2, State: base.TaskStateAggregating},
|
||||
{Msg: m3, State: base.TaskStateAggregating},
|
||||
},
|
||||
aggregationSets: map[string][]*redis.Z{
|
||||
aggregationSets: map[string][]redis.Z{
|
||||
base.AggregationSetKey("default", "mygroup", setID): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-5 * time.Minute).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-4 * time.Minute).Unix())},
|
||||
{Member: m3.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
|
||||
},
|
||||
},
|
||||
allAggregationSets: map[string][]*redis.Z{
|
||||
allAggregationSets: map[string][]redis.Z{
|
||||
base.AllAggregationSets("default"): {
|
||||
{Member: base.AggregationSetKey("default", "mygroup", setID), Score: float64(now.Add(aggregationTimeout).Unix())},
|
||||
},
|
||||
@ -3528,7 +3734,7 @@ func TestDeleteAggregationSet(t *testing.T) {
|
||||
{Msg: m2, State: base.TaskStateAggregating},
|
||||
{Msg: m3, State: base.TaskStateAggregating},
|
||||
},
|
||||
aggregationSets: map[string][]*redis.Z{
|
||||
aggregationSets: map[string][]redis.Z{
|
||||
base.AggregationSetKey("default", "mygroup", setID): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-5 * time.Minute).Unix())},
|
||||
},
|
||||
@ -3537,7 +3743,7 @@ func TestDeleteAggregationSet(t *testing.T) {
|
||||
{Member: m3.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
|
||||
},
|
||||
},
|
||||
allAggregationSets: map[string][]*redis.Z{
|
||||
allAggregationSets: map[string][]redis.Z{
|
||||
base.AllAggregationSets("default"): {
|
||||
{Member: base.AggregationSetKey("default", "mygroup", setID), Score: float64(now.Add(aggregationTimeout).Unix())},
|
||||
{Member: base.AggregationSetKey("default", "mygroup", otherSetID), Score: float64(now.Add(aggregationTimeout).Unix())},
|
||||
@ -3602,8 +3808,8 @@ func TestDeleteAggregationSetError(t *testing.T) {
|
||||
desc string
|
||||
// initial data
|
||||
tasks []*h.TaskSeedData
|
||||
aggregationSets map[string][]*redis.Z
|
||||
allAggregationSets map[string][]*redis.Z
|
||||
aggregationSets map[string][]redis.Z
|
||||
allAggregationSets map[string][]redis.Z
|
||||
|
||||
// args
|
||||
ctx context.Context
|
||||
@ -3622,14 +3828,14 @@ func TestDeleteAggregationSetError(t *testing.T) {
|
||||
{Msg: m2, State: base.TaskStateAggregating},
|
||||
{Msg: m3, State: base.TaskStateAggregating},
|
||||
},
|
||||
aggregationSets: map[string][]*redis.Z{
|
||||
aggregationSets: map[string][]redis.Z{
|
||||
base.AggregationSetKey("default", "mygroup", setID): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-5 * time.Minute).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-4 * time.Minute).Unix())},
|
||||
{Member: m3.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
|
||||
},
|
||||
},
|
||||
allAggregationSets: map[string][]*redis.Z{
|
||||
allAggregationSets: map[string][]redis.Z{
|
||||
base.AllAggregationSets("default"): {
|
||||
{Member: base.AggregationSetKey("default", "mygroup", setID), Score: float64(now.Add(aggregationTimeout).Unix())},
|
||||
},
|
||||
@ -3688,23 +3894,23 @@ func TestReclaimStaleAggregationSets(t *testing.T) {
|
||||
// Note: In this test, we're trying out a new way to test RDB by exactly describing how
|
||||
// keys and values are represented in Redis.
|
||||
tests := []struct {
|
||||
groups map[string][]*redis.Z // map redis-key to redis-zset
|
||||
aggregationSets map[string][]*redis.Z
|
||||
allAggregationSets map[string][]*redis.Z
|
||||
groups map[string][]redis.Z // map redis-key to redis-zset
|
||||
aggregationSets map[string][]redis.Z
|
||||
allAggregationSets map[string][]redis.Z
|
||||
qname string
|
||||
wantGroups map[string][]redis.Z
|
||||
wantAggregationSets map[string][]redis.Z
|
||||
wantAllAggregationSets map[string][]redis.Z
|
||||
}{
|
||||
{
|
||||
groups: map[string][]*redis.Z{
|
||||
groups: map[string][]redis.Z{
|
||||
base.GroupKey("default", "foo"): {},
|
||||
base.GroupKey("default", "bar"): {},
|
||||
base.GroupKey("default", "qux"): {
|
||||
{Member: m4.ID, Score: float64(now.Add(-10 * time.Second).Unix())},
|
||||
},
|
||||
},
|
||||
aggregationSets: map[string][]*redis.Z{
|
||||
aggregationSets: map[string][]redis.Z{
|
||||
base.AggregationSetKey("default", "foo", "set1"): {
|
||||
{Member: m1.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
|
||||
{Member: m2.ID, Score: float64(now.Add(-4 * time.Minute).Unix())},
|
||||
@ -3713,7 +3919,7 @@ func TestReclaimStaleAggregationSets(t *testing.T) {
|
||||
{Member: m3.ID, Score: float64(now.Add(-1 * time.Minute).Unix())},
|
||||
},
|
||||
},
|
||||
allAggregationSets: map[string][]*redis.Z{
|
||||
allAggregationSets: map[string][]redis.Z{
|
||||
base.AllAggregationSets("default"): {
|
||||
{Member: base.AggregationSetKey("default", "foo", "set1"), Score: float64(now.Add(-10 * time.Second).Unix())}, // set1 is expired
|
||||
{Member: base.AggregationSetKey("default", "bar", "set2"), Score: float64(now.Add(40 * time.Second).Unix())}, // set2 is not expired
|
||||
|
@ -11,8 +11,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"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) {
|
||||
|
@ -13,12 +13,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
"github.com/hibiken/asynq/internal/timeutil"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// EquateInt64Approx returns a Comparer option that treats int64 values
|
||||
@ -252,7 +252,7 @@ func SeedLease(tb testing.TB, r redis.UniversalClient, entries []base.Z, qname s
|
||||
seedRedisZSet(tb, r, base.LeaseKey(qname), entries, base.TaskStateActive)
|
||||
}
|
||||
|
||||
// SeedCompletedQueue initializes the completed set witht the given entries.
|
||||
// SeedCompletedQueue initializes the completed set with the given entries.
|
||||
func SeedCompletedQueue(tb testing.TB, r redis.UniversalClient, entries []base.Z, qname string) {
|
||||
tb.Helper()
|
||||
r.SAdd(context.Background(), base.AllQueues, qname)
|
||||
@ -377,7 +377,7 @@ func seedRedisZSet(tb testing.TB, c redis.UniversalClient, key string,
|
||||
for _, item := range items {
|
||||
msg := item.Message
|
||||
encoded := MustMarshal(tb, msg)
|
||||
z := &redis.Z{Member: msg.ID, Score: float64(item.Score)}
|
||||
z := redis.Z{Member: msg.ID, Score: float64(item.Score)}
|
||||
if err := c.ZAdd(context.Background(), key, z).Err(); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
@ -570,7 +570,7 @@ func SeedTasks(tb testing.TB, r redis.UniversalClient, taskData []*TaskSeedData)
|
||||
}
|
||||
}
|
||||
|
||||
func SeedRedisZSets(tb testing.TB, r redis.UniversalClient, zsets map[string][]*redis.Z) {
|
||||
func SeedRedisZSets(tb testing.TB, r redis.UniversalClient, zsets map[string][]redis.Z) {
|
||||
for key, zs := range zsets {
|
||||
// FIXME: How come we can't simply do ZAdd(ctx, key, zs...) here?
|
||||
for _, z := range zs {
|
||||
|
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,10 +7,11 @@ package asynq
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// PeriodicTaskManager manages scheduling of periodic tasks.
|
||||
@ -28,9 +29,12 @@ type PeriodicTaskManagerOpts struct {
|
||||
// Required: must be non nil
|
||||
PeriodicTaskConfigProvider PeriodicTaskConfigProvider
|
||||
|
||||
// Required: must be non nil
|
||||
// Optional: if RedisUniversalClient is nil must be non nil
|
||||
RedisConnOpt RedisConnOpt
|
||||
|
||||
// Optional: if RedisUniversalClient is non nil, RedisConnOpt is ignored.
|
||||
RedisUniversalClient redis.UniversalClient
|
||||
|
||||
// Optional: scheduler options
|
||||
*SchedulerOpts
|
||||
|
||||
@ -46,10 +50,16 @@ func NewPeriodicTaskManager(opts PeriodicTaskManagerOpts) (*PeriodicTaskManager,
|
||||
if opts.PeriodicTaskConfigProvider == nil {
|
||||
return nil, fmt.Errorf("PeriodicTaskConfigProvider cannot be nil")
|
||||
}
|
||||
if opts.RedisConnOpt == nil {
|
||||
return nil, fmt.Errorf("RedisConnOpt cannot be nil")
|
||||
if opts.RedisConnOpt == nil && opts.RedisUniversalClient == nil {
|
||||
return nil, fmt.Errorf("RedisConnOpt/RedisUniversalClient cannot be nil")
|
||||
}
|
||||
scheduler := NewScheduler(opts.RedisConnOpt, opts.SchedulerOpts)
|
||||
var scheduler *Scheduler
|
||||
if opts.RedisUniversalClient != nil {
|
||||
scheduler = NewSchedulerFromRedisClient(opts.RedisUniversalClient, opts.SchedulerOpts)
|
||||
} else {
|
||||
scheduler = NewScheduler(opts.RedisConnOpt, opts.SchedulerOpts)
|
||||
}
|
||||
|
||||
syncInterval := opts.SyncInterval
|
||||
if syncInterval == 0 {
|
||||
syncInterval = defaultSyncInterval
|
||||
@ -79,13 +89,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))
|
||||
}
|
||||
@ -173,8 +183,8 @@ func (mgr *PeriodicTaskManager) add(configs []*PeriodicTaskConfig) {
|
||||
for _, c := range configs {
|
||||
entryID, err := mgr.s.Register(c.Cronspec, c.Task, c.Opts...)
|
||||
if err != nil {
|
||||
mgr.s.logger.Errorf("Failed to register periodic task: cronspec=%q task=%q",
|
||||
c.Cronspec, c.Task.Type())
|
||||
mgr.s.logger.Errorf("Failed to register periodic task: cronspec=%q task=%q err=%v",
|
||||
c.Cronspec, c.Task.Type(), err)
|
||||
continue
|
||||
}
|
||||
mgr.m[c.hash()] = entryID
|
||||
|
@ -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) {
|
||||
|
133
processor.go
133
processor.go
@ -8,7 +8,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
@ -37,11 +37,11 @@ type processor struct {
|
||||
// orderedQueues is set only in strict-priority mode.
|
||||
orderedQueues []string
|
||||
|
||||
retryDelayFunc RetryDelayFunc
|
||||
isFailureFunc func(error) bool
|
||||
|
||||
errHandler ErrorHandler
|
||||
taskCheckInterval time.Duration
|
||||
retryDelayFunc RetryDelayFunc
|
||||
isFailureFunc func(error) bool
|
||||
|
||||
errHandler ErrorHandler
|
||||
shutdownTimeout time.Duration
|
||||
|
||||
// channel via which to send sync requests to syncer.
|
||||
@ -73,20 +73,21 @@ type processor struct {
|
||||
}
|
||||
|
||||
type processorParams struct {
|
||||
logger *log.Logger
|
||||
broker base.Broker
|
||||
baseCtxFn func() context.Context
|
||||
retryDelayFunc RetryDelayFunc
|
||||
isFailureFunc func(error) bool
|
||||
syncCh chan<- *syncRequest
|
||||
cancelations *base.Cancelations
|
||||
concurrency int
|
||||
queues map[string]int
|
||||
strictPriority bool
|
||||
errHandler ErrorHandler
|
||||
shutdownTimeout time.Duration
|
||||
starting chan<- *workerInfo
|
||||
finished chan<- *base.TaskMessage
|
||||
logger *log.Logger
|
||||
broker base.Broker
|
||||
baseCtxFn func() context.Context
|
||||
retryDelayFunc RetryDelayFunc
|
||||
taskCheckInterval time.Duration
|
||||
isFailureFunc func(error) bool
|
||||
syncCh chan<- *syncRequest
|
||||
cancelations *base.Cancelations
|
||||
concurrency int
|
||||
queues map[string]int
|
||||
strictPriority bool
|
||||
errHandler ErrorHandler
|
||||
shutdownTimeout time.Duration
|
||||
starting chan<- *workerInfo
|
||||
finished chan<- *base.TaskMessage
|
||||
}
|
||||
|
||||
// newProcessor constructs a new processor.
|
||||
@ -97,26 +98,27 @@ func newProcessor(params processorParams) *processor {
|
||||
orderedQueues = sortByPriority(queues)
|
||||
}
|
||||
return &processor{
|
||||
logger: params.logger,
|
||||
broker: params.broker,
|
||||
baseCtxFn: params.baseCtxFn,
|
||||
clock: timeutil.NewRealClock(),
|
||||
queueConfig: queues,
|
||||
orderedQueues: orderedQueues,
|
||||
retryDelayFunc: params.retryDelayFunc,
|
||||
isFailureFunc: params.isFailureFunc,
|
||||
syncRequestCh: params.syncCh,
|
||||
cancelations: params.cancelations,
|
||||
errLogLimiter: rate.NewLimiter(rate.Every(3*time.Second), 1),
|
||||
sema: make(chan struct{}, params.concurrency),
|
||||
done: make(chan struct{}),
|
||||
quit: make(chan struct{}),
|
||||
abort: make(chan struct{}),
|
||||
errHandler: params.errHandler,
|
||||
handler: HandlerFunc(func(ctx context.Context, t *Task) error { return fmt.Errorf("handler not set") }),
|
||||
shutdownTimeout: params.shutdownTimeout,
|
||||
starting: params.starting,
|
||||
finished: params.finished,
|
||||
logger: params.logger,
|
||||
broker: params.broker,
|
||||
baseCtxFn: params.baseCtxFn,
|
||||
clock: timeutil.NewRealClock(),
|
||||
queueConfig: queues,
|
||||
orderedQueues: orderedQueues,
|
||||
taskCheckInterval: params.taskCheckInterval,
|
||||
retryDelayFunc: params.retryDelayFunc,
|
||||
isFailureFunc: params.isFailureFunc,
|
||||
syncRequestCh: params.syncCh,
|
||||
cancelations: params.cancelations,
|
||||
errLogLimiter: rate.NewLimiter(rate.Every(3*time.Second), 1),
|
||||
sema: make(chan struct{}, params.concurrency),
|
||||
done: make(chan struct{}),
|
||||
quit: make(chan struct{}),
|
||||
abort: make(chan struct{}),
|
||||
errHandler: params.errHandler,
|
||||
handler: HandlerFunc(func(ctx context.Context, t *Task) error { return fmt.Errorf("handler not set") }),
|
||||
shutdownTimeout: params.shutdownTimeout,
|
||||
starting: params.starting,
|
||||
finished: params.finished,
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,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(time.Second)
|
||||
jitter := rand.N(p.taskCheckInterval)
|
||||
time.Sleep(p.taskCheckInterval/2 + jitter)
|
||||
<-p.sema // release token
|
||||
return
|
||||
case err != nil:
|
||||
@ -259,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)
|
||||
@ -281,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",
|
||||
@ -302,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)
|
||||
@ -321,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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -343,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)
|
||||
@ -365,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))
|
||||
@ -402,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))
|
||||
}
|
||||
|
||||
@ -420,12 +430,15 @@ func (p *processor) perform(ctx context.Context, task *Task) (err error) {
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
}()
|
||||
@ -521,3 +534,7 @@ func (p *processor) computeDeadline(msg *base.TaskMessage) time.Time {
|
||||
}
|
||||
return time.Unix(msg.Deadline, 0)
|
||||
}
|
||||
|
||||
func IsPanicError(err error) bool {
|
||||
return errors.IsPanicError(err)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@ -62,20 +63,21 @@ func newProcessorForTest(t *testing.T, r *rdb.RDB, h Handler) *processor {
|
||||
go fakeHeartbeater(starting, finished, done)
|
||||
go fakeSyncer(syncCh, done)
|
||||
p := newProcessor(processorParams{
|
||||
logger: testLogger,
|
||||
broker: r,
|
||||
baseCtxFn: context.Background,
|
||||
retryDelayFunc: DefaultRetryDelayFunc,
|
||||
isFailureFunc: defaultIsFailureFunc,
|
||||
syncCh: syncCh,
|
||||
cancelations: base.NewCancelations(),
|
||||
concurrency: 10,
|
||||
queues: defaultQueueConfig,
|
||||
strictPriority: false,
|
||||
errHandler: nil,
|
||||
shutdownTimeout: defaultShutdownTimeout,
|
||||
starting: starting,
|
||||
finished: finished,
|
||||
logger: testLogger,
|
||||
broker: r,
|
||||
baseCtxFn: context.Background,
|
||||
retryDelayFunc: DefaultRetryDelayFunc,
|
||||
taskCheckInterval: defaultTaskCheckInterval,
|
||||
isFailureFunc: defaultIsFailureFunc,
|
||||
syncCh: syncCh,
|
||||
cancelations: base.NewCancelations(),
|
||||
concurrency: 10,
|
||||
queues: defaultQueueConfig,
|
||||
strictPriority: false,
|
||||
errHandler: nil,
|
||||
shutdownTimeout: defaultShutdownTimeout,
|
||||
starting: starting,
|
||||
finished: finished,
|
||||
})
|
||||
p.handler = h
|
||||
return p
|
||||
@ -293,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
|
||||
@ -310,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,
|
||||
@ -344,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 {
|
||||
@ -538,20 +567,21 @@ func TestProcessorWithExpiredLease(t *testing.T) {
|
||||
}()
|
||||
go fakeSyncer(syncCh, done)
|
||||
p := newProcessor(processorParams{
|
||||
logger: testLogger,
|
||||
broker: rdbClient,
|
||||
baseCtxFn: context.Background,
|
||||
retryDelayFunc: DefaultRetryDelayFunc,
|
||||
isFailureFunc: defaultIsFailureFunc,
|
||||
syncCh: syncCh,
|
||||
cancelations: base.NewCancelations(),
|
||||
concurrency: 10,
|
||||
queues: defaultQueueConfig,
|
||||
strictPriority: false,
|
||||
errHandler: nil,
|
||||
shutdownTimeout: defaultShutdownTimeout,
|
||||
starting: starting,
|
||||
finished: finished,
|
||||
logger: testLogger,
|
||||
broker: rdbClient,
|
||||
baseCtxFn: context.Background,
|
||||
taskCheckInterval: defaultTaskCheckInterval,
|
||||
retryDelayFunc: DefaultRetryDelayFunc,
|
||||
isFailureFunc: defaultIsFailureFunc,
|
||||
syncCh: syncCh,
|
||||
cancelations: base.NewCancelations(),
|
||||
concurrency: 10,
|
||||
queues: defaultQueueConfig,
|
||||
strictPriority: false,
|
||||
errHandler: nil,
|
||||
shutdownTimeout: defaultShutdownTimeout,
|
||||
starting: starting,
|
||||
finished: finished,
|
||||
})
|
||||
p.handler = tc.handler
|
||||
var (
|
||||
@ -692,20 +722,21 @@ func TestProcessorWithStrictPriority(t *testing.T) {
|
||||
go fakeHeartbeater(starting, finished, done)
|
||||
go fakeSyncer(syncCh, done)
|
||||
p := newProcessor(processorParams{
|
||||
logger: testLogger,
|
||||
broker: rdbClient,
|
||||
baseCtxFn: context.Background,
|
||||
retryDelayFunc: DefaultRetryDelayFunc,
|
||||
isFailureFunc: defaultIsFailureFunc,
|
||||
syncCh: syncCh,
|
||||
cancelations: base.NewCancelations(),
|
||||
concurrency: 1, // Set concurrency to 1 to make sure tasks are processed one at a time.
|
||||
queues: queueCfg,
|
||||
strictPriority: true,
|
||||
errHandler: nil,
|
||||
shutdownTimeout: defaultShutdownTimeout,
|
||||
starting: starting,
|
||||
finished: finished,
|
||||
logger: testLogger,
|
||||
broker: rdbClient,
|
||||
baseCtxFn: context.Background,
|
||||
taskCheckInterval: defaultTaskCheckInterval,
|
||||
retryDelayFunc: DefaultRetryDelayFunc,
|
||||
isFailureFunc: defaultIsFailureFunc,
|
||||
syncCh: syncCh,
|
||||
cancelations: base.NewCancelations(),
|
||||
concurrency: 1, // Set concurrency to 1 to make sure tasks are processed one at a time.
|
||||
queues: queueCfg,
|
||||
strictPriority: true,
|
||||
errHandler: nil,
|
||||
shutdownTimeout: defaultShutdownTimeout,
|
||||
starting: starting,
|
||||
finished: finished,
|
||||
})
|
||||
p.handler = HandlerFunc(handler)
|
||||
|
||||
@ -921,3 +952,45 @@ func TestProcessorComputeDeadline(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReturnPanicError(t *testing.T) {
|
||||
|
||||
task := NewTask("gen_thumbnail", h.JSON(map[string]interface{}{"src": "some/img/path"}))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
handler HandlerFunc
|
||||
IsPanicError bool
|
||||
}{
|
||||
{
|
||||
name: "should return panic error when occurred panic recovery",
|
||||
handler: func(ctx context.Context, t *Task) error {
|
||||
panic("something went terribly wrong")
|
||||
},
|
||||
IsPanicError: true,
|
||||
},
|
||||
{
|
||||
name: "should return normal error when don't occur panic recovery",
|
||||
handler: func(ctx context.Context, t *Task) error {
|
||||
return fmt.Errorf("something went terribly wrong")
|
||||
},
|
||||
IsPanicError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := processor{
|
||||
logger: log.NewLogger(nil),
|
||||
handler: tc.handler,
|
||||
}
|
||||
got := p.perform(context.Background(), task)
|
||||
if tc.IsPanicError != IsPanicError(got) {
|
||||
t.Errorf("%s: got=%t, want=%t", tc.name, IsPanicError(got), tc.IsPanicError)
|
||||
}
|
||||
if tc.IsPanicError && !strings.HasPrefix(got.Error(), "panic error cause by:") {
|
||||
t.Error("wrong text msg for panic error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ func (r *recoverer) recover() {
|
||||
}
|
||||
|
||||
func (r *recoverer) recoverLeaseExpiredTasks() {
|
||||
// Get all tasks which have expired 30 seconds ago or earlier to accomodate certain amount of clock skew.
|
||||
// Get all tasks which have expired 30 seconds ago or earlier to accommodate certain amount of clock skew.
|
||||
cutoff := time.Now().Add(-30 * time.Second)
|
||||
msgs, err := r.broker.ListLeaseExpired(cutoff, r.queues...)
|
||||
if err != nil {
|
||||
|
170
scheduler.go
170
scheduler.go
@ -10,11 +10,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -26,14 +26,17 @@ type Scheduler struct {
|
||||
|
||||
state *serverState
|
||||
|
||||
logger *log.Logger
|
||||
client *Client
|
||||
rdb *rdb.RDB
|
||||
cron *cron.Cron
|
||||
location *time.Location
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
errHandler func(task *Task, opts []Option, err error)
|
||||
heartbeatInterval time.Duration
|
||||
logger *log.Logger
|
||||
client *Client
|
||||
rdb *rdb.RDB
|
||||
cron *cron.Cron
|
||||
location *time.Location
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
preEnqueueFunc func(task *Task, opts []Option)
|
||||
postEnqueueFunc func(info *TaskInfo, err error)
|
||||
errHandler func(task *Task, opts []Option, err error)
|
||||
|
||||
// guards idmap
|
||||
mu sync.Mutex
|
||||
@ -43,17 +46,48 @@ type Scheduler struct {
|
||||
idmap map[string]cron.EntryID
|
||||
}
|
||||
|
||||
const defaultHeartbeatInterval = 10 * time.Second
|
||||
|
||||
// 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)
|
||||
scheduler := newScheduler(opts)
|
||||
|
||||
redisClient, ok := r.MakeRedisClient().(redis.UniversalClient)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("asynq: unsupported RedisConnOpt type %T", r))
|
||||
}
|
||||
|
||||
rdb := rdb.NewRDB(redisClient)
|
||||
|
||||
scheduler.rdb = rdb
|
||||
scheduler.client = &Client{broker: rdb, 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 {
|
||||
scheduler := newScheduler(opts)
|
||||
|
||||
scheduler.rdb = rdb.NewRDB(c)
|
||||
scheduler.client = NewClientFromRedisClient(c)
|
||||
|
||||
return scheduler
|
||||
}
|
||||
|
||||
func newScheduler(opts *SchedulerOpts) *Scheduler {
|
||||
if opts == nil {
|
||||
opts = &SchedulerOpts{}
|
||||
}
|
||||
|
||||
heartbeatInterval := opts.HeartbeatInterval
|
||||
if heartbeatInterval <= 0 {
|
||||
heartbeatInterval = defaultHeartbeatInterval
|
||||
}
|
||||
|
||||
logger := log.NewLogger(opts.Logger)
|
||||
loglevel := opts.LogLevel
|
||||
if loglevel == level_unspecified {
|
||||
@ -67,16 +101,17 @@ func NewScheduler(r RedisConnOpt, opts *SchedulerOpts) *Scheduler {
|
||||
}
|
||||
|
||||
return &Scheduler{
|
||||
id: generateSchedulerID(),
|
||||
state: &serverState{value: srvStateNew},
|
||||
logger: logger,
|
||||
client: NewClient(r),
|
||||
rdb: rdb.NewRDB(c),
|
||||
cron: cron.New(cron.WithLocation(loc)),
|
||||
location: loc,
|
||||
done: make(chan struct{}),
|
||||
errHandler: opts.EnqueueErrorHandler,
|
||||
idmap: make(map[string]cron.EntryID),
|
||||
id: generateSchedulerID(),
|
||||
state: &serverState{value: srvStateNew},
|
||||
heartbeatInterval: heartbeatInterval,
|
||||
logger: logger,
|
||||
cron: cron.New(cron.WithLocation(loc)),
|
||||
location: loc,
|
||||
done: make(chan struct{}),
|
||||
preEnqueueFunc: opts.PreEnqueueFunc,
|
||||
postEnqueueFunc: opts.PostEnqueueFunc,
|
||||
errHandler: opts.EnqueueErrorHandler,
|
||||
idmap: make(map[string]cron.EntryID),
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,6 +125,15 @@ func generateSchedulerID() string {
|
||||
|
||||
// SchedulerOpts specifies scheduler options.
|
||||
type SchedulerOpts struct {
|
||||
// HeartbeatInterval specifies the interval between scheduler heartbeats.
|
||||
//
|
||||
// If unset, zero or a negative value, the interval is set to 10 second.
|
||||
//
|
||||
// Note: Setting this value too low may add significant load to redis.
|
||||
//
|
||||
// By default, HeartbeatInterval is set to 10 seconds.
|
||||
HeartbeatInterval time.Duration
|
||||
|
||||
// Logger specifies the logger used by the scheduler instance.
|
||||
//
|
||||
// If unset, the default logger is used.
|
||||
@ -105,28 +149,44 @@ type SchedulerOpts struct {
|
||||
// If unset, the UTC time zone (time.UTC) is used.
|
||||
Location *time.Location
|
||||
|
||||
// PreEnqueueFunc, if provided, is called before a task gets enqueued by Scheduler.
|
||||
// The callback function should return quickly to not block the current thread.
|
||||
PreEnqueueFunc func(task *Task, opts []Option)
|
||||
|
||||
// PostEnqueueFunc, if provided, is called after a task gets enqueued by Scheduler.
|
||||
// The callback function should return quickly to not block the current thread.
|
||||
PostEnqueueFunc func(info *TaskInfo, err error)
|
||||
|
||||
// Deprecated: Use PostEnqueueFunc instead
|
||||
// EnqueueErrorHandler gets called when scheduler cannot enqueue a registered task
|
||||
// due to an error.
|
||||
EnqueueErrorHandler func(task *Task, opts []Option, err error)
|
||||
}
|
||||
|
||||
// enqueueJob encapsulates the job of enqueing a task and recording the event.
|
||||
// enqueueJob encapsulates the job of enqueuing a task and recording the event.
|
||||
type enqueueJob struct {
|
||||
id uuid.UUID
|
||||
cronspec string
|
||||
task *Task
|
||||
opts []Option
|
||||
location *time.Location
|
||||
logger *log.Logger
|
||||
client *Client
|
||||
rdb *rdb.RDB
|
||||
errHandler func(task *Task, opts []Option, err error)
|
||||
id uuid.UUID
|
||||
cronspec string
|
||||
task *Task
|
||||
opts []Option
|
||||
location *time.Location
|
||||
logger *log.Logger
|
||||
client *Client
|
||||
rdb *rdb.RDB
|
||||
preEnqueueFunc func(task *Task, opts []Option)
|
||||
postEnqueueFunc func(info *TaskInfo, err error)
|
||||
errHandler func(task *Task, opts []Option, err error)
|
||||
}
|
||||
|
||||
func (j *enqueueJob) Run() {
|
||||
if j.preEnqueueFunc != nil {
|
||||
j.preEnqueueFunc(j.task, j.opts)
|
||||
}
|
||||
info, err := j.client.Enqueue(j.task, j.opts...)
|
||||
if j.postEnqueueFunc != nil {
|
||||
j.postEnqueueFunc(info, err)
|
||||
}
|
||||
if err != nil {
|
||||
j.logger.Errorf("scheduler could not enqueue a task %+v: %v", j.task, err)
|
||||
if j.errHandler != nil {
|
||||
j.errHandler(j.task, j.opts, err)
|
||||
}
|
||||
@ -139,7 +199,7 @@ func (j *enqueueJob) Run() {
|
||||
}
|
||||
err = j.rdb.RecordSchedulerEnqueueEvent(j.id.String(), event)
|
||||
if err != nil {
|
||||
j.logger.Errorf("scheduler could not record enqueue event of enqueued task %+v: %v", j.task, err)
|
||||
j.logger.Warnf("scheduler could not record enqueue event of enqueued task %s: %v", info.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,15 +207,17 @@ func (j *enqueueJob) Run() {
|
||||
// It returns an ID of the newly registered entry.
|
||||
func (s *Scheduler) Register(cronspec string, task *Task, opts ...Option) (entryID string, err error) {
|
||||
job := &enqueueJob{
|
||||
id: uuid.New(),
|
||||
cronspec: cronspec,
|
||||
task: task,
|
||||
opts: opts,
|
||||
location: s.location,
|
||||
client: s.client,
|
||||
rdb: s.rdb,
|
||||
logger: s.logger,
|
||||
errHandler: s.errHandler,
|
||||
id: uuid.New(),
|
||||
cronspec: cronspec,
|
||||
task: task,
|
||||
opts: opts,
|
||||
location: s.location,
|
||||
client: s.client,
|
||||
rdb: s.rdb,
|
||||
logger: s.logger,
|
||||
preEnqueueFunc: s.preEnqueueFunc,
|
||||
postEnqueueFunc: s.postEnqueueFunc,
|
||||
errHandler: s.errHandler,
|
||||
}
|
||||
cronID, err := s.cron.AddJob(cronspec, job)
|
||||
if err != nil {
|
||||
@ -239,19 +301,23 @@ 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)
|
||||
}
|
||||
s.logger.Info("Scheduler stopped")
|
||||
}
|
||||
|
||||
func (s *Scheduler) runHeartbeater() {
|
||||
defer s.wg.Done()
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
ticker := time.NewTicker(s.heartbeatInterval)
|
||||
for {
|
||||
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:
|
||||
s.beat()
|
||||
@ -275,8 +341,7 @@ func (s *Scheduler) beat() {
|
||||
}
|
||||
entries = append(entries, e)
|
||||
}
|
||||
s.logger.Debugf("Writing entries %v", entries)
|
||||
if err := s.rdb.WriteSchedulerEntries(s.id, entries, 5*time.Second); err != nil {
|
||||
if err := s.rdb.WriteSchedulerEntries(s.id, entries, s.heartbeatInterval*2); err != nil {
|
||||
s.logger.Warnf("Scheduler could not write heartbeat data: %v", err)
|
||||
}
|
||||
}
|
||||
@ -297,3 +362,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,8 @@ 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"
|
||||
)
|
||||
@ -57,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 {
|
||||
@ -74,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) {
|
||||
@ -89,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},
|
||||
)
|
||||
|
||||
@ -154,3 +179,56 @@ func TestSchedulerUnregister(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchedulerPostAndPreEnqueueHandler(t *testing.T) {
|
||||
var (
|
||||
preMu sync.Mutex
|
||||
preCounter int
|
||||
postMu sync.Mutex
|
||||
postCounter int
|
||||
)
|
||||
preHandler := func(task *Task, opts []Option) {
|
||||
preMu.Lock()
|
||||
preCounter++
|
||||
preMu.Unlock()
|
||||
}
|
||||
postHandler := func(info *TaskInfo, err error) {
|
||||
postMu.Lock()
|
||||
postCounter++
|
||||
postMu.Unlock()
|
||||
}
|
||||
|
||||
// Connect to non-existent redis instance to simulate a redis server being down.
|
||||
scheduler := NewScheduler(
|
||||
getRedisConnOpt(t),
|
||||
&SchedulerOpts{
|
||||
PreEnqueueFunc: preHandler,
|
||||
PostEnqueueFunc: postHandler,
|
||||
},
|
||||
)
|
||||
|
||||
task := NewTask("test", nil)
|
||||
|
||||
if _, err := scheduler.Register("@every 3s", task); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := scheduler.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Scheduler should attempt to enqueue the task three times (every 3s).
|
||||
time.Sleep(10 * time.Second)
|
||||
scheduler.Shutdown()
|
||||
|
||||
preMu.Lock()
|
||||
if preCounter != 3 {
|
||||
t.Errorf("PreEnqueueFunc was called %d times, want 3", preCounter)
|
||||
}
|
||||
preMu.Unlock()
|
||||
|
||||
postMu.Lock()
|
||||
if postCounter != 3 {
|
||||
t.Errorf("PostEnqueueFunc was called %d times, want 3", postCounter)
|
||||
}
|
||||
postMu.Unlock()
|
||||
}
|
||||
|
@ -144,9 +144,7 @@ func (mux *ServeMux) HandleFunc(pattern string, handler func(context.Context, *T
|
||||
func (mux *ServeMux) Use(mws ...MiddlewareFunc) {
|
||||
mux.mu.Lock()
|
||||
defer mux.mu.Unlock()
|
||||
for _, fn := range mws {
|
||||
mux.mws = append(mux.mws, fn)
|
||||
}
|
||||
mux.mws = append(mux.mws, mws...)
|
||||
}
|
||||
|
||||
// NotFound returns an error indicating that the handler was not found for the given task.
|
||||
|
164
server.go
164
server.go
@ -9,16 +9,16 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
"github.com/hibiken/asynq/internal/log"
|
||||
"github.com/hibiken/asynq/internal/rdb"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Server is responsible for task processing and task lifecycle management.
|
||||
@ -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
|
||||
|
||||
@ -104,6 +107,15 @@ type Config struct {
|
||||
// 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.
|
||||
//
|
||||
// Note: Setting this value too low may add significant load to redis.
|
||||
//
|
||||
// By default, TaskCheckInterval is set to 1 seconds.
|
||||
TaskCheckInterval time.Duration
|
||||
|
||||
// Function to calculate retry delay for a failed task.
|
||||
//
|
||||
// By default, it uses exponential backoff algorithm to calculate the delay.
|
||||
@ -162,6 +174,15 @@ type Config struct {
|
||||
// })
|
||||
//
|
||||
// ErrorHandler: asynq.ErrorHandlerFunc(reportError)
|
||||
//
|
||||
// we can also handle panic error like:
|
||||
// func reportError(ctx context, task *asynq.Task, err error) {
|
||||
// if asynq.IsPanicError(err) {
|
||||
// errorReportingService.Notify(err)
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// ErrorHandler: asynq.ErrorHandlerFunc(reportError)
|
||||
ErrorHandler ErrorHandler
|
||||
|
||||
// Logger specifies the logger used by the server instance.
|
||||
@ -220,6 +241,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.
|
||||
@ -242,7 +274,7 @@ func (fn GroupAggregatorFunc) Aggregate(group string, tasks []*Task) *Task {
|
||||
return fn(group, tasks)
|
||||
}
|
||||
|
||||
// An ErrorHandler handles an error occured during task processing.
|
||||
// An ErrorHandler handles an error occurred during task processing.
|
||||
type ErrorHandler interface {
|
||||
HandleError(ctx context.Context, task *Task, err error)
|
||||
}
|
||||
@ -367,9 +399,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
|
||||
}
|
||||
|
||||
@ -380,6 +411,8 @@ var defaultQueueConfig = map[string]int{
|
||||
}
|
||||
|
||||
const (
|
||||
defaultTaskCheckInterval = 1 * time.Second
|
||||
|
||||
defaultShutdownTimeout = 8 * time.Second
|
||||
|
||||
defaultHealthCheckInterval = 15 * time.Second
|
||||
@ -387,15 +420,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
|
||||
@ -404,6 +450,12 @@ func NewServer(r RedisConnOpt, cfg Config) *Server {
|
||||
if n < 1 {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
|
||||
taskCheckInterval := cfg.TaskCheckInterval
|
||||
if taskCheckInterval <= 0 {
|
||||
taskCheckInterval = defaultTaskCheckInterval
|
||||
}
|
||||
|
||||
delayFunc := cfg.RetryDelayFunc
|
||||
if delayFunc == nil {
|
||||
delayFunc = DefaultRetryDelayFunc
|
||||
@ -490,20 +542,21 @@ func NewServer(r RedisConnOpt, cfg Config) *Server {
|
||||
cancelations: cancels,
|
||||
})
|
||||
processor := newProcessor(processorParams{
|
||||
logger: logger,
|
||||
broker: rdb,
|
||||
retryDelayFunc: delayFunc,
|
||||
baseCtxFn: baseCtxFn,
|
||||
isFailureFunc: isFailureFunc,
|
||||
syncCh: syncCh,
|
||||
cancelations: cancels,
|
||||
concurrency: n,
|
||||
queues: queues,
|
||||
strictPriority: cfg.StrictPriority,
|
||||
errHandler: cfg.ErrorHandler,
|
||||
shutdownTimeout: shutdownTimeout,
|
||||
starting: starting,
|
||||
finished: finished,
|
||||
logger: logger,
|
||||
broker: rdb,
|
||||
retryDelayFunc: delayFunc,
|
||||
taskCheckInterval: taskCheckInterval,
|
||||
baseCtxFn: baseCtxFn,
|
||||
isFailureFunc: isFailureFunc,
|
||||
syncCh: syncCh,
|
||||
cancelations: cancels,
|
||||
concurrency: n,
|
||||
queues: queues,
|
||||
strictPriority: cfg.StrictPriority,
|
||||
errHandler: cfg.ErrorHandler,
|
||||
shutdownTimeout: shutdownTimeout,
|
||||
starting: starting,
|
||||
finished: finished,
|
||||
})
|
||||
recoverer := newRecoverer(recovererParams{
|
||||
logger: logger,
|
||||
@ -519,11 +572,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,
|
||||
@ -535,18 +603,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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -562,6 +631,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
|
||||
}
|
||||
@ -674,7 +747,9 @@ func (srv *Server) Shutdown() {
|
||||
srv.heartbeater.shutdown()
|
||||
srv.wg.Wait()
|
||||
|
||||
srv.broker.Close()
|
||||
if !srv.sharedConnection {
|
||||
srv.broker.Close()
|
||||
}
|
||||
srv.logger.Info("Exiting")
|
||||
}
|
||||
|
||||
@ -686,7 +761,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
|
||||
}
|
||||
@ -697,3 +772,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/go-redis/redis/v8/internal/pool.(*ConnPool).reaper")
|
||||
defer goleak.VerifyNoLeaks(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/go-redis/redis/v8/internal/pool.(*ConnPool).reaper")
|
||||
defer goleak.VerifyNoLeaks(t, ignoreOpt)
|
||||
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 @@
|
||||
// +build linux bsd darwin
|
||||
//go:build linux || dragonfly || freebsd || netbsd || openbsd || darwin
|
||||
|
||||
package asynq
|
||||
|
||||
@ -24,8 +24,10 @@ func (srv *Server) waitForSignals() {
|
||||
if sig == unix.SIGTSTP {
|
||||
srv.Stop()
|
||||
continue
|
||||
} else {
|
||||
srv.Stop()
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package asynq
|
||||
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
"github.com/hibiken/asynq/internal/log"
|
||||
)
|
||||
|
@ -12,7 +12,7 @@ Asynq CLI is a command line tool to monitor the queues and tasks managed by `asy
|
||||
|
||||
In order to use the tool, compile it using the following command:
|
||||
|
||||
go get github.com/hibiken/asynq/tools/asynq
|
||||
go install github.com/hibiken/asynq/tools/asynq@latest
|
||||
|
||||
This will create the asynq executable under your `$GOPATH/bin` directory.
|
||||
|
||||
@ -22,9 +22,10 @@ This will create the asynq executable under your `$GOPATH/bin` directory.
|
||||
|
||||
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 (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -24,21 +25,30 @@ func init() {
|
||||
}
|
||||
|
||||
var cronCmd = &cobra.Command{
|
||||
Use: "cron",
|
||||
Use: "cron <command> [flags]",
|
||||
Short: "Manage cron",
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq cron ls
|
||||
$ asynq cron history 7837f142-6337-4217-9276-8f27281b67d1`),
|
||||
}
|
||||
|
||||
var cronListCmd = &cobra.Command{
|
||||
Use: "ls",
|
||||
Short: "List cron entries",
|
||||
Run: cronList,
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List cron entries",
|
||||
Run: cronList,
|
||||
}
|
||||
|
||||
var cronHistoryCmd = &cobra.Command{
|
||||
Use: "history [ENTRY_ID...]",
|
||||
Use: "history <entry_id> [<entry_id>...]",
|
||||
Short: "Show history of each cron tasks",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: cronHistory,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq cron history 7837f142-6337-4217-9276-8f27281b67d1
|
||||
$ asynq cron history 7837f142-6337-4217-9276-8f27281b67d1 bf6a8594-cd03-4968-b36a-8572c5e160dd
|
||||
$ asynq cron history 7837f142-6337-4217-9276-8f27281b67d1 --size=100
|
||||
$ asynq cron history 7837f142-6337-4217-9276-8f27281b67d1 --page=2`),
|
||||
}
|
||||
|
||||
func cronList(cmd *cobra.Command, args []string) {
|
||||
|
45
tools/asynq/cmd/dash.go
Normal file
45
tools/asynq/cmd/dash.go
Normal file
@ -0,0 +1,45 @@
|
||||
// Copyright 2022 Kentaro Hibino. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/hibiken/asynq/tools/asynq/cmd/dash"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
flagPollInterval = 8 * time.Second
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(dashCmd)
|
||||
dashCmd.Flags().DurationVar(&flagPollInterval, "refresh", 8*time.Second, "Interval between data refresh (default: 8s, min allowed: 1s)")
|
||||
}
|
||||
|
||||
var dashCmd = &cobra.Command{
|
||||
Use: "dash",
|
||||
Short: "View dashboard",
|
||||
Long: heredoc.Doc(`
|
||||
Display interactive dashboard.`),
|
||||
Args: cobra.NoArgs,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq dash
|
||||
$ asynq dash --refresh=3s`),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if flagPollInterval < 1*time.Second {
|
||||
fmt.Println("error: --refresh cannot be less than 1s")
|
||||
os.Exit(1)
|
||||
}
|
||||
dash.Run(dash.Options{
|
||||
PollInterval: flagPollInterval,
|
||||
RedisConnOpt: getRedisConnOpt(),
|
||||
})
|
||||
},
|
||||
}
|
220
tools/asynq/cmd/dash/dash.go
Normal file
220
tools/asynq/cmd/dash/dash.go
Normal file
@ -0,0 +1,220 @@
|
||||
// Copyright 2022 Kentaro Hibino. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package dash
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
// viewType is an enum for dashboard views.
|
||||
type viewType int
|
||||
|
||||
const (
|
||||
viewTypeQueues viewType = iota
|
||||
viewTypeQueueDetails
|
||||
viewTypeHelp
|
||||
)
|
||||
|
||||
// State holds dashboard state.
|
||||
type State struct {
|
||||
queues []*asynq.QueueInfo
|
||||
tasks []*asynq.TaskInfo
|
||||
groups []*asynq.GroupInfo
|
||||
err error
|
||||
|
||||
// Note: index zero corresponds to the table header; index=1 correctponds to the first element
|
||||
queueTableRowIdx int // highlighted row in queue table
|
||||
taskTableRowIdx int // highlighted row in task table
|
||||
groupTableRowIdx int // highlighted row in group table
|
||||
taskState asynq.TaskState // highlighted task state in queue details view
|
||||
taskID string // selected task ID
|
||||
|
||||
selectedQueue *asynq.QueueInfo // queue shown on queue details view
|
||||
selectedGroup *asynq.GroupInfo
|
||||
selectedTask *asynq.TaskInfo
|
||||
|
||||
pageNum int // pagination page number
|
||||
|
||||
view viewType // current view type
|
||||
prevView viewType // to support "go back"
|
||||
}
|
||||
|
||||
func (s *State) DebugString() string {
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("len(queues)=%d ", len(s.queues)))
|
||||
b.WriteString(fmt.Sprintf("len(tasks)=%d ", len(s.tasks)))
|
||||
b.WriteString(fmt.Sprintf("len(groups)=%d ", len(s.groups)))
|
||||
b.WriteString(fmt.Sprintf("err=%v ", s.err))
|
||||
|
||||
if s.taskState != 0 {
|
||||
b.WriteString(fmt.Sprintf("taskState=%s ", s.taskState.String()))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf("taskState=0"))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("taskID=%s ", s.taskID))
|
||||
|
||||
b.WriteString(fmt.Sprintf("queueTableRowIdx=%d ", s.queueTableRowIdx))
|
||||
b.WriteString(fmt.Sprintf("taskTableRowIdx=%d ", s.taskTableRowIdx))
|
||||
b.WriteString(fmt.Sprintf("groupTableRowIdx=%d ", s.groupTableRowIdx))
|
||||
|
||||
if s.selectedQueue != nil {
|
||||
b.WriteString(fmt.Sprintf("selectedQueue={Queue:%s} ", s.selectedQueue.Queue))
|
||||
} else {
|
||||
b.WriteString("selectedQueue=nil ")
|
||||
}
|
||||
|
||||
if s.selectedGroup != nil {
|
||||
b.WriteString(fmt.Sprintf("selectedGroup={Group:%s} ", s.selectedGroup.Group))
|
||||
} else {
|
||||
b.WriteString("selectedGroup=nil ")
|
||||
}
|
||||
|
||||
if s.selectedTask != nil {
|
||||
b.WriteString(fmt.Sprintf("selectedTask={ID:%s} ", s.selectedTask.ID))
|
||||
} else {
|
||||
b.WriteString("selectedTask=nil ")
|
||||
}
|
||||
|
||||
b.WriteString(fmt.Sprintf("pageNum=%d", s.pageNum))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
DebugMode bool
|
||||
PollInterval time.Duration
|
||||
RedisConnOpt asynq.RedisConnOpt
|
||||
}
|
||||
|
||||
func Run(opts Options) {
|
||||
s, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create a screen: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := s.Init(); err != nil {
|
||||
fmt.Printf("failed to initialize screen: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
s.SetStyle(baseStyle) // set default text style
|
||||
|
||||
var (
|
||||
state = State{} // confined in this goroutine only; DO NOT SHARE
|
||||
|
||||
inspector = asynq.NewInspector(opts.RedisConnOpt)
|
||||
ticker = time.NewTicker(opts.PollInterval)
|
||||
|
||||
eventCh = make(chan tcell.Event)
|
||||
done = make(chan struct{})
|
||||
|
||||
// channels to send/receive data fetched asynchronously
|
||||
errorCh = make(chan error)
|
||||
queueCh = make(chan *asynq.QueueInfo)
|
||||
taskCh = make(chan *asynq.TaskInfo)
|
||||
queuesCh = make(chan []*asynq.QueueInfo)
|
||||
groupsCh = make(chan []*asynq.GroupInfo)
|
||||
tasksCh = make(chan []*asynq.TaskInfo)
|
||||
)
|
||||
defer ticker.Stop()
|
||||
|
||||
f := dataFetcher{
|
||||
inspector,
|
||||
opts,
|
||||
s,
|
||||
errorCh,
|
||||
queueCh,
|
||||
taskCh,
|
||||
queuesCh,
|
||||
groupsCh,
|
||||
tasksCh,
|
||||
}
|
||||
|
||||
d := dashDrawer{
|
||||
s,
|
||||
opts,
|
||||
}
|
||||
|
||||
h := keyEventHandler{
|
||||
s: s,
|
||||
fetcher: &f,
|
||||
drawer: &d,
|
||||
state: &state,
|
||||
done: done,
|
||||
ticker: ticker,
|
||||
pollInterval: opts.PollInterval,
|
||||
}
|
||||
|
||||
go fetchQueues(inspector, queuesCh, errorCh, opts)
|
||||
go s.ChannelEvents(eventCh, done) // TODO: Double check that we are not leaking goroutine with this one.
|
||||
d.Draw(&state) // draw initial screen
|
||||
|
||||
for {
|
||||
// Update screen
|
||||
s.Show()
|
||||
|
||||
select {
|
||||
case ev := <-eventCh:
|
||||
// Process event
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventResize:
|
||||
s.Sync()
|
||||
case *tcell.EventKey:
|
||||
h.HandleKeyEvent(ev)
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
f.Fetch(&state)
|
||||
|
||||
case queues := <-queuesCh:
|
||||
state.queues = queues
|
||||
state.err = nil
|
||||
if len(queues) < state.queueTableRowIdx {
|
||||
state.queueTableRowIdx = len(queues)
|
||||
}
|
||||
d.Draw(&state)
|
||||
|
||||
case q := <-queueCh:
|
||||
state.selectedQueue = q
|
||||
state.err = nil
|
||||
d.Draw(&state)
|
||||
|
||||
case groups := <-groupsCh:
|
||||
state.groups = groups
|
||||
state.err = nil
|
||||
if len(groups) < state.groupTableRowIdx {
|
||||
state.groupTableRowIdx = len(groups)
|
||||
}
|
||||
d.Draw(&state)
|
||||
|
||||
case tasks := <-tasksCh:
|
||||
state.tasks = tasks
|
||||
state.err = nil
|
||||
if len(tasks) < state.taskTableRowIdx {
|
||||
state.taskTableRowIdx = len(tasks)
|
||||
}
|
||||
d.Draw(&state)
|
||||
|
||||
case t := <-taskCh:
|
||||
state.selectedTask = t
|
||||
state.err = nil
|
||||
d.Draw(&state)
|
||||
|
||||
case err := <-errorCh:
|
||||
if errors.Is(err, asynq.ErrTaskNotFound) {
|
||||
state.selectedTask = nil
|
||||
} else {
|
||||
state.err = err
|
||||
}
|
||||
d.Draw(&state)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
724
tools/asynq/cmd/dash/draw.go
Normal file
724
tools/asynq/cmd/dash/draw.go
Normal file
@ -0,0 +1,724 @@
|
||||
// Copyright 2022 Kentaro Hibino. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package dash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
var (
|
||||
baseStyle = tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
|
||||
labelStyle = baseStyle.Foreground(tcell.ColorLightGray)
|
||||
|
||||
// styles for bar graph
|
||||
activeStyle = baseStyle.Foreground(tcell.ColorBlue)
|
||||
pendingStyle = baseStyle.Foreground(tcell.ColorGreen)
|
||||
aggregatingStyle = baseStyle.Foreground(tcell.ColorLightGreen)
|
||||
scheduledStyle = baseStyle.Foreground(tcell.ColorYellow)
|
||||
retryStyle = baseStyle.Foreground(tcell.ColorPink)
|
||||
archivedStyle = baseStyle.Foreground(tcell.ColorPurple)
|
||||
completedStyle = baseStyle.Foreground(tcell.ColorDarkGreen)
|
||||
)
|
||||
|
||||
// drawer draws UI with the given state.
|
||||
type drawer interface {
|
||||
Draw(state *State)
|
||||
}
|
||||
|
||||
type dashDrawer struct {
|
||||
s tcell.Screen
|
||||
opts Options
|
||||
}
|
||||
|
||||
func (dd *dashDrawer) Draw(state *State) {
|
||||
s, opts := dd.s, dd.opts
|
||||
s.Clear()
|
||||
// Simulate data update on every render
|
||||
d := NewScreenDrawer(s)
|
||||
switch state.view {
|
||||
case viewTypeQueues:
|
||||
d.Println("=== Queues ===", baseStyle.Bold(true))
|
||||
d.NL()
|
||||
drawQueueSizeGraphs(d, state)
|
||||
d.NL()
|
||||
drawQueueTable(d, baseStyle, state)
|
||||
case viewTypeQueueDetails:
|
||||
d.Println("=== Queue Summary ===", baseStyle.Bold(true))
|
||||
d.NL()
|
||||
drawQueueSummary(d, state)
|
||||
d.NL()
|
||||
d.NL()
|
||||
d.Println("=== Tasks ===", baseStyle.Bold(true))
|
||||
d.NL()
|
||||
drawTaskStateBreakdown(d, baseStyle, state)
|
||||
d.NL()
|
||||
drawTaskTable(d, state)
|
||||
drawTaskModal(d, state)
|
||||
case viewTypeHelp:
|
||||
drawHelp(d)
|
||||
}
|
||||
d.GoToBottom()
|
||||
if opts.DebugMode {
|
||||
drawDebugInfo(d, state)
|
||||
} else {
|
||||
drawFooter(d, state)
|
||||
}
|
||||
}
|
||||
|
||||
func drawQueueSizeGraphs(d *ScreenDrawer, state *State) {
|
||||
var qnames []string
|
||||
var qsizes []string // queue size in strings
|
||||
maxSize := 1 // not zero to avoid division by zero
|
||||
for _, q := range state.queues {
|
||||
qnames = append(qnames, q.Queue)
|
||||
qsizes = append(qsizes, strconv.Itoa(q.Size))
|
||||
if q.Size > maxSize {
|
||||
maxSize = q.Size
|
||||
}
|
||||
}
|
||||
qnameWidth := maxwidth(qnames)
|
||||
qsizeWidth := maxwidth(qsizes)
|
||||
|
||||
// Calculate the multipler to scale the graph
|
||||
screenWidth, _ := d.Screen().Size()
|
||||
graphMaxWidth := screenWidth - (qnameWidth + qsizeWidth + 3) // <qname> |<graph> <size>
|
||||
multipiler := 1.0
|
||||
if graphMaxWidth < maxSize {
|
||||
multipiler = float64(graphMaxWidth) / float64(maxSize)
|
||||
}
|
||||
|
||||
const tick = '▇'
|
||||
for _, q := range state.queues {
|
||||
d.Print(q.Queue, baseStyle)
|
||||
d.Print(strings.Repeat(" ", qnameWidth-runewidth.StringWidth(q.Queue)+1), baseStyle) // padding between qname and graph
|
||||
d.Print("|", baseStyle)
|
||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Active)*multipiler))), activeStyle)
|
||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Pending)*multipiler))), pendingStyle)
|
||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Aggregating)*multipiler))), aggregatingStyle)
|
||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Scheduled)*multipiler))), scheduledStyle)
|
||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Retry)*multipiler))), retryStyle)
|
||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Archived)*multipiler))), archivedStyle)
|
||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Completed)*multipiler))), completedStyle)
|
||||
d.Print(fmt.Sprintf(" %d", q.Size), baseStyle)
|
||||
d.NL()
|
||||
}
|
||||
d.NL()
|
||||
d.Print("active=", baseStyle)
|
||||
d.Print(string(tick), activeStyle)
|
||||
d.Print(" pending=", baseStyle)
|
||||
d.Print(string(tick), pendingStyle)
|
||||
d.Print(" aggregating=", baseStyle)
|
||||
d.Print(string(tick), aggregatingStyle)
|
||||
d.Print(" scheduled=", baseStyle)
|
||||
d.Print(string(tick), scheduledStyle)
|
||||
d.Print(" retry=", baseStyle)
|
||||
d.Print(string(tick), retryStyle)
|
||||
d.Print(" archived=", baseStyle)
|
||||
d.Print(string(tick), archivedStyle)
|
||||
d.Print(" completed=", baseStyle)
|
||||
d.Print(string(tick), completedStyle)
|
||||
d.NL()
|
||||
}
|
||||
|
||||
func drawFooter(d *ScreenDrawer, state *State) {
|
||||
if state.err != nil {
|
||||
style := baseStyle.Background(tcell.ColorDarkRed)
|
||||
d.Print(state.err.Error(), style)
|
||||
d.FillLine(' ', style)
|
||||
return
|
||||
}
|
||||
style := baseStyle.Background(tcell.ColorDarkSlateGray).Foreground(tcell.ColorWhite)
|
||||
switch state.view {
|
||||
case viewTypeHelp:
|
||||
d.Print("<Esc>: GoBack", style)
|
||||
default:
|
||||
d.Print("<?>: Help <Ctrl+C>: Exit ", style)
|
||||
}
|
||||
d.FillLine(' ', style)
|
||||
}
|
||||
|
||||
// returns the maximum width from the given list of names
|
||||
func maxwidth(names []string) int {
|
||||
max := 0
|
||||
for _, s := range names {
|
||||
if w := runewidth.StringWidth(s); w > max {
|
||||
max = w
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
// rpad adds padding to the right of a string.
|
||||
func rpad(s string, padding int) string {
|
||||
tmpl := fmt.Sprintf("%%-%ds ", padding)
|
||||
return fmt.Sprintf(tmpl, s)
|
||||
|
||||
}
|
||||
|
||||
// lpad adds padding to the left of a string.
|
||||
func lpad(s string, padding int) string {
|
||||
tmpl := fmt.Sprintf("%%%ds ", padding)
|
||||
return fmt.Sprintf(tmpl, s)
|
||||
}
|
||||
|
||||
// byteCount converts the given bytes into human readable string
|
||||
func byteCount(b int64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
var queueColumnConfigs = []*columnConfig[*asynq.QueueInfo]{
|
||||
{"Queue", alignLeft, func(q *asynq.QueueInfo) string { return q.Queue }},
|
||||
{"State", alignLeft, func(q *asynq.QueueInfo) string { return formatQueueState(q) }},
|
||||
{"Size", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Size) }},
|
||||
{"Latency", alignRight, func(q *asynq.QueueInfo) string { return q.Latency.Round(time.Second).String() }},
|
||||
{"MemoryUsage", alignRight, func(q *asynq.QueueInfo) string { return byteCount(q.MemoryUsage) }},
|
||||
{"Processed", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Processed) }},
|
||||
{"Failed", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Failed) }},
|
||||
{"ErrorRate", alignRight, func(q *asynq.QueueInfo) string { return formatErrorRate(q.Processed, q.Failed) }},
|
||||
}
|
||||
|
||||
func formatQueueState(q *asynq.QueueInfo) string {
|
||||
if q.Paused {
|
||||
return "PAUSED"
|
||||
}
|
||||
return "RUN"
|
||||
}
|
||||
|
||||
func formatErrorRate(processed, failed int) string {
|
||||
if processed == 0 {
|
||||
return "-"
|
||||
}
|
||||
return fmt.Sprintf("%.2f", float64(failed)/float64(processed))
|
||||
}
|
||||
|
||||
func formatNextProcessTime(t time.Time) string {
|
||||
now := time.Now()
|
||||
if t.Before(now) {
|
||||
return "now"
|
||||
}
|
||||
return fmt.Sprintf("in %v", (t.Sub(now).Round(time.Second)))
|
||||
}
|
||||
|
||||
func formatPastTime(t time.Time) string {
|
||||
now := time.Now()
|
||||
if t.After(now) || t.Equal(now) {
|
||||
return "just now"
|
||||
}
|
||||
return fmt.Sprintf("%v ago", time.Since(t).Round(time.Second))
|
||||
}
|
||||
|
||||
func drawQueueTable(d *ScreenDrawer, style tcell.Style, state *State) {
|
||||
drawTable(d, style, queueColumnConfigs, state.queues, state.queueTableRowIdx-1)
|
||||
}
|
||||
|
||||
func drawQueueSummary(d *ScreenDrawer, state *State) {
|
||||
q := state.selectedQueue
|
||||
if q == nil {
|
||||
d.Println("ERROR: Press q to go back", baseStyle)
|
||||
return
|
||||
}
|
||||
d.Print("Name ", labelStyle)
|
||||
d.Println(q.Queue, baseStyle)
|
||||
d.Print("Size ", labelStyle)
|
||||
d.Println(strconv.Itoa(q.Size), baseStyle)
|
||||
d.Print("Latency ", labelStyle)
|
||||
d.Println(q.Latency.Round(time.Second).String(), baseStyle)
|
||||
d.Print("MemUsage ", labelStyle)
|
||||
d.Println(byteCount(q.MemoryUsage), baseStyle)
|
||||
}
|
||||
|
||||
// Returns the max number of groups that can be displayed.
|
||||
func groupPageSize(s tcell.Screen) int {
|
||||
_, h := s.Size()
|
||||
return h - 16 // height - (# of rows used)
|
||||
}
|
||||
|
||||
// Returns the number of tasks to fetch.
|
||||
func taskPageSize(s tcell.Screen) int {
|
||||
_, h := s.Size()
|
||||
return h - 15 // height - (# of rows used)
|
||||
}
|
||||
|
||||
func shouldShowGroupTable(state *State) bool {
|
||||
return state.taskState == asynq.TaskStateAggregating && state.selectedGroup == nil
|
||||
}
|
||||
|
||||
func getTaskTableColumnConfig(taskState asynq.TaskState) []*columnConfig[*asynq.TaskInfo] {
|
||||
switch taskState {
|
||||
case asynq.TaskStateActive:
|
||||
return activeTaskTableColumns
|
||||
case asynq.TaskStatePending:
|
||||
return pendingTaskTableColumns
|
||||
case asynq.TaskStateAggregating:
|
||||
return aggregatingTaskTableColumns
|
||||
case asynq.TaskStateScheduled:
|
||||
return scheduledTaskTableColumns
|
||||
case asynq.TaskStateRetry:
|
||||
return retryTaskTableColumns
|
||||
case asynq.TaskStateArchived:
|
||||
return archivedTaskTableColumns
|
||||
case asynq.TaskStateCompleted:
|
||||
return completedTaskTableColumns
|
||||
}
|
||||
panic("unknown task state")
|
||||
}
|
||||
|
||||
var activeTaskTableColumns = []*columnConfig[*asynq.TaskInfo]{
|
||||
{"ID", alignLeft, func(t *asynq.TaskInfo) string { return t.ID }},
|
||||
{"Type", alignLeft, func(t *asynq.TaskInfo) string { return t.Type }},
|
||||
{"Retried", alignRight, func(t *asynq.TaskInfo) string { return strconv.Itoa(t.Retried) }},
|
||||
{"Max Retry", alignRight, func(t *asynq.TaskInfo) string { return strconv.Itoa(t.MaxRetry) }},
|
||||
{"Payload", alignLeft, func(t *asynq.TaskInfo) string { return formatByteSlice(t.Payload) }},
|
||||
}
|
||||
|
||||
var pendingTaskTableColumns = []*columnConfig[*asynq.TaskInfo]{
|
||||
{"ID", alignLeft, func(t *asynq.TaskInfo) string { return t.ID }},
|
||||
{"Type", alignLeft, func(t *asynq.TaskInfo) string { return t.Type }},
|
||||
{"Retried", alignRight, func(t *asynq.TaskInfo) string { return strconv.Itoa(t.Retried) }},
|
||||
{"Max Retry", alignRight, func(t *asynq.TaskInfo) string { return strconv.Itoa(t.MaxRetry) }},
|
||||
{"Payload", alignLeft, func(t *asynq.TaskInfo) string { return formatByteSlice(t.Payload) }},
|
||||
}
|
||||
|
||||
var aggregatingTaskTableColumns = []*columnConfig[*asynq.TaskInfo]{
|
||||
{"ID", alignLeft, func(t *asynq.TaskInfo) string { return t.ID }},
|
||||
{"Type", alignLeft, func(t *asynq.TaskInfo) string { return t.Type }},
|
||||
{"Payload", alignLeft, func(t *asynq.TaskInfo) string { return formatByteSlice(t.Payload) }},
|
||||
{"Group", alignLeft, func(t *asynq.TaskInfo) string { return t.Group }},
|
||||
}
|
||||
|
||||
var scheduledTaskTableColumns = []*columnConfig[*asynq.TaskInfo]{
|
||||
{"ID", alignLeft, func(t *asynq.TaskInfo) string { return t.ID }},
|
||||
{"Type", alignLeft, func(t *asynq.TaskInfo) string { return t.Type }},
|
||||
{"Next Process Time", alignLeft, func(t *asynq.TaskInfo) string {
|
||||
return formatNextProcessTime(t.NextProcessAt)
|
||||
}},
|
||||
{"Payload", alignLeft, func(t *asynq.TaskInfo) string { return formatByteSlice(t.Payload) }},
|
||||
}
|
||||
|
||||
var retryTaskTableColumns = []*columnConfig[*asynq.TaskInfo]{
|
||||
{"ID", alignLeft, func(t *asynq.TaskInfo) string { return t.ID }},
|
||||
{"Type", alignLeft, func(t *asynq.TaskInfo) string { return t.Type }},
|
||||
{"Retry", alignRight, func(t *asynq.TaskInfo) string { return fmt.Sprintf("%d/%d", t.Retried, t.MaxRetry) }},
|
||||
{"Last Failure", alignLeft, func(t *asynq.TaskInfo) string { return t.LastErr }},
|
||||
{"Last Failure Time", alignLeft, func(t *asynq.TaskInfo) string { return formatPastTime(t.LastFailedAt) }},
|
||||
{"Next Process Time", alignLeft, func(t *asynq.TaskInfo) string {
|
||||
return formatNextProcessTime(t.NextProcessAt)
|
||||
}},
|
||||
{"Payload", alignLeft, func(t *asynq.TaskInfo) string { return formatByteSlice(t.Payload) }},
|
||||
}
|
||||
|
||||
var archivedTaskTableColumns = []*columnConfig[*asynq.TaskInfo]{
|
||||
{"ID", alignLeft, func(t *asynq.TaskInfo) string { return t.ID }},
|
||||
{"Type", alignLeft, func(t *asynq.TaskInfo) string { return t.Type }},
|
||||
{"Retry", alignRight, func(t *asynq.TaskInfo) string { return fmt.Sprintf("%d/%d", t.Retried, t.MaxRetry) }},
|
||||
{"Last Failure", alignLeft, func(t *asynq.TaskInfo) string { return t.LastErr }},
|
||||
{"Last Failure Time", alignLeft, func(t *asynq.TaskInfo) string { return formatPastTime(t.LastFailedAt) }},
|
||||
{"Payload", alignLeft, func(t *asynq.TaskInfo) string { return formatByteSlice(t.Payload) }},
|
||||
}
|
||||
|
||||
var completedTaskTableColumns = []*columnConfig[*asynq.TaskInfo]{
|
||||
{"ID", alignLeft, func(t *asynq.TaskInfo) string { return t.ID }},
|
||||
{"Type", alignLeft, func(t *asynq.TaskInfo) string { return t.Type }},
|
||||
{"Completion Time", alignLeft, func(t *asynq.TaskInfo) string { return formatPastTime(t.CompletedAt) }},
|
||||
{"Payload", alignLeft, func(t *asynq.TaskInfo) string { return formatByteSlice(t.Payload) }},
|
||||
{"Result", alignLeft, func(t *asynq.TaskInfo) string { return formatByteSlice(t.Result) }},
|
||||
}
|
||||
|
||||
func drawTaskTable(d *ScreenDrawer, state *State) {
|
||||
if shouldShowGroupTable(state) {
|
||||
drawGroupTable(d, state)
|
||||
return
|
||||
}
|
||||
if len(state.tasks) == 0 {
|
||||
return // print nothing
|
||||
}
|
||||
drawTable(d, baseStyle, getTaskTableColumnConfig(state.taskState), state.tasks, state.taskTableRowIdx-1)
|
||||
|
||||
// Pagination
|
||||
pageSize := taskPageSize(d.Screen())
|
||||
totalCount := getTaskCount(state.selectedQueue, state.taskState)
|
||||
if state.taskState == asynq.TaskStateAggregating {
|
||||
// aggregating tasks are scoped to each group when shown in the table.
|
||||
totalCount = state.selectedGroup.Size
|
||||
}
|
||||
if pageSize < totalCount {
|
||||
start := (state.pageNum-1)*pageSize + 1
|
||||
end := start + len(state.tasks) - 1
|
||||
paginationStyle := baseStyle.Foreground(tcell.ColorLightGray)
|
||||
d.Print(fmt.Sprintf("Showing %d-%d out of %d", start, end, totalCount), paginationStyle)
|
||||
if isNextTaskPageAvailable(d.Screen(), state) {
|
||||
d.Print(" n=NextPage", paginationStyle)
|
||||
}
|
||||
if state.pageNum > 1 {
|
||||
d.Print(" p=PrevPage", paginationStyle)
|
||||
}
|
||||
d.FillLine(' ', paginationStyle)
|
||||
}
|
||||
}
|
||||
|
||||
func isNextTaskPageAvailable(s tcell.Screen, state *State) bool {
|
||||
totalCount := getTaskCount(state.selectedQueue, state.taskState)
|
||||
end := (state.pageNum-1)*taskPageSize(s) + len(state.tasks)
|
||||
return end < totalCount
|
||||
}
|
||||
|
||||
func drawGroupTable(d *ScreenDrawer, state *State) {
|
||||
if len(state.groups) == 0 {
|
||||
return // print nothing
|
||||
}
|
||||
d.Println("<<< Select group >>>", baseStyle)
|
||||
colConfigs := []*columnConfig[*asynq.GroupInfo]{
|
||||
{"Name", alignLeft, func(g *asynq.GroupInfo) string { return g.Group }},
|
||||
{"Size", alignRight, func(g *asynq.GroupInfo) string { return strconv.Itoa(g.Size) }},
|
||||
}
|
||||
// pagination
|
||||
pageSize := groupPageSize(d.Screen())
|
||||
total := len(state.groups)
|
||||
start := (state.pageNum - 1) * pageSize
|
||||
end := min(start+pageSize, total)
|
||||
drawTable(d, baseStyle, colConfigs, state.groups[start:end], state.groupTableRowIdx-1)
|
||||
|
||||
if pageSize < total {
|
||||
d.Print(fmt.Sprintf("Showing %d-%d out of %d", start+1, end, total), labelStyle)
|
||||
if end < total {
|
||||
d.Print(" n=NextPage", labelStyle)
|
||||
}
|
||||
if start > 0 {
|
||||
d.Print(" p=PrevPage", labelStyle)
|
||||
}
|
||||
}
|
||||
d.FillLine(' ', labelStyle)
|
||||
}
|
||||
|
||||
type number interface {
|
||||
int | int64 | float64
|
||||
}
|
||||
|
||||
// min returns the smaller of x and y. if x==y, returns x
|
||||
func min[V number](x, y V) V {
|
||||
if x > y {
|
||||
return y
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// Define the order of states to show
|
||||
var taskStates = []asynq.TaskState{
|
||||
asynq.TaskStateActive,
|
||||
asynq.TaskStatePending,
|
||||
asynq.TaskStateAggregating,
|
||||
asynq.TaskStateScheduled,
|
||||
asynq.TaskStateRetry,
|
||||
asynq.TaskStateArchived,
|
||||
asynq.TaskStateCompleted,
|
||||
}
|
||||
|
||||
func nextTaskState(current asynq.TaskState) asynq.TaskState {
|
||||
for i, ts := range taskStates {
|
||||
if current == ts {
|
||||
if i == len(taskStates)-1 {
|
||||
return taskStates[0]
|
||||
} else {
|
||||
return taskStates[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
panic("unknown task state")
|
||||
}
|
||||
|
||||
func prevTaskState(current asynq.TaskState) asynq.TaskState {
|
||||
for i, ts := range taskStates {
|
||||
if current == ts {
|
||||
if i == 0 {
|
||||
return taskStates[len(taskStates)-1]
|
||||
} else {
|
||||
return taskStates[i-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
panic("unknown task state")
|
||||
}
|
||||
|
||||
func getTaskCount(queue *asynq.QueueInfo, taskState asynq.TaskState) int {
|
||||
switch taskState {
|
||||
case asynq.TaskStateActive:
|
||||
return queue.Active
|
||||
case asynq.TaskStatePending:
|
||||
return queue.Pending
|
||||
case asynq.TaskStateAggregating:
|
||||
return queue.Aggregating
|
||||
case asynq.TaskStateScheduled:
|
||||
return queue.Scheduled
|
||||
case asynq.TaskStateRetry:
|
||||
return queue.Retry
|
||||
case asynq.TaskStateArchived:
|
||||
return queue.Archived
|
||||
case asynq.TaskStateCompleted:
|
||||
return queue.Completed
|
||||
}
|
||||
panic("unkonwn task state")
|
||||
}
|
||||
|
||||
func drawTaskStateBreakdown(d *ScreenDrawer, style tcell.Style, state *State) {
|
||||
const pad = " " // padding between states
|
||||
for _, ts := range taskStates {
|
||||
s := style
|
||||
if state.taskState == ts {
|
||||
s = s.Bold(true).Underline(true)
|
||||
}
|
||||
d.Print(fmt.Sprintf("%s:%d", strings.Title(ts.String()), getTaskCount(state.selectedQueue, ts)), s)
|
||||
d.Print(pad, style)
|
||||
}
|
||||
d.NL()
|
||||
}
|
||||
|
||||
func drawTaskModal(d *ScreenDrawer, state *State) {
|
||||
if state.taskID == "" {
|
||||
return
|
||||
}
|
||||
task := state.selectedTask
|
||||
if task == nil {
|
||||
// task no longer found
|
||||
fns := []func(d *modalRowDrawer){
|
||||
func(d *modalRowDrawer) { d.Print("=== Task Info ===", baseStyle.Bold(true)) },
|
||||
func(d *modalRowDrawer) { d.Print("", baseStyle) },
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print(fmt.Sprintf("Task %q no longer exists", state.taskID), baseStyle)
|
||||
},
|
||||
}
|
||||
withModal(d, fns)
|
||||
return
|
||||
}
|
||||
fns := []func(d *modalRowDrawer){
|
||||
func(d *modalRowDrawer) { d.Print("=== Task Info ===", baseStyle.Bold(true)) },
|
||||
func(d *modalRowDrawer) { d.Print("", baseStyle) },
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print("ID: ", labelStyle)
|
||||
d.Print(task.ID, baseStyle)
|
||||
},
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print("Type: ", labelStyle)
|
||||
d.Print(task.Type, baseStyle)
|
||||
},
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print("State: ", labelStyle)
|
||||
d.Print(task.State.String(), baseStyle)
|
||||
},
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print("Queue: ", labelStyle)
|
||||
d.Print(task.Queue, baseStyle)
|
||||
},
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print("Retry: ", labelStyle)
|
||||
d.Print(fmt.Sprintf("%d/%d", task.Retried, task.MaxRetry), baseStyle)
|
||||
},
|
||||
}
|
||||
if task.LastErr != "" {
|
||||
fns = append(fns, func(d *modalRowDrawer) {
|
||||
d.Print("Last Failure: ", labelStyle)
|
||||
d.Print(task.LastErr, baseStyle)
|
||||
})
|
||||
fns = append(fns, func(d *modalRowDrawer) {
|
||||
d.Print("Last Failure Time: ", labelStyle)
|
||||
d.Print(fmt.Sprintf("%v (%s)", task.LastFailedAt, formatPastTime(task.LastFailedAt)), baseStyle)
|
||||
})
|
||||
}
|
||||
if !task.NextProcessAt.IsZero() {
|
||||
fns = append(fns, func(d *modalRowDrawer) {
|
||||
d.Print("Next Process Time: ", labelStyle)
|
||||
d.Print(fmt.Sprintf("%v (%s)", task.NextProcessAt, formatNextProcessTime(task.NextProcessAt)), baseStyle)
|
||||
})
|
||||
}
|
||||
if !task.CompletedAt.IsZero() {
|
||||
fns = append(fns, func(d *modalRowDrawer) {
|
||||
d.Print("Completion Time: ", labelStyle)
|
||||
d.Print(fmt.Sprintf("%v (%s)", task.CompletedAt, formatPastTime(task.CompletedAt)), baseStyle)
|
||||
})
|
||||
}
|
||||
fns = append(fns, func(d *modalRowDrawer) {
|
||||
d.Print("Payload: ", labelStyle)
|
||||
d.Print(formatByteSlice(task.Payload), baseStyle)
|
||||
})
|
||||
if task.Result != nil {
|
||||
fns = append(fns, func(d *modalRowDrawer) {
|
||||
d.Print("Result: ", labelStyle)
|
||||
d.Print(formatByteSlice(task.Result), baseStyle)
|
||||
})
|
||||
}
|
||||
withModal(d, fns)
|
||||
}
|
||||
|
||||
// Reports whether the given byte slice is printable (i.e. human readable)
|
||||
func isPrintable(data []byte) bool {
|
||||
if !utf8.Valid(data) {
|
||||
return false
|
||||
}
|
||||
isAllSpace := true
|
||||
for _, r := range string(data) {
|
||||
if !unicode.IsGraphic(r) {
|
||||
return false
|
||||
}
|
||||
if !unicode.IsSpace(r) {
|
||||
isAllSpace = false
|
||||
}
|
||||
}
|
||||
return !isAllSpace
|
||||
}
|
||||
|
||||
func formatByteSlice(data []byte) string {
|
||||
if data == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
if !isPrintable(data) {
|
||||
return "<non-printable>"
|
||||
}
|
||||
return strings.ReplaceAll(string(data), "\n", " ")
|
||||
}
|
||||
|
||||
type modalRowDrawer struct {
|
||||
d *ScreenDrawer
|
||||
width int // current width occupied by content
|
||||
maxWidth int
|
||||
}
|
||||
|
||||
// Note: s should not include newline
|
||||
func (d *modalRowDrawer) Print(s string, style tcell.Style) {
|
||||
if d.width >= d.maxWidth {
|
||||
return // no longer write to this row
|
||||
}
|
||||
if d.width+runewidth.StringWidth(s) > d.maxWidth {
|
||||
s = truncate(s, d.maxWidth-d.width)
|
||||
}
|
||||
d.d.Print(s, style)
|
||||
}
|
||||
|
||||
// withModal draws a modal with the given functions row by row.
|
||||
func withModal(d *ScreenDrawer, rowPrintFns []func(d *modalRowDrawer)) {
|
||||
w, h := d.Screen().Size()
|
||||
var (
|
||||
modalWidth = int(math.Floor(float64(w) * 0.6))
|
||||
modalHeight = int(math.Floor(float64(h) * 0.6))
|
||||
rowOffset = int(math.Floor(float64(h) * 0.2)) // 20% from the top
|
||||
colOffset = int(math.Floor(float64(w) * 0.2)) // 20% from the left
|
||||
)
|
||||
if modalHeight < 3 {
|
||||
return // no content can be shown
|
||||
}
|
||||
d.Goto(colOffset, rowOffset)
|
||||
d.Print(string(tcell.RuneULCorner), baseStyle)
|
||||
d.Print(strings.Repeat(string(tcell.RuneHLine), modalWidth-2), baseStyle)
|
||||
d.Print(string(tcell.RuneURCorner), baseStyle)
|
||||
d.NL()
|
||||
rowDrawer := modalRowDrawer{
|
||||
d: d,
|
||||
width: 0,
|
||||
maxWidth: modalWidth - 4, /* borders + paddings */
|
||||
}
|
||||
for i := 1; i < modalHeight-1; i++ {
|
||||
d.Goto(colOffset, rowOffset+i)
|
||||
d.Print(fmt.Sprintf("%c ", tcell.RuneVLine), baseStyle)
|
||||
if i <= len(rowPrintFns) {
|
||||
rowPrintFns[i-1](&rowDrawer)
|
||||
}
|
||||
d.FillUntil(' ', baseStyle, colOffset+modalWidth-2)
|
||||
d.Print(fmt.Sprintf(" %c", tcell.RuneVLine), baseStyle)
|
||||
d.NL()
|
||||
}
|
||||
d.Goto(colOffset, rowOffset+modalHeight-1)
|
||||
d.Print(string(tcell.RuneLLCorner), baseStyle)
|
||||
d.Print(strings.Repeat(string(tcell.RuneHLine), modalWidth-2), baseStyle)
|
||||
d.Print(string(tcell.RuneLRCorner), baseStyle)
|
||||
d.NL()
|
||||
}
|
||||
|
||||
func adjustWidth(s string, width int) string {
|
||||
sw := runewidth.StringWidth(s)
|
||||
if sw > width {
|
||||
return truncate(s, width)
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(s)
|
||||
b.WriteString(strings.Repeat(" ", width-sw))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// truncates s if s exceeds max length.
|
||||
func truncate(s string, max int) string {
|
||||
if runewidth.StringWidth(s) <= max {
|
||||
return s
|
||||
}
|
||||
return string([]rune(s)[:max-1]) + "…"
|
||||
}
|
||||
|
||||
func drawDebugInfo(d *ScreenDrawer, state *State) {
|
||||
d.Println(state.DebugString(), baseStyle)
|
||||
}
|
||||
|
||||
func drawHelp(d *ScreenDrawer) {
|
||||
keyStyle := labelStyle.Bold(true)
|
||||
withModal(d, []func(*modalRowDrawer){
|
||||
func(d *modalRowDrawer) { d.Print("=== Help ===", baseStyle.Bold(true)) },
|
||||
func(d *modalRowDrawer) { d.Print("", baseStyle) },
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print("<Enter>", keyStyle)
|
||||
d.Print(" to select", baseStyle)
|
||||
},
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print("<Esc>", keyStyle)
|
||||
d.Print(" or ", baseStyle)
|
||||
d.Print("<q>", keyStyle)
|
||||
d.Print(" to go back", baseStyle)
|
||||
},
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print("<UpArrow>", keyStyle)
|
||||
d.Print(" or ", baseStyle)
|
||||
d.Print("<k>", keyStyle)
|
||||
d.Print(" to move up", baseStyle)
|
||||
},
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print("<DownArrow>", keyStyle)
|
||||
d.Print(" or ", baseStyle)
|
||||
d.Print("<j>", keyStyle)
|
||||
d.Print(" to move down", baseStyle)
|
||||
},
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print("<LeftArrow>", keyStyle)
|
||||
d.Print(" or ", baseStyle)
|
||||
d.Print("<h>", keyStyle)
|
||||
d.Print(" to move left", baseStyle)
|
||||
},
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print("<RightArrow>", keyStyle)
|
||||
d.Print(" or ", baseStyle)
|
||||
d.Print("<l>", keyStyle)
|
||||
d.Print(" to move right", baseStyle)
|
||||
},
|
||||
func(d *modalRowDrawer) {
|
||||
d.Print("<Ctrl+C>", keyStyle)
|
||||
d.Print(" to quit", baseStyle)
|
||||
},
|
||||
})
|
||||
}
|
33
tools/asynq/cmd/dash/draw_test.go
Normal file
33
tools/asynq/cmd/dash/draw_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright 2022 Kentaro Hibino. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package dash
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTruncate(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
max int
|
||||
want string
|
||||
}{
|
||||
{
|
||||
s: "hello world!",
|
||||
max: 15,
|
||||
want: "hello world!",
|
||||
},
|
||||
{
|
||||
s: "hello world!",
|
||||
max: 6,
|
||||
want: "hello…",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
got := truncate(tc.s, tc.max)
|
||||
if tc.want != got {
|
||||
t.Errorf("truncate(%q, %d) = %q, want %q", tc.s, tc.max, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
185
tools/asynq/cmd/dash/fetch.go
Normal file
185
tools/asynq/cmd/dash/fetch.go
Normal file
@ -0,0 +1,185 @@
|
||||
// Copyright 2022 Kentaro Hibino. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package dash
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
type fetcher interface {
|
||||
// Fetch retries data required by the given state of the dashboard.
|
||||
Fetch(state *State)
|
||||
}
|
||||
|
||||
type dataFetcher struct {
|
||||
inspector *asynq.Inspector
|
||||
opts Options
|
||||
s tcell.Screen
|
||||
|
||||
errorCh chan<- error
|
||||
queueCh chan<- *asynq.QueueInfo
|
||||
taskCh chan<- *asynq.TaskInfo
|
||||
queuesCh chan<- []*asynq.QueueInfo
|
||||
groupsCh chan<- []*asynq.GroupInfo
|
||||
tasksCh chan<- []*asynq.TaskInfo
|
||||
}
|
||||
|
||||
func (f *dataFetcher) Fetch(state *State) {
|
||||
switch state.view {
|
||||
case viewTypeQueues:
|
||||
f.fetchQueues()
|
||||
case viewTypeQueueDetails:
|
||||
if shouldShowGroupTable(state) {
|
||||
f.fetchGroups(state.selectedQueue.Queue)
|
||||
} else if state.taskState == asynq.TaskStateAggregating {
|
||||
f.fetchAggregatingTasks(state.selectedQueue.Queue, state.selectedGroup.Group, taskPageSize(f.s), state.pageNum)
|
||||
} else {
|
||||
f.fetchTasks(state.selectedQueue.Queue, state.taskState, taskPageSize(f.s), state.pageNum)
|
||||
}
|
||||
// if the task modal is open, additionally fetch the selected task's info
|
||||
if state.taskID != "" {
|
||||
f.fetchTaskInfo(state.selectedQueue.Queue, state.taskID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *dataFetcher) fetchQueues() {
|
||||
var (
|
||||
inspector = f.inspector
|
||||
queuesCh = f.queuesCh
|
||||
errorCh = f.errorCh
|
||||
opts = f.opts
|
||||
)
|
||||
go fetchQueues(inspector, queuesCh, errorCh, opts)
|
||||
}
|
||||
|
||||
func fetchQueues(i *asynq.Inspector, queuesCh chan<- []*asynq.QueueInfo, errorCh chan<- error, opts Options) {
|
||||
queues, err := i.Queues()
|
||||
if err != nil {
|
||||
errorCh <- err
|
||||
return
|
||||
}
|
||||
sort.Strings(queues)
|
||||
var res []*asynq.QueueInfo
|
||||
for _, q := range queues {
|
||||
info, err := i.GetQueueInfo(q)
|
||||
if err != nil {
|
||||
errorCh <- err
|
||||
return
|
||||
}
|
||||
res = append(res, info)
|
||||
}
|
||||
queuesCh <- res
|
||||
}
|
||||
|
||||
func fetchQueueInfo(i *asynq.Inspector, qname string, queueCh chan<- *asynq.QueueInfo, errorCh chan<- error) {
|
||||
q, err := i.GetQueueInfo(qname)
|
||||
if err != nil {
|
||||
errorCh <- err
|
||||
return
|
||||
}
|
||||
queueCh <- q
|
||||
}
|
||||
|
||||
func (f *dataFetcher) fetchGroups(qname string) {
|
||||
var (
|
||||
i = f.inspector
|
||||
groupsCh = f.groupsCh
|
||||
errorCh = f.errorCh
|
||||
queueCh = f.queueCh
|
||||
)
|
||||
go fetchGroups(i, qname, groupsCh, errorCh)
|
||||
go fetchQueueInfo(i, qname, queueCh, errorCh)
|
||||
}
|
||||
|
||||
func fetchGroups(i *asynq.Inspector, qname string, groupsCh chan<- []*asynq.GroupInfo, errorCh chan<- error) {
|
||||
groups, err := i.Groups(qname)
|
||||
if err != nil {
|
||||
errorCh <- err
|
||||
return
|
||||
}
|
||||
groupsCh <- groups
|
||||
}
|
||||
|
||||
func (f *dataFetcher) fetchAggregatingTasks(qname, group string, pageSize, pageNum int) {
|
||||
var (
|
||||
i = f.inspector
|
||||
tasksCh = f.tasksCh
|
||||
errorCh = f.errorCh
|
||||
queueCh = f.queueCh
|
||||
)
|
||||
go fetchAggregatingTasks(i, qname, group, pageSize, pageNum, tasksCh, errorCh)
|
||||
go fetchQueueInfo(i, qname, queueCh, errorCh)
|
||||
}
|
||||
|
||||
func fetchAggregatingTasks(i *asynq.Inspector, qname, group string, pageSize, pageNum int,
|
||||
tasksCh chan<- []*asynq.TaskInfo, errorCh chan<- error) {
|
||||
tasks, err := i.ListAggregatingTasks(qname, group, asynq.PageSize(pageSize), asynq.Page(pageNum))
|
||||
if err != nil {
|
||||
errorCh <- err
|
||||
return
|
||||
}
|
||||
tasksCh <- tasks
|
||||
}
|
||||
|
||||
func (f *dataFetcher) fetchTasks(qname string, taskState asynq.TaskState, pageSize, pageNum int) {
|
||||
var (
|
||||
i = f.inspector
|
||||
tasksCh = f.tasksCh
|
||||
errorCh = f.errorCh
|
||||
queueCh = f.queueCh
|
||||
)
|
||||
go fetchTasks(i, qname, taskState, pageSize, pageNum, tasksCh, errorCh)
|
||||
go fetchQueueInfo(i, qname, queueCh, errorCh)
|
||||
}
|
||||
|
||||
func fetchTasks(i *asynq.Inspector, qname string, taskState asynq.TaskState, pageSize, pageNum int,
|
||||
tasksCh chan<- []*asynq.TaskInfo, errorCh chan<- error) {
|
||||
var (
|
||||
tasks []*asynq.TaskInfo
|
||||
err error
|
||||
)
|
||||
opts := []asynq.ListOption{asynq.PageSize(pageSize), asynq.Page(pageNum)}
|
||||
switch taskState {
|
||||
case asynq.TaskStateActive:
|
||||
tasks, err = i.ListActiveTasks(qname, opts...)
|
||||
case asynq.TaskStatePending:
|
||||
tasks, err = i.ListPendingTasks(qname, opts...)
|
||||
case asynq.TaskStateScheduled:
|
||||
tasks, err = i.ListScheduledTasks(qname, opts...)
|
||||
case asynq.TaskStateRetry:
|
||||
tasks, err = i.ListRetryTasks(qname, opts...)
|
||||
case asynq.TaskStateArchived:
|
||||
tasks, err = i.ListArchivedTasks(qname, opts...)
|
||||
case asynq.TaskStateCompleted:
|
||||
tasks, err = i.ListCompletedTasks(qname, opts...)
|
||||
}
|
||||
if err != nil {
|
||||
errorCh <- err
|
||||
return
|
||||
}
|
||||
tasksCh <- tasks
|
||||
}
|
||||
|
||||
func (f *dataFetcher) fetchTaskInfo(qname, taskID string) {
|
||||
var (
|
||||
i = f.inspector
|
||||
taskCh = f.taskCh
|
||||
errorCh = f.errorCh
|
||||
)
|
||||
go fetchTaskInfo(i, qname, taskID, taskCh, errorCh)
|
||||
}
|
||||
|
||||
func fetchTaskInfo(i *asynq.Inspector, qname, taskID string, taskCh chan<- *asynq.TaskInfo, errorCh chan<- error) {
|
||||
info, err := i.GetTaskInfo(qname, taskID)
|
||||
if err != nil {
|
||||
errorCh <- err
|
||||
return
|
||||
}
|
||||
taskCh <- info
|
||||
}
|
317
tools/asynq/cmd/dash/key_event.go
Normal file
317
tools/asynq/cmd/dash/key_event.go
Normal file
@ -0,0 +1,317 @@
|
||||
// Copyright 2022 Kentaro Hibino. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package dash
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
// keyEventHandler handles keyboard events and updates the state.
|
||||
// It delegates data fetching to fetcher and UI rendering to drawer.
|
||||
type keyEventHandler struct {
|
||||
s tcell.Screen
|
||||
state *State
|
||||
done chan struct{}
|
||||
|
||||
fetcher fetcher
|
||||
drawer drawer
|
||||
|
||||
ticker *time.Ticker
|
||||
pollInterval time.Duration
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) quit() {
|
||||
h.s.Fini()
|
||||
close(h.done)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) HandleKeyEvent(ev *tcell.EventKey) {
|
||||
if ev.Key() == tcell.KeyEscape || ev.Rune() == 'q' {
|
||||
h.goBack() // Esc and 'q' key have "go back" semantics
|
||||
} else if ev.Key() == tcell.KeyCtrlC {
|
||||
h.quit()
|
||||
} else if ev.Key() == tcell.KeyCtrlL {
|
||||
h.s.Sync()
|
||||
} else if ev.Key() == tcell.KeyDown || ev.Rune() == 'j' {
|
||||
h.handleDownKey()
|
||||
} else if ev.Key() == tcell.KeyUp || ev.Rune() == 'k' {
|
||||
h.handleUpKey()
|
||||
} else if ev.Key() == tcell.KeyRight || ev.Rune() == 'l' {
|
||||
h.handleRightKey()
|
||||
} else if ev.Key() == tcell.KeyLeft || ev.Rune() == 'h' {
|
||||
h.handleLeftKey()
|
||||
} else if ev.Key() == tcell.KeyEnter {
|
||||
h.handleEnterKey()
|
||||
} else if ev.Rune() == '?' {
|
||||
h.showHelp()
|
||||
} else if ev.Rune() == 'n' {
|
||||
h.nextPage()
|
||||
} else if ev.Rune() == 'p' {
|
||||
h.prevPage()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) goBack() {
|
||||
var (
|
||||
state = h.state
|
||||
d = h.drawer
|
||||
f = h.fetcher
|
||||
)
|
||||
if state.view == viewTypeHelp {
|
||||
state.view = state.prevView // exit help
|
||||
f.Fetch(state)
|
||||
h.resetTicker()
|
||||
d.Draw(state)
|
||||
} else if state.view == viewTypeQueueDetails {
|
||||
// if task modal is open close it; otherwise go back to the previous view
|
||||
if state.taskID != "" {
|
||||
state.taskID = ""
|
||||
state.selectedTask = nil
|
||||
d.Draw(state)
|
||||
} else {
|
||||
state.view = viewTypeQueues
|
||||
f.Fetch(state)
|
||||
h.resetTicker()
|
||||
d.Draw(state)
|
||||
}
|
||||
} else {
|
||||
h.quit()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) handleDownKey() {
|
||||
switch h.state.view {
|
||||
case viewTypeQueues:
|
||||
h.downKeyQueues()
|
||||
case viewTypeQueueDetails:
|
||||
h.downKeyQueueDetails()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) downKeyQueues() {
|
||||
if h.state.queueTableRowIdx < len(h.state.queues) {
|
||||
h.state.queueTableRowIdx++
|
||||
} else {
|
||||
h.state.queueTableRowIdx = 0 // loop back
|
||||
}
|
||||
h.drawer.Draw(h.state)
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) downKeyQueueDetails() {
|
||||
s, state := h.s, h.state
|
||||
if shouldShowGroupTable(state) {
|
||||
if state.groupTableRowIdx < groupPageSize(s) {
|
||||
state.groupTableRowIdx++
|
||||
} else {
|
||||
state.groupTableRowIdx = 0 // loop back
|
||||
}
|
||||
} else if state.taskID == "" {
|
||||
if state.taskTableRowIdx < len(state.tasks) {
|
||||
state.taskTableRowIdx++
|
||||
} else {
|
||||
state.taskTableRowIdx = 0 // loop back
|
||||
}
|
||||
}
|
||||
h.drawer.Draw(state)
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) handleUpKey() {
|
||||
switch h.state.view {
|
||||
case viewTypeQueues:
|
||||
h.upKeyQueues()
|
||||
case viewTypeQueueDetails:
|
||||
h.upKeyQueueDetails()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) upKeyQueues() {
|
||||
state := h.state
|
||||
if state.queueTableRowIdx == 0 {
|
||||
state.queueTableRowIdx = len(state.queues)
|
||||
} else {
|
||||
state.queueTableRowIdx--
|
||||
}
|
||||
h.drawer.Draw(state)
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) upKeyQueueDetails() {
|
||||
s, state := h.s, h.state
|
||||
if shouldShowGroupTable(state) {
|
||||
if state.groupTableRowIdx == 0 {
|
||||
state.groupTableRowIdx = groupPageSize(s)
|
||||
} else {
|
||||
state.groupTableRowIdx--
|
||||
}
|
||||
} else if state.taskID == "" {
|
||||
if state.taskTableRowIdx == 0 {
|
||||
state.taskTableRowIdx = len(state.tasks)
|
||||
} else {
|
||||
state.taskTableRowIdx--
|
||||
}
|
||||
}
|
||||
h.drawer.Draw(state)
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) handleEnterKey() {
|
||||
switch h.state.view {
|
||||
case viewTypeQueues:
|
||||
h.enterKeyQueues()
|
||||
case viewTypeQueueDetails:
|
||||
h.enterKeyQueueDetails()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) resetTicker() {
|
||||
h.ticker.Reset(h.pollInterval)
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) enterKeyQueues() {
|
||||
var (
|
||||
state = h.state
|
||||
f = h.fetcher
|
||||
d = h.drawer
|
||||
)
|
||||
if state.queueTableRowIdx != 0 {
|
||||
state.selectedQueue = state.queues[state.queueTableRowIdx-1]
|
||||
state.view = viewTypeQueueDetails
|
||||
state.taskState = asynq.TaskStateActive
|
||||
state.tasks = nil
|
||||
state.pageNum = 1
|
||||
f.Fetch(state)
|
||||
h.resetTicker()
|
||||
d.Draw(state)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) enterKeyQueueDetails() {
|
||||
var (
|
||||
state = h.state
|
||||
f = h.fetcher
|
||||
d = h.drawer
|
||||
)
|
||||
if shouldShowGroupTable(state) && state.groupTableRowIdx != 0 {
|
||||
state.selectedGroup = state.groups[state.groupTableRowIdx-1]
|
||||
state.tasks = nil
|
||||
state.pageNum = 1
|
||||
f.Fetch(state)
|
||||
h.resetTicker()
|
||||
d.Draw(state)
|
||||
} else if !shouldShowGroupTable(state) && state.taskTableRowIdx != 0 {
|
||||
task := state.tasks[state.taskTableRowIdx-1]
|
||||
state.selectedTask = task
|
||||
state.taskID = task.ID
|
||||
f.Fetch(state)
|
||||
h.resetTicker()
|
||||
d.Draw(state)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) handleLeftKey() {
|
||||
var (
|
||||
state = h.state
|
||||
f = h.fetcher
|
||||
d = h.drawer
|
||||
)
|
||||
if state.view == viewTypeQueueDetails && state.taskID == "" {
|
||||
state.taskState = prevTaskState(state.taskState)
|
||||
state.pageNum = 1
|
||||
state.taskTableRowIdx = 0
|
||||
state.tasks = nil
|
||||
state.selectedGroup = nil
|
||||
f.Fetch(state)
|
||||
h.resetTicker()
|
||||
d.Draw(state)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) handleRightKey() {
|
||||
var (
|
||||
state = h.state
|
||||
f = h.fetcher
|
||||
d = h.drawer
|
||||
)
|
||||
if state.view == viewTypeQueueDetails && state.taskID == "" {
|
||||
state.taskState = nextTaskState(state.taskState)
|
||||
state.pageNum = 1
|
||||
state.taskTableRowIdx = 0
|
||||
state.tasks = nil
|
||||
state.selectedGroup = nil
|
||||
f.Fetch(state)
|
||||
h.resetTicker()
|
||||
d.Draw(state)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) nextPage() {
|
||||
var (
|
||||
s = h.s
|
||||
state = h.state
|
||||
f = h.fetcher
|
||||
d = h.drawer
|
||||
)
|
||||
if state.view == viewTypeQueueDetails {
|
||||
if shouldShowGroupTable(state) {
|
||||
pageSize := groupPageSize(s)
|
||||
total := len(state.groups)
|
||||
start := (state.pageNum - 1) * pageSize
|
||||
end := start + pageSize
|
||||
if end <= total {
|
||||
state.pageNum++
|
||||
d.Draw(state)
|
||||
}
|
||||
} else {
|
||||
pageSize := taskPageSize(s)
|
||||
totalCount := getTaskCount(state.selectedQueue, state.taskState)
|
||||
if (state.pageNum-1)*pageSize+len(state.tasks) < totalCount {
|
||||
state.pageNum++
|
||||
f.Fetch(state)
|
||||
h.resetTicker()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) prevPage() {
|
||||
var (
|
||||
s = h.s
|
||||
state = h.state
|
||||
f = h.fetcher
|
||||
d = h.drawer
|
||||
)
|
||||
if state.view == viewTypeQueueDetails {
|
||||
if shouldShowGroupTable(state) {
|
||||
pageSize := groupPageSize(s)
|
||||
start := (state.pageNum - 1) * pageSize
|
||||
if start > 0 {
|
||||
state.pageNum--
|
||||
d.Draw(state)
|
||||
}
|
||||
} else {
|
||||
if state.pageNum > 1 {
|
||||
state.pageNum--
|
||||
f.Fetch(state)
|
||||
h.resetTicker()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *keyEventHandler) showHelp() {
|
||||
var (
|
||||
state = h.state
|
||||
d = h.drawer
|
||||
)
|
||||
if state.view != viewTypeHelp {
|
||||
state.prevView = state.view
|
||||
state.view = viewTypeHelp
|
||||
d.Draw(state)
|
||||
}
|
||||
}
|
234
tools/asynq/cmd/dash/key_event_test.go
Normal file
234
tools/asynq/cmd/dash/key_event_test.go
Normal file
@ -0,0 +1,234 @@
|
||||
// Copyright 2022 Kentaro Hibino. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package dash
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
func makeKeyEventHandler(t *testing.T, state *State) *keyEventHandler {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
t.Cleanup(func() { ticker.Stop() })
|
||||
return &keyEventHandler{
|
||||
s: tcell.NewSimulationScreen("UTF-8"),
|
||||
state: state,
|
||||
done: make(chan struct{}),
|
||||
fetcher: &fakeFetcher{},
|
||||
drawer: &fakeDrawer{},
|
||||
ticker: ticker,
|
||||
pollInterval: time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
type keyEventHandlerTest struct {
|
||||
desc string // test description
|
||||
state *State // initial state, to be mutated by the handler
|
||||
events []*tcell.EventKey // keyboard events
|
||||
wantState State // expected state after the events
|
||||
}
|
||||
|
||||
func TestKeyEventHandler(t *testing.T) {
|
||||
tests := []*keyEventHandlerTest{
|
||||
{
|
||||
desc: "navigates to help view",
|
||||
state: &State{view: viewTypeQueues},
|
||||
events: []*tcell.EventKey{tcell.NewEventKey(tcell.KeyRune, '?', tcell.ModNone)},
|
||||
wantState: State{view: viewTypeHelp},
|
||||
},
|
||||
{
|
||||
desc: "navigates to queue details view",
|
||||
state: &State{
|
||||
view: viewTypeQueues,
|
||||
queues: []*asynq.QueueInfo{
|
||||
{Queue: "default", Size: 100, Active: 10, Pending: 40, Scheduled: 40, Completed: 10},
|
||||
},
|
||||
queueTableRowIdx: 0,
|
||||
},
|
||||
events: []*tcell.EventKey{
|
||||
tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone), // down
|
||||
tcell.NewEventKey(tcell.KeyEnter, '\n', tcell.ModNone), // Enter
|
||||
},
|
||||
wantState: State{
|
||||
view: viewTypeQueueDetails,
|
||||
queues: []*asynq.QueueInfo{
|
||||
{Queue: "default", Size: 100, Active: 10, Pending: 40, Scheduled: 40, Completed: 10},
|
||||
},
|
||||
selectedQueue: &asynq.QueueInfo{Queue: "default", Size: 100, Active: 10, Pending: 40, Scheduled: 40, Completed: 10},
|
||||
queueTableRowIdx: 1,
|
||||
taskState: asynq.TaskStateActive,
|
||||
pageNum: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "does nothing if no queues are present",
|
||||
state: &State{
|
||||
view: viewTypeQueues,
|
||||
queues: []*asynq.QueueInfo{}, // empty
|
||||
queueTableRowIdx: 0,
|
||||
},
|
||||
events: []*tcell.EventKey{
|
||||
tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone), // down
|
||||
tcell.NewEventKey(tcell.KeyEnter, '\n', tcell.ModNone), // Enter
|
||||
},
|
||||
wantState: State{
|
||||
view: viewTypeQueues,
|
||||
queues: []*asynq.QueueInfo{},
|
||||
queueTableRowIdx: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "opens task info modal",
|
||||
state: &State{
|
||||
view: viewTypeQueueDetails,
|
||||
queues: []*asynq.QueueInfo{
|
||||
{Queue: "default", Size: 500, Active: 10, Pending: 40},
|
||||
},
|
||||
queueTableRowIdx: 1,
|
||||
selectedQueue: &asynq.QueueInfo{Queue: "default", Size: 50, Active: 10, Pending: 40},
|
||||
taskState: asynq.TaskStatePending,
|
||||
pageNum: 1,
|
||||
tasks: []*asynq.TaskInfo{
|
||||
{ID: "xxxx", Type: "foo"},
|
||||
{ID: "yyyy", Type: "bar"},
|
||||
{ID: "zzzz", Type: "baz"},
|
||||
},
|
||||
taskTableRowIdx: 2,
|
||||
},
|
||||
events: []*tcell.EventKey{
|
||||
tcell.NewEventKey(tcell.KeyEnter, '\n', tcell.ModNone), // Enter
|
||||
},
|
||||
wantState: State{
|
||||
view: viewTypeQueueDetails,
|
||||
queues: []*asynq.QueueInfo{
|
||||
{Queue: "default", Size: 500, Active: 10, Pending: 40},
|
||||
},
|
||||
queueTableRowIdx: 1,
|
||||
selectedQueue: &asynq.QueueInfo{Queue: "default", Size: 50, Active: 10, Pending: 40},
|
||||
taskState: asynq.TaskStatePending,
|
||||
pageNum: 1,
|
||||
tasks: []*asynq.TaskInfo{
|
||||
{ID: "xxxx", Type: "foo"},
|
||||
{ID: "yyyy", Type: "bar"},
|
||||
{ID: "zzzz", Type: "baz"},
|
||||
},
|
||||
taskTableRowIdx: 2,
|
||||
// new states
|
||||
taskID: "yyyy",
|
||||
selectedTask: &asynq.TaskInfo{ID: "yyyy", Type: "bar"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Esc closes task info modal",
|
||||
state: &State{
|
||||
view: viewTypeQueueDetails,
|
||||
queues: []*asynq.QueueInfo{
|
||||
{Queue: "default", Size: 500, Active: 10, Pending: 40},
|
||||
},
|
||||
queueTableRowIdx: 1,
|
||||
selectedQueue: &asynq.QueueInfo{Queue: "default", Size: 50, Active: 10, Pending: 40},
|
||||
taskState: asynq.TaskStatePending,
|
||||
pageNum: 1,
|
||||
tasks: []*asynq.TaskInfo{
|
||||
{ID: "xxxx", Type: "foo"},
|
||||
{ID: "yyyy", Type: "bar"},
|
||||
{ID: "zzzz", Type: "baz"},
|
||||
},
|
||||
taskTableRowIdx: 2,
|
||||
taskID: "yyyy", // presence of this field opens the modal
|
||||
},
|
||||
events: []*tcell.EventKey{
|
||||
tcell.NewEventKey(tcell.KeyEscape, ' ', tcell.ModNone), // Esc
|
||||
},
|
||||
wantState: State{
|
||||
view: viewTypeQueueDetails,
|
||||
queues: []*asynq.QueueInfo{
|
||||
{Queue: "default", Size: 500, Active: 10, Pending: 40},
|
||||
},
|
||||
queueTableRowIdx: 1,
|
||||
selectedQueue: &asynq.QueueInfo{Queue: "default", Size: 50, Active: 10, Pending: 40},
|
||||
taskState: asynq.TaskStatePending,
|
||||
pageNum: 1,
|
||||
tasks: []*asynq.TaskInfo{
|
||||
{ID: "xxxx", Type: "foo"},
|
||||
{ID: "yyyy", Type: "bar"},
|
||||
{ID: "zzzz", Type: "baz"},
|
||||
},
|
||||
taskTableRowIdx: 2,
|
||||
taskID: "", // this field should be unset
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Arrow keys are disabled while task info modal is open",
|
||||
state: &State{
|
||||
view: viewTypeQueueDetails,
|
||||
queues: []*asynq.QueueInfo{
|
||||
{Queue: "default", Size: 500, Active: 10, Pending: 40},
|
||||
},
|
||||
queueTableRowIdx: 1,
|
||||
selectedQueue: &asynq.QueueInfo{Queue: "default", Size: 50, Active: 10, Pending: 40},
|
||||
taskState: asynq.TaskStatePending,
|
||||
pageNum: 1,
|
||||
tasks: []*asynq.TaskInfo{
|
||||
{ID: "xxxx", Type: "foo"},
|
||||
{ID: "yyyy", Type: "bar"},
|
||||
{ID: "zzzz", Type: "baz"},
|
||||
},
|
||||
taskTableRowIdx: 2,
|
||||
taskID: "yyyy", // presence of this field opens the modal
|
||||
},
|
||||
events: []*tcell.EventKey{
|
||||
tcell.NewEventKey(tcell.KeyLeft, ' ', tcell.ModNone),
|
||||
},
|
||||
|
||||
// no change
|
||||
wantState: State{
|
||||
view: viewTypeQueueDetails,
|
||||
queues: []*asynq.QueueInfo{
|
||||
{Queue: "default", Size: 500, Active: 10, Pending: 40},
|
||||
},
|
||||
queueTableRowIdx: 1,
|
||||
selectedQueue: &asynq.QueueInfo{Queue: "default", Size: 50, Active: 10, Pending: 40},
|
||||
taskState: asynq.TaskStatePending,
|
||||
pageNum: 1,
|
||||
tasks: []*asynq.TaskInfo{
|
||||
{ID: "xxxx", Type: "foo"},
|
||||
{ID: "yyyy", Type: "bar"},
|
||||
{ID: "zzzz", Type: "baz"},
|
||||
},
|
||||
taskTableRowIdx: 2,
|
||||
taskID: "yyyy", // presence of this field opens the modal
|
||||
},
|
||||
},
|
||||
// TODO: Add more tests
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
h := makeKeyEventHandler(t, tc.state)
|
||||
for _, e := range tc.events {
|
||||
h.HandleKeyEvent(e)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantState, *tc.state, cmp.AllowUnexported(State{})); diff != "" {
|
||||
t.Errorf("after state was %+v, want %+v: (-want,+got)\n%s", *tc.state, tc.wantState, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*** fake implementation for tests ***/
|
||||
|
||||
type fakeFetcher struct{}
|
||||
|
||||
func (f *fakeFetcher) Fetch(s *State) {}
|
||||
|
||||
type fakeDrawer struct{}
|
||||
|
||||
func (d *fakeDrawer) Draw(s *State) {}
|
100
tools/asynq/cmd/dash/screen_drawer.go
Normal file
100
tools/asynq/cmd/dash/screen_drawer.go
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright 2022 Kentaro Hibino. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package dash
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
/*** Screen Drawer ***/
|
||||
|
||||
// ScreenDrawer is used to draw contents on screen.
|
||||
//
|
||||
// Usage example:
|
||||
// d := NewScreenDrawer(s)
|
||||
// d.Println("Hello world", mystyle)
|
||||
// d.NL() // adds newline
|
||||
// d.Print("foo", mystyle.Bold(true))
|
||||
// d.Print("bar", mystyle.Italic(true))
|
||||
type ScreenDrawer struct {
|
||||
l *LineDrawer
|
||||
}
|
||||
|
||||
func NewScreenDrawer(s tcell.Screen) *ScreenDrawer {
|
||||
return &ScreenDrawer{l: NewLineDrawer(0, s)}
|
||||
}
|
||||
|
||||
func (d *ScreenDrawer) Print(s string, style tcell.Style) {
|
||||
d.l.Draw(s, style)
|
||||
}
|
||||
|
||||
func (d *ScreenDrawer) Println(s string, style tcell.Style) {
|
||||
d.Print(s, style)
|
||||
d.NL()
|
||||
}
|
||||
|
||||
// FillLine prints the given rune until the end of the current line
|
||||
// and adds a newline.
|
||||
func (d *ScreenDrawer) FillLine(r rune, style tcell.Style) {
|
||||
w, _ := d.Screen().Size()
|
||||
if w-d.l.col < 0 {
|
||||
d.NL()
|
||||
return
|
||||
}
|
||||
s := strings.Repeat(string(r), w-d.l.col)
|
||||
d.Print(s, style)
|
||||
d.NL()
|
||||
}
|
||||
|
||||
func (d *ScreenDrawer) FillUntil(r rune, style tcell.Style, limit int) {
|
||||
if d.l.col > limit {
|
||||
return // already passed the limit
|
||||
}
|
||||
s := strings.Repeat(string(r), limit-d.l.col)
|
||||
d.Print(s, style)
|
||||
}
|
||||
|
||||
// NL adds a newline (i.e., moves to the next line).
|
||||
func (d *ScreenDrawer) NL() {
|
||||
d.l.row++
|
||||
d.l.col = 0
|
||||
}
|
||||
|
||||
func (d *ScreenDrawer) Screen() tcell.Screen {
|
||||
return d.l.s
|
||||
}
|
||||
|
||||
// Goto moves the screendrawer to the specified cell.
|
||||
func (d *ScreenDrawer) Goto(x, y int) {
|
||||
d.l.row = y
|
||||
d.l.col = x
|
||||
}
|
||||
|
||||
// Go to the bottom of the screen.
|
||||
func (d *ScreenDrawer) GoToBottom() {
|
||||
_, h := d.Screen().Size()
|
||||
d.l.row = h - 1
|
||||
d.l.col = 0
|
||||
}
|
||||
|
||||
type LineDrawer struct {
|
||||
s tcell.Screen
|
||||
row int
|
||||
col int
|
||||
}
|
||||
|
||||
func NewLineDrawer(row int, s tcell.Screen) *LineDrawer {
|
||||
return &LineDrawer{row: row, col: 0, s: s}
|
||||
}
|
||||
|
||||
func (d *LineDrawer) Draw(s string, style tcell.Style) {
|
||||
for _, r := range s {
|
||||
d.s.SetContent(d.col, d.row, r, nil, style)
|
||||
d.col += runewidth.RuneWidth(r)
|
||||
}
|
||||
}
|
70
tools/asynq/cmd/dash/table.go
Normal file
70
tools/asynq/cmd/dash/table.go
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright 2022 Kentaro Hibino. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package dash
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
type columnAlignment int
|
||||
|
||||
const (
|
||||
alignRight columnAlignment = iota
|
||||
alignLeft
|
||||
)
|
||||
|
||||
type columnConfig[V any] struct {
|
||||
name string
|
||||
alignment columnAlignment
|
||||
displayFn func(v V) string
|
||||
}
|
||||
|
||||
type column[V any] struct {
|
||||
*columnConfig[V]
|
||||
width int
|
||||
}
|
||||
|
||||
// Helper to draw a table.
|
||||
func drawTable[V any](d *ScreenDrawer, style tcell.Style, configs []*columnConfig[V], data []V, highlightRowIdx int) {
|
||||
const colBuffer = " " // extra buffer between columns
|
||||
cols := make([]*column[V], len(configs))
|
||||
for i, cfg := range configs {
|
||||
cols[i] = &column[V]{cfg, runewidth.StringWidth(cfg.name)}
|
||||
}
|
||||
// adjust the column width to accommodate the widest value.
|
||||
for _, v := range data {
|
||||
for _, col := range cols {
|
||||
if w := runewidth.StringWidth(col.displayFn(v)); col.width < w {
|
||||
col.width = w
|
||||
}
|
||||
}
|
||||
}
|
||||
// print header
|
||||
headerStyle := style.Background(tcell.ColorDimGray).Foreground(tcell.ColorWhite)
|
||||
for _, col := range cols {
|
||||
if col.alignment == alignLeft {
|
||||
d.Print(rpad(col.name, col.width)+colBuffer, headerStyle)
|
||||
} else {
|
||||
d.Print(lpad(col.name, col.width)+colBuffer, headerStyle)
|
||||
}
|
||||
}
|
||||
d.FillLine(' ', headerStyle)
|
||||
// print body
|
||||
for i, v := range data {
|
||||
rowStyle := style
|
||||
if highlightRowIdx == i {
|
||||
rowStyle = style.Background(tcell.ColorDarkOliveGreen)
|
||||
}
|
||||
for _, col := range cols {
|
||||
if col.alignment == alignLeft {
|
||||
d.Print(rpad(col.displayFn(v), col.width)+colBuffer, rowStyle)
|
||||
} else {
|
||||
d.Print(lpad(col.displayFn(v), col.width)+colBuffer, rowStyle)
|
||||
}
|
||||
}
|
||||
d.FillLine(' ', rowStyle)
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -19,15 +20,18 @@ func init() {
|
||||
}
|
||||
|
||||
var groupCmd = &cobra.Command{
|
||||
Use: "group",
|
||||
Use: "group <command> [flags]",
|
||||
Short: "Manage groups",
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq group list --queue=myqueue`),
|
||||
}
|
||||
|
||||
var groupListCmd = &cobra.Command{
|
||||
Use: "ls",
|
||||
Short: "List groups",
|
||||
Args: cobra.NoArgs,
|
||||
Run: groupLists,
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List groups",
|
||||
Args: cobra.NoArgs,
|
||||
Run: groupLists,
|
||||
}
|
||||
|
||||
func groupLists(cmd *cobra.Command, args []string) {
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/fatih/color"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/hibiken/asynq/internal/errors"
|
||||
@ -31,51 +32,75 @@ func init() {
|
||||
}
|
||||
|
||||
var queueCmd = &cobra.Command{
|
||||
Use: "queue",
|
||||
Use: "queue <command> [flags]",
|
||||
Short: "Manage queues",
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq queue ls
|
||||
$ asynq queue inspect myqueue
|
||||
$ asynq queue pause myqueue`),
|
||||
}
|
||||
|
||||
var queueListCmd = &cobra.Command{
|
||||
Use: "ls",
|
||||
Short: "List queues",
|
||||
Use: "list",
|
||||
Short: "List queues",
|
||||
Aliases: []string{"ls"},
|
||||
// TODO: Use RunE instead?
|
||||
Run: queueList,
|
||||
}
|
||||
|
||||
var queueInspectCmd = &cobra.Command{
|
||||
Use: "inspect QUEUE [QUEUE...]",
|
||||
Use: "inspect <queue> [<queue>...]",
|
||||
Short: "Display detailed information on one or more queues",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
// TODO: Use RunE instead?
|
||||
Run: queueInspect,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq queue inspect myqueue
|
||||
$ asynq queue inspect queue1 queue2 queue3`),
|
||||
}
|
||||
|
||||
var queueHistoryCmd = &cobra.Command{
|
||||
Use: "history QUEUE [QUEUE...]",
|
||||
Use: "history <queue> [<queue>...]",
|
||||
Short: "Display historical aggregate data from one or more queues",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: queueHistory,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq queue history myqueue
|
||||
$ asynq queue history queue1 queue2 queue3
|
||||
$ asynq queue history myqueue --days=90`),
|
||||
}
|
||||
|
||||
var queuePauseCmd = &cobra.Command{
|
||||
Use: "pause QUEUE [QUEUE...]",
|
||||
Use: "pause <queue> [<queue>...]",
|
||||
Short: "Pause one or more queues",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: queuePause,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq queue pause myqueue
|
||||
$ asynq queue pause queue1 queue2 queue3`),
|
||||
}
|
||||
|
||||
var queueUnpauseCmd = &cobra.Command{
|
||||
Use: "unpause QUEUE [QUEUE...]",
|
||||
Short: "Unpause one or more queues",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: queueUnpause,
|
||||
Use: "resume <queue> [<queue>...]",
|
||||
Short: "Resume (unpause) one or more queues",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Aliases: []string{"unpause"},
|
||||
Run: queueUnpause,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq queue resume myqueue
|
||||
$ asynq queue resume queue1 queue2 queue3`),
|
||||
}
|
||||
|
||||
var queueRemoveCmd = &cobra.Command{
|
||||
Use: "rm QUEUE [QUEUE...]",
|
||||
Short: "Remove one or more queues",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: queueRemove,
|
||||
Use: "remove <queue> [<queue>...]",
|
||||
Short: "Remove one or more queues",
|
||||
Aliases: []string{"rm", "delete"},
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: queueRemove,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq queue rm myqueue
|
||||
$ asynq queue rm queue1 queue2 queue3
|
||||
$ asynq queue rm myqueue --force`),
|
||||
}
|
||||
|
||||
func queueList(cmd *cobra.Command, args []string) {
|
||||
|
@ -11,14 +11,19 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/fatih/color"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
"github.com/hibiken/asynq/internal/rdb"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/exp/utf8string"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/viper"
|
||||
@ -35,14 +40,27 @@ var (
|
||||
useRedisCluster bool
|
||||
clusterAddrs string
|
||||
tlsServerName string
|
||||
insecure bool
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "asynq",
|
||||
Short: "A monitoring tool for asynq queues",
|
||||
Long: `Asynq is a montoring CLI to inspect tasks and queues managed by asynq.`,
|
||||
Use: "asynq <command> <subcommand> [flags]",
|
||||
Short: "Asynq CLI",
|
||||
Long: `Command line tool to inspect tasks and queues managed by Asynq`,
|
||||
Version: base.Version,
|
||||
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq stats
|
||||
$ asynq queue pause myqueue
|
||||
$ asynq task list --queue=myqueue --state=archived`),
|
||||
Annotations: map[string]string{
|
||||
"help:feedback": heredoc.Doc(`
|
||||
Open an issue at https://github.com/hibiken/asynq/issues/new/choose`),
|
||||
},
|
||||
}
|
||||
|
||||
var versionOutput = fmt.Sprintf("asynq version %s\n", base.Version)
|
||||
@ -64,22 +82,241 @@ func Execute() {
|
||||
}
|
||||
}
|
||||
|
||||
func isRootCmd(cmd *cobra.Command) bool {
|
||||
return cmd != nil && !cmd.HasParent()
|
||||
}
|
||||
|
||||
// displayLine represents a line displayed in the output as '<name> <desc>',
|
||||
// where pad is used to pad the name from desc.
|
||||
type displayLine struct {
|
||||
name string
|
||||
desc string
|
||||
pad int // number of rpad
|
||||
}
|
||||
|
||||
func (l *displayLine) String() string {
|
||||
return rpad(l.name, l.pad) + l.desc
|
||||
}
|
||||
|
||||
type displayLines []*displayLine
|
||||
|
||||
func (dls displayLines) String() string {
|
||||
var lines []string
|
||||
for _, dl := range dls {
|
||||
lines = append(lines, dl.String())
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// Capitalize the first word in the given string.
|
||||
func capitalize(s string) string {
|
||||
str := utf8string.NewString(s)
|
||||
if str.RuneCount() == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(strings.ToUpper(string(str.At(0))))
|
||||
b.WriteString(str.Slice(1, str.RuneCount()))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func rootHelpFunc(cmd *cobra.Command, args []string) {
|
||||
// Display helpful error message when user mistypes a subcommand (e.g. 'asynq queue lst').
|
||||
if isRootCmd(cmd.Parent()) && len(args) >= 2 && args[1] != "--help" && args[1] != "-h" {
|
||||
printSubcommandSuggestions(cmd, args[1])
|
||||
return
|
||||
}
|
||||
|
||||
var lines []*displayLine
|
||||
var commands []*displayLine
|
||||
for _, c := range cmd.Commands() {
|
||||
if c.Hidden || c.Short == "" || c.Name() == "help" {
|
||||
continue
|
||||
}
|
||||
l := &displayLine{name: c.Name() + ":", desc: capitalize(c.Short)}
|
||||
commands = append(commands, l)
|
||||
lines = append(lines, l)
|
||||
}
|
||||
var localFlags []*displayLine
|
||||
cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
|
||||
l := &displayLine{name: "--" + f.Name, desc: capitalize(f.Usage)}
|
||||
localFlags = append(localFlags, l)
|
||||
lines = append(lines, l)
|
||||
})
|
||||
var inheritedFlags []*displayLine
|
||||
cmd.InheritedFlags().VisitAll(func(f *pflag.Flag) {
|
||||
l := &displayLine{name: "--" + f.Name, desc: capitalize(f.Usage)}
|
||||
inheritedFlags = append(inheritedFlags, l)
|
||||
lines = append(lines, l)
|
||||
})
|
||||
adjustPadding(lines...)
|
||||
|
||||
type helpEntry struct {
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
var helpEntries []*helpEntry
|
||||
desc := cmd.Long
|
||||
if desc == "" {
|
||||
desc = cmd.Short
|
||||
}
|
||||
if desc != "" {
|
||||
helpEntries = append(helpEntries, &helpEntry{"", desc})
|
||||
}
|
||||
helpEntries = append(helpEntries, &helpEntry{"USAGE", cmd.UseLine()})
|
||||
if len(commands) > 0 {
|
||||
helpEntries = append(helpEntries, &helpEntry{"COMMANDS", displayLines(commands).String()})
|
||||
}
|
||||
if cmd.LocalFlags().HasFlags() {
|
||||
helpEntries = append(helpEntries, &helpEntry{"FLAGS", displayLines(localFlags).String()})
|
||||
}
|
||||
if cmd.InheritedFlags().HasFlags() {
|
||||
helpEntries = append(helpEntries, &helpEntry{"INHERITED FLAGS", displayLines(inheritedFlags).String()})
|
||||
}
|
||||
if cmd.Example != "" {
|
||||
helpEntries = append(helpEntries, &helpEntry{"EXAMPLES", cmd.Example})
|
||||
}
|
||||
helpEntries = append(helpEntries, &helpEntry{"LEARN MORE", heredoc.Doc(`
|
||||
Use 'asynq <command> <subcommand> --help' for more information about a command.`)})
|
||||
if s, ok := cmd.Annotations["help:feedback"]; ok {
|
||||
helpEntries = append(helpEntries, &helpEntry{"FEEDBACK", s})
|
||||
}
|
||||
|
||||
out := cmd.OutOrStdout()
|
||||
bold := color.New(color.Bold)
|
||||
for _, e := range helpEntries {
|
||||
if e.Title != "" {
|
||||
// If there is a title, add indentation to each line in the body
|
||||
bold.Fprintln(out, e.Title)
|
||||
fmt.Fprintln(out, indent(e.Body, 2 /* spaces */))
|
||||
} else {
|
||||
// If there is no title, print the body as is
|
||||
fmt.Fprintln(out, e.Body)
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
}
|
||||
|
||||
func rootUsageFunc(cmd *cobra.Command) error {
|
||||
out := cmd.OutOrStdout()
|
||||
fmt.Fprintf(out, "Usage: %s", cmd.UseLine())
|
||||
if subcmds := cmd.Commands(); len(subcmds) > 0 {
|
||||
fmt.Fprint(out, "\n\nAvailable commands:\n")
|
||||
for _, c := range subcmds {
|
||||
if c.Hidden {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(out, " %s\n", c.Name())
|
||||
}
|
||||
}
|
||||
|
||||
var localFlags []*displayLine
|
||||
cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
|
||||
localFlags = append(localFlags, &displayLine{name: "--" + f.Name, desc: capitalize(f.Usage)})
|
||||
})
|
||||
adjustPadding(localFlags...)
|
||||
if len(localFlags) > 0 {
|
||||
fmt.Fprint(out, "\n\nFlags:\n")
|
||||
for _, l := range localFlags {
|
||||
fmt.Fprintf(out, " %s\n", l.String())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printSubcommandSuggestions(cmd *cobra.Command, arg string) {
|
||||
out := cmd.OutOrStdout()
|
||||
fmt.Fprintf(out, "unknown command %q for %q\n", arg, cmd.CommandPath())
|
||||
if cmd.SuggestionsMinimumDistance <= 0 {
|
||||
cmd.SuggestionsMinimumDistance = 2
|
||||
}
|
||||
candidates := cmd.SuggestionsFor(arg)
|
||||
if len(candidates) > 0 {
|
||||
fmt.Fprint(out, "\nDid you mean this?\n")
|
||||
for _, c := range candidates {
|
||||
fmt.Fprintf(out, "\t%s\n", c)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
rootUsageFunc(cmd)
|
||||
}
|
||||
|
||||
func adjustPadding(lines ...*displayLine) {
|
||||
// find the maximum width of the name
|
||||
max := 0
|
||||
for _, l := range lines {
|
||||
if n := utf8.RuneCountInString(l.name); n > max {
|
||||
max = n
|
||||
}
|
||||
}
|
||||
for _, l := range lines {
|
||||
l.pad = max
|
||||
}
|
||||
}
|
||||
|
||||
// rpad adds padding to the right of a string.
|
||||
func rpad(s string, padding int) string {
|
||||
tmpl := fmt.Sprintf("%%-%ds ", padding)
|
||||
return fmt.Sprintf(tmpl, s)
|
||||
|
||||
}
|
||||
|
||||
// lpad adds padding to the left of a string.
|
||||
func lpad(s string, padding int) string {
|
||||
tmpl := fmt.Sprintf("%%%ds ", padding)
|
||||
return fmt.Sprintf(tmpl, s)
|
||||
}
|
||||
|
||||
// indent indents the given text by given spaces.
|
||||
func indent(text string, space int) string {
|
||||
if len(text) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
indentation := strings.Repeat(" ", space)
|
||||
lastRune := '\n'
|
||||
for _, r := range text {
|
||||
if lastRune == '\n' {
|
||||
b.WriteString(indentation)
|
||||
}
|
||||
b.WriteRune(r)
|
||||
lastRune = r
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// dedent removes any indentation from the given text.
|
||||
func dedent(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
var b strings.Builder
|
||||
for _, l := range lines {
|
||||
b.WriteString(strings.TrimLeftFunc(l, unicode.IsSpace))
|
||||
b.WriteRune('\n')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
rootCmd.SetHelpFunc(rootHelpFunc)
|
||||
rootCmd.SetUsageFunc(rootUsageFunc)
|
||||
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.SetVersionTemplate(versionOutput)
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file to set flag defaut values (default is $HOME/.asynq.yaml)")
|
||||
rootCmd.PersistentFlags().StringVarP(&uri, "uri", "u", "127.0.0.1:6379", "redis server URI")
|
||||
rootCmd.PersistentFlags().IntVarP(&db, "db", "n", 0, "redis database number (default is 0)")
|
||||
rootCmd.PersistentFlags().StringVarP(&password, "password", "p", "", "password to use when connecting to redis server")
|
||||
rootCmd.PersistentFlags().BoolVar(&useRedisCluster, "cluster", false, "connect to redis cluster")
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "Config file to set flag defaut values (default is $HOME/.asynq.yaml)")
|
||||
rootCmd.PersistentFlags().StringVarP(&uri, "uri", "u", "127.0.0.1:6379", "Redis server URI")
|
||||
rootCmd.PersistentFlags().IntVarP(&db, "db", "n", 0, "Redis database number (default is 0)")
|
||||
rootCmd.PersistentFlags().StringVarP(&password, "password", "p", "", "Password to use when connecting to redis server")
|
||||
rootCmd.PersistentFlags().BoolVar(&useRedisCluster, "cluster", false, "Connect to redis cluster")
|
||||
rootCmd.PersistentFlags().StringVar(&clusterAddrs, "cluster_addrs",
|
||||
"127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005",
|
||||
"list of comma-separated redis server addresses")
|
||||
"List of comma-separated redis server addresses")
|
||||
rootCmd.PersistentFlags().StringVar(&tlsServerName, "tls_server",
|
||||
"", "server name for TLS validation")
|
||||
"", "Server name for TLS validation")
|
||||
rootCmd.PersistentFlags().BoolVar(&insecure, "insecure",
|
||||
false, "Allow insecure TLS connection by skipping cert validation")
|
||||
// Bind flags with config.
|
||||
viper.BindPFlag("uri", rootCmd.PersistentFlags().Lookup("uri"))
|
||||
viper.BindPFlag("db", rootCmd.PersistentFlags().Lookup("db"))
|
||||
@ -87,6 +324,7 @@ func init() {
|
||||
viper.BindPFlag("cluster", rootCmd.PersistentFlags().Lookup("cluster"))
|
||||
viper.BindPFlag("cluster_addrs", rootCmd.PersistentFlags().Lookup("cluster_addrs"))
|
||||
viper.BindPFlag("tls_server", rootCmd.PersistentFlags().Lookup("tls_server"))
|
||||
viper.BindPFlag("insecure", rootCmd.PersistentFlags().Lookup("insecure"))
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
@ -118,7 +356,7 @@ func initConfig() {
|
||||
// createRDB creates a RDB instance using flag values and returns it.
|
||||
func createRDB() *rdb.RDB {
|
||||
var c redis.UniversalClient
|
||||
if useRedisCluster {
|
||||
if viper.GetBool("cluster") {
|
||||
addrs := strings.Split(viper.GetString("cluster_addrs"), ",")
|
||||
c = redis.NewClusterClient(&redis.ClusterOptions{
|
||||
Addrs: addrs,
|
||||
@ -136,13 +374,18 @@ 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())
|
||||
}
|
||||
|
||||
func getRedisConnOpt() asynq.RedisConnOpt {
|
||||
if useRedisCluster {
|
||||
if viper.GetBool("cluster") {
|
||||
addrs := strings.Split(viper.GetString("cluster_addrs"), ",")
|
||||
return asynq.RedisClusterClientOpt{
|
||||
Addrs: addrs,
|
||||
@ -163,7 +406,7 @@ func getTLSConfig() *tls.Config {
|
||||
if tlsServer == "" {
|
||||
return nil
|
||||
}
|
||||
return &tls.Config{ServerName: tlsServer}
|
||||
return &tls.Config{ServerName: tlsServer, InsecureSkipVerify: viper.GetBool("insecure")}
|
||||
}
|
||||
|
||||
// printTable is a helper function to print data in table format.
|
||||
@ -223,3 +466,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
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -21,13 +22,16 @@ func init() {
|
||||
}
|
||||
|
||||
var serverCmd = &cobra.Command{
|
||||
Use: "server",
|
||||
Use: "server <command> [flags]",
|
||||
Short: "Manage servers",
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq server list`),
|
||||
}
|
||||
|
||||
var serverListCmd = &cobra.Command{
|
||||
Use: "ls",
|
||||
Short: "List servers",
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List servers",
|
||||
Long: `Server list (asynq server ls) shows all running worker servers
|
||||
pulling tasks from the given redis instance.
|
||||
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/fatih/color"
|
||||
"github.com/hibiken/asynq/internal/rdb"
|
||||
"github.com/spf13/cobra"
|
||||
@ -24,19 +25,15 @@ import (
|
||||
// statsCmd represents the stats command
|
||||
var statsCmd = &cobra.Command{
|
||||
Use: "stats",
|
||||
Short: "Shows current state of the tasks and queues",
|
||||
Long: `Stats (aysnq stats) will show the overview of tasks and queues at that instant.
|
||||
Short: "View current state",
|
||||
Long: heredoc.Doc(`
|
||||
Stats shows the overview of tasks and queues at that instant.
|
||||
|
||||
Specifically, the command shows the following:
|
||||
* Number of tasks in each state
|
||||
* Number of tasks in each queue
|
||||
* Aggregate data for the current day
|
||||
* Basic information about the running redis instance
|
||||
|
||||
To monitor the tasks continuously, it's recommended that you run this
|
||||
command in conjunction with the watch command.
|
||||
|
||||
Example: watch -n 3 asynq stats -> Shows current state of tasks every three seconds`,
|
||||
The command shows the following:
|
||||
* Number of tasks in each state
|
||||
* Number of tasks in each queue
|
||||
* Aggregate data for the current day
|
||||
* Basic information about the running redis instance`),
|
||||
Args: cobra.NoArgs,
|
||||
Run: stats,
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/fatih/color"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/spf13/cobra"
|
||||
@ -18,8 +19,8 @@ import (
|
||||
func init() {
|
||||
rootCmd.AddCommand(taskCmd)
|
||||
taskCmd.AddCommand(taskListCmd)
|
||||
taskListCmd.Flags().StringP("queue", "q", "", "queue to inspect")
|
||||
taskListCmd.Flags().StringP("state", "s", "", "state of the tasks to inspect")
|
||||
taskListCmd.Flags().StringP("queue", "q", "", "queue to inspect (required)")
|
||||
taskListCmd.Flags().StringP("state", "s", "", "state of the tasks; one of { active | pending | aggregating | scheduled | retry | archived | completed } (required)")
|
||||
taskListCmd.Flags().Int("page", 1, "page number")
|
||||
taskListCmd.Flags().Int("size", 30, "page size")
|
||||
taskListCmd.Flags().StringP("group", "g", "", "group to inspect (required for listing aggregating tasks)")
|
||||
@ -29,141 +30,183 @@ func init() {
|
||||
taskCmd.AddCommand(taskCancelCmd)
|
||||
|
||||
taskCmd.AddCommand(taskInspectCmd)
|
||||
taskInspectCmd.Flags().StringP("queue", "q", "", "queue to which the task belongs")
|
||||
taskInspectCmd.Flags().StringP("id", "i", "", "id of the task")
|
||||
taskInspectCmd.Flags().StringP("queue", "q", "", "queue to which the task belongs (required)")
|
||||
taskInspectCmd.Flags().StringP("id", "i", "", "id of the task (required)")
|
||||
taskInspectCmd.MarkFlagRequired("queue")
|
||||
taskInspectCmd.MarkFlagRequired("id")
|
||||
|
||||
taskCmd.AddCommand(taskArchiveCmd)
|
||||
taskArchiveCmd.Flags().StringP("queue", "q", "", "queue to which the task belongs")
|
||||
taskArchiveCmd.Flags().StringP("id", "i", "", "id of the task")
|
||||
taskArchiveCmd.Flags().StringP("queue", "q", "", "queue to which the task belongs (required)")
|
||||
taskArchiveCmd.Flags().StringP("id", "i", "", "id of the task (required)")
|
||||
taskArchiveCmd.MarkFlagRequired("queue")
|
||||
taskArchiveCmd.MarkFlagRequired("id")
|
||||
|
||||
taskCmd.AddCommand(taskDeleteCmd)
|
||||
taskDeleteCmd.Flags().StringP("queue", "q", "", "queue to which the task belongs")
|
||||
taskDeleteCmd.Flags().StringP("id", "i", "", "id of the task")
|
||||
taskDeleteCmd.Flags().StringP("queue", "q", "", "queue to which the task belongs (required)")
|
||||
taskDeleteCmd.Flags().StringP("id", "i", "", "id of the task (required)")
|
||||
taskDeleteCmd.MarkFlagRequired("queue")
|
||||
taskDeleteCmd.MarkFlagRequired("id")
|
||||
|
||||
taskCmd.AddCommand(taskRunCmd)
|
||||
taskRunCmd.Flags().StringP("queue", "q", "", "queue to which the task belongs")
|
||||
taskRunCmd.Flags().StringP("id", "i", "", "id of the task")
|
||||
taskRunCmd.Flags().StringP("queue", "q", "", "queue to which the task belongs (required)")
|
||||
taskRunCmd.Flags().StringP("id", "i", "", "id of the task (required)")
|
||||
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")
|
||||
taskArchiveAllCmd.Flags().StringP("state", "s", "", "state of the tasks")
|
||||
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)")
|
||||
taskArchiveAllCmd.MarkFlagRequired("queue")
|
||||
taskArchiveAllCmd.MarkFlagRequired("state")
|
||||
taskArchiveAllCmd.Flags().StringP("group", "g", "", "group to which the tasks belong (required for archiving aggregating tasks)")
|
||||
|
||||
taskCmd.AddCommand(taskDeleteAllCmd)
|
||||
taskDeleteAllCmd.Flags().StringP("queue", "q", "", "queue to which the tasks belong")
|
||||
taskDeleteAllCmd.Flags().StringP("state", "s", "", "state of the tasks")
|
||||
taskDeleteAllCmd.Flags().StringP("queue", "q", "", "queue to which the tasks belong (required)")
|
||||
taskDeleteAllCmd.Flags().StringP("state", "s", "", "state of the tasks; one of { pending | aggregating | scheduled | retry | archived | completed } (required)")
|
||||
taskDeleteAllCmd.MarkFlagRequired("queue")
|
||||
taskDeleteAllCmd.MarkFlagRequired("state")
|
||||
taskDeleteAllCmd.Flags().StringP("group", "g", "", "group to which the tasks belong (required for deleting aggregating tasks)")
|
||||
|
||||
taskCmd.AddCommand(taskRunAllCmd)
|
||||
taskRunAllCmd.Flags().StringP("queue", "q", "", "queue to which the tasks belong")
|
||||
taskRunAllCmd.Flags().StringP("state", "s", "", "state of the tasks")
|
||||
taskRunAllCmd.Flags().StringP("queue", "q", "", "queue to which the tasks belong (required)")
|
||||
taskRunAllCmd.Flags().StringP("state", "s", "", "state of the tasks; one of { scheduled | retry | archived } (required)")
|
||||
taskRunAllCmd.MarkFlagRequired("queue")
|
||||
taskRunAllCmd.MarkFlagRequired("state")
|
||||
taskRunAllCmd.Flags().StringP("group", "g", "", "group to which the tasks belong (required for running aggregating tasks)")
|
||||
}
|
||||
|
||||
var taskCmd = &cobra.Command{
|
||||
Use: "task",
|
||||
Use: "task <command> [flags]",
|
||||
Short: "Manage tasks",
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq task list --queue=myqueue --state=scheduled
|
||||
$ asynq task inspect --queue=myqueue --id=7837f142-6337-4217-9276-8f27281b67d1
|
||||
$ asynq task delete --queue=myqueue --id=7837f142-6337-4217-9276-8f27281b67d1
|
||||
$ asynq task deleteall --queue=myqueue --state=archived`),
|
||||
}
|
||||
|
||||
var taskListCmd = &cobra.Command{
|
||||
Use: "ls --queue=QUEUE --state=STATE",
|
||||
Short: "List tasks",
|
||||
Long: `List tasks of the given state from the specified queue.
|
||||
Use: "list --queue=<queue> --state=<state> [flags]",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List tasks",
|
||||
Long: heredoc.Doc(`
|
||||
List tasks of the given state from the specified queue.
|
||||
|
||||
The value for the state flag should be one of:
|
||||
- active
|
||||
- pending
|
||||
- aggregating
|
||||
- scheduled
|
||||
- retry
|
||||
- archived
|
||||
- completed
|
||||
The --queue and --state flags are required.
|
||||
|
||||
List opeartion paginates the result set.
|
||||
By default, the command fetches the first 30 tasks.
|
||||
Use --page and --size flags to specify the page number and size.
|
||||
Note: For aggregating tasks, additional --group flag is required.
|
||||
|
||||
|
||||
Example:
|
||||
To list pending tasks from "default" queue, run
|
||||
asynq task ls --queue=default --state=pending
|
||||
|
||||
To list the tasks from the second page, run
|
||||
asynq task ls --queue=default --state=pending --page=1
|
||||
|
||||
For aggregating tasks, additional --group flag is required.
|
||||
|
||||
Example:
|
||||
asynq task ls --queue=default --state=aggregating --group=mygroup
|
||||
`,
|
||||
List opeartion paginates the result set. By default, the command fetches the first 30 tasks.
|
||||
Use --page and --size flags to specify the page number and size.`),
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq task list --queue=myqueue --state=pending
|
||||
$ asynq task list --queue=myqueue --state=aggregating --group=mygroup
|
||||
$ asynq task list --queue=myqueue --state=scheduled --page=2`),
|
||||
Run: taskList,
|
||||
}
|
||||
|
||||
var taskInspectCmd = &cobra.Command{
|
||||
Use: "inspect --queue=QUEUE --id=TASK_ID",
|
||||
Use: "inspect --queue=<queue> --id=<task_id>",
|
||||
Short: "Display detailed information on the specified task",
|
||||
Args: cobra.NoArgs,
|
||||
Run: taskInspect,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq task inspect --queue=myqueue --id=f1720682-f5a6-4db1-8953-4f48ae541d0f`),
|
||||
}
|
||||
|
||||
var taskCancelCmd = &cobra.Command{
|
||||
Use: "cancel TASK_ID [TASK_ID...]",
|
||||
Use: "cancel <task_id> [<task_id>...]",
|
||||
Short: "Cancel one or more active tasks",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: taskCancel,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq task cancel f1720682-f5a6-4db1-8953-4f48ae541d0f`),
|
||||
}
|
||||
|
||||
var taskArchiveCmd = &cobra.Command{
|
||||
Use: "archive --queue=QUEUE --id=TASK_ID",
|
||||
Use: "archive --queue=<queue> --id=<task_id>",
|
||||
Short: "Archive a task with the given id",
|
||||
Args: cobra.NoArgs,
|
||||
Run: taskArchive,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq task archive --queue=myqueue --id=f1720682-f5a6-4db1-8953-4f48ae541d0f`),
|
||||
}
|
||||
|
||||
var taskDeleteCmd = &cobra.Command{
|
||||
Use: "delete --queue=QUEUE --id=TASK_ID",
|
||||
Short: "Delete a task with the given id",
|
||||
Args: cobra.NoArgs,
|
||||
Run: taskDelete,
|
||||
Use: "delete --queue=<queue> --id=<task_id>",
|
||||
Aliases: []string{"remove", "rm"},
|
||||
Short: "Delete a task with the given id",
|
||||
Args: cobra.NoArgs,
|
||||
Run: taskDelete,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq task delete --queue=myqueue --id=f1720682-f5a6-4db1-8953-4f48ae541d0f`),
|
||||
}
|
||||
|
||||
var taskRunCmd = &cobra.Command{
|
||||
Use: "run --queue=QUEUE --id=TASK_ID",
|
||||
Use: "run --queue=<queue> --id=<task_id>",
|
||||
Short: "Run a task with the given id",
|
||||
Args: cobra.NoArgs,
|
||||
Run: taskRun,
|
||||
Example: heredoc.Doc(`
|
||||
$ 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",
|
||||
Use: "archiveall --queue=<queue> --state=<state>",
|
||||
Short: "Archive all tasks in the given state",
|
||||
Args: cobra.NoArgs,
|
||||
Run: taskArchiveAll,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq task archiveall --queue=myqueue --state=retry
|
||||
$ asynq task archiveall --queue=myqueue --state=aggregating --group=mygroup`),
|
||||
}
|
||||
|
||||
var taskDeleteAllCmd = &cobra.Command{
|
||||
Use: "deleteall --queue=QUEUE --state=STATE",
|
||||
Use: "deleteall --queue=<queue> --state=<state>",
|
||||
Short: "Delete all tasks in the given state",
|
||||
Args: cobra.NoArgs,
|
||||
Run: taskDeleteAll,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq task deleteall --queue=myqueue --state=archived
|
||||
$ asynq task deleteall --queue=myqueue --state=aggregating --group=mygroup`),
|
||||
}
|
||||
|
||||
var taskRunAllCmd = &cobra.Command{
|
||||
Use: "runall --queue=QUEUE --state=STATE",
|
||||
Use: "runall --queue=<queue> --state=<state>",
|
||||
Short: "Run all tasks in the given state",
|
||||
Args: cobra.NoArgs,
|
||||
Run: taskRunAll,
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq task runall --queue=myqueue --state=retry
|
||||
$ asynq task runall --queue=myqueue --state=aggregating --group=mygroup`),
|
||||
}
|
||||
|
||||
func taskList(cmd *cobra.Command, args []string) {
|
||||
@ -506,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 {
|
||||
@ -527,6 +659,17 @@ func taskArchiveAll(cmd *cobra.Command, args []string) {
|
||||
n, err = i.ArchiveAllScheduledTasks(qname)
|
||||
case "retry":
|
||||
n, err = i.ArchiveAllRetryTasks(qname)
|
||||
case "aggregating":
|
||||
group, err := cmd.Flags().GetString("group")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if group == "" {
|
||||
fmt.Println("error: Flag --group is required for aggregating tasks")
|
||||
os.Exit(1)
|
||||
}
|
||||
n, err = i.ArchiveAllAggregatingTasks(qname, group)
|
||||
default:
|
||||
fmt.Printf("error: unsupported state %q\n", state)
|
||||
os.Exit(1)
|
||||
@ -563,6 +706,17 @@ func taskDeleteAll(cmd *cobra.Command, args []string) {
|
||||
n, err = i.DeleteAllArchivedTasks(qname)
|
||||
case "completed":
|
||||
n, err = i.DeleteAllCompletedTasks(qname)
|
||||
case "aggregating":
|
||||
group, err := cmd.Flags().GetString("group")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if group == "" {
|
||||
fmt.Println("error: Flag --group is required for aggregating tasks")
|
||||
os.Exit(1)
|
||||
}
|
||||
n, err = i.DeleteAllAggregatingTasks(qname, group)
|
||||
default:
|
||||
fmt.Printf("error: unsupported state %q\n", state)
|
||||
os.Exit(1)
|
||||
@ -595,6 +749,17 @@ func taskRunAll(cmd *cobra.Command, args []string) {
|
||||
n, err = i.RunAllRetryTasks(qname)
|
||||
case "archived":
|
||||
n, err = i.RunAllArchivedTasks(qname)
|
||||
case "aggregating":
|
||||
group, err := cmd.Flags().GetString("group")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if group == "" {
|
||||
fmt.Println("error: Flag --group is required for aggregating tasks")
|
||||
os.Exit(1)
|
||||
}
|
||||
n, err = i.RunAllAggregatingTasks(qname, group)
|
||||
default:
|
||||
fmt.Printf("error: unsupported state %q\n", state)
|
||||
os.Exit(1)
|
||||
@ -605,3 +770,4 @@ func taskRunAll(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
fmt.Printf("%d tasks are now pending\n", n)
|
||||
}
|
||||
|
||||
|
52
tools/go.mod
52
tools/go.mod
@ -1,17 +1,55 @@
|
||||
module github.com/hibiken/asynq/tools
|
||||
|
||||
go 1.13
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/go-redis/redis/v8 v8.11.4
|
||||
github.com/hibiken/asynq v0.21.0
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/gdamore/tcell/v2 v2.5.1
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/hibiken/asynq v0.25.0
|
||||
github.com/hibiken/asynq/x v0.0.0-20220131170841-349f4c50fb1d
|
||||
github.com/mattn/go-runewidth v0.0.16
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/spf13/afero v1.1.2 // indirect
|
||||
github.com/prometheus/client_golang v1.11.1
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/spf13/cobra v1.1.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.7.0
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136
|
||||
)
|
||||
|
||||
replace github.com/hibiken/asynq => ../
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
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.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
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.26.0 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/spf13/afero v1.1.2 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.0.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
golang.org/x/sys v0.26.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.7.0 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/ini.v1 v1.51.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
97
tools/go.sum
97
tools/go.sum
@ -14,6 +14,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A=
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@ -29,12 +31,16 @@ 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/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=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
@ -52,11 +58,17 @@ 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.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/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=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I=
|
||||
github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@ -66,7 +78,6 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-redis/redis/v8 v8.11.2/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
|
||||
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
|
||||
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
@ -89,8 +100,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
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/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@ -100,16 +112,18 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
@ -140,8 +154,8 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hibiken/asynq v0.19.0/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
|
||||
github.com/hibiken/asynq v0.21.0 h1:uH9XogJhjq/S39E0/DEPWLZQ6hHJ73UiblZTe4RzHwA=
|
||||
github.com/hibiken/asynq v0.21.0/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
|
||||
github.com/hibiken/asynq v0.25.0 h1:VCPyRRrrjFChsTSI8x5OCPu51MlEz6Rk+1p0kHKnZug=
|
||||
github.com/hibiken/asynq v0.25.0/go.mod h1:DYQ1etBEl2Y+uSkqFElGYbk3M0ujLVwCfWE+TlvxtEk=
|
||||
github.com/hibiken/asynq/x v0.0.0-20220131170841-349f4c50fb1d h1:Er+U+9PmnyRHRDQjSjRQ24HoWvOY7w9Pk7bUPYM3Ags=
|
||||
github.com/hibiken/asynq/x v0.0.0-20220131170841-349f4c50fb1d/go.mod h1:VmxwMfMKyb6gyv8xG0oOBMXIhquWKPx+zPtbVBd2Q1s=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
@ -162,20 +176,27 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
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/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
@ -196,18 +217,15 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
|
||||
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
|
||||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
@ -222,8 +240,9 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s=
|
||||
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@ -242,10 +261,16 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
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/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=
|
||||
@ -262,8 +287,9 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
|
||||
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
@ -290,8 +316,9 @@ go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
|
||||
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@ -305,6 +332,7 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136 h1:A1gGSx58LAGVHUUsOf7IiR0u8Xb6W51gRwfDBhkdcaw=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
@ -340,7 +368,6 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -361,7 +388,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=
|
||||
@ -371,7 +397,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=
|
||||
@ -382,18 +407,27 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.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=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -417,7 +451,6 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
@ -453,8 +486,9 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@ -465,7 +499,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
25
x/go.mod
25
x/go.mod
@ -1,10 +1,25 @@
|
||||
module github.com/hibiken/asynq/x
|
||||
|
||||
go 1.16
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/go-redis/redis/v8 v8.11.4
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hibiken/asynq v0.21.0
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hibiken/asynq v0.25.0
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
)
|
||||
|
296
x/go.sum
296
x/go.sum
@ -1,258 +1,50 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/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/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=
|
||||
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=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-redis/redis/v8 v8.11.2/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
|
||||
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
|
||||
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hibiken/asynq v0.21.0 h1:uH9XogJhjq/S39E0/DEPWLZQ6hHJ73UiblZTe4RzHwA=
|
||||
github.com/hibiken/asynq v0.21.0/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
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/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/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=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
|
||||
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
|
||||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
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/hibiken/asynq v0.25.0 h1:VCPyRRrrjFChsTSI8x5OCPu51MlEz6Rk+1p0kHKnZug=
|
||||
github.com/hibiken/asynq v0.25.0/go.mod h1:DYQ1etBEl2Y+uSkqFElGYbk3M0ujLVwCfWE+TlvxtEk=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
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/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=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
|
||||
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/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-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=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
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.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=
|
||||
|
@ -7,9 +7,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/hibiken/asynq"
|
||||
asynqcontext "github.com/hibiken/asynq/internal/context"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// NewSemaphore creates a counting Semaphore for the given scope with the given number of tokens.
|
||||
@ -110,5 +110,5 @@ func (s *Semaphore) Close() error {
|
||||
}
|
||||
|
||||
func semaphoreKey(scope string) string {
|
||||
return fmt.Sprintf("asynq:sema:%s", scope)
|
||||
return "asynq:sema:" + scope
|
||||
}
|
||||
|
@ -8,11 +8,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
asynqcontext "github.com/hibiken/asynq/internal/context"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -91,7 +91,7 @@ func TestNewSemaphore_Acquire(t *testing.T) {
|
||||
maxConcurrency: 3,
|
||||
taskIDs: []string{uuid.NewString(), uuid.NewString()},
|
||||
ctxFunc: func(id string) (context.Context, context.CancelFunc) {
|
||||
return asynqcontext.New(&base.TaskMessage{
|
||||
return asynqcontext.New(context.Background(), &base.TaskMessage{
|
||||
ID: id,
|
||||
Queue: "task-1",
|
||||
}, time.Now().Add(time.Second))
|
||||
@ -104,7 +104,7 @@ func TestNewSemaphore_Acquire(t *testing.T) {
|
||||
maxConcurrency: 3,
|
||||
taskIDs: []string{uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString()},
|
||||
ctxFunc: func(id string) (context.Context, context.CancelFunc) {
|
||||
return asynqcontext.New(&base.TaskMessage{
|
||||
return asynqcontext.New(context.Background(), &base.TaskMessage{
|
||||
ID: id,
|
||||
Queue: "task-2",
|
||||
}, time.Now().Add(time.Second))
|
||||
@ -211,7 +211,7 @@ func TestNewSemaphore_Acquire_StaleToken(t *testing.T) {
|
||||
|
||||
// adding a set member to mimic the case where token is acquired but the goroutine crashed,
|
||||
// in which case, the token will not be explicitly removed and should be present already
|
||||
rc.ZAdd(context.Background(), semaphoreKey("stale-token"), &redis.Z{
|
||||
rc.ZAdd(context.Background(), semaphoreKey("stale-token"), redis.Z{
|
||||
Score: float64(time.Now().Add(-10 * time.Second).Unix()),
|
||||
Member: taskID,
|
||||
})
|
||||
@ -219,7 +219,7 @@ func TestNewSemaphore_Acquire_StaleToken(t *testing.T) {
|
||||
sema := NewSemaphore(opt, "stale-token", 1)
|
||||
defer sema.Close()
|
||||
|
||||
ctx, cancel := asynqcontext.New(&base.TaskMessage{
|
||||
ctx, cancel := asynqcontext.New(context.Background(), &base.TaskMessage{
|
||||
ID: taskID,
|
||||
Queue: "task-1",
|
||||
}, time.Now().Add(time.Second))
|
||||
@ -248,7 +248,7 @@ func TestNewSemaphore_Release(t *testing.T) {
|
||||
name: "task-5",
|
||||
taskIDs: []string{uuid.NewString()},
|
||||
ctxFunc: func(id string) (context.Context, context.CancelFunc) {
|
||||
return asynqcontext.New(&base.TaskMessage{
|
||||
return asynqcontext.New(context.Background(), &base.TaskMessage{
|
||||
ID: id,
|
||||
Queue: "task-3",
|
||||
}, time.Now().Add(time.Second))
|
||||
@ -259,7 +259,7 @@ func TestNewSemaphore_Release(t *testing.T) {
|
||||
name: "task-6",
|
||||
taskIDs: []string{uuid.NewString(), uuid.NewString()},
|
||||
ctxFunc: func(id string) (context.Context, context.CancelFunc) {
|
||||
return asynqcontext.New(&base.TaskMessage{
|
||||
return asynqcontext.New(context.Background(), &base.TaskMessage{
|
||||
ID: id,
|
||||
Queue: "task-4",
|
||||
}, time.Now().Add(time.Second))
|
||||
@ -277,9 +277,9 @@ func TestNewSemaphore_Release(t *testing.T) {
|
||||
t.Errorf("%s;\nredis.UniversalClient.Del() got error %v", tt.desc, err)
|
||||
}
|
||||
|
||||
var members []*redis.Z
|
||||
var members []redis.Z
|
||||
for i := 0; i < len(tt.taskIDs); i++ {
|
||||
members = append(members, &redis.Z{
|
||||
members = append(members, redis.Z{
|
||||
Score: float64(time.Now().Add(time.Duration(i) * time.Second).Unix()),
|
||||
Member: tt.taskIDs[i],
|
||||
})
|
||||
@ -337,7 +337,7 @@ func TestNewSemaphore_Release_Error(t *testing.T) {
|
||||
name: "task-8",
|
||||
taskIDs: []string{uuid.NewString()},
|
||||
ctxFunc: func(_ string) (context.Context, context.CancelFunc) {
|
||||
return asynqcontext.New(&base.TaskMessage{
|
||||
return asynqcontext.New(context.Background(), &base.TaskMessage{
|
||||
ID: testID,
|
||||
Queue: "task-4",
|
||||
}, time.Now().Add(time.Second))
|
||||
@ -356,9 +356,9 @@ func TestNewSemaphore_Release_Error(t *testing.T) {
|
||||
t.Errorf("%s;\nredis.UniversalClient.Del() got error %v", tt.desc, err)
|
||||
}
|
||||
|
||||
var members []*redis.Z
|
||||
var members []redis.Z
|
||||
for i := 0; i < len(tt.taskIDs); i++ {
|
||||
members = append(members, &redis.Z{
|
||||
members = append(members, redis.Z{
|
||||
Score: float64(time.Now().Add(time.Duration(i) * time.Second).Unix()),
|
||||
Member: tt.taskIDs[i],
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user