mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
329 lines
7.9 KiB
Go
329 lines
7.9 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package message
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
compression "github.com/luxfi/compress"
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/metric"
|
|
"github.com/luxfi/node/proto/p2p"
|
|
"github.com/luxfi/timer/mockable"
|
|
)
|
|
|
|
const (
|
|
typeLabel = "type"
|
|
opLabel = "op"
|
|
directionLabel = "direction"
|
|
|
|
compressionLabel = "compression"
|
|
decompressionLabel = "decompression"
|
|
)
|
|
|
|
var (
|
|
_ InboundMessage = (*inboundMessage)(nil)
|
|
_ OutboundMessage = (*outboundMessage)(nil)
|
|
|
|
metricLabels = []string{typeLabel, opLabel, directionLabel}
|
|
|
|
errUnknownCompressionType = errors.New("message is compressed with an unknown compression type")
|
|
)
|
|
|
|
// InboundMessage represents a set of fields for an inbound message
|
|
type InboundMessage interface {
|
|
fmt.Stringer
|
|
// NodeID returns the ID of the node that sent this message
|
|
NodeID() ids.NodeID
|
|
// Op returns the op that describes this message type
|
|
Op() Op
|
|
// Message returns the message that was sent
|
|
Message() fmt.Stringer
|
|
// Expiration returns the time that the sender will have already timed out
|
|
// this request
|
|
Expiration() time.Time
|
|
// OnFinishedHandling must be called one time when this message has been
|
|
// handled by the message handler
|
|
OnFinishedHandling()
|
|
// BytesSavedCompression returns the number of bytes that this message saved
|
|
// due to being compressed
|
|
BytesSavedCompression() int
|
|
}
|
|
|
|
type inboundMessage struct {
|
|
nodeID ids.NodeID
|
|
op Op
|
|
message fmt.Stringer
|
|
expiration time.Time
|
|
onFinishedHandling func()
|
|
bytesSavedCompression int
|
|
}
|
|
|
|
func (m *inboundMessage) NodeID() ids.NodeID {
|
|
return m.nodeID
|
|
}
|
|
|
|
func (m *inboundMessage) Op() Op {
|
|
return m.op
|
|
}
|
|
|
|
func (m *inboundMessage) Message() fmt.Stringer {
|
|
return m.message
|
|
}
|
|
|
|
func (m *inboundMessage) Expiration() time.Time {
|
|
return m.expiration
|
|
}
|
|
|
|
func (m *inboundMessage) OnFinishedHandling() {
|
|
if m.onFinishedHandling != nil {
|
|
m.onFinishedHandling()
|
|
}
|
|
}
|
|
|
|
func (m *inboundMessage) BytesSavedCompression() int {
|
|
return m.bytesSavedCompression
|
|
}
|
|
|
|
func (m *inboundMessage) String() string {
|
|
return fmt.Sprintf("%s Op: %s Message: %s",
|
|
m.nodeID, m.op, m.message)
|
|
}
|
|
|
|
// OutboundMessage represents a set of fields for an outbound message that can
|
|
// be serialized into a byte stream
|
|
type OutboundMessage interface {
|
|
// BypassThrottling returns true if we should send this message, regardless
|
|
// of any outbound message throttling
|
|
BypassThrottling() bool
|
|
// Op returns the op that describes this message type
|
|
Op() Op
|
|
// Bytes returns the bytes that will be sent
|
|
Bytes() []byte
|
|
// BytesSavedCompression returns the number of bytes that this message saved
|
|
// due to being compressed
|
|
BytesSavedCompression() int
|
|
}
|
|
|
|
type outboundMessage struct {
|
|
bypassThrottling bool
|
|
op Op
|
|
bytes []byte
|
|
bytesSavedCompression int
|
|
}
|
|
|
|
func (m *outboundMessage) BypassThrottling() bool {
|
|
return m.bypassThrottling
|
|
}
|
|
|
|
func (m *outboundMessage) Op() Op {
|
|
return m.op
|
|
}
|
|
|
|
func (m *outboundMessage) Bytes() []byte {
|
|
return m.bytes
|
|
}
|
|
|
|
func (m *outboundMessage) BytesSavedCompression() int {
|
|
return m.bytesSavedCompression
|
|
}
|
|
|
|
type msgBuilder struct {
|
|
zstdCompressor compression.Compressor
|
|
count metric.CounterVec // type + op + direction
|
|
duration metric.GaugeVec // type + op + direction
|
|
|
|
maxMessageTimeout time.Duration
|
|
}
|
|
|
|
func newMsgBuilder(
|
|
metrics metric.Registerer,
|
|
maxMessageTimeout time.Duration,
|
|
) (*msgBuilder, error) {
|
|
zstdCompressor, err := compression.NewZstdCompressor(constants.DefaultMaxMessageSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mb := &msgBuilder{
|
|
zstdCompressor: zstdCompressor,
|
|
count: metric.NewCounterVec(
|
|
metric.CounterOpts{
|
|
Name: "codec_compressed_count",
|
|
Help: "number of compressed messages",
|
|
},
|
|
metricLabels,
|
|
),
|
|
duration: metric.NewGaugeVec(
|
|
metric.GaugeOpts{
|
|
Name: "codec_compressed_duration",
|
|
Help: "time spent handling compressed messages",
|
|
},
|
|
metricLabels,
|
|
),
|
|
|
|
maxMessageTimeout: maxMessageTimeout,
|
|
}
|
|
return mb, nil
|
|
}
|
|
|
|
func (mb *msgBuilder) marshal(
|
|
uncompressedMsg *p2p.Message,
|
|
compressionType compression.Type,
|
|
) ([]byte, int, Op, error) {
|
|
uncompressedMsgBytes, err := p2p.Marshal(uncompressedMsg)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
op, err := ToOp(uncompressedMsg)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
// If compression is enabled, we marshal twice:
|
|
// 1. the original message
|
|
// 2. the message with compressed bytes
|
|
//
|
|
// This recursive packing allows us to avoid an extra compression on/off
|
|
// field in the message.
|
|
var (
|
|
startTime = time.Now()
|
|
compressedMsg p2p.Message
|
|
)
|
|
switch compressionType {
|
|
case compression.TypeNone:
|
|
return uncompressedMsgBytes, 0, op, nil
|
|
case compression.TypeZstd:
|
|
compressedBytes, err := mb.zstdCompressor.Compress(uncompressedMsgBytes)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
compressedMsg = p2p.Message{
|
|
Message: &p2p.Message_CompressedZstd{
|
|
CompressedZstd: compressedBytes,
|
|
},
|
|
}
|
|
default:
|
|
return nil, 0, 0, errUnknownCompressionType
|
|
}
|
|
|
|
compressedMsgBytes, err := p2p.Marshal(&compressedMsg)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
compressTook := time.Since(startTime)
|
|
|
|
labels := metric.Labels{
|
|
typeLabel: compressionType.String(),
|
|
opLabel: op.String(),
|
|
directionLabel: compressionLabel,
|
|
}
|
|
mb.count.With(labels).Inc()
|
|
mb.duration.With(labels).Add(float64(compressTook))
|
|
|
|
bytesSaved := len(uncompressedMsgBytes) - len(compressedMsgBytes)
|
|
return compressedMsgBytes, bytesSaved, op, nil
|
|
}
|
|
|
|
func (mb *msgBuilder) unmarshal(b []byte) (*p2p.Message, int, Op, error) {
|
|
m := new(p2p.Message)
|
|
if err := p2p.Unmarshal(b, m); err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
// Figure out what compression type, if any, was used to compress the message.
|
|
var (
|
|
compressor compression.Compressor
|
|
compressedBytes []byte
|
|
zstdCompressed = m.GetCompressedZstd()
|
|
)
|
|
switch {
|
|
case len(zstdCompressed) > 0:
|
|
compressor = mb.zstdCompressor
|
|
compressedBytes = zstdCompressed
|
|
default:
|
|
// The message wasn't compressed
|
|
op, err := ToOp(m)
|
|
return m, 0, op, err
|
|
}
|
|
|
|
startTime := time.Now()
|
|
|
|
decompressed, err := compressor.Decompress(compressedBytes)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
bytesSavedCompression := len(decompressed) - len(compressedBytes)
|
|
|
|
if err := p2p.Unmarshal(decompressed, m); err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
decompressTook := time.Since(startTime)
|
|
|
|
// Record decompression time metrics
|
|
op, err := ToOp(m)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
labels := metric.Labels{
|
|
typeLabel: compression.TypeZstd.String(),
|
|
opLabel: op.String(),
|
|
directionLabel: decompressionLabel,
|
|
}
|
|
mb.count.With(labels).Inc()
|
|
mb.duration.With(labels).Add(float64(decompressTook))
|
|
|
|
return m, bytesSavedCompression, op, nil
|
|
}
|
|
|
|
func (mb *msgBuilder) createOutbound(m *p2p.Message, compressionType compression.Type, bypassThrottling bool) (*outboundMessage, error) {
|
|
b, saved, op, err := mb.marshal(m, compressionType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &outboundMessage{
|
|
bypassThrottling: bypassThrottling,
|
|
op: op,
|
|
bytes: b,
|
|
bytesSavedCompression: saved,
|
|
}, nil
|
|
}
|
|
|
|
func (mb *msgBuilder) parseInbound(
|
|
bytes []byte,
|
|
nodeID ids.NodeID,
|
|
onFinishedHandling func(),
|
|
) (*inboundMessage, error) {
|
|
m, bytesSavedCompression, op, err := mb.unmarshal(bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
msg, err := Unwrap(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
expiration := mockable.MaxTime
|
|
if deadline, ok := GetDeadline(msg); ok {
|
|
deadline = min(deadline, mb.maxMessageTimeout)
|
|
expiration = time.Now().Add(deadline)
|
|
}
|
|
|
|
return &inboundMessage{
|
|
nodeID: nodeID,
|
|
op: op,
|
|
message: msg,
|
|
expiration: expiration,
|
|
onFinishedHandling: onFinishedHandling,
|
|
bytesSavedCompression: bytesSavedCompression,
|
|
}, nil
|
|
}
|