From 38b3845632b35f470f2a52343ad431f9c997607a Mon Sep 17 00:00:00 2001 From: Rafi Date: Fri, 4 Oct 2024 01:33:40 -0300 Subject: [PATCH] Remove tools and extra from the main repo --- .github/workflows/benchstat.yml | 6 +- .github/workflows/build.yml | 26 - tools/asynq/README.md | 64 --- tools/asynq/cmd/cron.go | 135 ----- tools/asynq/cmd/dash.go | 39 -- tools/asynq/cmd/dash/dash.go | 215 -------- tools/asynq/cmd/dash/draw.go | 718 ------------------------- tools/asynq/cmd/dash/draw_test.go | 29 - tools/asynq/cmd/dash/fetch.go | 183 ------- tools/asynq/cmd/dash/key_event.go | 312 ----------- tools/asynq/cmd/dash/key_event_test.go | 229 -------- tools/asynq/cmd/dash/screen_drawer.go | 97 ---- tools/asynq/cmd/dash/table.go | 66 --- tools/asynq/cmd/group.go | 48 -- tools/asynq/cmd/queue.go | 277 ---------- tools/asynq/cmd/root.go | 457 ---------------- tools/asynq/cmd/server.go | 115 ---- tools/asynq/cmd/stats.go | 262 --------- tools/asynq/cmd/task.go | 651 ---------------------- tools/asynq/main.go | 7 - tools/go.mod | 56 -- tools/go.sum | 394 -------------- tools/metrics_exporter/main.go | 56 -- x/go.mod | 27 - x/go.sum | 50 -- x/metrics/metrics.go | 190 ------- x/rate/example_test.go | 40 -- x/rate/semaphore.go | 114 ---- x/rate/semaphore_test.go | 407 -------------- 29 files changed, 3 insertions(+), 5267 deletions(-) delete mode 100644 tools/asynq/README.md delete mode 100644 tools/asynq/cmd/cron.go delete mode 100644 tools/asynq/cmd/dash.go delete mode 100644 tools/asynq/cmd/dash/dash.go delete mode 100644 tools/asynq/cmd/dash/draw.go delete mode 100644 tools/asynq/cmd/dash/draw_test.go delete mode 100644 tools/asynq/cmd/dash/fetch.go delete mode 100644 tools/asynq/cmd/dash/key_event.go delete mode 100644 tools/asynq/cmd/dash/key_event_test.go delete mode 100644 tools/asynq/cmd/dash/screen_drawer.go delete mode 100644 tools/asynq/cmd/dash/table.go delete mode 100644 tools/asynq/cmd/group.go delete mode 100644 tools/asynq/cmd/queue.go delete mode 100644 tools/asynq/cmd/root.go delete mode 100644 tools/asynq/cmd/server.go delete mode 100644 tools/asynq/cmd/stats.go delete mode 100644 tools/asynq/cmd/task.go delete mode 100644 tools/asynq/main.go delete mode 100644 tools/go.mod delete mode 100644 tools/go.sum delete mode 100644 tools/metrics_exporter/main.go delete mode 100644 x/go.mod delete mode 100644 x/go.sum delete mode 100644 x/metrics/metrics.go delete mode 100644 x/rate/example_test.go delete mode 100644 x/rate/semaphore.go delete mode 100644 x/rate/semaphore_test.go diff --git a/.github/workflows/benchstat.yml b/.github/workflows/benchstat.yml index 02ffb50d9..9797ec525 100644 --- a/.github/workflows/benchstat.yml +++ b/.github/workflows/benchstat.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.21.x + go-version: 1.23.x - name: Benchmark run: go test -run=^$ -bench=. -count=5 -timeout=60m ./... | tee -a new.txt - name: Upload Benchmark @@ -44,7 +44,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.21.x + go-version: 1.23.x - name: Benchmark run: go test -run=^$ -bench=. -count=5 -timeout=60m ./... | tee -a old.txt - name: Upload Benchmark @@ -62,7 +62,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.21.x + go-version: 1.23.x - name: Install benchstat run: go get -u golang.org/x/perf/cmd/benchstat - name: Download Incoming diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b437feeb5..0bea0f35a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,29 +40,3 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 - - 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@v4 - 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 .. diff --git a/tools/asynq/README.md b/tools/asynq/README.md deleted file mode 100644 index 5d582e0ae..000000000 --- a/tools/asynq/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Asynq CLI - -Asynq CLI is a command line tool to monitor the queues and tasks managed by `asynq` package. - -## Table of Contents - -- [Installation](#installation) -- [Usage](#usage) -- [Config File](#config-file) - -## Installation - -In order to use the tool, compile it using the following command: - - go install github.com/AsynqLab/asynq/tools/asynq@latest - -This will create the asynq executable under your `$GOPATH/bin` directory. - -## Usage - -### Commands - -To view details on any command, use `asynq help `. - -- `asynq dash` -- `asynq stats` -- `asynq queue [ls inspect history rm pause unpause]` -- `asynq task [ls cancel delete archive run deleteall archiveall runall]` -- `asynq server [ls]` - -### Global flags - -Asynq CLI needs to connect to a redis-server to inspect the state of queues and tasks. Use flags to specify the options to connect to the redis-server used by your application. -To connect to a redis cluster, pass `--cluster` and `--cluster_addrs` flags. - -By default, CLI will try to connect to a redis server running at `localhost:6379`. - -``` - --config string config file to set flag defaut values (default is $HOME/.asynq.yaml) - -n, --db int redis database number (default is 0) - -h, --help help for asynq - -p, --password string password to use when connecting to redis server - -u, --uri string redis server URI (default "127.0.0.1:6379") - - --cluster connect to redis cluster - --cluster_addrs string list of comma-separated redis server addresses -``` - -## Config File - -You can use a config file to set default values for the flags. - -By default, `asynq` will try to read config file located in -`$HOME/.asynq.(yml|json)`. You can specify the file location via `--config` flag. - -Config file example: - -```yaml -uri: 127.0.0.1:6379 -db: 2 -password: mypassword -``` - -This will set the default values for `--uri`, `--db`, and `--password` flags. diff --git a/tools/asynq/cmd/cron.go b/tools/asynq/cmd/cron.go deleted file mode 100644 index 7c9eabfbd..000000000 --- a/tools/asynq/cmd/cron.go +++ /dev/null @@ -1,135 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "os" - "sort" - "time" - - "github.com/AsynqLab/asynq" - "github.com/MakeNowJust/heredoc/v2" - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(cronCmd) - cronCmd.AddCommand(cronListCmd) - cronCmd.AddCommand(cronHistoryCmd) - cronHistoryCmd.Flags().Int("page", 1, "page number") - cronHistoryCmd.Flags().Int("size", 30, "page size") -} - -var cronCmd = &cobra.Command{ - Use: "cron [flags]", - Short: "Manage cron", - Example: heredoc.Doc(` - $ asynq cron ls - $ asynq cron history 7837f142-6337-4217-9276-8f27281b67d1`), -} - -var cronListCmd = &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, - Short: "List cron entries", - Run: cronList, -} - -var cronHistoryCmd = &cobra.Command{ - Use: "history [...]", - 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) { - inspector := createInspector() - - entries, err := inspector.SchedulerEntries() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - if len(entries) == 0 { - fmt.Println("No scheduler entries") - return - } - - // Sort entries by spec. - sort.Slice(entries, func(i, j int) bool { - x, y := entries[i], entries[j] - return x.Spec < y.Spec - }) - - cols := []string{"EntryID", "Spec", "Type", "Payload", "Options", "Next", "Prev"} - printRows := func(w io.Writer, tmpl string) { - for _, e := range entries { - fmt.Fprintf(w, tmpl, e.ID, e.Spec, e.Task.Type(), sprintBytes(e.Task.Payload()), e.Opts, - nextEnqueue(e.Next), prevEnqueue(e.Prev)) - } - } - printTable(cols, printRows) -} - -// Returns a string describing when the next enqueue will happen. -func nextEnqueue(nextEnqueueAt time.Time) string { - d := nextEnqueueAt.Sub(time.Now()).Round(time.Second) - if d < 0 { - return "Now" - } - return fmt.Sprintf("In %v", d) -} - -// Returns a string describing when the previous enqueue was. -func prevEnqueue(prevEnqueuedAt time.Time) string { - if prevEnqueuedAt.IsZero() { - return "N/A" - } - return fmt.Sprintf("%v ago", time.Since(prevEnqueuedAt).Round(time.Second)) -} - -func cronHistory(cmd *cobra.Command, args []string) { - pageNum, err := cmd.Flags().GetInt("page") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - pageSize, err := cmd.Flags().GetInt("size") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - inspector := createInspector() - for i, entryID := range args { - if i > 0 { - fmt.Printf("\n%s\n", separator) - } - fmt.Println() - - fmt.Printf("Entry: %s\n\n", entryID) - - events, err := inspector.ListSchedulerEnqueueEvents( - entryID, asynq.PageSize(pageSize), asynq.Page(pageNum)) - if err != nil { - fmt.Printf("error: %v\n", err) - continue - } - if len(events) == 0 { - fmt.Printf("No scheduler enqueue events found for entry: %s\n", entryID) - continue - } - - cols := []string{"TaskID", "EnqueuedAt"} - printRows := func(w io.Writer, tmpl string) { - for _, e := range events { - fmt.Fprintf(w, tmpl, e.TaskID, e.EnqueuedAt) - } - } - printTable(cols, printRows) - } -} diff --git a/tools/asynq/cmd/dash.go b/tools/asynq/cmd/dash.go deleted file mode 100644 index 04b7d9ef2..000000000 --- a/tools/asynq/cmd/dash.go +++ /dev/null @@ -1,39 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "time" - - "github.com/AsynqLab/asynq/tools/asynq/cmd/dash" - "github.com/MakeNowJust/heredoc/v2" - "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(), - }) - }, -} diff --git a/tools/asynq/cmd/dash/dash.go b/tools/asynq/cmd/dash/dash.go deleted file mode 100644 index 42731da49..000000000 --- a/tools/asynq/cmd/dash/dash.go +++ /dev/null @@ -1,215 +0,0 @@ -package dash - -import ( - "errors" - "fmt" - "os" - "strings" - "time" - - "github.com/AsynqLab/asynq" - "github.com/gdamore/tcell/v2" -) - -// 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) - } - } -} diff --git a/tools/asynq/cmd/dash/draw.go b/tools/asynq/cmd/dash/draw.go deleted file mode 100644 index e5cca9371..000000000 --- a/tools/asynq/cmd/dash/draw.go +++ /dev/null @@ -1,718 +0,0 @@ -package dash - -import ( - "fmt" - "math" - "strconv" - "strings" - "time" - "unicode" - "unicode/utf8" - - "github.com/AsynqLab/asynq" - "github.com/gdamore/tcell/v2" - "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) // | - 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(": GoBack", style) - default: - d.Print(": Help : 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 "" - } - if !isPrintable(data) { - return "" - } - 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("", keyStyle) - d.Print(" to select", baseStyle) - }, - func(d *modalRowDrawer) { - d.Print("", keyStyle) - d.Print(" or ", baseStyle) - d.Print("", keyStyle) - d.Print(" to go back", baseStyle) - }, - func(d *modalRowDrawer) { - d.Print("", keyStyle) - d.Print(" or ", baseStyle) - d.Print("", keyStyle) - d.Print(" to move up", baseStyle) - }, - func(d *modalRowDrawer) { - d.Print("", keyStyle) - d.Print(" or ", baseStyle) - d.Print("", keyStyle) - d.Print(" to move down", baseStyle) - }, - func(d *modalRowDrawer) { - d.Print("", keyStyle) - d.Print(" or ", baseStyle) - d.Print("", keyStyle) - d.Print(" to move left", baseStyle) - }, - func(d *modalRowDrawer) { - d.Print("", keyStyle) - d.Print(" or ", baseStyle) - d.Print("", keyStyle) - d.Print(" to move right", baseStyle) - }, - func(d *modalRowDrawer) { - d.Print("", keyStyle) - d.Print(" to quit", baseStyle) - }, - }) -} diff --git a/tools/asynq/cmd/dash/draw_test.go b/tools/asynq/cmd/dash/draw_test.go deleted file mode 100644 index 53d5e79a5..000000000 --- a/tools/asynq/cmd/dash/draw_test.go +++ /dev/null @@ -1,29 +0,0 @@ -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) - } - } -} diff --git a/tools/asynq/cmd/dash/fetch.go b/tools/asynq/cmd/dash/fetch.go deleted file mode 100644 index 4ffff7455..000000000 --- a/tools/asynq/cmd/dash/fetch.go +++ /dev/null @@ -1,183 +0,0 @@ -package dash - -import ( - "sort" - - "github.com/AsynqLab/asynq" - "github.com/gdamore/tcell/v2" -) - -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 -} diff --git a/tools/asynq/cmd/dash/key_event.go b/tools/asynq/cmd/dash/key_event.go deleted file mode 100644 index 6b9dba205..000000000 --- a/tools/asynq/cmd/dash/key_event.go +++ /dev/null @@ -1,312 +0,0 @@ -package dash - -import ( - "os" - "time" - - "github.com/AsynqLab/asynq" - "github.com/gdamore/tcell/v2" -) - -// 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) - } -} diff --git a/tools/asynq/cmd/dash/key_event_test.go b/tools/asynq/cmd/dash/key_event_test.go deleted file mode 100644 index 9b2f3c6d2..000000000 --- a/tools/asynq/cmd/dash/key_event_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package dash - -import ( - "testing" - "time" - - "github.com/AsynqLab/asynq" - "github.com/gdamore/tcell/v2" - "github.com/google/go-cmp/cmp" -) - -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) {} diff --git a/tools/asynq/cmd/dash/screen_drawer.go b/tools/asynq/cmd/dash/screen_drawer.go deleted file mode 100644 index ba5aaa6ad..000000000 --- a/tools/asynq/cmd/dash/screen_drawer.go +++ /dev/null @@ -1,97 +0,0 @@ -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) - } -} diff --git a/tools/asynq/cmd/dash/table.go b/tools/asynq/cmd/dash/table.go deleted file mode 100644 index 7cc0416ec..000000000 --- a/tools/asynq/cmd/dash/table.go +++ /dev/null @@ -1,66 +0,0 @@ -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) - } -} diff --git a/tools/asynq/cmd/group.go b/tools/asynq/cmd/group.go deleted file mode 100644 index 79ade980c..000000000 --- a/tools/asynq/cmd/group.go +++ /dev/null @@ -1,48 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/MakeNowJust/heredoc/v2" - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(groupCmd) - groupCmd.AddCommand(groupListCmd) - groupListCmd.Flags().StringP("queue", "q", "", "queue to inspect") - groupListCmd.MarkFlagRequired("queue") -} - -var groupCmd = &cobra.Command{ - Use: "group [flags]", - Short: "Manage groups", - Example: heredoc.Doc(` - $ asynq group list --queue=myqueue`), -} - -var groupListCmd = &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, - Short: "List groups", - Args: cobra.NoArgs, - Run: groupLists, -} - -func groupLists(cmd *cobra.Command, args []string) { - qname, err := cmd.Flags().GetString("queue") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - inspector := createInspector() - groups, err := inspector.Groups(qname) - if len(groups) == 0 { - fmt.Printf("No groups found in queue %q\n", qname) - return - } - for _, g := range groups { - fmt.Println(g.Group) - } -} diff --git a/tools/asynq/cmd/queue.go b/tools/asynq/cmd/queue.go deleted file mode 100644 index 20528d46f..000000000 --- a/tools/asynq/cmd/queue.go +++ /dev/null @@ -1,277 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "os" - - "github.com/AsynqLab/asynq" - "github.com/AsynqLab/asynq/internal/errors" - "github.com/MakeNowJust/heredoc/v2" - "github.com/fatih/color" - "github.com/spf13/cobra" -) - -const separator = "=================================================" - -func init() { - rootCmd.AddCommand(queueCmd) - queueCmd.AddCommand(queueListCmd) - queueCmd.AddCommand(queueInspectCmd) - queueCmd.AddCommand(queueHistoryCmd) - queueHistoryCmd.Flags().IntP("days", "x", 10, "show data from last x days") - - queueCmd.AddCommand(queuePauseCmd) - queueCmd.AddCommand(queueUnpauseCmd) - queueCmd.AddCommand(queueRemoveCmd) - queueRemoveCmd.Flags().BoolP("force", "f", false, "remove the queue regardless of its size") -} - -var queueCmd = &cobra.Command{ - Use: "queue [flags]", - Short: "Manage queues", - Example: heredoc.Doc(` - $ asynq queue ls - $ asynq queue inspect myqueue - $ asynq queue pause myqueue`), -} - -var queueListCmd = &cobra.Command{ - Use: "list", - Short: "List queues", - Aliases: []string{"ls"}, - // TODO: Use RunE instead? - Run: queueList, -} - -var queueInspectCmd = &cobra.Command{ - Use: "inspect [...]", - 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 [...]", - 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 [...]", - 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: "resume [...]", - 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: "remove [...]", - 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) { - type queueInfo struct { - name string - keyslot int64 - nodes []*asynq.ClusterNode - } - inspector := createInspector() - queues, err := inspector.Queues() - if err != nil { - fmt.Printf("error: Could not fetch list of queues: %v\n", err) - os.Exit(1) - } - var qs []*queueInfo - for _, qname := range queues { - q := queueInfo{name: qname} - if useRedisCluster { - keyslot, err := inspector.ClusterKeySlot(qname) - if err != nil { - fmt.Errorf("error: Could not get cluster keyslot for %q\n", qname) - continue - } - q.keyslot = keyslot - nodes, err := inspector.ClusterNodes(qname) - if err != nil { - fmt.Errorf("error: Could not get cluster nodes for %q\n", qname) - continue - } - q.nodes = nodes - } - qs = append(qs, &q) - } - if useRedisCluster { - printTable( - []string{"Queue", "Cluster KeySlot", "Cluster Nodes"}, - func(w io.Writer, tmpl string) { - for _, q := range qs { - fmt.Fprintf(w, tmpl, q.name, q.keyslot, q.nodes) - } - }, - ) - } else { - for _, q := range qs { - fmt.Println(q.name) - } - } -} - -func queueInspect(cmd *cobra.Command, args []string) { - inspector := createInspector() - for i, qname := range args { - if i > 0 { - fmt.Printf("\n%s\n\n", separator) - } - info, err := inspector.GetQueueInfo(qname) - if err != nil { - fmt.Printf("error: %v\n", err) - continue - } - printQueueInfo(info) - } -} - -func printQueueInfo(info *asynq.QueueInfo) { - bold := color.New(color.Bold) - bold.Println("Queue Info") - fmt.Printf("Name: %s\n", info.Queue) - fmt.Printf("Size: %d\n", info.Size) - fmt.Printf("Groups: %d\n", info.Groups) - fmt.Printf("Paused: %t\n\n", info.Paused) - bold.Println("Task Count by State") - printTable( - []string{"active", "pending", "aggregating", "scheduled", "retry", "archived", "completed"}, - func(w io.Writer, tmpl string) { - fmt.Fprintf(w, tmpl, info.Active, info.Pending, info.Aggregating, info.Scheduled, info.Retry, info.Archived, info.Completed) - }, - ) - fmt.Println() - bold.Printf("Daily Stats %s UTC\n", info.Timestamp.UTC().Format("2006-01-02")) - printTable( - []string{"processed", "failed", "error rate"}, - func(w io.Writer, tmpl string) { - var errRate string - if info.Processed == 0 { - errRate = "N/A" - } else { - errRate = fmt.Sprintf("%.2f%%", float64(info.Failed)/float64(info.Processed)*100) - } - fmt.Fprintf(w, tmpl, info.Processed, info.Failed, errRate) - }, - ) -} - -func queueHistory(cmd *cobra.Command, args []string) { - days, err := cmd.Flags().GetInt("days") - if err != nil { - fmt.Printf("error: Internal error: %v\n", err) - os.Exit(1) - } - inspector := createInspector() - for i, qname := range args { - if i > 0 { - fmt.Printf("\n%s\n\n", separator) - } - fmt.Printf("Queue: %s\n\n", qname) - stats, err := inspector.History(qname, days) - if err != nil { - fmt.Printf("error: %v\n", err) - continue - } - printDailyStats(stats) - } -} - -func printDailyStats(stats []*asynq.DailyStats) { - printTable( - []string{"date (UTC)", "processed", "failed", "error rate"}, - func(w io.Writer, tmpl string) { - for _, s := range stats { - var errRate string - if s.Processed == 0 { - errRate = "N/A" - } else { - errRate = fmt.Sprintf("%.2f%%", float64(s.Failed)/float64(s.Processed)*100) - } - fmt.Fprintf(w, tmpl, s.Date.Format("2006-01-02"), s.Processed, s.Failed, errRate) - } - }, - ) -} - -func queuePause(cmd *cobra.Command, args []string) { - inspector := createInspector() - for _, qname := range args { - err := inspector.PauseQueue(qname) - if err != nil { - fmt.Println(err) - continue - } - fmt.Printf("Successfully paused queue %q\n", qname) - } -} - -func queueUnpause(cmd *cobra.Command, args []string) { - inspector := createInspector() - for _, qname := range args { - err := inspector.UnpauseQueue(qname) - if err != nil { - fmt.Println(err) - continue - } - fmt.Printf("Successfully unpaused queue %q\n", qname) - } -} - -func queueRemove(cmd *cobra.Command, args []string) { - // TODO: Use inspector once RemoveQueue become public API. - force, err := cmd.Flags().GetBool("force") - if err != nil { - fmt.Printf("error: Internal error: %v\n", err) - os.Exit(1) - } - - r := createRDB() - for _, qname := range args { - err = r.RemoveQueue(qname, force) - if err != nil { - if errors.IsQueueNotEmpty(err) { - fmt.Printf("error: %v\nIf you are sure you want to delete it, run 'asynq queue rm --force %s'\n", err, qname) - continue - } - fmt.Printf("error: %v\n", err) - continue - } - fmt.Printf("Successfully removed queue %q\n", qname) - } -} diff --git a/tools/asynq/cmd/root.go b/tools/asynq/cmd/root.go deleted file mode 100644 index 430536118..000000000 --- a/tools/asynq/cmd/root.go +++ /dev/null @@ -1,457 +0,0 @@ -package cmd - -import ( - "crypto/tls" - "fmt" - "io" - "os" - "strings" - "text/tabwriter" - "unicode" - "unicode/utf8" - - "github.com/AsynqLab/asynq" - "github.com/AsynqLab/asynq/internal/base" - "github.com/AsynqLab/asynq/internal/rdb" - "github.com/MakeNowJust/heredoc/v2" - "github.com/fatih/color" - "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" -) - -var cfgFile string - -// Global flag variables -var ( - uri string - db int - password string - - useRedisCluster bool - clusterAddrs string - tlsServerName string -) - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "asynq [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/AsynqLab/asynq/issues/new/choose`), - }, -} - -var versionOutput = fmt.Sprintf("asynq version %s\n", base.Version) - -var versionCmd = &cobra.Command{ - Use: "version", - Hidden: true, - Run: func(cmd *cobra.Command, args []string) { - fmt.Print(versionOutput) - }, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -func isRootCmd(cmd *cobra.Command) bool { - return cmd != nil && !cmd.HasParent() -} - -// displayLine represents a line displayed in the output as ' ', -// 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 --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(&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") - rootCmd.PersistentFlags().StringVar(&tlsServerName, "tls_server", - "", "Server name for TLS validation") - // Bind flags with config. - viper.BindPFlag("uri", rootCmd.PersistentFlags().Lookup("uri")) - viper.BindPFlag("db", rootCmd.PersistentFlags().Lookup("db")) - viper.BindPFlag("password", rootCmd.PersistentFlags().Lookup("password")) - 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")) -} - -// initConfig reads in config file and ENV variables if set. -func initConfig() { - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := homedir.Dir() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - // Search config in home directory with name ".asynq" (without extension). - viper.AddConfigPath(home) - viper.SetConfigName(".asynq") - } - - viper.AutomaticEnv() // read in environment variables that match - - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - fmt.Println("Using config file:", viper.ConfigFileUsed()) - } -} - -// createRDB creates a RDB instance using flag values and returns it. -func createRDB() *rdb.RDB { - var c redis.UniversalClient - if viper.GetBool("cluster") { - addrs := strings.Split(viper.GetString("cluster_addrs"), ",") - c = redis.NewClusterClient(&redis.ClusterOptions{ - Addrs: addrs, - Password: viper.GetString("password"), - TLSConfig: getTLSConfig(), - }) - } else { - c = redis.NewClient(&redis.Options{ - Addr: viper.GetString("uri"), - DB: viper.GetInt("db"), - Password: viper.GetString("password"), - TLSConfig: getTLSConfig(), - }) - } - return rdb.NewRDB(c) -} - -// createInspector creates a Inspector instance using flag values and returns it. -func createInspector() *asynq.Inspector { - return asynq.NewInspector(getRedisConnOpt()) -} - -func getRedisConnOpt() asynq.RedisConnOpt { - if viper.GetBool("cluster") { - addrs := strings.Split(viper.GetString("cluster_addrs"), ",") - return asynq.RedisClusterClientOpt{ - Addrs: addrs, - Password: viper.GetString("password"), - TLSConfig: getTLSConfig(), - } - } - return asynq.RedisClientOpt{ - Addr: viper.GetString("uri"), - DB: viper.GetInt("db"), - Password: viper.GetString("password"), - TLSConfig: getTLSConfig(), - } -} - -func getTLSConfig() *tls.Config { - tlsServer := viper.GetString("tls_server") - if tlsServer == "" { - return nil - } - return &tls.Config{ServerName: tlsServer} -} - -// printTable is a helper function to print data in table format. -// -// cols is a list of headers and printRow specifies how to print rows. -// -// Example: -// -// type User struct { -// Name string -// Addr string -// Age int -// } -// -// data := []*User{{"user1", "addr1", 24}, {"user2", "addr2", 42}, ...} -// cols := []string{"Name", "Addr", "Age"} -// -// printRows := func(w io.Writer, tmpl string) { -// for _, u := range data { -// fmt.Fprintf(w, tmpl, u.Name, u.Addr, u.Age) -// } -// } -// -// printTable(cols, printRows) -func printTable(cols []string, printRows func(w io.Writer, tmpl string)) { - format := strings.Repeat("%v\t", len(cols)) + "\n" - tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0) - var headers []interface{} - var seps []interface{} - for _, name := range cols { - headers = append(headers, name) - seps = append(seps, strings.Repeat("-", len(name))) - } - fmt.Fprintf(tw, format, headers...) - fmt.Fprintf(tw, format, seps...) - printRows(tw, format) - tw.Flush() -} - -// sprintBytes returns a string representation of the given byte slice if data is printable. -// If data is not printable, it returns a string describing it is not printable. -func sprintBytes(payload []byte) string { - if !isPrintable(payload) { - return "non-printable bytes" - } - return string(payload) -} - -func isPrintable(data []byte) bool { - if !utf8.Valid(data) { - return false - } - isAllSpace := true - for _, r := range string(data) { - if !unicode.IsPrint(r) { - return false - } - if !unicode.IsSpace(r) { - isAllSpace = false - } - } - return !isAllSpace -} diff --git a/tools/asynq/cmd/server.go b/tools/asynq/cmd/server.go deleted file mode 100644 index e6166aeed..000000000 --- a/tools/asynq/cmd/server.go +++ /dev/null @@ -1,115 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "os" - "sort" - "strings" - "time" - - "github.com/MakeNowJust/heredoc/v2" - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(serverCmd) - serverCmd.AddCommand(serverListCmd) -} - -var serverCmd = &cobra.Command{ - Use: "server [flags]", - Short: "Manage servers", - Example: heredoc.Doc(` - $ asynq server list`), -} - -var serverListCmd = &cobra.Command{ - 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. - -The command shows the following for each server: -* Host and PID of the process in which the server is running -* Number of active workers out of worker pool -* Queue configuration -* State of the worker server ("active" | "stopped") -* Time the server was started - -A "active" server is pulling tasks from queues and processing them. -A "stopped" server is no longer pulling new tasks from queues`, - Run: serverList, -} - -func serverList(cmd *cobra.Command, args []string) { - r := createRDB() - - servers, err := r.ListServers() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - if len(servers) == 0 { - fmt.Println("No running servers") - return - } - - // sort by hostname and pid - sort.Slice(servers, func(i, j int) bool { - x, y := servers[i], servers[j] - if x.Host != y.Host { - return x.Host < y.Host - } - return x.PID < y.PID - }) - - // print server info - cols := []string{"Host", "PID", "State", "Active Workers", "Queues", "Started"} - printRows := func(w io.Writer, tmpl string) { - for _, info := range servers { - fmt.Fprintf(w, tmpl, - info.Host, info.PID, info.Status, - fmt.Sprintf("%d/%d", info.ActiveWorkerCount, info.Concurrency), - formatQueues(info.Queues), timeAgo(info.Started)) - } - } - printTable(cols, printRows) -} - -func formatQueues(qmap map[string]int) string { - // sort queues by priority and name - type queue struct { - name string - priority int - } - var queues []*queue - for qname, p := range qmap { - queues = append(queues, &queue{qname, p}) - } - sort.Slice(queues, func(i, j int) bool { - x, y := queues[i], queues[j] - if x.priority != y.priority { - return x.priority > y.priority - } - return x.name < y.name - }) - - var b strings.Builder - l := len(queues) - for _, q := range queues { - fmt.Fprintf(&b, "%s:%d", q.name, q.priority) - l-- - if l > 0 { - b.WriteString(" ") - } - } - return b.String() -} - -// timeAgo takes a time and returns a string of the format " ago". -func timeAgo(since time.Time) string { - d := time.Since(since).Round(time.Second) - return fmt.Sprintf("%v ago", d) -} diff --git a/tools/asynq/cmd/stats.go b/tools/asynq/cmd/stats.go deleted file mode 100644 index af5b3ae9b..000000000 --- a/tools/asynq/cmd/stats.go +++ /dev/null @@ -1,262 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "io" - "math" - "os" - "strconv" - "strings" - "text/tabwriter" - "time" - "unicode/utf8" - - "github.com/AsynqLab/asynq/internal/rdb" - "github.com/MakeNowJust/heredoc/v2" - "github.com/fatih/color" - "github.com/spf13/cobra" -) - -// statsCmd represents the stats command -var statsCmd = &cobra.Command{ - Use: "stats", - Short: "View current state", - Long: heredoc.Doc(` - Stats shows the overview of tasks and queues at that instant. - - 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, -} - -var jsonFlag bool - -func init() { - rootCmd.AddCommand(statsCmd) - statsCmd.Flags().BoolVar(&jsonFlag, "json", false, "Output stats in JSON format.") - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // statsCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // statsCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} - -type AggregateStats struct { - Active int `json:"active"` - Pending int `json:"pending"` - Aggregating int `json:"aggregating"` - Scheduled int `json:"scheduled"` - Retry int `json:"retry"` - Archived int `json:"archived"` - Completed int `json:"completed"` - Processed int `json:"processed"` - Failed int `json:"failed"` - Timestamp time.Time `json:"timestamp"` -} - -type FullStats struct { - Aggregate AggregateStats `json:"aggregate"` - QueueStats []*rdb.Stats `json:"queues"` - RedisInfo map[string]string `json:"redis"` -} - -func stats(cmd *cobra.Command, args []string) { - r := createRDB() - - queues, err := r.AllQueues() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - var aggStats AggregateStats - var stats []*rdb.Stats - for _, qname := range queues { - s, err := r.CurrentStats(qname) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - aggStats.Active += s.Active - aggStats.Pending += s.Pending - aggStats.Aggregating += s.Aggregating - aggStats.Scheduled += s.Scheduled - aggStats.Retry += s.Retry - aggStats.Archived += s.Archived - aggStats.Completed += s.Completed - aggStats.Processed += s.Processed - aggStats.Failed += s.Failed - aggStats.Timestamp = s.Timestamp - stats = append(stats, s) - } - var info map[string]string - if useRedisCluster { - info, err = r.RedisClusterInfo() - } else { - info, err = r.RedisInfo() - } - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if jsonFlag { - statsJSON, err := json.Marshal(FullStats{ - Aggregate: aggStats, - QueueStats: stats, - RedisInfo: info, - }) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - fmt.Println(string(statsJSON)) - return - } - - bold := color.New(color.Bold) - bold.Println("Task Count by State") - printStatsByState(&aggStats) - fmt.Println() - - bold.Println("Task Count by Queue") - printStatsByQueue(stats) - fmt.Println() - - bold.Printf("Daily Stats %s UTC\n", aggStats.Timestamp.UTC().Format("2006-01-02")) - printSuccessFailureStats(&aggStats) - fmt.Println() - - if useRedisCluster { - bold.Println("Redis Cluster Info") - printClusterInfo(info) - } else { - bold.Println("Redis Info") - printInfo(info) - } - fmt.Println() -} - -func printStatsByState(s *AggregateStats) { - format := strings.Repeat("%v\t", 7) + "\n" - tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0) - fmt.Fprintf(tw, format, "active", "pending", "aggregating", "scheduled", "retry", "archived", "completed") - width := maxInt(9 /* defaultWidth */, maxWidthOf(s.Active, s.Pending, s.Aggregating, s.Scheduled, s.Retry, s.Archived, s.Completed)) // length of widest column - sep := strings.Repeat("-", width) - fmt.Fprintf(tw, format, sep, sep, sep, sep, sep, sep, sep) - fmt.Fprintf(tw, format, s.Active, s.Pending, s.Aggregating, s.Scheduled, s.Retry, s.Archived, s.Completed) - tw.Flush() -} - -// numDigits returns the number of digits in n. -func numDigits(n int) int { - return len(strconv.Itoa(n)) -} - -// maxWidthOf returns the max number of digits amount the provided vals. -func maxWidthOf(vals ...int) int { - max := 0 - for _, v := range vals { - if vw := numDigits(v); vw > max { - max = vw - } - } - return max -} - -func maxInt(a, b int) int { - return int(math.Max(float64(a), float64(b))) -} - -func printStatsByQueue(stats []*rdb.Stats) { - var headers, seps, counts []string - maxHeaderWidth := 0 - for _, s := range stats { - title := queueTitle(s) - headers = append(headers, title) - if w := utf8.RuneCountInString(title); w > maxHeaderWidth { - maxHeaderWidth = w - } - counts = append(counts, strconv.Itoa(s.Size)) - } - for i := 0; i < len(headers); i++ { - seps = append(seps, strings.Repeat("-", maxHeaderWidth)) - } - format := strings.Repeat("%v\t", len(headers)) + "\n" - tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0) - fmt.Fprintf(tw, format, toInterfaceSlice(headers)...) - fmt.Fprintf(tw, format, toInterfaceSlice(seps)...) - fmt.Fprintf(tw, format, toInterfaceSlice(counts)...) - tw.Flush() -} - -func queueTitle(s *rdb.Stats) string { - var b strings.Builder - b.WriteString(s.Queue) - if s.Paused { - b.WriteString(" (paused)") - } - return b.String() -} - -func printSuccessFailureStats(s *AggregateStats) { - format := strings.Repeat("%v\t", 3) + "\n" - tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0) - fmt.Fprintf(tw, format, "processed", "failed", "error rate") - fmt.Fprintf(tw, format, "---------", "------", "----------") - var errrate string - if s.Processed == 0 { - errrate = "N/A" - } else { - errrate = fmt.Sprintf("%.2f%%", float64(s.Failed)/float64(s.Processed)*100) - } - fmt.Fprintf(tw, format, s.Processed, s.Failed, errrate) - tw.Flush() -} - -func printInfo(info map[string]string) { - format := strings.Repeat("%v\t", 5) + "\n" - tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0) - fmt.Fprintf(tw, format, "version", "uptime", "connections", "memory usage", "peak memory usage") - fmt.Fprintf(tw, format, "-------", "------", "-----------", "------------", "-----------------") - fmt.Fprintf(tw, format, - info["redis_version"], - fmt.Sprintf("%s days", info["uptime_in_days"]), - info["connected_clients"], - fmt.Sprintf("%sB", info["used_memory_human"]), - fmt.Sprintf("%sB", info["used_memory_peak_human"]), - ) - tw.Flush() -} - -func printClusterInfo(info map[string]string) { - printTable( - []string{"State", "Known Nodes", "Cluster Size"}, - func(w io.Writer, tmpl string) { - fmt.Fprintf(w, tmpl, - strings.ToUpper(info["cluster_state"]), - info["cluster_known_nodes"], - info["cluster_size"], - ) - }, - ) -} - -func toInterfaceSlice(strs []string) []interface{} { - var res []interface{} - for _, s := range strs { - res = append(res, s) - } - return res -} diff --git a/tools/asynq/cmd/task.go b/tools/asynq/cmd/task.go deleted file mode 100644 index 42369b8c3..000000000 --- a/tools/asynq/cmd/task.go +++ /dev/null @@ -1,651 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "os" - "time" - - "github.com/AsynqLab/asynq" - "github.com/MakeNowJust/heredoc/v2" - "github.com/fatih/color" - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(taskCmd) - taskCmd.AddCommand(taskListCmd) - 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)") - taskListCmd.MarkFlagRequired("queue") - taskListCmd.MarkFlagRequired("state") - - taskCmd.AddCommand(taskCancelCmd) - - taskCmd.AddCommand(taskInspectCmd) - 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 (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 (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 (required)") - taskRunCmd.Flags().StringP("id", "i", "", "id of the task (required)") - taskRunCmd.MarkFlagRequired("queue") - taskRunCmd.MarkFlagRequired("id") - - taskCmd.AddCommand(taskArchiveAllCmd) - taskArchiveAllCmd.Flags().StringP("queue", "q", "", "queue to which the tasks belong (required)") - taskArchiveAllCmd.Flags().StringP("state", "s", "", "state of the tasks; one of { pending | aggregating | scheduled | retry } (required)") - 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 (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 (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 [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: "list --queue= --state= [flags]", - Aliases: []string{"ls"}, - Short: "List tasks", - Long: heredoc.Doc(` - List tasks of the given state from the specified queue. - - The --queue and --state flags are required. - - Note: For aggregating tasks, additional --group flag is 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.`), - 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= --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 [...]", - 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= --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= --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= --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 taskArchiveAllCmd = &cobra.Command{ - Use: "archiveall --queue= --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= --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= --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) { - qname, err := cmd.Flags().GetString("queue") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - state, err := cmd.Flags().GetString("state") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - pageNum, err := cmd.Flags().GetInt("page") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - pageSize, err := cmd.Flags().GetInt("size") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - switch state { - case "active": - listActiveTasks(qname, pageNum, pageSize) - case "pending": - listPendingTasks(qname, pageNum, pageSize) - case "scheduled": - listScheduledTasks(qname, pageNum, pageSize) - case "retry": - listRetryTasks(qname, pageNum, pageSize) - case "archived": - listArchivedTasks(qname, pageNum, pageSize) - case "completed": - listCompletedTasks(qname, pageNum, pageSize) - case "aggregating": - group, err := cmd.Flags().GetString("group") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - if group == "" { - fmt.Println("Flag --group is required for listing aggregating tasks") - os.Exit(1) - } - listAggregatingTasks(qname, group, pageNum, pageSize) - default: - fmt.Printf("error: state=%q is not supported\n", state) - os.Exit(1) - } -} - -func listActiveTasks(qname string, pageNum, pageSize int) { - i := createInspector() - tasks, err := i.ListActiveTasks(qname, asynq.PageSize(pageSize), asynq.Page(pageNum)) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - if len(tasks) == 0 { - fmt.Printf("No active tasks in %q queue\n", qname) - return - } - printTable( - []string{"ID", "Type", "Payload"}, - func(w io.Writer, tmpl string) { - for _, t := range tasks { - fmt.Fprintf(w, tmpl, t.ID, t.Type, sprintBytes(t.Payload)) - } - }, - ) -} - -func listPendingTasks(qname string, pageNum, pageSize int) { - i := createInspector() - tasks, err := i.ListPendingTasks(qname, asynq.PageSize(pageSize), asynq.Page(pageNum)) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - if len(tasks) == 0 { - fmt.Printf("No pending tasks in %q queue\n", qname) - return - } - printTable( - []string{"ID", "Type", "Payload"}, - func(w io.Writer, tmpl string) { - for _, t := range tasks { - fmt.Fprintf(w, tmpl, t.ID, t.Type, sprintBytes(t.Payload)) - } - }, - ) -} - -func listScheduledTasks(qname string, pageNum, pageSize int) { - i := createInspector() - tasks, err := i.ListScheduledTasks(qname, asynq.PageSize(pageSize), asynq.Page(pageNum)) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - if len(tasks) == 0 { - fmt.Printf("No scheduled tasks in %q queue\n", qname) - return - } - printTable( - []string{"ID", "Type", "Payload", "Process In"}, - func(w io.Writer, tmpl string) { - for _, t := range tasks { - fmt.Fprintf(w, tmpl, t.ID, t.Type, sprintBytes(t.Payload), formatProcessAt(t.NextProcessAt)) - } - }, - ) -} - -// formatProcessAt formats next process at time to human friendly string. -// If processAt time is in the past, returns "right now". -// If processAt time is in the future, returns "in xxx" where xxx is the duration from now. -func formatProcessAt(processAt time.Time) string { - d := processAt.Sub(time.Now()) - if d < 0 { - return "right now" - } - return fmt.Sprintf("in %v", d.Round(time.Second)) -} - -func listRetryTasks(qname string, pageNum, pageSize int) { - i := createInspector() - tasks, err := i.ListRetryTasks(qname, asynq.PageSize(pageSize), asynq.Page(pageNum)) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - if len(tasks) == 0 { - fmt.Printf("No retry tasks in %q queue\n", qname) - return - } - printTable( - []string{"ID", "Type", "Payload", "Next Retry", "Last Error", "Last Failed", "Retried", "Max Retry"}, - func(w io.Writer, tmpl string) { - for _, t := range tasks { - fmt.Fprintf(w, tmpl, t.ID, t.Type, sprintBytes(t.Payload), formatProcessAt(t.NextProcessAt), - t.LastErr, formatPastTime(t.LastFailedAt), t.Retried, t.MaxRetry) - } - }, - ) -} - -func listArchivedTasks(qname string, pageNum, pageSize int) { - i := createInspector() - tasks, err := i.ListArchivedTasks(qname, asynq.PageSize(pageSize), asynq.Page(pageNum)) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - if len(tasks) == 0 { - fmt.Printf("No archived tasks in %q queue\n", qname) - return - } - printTable( - []string{"ID", "Type", "Payload", "Last Failed", "Last Error"}, - func(w io.Writer, tmpl string) { - for _, t := range tasks { - fmt.Fprintf(w, tmpl, t.ID, t.Type, sprintBytes(t.Payload), formatPastTime(t.LastFailedAt), t.LastErr) - } - }) -} - -func listCompletedTasks(qname string, pageNum, pageSize int) { - i := createInspector() - tasks, err := i.ListCompletedTasks(qname, asynq.PageSize(pageSize), asynq.Page(pageNum)) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - if len(tasks) == 0 { - fmt.Printf("No completed tasks in %q queue\n", qname) - return - } - printTable( - []string{"ID", "Type", "Payload", "CompletedAt", "Result"}, - func(w io.Writer, tmpl string) { - for _, t := range tasks { - fmt.Fprintf(w, tmpl, t.ID, t.Type, sprintBytes(t.Payload), formatPastTime(t.CompletedAt), sprintBytes(t.Result)) - } - }) -} - -func listAggregatingTasks(qname, group string, pageNum, pageSize int) { - i := createInspector() - tasks, err := i.ListAggregatingTasks(qname, group, asynq.PageSize(pageSize), asynq.Page(pageNum)) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - if len(tasks) == 0 { - fmt.Printf("No aggregating tasks in group %q \n", group) - return - } - printTable( - []string{"ID", "Type", "Payload", "Group"}, - func(w io.Writer, tmpl string) { - for _, t := range tasks { - fmt.Fprintf(w, tmpl, t.ID, t.Type, sprintBytes(t.Payload), t.Group) - } - }, - ) -} - -func taskCancel(cmd *cobra.Command, args []string) { - i := createInspector() - for _, id := range args { - if err := i.CancelProcessing(id); err != nil { - fmt.Printf("error: could not send cancelation signal: %v\n", err) - continue - } - fmt.Printf("Sent cancelation signal for task %s\n", id) - } -} - -func taskInspect(cmd *cobra.Command, args []string) { - qname, err := cmd.Flags().GetString("queue") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - id, err := cmd.Flags().GetString("id") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - - i := createInspector() - info, err := i.GetTaskInfo(qname, id) - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - printTaskInfo(info) -} - -func printTaskInfo(info *asynq.TaskInfo) { - bold := color.New(color.Bold) - bold.Println("Task Info") - fmt.Printf("Queue: %s\n", info.Queue) - fmt.Printf("ID: %s\n", info.ID) - fmt.Printf("Type: %s\n", info.Type) - fmt.Printf("State: %v\n", info.State) - fmt.Printf("Retried: %d/%d\n", info.Retried, info.MaxRetry) - fmt.Println() - fmt.Printf("Next process time: %s\n", formatNextProcessAt(info.NextProcessAt)) - if len(info.LastErr) != 0 { - fmt.Println() - bold.Println("Last Failure") - fmt.Printf("Failed at: %s\n", formatPastTime(info.LastFailedAt)) - fmt.Printf("Error message: %s\n", info.LastErr) - } -} - -func formatNextProcessAt(processAt time.Time) string { - if processAt.IsZero() || processAt.Unix() == 0 { - return "n/a" - } - if processAt.Before(time.Now()) { - return "now" - } - return fmt.Sprintf("%s (in %v)", processAt.Format(time.UnixDate), processAt.Sub(time.Now()).Round(time.Second)) -} - -// formatPastTime takes t which is time in the past and returns a user-friendly string. -func formatPastTime(t time.Time) string { - if t.IsZero() || t.Unix() == 0 { - return "" - } - return t.Format(time.UnixDate) -} - -func taskArchive(cmd *cobra.Command, args []string) { - qname, err := cmd.Flags().GetString("queue") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - id, err := cmd.Flags().GetString("id") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - - i := createInspector() - err = i.ArchiveTask(qname, id) - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - fmt.Println("task archived") -} - -func taskDelete(cmd *cobra.Command, args []string) { - qname, err := cmd.Flags().GetString("queue") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - id, err := cmd.Flags().GetString("id") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - - i := createInspector() - err = i.DeleteTask(qname, id) - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - fmt.Println("task deleted") -} - -func taskRun(cmd *cobra.Command, args []string) { - qname, err := cmd.Flags().GetString("queue") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - id, err := cmd.Flags().GetString("id") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - - i := createInspector() - err = i.RunTask(qname, id) - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - fmt.Println("task is now pending") -} - -func taskArchiveAll(cmd *cobra.Command, args []string) { - qname, err := cmd.Flags().GetString("queue") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - state, err := cmd.Flags().GetString("state") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - - i := createInspector() - var n int - switch state { - case "pending": - n, err = i.ArchiveAllPendingTasks(qname) - case "scheduled": - 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) - } - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - fmt.Printf("%d tasks archived\n", n) -} - -func taskDeleteAll(cmd *cobra.Command, args []string) { - qname, err := cmd.Flags().GetString("queue") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - state, err := cmd.Flags().GetString("state") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - - i := createInspector() - var n int - switch state { - case "pending": - n, err = i.DeleteAllPendingTasks(qname) - case "scheduled": - n, err = i.DeleteAllScheduledTasks(qname) - case "retry": - n, err = i.DeleteAllRetryTasks(qname) - case "archived": - 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) - } - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - fmt.Printf("%d tasks deleted\n", n) -} - -func taskRunAll(cmd *cobra.Command, args []string) { - qname, err := cmd.Flags().GetString("queue") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - state, err := cmd.Flags().GetString("state") - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - - i := createInspector() - var n int - switch state { - case "scheduled": - n, err = i.RunAllScheduledTasks(qname) - case "retry": - 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) - } - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - } - fmt.Printf("%d tasks are now pending\n", n) -} diff --git a/tools/asynq/main.go b/tools/asynq/main.go deleted file mode 100644 index d26ad61a0..000000000 --- a/tools/asynq/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "github.com/AsynqLab/asynq/tools/asynq/cmd" - -func main() { - cmd.Execute() -} diff --git a/tools/go.mod b/tools/go.mod deleted file mode 100644 index cc8cc93e8..000000000 --- a/tools/go.mod +++ /dev/null @@ -1,56 +0,0 @@ -module github.com/AsynqLab/asynq/tools - -go 1.23 - -toolchain go1.23.1 - -require ( - github.com/AsynqLab/asynq v0.24.2 - github.com/AsynqLab/asynq/x v0.0.0-20241002030631-95a67ed7ab45 - github.com/MakeNowJust/heredoc/v2 v2.0.1 - github.com/fatih/color v1.17.0 - github.com/gdamore/tcell/v2 v2.5.1 - github.com/google/go-cmp v0.6.0 - github.com/mattn/go-runewidth v0.0.16 - github.com/mitchellh/go-homedir v1.1.0 - github.com/prometheus/client_golang v1.19.1 - github.com/redis/go-redis/v9 v9.6.1 - github.com/spf13/cobra v1.8.1 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.7.0 - golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136 -) - -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/fsnotify/fsnotify v1.4.9 // indirect - github.com/gdamore/encoding v1.0.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.1.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/mitchellh/mapstructure v1.1.2 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml v1.2.0 // 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/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.25.0 // indirect - golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.6.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect - gopkg.in/ini.v1 v1.51.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect -) diff --git a/tools/go.sum b/tools/go.sum deleted file mode 100644 index 54dea5d98..000000000 --- a/tools/go.sum +++ /dev/null @@ -1,394 +0,0 @@ -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= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -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/AsynqLab/asynq v0.24.2 h1:Tiu73EJvY53GnK8yzilDA+NOAiI0y4oPkC+AdrPNGwE= -github.com/AsynqLab/asynq v0.24.2/go.mod h1:ojJLg8KA63k/mZhZlA5a/3lGPkV9EY92sMOeywGaV6Y= -github.com/AsynqLab/asynq/x v0.0.0-20241002025248-ccf7a3b04d42 h1:ikKcGkiYqALIos6KK++or18cdIrfBIpqFEs1eBICRCY= -github.com/AsynqLab/asynq/x v0.0.0-20241002025248-ccf7a3b04d42/go.mod h1:u4tJVVJ21WCcsZqAlby0RrN3qPbf1t5OhDoBXiydXVg= -github.com/AsynqLab/asynq/x v0.0.0-20241002030631-95a67ed7ab45 h1:sKIuLTlXmHpwdnLpsydlrjqkb1mFC/WuDvk3u6nK74s= -github.com/AsynqLab/asynq/x v0.0.0-20241002030631-95a67ed7ab45/go.mod h1:6xeZr3GOcP16itt7699OFzNmvOrBuI4pMrCD3E9mPfI= -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/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -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/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/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -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/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= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= -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= -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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -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/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= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -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/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.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= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/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/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.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.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/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= -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.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -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.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -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/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= -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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/russross/blackfriday/v2 v2.1.0/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= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -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.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -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 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= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -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= -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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -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-20181023162649-9b4f9f5ad519/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-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -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/oauth2 v0.0.0-20190604053449-0f29369cfe45/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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -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-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -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-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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.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= -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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -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= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -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/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= -gopkg.in/yaml.v2 v2.2.4/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/tools/metrics_exporter/main.go b/tools/metrics_exporter/main.go deleted file mode 100644 index 69b174efe..000000000 --- a/tools/metrics_exporter/main.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "net/http" - - "github.com/AsynqLab/asynq" - "github.com/AsynqLab/asynq/x/metrics" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -// Declare command-line flags. -// These variables are binded to flags in init(). -var ( - flagRedisAddr string - flagRedisDB int - flagRedisPassword string - flagRedisUsername string - flagPort int -) - -func init() { - flag.StringVar(&flagRedisAddr, "redis-addr", "127.0.0.1:6379", "host:port of redis server to connect to") - flag.IntVar(&flagRedisDB, "redis-db", 0, "redis DB number to use") - flag.StringVar(&flagRedisPassword, "redis-password", "", "password used to connect to redis server") - flag.StringVar(&flagRedisUsername, "redis-username", "", "username used to connect to redis server") - flag.IntVar(&flagPort, "port", 9876, "port to use for the HTTP server") -} - -func main() { - flag.Parse() - // Using NewPedanticRegistry here to test the implementation of Collectors and Metrics. - reg := prometheus.NewPedanticRegistry() - - inspector := asynq.NewInspector(asynq.RedisClientOpt{ - Addr: flagRedisAddr, - DB: flagRedisDB, - Password: flagRedisPassword, - Username: flagRedisUsername, - }) - - reg.MustRegister( - metrics.NewQueueMetricsCollector(inspector), - // Add the standard process and go metrics to the registry - collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), - collectors.NewGoCollector(), - ) - - http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{})) - log.Printf("exporter server is listening on port: %d\n", flagPort) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil)) -} diff --git a/x/go.mod b/x/go.mod deleted file mode 100644 index cb5bcb406..000000000 --- a/x/go.mod +++ /dev/null @@ -1,27 +0,0 @@ -module github.com/AsynqLab/asynq/x - -go 1.23 - -toolchain go1.23.1 - -require ( - github.com/AsynqLab/asynq v0.24.2 - github.com/google/uuid v1.6.0 - github.com/prometheus/client_golang v1.20.4 - github.com/redis/go-redis/v9 v9.6.1 -) - -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.25.0 // indirect - golang.org/x/time v0.6.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect -) diff --git a/x/go.sum b/x/go.sum deleted file mode 100644 index 73c993f1e..000000000 --- a/x/go.sum +++ /dev/null @@ -1,50 +0,0 @@ -github.com/AsynqLab/asynq v0.24.2 h1:Tiu73EJvY53GnK8yzilDA+NOAiI0y4oPkC+AdrPNGwE= -github.com/AsynqLab/asynq v0.24.2/go.mod h1:ojJLg8KA63k/mZhZlA5a/3lGPkV9EY92sMOeywGaV6Y= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bsm/ginkgo/v2 v2.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/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/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.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= -github.com/prometheus/client_golang v1.20.4/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.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= -github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/x/metrics/metrics.go b/x/metrics/metrics.go deleted file mode 100644 index f7f590532..000000000 --- a/x/metrics/metrics.go +++ /dev/null @@ -1,190 +0,0 @@ -// Package metrics provides implementations of prometheus.Collector to collect Asynq queue metrics. -package metrics - -import ( - "fmt" - "log" - - "github.com/AsynqLab/asynq" - "github.com/prometheus/client_golang/prometheus" -) - -// Namespace used in fully-qualified metrics names. -const namespace = "asynq" - -// QueueMetricsCollector gathers queue metrics. -// It implements prometheus.Collector interface. -// -// All metrics exported from this collector have prefix "asynq". -type QueueMetricsCollector struct { - inspector *asynq.Inspector -} - -// collectQueueInfo gathers QueueInfo of all queues. -// Since this operation is expensive, it must be called once per collection. -func (qmc *QueueMetricsCollector) collectQueueInfo() ([]*asynq.QueueInfo, error) { - qnames, err := qmc.inspector.Queues() - if err != nil { - return nil, fmt.Errorf("failed to get queue names: %v", err) - } - infos := make([]*asynq.QueueInfo, len(qnames)) - for i, qname := range qnames { - qinfo, err := qmc.inspector.GetQueueInfo(qname) - if err != nil { - return nil, fmt.Errorf("failed to get queue info: %v", err) - } - infos[i] = qinfo - } - return infos, nil -} - -// Descriptors used by QueueMetricsCollector -var ( - tasksQueuedDesc = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "tasks_enqueued_total"), - "Number of tasks enqueued; broken down by queue and state.", - []string{"queue", "state"}, nil, - ) - - queueSizeDesc = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "queue_size"), - "Number of tasks in a queue", - []string{"queue"}, nil, - ) - - queueLatencyDesc = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "queue_latency_seconds"), - "Number of seconds the oldest pending task is waiting in pending state to be processed.", - []string{"queue"}, nil, - ) - - queueMemUsgDesc = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "queue_memory_usage_approx_bytes"), - "Number of memory used by a given queue (approximated number by sampling).", - []string{"queue"}, nil, - ) - - tasksProcessedTotalDesc = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "tasks_processed_total"), - "Number of tasks processed (both succeeded and failed); broken down by queue", - []string{"queue"}, nil, - ) - - tasksFailedTotalDesc = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "tasks_failed_total"), - "Number of tasks failed; broken down by queue", - []string{"queue"}, nil, - ) - - pausedQueues = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "queue_paused_total"), - "Number of queues paused", - []string{"queue"}, nil, - ) -) - -func (qmc *QueueMetricsCollector) Describe(ch chan<- *prometheus.Desc) { - prometheus.DescribeByCollect(qmc, ch) -} - -func (qmc *QueueMetricsCollector) Collect(ch chan<- prometheus.Metric) { - queueInfos, err := qmc.collectQueueInfo() - if err != nil { - log.Printf("Failed to collect metrics data: %v", err) - } - for _, info := range queueInfos { - ch <- prometheus.MustNewConstMetric( - tasksQueuedDesc, - prometheus.GaugeValue, - float64(info.Active), - info.Queue, - "active", - ) - ch <- prometheus.MustNewConstMetric( - tasksQueuedDesc, - prometheus.GaugeValue, - float64(info.Pending), - info.Queue, - "pending", - ) - ch <- prometheus.MustNewConstMetric( - tasksQueuedDesc, - prometheus.GaugeValue, - float64(info.Scheduled), - info.Queue, - "scheduled", - ) - ch <- prometheus.MustNewConstMetric( - tasksQueuedDesc, - prometheus.GaugeValue, - float64(info.Retry), - info.Queue, - "retry", - ) - ch <- prometheus.MustNewConstMetric( - tasksQueuedDesc, - prometheus.GaugeValue, - float64(info.Archived), - info.Queue, - "archived", - ) - ch <- prometheus.MustNewConstMetric( - tasksQueuedDesc, - prometheus.GaugeValue, - float64(info.Completed), - info.Queue, - "completed", - ) - - ch <- prometheus.MustNewConstMetric( - queueSizeDesc, - prometheus.GaugeValue, - float64(info.Size), - info.Queue, - ) - - ch <- prometheus.MustNewConstMetric( - queueLatencyDesc, - prometheus.GaugeValue, - info.Latency.Seconds(), - info.Queue, - ) - - ch <- prometheus.MustNewConstMetric( - queueMemUsgDesc, - prometheus.GaugeValue, - float64(info.MemoryUsage), - info.Queue, - ) - - ch <- prometheus.MustNewConstMetric( - tasksProcessedTotalDesc, - prometheus.CounterValue, - float64(info.ProcessedTotal), - info.Queue, - ) - - ch <- prometheus.MustNewConstMetric( - tasksFailedTotalDesc, - prometheus.CounterValue, - float64(info.FailedTotal), - info.Queue, - ) - - pausedValue := 0 // zero to indicate "not paused" - if info.Paused { - pausedValue = 1 - } - ch <- prometheus.MustNewConstMetric( - pausedQueues, - prometheus.GaugeValue, - float64(pausedValue), - info.Queue, - ) - } -} - -// NewQueueMetricsCollector returns a collector that exports metrics about Asynq queues. -func NewQueueMetricsCollector(inspector *asynq.Inspector) *QueueMetricsCollector { - return &QueueMetricsCollector{inspector: inspector} -} diff --git a/x/rate/example_test.go b/x/rate/example_test.go deleted file mode 100644 index ef563f03f..000000000 --- a/x/rate/example_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package rate_test - -import ( - "context" - "fmt" - "time" - - "github.com/AsynqLab/asynq" - "github.com/AsynqLab/asynq/x/rate" -) - -type RateLimitError struct { - RetryIn time.Duration -} - -func (e *RateLimitError) Error() string { - return fmt.Sprintf("rate limited (retry in %v)", e.RetryIn) -} - -func ExampleNewSemaphore() { - redisConnOpt := asynq.RedisClientOpt{Addr: ":6379"} - sema := rate.NewSemaphore(redisConnOpt, "my_queue", 10) - // call sema.Close() when appropriate - - _ = asynq.HandlerFunc(func(ctx context.Context, task *asynq.Task) error { - ok, err := sema.Acquire(ctx) - if err != nil { - return err - } - if !ok { - return &RateLimitError{RetryIn: 30 * time.Second} - } - - // Make sure to release the token once we're done. - defer sema.Release(ctx) - - // Process task - return nil - }) -} diff --git a/x/rate/semaphore.go b/x/rate/semaphore.go deleted file mode 100644 index addcb58c7..000000000 --- a/x/rate/semaphore.go +++ /dev/null @@ -1,114 +0,0 @@ -// Package rate contains rate limiting strategies for asynq.Handler(s). -package rate - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/AsynqLab/asynq" - asynqcontext "github.com/AsynqLab/asynq/internal/context" - "github.com/redis/go-redis/v9" -) - -// NewSemaphore creates a counting Semaphore for the given scope with the given number of tokens. -func NewSemaphore(rco asynq.RedisConnOpt, scope string, maxTokens int) *Semaphore { - rc, ok := rco.MakeRedisClient().(redis.UniversalClient) - if !ok { - panic(fmt.Sprintf("rate.NewSemaphore: unsupported RedisConnOpt type %T", rco)) - } - - if maxTokens < 1 { - panic("rate.NewSemaphore: maxTokens cannot be less than 1") - } - - if len(strings.TrimSpace(scope)) == 0 { - panic("rate.NewSemaphore: scope should not be empty") - } - - return &Semaphore{ - rc: rc, - scope: scope, - maxTokens: maxTokens, - } -} - -// Semaphore is a distributed counting semaphore which can be used to set maxTokens across multiple asynq servers. -type Semaphore struct { - rc redis.UniversalClient - maxTokens int - scope string -} - -// KEYS[1] -> asynq:sema: -// ARGV[1] -> max concurrency -// ARGV[2] -> current time in unix time -// ARGV[3] -> deadline in unix time -// ARGV[4] -> task ID -var acquireCmd = redis.NewScript(` -redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", tonumber(ARGV[2])-1) -local count = redis.call("ZCARD", KEYS[1]) - -if (count < tonumber(ARGV[1])) then - redis.call("ZADD", KEYS[1], ARGV[3], ARGV[4]) - return 'true' -else - return 'false' -end -`) - -// Acquire attempts to acquire a token from the semaphore. -// - Returns (true, nil), iff semaphore key exists and current value is less than maxTokens -// - Returns (false, nil) when token cannot be acquired -// - Returns (false, error) otherwise -// -// The context.Context passed to Acquire must have a deadline set, -// this ensures that token is released if the job goroutine crashes and does not call Release. -func (s *Semaphore) Acquire(ctx context.Context) (bool, error) { - d, ok := ctx.Deadline() - if !ok { - return false, fmt.Errorf("provided context must have a deadline") - } - - taskID, ok := asynqcontext.GetTaskID(ctx) - if !ok { - return false, fmt.Errorf("provided context is missing task ID value") - } - - return acquireCmd.Run(ctx, s.rc, - []string{semaphoreKey(s.scope)}, - s.maxTokens, - time.Now().Unix(), - d.Unix(), - taskID, - ).Bool() -} - -// Release will release the token on the counting semaphore. -func (s *Semaphore) Release(ctx context.Context) error { - taskID, ok := asynqcontext.GetTaskID(ctx) - if !ok { - return fmt.Errorf("provided context is missing task ID value") - } - - n, err := s.rc.ZRem(ctx, semaphoreKey(s.scope), taskID).Result() - if err != nil { - return fmt.Errorf("redis command failed: %v", err) - } - - if n == 0 { - return fmt.Errorf("no token found for task %q", taskID) - } - - return nil -} - -// Close closes the connection to redis. -func (s *Semaphore) Close() error { - return s.rc.Close() -} - -func semaphoreKey(scope string) string { - return fmt.Sprintf("asynq:sema:%s", scope) -} diff --git a/x/rate/semaphore_test.go b/x/rate/semaphore_test.go deleted file mode 100644 index 344157e05..000000000 --- a/x/rate/semaphore_test.go +++ /dev/null @@ -1,407 +0,0 @@ -package rate - -import ( - "context" - "flag" - "fmt" - "strings" - "testing" - "time" - - "github.com/AsynqLab/asynq" - "github.com/AsynqLab/asynq/internal/base" - asynqcontext "github.com/AsynqLab/asynq/internal/context" - "github.com/google/uuid" - "github.com/redis/go-redis/v9" -) - -var ( - redisAddr string - redisDB int - - useRedisCluster bool - redisClusterAddrs string // comma-separated list of host:port -) - -func init() { - flag.StringVar(&redisAddr, "redis_addr", "localhost:6379", "redis address to use in testing") - flag.IntVar(&redisDB, "redis_db", 14, "redis db number to use in testing") - flag.BoolVar(&useRedisCluster, "redis_cluster", false, "use redis cluster as a broker in testing") - flag.StringVar(&redisClusterAddrs, "redis_cluster_addrs", "localhost:7000,localhost:7001,localhost:7002", "comma separated list of redis server addresses") -} - -func TestNewSemaphore(t *testing.T) { - tests := []struct { - desc string - name string - maxConcurrency int - wantPanic string - connOpt asynq.RedisConnOpt - }{ - { - desc: "Bad RedisConnOpt", - wantPanic: "rate.NewSemaphore: unsupported RedisConnOpt type *rate.badConnOpt", - connOpt: &badConnOpt{}, - }, - { - desc: "Zero maxTokens should panic", - wantPanic: "rate.NewSemaphore: maxTokens cannot be less than 1", - }, - { - desc: "Empty scope should panic", - maxConcurrency: 2, - name: " ", - wantPanic: "rate.NewSemaphore: scope should not be empty", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - if tt.wantPanic != "" { - defer func() { - if r := recover(); r.(string) != tt.wantPanic { - t.Errorf("%s;\nNewSemaphore should panic with msg: %s, got %s", tt.desc, tt.wantPanic, r.(string)) - } - }() - } - - opt := tt.connOpt - if tt.connOpt == nil { - opt = getRedisConnOpt(t) - } - - sema := NewSemaphore(opt, tt.name, tt.maxConcurrency) - defer sema.Close() - }) - } -} - -func TestNewSemaphore_Acquire(t *testing.T) { - tests := []struct { - desc string - name string - maxConcurrency int - taskIDs []string - ctxFunc func(string) (context.Context, context.CancelFunc) - want []bool - }{ - { - desc: "Should acquire token when current token count is less than maxTokens", - name: "task-1", - maxConcurrency: 3, - taskIDs: []string{uuid.NewString(), uuid.NewString()}, - ctxFunc: func(id string) (context.Context, context.CancelFunc) { - return asynqcontext.New(context.Background(), &base.TaskMessage{ - ID: id, - Queue: "task-1", - }, time.Now().Add(time.Second)) - }, - want: []bool{true, true}, - }, - { - desc: "Should fail acquiring token when current token count is equal to maxTokens", - name: "task-2", - maxConcurrency: 3, - taskIDs: []string{uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString()}, - ctxFunc: func(id string) (context.Context, context.CancelFunc) { - return asynqcontext.New(context.Background(), &base.TaskMessage{ - ID: id, - Queue: "task-2", - }, time.Now().Add(time.Second)) - }, - want: []bool{true, true, true, false}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - opt := getRedisConnOpt(t) - rc := opt.MakeRedisClient().(redis.UniversalClient) - defer rc.Close() - - if err := rc.Del(context.Background(), semaphoreKey(tt.name)).Err(); err != nil { - t.Errorf("%s;\nredis.UniversalClient.Del() got error %v", tt.desc, err) - } - - sema := NewSemaphore(opt, tt.name, tt.maxConcurrency) - defer sema.Close() - - for i := 0; i < len(tt.taskIDs); i++ { - ctx, cancel := tt.ctxFunc(tt.taskIDs[i]) - - got, err := sema.Acquire(ctx) - if err != nil { - t.Errorf("%s;\nSemaphore.Acquire() got error %v", tt.desc, err) - } - - if got != tt.want[i] { - t.Errorf("%s;\nSemaphore.Acquire(ctx) returned %v, want %v", tt.desc, got, tt.want[i]) - } - - cancel() - } - }) - } -} - -func TestNewSemaphore_Acquire_Error(t *testing.T) { - tests := []struct { - desc string - name string - maxConcurrency int - taskIDs []string - ctxFunc func(string) (context.Context, context.CancelFunc) - errStr string - }{ - { - desc: "Should return error if context has no deadline", - name: "task-3", - maxConcurrency: 1, - taskIDs: []string{uuid.NewString(), uuid.NewString()}, - ctxFunc: func(id string) (context.Context, context.CancelFunc) { - return context.Background(), func() {} - }, - errStr: "provided context must have a deadline", - }, - { - desc: "Should return error when context is missing taskID", - name: "task-4", - maxConcurrency: 1, - taskIDs: []string{uuid.NewString()}, - ctxFunc: func(_ string) (context.Context, context.CancelFunc) { - return context.WithTimeout(context.Background(), time.Second) - }, - errStr: "provided context is missing task ID value", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - opt := getRedisConnOpt(t) - rc := opt.MakeRedisClient().(redis.UniversalClient) - defer rc.Close() - - if err := rc.Del(context.Background(), semaphoreKey(tt.name)).Err(); err != nil { - t.Errorf("%s;\nredis.UniversalClient.Del() got error %v", tt.desc, err) - } - - sema := NewSemaphore(opt, tt.name, tt.maxConcurrency) - defer sema.Close() - - for i := 0; i < len(tt.taskIDs); i++ { - ctx, cancel := tt.ctxFunc(tt.taskIDs[i]) - - _, err := sema.Acquire(ctx) - if err == nil || err.Error() != tt.errStr { - t.Errorf("%s;\nSemaphore.Acquire() got error %v want error %v", tt.desc, err, tt.errStr) - } - - cancel() - } - }) - } -} - -func TestNewSemaphore_Acquire_StaleToken(t *testing.T) { - opt := getRedisConnOpt(t) - rc := opt.MakeRedisClient().(redis.UniversalClient) - defer rc.Close() - - taskID := uuid.NewString() - - // 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{ - Score: float64(time.Now().Add(-10 * time.Second).Unix()), - Member: taskID, - }) - - sema := NewSemaphore(opt, "stale-token", 1) - defer sema.Close() - - ctx, cancel := asynqcontext.New(context.Background(), &base.TaskMessage{ - ID: taskID, - Queue: "task-1", - }, time.Now().Add(time.Second)) - defer cancel() - - got, err := sema.Acquire(ctx) - if err != nil { - t.Errorf("Acquire_StaleToken;\nSemaphore.Acquire() got error %v", err) - } - - if !got { - t.Error("Acquire_StaleToken;\nSemaphore.Acquire() got false want true") - } -} - -func TestNewSemaphore_Release(t *testing.T) { - tests := []struct { - desc string - name string - taskIDs []string - ctxFunc func(string) (context.Context, context.CancelFunc) - wantCount int64 - }{ - { - desc: "Should decrease token count", - name: "task-5", - taskIDs: []string{uuid.NewString()}, - ctxFunc: func(id string) (context.Context, context.CancelFunc) { - return asynqcontext.New(context.Background(), &base.TaskMessage{ - ID: id, - Queue: "task-3", - }, time.Now().Add(time.Second)) - }, - }, - { - desc: "Should decrease token count by 2", - name: "task-6", - taskIDs: []string{uuid.NewString(), uuid.NewString()}, - ctxFunc: func(id string) (context.Context, context.CancelFunc) { - return asynqcontext.New(context.Background(), &base.TaskMessage{ - ID: id, - Queue: "task-4", - }, time.Now().Add(time.Second)) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - opt := getRedisConnOpt(t) - rc := opt.MakeRedisClient().(redis.UniversalClient) - defer rc.Close() - - if err := rc.Del(context.Background(), semaphoreKey(tt.name)).Err(); err != nil { - t.Errorf("%s;\nredis.UniversalClient.Del() got error %v", tt.desc, err) - } - - var members []redis.Z - for i := 0; i < len(tt.taskIDs); i++ { - members = append(members, redis.Z{ - Score: float64(time.Now().Add(time.Duration(i) * time.Second).Unix()), - Member: tt.taskIDs[i], - }) - } - if err := rc.ZAdd(context.Background(), semaphoreKey(tt.name), members...).Err(); err != nil { - t.Errorf("%s;\nredis.UniversalClient.ZAdd() got error %v", tt.desc, err) - } - - sema := NewSemaphore(opt, tt.name, 3) - defer sema.Close() - - for i := 0; i < len(tt.taskIDs); i++ { - ctx, cancel := tt.ctxFunc(tt.taskIDs[i]) - - if err := sema.Release(ctx); err != nil { - t.Errorf("%s;\nSemaphore.Release() got error %v", tt.desc, err) - } - - cancel() - } - - i, err := rc.ZCount(context.Background(), semaphoreKey(tt.name), "-inf", "+inf").Result() - if err != nil { - t.Errorf("%s;\nredis.UniversalClient.ZCount() got error %v", tt.desc, err) - } - - if i != tt.wantCount { - t.Errorf("%s;\nSemaphore.Release(ctx) didn't release token, got %v want 0", tt.desc, i) - } - }) - } -} - -func TestNewSemaphore_Release_Error(t *testing.T) { - testID := uuid.NewString() - - tests := []struct { - desc string - name string - taskIDs []string - ctxFunc func(string) (context.Context, context.CancelFunc) - errStr string - }{ - { - desc: "Should return error when context is missing taskID", - name: "task-7", - taskIDs: []string{uuid.NewString()}, - ctxFunc: func(_ string) (context.Context, context.CancelFunc) { - return context.WithTimeout(context.Background(), time.Second) - }, - errStr: "provided context is missing task ID value", - }, - { - desc: "Should return error when context has taskID which never acquired token", - name: "task-8", - taskIDs: []string{uuid.NewString()}, - ctxFunc: func(_ string) (context.Context, context.CancelFunc) { - return asynqcontext.New(context.Background(), &base.TaskMessage{ - ID: testID, - Queue: "task-4", - }, time.Now().Add(time.Second)) - }, - errStr: fmt.Sprintf("no token found for task %q", testID), - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - opt := getRedisConnOpt(t) - rc := opt.MakeRedisClient().(redis.UniversalClient) - defer rc.Close() - - if err := rc.Del(context.Background(), semaphoreKey(tt.name)).Err(); err != nil { - t.Errorf("%s;\nredis.UniversalClient.Del() got error %v", tt.desc, err) - } - - var members []redis.Z - for i := 0; i < len(tt.taskIDs); i++ { - members = append(members, redis.Z{ - Score: float64(time.Now().Add(time.Duration(i) * time.Second).Unix()), - Member: tt.taskIDs[i], - }) - } - if err := rc.ZAdd(context.Background(), semaphoreKey(tt.name), members...).Err(); err != nil { - t.Errorf("%s;\nredis.UniversalClient.ZAdd() got error %v", tt.desc, err) - } - - sema := NewSemaphore(opt, tt.name, 3) - defer sema.Close() - - for i := 0; i < len(tt.taskIDs); i++ { - ctx, cancel := tt.ctxFunc(tt.taskIDs[i]) - - if err := sema.Release(ctx); err == nil || err.Error() != tt.errStr { - t.Errorf("%s;\nSemaphore.Release() got error %v want error %v", tt.desc, err, tt.errStr) - } - - cancel() - } - }) - } -} - -func getRedisConnOpt(tb testing.TB) asynq.RedisConnOpt { - tb.Helper() - if useRedisCluster { - addrs := strings.Split(redisClusterAddrs, ",") - if len(addrs) == 0 { - tb.Fatal("No redis cluster addresses provided. Please set addresses using --redis_cluster_addrs flag.") - } - return asynq.RedisClusterClientOpt{ - Addrs: addrs, - } - } - return asynq.RedisClientOpt{ - Addr: redisAddr, - DB: redisDB, - } -} - -type badConnOpt struct{} - -func (b badConnOpt) MakeRedisClient() interface{} { - return nil -}