feat: 完善 RecordStream 的协议协商、运行观测与文档说明
- 将 RecordStream 出站路径收敛为单 writer loop - 支持在 batch header 中 piggyback AckSeq,保留独立 ack 作为兼容回退 - 增加 record stream 打开阶段能力协商,支持 mixed-version peer 自动降级 - 补充 RecordSnapshot 与 diagnostics summary 的 record-plane 观测项 - 增加 batch/ack/error frame、piggyback ack、barrier 等待拆分与 apply backlog 指标 - 收紧 TransportConn detach 后的 runtime snapshot 语义 - 补充 README 中的 RecordStream 语义、兼容行为与诊断快照说明 - 补充相关单测与 race 回归验证
This commit is contained in:
+117
-33
@@ -6,14 +6,16 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
recordFrameMagic = "NRS1"
|
||||
recordFrameVersion = 1
|
||||
recordFrameTypeBatch uint8 = 1
|
||||
recordFrameTypeAck uint8 = 2
|
||||
recordFrameTypeError uint8 = 3
|
||||
recordFrameHeaderSize = 8
|
||||
recordBatchHeaderSize = 10
|
||||
recordErrorHeaderSize = 16
|
||||
recordFrameMagic = "NRS1"
|
||||
recordFrameVersionV1 = 1
|
||||
recordFrameVersionV2 = 2
|
||||
recordFrameTypeBatch uint8 = 1
|
||||
recordFrameTypeAck uint8 = 2
|
||||
recordFrameTypeError uint8 = 3
|
||||
recordFrameHeaderSize = 8
|
||||
recordBatchHeaderV1Size = 10
|
||||
recordBatchHeaderV2Size = 18
|
||||
recordErrorHeaderSize = 16
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -27,6 +29,7 @@ type recordOutboundMessage struct {
|
||||
}
|
||||
|
||||
type recordFrame struct {
|
||||
Version uint8
|
||||
Type uint8
|
||||
Batch []recordOutboundMessage
|
||||
AckSeq uint64
|
||||
@@ -34,7 +37,7 @@ type recordFrame struct {
|
||||
Retryable bool
|
||||
}
|
||||
|
||||
func encodeRecordBatchFrame(batch []recordOutboundMessage) ([]byte, error) {
|
||||
func encodeRecordBatchFrame(batch []recordOutboundMessage, ackSeq uint64, useV2 bool) ([]byte, error) {
|
||||
if len(batch) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -42,7 +45,13 @@ func encodeRecordBatchFrame(batch []recordOutboundMessage) ([]byte, error) {
|
||||
if firstSeq == 0 {
|
||||
return nil, errRecordSeqInvalid
|
||||
}
|
||||
size := recordFrameHeaderSize + recordBatchHeaderSize
|
||||
version := uint8(recordFrameVersionV1)
|
||||
batchHeaderSize := recordBatchHeaderV1Size
|
||||
if useV2 {
|
||||
version = recordFrameVersionV2
|
||||
batchHeaderSize = recordBatchHeaderV2Size
|
||||
}
|
||||
size := recordFrameHeaderSize + batchHeaderSize
|
||||
for index, item := range batch {
|
||||
wantSeq := firstSeq + uint64(index)
|
||||
if item.Seq != wantSeq {
|
||||
@@ -52,11 +61,14 @@ func encodeRecordBatchFrame(batch []recordOutboundMessage) ([]byte, error) {
|
||||
}
|
||||
frame := make([]byte, size)
|
||||
copy(frame[:4], recordFrameMagic)
|
||||
frame[4] = recordFrameVersion
|
||||
frame[4] = version
|
||||
frame[5] = recordFrameTypeBatch
|
||||
binary.BigEndian.PutUint16(frame[8:10], uint16(len(batch)))
|
||||
binary.BigEndian.PutUint64(frame[10:18], firstSeq)
|
||||
offset := recordFrameHeaderSize + recordBatchHeaderSize
|
||||
offset := recordFrameHeaderSize + batchHeaderSize
|
||||
if useV2 {
|
||||
binary.BigEndian.PutUint64(frame[18:26], ackSeq)
|
||||
}
|
||||
for _, item := range batch {
|
||||
binary.BigEndian.PutUint32(frame[offset:offset+4], uint32(len(item.Payload)))
|
||||
offset += 4
|
||||
@@ -69,7 +81,7 @@ func encodeRecordBatchFrame(batch []recordOutboundMessage) ([]byte, error) {
|
||||
func encodeRecordAckFrame(ackSeq uint64) ([]byte, error) {
|
||||
frame := make([]byte, recordFrameHeaderSize+8)
|
||||
copy(frame[:4], recordFrameMagic)
|
||||
frame[4] = recordFrameVersion
|
||||
frame[4] = recordFrameVersionV1
|
||||
frame[5] = recordFrameTypeAck
|
||||
binary.BigEndian.PutUint64(frame[8:16], ackSeq)
|
||||
return frame, nil
|
||||
@@ -83,7 +95,7 @@ func encodeRecordErrorFrame(failure RecordFailure) ([]byte, error) {
|
||||
msgBytes := []byte(failure.Message)
|
||||
frame := make([]byte, recordFrameHeaderSize+recordErrorHeaderSize+len(codeBytes)+len(msgBytes))
|
||||
copy(frame[:4], recordFrameMagic)
|
||||
frame[4] = recordFrameVersion
|
||||
frame[4] = recordFrameVersionV1
|
||||
frame[5] = recordFrameTypeError
|
||||
if failure.Retryable {
|
||||
frame[6] = 1
|
||||
@@ -102,30 +114,62 @@ func decodeRecordFrame(payload []byte) (recordFrame, error) {
|
||||
if len(payload) < recordFrameHeaderSize || string(payload[:4]) != recordFrameMagic {
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
if payload[4] != recordFrameVersion {
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
version := payload[4]
|
||||
frameType := payload[5]
|
||||
switch frameType {
|
||||
case recordFrameTypeBatch:
|
||||
return decodeRecordBatchFrame(payload)
|
||||
case recordFrameTypeAck:
|
||||
if len(payload) != recordFrameHeaderSize+8 {
|
||||
switch version {
|
||||
case recordFrameVersionV1:
|
||||
switch frameType {
|
||||
case recordFrameTypeBatch:
|
||||
return decodeRecordBatchFrameV1(payload)
|
||||
case recordFrameTypeAck:
|
||||
if len(payload) != recordFrameHeaderSize+8 {
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
return recordFrame{
|
||||
Version: recordFrameVersionV1,
|
||||
Type: recordFrameTypeAck,
|
||||
AckSeq: binary.BigEndian.Uint64(payload[8:16]),
|
||||
}, nil
|
||||
case recordFrameTypeError:
|
||||
frame, err := decodeRecordErrorFrame(payload)
|
||||
if err != nil {
|
||||
return recordFrame{}, err
|
||||
}
|
||||
frame.Version = recordFrameVersionV1
|
||||
return frame, nil
|
||||
default:
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
case recordFrameVersionV2:
|
||||
switch frameType {
|
||||
case recordFrameTypeBatch:
|
||||
return decodeRecordBatchFrameV2(payload)
|
||||
case recordFrameTypeAck:
|
||||
if len(payload) != recordFrameHeaderSize+8 {
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
return recordFrame{
|
||||
Version: recordFrameVersionV2,
|
||||
Type: recordFrameTypeAck,
|
||||
AckSeq: binary.BigEndian.Uint64(payload[8:16]),
|
||||
}, nil
|
||||
case recordFrameTypeError:
|
||||
frame, err := decodeRecordErrorFrame(payload)
|
||||
if err != nil {
|
||||
return recordFrame{}, err
|
||||
}
|
||||
frame.Version = recordFrameVersionV2
|
||||
return frame, nil
|
||||
default:
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
return recordFrame{
|
||||
Type: recordFrameTypeAck,
|
||||
AckSeq: binary.BigEndian.Uint64(payload[8:16]),
|
||||
}, nil
|
||||
case recordFrameTypeError:
|
||||
return decodeRecordErrorFrame(payload)
|
||||
default:
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRecordBatchFrame(payload []byte) (recordFrame, error) {
|
||||
if len(payload) < recordFrameHeaderSize+recordBatchHeaderSize {
|
||||
func decodeRecordBatchFrameV1(payload []byte) (recordFrame, error) {
|
||||
if len(payload) < recordFrameHeaderSize+recordBatchHeaderV1Size {
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
count := int(binary.BigEndian.Uint16(payload[8:10]))
|
||||
@@ -133,7 +177,7 @@ func decodeRecordBatchFrame(payload []byte) (recordFrame, error) {
|
||||
if count <= 0 || firstSeq == 0 {
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
offset := recordFrameHeaderSize + recordBatchHeaderSize
|
||||
offset := recordFrameHeaderSize + recordBatchHeaderV1Size
|
||||
batch := make([]recordOutboundMessage, 0, count)
|
||||
for index := 0; index < count; index++ {
|
||||
if offset+4 > len(payload) {
|
||||
@@ -155,8 +199,48 @@ func decodeRecordBatchFrame(payload []byte) (recordFrame, error) {
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
return recordFrame{
|
||||
Type: recordFrameTypeBatch,
|
||||
Batch: batch,
|
||||
Version: recordFrameVersionV1,
|
||||
Type: recordFrameTypeBatch,
|
||||
Batch: batch,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeRecordBatchFrameV2(payload []byte) (recordFrame, error) {
|
||||
if len(payload) < recordFrameHeaderSize+recordBatchHeaderV2Size {
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
count := int(binary.BigEndian.Uint16(payload[8:10]))
|
||||
firstSeq := binary.BigEndian.Uint64(payload[10:18])
|
||||
ackSeq := binary.BigEndian.Uint64(payload[18:26])
|
||||
if count <= 0 || firstSeq == 0 {
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
offset := recordFrameHeaderSize + recordBatchHeaderV2Size
|
||||
batch := make([]recordOutboundMessage, 0, count)
|
||||
for index := 0; index < count; index++ {
|
||||
if offset+4 > len(payload) {
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
itemLen := int(binary.BigEndian.Uint32(payload[offset : offset+4]))
|
||||
offset += 4
|
||||
if itemLen < 0 || offset+itemLen > len(payload) {
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
item := recordOutboundMessage{
|
||||
Seq: firstSeq + uint64(index),
|
||||
Payload: append([]byte(nil), payload[offset:offset+itemLen]...),
|
||||
}
|
||||
offset += itemLen
|
||||
batch = append(batch, item)
|
||||
}
|
||||
if offset != len(payload) {
|
||||
return recordFrame{}, errRecordFrameInvalid
|
||||
}
|
||||
return recordFrame{
|
||||
Version: recordFrameVersionV2,
|
||||
Type: recordFrameTypeBatch,
|
||||
Batch: batch,
|
||||
AckSeq: ackSeq,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user