github.com/MetalBlockchain/metalgo@v1.11.9/network/peer/message_queue.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package peer 5 6 import ( 7 "context" 8 "sync" 9 10 "go.uber.org/zap" 11 12 "github.com/MetalBlockchain/metalgo/ids" 13 "github.com/MetalBlockchain/metalgo/message" 14 "github.com/MetalBlockchain/metalgo/network/throttling" 15 "github.com/MetalBlockchain/metalgo/utils/buffer" 16 "github.com/MetalBlockchain/metalgo/utils/logging" 17 ) 18 19 const initialQueueSize = 64 20 21 var ( 22 _ MessageQueue = (*throttledMessageQueue)(nil) 23 _ MessageQueue = (*blockingMessageQueue)(nil) 24 ) 25 26 type SendFailedCallback interface { 27 SendFailed(message.OutboundMessage) 28 } 29 30 type SendFailedFunc func(message.OutboundMessage) 31 32 func (f SendFailedFunc) SendFailed(msg message.OutboundMessage) { 33 f(msg) 34 } 35 36 type MessageQueue interface { 37 // Push attempts to add the message to the queue. If the context is 38 // canceled, then pushing the message will return `false` and the message 39 // will not be added to the queue. 40 Push(ctx context.Context, msg message.OutboundMessage) bool 41 42 // Pop blocks until a message is available and then returns the message. If 43 // the queue is closed, then `false` is returned. 44 Pop() (message.OutboundMessage, bool) 45 46 // PopNow attempts to return a message without blocking. If a message is not 47 // available or the queue is closed, then `false` is returned. 48 PopNow() (message.OutboundMessage, bool) 49 50 // Close empties the queue and prevents further messages from being pushed 51 // onto it. After calling close once, future calls to close will do nothing. 52 Close() 53 } 54 55 type throttledMessageQueue struct { 56 onFailed SendFailedCallback 57 // [id] of the peer we're sending messages to 58 id ids.NodeID 59 log logging.Logger 60 outboundMsgThrottler throttling.OutboundMsgThrottler 61 62 // Signalled when a message is added to the queue and when Close() is 63 // called. 64 cond *sync.Cond 65 66 // closed flags whether the send queue has been closed. 67 // [cond.L] must be held while accessing [closed]. 68 closed bool 69 70 // queue of the messages 71 // [cond.L] must be held while accessing [queue]. 72 queue buffer.Deque[message.OutboundMessage] 73 } 74 75 func NewThrottledMessageQueue( 76 onFailed SendFailedCallback, 77 id ids.NodeID, 78 log logging.Logger, 79 outboundMsgThrottler throttling.OutboundMsgThrottler, 80 ) MessageQueue { 81 return &throttledMessageQueue{ 82 onFailed: onFailed, 83 id: id, 84 log: log, 85 outboundMsgThrottler: outboundMsgThrottler, 86 cond: sync.NewCond(&sync.Mutex{}), 87 queue: buffer.NewUnboundedDeque[message.OutboundMessage](initialQueueSize), 88 } 89 } 90 91 func (q *throttledMessageQueue) Push(ctx context.Context, msg message.OutboundMessage) bool { 92 if err := ctx.Err(); err != nil { 93 q.log.Debug( 94 "dropping outgoing message", 95 zap.Stringer("messageOp", msg.Op()), 96 zap.Stringer("nodeID", q.id), 97 zap.Error(err), 98 ) 99 q.onFailed.SendFailed(msg) 100 return false 101 } 102 103 // Acquire space on the outbound message queue, or drop [msg] if we can't. 104 if !q.outboundMsgThrottler.Acquire(msg, q.id) { 105 q.log.Debug( 106 "dropping outgoing message", 107 zap.String("reason", "rate-limiting"), 108 zap.Stringer("messageOp", msg.Op()), 109 zap.Stringer("nodeID", q.id), 110 ) 111 q.onFailed.SendFailed(msg) 112 return false 113 } 114 115 // Invariant: must call q.outboundMsgThrottler.Release(msg, q.id) when [msg] 116 // is popped or, if this queue closes before [msg] is popped, when this 117 // queue closes. 118 119 q.cond.L.Lock() 120 defer q.cond.L.Unlock() 121 122 if q.closed { 123 q.log.Debug( 124 "dropping outgoing message", 125 zap.String("reason", "closed queue"), 126 zap.Stringer("messageOp", msg.Op()), 127 zap.Stringer("nodeID", q.id), 128 ) 129 q.outboundMsgThrottler.Release(msg, q.id) 130 q.onFailed.SendFailed(msg) 131 return false 132 } 133 134 q.queue.PushRight(msg) 135 q.cond.Signal() 136 return true 137 } 138 139 func (q *throttledMessageQueue) Pop() (message.OutboundMessage, bool) { 140 q.cond.L.Lock() 141 defer q.cond.L.Unlock() 142 143 for { 144 if q.closed { 145 return nil, false 146 } 147 if q.queue.Len() > 0 { 148 // There is a message 149 break 150 } 151 // Wait until there is a message 152 q.cond.Wait() 153 } 154 155 return q.pop(), true 156 } 157 158 func (q *throttledMessageQueue) PopNow() (message.OutboundMessage, bool) { 159 q.cond.L.Lock() 160 defer q.cond.L.Unlock() 161 162 if q.closed || q.queue.Len() == 0 { 163 // There isn't a message 164 return nil, false 165 } 166 167 return q.pop(), true 168 } 169 170 func (q *throttledMessageQueue) pop() message.OutboundMessage { 171 msg, _ := q.queue.PopLeft() 172 173 q.outboundMsgThrottler.Release(msg, q.id) 174 return msg 175 } 176 177 func (q *throttledMessageQueue) Close() { 178 q.cond.L.Lock() 179 defer q.cond.L.Unlock() 180 181 if q.closed { 182 return 183 } 184 185 q.closed = true 186 187 for q.queue.Len() > 0 { 188 msg, _ := q.queue.PopLeft() 189 q.outboundMsgThrottler.Release(msg, q.id) 190 q.onFailed.SendFailed(msg) 191 } 192 q.queue = nil 193 194 q.cond.Broadcast() 195 } 196 197 type blockingMessageQueue struct { 198 onFailed SendFailedCallback 199 log logging.Logger 200 201 closeOnce sync.Once 202 closingLock sync.RWMutex 203 closing chan struct{} 204 205 // queue of the messages 206 queue chan message.OutboundMessage 207 } 208 209 func NewBlockingMessageQueue( 210 onFailed SendFailedCallback, 211 log logging.Logger, 212 bufferSize int, 213 ) MessageQueue { 214 return &blockingMessageQueue{ 215 onFailed: onFailed, 216 log: log, 217 218 closing: make(chan struct{}), 219 queue: make(chan message.OutboundMessage, bufferSize), 220 } 221 } 222 223 func (q *blockingMessageQueue) Push(ctx context.Context, msg message.OutboundMessage) bool { 224 q.closingLock.RLock() 225 defer q.closingLock.RUnlock() 226 227 ctxDone := ctx.Done() 228 select { 229 case <-q.closing: 230 q.log.Debug( 231 "dropping message", 232 zap.String("reason", "closed queue"), 233 zap.Stringer("messageOp", msg.Op()), 234 ) 235 q.onFailed.SendFailed(msg) 236 return false 237 case <-ctxDone: 238 q.log.Debug( 239 "dropping message", 240 zap.String("reason", "cancelled context"), 241 zap.Stringer("messageOp", msg.Op()), 242 ) 243 q.onFailed.SendFailed(msg) 244 return false 245 default: 246 } 247 248 select { 249 case q.queue <- msg: 250 return true 251 case <-ctxDone: 252 q.log.Debug( 253 "dropping message", 254 zap.String("reason", "cancelled context"), 255 zap.Stringer("messageOp", msg.Op()), 256 ) 257 q.onFailed.SendFailed(msg) 258 return false 259 case <-q.closing: 260 q.log.Debug( 261 "dropping message", 262 zap.String("reason", "closed queue"), 263 zap.Stringer("messageOp", msg.Op()), 264 ) 265 q.onFailed.SendFailed(msg) 266 return false 267 } 268 } 269 270 func (q *blockingMessageQueue) Pop() (message.OutboundMessage, bool) { 271 select { 272 case msg := <-q.queue: 273 return msg, true 274 case <-q.closing: 275 return nil, false 276 } 277 } 278 279 func (q *blockingMessageQueue) PopNow() (message.OutboundMessage, bool) { 280 select { 281 case msg := <-q.queue: 282 return msg, true 283 default: 284 return nil, false 285 } 286 } 287 288 func (q *blockingMessageQueue) Close() { 289 q.closeOnce.Do(func() { 290 close(q.closing) 291 292 q.closingLock.Lock() 293 defer q.closingLock.Unlock() 294 295 for { 296 select { 297 case msg := <-q.queue: 298 q.onFailed.SendFailed(msg) 299 default: 300 return 301 } 302 } 303 }) 304 }