package main import ( "crypto/md5" "encoding/binary" "encoding/hex" "encoding/json" "fmt" "math/rand" "net" "net/http" "net/url" "os" "sync" "time" "github.com/gorilla/websocket" "github.com/pkg/errors" "github.com/tendermint/tendermint/libs/log" rpctypes "github.com/tendermint/tendermint/rpc/lib/types" ) const ( sendTimeout = 10 * time.Second // see https://github.com/tendermint/tendermint/blob/master/rpc/lib/server/handlers.go pingPeriod = (30 * 9 / 10) * time.Second ) type transacter struct { Target string Rate int Size int Connections int BroadcastTxMethod string conns []*websocket.Conn connsBroken []bool wg sync.WaitGroup stopped bool logger log.Logger } func newTransacter(target string, connections, rate int, size int, broadcastTxMethod string) *transacter { return &transacter{ Target: target, Rate: rate, Size: size, Connections: connections, BroadcastTxMethod: broadcastTxMethod, conns: make([]*websocket.Conn, connections), connsBroken: make([]bool, connections), logger: log.NewNopLogger(), } } // SetLogger lets you set your own logger func (t *transacter) SetLogger(l log.Logger) { t.logger = l } // Start opens N = `t.Connections` connections to the target and creates read // and write goroutines for each connection. func (t *transacter) Start() error { t.stopped = false rand.Seed(time.Now().Unix()) for i := 0; i < t.Connections; i++ { c, _, err := connect(t.Target) if err != nil { return err } t.conns[i] = c } t.wg.Add(2 * t.Connections) for i := 0; i < t.Connections; i++ { go t.sendLoop(i) go t.receiveLoop(i) } return nil } // Stop closes the connections. func (t *transacter) Stop() { t.stopped = true t.wg.Wait() for _, c := range t.conns { c.Close() } } // receiveLoop reads messages from the connection (empty in case of // `broadcast_tx_async`). func (t *transacter) receiveLoop(connIndex int) { c := t.conns[connIndex] defer t.wg.Done() for { _, _, err := c.ReadMessage() if err != nil { if !websocket.IsCloseError(err, websocket.CloseNormalClosure) { t.logger.Error( fmt.Sprintf("failed to read response on conn %d", connIndex), "err", err, ) } return } if t.stopped || t.connsBroken[connIndex] { return } } } // sendLoop generates transactions at a given rate. func (t *transacter) sendLoop(connIndex int) { c := t.conns[connIndex] c.SetPingHandler(func(message string) error { err := c.WriteControl(websocket.PongMessage, []byte(message), time.Now().Add(sendTimeout)) if err == websocket.ErrCloseSent { return nil } else if e, ok := err.(net.Error); ok && e.Temporary() { return nil } return err }) logger := t.logger.With("addr", c.RemoteAddr()) var txNumber = 0 pingsTicker := time.NewTicker(pingPeriod) txsTicker := time.NewTicker(1 * time.Second) defer func() { pingsTicker.Stop() txsTicker.Stop() t.wg.Done() }() // hash of the host name is a part of each tx var hostnameHash [md5.Size]byte hostname, err := os.Hostname() if err != nil { hostname = "127.0.0.1" } hostnameHash = md5.Sum([]byte(hostname)) for { select { case <-txsTicker.C: startTime := time.Now() endTime := startTime.Add(time.Second) numTxSent := t.Rate for i := 0; i < t.Rate; i++ { // each transaction embeds connection index, tx number and hash of the hostname tx := generateTx(connIndex, txNumber, t.Size, hostnameHash) paramsJSON, err := json.Marshal(map[string]interface{}{"tx": hex.EncodeToString(tx)}) if err != nil { fmt.Printf("failed to encode params: %v\n", err) os.Exit(1) } rawParamsJSON := json.RawMessage(paramsJSON) c.SetWriteDeadline(time.Now().Add(sendTimeout)) err = c.WriteJSON(rpctypes.RPCRequest{ JSONRPC: "2.0", ID: "tm-bench", Method: t.BroadcastTxMethod, Params: rawParamsJSON, }) if err != nil { err = errors.Wrap(err, fmt.Sprintf("txs send failed on connection #%d", connIndex)) t.connsBroken[connIndex] = true logger.Error(err.Error()) return } // Time added here is 7.13 ns/op, not significant enough to worry about if i%20 == 0 { if time.Now().After(endTime) { // Plus one accounts for sending this tx numTxSent = i + 1 break } } txNumber++ } timeToSend := time.Since(startTime) logger.Info(fmt.Sprintf("sent %d transactions", numTxSent), "took", timeToSend) if timeToSend < 1*time.Second { sleepTime := time.Second - timeToSend logger.Debug(fmt.Sprintf("connection #%d is sleeping for %f seconds", connIndex, sleepTime.Seconds())) time.Sleep(sleepTime) } case <-pingsTicker.C: // go-rpc server closes the connection in the absence of pings c.SetWriteDeadline(time.Now().Add(sendTimeout)) if err := c.WriteMessage(websocket.PingMessage, []byte{}); err != nil { err = errors.Wrap(err, fmt.Sprintf("failed to write ping message on conn #%d", connIndex)) logger.Error(err.Error()) t.connsBroken[connIndex] = true } } if t.stopped { // To cleanly close a connection, a client should send a close // frame and wait for the server to close the connection. c.SetWriteDeadline(time.Now().Add(sendTimeout)) err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) if err != nil { err = errors.Wrap(err, fmt.Sprintf("failed to write close message on conn #%d", connIndex)) logger.Error(err.Error()) t.connsBroken[connIndex] = true } return } } } func connect(host string) (*websocket.Conn, *http.Response, error) { u := url.URL{Scheme: "ws", Host: host, Path: "/websocket"} return websocket.DefaultDialer.Dial(u.String(), nil) } func generateTx(connIndex int, txNumber int, txSize int, hostnameHash [md5.Size]byte) []byte { tx := make([]byte, txSize) binary.PutUvarint(tx[:8], uint64(connIndex)) binary.PutUvarint(tx[8:16], uint64(txNumber)) copy(tx[16:32], hostnameHash[:16]) binary.PutUvarint(tx[32:40], uint64(time.Now().Unix())) // 40-* random data if _, err := rand.Read(tx[40:]); err != nil { panic(errors.Wrap(err, "failed to read random bytes")) } return tx }