2
0
mirror of https://github.com/hibiken/asynq.git synced 2025-10-03 17:22:01 +08:00

(cli): Refactor dash package

This commit is contained in:
Ken Hibino
2022-05-22 10:05:22 -07:00
parent 2d5ca43424
commit 500e0c02ea
6 changed files with 154 additions and 66 deletions

View File

@@ -72,13 +72,19 @@ func Run(opts Options) {
fmt.Printf("failed to initialize screen: %v\n", err) fmt.Printf("failed to initialize screen: %v\n", err)
os.Exit(1) os.Exit(1)
} }
// Set default text style s.SetStyle(baseStyle) // set default text style
s.SetStyle(baseStyle) opts.PollInterval = 2 * time.Second
inspector := asynq.NewInspector(asynq.RedisClientOpt{Addr: ":6379"})
// channels to send/receive data fetched asynchronously
var ( var (
state = State{} // confined in this goroutine only; DO NOT SHARE
inspector = asynq.NewInspector(asynq.RedisClientOpt{Addr: ":6379"})
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) errorCh = make(chan error)
queueCh = make(chan *asynq.QueueInfo) queueCh = make(chan *asynq.QueueInfo)
queuesCh = make(chan []*asynq.QueueInfo) queuesCh = make(chan []*asynq.QueueInfo)
@@ -86,17 +92,6 @@ func Run(opts Options) {
tasksCh = make(chan []*asynq.TaskInfo) tasksCh = make(chan []*asynq.TaskInfo)
redisInfoCh = make(chan *redisInfo) redisInfoCh = make(chan *redisInfo)
) )
go fetchQueues(inspector, queuesCh, errorCh, opts)
var state State // contained in this goroutine only; do not share
// draw initial screen
drawDash(s, &state, opts)
eventCh := make(chan tcell.Event)
done := make(chan struct{})
opts.PollInterval = 2 * time.Second
ticker := time.NewTicker(opts.PollInterval)
defer ticker.Stop() defer ticker.Stop()
f := dataFetcher{ f := dataFetcher{
@@ -111,16 +106,22 @@ func Run(opts Options) {
redisInfoCh, redisInfoCh,
} }
d := dashDrawer{
s,
opts,
}
h := keyEventHandler{ h := keyEventHandler{
s: s, s: s,
fetcher: &f, fetcher: &f,
drawer: &d,
state: &state, state: &state,
opts: opts,
done: done, done: done,
} }
// TODO: Double check that we are not leaking goroutine with this one. go fetchQueues(inspector, queuesCh, errorCh, opts)
go s.ChannelEvents(eventCh, done) go s.ChannelEvents(eventCh, done) // TODO: Double check that we are not leaking goroutine with this one.
d.draw(&state) // draw initial screen
for { for {
// Update screen // Update screen
@@ -158,31 +159,31 @@ func Run(opts Options) {
case queues := <-queuesCh: case queues := <-queuesCh:
state.queues = queues state.queues = queues
state.err = nil state.err = nil
drawDash(s, &state, opts) d.draw(&state)
case q := <-queueCh: case q := <-queueCh:
state.selectedQueue = q state.selectedQueue = q
state.err = nil state.err = nil
drawDash(s, &state, opts) d.draw(&state)
case groups := <-groupsCh: case groups := <-groupsCh:
state.groups = groups state.groups = groups
state.err = nil state.err = nil
drawDash(s, &state, opts) d.draw(&state)
case tasks := <-tasksCh: case tasks := <-tasksCh:
state.tasks = tasks state.tasks = tasks
state.err = nil state.err = nil
drawDash(s, &state, opts) d.draw(&state)
case redisInfo := <-redisInfoCh: case redisInfo := <-redisInfoCh:
state.redisInfo = *redisInfo state.redisInfo = *redisInfo
state.err = nil state.err = nil
drawDash(s, &state, opts) d.draw(&state)
case err := <-errorCh: case err := <-errorCh:
state.err = err state.err = err
drawDash(s, &state, opts) d.draw(&state)
} }
} }

View File

