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 }