Skip to content

Commit

Permalink
add sequencer coordinator management UI tool
Browse files Browse the repository at this point in the history
  • Loading branch information
ganeshvanahalli committed Aug 18, 2023
1 parent 56b8434 commit 810887c
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 2 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ WORKDIR /home/user
COPY --from=node-builder /workspace/target/bin/nitro /usr/local/bin/
COPY --from=node-builder /workspace/target/bin/relay /usr/local/bin/
COPY --from=node-builder /workspace/target/bin/nitro-val /usr/local/bin/
COPY --from=node-builder /workspace/target/bin/seq-coordinator-manager /usr/local/bin/
COPY --from=machine-versions /workspace/machines /home/user/target/machines
USER root
RUN export DEBIAN_FRONTEND=noninteractive && \
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ push: lint test-go .make/fmt
all: build build-replay-env test-gen-proofs
@touch .make/all

build: $(patsubst %,$(output_root)/bin/%, nitro deploy relay daserver datool seq-coordinator-invalidate nitro-val)
build: $(patsubst %,$(output_root)/bin/%, nitro deploy relay daserver datool seq-coordinator-invalidate nitro-val seq-coordinator-manager)
@printf $(done)

build-node-deps: $(go_source) build-prover-header build-prover-lib build-jit .make/solgen .make/cbrotli-lib
Expand Down Expand Up @@ -185,6 +185,9 @@ $(output_root)/bin/seq-coordinator-invalidate: $(DEP_PREDICATE) build-node-deps
$(output_root)/bin/nitro-val: $(DEP_PREDICATE) build-node-deps
go build $(GOLANG_PARAMS) -o $@ "$(CURDIR)/cmd/nitro-val"

$(output_root)/bin/seq-coordinator-manager: $(DEP_PREDICATE) build-node-deps
go build $(GOLANG_PARAMS) -o $@ "$(CURDIR)/cmd/seq-coordinator-manager"

# recompile wasm, but don't change timestamp unless files differ
$(replay_wasm): $(DEP_PREDICATE) $(go_source) .make/solgen
mkdir -p `dirname $(replay_wasm)`
Expand Down
77 changes: 77 additions & 0 deletions cmd/seq-coordinator-manager/rediscoordinator/redis_coordinator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package rediscoordinator

import (
"context"
"errors"
"strings"

"github.com/go-redis/redis/v8"
"github.com/offchainlabs/nitro/util/redisutil"
)

type RedisCoordinator struct {
Client redis.UniversalClient
}

func NewRedisCoordinator(redisURL string) (*RedisCoordinator, error) {
redisClient, err := redisutil.RedisClientFromURL(redisURL)
if err != nil {
return nil, err
}

return &RedisCoordinator{
Client: redisClient,
}, nil
}

func (rc *RedisCoordinator) GetPriorities(ctx context.Context) ([]string, map[string]int, error) {
prioritiesMap := make(map[string]int)
prioritiesString, err := rc.Client.Get(ctx, redisutil.PRIORITIES_KEY).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
err = errors.New("sequencer priorities unset")
}
return []string{}, prioritiesMap, err
}
priorities := strings.Split(prioritiesString, ",")
for _, url := range priorities {
prioritiesMap[url]++
}
return priorities, prioritiesMap, nil
}

func (rc *RedisCoordinator) GetLivelinessMap(ctx context.Context) (map[string]int, error) {
livelinessMap := make(map[string]int)
livelinessList, _, err := rc.Client.Scan(ctx, 0, redisutil.WANTS_LOCKOUT_KEY_PREFIX+"*", 0).Result()
if err != nil {
return livelinessMap, err
}
for _, elem := range livelinessList {
url := strings.TrimPrefix(elem, redisutil.WANTS_LOCKOUT_KEY_PREFIX)
livelinessMap[url]++
}
return livelinessMap, nil
}

func (rc *RedisCoordinator) UpdatePriorities(ctx context.Context, priorities []string) error {
prioritiesString := strings.Join(priorities, ",")
err := rc.Client.Set(ctx, redisutil.PRIORITIES_KEY, prioritiesString, 0).Err()
if err != nil {
if errors.Is(err, redis.Nil) {
err = errors.New("sequencer priorities unset")
}
}
return err
}

// CurrentChosenSequencer retrieves the current chosen sequencer holding the lock
func (c *RedisCoordinator) CurrentChosenSequencer(ctx context.Context) (string, error) {
current, err := c.Client.Get(ctx, redisutil.CHOSENSEQ_KEY).Result()
if errors.Is(err, redis.Nil) {
return "", nil
}
if err != nil {
return "", err
}
return current, nil
}
249 changes: 249 additions & 0 deletions cmd/seq-coordinator-manager/seq-coordinator-manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package main

