diff --git a/cmd/run/run.go b/cmd/run/run.go index 93017c14..bd577431 100644 --- a/cmd/run/run.go +++ b/cmd/run/run.go @@ -12,6 +12,7 @@ import ( "github.com/dymensionxyz/roller/cmd/utils" "github.com/dymensionxyz/roller/config" datalayer "github.com/dymensionxyz/roller/data_layer" + servicemanager "github.com/dymensionxyz/roller/utils/service_manager" "github.com/spf13/cobra" ) @@ -23,16 +24,17 @@ func Cmd() *cobra.Command { home := cmd.Flag(utils.FlagNames.Home).Value.String() rollappConfig, err := config.LoadConfigFromTOML(home) utils.PrettifyErrorIfExists(err) - verifyBalances(rollappConfig) logger := utils.GetRollerLogger(rollappConfig.Home) + ctx, cancel := context.WithCancel(context.Background()) waitingGroup := sync.WaitGroup{} - waitingGroup.Add(3) - serviceConfig := utils.ServiceConfig{ + serviceConfig := &servicemanager.ServiceConfig{ Logger: logger, Context: ctx, WaitGroup: &waitingGroup, } + /* ----------------------------- verify balances ---------------------------- */ + verifyBalances(rollappConfig) /* ------------------------------ run processes ----------------------------- */ runDaWithRestarts(rollappConfig, serviceConfig) @@ -40,7 +42,7 @@ func Cmd() *cobra.Command { runRelayerWithRestarts(rollappConfig, serviceConfig) /* ------------------------------ render output ----------------------------- */ - RenderUI(rollappConfig) + RenderUI(rollappConfig, serviceConfig) cancel() waitingGroup.Wait() }, @@ -49,9 +51,15 @@ func Cmd() *cobra.Command { return cmd } -func runRelayerWithRestarts(config config.RollappConfig, serviceConfig utils.ServiceConfig) { - startRelayerCmd := getStartRelayerCmd(config) - utils.RunServiceWithRestart(startRelayerCmd, serviceConfig) +func runRelayerWithRestarts(cfg config.RollappConfig, serviceConfig *servicemanager.ServiceConfig) { + startRelayerCmd := getStartRelayerCmd(cfg) + service := servicemanager.ServiceData{ + Command: startRelayerCmd, + FetchFn: utils.GetRelayerAddresses, + UIData: servicemanager.UIData{Name: "Relayer"}, + } + serviceConfig.AddService("Relayer", service) + serviceConfig.RunServiceWithRestart("Relayer") } func getStartRelayerCmd(config config.RollappConfig) *exec.Cmd { @@ -62,20 +70,32 @@ func getStartRelayerCmd(config config.RollappConfig) *exec.Cmd { return exec.Command(ex, "relayer", "start", "--home", config.Home) } -func runDaWithRestarts(rollappConfig config.RollappConfig, serviceConfig utils.ServiceConfig) { +func runDaWithRestarts(rollappConfig config.RollappConfig, serviceConfig *servicemanager.ServiceConfig) { damanager := datalayer.NewDAManager(rollappConfig.DA, rollappConfig.Home) daLogFilePath := utils.GetDALogFilePath(rollappConfig.Home) startDALCCmd := damanager.GetStartDACmd(consts.DefaultCelestiaRPC) if startDALCCmd == nil { - serviceConfig.WaitGroup.Done() return } - utils.RunServiceWithRestart(startDALCCmd, serviceConfig, utils.WithLogging(daLogFilePath)) + + service := servicemanager.ServiceData{ + Command: startDALCCmd, + FetchFn: damanager.GetDAAccData, + UIData: servicemanager.UIData{Name: "DA Light Client"}, + } + serviceConfig.AddService("DA Light Client", service) + serviceConfig.RunServiceWithRestart("DA Light Client", utils.WithLogging(daLogFilePath)) } -func runSequencerWithRestarts(rollappConfig config.RollappConfig, serviceConfig utils.ServiceConfig) { +func runSequencerWithRestarts(rollappConfig config.RollappConfig, serviceConfig *servicemanager.ServiceConfig) { startRollappCmd := sequnecer_start.GetStartRollappCmd(rollappConfig, consts.DefaultDALCRPC) - utils.RunServiceWithRestart(startRollappCmd, serviceConfig, utils.WithLogging(utils.GetSequencerLogPath(rollappConfig))) + service := servicemanager.ServiceData{ + Command: startRollappCmd, + FetchFn: utils.GetSequencerData, + UIData: servicemanager.UIData{Name: "Sequencer"}, + } + serviceConfig.AddService("Sequencer", service) + serviceConfig.RunServiceWithRestart("Sequencer", utils.WithLogging(utils.GetSequencerLogPath(rollappConfig))) } func verifyBalances(rollappConfig config.RollappConfig) { diff --git a/cmd/run/services.go b/cmd/run/services.go deleted file mode 100644 index 3306812d..00000000 --- a/cmd/run/services.go +++ /dev/null @@ -1,71 +0,0 @@ -package run - -import ( - "log" - - "github.com/dymensionxyz/roller/cmd/utils" - "github.com/dymensionxyz/roller/config" - datalayer "github.com/dymensionxyz/roller/data_layer" -) - -type ServiceData struct { - Name string - Balance string - Status string -} - -type fetchResult struct { - data *utils.AccountData - err error - id int -} - -func fetchServicesData(rollappConfig config.RollappConfig, logger *log.Logger) ([]ServiceData, error) { - damanager := datalayer.NewDAManager(rollappConfig.DA, rollappConfig.Home) - - //TODO: avoid requiring passing rollappConfig to every function - fetchFuncs := []func(config.RollappConfig) (*utils.AccountData, error){ - utils.GetSequencerData, - utils.GetHubRlyAccData, - utils.GetRolRlyAccData, - } - - if damanager.GetLightNodeEndpoint() != "" { - fetchFuncs = append(fetchFuncs, damanager.GetDAAccData) - } - - results := fetchAsync(fetchFuncs, rollappConfig) - data := processDataResults(results, len(fetchFuncs), logger) - return buildServiceData(data, rollappConfig), nil -} - -func getInitialServiceData() []ServiceData { - return []ServiceData{ - { - Name: "Sequencer", - Balance: "Fetching...", - Status: "Active", - }, - { - Name: "DA Light Client", - Balance: "Fetching...", - Status: "Active", - }, - { - Name: "Relayer", - Balance: "Fetching...", - Status: "Starting...", - }, - } -} - -func fetchAsync(fetchFuncs []func(config.RollappConfig) (*utils.AccountData, error), rollappConfig config.RollappConfig) chan fetchResult { - results := make(chan fetchResult, len(fetchFuncs)) - for i, fn := range fetchFuncs { - go func(id int, fn func(config.RollappConfig) (*utils.AccountData, error)) { - data, err := fn(rollappConfig) - results <- fetchResult{data, err, id} - }(i, fn) - } - return results -} diff --git a/cmd/run/services_status.go b/cmd/run/services_status.go index c6709062..fb03936e 100644 --- a/cmd/run/services_status.go +++ b/cmd/run/services_status.go @@ -2,72 +2,18 @@ package run import ( "log" - "math/big" "path/filepath" "time" - "github.com/dymensionxyz/roller/cmd/consts" "github.com/dymensionxyz/roller/cmd/utils" "github.com/dymensionxyz/roller/config" - datalayer "github.com/dymensionxyz/roller/data_layer" + servicemanager "github.com/dymensionxyz/roller/utils/service_manager" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) -func processDataResults(results chan fetchResult, size int, logger *log.Logger) []*utils.AccountData { - data := make([]*utils.AccountData, size) - for i := 0; i < size; i++ { - res := <-results - if res.err != nil { - logger.Println(res.err) - data[res.id] = &utils.AccountData{ - Address: "", - Balance: big.NewInt(0), - } - } else { - data[res.id] = res.data - } - } - return data -} - -func activeIfSufficientBalance(currentBalance, threshold *big.Int) string { - if currentBalance.Cmp(threshold) >= 0 { - return "Active" - } else { - return "Stopped" - } -} - -func buildServiceData(data []*utils.AccountData, rollappConfig config.RollappConfig) []ServiceData { - damanager := datalayer.NewDAManager(rollappConfig.DA, rollappConfig.Home) - - var servicedata = []ServiceData{ - { - Name: "Sequencer", - Balance: data[0].Balance.String() + consts.Denoms.Hub, - Status: activeIfSufficientBalance(data[0].Balance, big.NewInt(1)), - }, - { - Name: "Relayer", - Balance: data[1].Balance.String() + consts.Denoms.Hub + ", " + - data[1].Balance.String() + rollappConfig.Denom, - Status: "Starting...", - }, - } - - if damanager.GetLightNodeEndpoint() != "" { - servicedata = append(servicedata, ServiceData{ - Name: "DA Light Client", - Balance: data[3].Balance.String() + consts.Denoms.Celestia, - Status: activeIfSufficientBalance(data[3].Balance, consts.OneDAWritePrice), - }) - } - return servicedata -} - -func RenderUI(rollappConfig config.RollappConfig) { +func RenderUI(rollappConfig config.RollappConfig, manager *servicemanager.ServiceConfig) { logger := utils.GetLogger(filepath.Join(rollappConfig.Home, "roller.log")) initializeUI() defer ui.Close() @@ -78,28 +24,23 @@ func RenderUI(rollappConfig config.RollappConfig) { servicesStatusTable := NewServiceStatusTable(termWidth) servicesInfoTable := NewServicesInfoTable(rollappConfig, termWidth) - serviceData, err := fetchServicesData(rollappConfig, logger) - if err != nil { - logger.Printf("Error: failed to fetch service data: %v", err) - serviceData = []ServiceData{} - } - updateUITable(serviceData, servicesStatusTable) + manager.FetchServicesData(rollappConfig) + updateUITable(manager.GetUIData(), servicesStatusTable) ui.Render(p, servicesStatusTable, servicesInfoTable) - events := ui.PollEvents() - ticker := time.NewTicker(time.Second * 5).C - //TODO: the renderer should be a struct that holds the config and the tables config := ServiceStatusConfig{ rollappConfig: rollappConfig, logger: logger, table: servicesStatusTable, } + events := ui.PollEvents() + ticker := time.NewTicker(time.Second * 5).C - eventLoop(events, ticker, config) + eventLoop(events, ticker, manager, config) } -func eventLoop(events <-chan ui.Event, ticker <-chan time.Time, config ServiceStatusConfig) { +func eventLoop(events <-chan ui.Event, ticker <-chan time.Time, manager *servicemanager.ServiceConfig, config ServiceStatusConfig) { for { select { case e := <-events: @@ -107,15 +48,9 @@ func eventLoop(events <-chan ui.Event, ticker <-chan time.Time, config ServiceSt return } case <-ticker: - config.logger.Println("Fetching service data...") - serviceData, err := fetchServicesData(config.rollappConfig, config.logger) - if err != nil { - config.logger.Printf("Error: failed to fetch service data: %v", err) - serviceData = []ServiceData{} - } else { - config.logger.Printf("Fetched services data successfully %s", serviceData) - } - updateUITable(serviceData, config.table) + manager.Logger.Println("Fetching service data...") + manager.FetchServicesData(config.rollappConfig) + updateUITable(manager.GetUIData(), config.table) ui.Render(config.table) } } diff --git a/cmd/run/ui.go b/cmd/run/ui.go index 85c7a392..5d6994b9 100644 --- a/cmd/run/ui.go +++ b/cmd/run/ui.go @@ -3,8 +3,10 @@ package run import ( "fmt" "log" + "strings" "github.com/dymensionxyz/roller/config" + servicemanager "github.com/dymensionxyz/roller/utils/service_manager" "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) @@ -31,9 +33,13 @@ func NewServiceStatusTable(termWidth int) *widgets.Table { return table } -func updateUITable(serviceData []ServiceData, table *widgets.Table) { +func updateUITable(serviceData []servicemanager.UIData, table *widgets.Table) { table.Rows = [][]string{{"Name", "Balance", "Status"}} - for _, data := range serviceData { - table.Rows = append(table.Rows, []string{data.Name, data.Balance, data.Status}) + for _, service := range serviceData { + balances := []string{} + for _, account := range service.Accounts { + balances = append(balances, account.Balance.String()) + } + table.Rows = append(table.Rows, []string{service.Name, strings.Join(balances, ","), service.Status}) } } diff --git a/cmd/utils/balance.go b/cmd/utils/balance.go index 02aed9fc..49277776 100644 --- a/cmd/utils/balance.go +++ b/cmd/utils/balance.go @@ -67,16 +67,18 @@ func GetSequencerInsufficientAddrs(cfg config.RollappConfig, requiredBalance big if err != nil { return nil, err } - if sequencerData.Balance.Cmp(&requiredBalance) < 0 { - return []NotFundedAddressData{ - { - Address: sequencerData.Address, - Denom: consts.Denoms.Hub, - CurrentBalance: sequencerData.Balance, - RequiredBalance: &requiredBalance, - KeyName: consts.KeysIds.HubSequencer, - }, - }, nil + for _, seq := range sequencerData { + if seq.Balance.Cmp(&requiredBalance) < 0 { + return []NotFundedAddressData{ + { + Address: seq.Address, + Denom: consts.Denoms.Hub, + CurrentBalance: seq.Balance, + RequiredBalance: &requiredBalance, + KeyName: consts.KeysIds.HubSequencer, + }, + }, nil + } } return []NotFundedAddressData{}, nil } diff --git a/cmd/utils/fetch_accounts_data.go b/cmd/utils/fetch_accounts_data.go index 55e09845..17c5bb9f 100644 --- a/cmd/utils/fetch_accounts_data.go +++ b/cmd/utils/fetch_accounts_data.go @@ -7,6 +7,21 @@ import ( "github.com/dymensionxyz/roller/config" ) +func GetRelayerAddresses(cfg config.RollappConfig) ([]AccountData, error) { + data := []AccountData{} + rollappRlyAcc, err := GetRolRlyAccData(cfg) + if err != nil { + return nil, err + } + data = append(data, *rollappRlyAcc) + hubRlyAcc, err := GetHubRlyAccData(cfg) + if err != nil { + return nil, err + } + data = append(data, *hubRlyAcc) + return data, nil +} + func GetRolRlyAccData(cfg config.RollappConfig) (*AccountData, error) { RollappRlyAddr, err := GetRelayerAddress(cfg.Home, cfg.RollappID) if err != nil { @@ -45,7 +60,7 @@ func GetHubRlyAccData(cfg config.RollappConfig) (*AccountData, error) { }, nil } -func GetSequencerData(cfg config.RollappConfig) (*AccountData, error) { +func GetSequencerData(cfg config.RollappConfig) ([]AccountData, error) { sequencerAddress, err := GetAddressBinary(GetKeyConfig{ ID: consts.KeysIds.HubSequencer, Dir: filepath.Join(cfg.Home, consts.ConfigDirName.HubKeys), @@ -61,8 +76,10 @@ func GetSequencerData(cfg config.RollappConfig) (*AccountData, error) { if err != nil { return nil, err } - return &AccountData{ - Address: sequencerAddress, - Balance: sequencerBalance, + return []AccountData{ + { + Address: sequencerAddress, + Balance: sequencerBalance, + }, }, nil } diff --git a/cmd/utils/service.go b/cmd/utils/service.go deleted file mode 100644 index c9d17676..00000000 --- a/cmd/utils/service.go +++ /dev/null @@ -1,39 +0,0 @@ -package utils - -import ( - "context" - "log" - "os/exec" - "sync" -) - -type ServiceConfig struct { - Context context.Context - WaitGroup *sync.WaitGroup - Logger *log.Logger -} - -// FIXME(#154): this functions have busy loop in case some process fails to start -func RunServiceWithRestart(cmd *exec.Cmd, serviceConfig ServiceConfig, options ...CommandOption) { - go func() { - defer serviceConfig.WaitGroup.Done() - for { - newCmd := exec.CommandContext(serviceConfig.Context, cmd.Path, cmd.Args[1:]...) - for _, option := range options { - option(newCmd) - } - commandExited := make(chan error, 1) - go func() { - serviceConfig.Logger.Printf("starting service command %s", newCmd.String()) - commandExited <- newCmd.Run() - }() - select { - case <-serviceConfig.Context.Done(): - return - case <-commandExited: - serviceConfig.Logger.Printf("process %s exited, restarting...", newCmd.String()) - continue - } - } - }() -} diff --git a/data_layer/celestia/celestia.go b/data_layer/celestia/celestia.go index cf0be8c5..8098280f 100644 --- a/data_layer/celestia/celestia.go +++ b/data_layer/celestia/celestia.go @@ -55,7 +55,7 @@ func (c *Celestia) InitializeLightNodeConfig() error { return nil } -func (c *Celestia) GetDAAccData(config.RollappConfig) (*utils.AccountData, error) { +func (c *Celestia) getDAAccData(config.RollappConfig) (*utils.AccountData, error) { celAddress, err := c.GetDAAccountAddress() if err != nil { return nil, err @@ -78,8 +78,13 @@ func (c *Celestia) GetDAAccData(config.RollappConfig) (*utils.AccountData, error }, nil } +func (c *Celestia) GetDAAccData(cfg config.RollappConfig) ([]utils.AccountData, error) { + celAddress, err := c.getDAAccData(cfg) + return []utils.AccountData{*celAddress}, err +} + func (c *Celestia) CheckDABalance() ([]utils.NotFundedAddressData, error) { - accData, err := c.GetDAAccData(config.RollappConfig{}) + accData, err := c.getDAAccData(config.RollappConfig{}) if err != nil { return nil, err } diff --git a/data_layer/da_layer.go b/data_layer/da_layer.go index 605d6ee1..2e6fb4c5 100644 --- a/data_layer/da_layer.go +++ b/data_layer/da_layer.go @@ -14,7 +14,7 @@ type DataLayer interface { InitializeLightNodeConfig() error CheckDABalance() ([]utils.NotFundedAddressData, error) GetStartDACmd(rpcEndpoint string) *exec.Cmd - GetDAAccData(c config.RollappConfig) (*utils.AccountData, error) + GetDAAccData(c config.RollappConfig) ([]utils.AccountData, error) GetLightNodeEndpoint() string } diff --git a/data_layer/damock/damock.go b/data_layer/damock/damock.go index 795c4ba3..16737e85 100644 --- a/data_layer/damock/damock.go +++ b/data_layer/damock/damock.go @@ -32,10 +32,12 @@ func (d *DAMock) GetStartDACmd(rpcEndpoint string) *exec.Cmd { return nil } -func (d *DAMock) GetDAAccData(c config.RollappConfig) (*utils.AccountData, error) { - return &utils.AccountData{ - Address: "mockDA", - Balance: big.NewInt(999999999999999), +func (d *DAMock) GetDAAccData(c config.RollappConfig) ([]utils.AccountData, error) { + return []utils.AccountData{ + { + Address: "mockDA", + Balance: big.NewInt(999999999999999), + }, }, nil } diff --git a/utils/service_manager/service.go b/utils/service_manager/service.go new file mode 100644 index 00000000..b02dc8ae --- /dev/null +++ b/utils/service_manager/service.go @@ -0,0 +1,117 @@ +package servicemanager + +import ( + "context" + "log" + "math/big" + "os/exec" + "sync" + "time" + + "github.com/dymensionxyz/roller/cmd/utils" + "github.com/dymensionxyz/roller/config" +) + +type ServiceConfig struct { + Context context.Context + WaitGroup *sync.WaitGroup + Logger *log.Logger + Services map[string]ServiceData +} + +type UIData struct { + //TODO: try to remove as it stored in a map + Name string + Accounts []utils.AccountData + Balance string + Status string +} + +type ServiceData struct { + Command *exec.Cmd + FetchFn func(config.RollappConfig) ([]utils.AccountData, error) + UIData UIData +} + +// TODO: move this to a separate file +// TODO: status should be Enum +func activeIfSufficientBalance(currentBalance, threshold *big.Int) string { + if currentBalance.Cmp(threshold) >= 0 { + return "Active" + } else { + return "Stopped" + } +} + +// TODO: fetch all data and populate UIData +func (s *ServiceConfig) FetchServicesData(cfg config.RollappConfig) { + for k, service := range s.Services { + //TODO: make this async + if service.FetchFn != nil { + accountData, err := service.FetchFn(cfg) + if err != nil { + //TODO: set the status to FAILED + return + } + service.UIData.Accounts = accountData + + //FIXME: the status function should be part of the service + for _, account := range accountData { + service.UIData.Status = activeIfSufficientBalance(account.Balance, big.NewInt(1)) + } + if k == "Relayer" { + service.UIData.Status = "Starting..." + } + + s.Services[k] = service + } + } +} + +func (s *ServiceConfig) GetUIData() []UIData { + var uiData []UIData + for _, service := range s.Services { + uiData = append(uiData, service.UIData) + } + return uiData +} + +func (s *ServiceConfig) AddService(name string, data ServiceData) { + if s.Services == nil { + s.Services = make(map[string]ServiceData) + } + + s.Services[name] = data +} + +// FIXME(#154): this functions have busy loop in case some process fails to start +func (s *ServiceConfig) RunServiceWithRestart(name string, options ...utils.CommandOption) { + if _, ok := s.Services[name]; !ok { + panic("service with that name does not exist") + } + cmd := s.Services[name].Command + + s.WaitGroup.Add(1) + go func() { + defer s.WaitGroup.Done() + for { + newCmd := exec.CommandContext(s.Context, cmd.Path, cmd.Args[1:]...) + for _, option := range options { + option(newCmd) + } + commandExited := make(chan error, 1) + go func() { + s.Logger.Printf("starting service command %s", newCmd.String()) + commandExited <- newCmd.Run() + }() + select { + case <-s.Context.Done(): + return + case <-commandExited: + s.Logger.Printf("process %s exited, restarting...", newCmd.String()) + time.Sleep(5 * time.Second) + continue + } + } + }() +}