github.com/consideritdone/landslidecore@v0.0.0-20230718131026-a8b21c5cf8a7/test/e2e/runner/load.go (about) 1 package main 2 3 import ( 4 "context" 5 "crypto/rand" 6 "errors" 7 "fmt" 8 "math" 9 "time" 10 11 rpchttp "github.com/consideritdone/landslidecore/rpc/client/http" 12 e2e "github.com/consideritdone/landslidecore/test/e2e/pkg" 13 "github.com/consideritdone/landslidecore/types" 14 ) 15 16 // Load generates transactions against the network until the given context is 17 // canceled. A multiplier of greater than one can be supplied if load needs to 18 // be generated beyond a minimum amount. 19 func Load(ctx context.Context, testnet *e2e.Testnet, multiplier int) error { 20 // Since transactions are executed across all nodes in the network, we need 21 // to reduce transaction load for larger networks to avoid using too much 22 // CPU. This gives high-throughput small networks and low-throughput large ones. 23 // This also limits the number of TCP connections, since each worker has 24 // a connection to all nodes. 25 concurrency := 64 / len(testnet.Nodes) 26 if concurrency == 0 { 27 concurrency = 1 28 } 29 initialTimeout := 1 * time.Minute 30 stallTimeout := 30 * time.Second 31 32 chTx := make(chan types.Tx) 33 chSuccess := make(chan types.Tx) 34 ctx, cancel := context.WithCancel(ctx) 35 defer cancel() 36 37 // Spawn job generator and processors. 38 logger.Info(fmt.Sprintf("Starting transaction load (%v workers)...", concurrency)) 39 started := time.Now() 40 41 go loadGenerate(ctx, chTx, multiplier) 42 43 for w := 0; w < concurrency; w++ { 44 go loadProcess(ctx, testnet, chTx, chSuccess) 45 } 46 47 // Monitor successful transactions, and abort on stalls. 48 success := 0 49 timeout := initialTimeout 50 for { 51 select { 52 case <-chSuccess: 53 success++ 54 timeout = stallTimeout 55 case <-time.After(timeout): 56 return fmt.Errorf("unable to submit transactions for %v", timeout) 57 case <-ctx.Done(): 58 if success == 0 { 59 return errors.New("failed to submit any transactions") 60 } 61 logger.Info(fmt.Sprintf("Ending transaction load after %v txs (%.1f tx/s)...", 62 success, float64(success)/time.Since(started).Seconds())) 63 return nil 64 } 65 } 66 } 67 68 // loadGenerate generates jobs until the context is canceled 69 func loadGenerate(ctx context.Context, chTx chan<- types.Tx, multiplier int) { 70 for i := 0; i < math.MaxInt64; i++ { 71 // We keep generating the same 1000 keys over and over, with different values. 72 // This gives a reasonable load without putting too much data in the app. 73 id := i % 1000 74 75 bz := make([]byte, 1024) // 1kb hex-encoded 76 _, err := rand.Read(bz) 77 if err != nil { 78 panic(fmt.Sprintf("Failed to read random bytes: %v", err)) 79 } 80 tx := types.Tx(fmt.Sprintf("load-%X=%x", id, bz)) 81 82 select { 83 case chTx <- tx: 84 time.Sleep(time.Second / time.Duration(multiplier)) 85 86 case <-ctx.Done(): 87 close(chTx) 88 return 89 } 90 } 91 } 92 93 // loadProcess processes transactions 94 func loadProcess(ctx context.Context, testnet *e2e.Testnet, chTx <-chan types.Tx, chSuccess chan<- types.Tx) { 95 // Each worker gets its own client to each node, which allows for some 96 // concurrency while still bounding it. 97 clients := map[string]*rpchttp.HTTP{} 98 99 var err error 100 for tx := range chTx { 101 node := testnet.RandomNode() 102 client, ok := clients[node.Name] 103 if !ok { 104 client, err = node.Client() 105 if err != nil { 106 continue 107 } 108 109 // check that the node is up 110 _, err = client.Health(ctx) 111 if err != nil { 112 continue 113 } 114 115 clients[node.Name] = client 116 } 117 118 if _, err = client.BroadcastTxSync(ctx, tx); err != nil { 119 continue 120 } 121 chSuccess <- tx 122 } 123 }