From 80d1d0d05b6d9be4d280a5abc575809e2f6eaf40 Mon Sep 17 00:00:00 2001 From: Brian Olson Date: Fri, 29 Jul 2022 14:25:30 -0400 Subject: [PATCH 1/2] adapt loadgenerator to cluster testing add -d ALGORAND_DATA for use like pingpong add -config '{json literal}' --- cmd/loadgenerator/config.go | 26 +++++--- cmd/loadgenerator/main.go | 130 ++++++++++++++++++++++++++++-------- 2 files changed, 122 insertions(+), 34 deletions(-) diff --git a/cmd/loadgenerator/config.go b/cmd/loadgenerator/config.go index ec9576634d..8d17b483d5 100644 --- a/cmd/loadgenerator/config.go +++ b/cmd/loadgenerator/config.go @@ -18,12 +18,12 @@ package main import ( "encoding/json" + "io" "net/url" "os" + "strings" ) -const configFileName = "loadgenerator.config" - type config struct { // AccountMnemonic is the mnemonic of the account from which we would like to spend Algos. AccountMnemonic string @@ -37,6 +37,8 @@ type config struct { RoundOffset uint64 // Fee is the amount of algos that would be specified in the transaction fee field. Fee uint64 + // TxnsToSend is the number of transactions to send in the round where (((round + RoundOffset) % RoundModulator) == 0) + TxnsToSend int } type fileConfig struct { @@ -44,13 +46,21 @@ type fileConfig struct { ClientURL string `json:"ClientURL"` } -func loadConfig() (cfg config, err error) { - var fd *os.File - fd, err = os.Open(configFileName) - if err != nil { - return config{}, err +func loadConfig(configFileName string) (cfg config, err error) { + var fin io.Reader + if len(configFileName) > 0 && configFileName[0] == '{' { + // read -config "{json literal}" + fin = strings.NewReader(configFileName) + } else { + var fd *os.File + fd, err = os.Open(configFileName) + if err != nil { + return config{}, err + } + defer fd.Close() + fin = fd } - jsonDecoder := json.NewDecoder(fd) + jsonDecoder := json.NewDecoder(fin) var fileCfg fileConfig err = jsonDecoder.Decode(&fileCfg) if err == nil { diff --git a/cmd/loadgenerator/main.go b/cmd/loadgenerator/main.go index 8c2417a2c8..f23cb7f37a 100644 --- a/cmd/loadgenerator/main.go +++ b/cmd/loadgenerator/main.go @@ -19,7 +19,12 @@ package main import ( "flag" "fmt" + "io/fs" + "io/ioutil" + "math/rand" + "net/url" "os" + "path/filepath" "runtime" "strings" "sync" @@ -30,19 +35,21 @@ import ( "github.com/algorand/go-algorand/daemon/algod/api/client" generatedV2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" "github.com/algorand/go-algorand/daemon/algod/api/spec/common" + algodAcct "github.com/algorand/go-algorand/data/account" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util/db" ) -const transactionBlockSize = 800 - -var runOnce = flag.Bool("once", false, "Terminate after first spend loop") - var nroutines = runtime.NumCPU() * 2 -func init() { - flag.Parse() +func maybefail(err error, msg string, args ...interface{}) { + if err == nil { + return + } + fmt.Fprintf(os.Stderr, msg, args...) + os.Exit(1) } func loadMnemonic(mnemonic string) crypto.Seed { @@ -57,18 +64,81 @@ func loadMnemonic(mnemonic string) crypto.Seed { return seed } +// Like shared/pingpong/accounts.go +func findRootKeys(algodDir string) []*crypto.SignatureSecrets { + keylist := make([]*crypto.SignatureSecrets, 0, 5) + filepath.Walk(algodDir, func(path string, info fs.FileInfo, err error) error { + var handle db.Accessor + handle, err = db.MakeErasableAccessor(path) + if err != nil { + return nil // don't care, move on + } + defer handle.Close() + + // Fetch an account.Participation from the database + root, err := algodAcct.RestoreRoot(handle) + if err != nil { + return nil // don't care, move on + } + keylist = append(keylist, root.Secrets()) + return nil + }) + return keylist +} + +var runOnce = flag.Bool("once", false, "Terminate after first spend loop") + func main() { + var algodDir string + flag.StringVar(&algodDir, "d", "", "algorand data dir") + var configArg string + flag.StringVar(&configArg, "config", "loadgenerator.config", "path to json or json literal") + var cfg config var err error - if cfg, err = loadConfig(); err != nil { + flag.Parse() + if cfg, err = loadConfig(configArg); err != nil { fmt.Fprintf(os.Stderr, "unable to load config : %v\n", err) os.Exit(1) } + + if (cfg.ClientURL == nil || cfg.ClientURL.String() == "") || cfg.APIToken == "" { + if algodDir != "" { + path := filepath.Join(algodDir, "algod.net") + net, err := ioutil.ReadFile(path) + maybefail(err, "%s: %v\n", path, err) + path = filepath.Join(algodDir, "algod.token") + token, err := ioutil.ReadFile(path) + maybefail(err, "%s: %v\n", path, err) + cfg.ClientURL, err = url.Parse(fmt.Sprintf("http://%s", string(strings.TrimSpace(string(net))))) + maybefail(err, "bad net url %v\n", err) + cfg.APIToken = string(token) + } else { + fmt.Fprintf(os.Stderr, "need (config.ClientURL and config.APIToken) or (-d ALGORAND_DATA)\n") + os.Exit(1) + } + } fmt.Printf("Configuration file loaded successfully.\n") - seed := loadMnemonic(cfg.AccountMnemonic) - privateKey := crypto.GenerateSignatureSecrets(seed) - publicKey := basics.Address(privateKey.SignatureVerifier) + var privateKey *crypto.SignatureSecrets + var publicKey basics.Address + if len(cfg.AccountMnemonic) > 0 { + seed := loadMnemonic(cfg.AccountMnemonic) + privateKey = crypto.GenerateSignatureSecrets(seed) + publicKey = basics.Address(privateKey.SignatureVerifier) + } else if len(algodDir) > 0 { + // get test cluster local unlocked wallet + keys := findRootKeys(algodDir) + if len(keys) == 0 { + fmt.Fprintf(os.Stderr, "%s: found no root keys\n", algodDir) + os.Exit(1) + } + privateKey = keys[rand.Intn(len(keys))] + publicKey = basics.Address(privateKey.SignatureVerifier) + } else { + fmt.Fprintf(os.Stderr, "need either config.AccountMnemonic or -d ALGORAND_DATA for spendable account") + os.Exit(1) + } fmt.Printf("Spending account public key : %v\n", publicKey.String()) @@ -95,9 +165,10 @@ func nextSpendRound(cfg config, round uint64) uint64 { func spendLoop(cfg config, privateKey *crypto.SignatureSecrets, publicKey basics.Address) (err error) { restClient := client.MakeRestClient(*cfg.ClientURL, cfg.APIToken) for { - waitForRound(restClient, cfg, true) - queueFull := generateTransactions(restClient, cfg, privateKey, publicKey) + nodeStatus := waitForRound(restClient, cfg, true) + queueFull := generateTransactions(restClient, cfg, privateKey, publicKey, nodeStatus) if queueFull { + // done for this round, wait for a non-send round waitForRound(restClient, cfg, false) if *runOnce { fmt.Fprintf(os.Stdout, "Once flag set, terminating.\n") @@ -108,8 +179,7 @@ func spendLoop(cfg config, privateKey *crypto.SignatureSecrets, publicKey basics return nil } -func waitForRound(restClient client.RestClient, cfg config, spendingRound bool) { - var nodeStatus generatedV2.NodeStatusResponse +func waitForRound(restClient client.RestClient, cfg config, spendingRound bool) (nodeStatus generatedV2.NodeStatusResponse) { var err error for { nodeStatus, err = restClient.Status() @@ -123,7 +193,7 @@ func waitForRound(restClient client.RestClient, cfg config, spendingRound bool) return } if spendingRound { - fmt.Printf("Current round %d, waiting for spending round %d\n", nodeStatus.LastRound, nextSpendRound(cfg, nodeStatus.LastRound)) + fmt.Printf("Last round %d, waiting for spending round %d\n", nodeStatus.LastRound, nextSpendRound(cfg, nodeStatus.LastRound)) } for { // wait for the next round. @@ -141,14 +211,11 @@ func waitForRound(restClient client.RestClient, cfg config, spendingRound bool) } } -func generateTransactions(restClient client.RestClient, cfg config, privateKey *crypto.SignatureSecrets, publicKey basics.Address) (queueFull bool) { - var nodeStatus generatedV2.NodeStatusResponse +const transactionBlockSize = 800 + +func generateTransactions(restClient client.RestClient, cfg config, privateKey *crypto.SignatureSecrets, publicKey basics.Address, nodeStatus generatedV2.NodeStatusResponse) (queueFull bool) { + start := time.Now() var err error - nodeStatus, err = restClient.Status() - if err != nil { - fmt.Fprintf(os.Stderr, "unable to check status : %v", err) - return false - } var vers common.Version vers, err = restClient.Versions() if err != nil { @@ -157,8 +224,12 @@ func generateTransactions(restClient client.RestClient, cfg config, privateKey * } var genesisHash crypto.Digest copy(genesisHash[:], vers.GenesisHash) - // create transactionBlockSize transaction to send. - txns := make([]transactions.SignedTxn, transactionBlockSize, transactionBlockSize) + sendSize := cfg.TxnsToSend + if cfg.TxnsToSend == 0 { + sendSize = transactionBlockSize + } + // create sendSize transaction to send. + txns := make([]transactions.SignedTxn, sendSize, sendSize) for i := range txns { tx := transactions.Transaction{ Header: transactions.Header{ @@ -181,13 +252,14 @@ func generateTransactions(restClient client.RestClient, cfg config, privateKey * } // create multiple go-routines to send all these requests. + // each thread makes new HTTP connections per API call var sendWaitGroup sync.WaitGroup sendWaitGroup.Add(nroutines) sent := make([]int, nroutines, nroutines) for i := 0; i < nroutines; i++ { go func(base int) { defer sendWaitGroup.Done() - for x := base; x < transactionBlockSize; x += nroutines { + for x := base; x < sendSize; x += nroutines { _, err2 := restClient.SendRawTransaction(txns[x]) if err2 != nil { if strings.Contains(err2.Error(), "txn dead") || strings.Contains(err2.Error(), "below threshold") { @@ -205,5 +277,11 @@ func generateTransactions(restClient client.RestClient, cfg config, privateKey * for i := 0; i < nroutines; i++ { totalSent += sent[i] } - return totalSent != transactionBlockSize + dt := time.Now().Sub(start) + fmt.Fprintf(os.Stdout, "sent %d/%d in %s (%.1f/s)\n", totalSent, sendSize, dt.String(), float64(totalSent)/dt.Seconds()) + if cfg.TxnsToSend != 0 { + // We attempted what we were asked. We're done. + return true + } + return totalSent != sendSize } From bb0042c2a2d2fb5a78164cad55a470bf285146f2 Mon Sep 17 00:00:00 2001 From: Brian Olson Date: Mon, 1 Aug 2022 09:02:54 -0400 Subject: [PATCH 2/2] warning-print of Walk err --- cmd/loadgenerator/main.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/loadgenerator/main.go b/cmd/loadgenerator/main.go index ee47265d68..cfa1ea66af 100644 --- a/cmd/loadgenerator/main.go +++ b/cmd/loadgenerator/main.go @@ -66,7 +66,7 @@ func loadMnemonic(mnemonic string) crypto.Seed { // Like shared/pingpong/accounts.go func findRootKeys(algodDir string) []*crypto.SignatureSecrets { keylist := make([]*crypto.SignatureSecrets, 0, 5) - filepath.Walk(algodDir, func(path string, info fs.FileInfo, err error) error { + err := filepath.Walk(algodDir, func(path string, info fs.FileInfo, err error) error { var handle db.Accessor handle, err = db.MakeErasableAccessor(path) if err != nil { @@ -82,6 +82,9 @@ func findRootKeys(algodDir string) []*crypto.SignatureSecrets { keylist = append(keylist, root.Secrets()) return nil }) + if err != nil { + fmt.Fprintf(os.Stderr, "%s: warning, %v\n", algodDir, err) + } return keylist }