import (
"context"
"fmt"
"os"
"strconv"

"github.com/enescakir/emoji"
"github.com/ethereum/go-ethereum/log"
"github.com/gdamore/tcell/v2"
"github.com/offchainlabs/nitro/cmd/seq-coordinator-manager/rediscoordinator"
"github.com/rivo/tview"
)

// Tview
var pages = tview.NewPages()
var app = tview.NewApplication()

// Lists
var prioritySeqList = tview.NewList().ShowSecondaryText(false)
var nonPrioritySeqList = tview.NewList().ShowSecondaryText(false)

// Forms
var addSeqForm = tview.NewForm()
var priorityForm = tview.NewForm()
var nonPriorityForm = tview.NewForm()

// Sequencer coordinator managment UI data store
type manager struct {
redisCoordinator *rediscoordinator.RedisCoordinator
prioritiesMap map[string]int
livelinessMap map[string]int
priorityList []string
nonPriorityList []string
}

func main() {
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()

args := os.Args[1:]
if len(args) != 1 {
fmt.Fprintf(os.Stderr, "Usage: redis-seq-manager [redis-url]\n")
os.Exit(1)
}
redisURL := args[0]
redisCoordinator, err := rediscoordinator.NewRedisCoordinator(redisURL)
if err != nil {
panic(err)
}

seqManager := &manager{
redisCoordinator: redisCoordinator,
prioritiesMap: make(map[string]int),
livelinessMap: make(map[string]int),
}

seqManager.refreshAllLists(ctx)
seqManager.populateLists(ctx)

prioritySeqList.SetSelectedFunc(func(index int, name string, second_name string, shortcut rune) {
nonPriorityForm.Clear(true)

n := len(seqManager.priorityList)
priorities := make([]string, n)
for i := 0; i < n; i++ {
priorities[i] = strconv.Itoa(i)
}

target := index
priorityForm.Clear(true)
priorityForm.AddTextView("Additional details:", "Status:\nBlockNumber:", 0, 2, false, true)
priorityForm.AddDropDown("Change priority to ->", priorities, index, func(priority string, selection int) {
target = selection
})
priorityForm.AddButton("Save", func() {
if target != index {
seqManager.updatePriorityList(ctx, index, target)
}
seqManager.populateLists(ctx)
pages.SwitchToPage("Menu")
})
})

nonPrioritySeqList.SetSelectedFunc(func(index int, name string, second_name string, shortcut rune) {
priorityForm.Clear(true)

n := len(seqManager.priorityList)
priorities := make([]string, n+1)
for i := 0; i < n+1; i++ {
priorities[i] = strconv.Itoa(i)
}

target := index
nonPriorityForm.Clear(true)
nonPriorityForm.AddTextView("Additional details:", "Status:\nBlockNumber:", 0, 2, false, true)
nonPriorityForm.AddDropDown("Set priority to ->", priorities, index, func(priority string, selection int) {
target = selection
})
nonPriorityForm.AddButton("Save", func() {
seqManager.priorityList = append(seqManager.priorityList, seqManager.nonPriorityList[index])
index = len(seqManager.priorityList) - 1
seqManager.updatePriorityList(ctx, index, target)
nonPriorityForm.Clear(true)
seqManager.populateLists(ctx)
pages.SwitchToPage("Menu")
})
})

// UI design
flex := tview.NewFlex()
priorityHeading := tview.NewTextView().
SetTextColor(tcell.ColorYellow).
SetText("-----Priority List-----")
nonPriorityHeading := tview.NewTextView().
SetTextColor(tcell.ColorYellow).
SetText("-----Not in priority list but online-----")
instructions := tview.NewTextView().
SetTextColor(tcell.ColorYellow).
SetText("(r) to refresh \n(a) to add sequencer\n(q) to quit")

flex.SetDirection(tview.FlexRow).
AddItem(priorityHeading, 0, 1, false).
AddItem(tview.NewFlex().
AddItem(prioritySeqList, 0, 2, true).
AddItem(priorityForm, 0, 3, false), 0, 12, false).
AddItem(nonPriorityHeading, 0, 1, false).
AddItem(tview.NewFlex().
AddItem(nonPrioritySeqList, 0, 2, true).
AddItem(nonPriorityForm, 0, 3, false), 0, 12, false).
AddItem(instructions, 0, 2, false).SetBorder(true)

flex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Rune() == 114 {
seqManager.refreshAllLists(ctx)
priorityForm.Clear(true)
nonPriorityForm.Clear(true)
seqManager.populateLists(ctx)
pages.SwitchToPage("Menu")
} else if event.Rune() == 97 {
addSeqForm.Clear(true)
seqManager.addSeqPriorityForm(ctx)
pages.SwitchToPage("Add Sequencer")
} else if event.Rune() == 113 {
app.Stop()
}
return event
})