@@ -16,7 +16,18 @@ import (
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
) )
func drawDash(s tcell.Screen, state *State, opts Options) { // 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() s.Clear()
// Simulate data update on every render // Simulate data update on every render
d := NewScreenDrawer(s) d := NewScreenDrawer(s)

View File

@@ -11,6 +11,15 @@ import (
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
) )
type fetcher interface {
fetchQueues()
fetchQueueInfo(qname string)
fetchRedisInfo()
fetchTasks(qname string, taskState asynq.TaskState, pageSize, pageNum int)
fetchAggregatingTasks(qname, group string, pageSize, pageNum int)
fetchGroups(qname string)
}
type dataFetcher struct { type dataFetcher struct {
ticker *time.Ticker ticker *time.Ticker
inspector *asynq.Inspector inspector *asynq.Inspector

View File

@@ -11,13 +11,15 @@ import (
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
) )
// keyEventHandler handles keyboard events and updates the state.
// It delegates data fetching to fetcher and UI rendering to drawer.
type keyEventHandler struct { type keyEventHandler struct {
s tcell.Screen s tcell.Screen
state *State state *State
opts Options
done chan struct{} done chan struct{}
fetcher *dataFetcher fetcher fetcher
drawer drawer
} }
func (h *keyEventHandler) quit() { func (h *keyEventHandler) quit() {
@@ -62,16 +64,15 @@ func (h *keyEventHandler) HandleKeyEvent(ev *tcell.EventKey) {
func (h *keyEventHandler) goBack() { func (h *keyEventHandler) goBack() {
var ( var (
s = h.s
state = h.state state = h.state
opts = h.opts d = h.drawer
) )
if state.view == viewTypeHelp { if state.view == viewTypeHelp {
state.view = state.prevView // exit help state.view = state.prevView // exit help
drawDash(s, state, opts) d.draw(state)
} else if state.view == viewTypeQueueDetails { } else if state.view == viewTypeQueueDetails {
state.view = viewTypeQueues state.view = viewTypeQueues
drawDash(s, state, opts) d.draw(state)
} else { } else {
h.quit() h.quit()
} }
@@ -92,11 +93,11 @@ func (h *keyEventHandler) downKeyQueues() {
} else { } else {
h.state.queueTableRowIdx = 0 // loop back h.state.queueTableRowIdx = 0 // loop back
} }
drawDash(h.s, h.state, h.opts) h.drawer.draw(h.state)
} }
func (h *keyEventHandler) downKeyQueueDetails() { func (h *keyEventHandler) downKeyQueueDetails() {
s, state, opts := h.s, h.state, h.opts s, state := h.s, h.state
if shouldShowGroupTable(state) { if shouldShowGroupTable(state) {
if state.groupTableRowIdx < groupPageSize(s) { if state.groupTableRowIdx < groupPageSize(s) {
state.groupTableRowIdx++ state.groupTableRowIdx++
@@ -110,7 +111,7 @@ func (h *keyEventHandler) downKeyQueueDetails() {
state.taskTableRowIdx = 0 // loop back state.taskTableRowIdx = 0 // loop back
} }
} }
drawDash(s, state, opts) h.drawer.draw(state)
} }
func (h *keyEventHandler) handleUpKey() { func (h *keyEventHandler) handleUpKey() {
@@ -123,17 +124,17 @@ func (h *keyEventHandler) handleUpKey() {
} }
func (h *keyEventHandler) upKeyQueues() { func (h *keyEventHandler) upKeyQueues() {
s, state, opts := h.s, h.state, h.opts state := h.state
if state.queueTableRowIdx == 0 { if state.queueTableRowIdx == 0 {
state.queueTableRowIdx = len(state.queues) state.queueTableRowIdx = len(state.queues)
} else { } else {
state.queueTableRowIdx-- state.queueTableRowIdx--
} }
drawDash(s, state, opts) h.drawer.draw(state)
} }
func (h *keyEventHandler) upKeyQueueDetails() { func (h *keyEventHandler) upKeyQueueDetails() {
s, state, opts := h.s, h.state, h.opts s, state := h.s, h.state
if shouldShowGroupTable(state) { if shouldShowGroupTable(state) {
if state.groupTableRowIdx == 0 { if state.groupTableRowIdx == 0 {
state.groupTableRowIdx = groupPageSize(s) state.groupTableRowIdx = groupPageSize(s)
@@ -147,7 +148,7 @@ func (h *keyEventHandler) upKeyQueueDetails() {
state.taskTableRowIdx-- state.taskTableRowIdx--
} }
} }
drawDash(s, state, opts) h.drawer.draw(state)
} }
func (h *keyEventHandler) handleEnterKey() { func (h *keyEventHandler) handleEnterKey() {
@@ -163,8 +164,8 @@ func (h *keyEventHandler) enterKeyQueues() {
var ( var (
s = h.s s = h.s
state = h.state state = h.state
opts = h.opts
f = h.fetcher f = h.fetcher
d = h.drawer
) )
if state.queueTableRowIdx != 0 { if state.queueTableRowIdx != 0 {
state.selectedQueue = state.queues[state.queueTableRowIdx-1] state.selectedQueue = state.queues[state.queueTableRowIdx-1]
@@ -173,7 +174,7 @@ func (h *keyEventHandler) enterKeyQueues() {
state.tasks = nil state.tasks = nil
state.pageNum = 1 state.pageNum = 1
f.fetchTasks(state.selectedQueue.Queue, state.taskState, taskPageSize(s), state.pageNum) f.fetchTasks(state.selectedQueue.Queue, state.taskState, taskPageSize(s), state.pageNum)
drawDash(s, state, opts) d.draw(state)
} }
} }
@@ -181,15 +182,15 @@ func (h *keyEventHandler) enterKeyQueueDetails() {
var ( var (
s = h.s s = h.s
state = h.state state = h.state
opts = h.opts
f = h.fetcher f = h.fetcher
d = h.drawer
) )
if shouldShowGroupTable(state) && state.groupTableRowIdx != 0 { if shouldShowGroupTable(state) && state.groupTableRowIdx != 0 {
state.selectedGroup = state.groups[state.groupTableRowIdx-1] state.selectedGroup = state.groups[state.groupTableRowIdx-1]
state.tasks = nil state.tasks = nil
state.pageNum = 1 state.pageNum = 1
f.fetchAggregatingTasks(state.selectedQueue.Queue, state.selectedGroup.Group, taskPageSize(s), state.pageNum) f.fetchAggregatingTasks(state.selectedQueue.Queue, state.selectedGroup.Group, taskPageSize(s), state.pageNum)
drawDash(s, state, opts) d.draw(state)
} }
} }
@@ -197,8 +198,8 @@ func (h *keyEventHandler) handleLeftKey() {
var ( var (
s = h.s s = h.s
state = h.state state = h.state
opts = h.opts
f = h.fetcher f = h.fetcher
d = h.drawer
) )
if state.view == viewTypeQueueDetails { if state.view == viewTypeQueueDetails {
state.taskState = prevTaskState(state.taskState) state.taskState = prevTaskState(state.taskState)
@@ -211,7 +212,7 @@ func (h *keyEventHandler) handleLeftKey() {
} else { } else {
f.fetchTasks(state.selectedQueue.Queue, state.taskState, taskPageSize(s), state.pageNum) f.fetchTasks(state.selectedQueue.Queue, state.taskState, taskPageSize(s), state.pageNum)
} }
drawDash(s, state, opts) d.draw(state)
} }
} }
@@ -219,8 +220,8 @@ func (h *keyEventHandler) handleRightKey() {
var ( var (
s = h.s s = h.s
state = h.state state = h.state
opts = h.opts
f = h.fetcher f = h.fetcher
d = h.drawer
) )
if state.view == viewTypeQueueDetails { if state.view == viewTypeQueueDetails {
state.taskState = nextTaskState(state.taskState) state.taskState = nextTaskState(state.taskState)
@@ -233,7 +234,7 @@ func (h *keyEventHandler) handleRightKey() {
} else { } else {
f.fetchTasks(state.selectedQueue.Queue, state.taskState, taskPageSize(s), state.pageNum) f.fetchTasks(state.selectedQueue.Queue, state.taskState, taskPageSize(s), state.pageNum)
} }
drawDash(s, state, opts) d.draw(state)
} }
} }
@@ -241,8 +242,8 @@ func (h *keyEventHandler) nextPage() {
var ( var (
s = h.s s = h.s
state = h.state state = h.state
opts = h.opts
f = h.fetcher f = h.fetcher
d = h.drawer
) )
if state.view == viewTypeQueueDetails { if state.view == viewTypeQueueDetails {
if shouldShowGroupTable(state) { if shouldShowGroupTable(state) {
@@ -252,7 +253,7 @@ func (h *keyEventHandler) nextPage() {
end := start + pageSize end := start + pageSize
if end <= total { if end <= total {
state.pageNum++ state.pageNum++
drawDash(s, state, opts) d.draw(state)
} }
} else { } else {
pageSize := taskPageSize(s) pageSize := taskPageSize(s)
@@ -269,8 +270,8 @@ func (h *keyEventHandler) prevPage() {
var ( var (
s = h.s s = h.s
state = h.state state = h.state
opts = h.opts
f = h.fetcher f = h.fetcher
d = h.drawer
) )
if state.view == viewTypeQueueDetails { if state.view == viewTypeQueueDetails {
if shouldShowGroupTable(state) { if shouldShowGroupTable(state) {
@@ -278,7 +279,7 @@ func (h *keyEventHandler) prevPage() {
start := (state.pageNum - 1) * pageSize start := (state.pageNum - 1) * pageSize
if start > 0 { if start > 0 {
state.pageNum-- state.pageNum--
drawDash(s, state, opts) d.draw(state)
} }
} else { } else {
if state.pageNum > 1 { if state.pageNum > 1 {
@@ -291,67 +292,62 @@ func (h *keyEventHandler) prevPage() {
func (h *keyEventHandler) showQueues() { func (h *keyEventHandler) showQueues() {
var ( var (
s = h.s
state = h.state state = h.state
opts = h.opts
f = h.fetcher f = h.fetcher
d = h.drawer
) )
if state.view != viewTypeQueues { if state.view != viewTypeQueues {
f.fetchQueues() f.fetchQueues()
state.view = viewTypeQueues state.view = viewTypeQueues
drawDash(s, state, opts) d.draw(state)
} }
} }
func (h *keyEventHandler) showServers() { func (h *keyEventHandler) showServers() {
var ( var (
s = h.s
state = h.state state = h.state
opts = h.opts d = h.drawer
) )
if state.view != viewTypeServers { if state.view != viewTypeServers {
//TODO Start data fetch and reset ticker //TODO Start data fetch and reset ticker
state.view = viewTypeServers state.view = viewTypeServers
drawDash(s, state, opts) d.draw(state)
} }
} }
func (h *keyEventHandler) showSchedulers() { func (h *keyEventHandler) showSchedulers() {
var ( var (
s = h.s
state = h.state state = h.state
opts = h.opts d = h.drawer
) )
if state.view != viewTypeSchedulers { if state.view != viewTypeSchedulers {
//TODO Start data fetch and reset ticker //TODO Start data fetch and reset ticker
state.view = viewTypeSchedulers state.view = viewTypeSchedulers
drawDash(s, state, opts) d.draw(state)
} }
} }
func (h *keyEventHandler) showRedisInfo() { func (h *keyEventHandler) showRedisInfo() {
var ( var (
s = h.s
state = h.state state = h.state
opts = h.opts
f = h.fetcher f = h.fetcher
d = h.drawer
) )
if state.view != viewTypeRedis { if state.view != viewTypeRedis {
f.fetchRedisInfo() f.fetchRedisInfo()
state.view = viewTypeRedis state.view = viewTypeRedis
drawDash(s, state, opts) d.draw(state)
} }
} }
func (h *keyEventHandler) showHelp() { func (h *keyEventHandler) showHelp() {
var ( var (
s = h.s
state = h.state state = h.state
opts = h.opts d = h.drawer
) )
if state.view != viewTypeHelp { if state.view != viewTypeHelp {
state.prevView = state.view state.prevView = state.view
state.view = viewTypeHelp state.view = viewTypeHelp
drawDash(s, state, opts) d.draw(state)
} }
} }

View File

@@ -0,0 +1,70 @@
// Copyright 2022 Kentaro Hibino. All rights reserved.
// Use of this source code is governed by a MIT license
// that can be found in the LICENSE file.
package dash
import (
"testing"
"github.com/gdamore/tcell/v2"
"github.com/google/go-cmp/cmp"
"github.com/hibiken/asynq"
)
func makeKeyEventHandler(state *State) *keyEventHandler {
return &keyEventHandler{
s: tcell.NewSimulationScreen("UTF-8"),
state: state,
done: make(chan struct{}),
fetcher: &fakeFetcher{},
drawer: &fakeDrawer{},
}
}
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 page",
state: &State{view: viewTypeQueues},
events: []*tcell.EventKey{tcell.NewEventKey(tcell.KeyRune, '?', tcell.ModNone)},
wantState: State{view: viewTypeHelp},
},
// TODO: Add more tests
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
h := makeKeyEventHandler(tc.state)
for _, e := range tc.events {
h.HandleKeyEvent(e)
}
if diff := cmp.Diff(tc.wantState, *tc.state, cmp.AllowUnexported(State{}, redisInfo{})); 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) fetchQueues() {}
func (f *fakeFetcher) fetchQueueInfo(qname string) {}
func (f *fakeFetcher) fetchRedisInfo() {}
func (f *fakeFetcher) fetchTasks(qname string, taskState asynq.TaskState, pageSize, pageNum int) {}
func (f *fakeFetcher) fetchAggregatingTasks(qname, group string, pageSize, pageNum int) {}
func (f *fakeFetcher) fetchGroups(qname string) {}
type fakeDrawer struct{}
func (d *fakeDrawer) draw(s *State) {}

View File

@@ -7,6 +7,7 @@ require (
github.com/fatih/color v1.9.0 github.com/fatih/color v1.9.0
github.com/gdamore/tcell/v2 v2.5.1 github.com/gdamore/tcell/v2 v2.5.1
github.com/go-redis/redis/v8 v8.11.4 github.com/go-redis/redis/v8 v8.11.4
github.com/google/go-cmp v0.5.6
github.com/hibiken/asynq v0.23.0 github.com/hibiken/asynq v0.23.0
github.com/hibiken/asynq/x v0.0.0-20220131170841-349f4c50fb1d github.com/hibiken/asynq/x v0.0.0-20220131170841-349f4c50fb1d
github.com/mattn/go-runewidth v0.0.13 github.com/mattn/go-runewidth v0.0.13