mirror of
https://github.com/hanzoai/kafka.git
synced 2026-10-30 14:48:25 +00:00
The 2026-07 insights outage, decomplected into its three real defects: - CommitOffset rejected any offset above 2^50 as 'corruption' and GetCommittedOffset purged stored ones. Hanzo PubSub allocates sequences from a sparse e18 space, so every legitimate commit was refused (librdkafka's 'Unknown broker error') and group state self-destructed. Offsets are opaque int64s now; only negative is invalid. - Anything published raw onto a kafka-* subject was served verbatim as a record batch; the client read its bytes 8-11 as a batch length and died with MSG_SIZE_TOO_LARGE, produce died parsing it, and binary search went blind — one stray message wedged the partition until a stream purge. walkBatches() is now the single gate: produce refuses chains that do not walk (INVALID_RECORD), fetch and bounds skip stored messages that do not. - Sequence arithmetic assumed density. All log positions now come from the stamped batch headers via partitionBounds/findRecordSet, reading through NextMessage (by-start-sequence subscription) so holes and e18-sparse sequence spaces behave identically to dense ones. Also: fetch answers OFFSET_OUT_OF_RANGE instead of stalling consumers on dead positions; per-partition errors replace the top-level goto; the response encoder grows to fit instead of silently truncating past 64KiB; request frames are capped at 128MiB so a port scan cannot OOM the broker; OffsetFetch v8 no longer misparses (member fields are v9+) and its response drops a field the schema never had. Co-authored-by: Hanzo Dev <[email protected]>
135 lines
4.7 KiB
Go
135 lines
4.7 KiB
Go
package protocol
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
)
|
|
|
|
// RecordBatchHeader contains parsed fields from a Kafka RecordBatch v2 header.
|
|
//
|
|
// Header layout (61 bytes minimum):
|
|
//
|
|
// baseOffset: int64 (bytes 0-7) — NOT covered by CRC
|
|
// batchLength: int32 (bytes 8-11) — NOT covered by CRC
|
|
// partitionLeaderEpoch: int32 (bytes 12-15) — NOT covered by CRC
|
|
// magic: int8 (byte 16) — NOT covered by CRC
|
|
// crc: int32 (bytes 17-20) — NOT covered by CRC
|
|
// attributes: int16 (bytes 21-22) — CRC starts here
|
|
// lastOffsetDelta: int32 (bytes 23-26)
|
|
// baseTimestamp: int64 (bytes 27-34)
|
|
// maxTimestamp: int64 (bytes 35-42)
|
|
// producerId: int64 (bytes 43-50)
|
|
// producerEpoch: int16 (bytes 51-52)
|
|
// baseSequence: int32 (bytes 53-56)
|
|
// recordCount: int32 (bytes 57-60)
|
|
type RecordBatchHeader struct {
|
|
BaseOffset int64
|
|
BatchLength int32
|
|
LastOffsetDelta int32
|
|
RecordCount int32
|
|
}
|
|
|
|
const recordBatchHeaderMinSize = 61
|
|
|
|
// recordBatchMagic is the only batch format this gateway stores or serves.
|
|
const recordBatchMagic = 2
|
|
|
|
// batchPrefix is the number of bytes batchLength does not count:
|
|
// baseOffset (8) + batchLength itself (4).
|
|
const batchPrefix = 12
|
|
|
|
// ParseRecordBatchHeader extracts header fields from raw RecordBatch bytes.
|
|
func ParseRecordBatchHeader(data []byte) (RecordBatchHeader, error) {
|
|
if len(data) < recordBatchHeaderMinSize {
|
|
return RecordBatchHeader{}, fmt.Errorf("record batch too short: %d bytes", len(data))
|
|
}
|
|
return RecordBatchHeader{
|
|
BaseOffset: int64(binary.BigEndian.Uint64(data[0:8])),
|
|
BatchLength: int32(binary.BigEndian.Uint32(data[8:12])),
|
|
LastOffsetDelta: int32(binary.BigEndian.Uint32(data[23:27])),
|
|
RecordCount: int32(binary.BigEndian.Uint32(data[57:61])),
|
|
}, nil
|
|
}
|
|
|
|
// SetBaseOffset overwrites the baseOffset field in RecordBatch bytes.
|
|
// This field is NOT covered by the CRC (which starts at the attributes field,
|
|
// byte 21), so modifying it does not invalidate the batch checksum.
|
|
func SetBaseOffset(data []byte, baseOffset int64) {
|
|
binary.BigEndian.PutUint64(data[0:8], uint64(baseOffset))
|
|
}
|
|
|
|
// OffsetCount returns the number of Kafka offsets consumed by this batch.
|
|
func (h RecordBatchHeader) OffsetCount() int64 {
|
|
return int64(h.LastOffsetDelta) + 1
|
|
}
|
|
|
|
// Size returns the batch's total wire size (batchLength counts from the
|
|
// partitionLeaderEpoch field, so the 12-byte prefix is added back).
|
|
func (h RecordBatchHeader) Size() int {
|
|
return batchPrefix + int(h.BatchLength)
|
|
}
|
|
|
|
// batchAt is a parsed batch header plus its byte position in a record set.
|
|
type batchAt struct {
|
|
At int
|
|
Header RecordBatchHeader
|
|
}
|
|
|
|
// walkBatches parses the RecordBatch chain covering data, succeeding only when
|
|
// the chain is well-formed: every batch has magic v2 and a sane length, and
|
|
// the last one ends exactly at len(data). Anything else is not a record set
|
|
// this broker will store or serve — a raw publish onto a kafka-* subject, a
|
|
// truncated write, corruption. One such message served verbatim poisons every
|
|
// consumer that fetches it (the client reads its garbage bytes as a batch
|
|
// length), so this walk is the single gate both produce and fetch trust.
|
|
func walkBatches(data []byte) ([]batchAt, bool) {
|
|
var out []batchAt
|
|
at := 0
|
|
for at < len(data) {
|
|
rest := data[at:]
|
|
if len(rest) < recordBatchHeaderMinSize || rest[16] != recordBatchMagic {
|
|
return nil, false
|
|
}
|
|
h, err := ParseRecordBatchHeader(rest)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
if h.BatchLength < recordBatchHeaderMinSize-batchPrefix ||
|
|
h.LastOffsetDelta < 0 || h.RecordCount < 1 ||
|
|
at+h.Size() > len(data) {
|
|
return nil, false
|
|
}
|
|
out = append(out, batchAt{At: at, Header: h})
|
|
at += h.Size()
|
|
}
|
|
return out, len(out) > 0
|
|
}
|
|
|
|
// stampBaseOffsets rewrites every batch's baseOffset so the chain occupies
|
|
// Kafka offsets [base, base+n). Returns n, or ok=false if data is not a valid
|
|
// batch chain (in which case nothing is modified).
|
|
func stampBaseOffsets(data []byte, base int64) (int64, bool) {
|
|
batches, ok := walkBatches(data)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
n := int64(0)
|
|
for _, b := range batches {
|
|
SetBaseOffset(data[b.At:], base+n)
|
|
n += b.Header.OffsetCount()
|
|
}
|
|
return n, true
|
|
}
|
|
|
|
// batchSpan reports the Kafka offsets [first, last] covered by a stored record
|
|
// set, ok=false if the data does not walk as a valid chain.
|
|
func batchSpan(data []byte) (first, last int64, ok bool) {
|
|
batches, ok := walkBatches(data)
|
|
if !ok {
|
|
return 0, 0, false
|
|
}
|
|
h0 := batches[0].Header
|
|
hn := batches[len(batches)-1].Header
|
|
return h0.BaseOffset, hn.BaseOffset + int64(hn.LastOffsetDelta), true
|
|
}
|