pages.AddPage("Menu", flex, true, true)
pages.AddPage("Add Sequencer", addSeqForm, true, false)

if err := app.SetRoot(pages, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

// updatePriorityList updates the list by changing the position of seq present at `index` to target
func (sm *manager) updatePriorityList(ctx context.Context, index int, target int) {
for i := index - 1; i >= target; i-- {
sm.priorityList[i], sm.priorityList[i+1] = sm.priorityList[i+1], sm.priorityList[i]
}
for i := index + 1; i <= target; i++ {
sm.priorityList[i], sm.priorityList[i-1] = sm.priorityList[i-1], sm.priorityList[i]
}
err := sm.redisCoordinator.UpdatePriorities(ctx, sm.priorityList)
if err != nil {
log.Warn("Failed to update priority, reverting change", "sequencer", sm.priorityList[target], "err", err)
}
sm.refreshAllLists(ctx)
}

// populateLists populates seq's in priority list and seq's that are online but not in priority
func (sm *manager) populateLists(ctx context.Context) {
prioritySeqList.Clear()
chosen, err := sm.redisCoordinator.CurrentChosenSequencer(ctx)
if err != nil {
panic(err)
}
for index, seqURL := range sm.priorityList {
sec := ""
if seqURL == chosen {
sec = fmt.Sprintf(" %vchosen", emoji.LeftArrow)
}
status := fmt.Sprintf("%v ", emoji.RedCircle)
if _, ok := sm.livelinessMap[seqURL]; ok {
status = fmt.Sprintf("%v ", emoji.GreenCircle)
}
prioritySeqList.AddItem(status+seqURL+sec, "", rune(48+index), nil).SetSecondaryTextColor(tcell.ColorPurple)
}

nonPrioritySeqList.Clear()
status := fmt.Sprintf("%v ", emoji.GreenCircle)
for _, seqURL := range sm.nonPriorityList {
nonPrioritySeqList.AddItem(status+seqURL, "", rune(45), nil)
}
}

// addSeqPriorityForm returns a form with fields to add a new sequencer to priority list
func (sm *manager) addSeqPriorityForm(ctx context.Context) *tview.Form {
URL := ""
addSeqForm.AddInputField("Sequencer URL", "", 0, nil, func(url string) {
URL = url
})
addSeqForm.AddButton("Cancel", func() {
priorityForm.Clear(true)
sm.populateLists(ctx)
pages.SwitchToPage("Menu")
})
addSeqForm.AddButton("Add", func() {
// check if url is valid, i.e it doesnt already exist in the priority list
if _, ok := sm.prioritiesMap[URL]; !ok && URL != "" {
sm.priorityList = append(sm.priorityList, URL)
err := sm.redisCoordinator.UpdatePriorities(ctx, sm.priorityList)
if err != nil {
log.Warn("Failed to add sequencer to the priority list", URL)
}
sm.refreshAllLists(ctx)
}
sm.populateLists(ctx)
pages.SwitchToPage("Menu")
})
return addSeqForm
}

// refreshAllLists gets the current status of all the lists displayed in the UI
func (sm *manager) refreshAllLists(ctx context.Context) {
sequencerURLList, mapping, err := sm.redisCoordinator.GetPriorities(ctx)
if err != nil {
panic(err)
}
sm.priorityList = sequencerURLList
sm.prioritiesMap = mapping

mapping, err = sm.redisCoordinator.GetLivelinessMap(ctx)
if err != nil {
panic(err)
}
sm.livelinessMap = mapping

urlList := []string{}
for url := range sm.livelinessMap {
if _, ok := sm.prioritiesMap[url]; !ok {
urlList = append(urlList, url)
}
}
sm.nonPriorityList = urlList
}
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,14 @@ require (
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/elastic/gosigar v0.14.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/enescakir/emoji v1.0.0 // indirect
github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 // indirect
github.com/flynn/noise v1.0.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gammazero/deque v0.2.1 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.6.0 // indirect
github.com/getsentry/sentry-go v0.18.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand Down Expand Up @@ -188,6 +191,7 @@ require (
github.com/libp2p/go-reuseport v0.2.0 // indirect
github.com/libp2p/go-yamux/v4 v4.0.0 // indirect
github.com/libp2p/zeroconf/v2 v2.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/miekg/dns v1.1.50 // indirect
Expand Down Expand Up @@ -224,6 +228,8 @@ require (
github.com/quic-go/webtransport-go v0.5.2 // indirect
github.com/raulk/go-watchdog v1.3.0 // indirect
github.com/rhnvrm/simples3 v0.6.1 // indirect
github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/samber/lo v1.36.0 // indirect
Expand Down Expand Up @@ -298,7 +304,7 @@ require (
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.4.2
github.com/mitchellh/pointerstructure v1.2.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
Expand Down
Loading

0 comments on commit 810887c

Please sign in to comment.