mempool: batch txs per peer in broadcastTxRoutine (#5321)

Closes #625
This commit is contained in:
Anton Kaliaev
2020-09-23 14:13:13 +04:00
committed by GitHub
parent 4b99502d5b
commit ffe2742a6c
11 changed files with 238 additions and 147 deletions
-3
View File
@@ -242,9 +242,6 @@ func (mem *CListMempool) CheckTx(tx types.Tx, cb func(*abci.Response), txInfo Tx
return err
}
// The size of the corresponding TxMessage
// can't be larger than the maxMsgSize, otherwise we can't
// relay it to peers.
if txSize > mem.config.MaxTxBytes {
return ErrTxTooLarge{mem.config.MaxTxBytes, txSize}
}
+9 -18
View File
@@ -420,52 +420,43 @@ func TestMempoolCloseWAL(t *testing.T) {
require.Equal(t, 1, len(m3), "expecting the wal match in")
}
func TestMempoolMaxMsgSize(t *testing.T) {
func TestMempool_CheckTxChecksTxSize(t *testing.T) {
app := kvstore.NewApplication()
cc := proxy.NewLocalClientCreator(app)
mempl, cleanup := newMempoolWithApp(cc)
defer cleanup()
maxTxSize := mempl.config.MaxTxBytes
maxMsgSize := calcMaxMsgSize(maxTxSize)
testCases := []struct {
len int
err bool
}{
// check small txs. no error
{10, false},
{1000, false},
{1000000, false},
0: {10, false},
1: {1000, false},
2: {1000000, false},
// check around maxTxSize
// changes from no error to error
{maxTxSize - 2, false},
{maxTxSize - 1, false},
{maxTxSize, false},
{maxTxSize + 1, true},
{maxTxSize + 2, true},
// check around maxMsgSize. all error
{maxMsgSize - 1, true},
{maxMsgSize, true},
{maxMsgSize + 1, true},
3: {maxTxSize - 1, false},
4: {maxTxSize, false},
5: {maxTxSize + 1, true},
}
for i, testCase := range testCases {
caseString := fmt.Sprintf("case %d, len %d", i, testCase.len)
tx := tmrand.Bytes(testCase.len)
err := mempl.CheckTx(tx, nil, TxInfo{})
bv := gogotypes.BytesValue{Value: tx}
bz, err2 := bv.Marshal()
require.NoError(t, err2)
require.Equal(t, len(bz), proto.Size(&bv), caseString)
if !testCase.err {
require.True(t, len(bz) <= maxMsgSize, caseString)
require.NoError(t, err, caseString)
} else {
require.True(t, len(bz) > maxMsgSize, caseString)
require.Equal(t, err, ErrTxTooLarge{maxTxSize, testCase.len}, caseString)
}
}
+75 -37
View File
@@ -1,6 +1,7 @@
package mempool
import (
"errors"
"fmt"
"math"
"time"
@@ -17,8 +18,6 @@ import (
const (
MempoolChannel = byte(0x30)
protoOverheadForTxMessage = 4
peerCatchupSleepIntervalMS = 100 // If peer is behind, sleep this amount
// UnknownPeerID is the peer ID to use when running CheckTx when there is
@@ -132,10 +131,10 @@ func (memR *Reactor) OnStart() error {
return nil
}
// GetChannels implements Reactor.
// It returns the list of channels for this reactor.
// GetChannels implements Reactor by returning the list of channels for this
// reactor.
func (memR *Reactor) GetChannels() []*p2p.ChannelDescriptor {
maxMsgSize := calcMaxMsgSize(memR.config.MaxTxBytes)
maxMsgSize := memR.config.MaxBatchBytes
return []*p2p.ChannelDescriptor{
{
ID: MempoolChannel,
@@ -174,9 +173,11 @@ func (memR *Reactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) {
if src != nil {
txInfo.SenderP2PID = src.ID()
}
err = memR.mempool.CheckTx(msg.Tx, nil, txInfo)
if err != nil {
memR.Logger.Info("Could not check tx", "tx", txID(msg.Tx), "err", err)
for _, tx := range msg.Txs {
err = memR.mempool.CheckTx(tx, nil, txInfo)
if err != nil {
memR.Logger.Info("Could not check tx", "tx", txID(tx), "err", err)
}
}
// broadcasting happens from go routines per peer
}
@@ -190,6 +191,7 @@ type PeerState interface {
func (memR *Reactor) broadcastTxRoutine(peer p2p.Peer) {
peerID := memR.ids.GetForPeer(peer)
var next *clist.CElement
for {
// In case of both next.NextWaitChan() and peer.Quit() are variable at the same time
if !memR.IsRunning() || !peer.IsRunning() {
@@ -211,9 +213,7 @@ func (memR *Reactor) broadcastTxRoutine(peer p2p.Peer) {
}
}
memTx := next.Value.(*mempoolTx)
// make sure the peer is up to date
// Make sure the peer is up to date.
peerState, ok := peer.Get(types.PeerStateKey).(PeerState)
if !ok {
// Peer does not have a state yet. We set it in the consensus reactor, but
@@ -224,25 +224,28 @@ func (memR *Reactor) broadcastTxRoutine(peer p2p.Peer) {
time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond)
continue
}
if peerState.GetHeight() < memTx.Height()-1 { // Allow for a lag of 1 block
// Allow for a lag of 1 block.
memTx := next.Value.(*mempoolTx)
if peerState.GetHeight() < memTx.Height()-1 {
time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond)
continue
}
// ensure peer hasn't already sent us this tx
if _, ok := memTx.senders.Load(peerID); !ok {
txs := memR.txs(next, peerID, peerState.GetHeight()) // WARNING: mutates next!
// send txs
if len(txs) > 0 {
msg := protomem.Message{
Sum: &protomem.Message_Tx{
Tx: &protomem.Tx{
Tx: []byte(memTx.tx),
},
Sum: &protomem.Message_Txs{
Txs: &protomem.Txs{Txs: txs},
},
}
bz, err := msg.Marshal()
if err != nil {
panic(err)
}
memR.Logger.Debug("Sending N txs to peer", "N", len(txs), "peer", peer)
success := peer.Send(MempoolChannel, bz)
if !success {
time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond)
@@ -262,21 +265,62 @@ func (memR *Reactor) broadcastTxRoutine(peer p2p.Peer) {
}
}
// txs iterates over the transaction list and builds a batch of txs. next is
// included.
// WARNING: mutates next!
func (memR *Reactor) txs(next *clist.CElement, peerID uint16, peerHeight int64) [][]byte {
batch := make([][]byte, 0)
for {
memTx := next.Value.(*mempoolTx)
if _, ok := memTx.senders.Load(peerID); !ok {
// If current batch + this tx size is greater than max => return.
batchMsg := protomem.Message{
Sum: &protomem.Message_Txs{
Txs: &protomem.Txs{Txs: append(batch, memTx.tx)},
},
}
if batchMsg.Size() > memR.config.MaxBatchBytes {
return batch
}
batch = append(batch, memTx.tx)
}
if next.Next() == nil {
return batch
}
next = next.Next()
}
}
//-----------------------------------------------------------------------------
// Messages
func (memR *Reactor) decodeMsg(bz []byte) (TxMessage, error) {
func (memR *Reactor) decodeMsg(bz []byte) (TxsMessage, error) {
msg := protomem.Message{}
err := msg.Unmarshal(bz)
if err != nil {
return TxMessage{}, err
return TxsMessage{}, err
}
var message TxMessage
var message TxsMessage
if i, ok := msg.Sum.(*protomem.Message_Tx); ok {
message = TxMessage{
Tx: types.Tx(i.Tx.GetTx()),
if i, ok := msg.Sum.(*protomem.Message_Txs); ok {
txs := i.Txs.GetTxs()
if len(txs) == 0 {
return message, errors.New("empty TxsMessage")
}
decoded := make([]types.Tx, len(txs))
for j, tx := range txs {
decoded[j] = types.Tx(tx)
}
message = TxsMessage{
Txs: decoded,
}
return message, nil
}
@@ -285,18 +329,12 @@ func (memR *Reactor) decodeMsg(bz []byte) (TxMessage, error) {
//-------------------------------------
// TxMessage is a Message containing a transaction.
type TxMessage struct {
Tx types.Tx
// TxsMessage is a Message containing transactions.
type TxsMessage struct {
Txs []types.Tx
}
// String returns a string representation of the TxMessage.
func (m *TxMessage) String() string {
return fmt.Sprintf("[TxMessage %v]", m.Tx)
}
// calcMaxMsgSize returns the max size of TxMessage
// account for proto overhead of bytesValue
func calcMaxMsgSize(maxTxSize int) int {
return maxTxSize + protoOverheadForTxMessage
// String returns a string representation of the TxsMessage.
func (m *TxsMessage) String() string {
return fmt.Sprintf("[TxsMessage %v]", m.Txs)
}
+44 -5
View File
@@ -16,6 +16,7 @@ import (
"github.com/tendermint/tendermint/abci/example/kvstore"
cfg "github.com/tendermint/tendermint/config"
"github.com/tendermint/tendermint/libs/log"
tmrand "github.com/tendermint/tendermint/libs/rand"
"github.com/tendermint/tendermint/p2p"
"github.com/tendermint/tendermint/p2p/mock"
memproto "github.com/tendermint/tendermint/proto/tendermint/mempool"
@@ -38,7 +39,7 @@ func (ps peerState) GetHeight() int64 {
// Send a bunch of txs to the first reactor's mempool and wait for them all to
// be received in the others.
func TestReactorBroadcastTxMessage(t *testing.T) {
func TestReactorBroadcastTxsMessage(t *testing.T) {
config := cfg.TestConfig()
// if there were more than two reactors, the order of transactions could not be
// asserted in waitForTxsOnReactors (due to transactions gossiping). If we
@@ -87,6 +88,46 @@ func TestReactorNoBroadcastToSender(t *testing.T) {
ensureNoTxs(t, reactors[peerID], 100*time.Millisecond)
}
func TestReactor_MaxBatchBytes(t *testing.T) {
config := cfg.TestConfig()
config.Mempool.MaxBatchBytes = 1024
const N = 2
reactors := makeAndConnectReactors(config, N)
defer func() {
for _, r := range reactors {
if err := r.Stop(); err != nil {
assert.NoError(t, err)
}
}
}()
for _, r := range reactors {
for _, peer := range r.Switch.Peers().List() {
peer.Set(types.PeerStateKey, peerState{1})
}
}
// Broadcast a tx, which has the max size (minus proto overhead)
// => ensure it's received by the second reactor.
tx1 := tmrand.Bytes(1018)
err := reactors[0].mempool.CheckTx(tx1, nil, TxInfo{SenderID: UnknownPeerID})
require.NoError(t, err)
waitForTxsOnReactors(t, []types.Tx{tx1}, reactors)
reactors[0].mempool.Flush()
reactors[1].mempool.Flush()
// Broadcast a tx, which is beyond the max size
// => ensure it's not sent
tx2 := tmrand.Bytes(1020)
err = reactors[0].mempool.CheckTx(tx2, nil, TxInfo{SenderID: UnknownPeerID})
require.NoError(t, err)
ensureNoTxs(t, reactors[1], 100*time.Millisecond)
// => ensure the second reactor did not disconnect from us
out, in, _ := reactors[1].Switch.NumPeers()
assert.Equal(t, 1, out+in)
}
func TestBroadcastTxForPeerStopsWhenPeerStops(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
@@ -266,7 +307,6 @@ func ensureNoTxs(t *testing.T, reactor *Reactor, timeout time.Duration) {
}
func TestMempoolVectors(t *testing.T) {
testCases := []struct {
testName string
tx []byte
@@ -280,8 +320,8 @@ func TestMempoolVectors(t *testing.T) {
tc := tc
msg := memproto.Message{
Sum: &memproto.Message_Tx{
Tx: &memproto.Tx{Tx: tc.tx},
Sum: &memproto.Message_Txs{
Txs: &memproto.Txs{Txs: [][]byte{tc.tx}},
},
}
bz, err := msg.Marshal()
@@ -289,5 +329,4 @@ func TestMempoolVectors(t *testing.T) {
require.Equal(t, tc.expBytes, hex.EncodeToString(bz), tc.testName)
}
}