Files
tendermint/tools/tm-bench/transacter.go
ValarDragon 99e582d79a crypto: Refactor to move files out of the top level directory
Currently the top level directory contains basically all of the code
for the crypto package. This PR moves the crypto code into submodules
in a similar manner to what `golang/x/crypto` does. This improves code
organization.

Ref discussion: https://github.com/tendermint/tendermint/pull/1966

Closes #1956
2018-07-18 08:38:44 -07:00

285 lines
7.0 KiB
Go

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
startingWg sync.WaitGroup
endingWg 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.startingWg.Add(t.Connections)
t.endingWg.Add(2 * t.Connections)
for i := 0; i < t.Connections; i++ {
go t.sendLoop(i)
go t.receiveLoop(i)
}
t.startingWg.Wait()
return nil
}
// Stop closes the connections.
func (t *transacter) Stop() {
t.stopped = true
t.endingWg.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.endingWg.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) {
started := false
// Close the starting waitgroup, in the event that this fails to start
defer func() {
if !started {
t.startingWg.Done()
}
}()
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.endingWg.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))
// each transaction embeds connection index, tx number and hash of the hostname
// we update the tx number between successive txs
tx := generateTx(connIndex, txNumber, t.Size, hostnameHash)
txHex := make([]byte, len(tx)*2)
hex.Encode(txHex, tx)
for {
select {
case <-txsTicker.C:
startTime := time.Now()
endTime := startTime.Add(time.Second)
numTxSent := t.Rate
if !started {
t.startingWg.Done()
started = true
}
now := time.Now()
for i := 0; i < t.Rate; i++ {
// update tx number of the tx, and the corresponding hex
updateTx(tx, txHex, txNumber)
paramsJSON, err := json.Marshal(map[string]interface{}{"tx": txHex})
if err != nil {
fmt.Printf("failed to encode params: %v\n", err)
os.Exit(1)
}
rawParamsJSON := json.RawMessage(paramsJSON)
c.SetWriteDeadline(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
}
// cache the time.Now() reads to save time.
if i%5 == 0 {
now = time.Now()
if 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
}
// warning, mutates input byte slice
func updateTx(tx []byte, txHex []byte, txNumber int) {
binary.PutUvarint(tx[8:16], uint64(txNumber))
hexUpdate := make([]byte, 16)
hex.Encode(hexUpdate, tx[8:16])
for i := 16; i < 32; i++ {
txHex[i] = hexUpdate[i-16]
}
}