github.com/iotexproject/iotex-core@v1.14.1-rc1/pkg/messagebatcher/batchwriter.go (about)

     1  package batch
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/binary"
     7  	"fmt"
     8  	"sync"
     9  	"sync/atomic"
    10  	"time"
    11  
    12  	"github.com/cespare/xxhash/v2"
    13  	goproto "github.com/iotexproject/iotex-proto/golang"
    14  	"github.com/iotexproject/iotex-proto/golang/iotexrpc"
    15  	"github.com/iotexproject/iotex-proto/golang/iotextypes"
    16  	"github.com/libp2p/go-libp2p-core/peer"
    17  	"github.com/pkg/errors"
    18  	"go.uber.org/zap"
    19  	"google.golang.org/protobuf/proto"
    20  
    21  	"github.com/iotexproject/iotex-core/pkg/lifecycle"
    22  	"github.com/iotexproject/iotex-core/pkg/log"
    23  )
    24  
    25  const (
    26  	_bufferLength    = 500
    27  	_maxWriters      = 5000
    28  	_cleanupInterval = 15 * time.Minute
    29  )
    30  
    31  // Option sets parameter for batch
    32  type Option func(cfg *writerConfig)
    33  
    34  // WithInterval sets batch with time interval
    35  func WithInterval(t time.Duration) Option {
    36  	return func(cfg *writerConfig) {
    37  		cfg.msgInterval = t
    38  	}
    39  }
    40  
    41  // WithSizeLimit sets batch with limited size
    42  func WithSizeLimit(limit uint64) Option {
    43  	return func(cfg *writerConfig) {
    44  		cfg.sizeLimit = limit
    45  	}
    46  }
    47  
    48  type (
    49  	// Manager is the manager of batch component
    50  	Manager struct {
    51  		mu             sync.RWMutex
    52  		writerMap      map[batchID]*batchWriter
    53  		outputQueue    chan *Message // assembled message queue for external reader
    54  		assembleQueue  chan *batch   // batch queue which collects batches sent from writers
    55  		messageHandler messageOutbound
    56  		cancelHanlders context.CancelFunc
    57  	}
    58  
    59  	batchID uint64
    60  
    61  	messageOutbound func(*Message) error
    62  )
    63  
    64  // NewManager creates a new Manager with callback
    65  func NewManager(handler messageOutbound) *Manager {
    66  	return &Manager{
    67  		writerMap:      make(map[batchID]*batchWriter),
    68  		outputQueue:    make(chan *Message, _bufferLength),
    69  		assembleQueue:  make(chan *batch, _bufferLength),
    70  		messageHandler: handler,
    71  	}
    72  }
    73  
    74  // Start start the Manager
    75  func (bm *Manager) Start() error {
    76  	ctx, cancel := context.WithCancel(context.Background())
    77  	go bm.assemble(ctx)
    78  	go bm.cleanup(ctx, _cleanupInterval)
    79  	go bm.callback(ctx)
    80  	bm.cancelHanlders = cancel
    81  	return nil
    82  }
    83  
    84  // Stop stops the Manager
    85  func (bm *Manager) Stop() error {
    86  	bm.cancelHanlders()
    87  	return nil
    88  }
    89  
    90  // Put puts a batchmessage into the Manager
    91  func (bm *Manager) Put(msg *Message, opts ...Option) error {
    92  	if !bm.supported(msg.messageType()) {
    93  		return errors.New("message is unsupported for batching")
    94  	}
    95  	id, err := msg.batchID()
    96  	if err != nil {
    97  		return err
    98  	}
    99  	bm.mu.RLock()
   100  	writer, exist := bm.writerMap[id]
   101  	bm.mu.RUnlock()
   102  	if !exist {
   103  		cfg := _defaultWriterConfig
   104  		for _, opt := range opts {
   105  			opt(cfg)
   106  		}
   107  		bm.mu.Lock()
   108  		if len(bm.writerMap) > _maxWriters {
   109  			bm.mu.Unlock()
   110  			return errors.New("the batch is full")
   111  		}
   112  		writer = newBatchWriter(cfg, bm)
   113  		bm.writerMap[id] = writer
   114  		bm.mu.Unlock()
   115  	}
   116  	return writer.Put(msg)
   117  }
   118  
   119  func (bm *Manager) supported(msgType iotexrpc.MessageType) bool {
   120  	return msgType == iotexrpc.MessageType_ACTION
   121  }
   122  
   123  func (bm *Manager) assemble(ctx context.Context) {
   124  	for {
   125  		select {
   126  		case batch := <-bm.assembleQueue:
   127  			if batch.Size() == 0 {
   128  				continue
   129  			}
   130  			var (
   131  				msg0 = batch.msgs[0]
   132  			)
   133  			bm.outputQueue <- &Message{
   134  				msgType: msg0.msgType,
   135  				ChainID: msg0.ChainID,
   136  				Target:  msg0.Target,
   137  				Data:    packMessageData(msg0.msgType, batch.msgs),
   138  			}
   139  		case <-ctx.Done():
   140  			return
   141  		}
   142  	}
   143  }
   144  
   145  func packMessageData(msgType iotexrpc.MessageType, arr []*Message) proto.Message {
   146  	switch msgType {
   147  	case iotexrpc.MessageType_ACTION:
   148  		actions := make([]*iotextypes.Action, 0, len(arr))
   149  		for i := range arr {
   150  			actions = append(actions, arr[i].Data.(*iotextypes.Action))
   151  		}
   152  		return &iotextypes.Actions{Actions: actions}
   153  	default:
   154  		panic(fmt.Sprintf("the message type %v is not supported", msgType))
   155  	}
   156  }
   157  
   158  func (bm *Manager) cleanup(ctx context.Context, interval time.Duration) {
   159  	ticker := time.NewTicker(interval)
   160  	for {
   161  		select {
   162  		case <-ticker.C:
   163  			bm.cleanupLoop()
   164  		case <-ctx.Done():
   165  			ticker.Stop()
   166  			return
   167  		}
   168  	}
   169  }
   170  
   171  func (bm *Manager) callback(ctx context.Context) {
   172  	for {
   173  		select {
   174  		case msg := <-bm.outputQueue:
   175  			err := bm.messageHandler(msg)
   176  			if err != nil {
   177  				log.L().Error("fail to handle a batch message when calling back", zap.Error(err))
   178  			}
   179  		case <-ctx.Done():
   180  			return
   181  		}
   182  	}
   183  }
   184  
   185  func (bm *Manager) cleanupLoop() {
   186  	bm.mu.Lock()
   187  	defer bm.mu.Unlock()
   188  	for k, v := range bm.writerMap {
   189  		v.AddTimeoutTimes()
   190  		if v.Expired() {
   191  			v.Close()
   192  			delete(bm.writerMap, k)
   193  		}
   194  	}
   195  }
   196  
   197  type writerConfig struct {
   198  	expiredThreshold uint32
   199  	sizeLimit        uint64
   200  	msgInterval      time.Duration
   201  }
   202  
   203  var _defaultWriterConfig = &writerConfig{
   204  	expiredThreshold: 2,
   205  	msgInterval:      100 * time.Millisecond,
   206  	sizeLimit:        1000,
   207  }
   208  
   209  type batchWriter struct {
   210  	lifecycle.Readiness
   211  	mu sync.RWMutex
   212  
   213  	manager *Manager
   214  	cfg     writerConfig
   215  
   216  	msgBuffer chan *Message
   217  	curBatch  *batch
   218  
   219  	timeoutTimes uint32
   220  }
   221  
   222  func newBatchWriter(cfg *writerConfig, manager *Manager) *batchWriter {
   223  	bw := &batchWriter{
   224  		manager:   manager,
   225  		cfg:       *cfg,
   226  		msgBuffer: make(chan *Message, _bufferLength),
   227  	}
   228  	go bw.handleMsg()
   229  	bw.TurnOn()
   230  	return bw
   231  }
   232  
   233  func (bw *batchWriter) handleMsg() {
   234  	for {
   235  		select {
   236  		case msg, more := <-bw.msgBuffer:
   237  			if !more {
   238  				bw.closeCurBatch()
   239  				return
   240  			}
   241  			bw.addBatch(msg, more)
   242  		}
   243  	}
   244  }
   245  
   246  func (bw *batchWriter) closeCurBatch() {
   247  	bw.mu.Lock()
   248  	defer bw.mu.Unlock()
   249  	if bw.curBatch != nil {
   250  		bw.curBatch.ready <- struct{}{}
   251  		close(bw.curBatch.ready)
   252  	}
   253  }
   254  
   255  func (bw *batchWriter) addBatch(msg *Message, more bool) {
   256  	bw.mu.Lock()
   257  	defer bw.mu.Unlock()
   258  	if bw.curBatch == nil {
   259  		bw.curBatch = bw.newBatch(bw.cfg.msgInterval, bw.cfg.sizeLimit)
   260  	}
   261  	bw.extendTimeout()
   262  	bw.curBatch.Add(msg)
   263  	if bw.curBatch.Full() {
   264  		bw.curBatch.Flush()
   265  		bw.curBatch = nil
   266  	}
   267  }
   268  
   269  func (bw *batchWriter) newBatch(msgInterval time.Duration, limit uint64) *batch {
   270  	batch := &batch{
   271  		msgs:      make([]*Message, 0),
   272  		sizeLimit: limit,
   273  		timer:     time.NewTimer(msgInterval),
   274  		ready:     make(chan struct{}),
   275  	}
   276  	go bw.awaitBatch(batch)
   277  	return batch
   278  }
   279  
   280  func (bw *batchWriter) awaitBatch(b *batch) {
   281  	select {
   282  	case <-b.timer.C:
   283  	case <-b.ready:
   284  	}
   285  	bw.mu.Lock()
   286  	if bw.curBatch == b {
   287  		bw.curBatch = nil
   288  	}
   289  	bw.mu.Unlock()
   290  	bw.manager.assembleQueue <- b
   291  	b.timer.Stop()
   292  }
   293  
   294  func (bw *batchWriter) extendTimeout() {
   295  	atomic.SwapUint32(&bw.timeoutTimes, 0)
   296  }
   297  
   298  func (bw *batchWriter) Put(msg *Message) error {
   299  	if !bw.IsReady() {
   300  		return errors.New("writer hasn't started yet")
   301  	}
   302  	select {
   303  	case bw.msgBuffer <- msg:
   304  		return nil
   305  	default:
   306  		return errors.New("the msg buffer of writer is full")
   307  	}
   308  }
   309  
   310  func (bw *batchWriter) AddTimeoutTimes() {
   311  	atomic.AddUint32(&bw.timeoutTimes, 1)
   312  }
   313  
   314  func (bw *batchWriter) Expired() bool {
   315  	return atomic.LoadUint32(&bw.timeoutTimes) >= bw.cfg.expiredThreshold
   316  }
   317  
   318  func (bw *batchWriter) Close() {
   319  	bw.TurnOff()
   320  	close(bw.msgBuffer)
   321  }
   322  
   323  type batch struct {
   324  	msgs      []*Message
   325  	sizeLimit uint64
   326  	timer     *time.Timer
   327  	ready     chan struct{}
   328  }
   329  
   330  func (b *batch) Size() int {
   331  	return len(b.msgs)
   332  }
   333  
   334  func (b *batch) Add(msg *Message) {
   335  	b.msgs = append(b.msgs, msg)
   336  }
   337  
   338  func (b *batch) Full() bool {
   339  	return uint64(len(b.msgs)) >= b.sizeLimit
   340  }
   341  
   342  func (b *batch) Flush() {
   343  	b.ready <- struct{}{}
   344  	close(b.ready)
   345  }
   346  
   347  // Message is the message to be batching
   348  type Message struct {
   349  	ChainID uint32
   350  	Data    proto.Message
   351  	Target  *peer.AddrInfo       // target of broadcast msg is nil
   352  	id      *batchID             // cache for Message.BatchID()
   353  	msgType iotexrpc.MessageType // generated by MessageType()
   354  }
   355  
   356  func (msg *Message) batchID() (id batchID, err error) {
   357  	if msg.id != nil {
   358  		id = *msg.id
   359  		return
   360  	}
   361  	buf := new(bytes.Buffer)
   362  	if err = binary.Write(buf, binary.LittleEndian, msg.messageType()); err != nil {
   363  		return
   364  	}
   365  	if err = binary.Write(buf, binary.LittleEndian, msg.ChainID); err != nil {
   366  		return
   367  	}
   368  	var idInBytes []byte
   369  	if msg.Target != nil {
   370  		idInBytes, err = msg.Target.ID.Marshal()
   371  		if err != nil {
   372  			return
   373  		}
   374  	}
   375  	// non-cryptographic fast hash algorithm xxhash is used for generating batchID
   376  	h := xxhash.Sum64(append(buf.Bytes(), idInBytes...))
   377  	id = batchID(h)
   378  	msg.id = &id
   379  	return
   380  }
   381  
   382  func (msg *Message) messageType() iotexrpc.MessageType {
   383  	if msg.msgType == iotexrpc.MessageType_UNKNOWN {
   384  		var err error
   385  		msg.msgType, err = goproto.GetTypeFromRPCMsg(msg.Data)
   386  		if err != nil {
   387  			return iotexrpc.MessageType_UNKNOWN
   388  		}
   389  	}
   390  	return msg.msgType
   391  }