Skip to content

Commit

Permalink
Merge pull request #125 from algorandfoundation/fix/boostrap-handle-u…
Browse files Browse the repository at this point in the history
…nstable-node

fix: wait for status from unstable clients
  • Loading branch information
PhearZero authored Jan 22, 2025
2 parents 41d034f + 0dc563e commit a3421bc
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 40 deletions.
112 changes: 83 additions & 29 deletions cmd/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import (
"github.com/spf13/cobra"
)

const CheckAlgodInterval = 10 * time.Second
const CheckAlgodTimeout = 2 * time.Minute

var CatchpointLagThreshold int = 30_000

// bootstrapCmdShort provides a brief description of the "bootstrap" command to initialize a fresh Algorand node.
var bootstrapCmdShort = "Initialize a fresh node"

Expand All @@ -42,13 +47,56 @@ This is the beginning of your adventure into running an Algorand node!
`

var FailedToAutoStartMessage = "Failed to start Algorand automatically."

// bootstrapCmd defines the "debug" command used to display diagnostic information for developers, including debug data.
var bootstrapCmd = &cobra.Command{
Use: "bootstrap",
Short: bootstrapCmdShort,
Long: bootstrapCmdLong,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
var client *api.ClientWithResponses
// Create the Bootstrap TUI
model := bootstrap.NewModel()
log.Warn(style.Yellow.Render(explanations.SudoWarningMsg))
// Try to launch the TUI if it's already running and configured
if algod.IsInitialized() {
// Parse the data directory
dir, err := algod.GetDataDir("")
if err != nil {
log.Fatal(err)
}

Check warning on line 69 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L59-L69

Added lines #L59 - L69 were not covered by tests

// Wait for the client to respond
log.Warn(style.Yellow.Render("Waiting for the node to start..."))
client, err = algod.WaitForClient(context.Background(), dir, CheckAlgodInterval, CheckAlgodTimeout)
if err != nil {
log.Fatal(err)
}

Check warning on line 76 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L72-L76

Added lines #L72 - L76 were not covered by tests

// Fetch the latest status
var resp *api.GetStatusResponse
resp, err = client.GetStatusWithResponse(context.Background())
// This should not happen, we waited for a status already
if err != nil {
log.Fatal(err)
}
if resp.StatusCode() != 200 {
log.Fatal(fmt.Sprintf("Failed to connect to the node at %s", dir))
}

Check warning on line 87 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L79-L87

Added lines #L79 - L87 were not covered by tests

// Execute the TUI if we are caught up.
// TODO: check the delta to see if it is necessary,
if resp.JSON200.CatchupTime == 0 {
err = runTUI(RootCmd, dir, false)
if err != nil {
log.Fatal(err)
}
return nil

Check warning on line 96 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L91-L96

Added lines #L91 - L96 were not covered by tests
}
}

// Exit the application in an invalid state
if algod.IsInstalled() && !algod.IsService() {
dataDir, _ := algod.GetDataDir("")
Expand All @@ -60,19 +108,6 @@ var bootstrapCmd = &cobra.Command{
log.Fatal("invalid state, exiting")
}

// Just launch the TUI if it's already running
if algod.IsInstalled() && algod.IsService() && algod.IsRunning() {
dir, err := algod.GetDataDir("")
if err != nil {
log.Fatal(err)
}
err = runTUI(RootCmd, dir, false)
if err != nil {
log.Fatal(err)
}
return nil
}

// Render the welcome text
r, _ := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
Expand All @@ -84,12 +119,25 @@ var bootstrapCmd = &cobra.Command{
}
fmt.Println(out)

// Create the Bootstrap TUI
model := bootstrap.NewModel()
// Ensure it the service is started,
// in this case we won't be able to query state without the node running
if algod.IsInstalled() && algod.IsService() && !algod.IsRunning() {
log.Debug("Algorand is installed, but not running. Attempting to start it automatically.")
log.Warn(style.Yellow.Render(explanations.SudoWarningMsg))
err := algod.Start()
if err != nil {
log.Error(FailedToAutoStartMessage)
log.Fatal(err)
}

Check warning on line 131 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L122-L131

Added lines #L122 - L131 were not covered by tests

}

// Prefill questions
if algod.IsInstalled() {
model.BootstrapMsg.Install = false
model.Question = bootstrap.CatchupQuestion
}
// Run the Bootstrap TUI
p := tea.NewProgram(model)
var msg *app.BootstrapMsg
go func() {
Expand All @@ -115,21 +163,31 @@ var bootstrapCmd = &cobra.Command{
if msg.Install {
log.Warn(style.Yellow.Render(explanations.SudoWarningMsg))

// Run the installer

Check warning on line 166 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L166

Added line #L166 was not covered by tests
err := algod.Install()
if err != nil {
return err
}

// Wait for algod
time.Sleep(10 * time.Second)
// Parse the data directory
dir, err := algod.GetDataDir("")
if err != nil {
log.Fatal(err)
}

Check warning on line 176 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L173-L176

Added lines #L173 - L176 were not covered by tests

// Wait for the client to respond
client, err = algod.WaitForClient(context.Background(), dir, CheckAlgodInterval, CheckAlgodTimeout)
if err != nil {
log.Fatal(err)
}

Check warning on line 182 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L179-L182

Added lines #L179 - L182 were not covered by tests

if !algod.IsRunning() {
log.Fatal("algod is not running. Something went wrong with installation")
}
} else {
// This should not happen but just in case, ensure it is running

Check warning on line 188 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L188

Added line #L188 was not covered by tests
if !algod.IsRunning() {
log.Info(style.Green.Render("Starting Algod 🚀"))
log.Warn(style.Yellow.Render(explanations.SudoWarningMsg))
err := algod.Start()
if err != nil {
log.Fatal(err)
Expand All @@ -141,20 +199,16 @@ var bootstrapCmd = &cobra.Command{

// Find the data directory automatically
dataDir, err := algod.GetDataDir("")
// Wait for the client to respond
client, err = algod.WaitForClient(context.Background(), dataDir, CheckAlgodInterval, CheckAlgodTimeout)

Check warning on line 203 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L202-L203

Added lines #L202 - L203 were not covered by tests
if err != nil {
log.Fatal(err)

Check warning on line 205 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L205

Added line #L205 was not covered by tests
}

// User answer for catchup question
if msg.Catchup {
ctx := context.Background()
httpPkg := new(api.HttpPkg)

Check warning on line 211 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L210-L211

Added lines #L210 - L211 were not covered by tests

if err != nil {
return err
}
// Create the client
client, err := algod.GetClient(dataDir)
if err != nil {
return err
}
network, err := utils.GetNetworkFromDataDir(dataDir)
if err != nil {
return err
Expand All @@ -167,8 +221,8 @@ var bootstrapCmd = &cobra.Command{
log.Info(style.Green.Render("Latest Catchpoint: " + catchpoint))
}

// Start catchup
res, _, err := algod.StartCatchup(ctx, client, catchpoint, nil)
// Start catchup with round threshold
res, _, err := algod.StartCatchup(ctx, client, catchpoint, &api.StartCatchupParams{Min: &CatchpointLagThreshold})

Check warning on line 225 in cmd/bootstrap.go

View check run for this annotation

Codecov / codecov/patch

cmd/bootstrap.go#L225

Added line #L225 was not covered by tests
if err != nil {
log.Fatal(err)
}
Expand Down
5 changes: 5 additions & 0 deletions internal/algod/algod.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,8 @@ func Stop() error {
return fmt.Errorf(UnsupportedOSError)
}
}

// IsInitialized determines if the Algod software is installed, configured as a service, and currently running.
func IsInitialized() bool {
return IsInstalled() && IsService() && IsRunning()

Check warning on line 151 in internal/algod/algod.go

View check run for this annotation

Codecov / codecov/patch

internal/algod/algod.go#L150-L151

Added lines #L150 - L151 were not covered by tests
}
40 changes: 40 additions & 0 deletions internal/algod/client.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package algod

import (
"context"
"errors"
"github.com/algorandfoundation/nodekit/api"
"github.com/algorandfoundation/nodekit/internal/algod/utils"
"github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider"
"os"
"path/filepath"
"runtime"
"time"
)

const InvalidDataDirMsg = "invalid data directory"
const ClientTimeoutMsg = "the client has timed out"

func GetDataDir(dataDir string) (string, error) {
envDataDir := os.Getenv("ALGORAND_DATA")
Expand Down Expand Up @@ -57,3 +60,40 @@ func GetClient(dataDir string) (*api.ClientWithResponses, error) {
}
return api.NewClientWithResponses(config.Endpoint, api.WithRequestEditorFn(apiToken.Intercept))
}

func WaitForClient(ctx context.Context, dataDir string, interval time.Duration, timeout time.Duration) (*api.ClientWithResponses, error) {
var client *api.ClientWithResponses
var err error
dataDir, err = GetDataDir(dataDir)
if err != nil {
return client, err
}

Check warning on line 70 in internal/algod/client.go

View check run for this annotation

Codecov / codecov/patch

internal/algod/client.go#L64-L70

Added lines #L64 - L70 were not covered by tests
// Try to fetch the client before waiting
client, err = GetClient(dataDir)
if err == nil {
var resp api.ResponseInterface
resp, err = client.GetStatusWithResponse(ctx)
if err == nil && resp.StatusCode() == 200 {
return client, nil
}

Check warning on line 78 in internal/algod/client.go

View check run for this annotation

Codecov / codecov/patch

internal/algod/client.go#L72-L78

Added lines #L72 - L78 were not covered by tests
}
// Wait for client to respond
for {
select {
case <-ctx.Done():
return client, nil
case <-time.After(interval):
client, err = GetClient(dataDir)
if err == nil {
var resp api.ResponseInterface
resp, err = client.GetStatusWithResponse(ctx)
if err == nil && resp.StatusCode() == 200 {
return client, nil
}

Check warning on line 92 in internal/algod/client.go

View check run for this annotation

Codecov / codecov/patch

internal/algod/client.go#L81-L92

Added lines #L81 - L92 were not covered by tests
}
case <-time.After(timeout):
return client, errors.New(ClientTimeoutMsg)

Check warning on line 95 in internal/algod/client.go

View check run for this annotation

Codecov / codecov/patch

internal/algod/client.go#L94-L95

Added lines #L94 - L95 were not covered by tests
}

}
}
24 changes: 13 additions & 11 deletions ui/bootstrap/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package bootstrap

import (
"github.com/algorandfoundation/nodekit/ui/app"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/log"
)

type Question string
Expand Down Expand Up @@ -43,8 +43,17 @@ func NewModel() Model {
},
}
}

var termMarkdown *glamour.TermRenderer

func (m Model) Init() tea.Cmd {
return textinput.Blink
var err error
termMarkdown, err = glamour.NewTermRenderer(glamour.WithAutoStyle())
if err != nil {
log.Fatal(err)
}

Check warning on line 54 in ui/bootstrap/model.go

View check run for this annotation

Codecov / codecov/patch

ui/bootstrap/model.go#L50-L54

Added lines #L50 - L54 were not covered by tests

return nil

Check warning on line 56 in ui/bootstrap/model.go

View check run for this annotation

Codecov / codecov/patch

ui/bootstrap/model.go#L56

Added line #L56 was not covered by tests
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
Expand Down Expand Up @@ -95,13 +104,6 @@ func (m Model) View() string {
case CatchupQuestion:
str = CatchupQuestionMsg
}
var msg string
r, err := glamour.NewTermRenderer(glamour.WithAutoStyle())
if err != nil {
// Fallback to dark mode
msg, _ = glamour.Render(str, "dark")
} else {
msg, _ = r.Render(str)
}
return msg
str, _ = termMarkdown.Render(str)
return str

Check warning on line 108 in ui/bootstrap/model.go

View check run for this annotation

Codecov / codecov/patch

ui/bootstrap/model.go#L107-L108

Added lines #L107 - L108 were not covered by tests
}

0 comments on commit a3421bc

Please sign in to comment.