mirror of
https://github.com/luxfi/p2p.git
synced 2026-10-30 15:22:55 +00:00
306 lines
6.7 KiB
Go
306 lines
6.7 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package peer
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/node/utils/buffer"
|
|
"github.com/luxfi/p2p/message"
|
|
"github.com/luxfi/p2p/throttling"
|
|
)
|
|
|
|
const initialQueueSize = 64
|
|
|
|
var (
|
|
_ MessageQueue = (*throttledMessageQueue)(nil)
|
|
_ MessageQueue = (*blockingMessageQueue)(nil)
|
|
)
|
|
|
|
type SendFailedCallback interface {
|
|
SendFailed(message.OutboundMessage)
|
|
}
|
|
|
|
type SendFailedFunc func(message.OutboundMessage)
|
|
|
|
func (f SendFailedFunc) SendFailed(msg message.OutboundMessage) {
|
|
f(msg)
|
|
}
|
|
|
|
type MessageQueue interface {
|
|
// Push attempts to add the message to the queue. If the context is
|
|
// canceled, then pushing the message will return `false` and the message
|
|
// will not be added to the queue.
|
|
Push(ctx context.Context, msg message.OutboundMessage) bool
|
|
|
|
// Pop blocks until a message is available and then returns the message. If
|
|
// the queue is closed, then `false` is returned.
|
|
Pop() (message.OutboundMessage, bool)
|
|
|
|
// PopNow attempts to return a message without blocking. If a message is not
|
|
// available or the queue is closed, then `false` is returned.
|
|
PopNow() (message.OutboundMessage, bool)
|
|
|
|
// Close empties the queue and prevents further messages from being pushed
|
|
// onto it. After calling close once, future calls to close will do nothing.
|
|
Close()
|
|
}
|
|
|
|
type throttledMessageQueue struct {
|
|
onFailed SendFailedCallback
|
|
// [id] of the peer we're sending messages to
|
|
id ids.NodeID
|
|
log log.Logger
|
|
outboundMsgThrottler throttling.OutboundMsgThrottler
|
|
|
|
// Signalled when a message is added to the queue and when Close() is
|
|
// called.
|
|
cond *sync.Cond
|
|
|
|
// closed flags whether the send queue has been closed.
|
|
// [cond.L] must be held while accessing [closed].
|
|
closed bool
|
|
|
|
// queue of the messages
|
|
// [cond.L] must be held while accessing [queue].
|
|
queue buffer.Deque[message.OutboundMessage]
|
|
}
|
|
|
|
func NewThrottledMessageQueue(
|
|
onFailed SendFailedCallback,
|
|
id ids.NodeID,
|
|
log log.Logger,
|
|
outboundMsgThrottler throttling.OutboundMsgThrottler,
|
|
) MessageQueue {
|
|
return &throttledMessageQueue{
|
|
onFailed: onFailed,
|
|
id: id,
|
|
log: log,
|
|
outboundMsgThrottler: outboundMsgThrottler,
|
|
cond: sync.NewCond(&sync.Mutex{}),
|
|
queue: buffer.NewUnboundedDeque[message.OutboundMessage](initialQueueSize),
|
|
}
|
|
}
|
|
|
|
func (q *throttledMessageQueue) Push(ctx context.Context, msg message.OutboundMessage) bool {
|
|
if err := ctx.Err(); err != nil {
|
|
q.log.Debug(
|
|
"dropping outgoing message",
|
|
log.Stringer("messageOp", msg.Op()),
|
|
log.Stringer("nodeID", q.id),
|
|
log.Err(err),
|
|
)
|
|
q.onFailed.SendFailed(msg)
|
|
return false
|
|
}
|
|
|
|
// Acquire space on the outbound message queue, or drop [msg] if we can't.
|
|
if !q.outboundMsgThrottler.Acquire(msg, q.id) {
|
|
q.log.Debug(
|
|
"dropping outgoing message",
|
|
log.String("reason", "rate-limiting"),
|
|
log.Stringer("messageOp", msg.Op()),
|
|
log.Stringer("nodeID", q.id),
|
|
)
|
|
q.onFailed.SendFailed(msg)
|
|
return false
|
|
}
|
|
|
|
// Invariant: must call q.outboundMsgThrottler.Release(msg, q.id) when [msg]
|
|
// is popped or, if this queue closes before [msg] is popped, when this
|
|
// queue closes.
|
|
|
|
q.cond.L.Lock()
|
|
defer q.cond.L.Unlock()
|
|
|
|
if q.closed {
|
|
q.log.Debug(
|
|
"dropping outgoing message",
|
|
log.String("reason", "closed queue"),
|
|
log.Stringer("messageOp", msg.Op()),
|
|
log.Stringer("nodeID", q.id),
|
|
)
|
|
q.outboundMsgThrottler.Release(msg, q.id)
|
|
q.onFailed.SendFailed(msg)
|
|
return false
|
|
}
|
|
|
|
q.queue.PushRight(msg)
|
|
q.cond.Signal()
|
|
return true
|
|
}
|
|
|
|
func (q *throttledMessageQueue) Pop() (message.OutboundMessage, bool) {
|
|
q.cond.L.Lock()
|
|
defer q.cond.L.Unlock()
|
|
|
|
for {
|
|
if q.closed {
|
|
return nil, false
|
|
}
|
|
if q.queue.Len() > 0 {
|
|
// There is a message
|
|
break
|
|
}
|
|
// Wait until there is a message
|
|
q.cond.Wait()
|
|
}
|
|
|
|
return q.pop(), true
|
|
}
|
|
|
|
func (q *throttledMessageQueue) PopNow() (message.OutboundMessage, bool) {
|
|
q.cond.L.Lock()
|
|
defer q.cond.L.Unlock()
|
|
|
|
if q.closed || q.queue.Len() == 0 {
|
|
// There isn't a message
|
|
return nil, false
|
|
}
|
|
|
|
return q.pop(), true
|
|
}
|
|
|
|
func (q *throttledMessageQueue) pop() message.OutboundMessage {
|
|
msg, _ := q.queue.PopLeft()
|
|
|
|
q.outboundMsgThrottler.Release(msg, q.id)
|
|
return msg
|
|
}
|
|
|
|
func (q *throttledMessageQueue) Close() {
|
|
q.cond.L.Lock()
|
|
defer q.cond.L.Unlock()
|
|
|
|
if q.closed {
|
|
return
|
|
}
|
|
|
|
q.closed = true
|
|
|
|
for q.queue.Len() > 0 {
|
|
msg, _ := q.queue.PopLeft()
|
|
q.outboundMsgThrottler.Release(msg, q.id)
|
|
q.onFailed.SendFailed(msg)
|
|
}
|
|
q.queue = nil
|
|
|
|
q.cond.Broadcast()
|
|
}
|
|
|
|
type blockingMessageQueue struct {
|
|
onFailed SendFailedCallback
|
|
log log.Logger
|
|
|
|
closeOnce sync.Once
|
|
closingLock sync.RWMutex
|
|
closing chan struct{}
|
|
|
|
// queue of the messages
|
|
queue chan message.OutboundMessage
|
|
}
|
|
|
|
func NewBlockingMessageQueue(
|
|
onFailed SendFailedCallback,
|
|
log log.Logger,
|
|
bufferSize int,
|
|
) MessageQueue {
|
|
return &blockingMessageQueue{
|
|
onFailed: onFailed,
|
|
log: log,
|
|
|
|
closing: make(chan struct{}),
|
|
queue: make(chan message.OutboundMessage, bufferSize),
|
|
}
|
|
}
|
|
|
|
func (q *blockingMessageQueue) Push(ctx context.Context, msg message.OutboundMessage) bool {
|
|
q.closingLock.RLock()
|
|
defer q.closingLock.RUnlock()
|
|
|
|
ctxDone := ctx.Done()
|
|
select {
|
|
case <-q.closing:
|
|
q.log.Debug(
|
|
"dropping message",
|
|
log.String("reason", "closed queue"),
|
|
log.Stringer("messageOp", msg.Op()),
|
|
)
|
|
q.onFailed.SendFailed(msg)
|
|
return false
|
|
case <-ctxDone:
|
|
q.log.Debug(
|
|
"dropping message",
|
|
log.String("reason", "cancelled context"),
|
|
log.Stringer("messageOp", msg.Op()),
|
|
)
|
|
q.onFailed.SendFailed(msg)
|
|
return false
|
|
default:
|
|
}
|
|
|
|
select {
|
|
case q.queue <- msg:
|
|
return true
|
|
case <-ctxDone:
|
|
q.log.Debug(
|
|
"dropping message",
|
|
log.String("reason", "cancelled context"),
|
|
log.Stringer("messageOp", msg.Op()),
|
|
)
|
|
q.onFailed.SendFailed(msg)
|
|
return false
|
|
case <-q.closing:
|
|
q.log.Debug(
|
|
"dropping message",
|
|
log.String("reason", "closed queue"),
|
|
log.Stringer("messageOp", msg.Op()),
|
|
)
|
|
q.onFailed.SendFailed(msg)
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (q *blockingMessageQueue) Pop() (message.OutboundMessage, bool) {
|
|
select {
|
|
case msg := <-q.queue:
|
|
return msg, true
|
|
case <-q.closing:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
func (q *blockingMessageQueue) PopNow() (message.OutboundMessage, bool) {
|
|
select {
|
|
case msg := <-q.queue:
|
|
return msg, true
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
func (q *blockingMessageQueue) Close() {
|
|
q.closeOnce.Do(func() {
|
|
close(q.closing)
|
|
|
|
q.closingLock.Lock()
|
|
defer q.closingLock.Unlock()
|
|
|
|
for {
|
|
select {
|
|
case msg := <-q.queue:
|
|
q.onFailed.SendFailed(msg)
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
})
|
|
}
|