Files
seaweedfs/weed/mq/kafka/offset/ledger.go
T
chrislu deb315a8a9 persist kafka offset
Phase E2: Integrate Protobuf descriptor parser with decoder

- Update NewProtobufDecoder to use ProtobufDescriptorParser
- Add findFirstMessageName helper for automatic message detection
- Fix ParseBinaryDescriptor to return schema even on resolution failure
- Add comprehensive tests for protobuf decoder integration
- Improve error handling and caching behavior

This enables proper binary descriptor parsing in the protobuf decoder,
completing the integration between descriptor parsing and decoding.

Phase E3: Complete Protobuf message descriptor resolution

- Implement full protobuf descriptor resolution using protoreflect API
- Add buildFileDescriptor and findMessageInFileDescriptor methods
- Support nested message resolution with findNestedMessageDescriptor
- Add proper mutex protection for thread-safe cache access
- Update all test data to use proper field cardinality labels
- Update test expectations to handle successful descriptor resolution
- Enable full protobuf decoder creation from binary descriptors

Phase E (Protobuf Support) is now complete:
✅ E1: Binary descriptor parsing
✅ E2: Decoder integration
✅ E3: Full message descriptor resolution

Protobuf messages can now be fully parsed and decoded

Phase F: Implement Kafka record batch compression support

- Add comprehensive compression module supporting gzip/snappy/lz4/zstd
- Implement RecordBatchParser with full compression and CRC validation
- Support compression codec extraction from record batch attributes
- Add compression/decompression for all major Kafka codecs
- Integrate compression support into Produce and Fetch handlers
- Add extensive unit tests for all compression codecs
- Support round-trip compression/decompression with proper error handling
- Add performance benchmarks for compression operations

Key features:
✅ Gzip compression (ratio: 0.02)
✅ Snappy compression (ratio: 0.06, fastest)
✅ LZ4 compression (ratio: 0.02)
✅ Zstd compression (ratio: 0.01, best compression)
✅ CRC32 validation for record batch integrity
✅ Proper Kafka record batch format v2 parsing
✅ Backward compatibility with uncompressed records

Phase F (Compression Handling) is now complete.

Phase G: Implement advanced schema compatibility checking and migration

- Add comprehensive SchemaEvolutionChecker with full compatibility rules
- Support BACKWARD, FORWARD, FULL, and NONE compatibility levels
- Implement Avro schema compatibility checking with field analysis
- Add JSON Schema compatibility validation
- Support Protobuf compatibility checking (simplified implementation)
- Add type promotion rules (int->long, float->double, string<->bytes)
- Integrate schema evolution into Manager with validation methods
- Add schema evolution suggestions and migration guidance
- Support schema compatibility validation before evolution
- Add comprehensive unit tests for all compatibility scenarios

Key features:
✅ BACKWARD compatibility: New schema can read old data
✅ FORWARD compatibility: Old schema can read new data
✅ FULL compatibility: Both backward and forward compatible
✅ Type promotion support for safe schema evolution
✅ Field addition/removal validation with default value checks
✅ Schema evolution suggestions for incompatible changes
✅ Integration with schema registry for validation workflows

Phase G (Schema Evolution) is now complete.

fmt
2025-09-11 19:53:00 -07:00

183 lines
4.8 KiB
Go

package offset
import (
"fmt"
"sort"
"sync"
"time"
)
// OffsetEntry represents a single offset mapping
type OffsetEntry struct {
KafkaOffset int64 // Kafka offset (sequential integer)
Timestamp int64 // SeaweedMQ timestamp (nanoseconds)
Size int32 // Message size in bytes
}
// Ledger maintains the mapping between Kafka offsets and SeaweedMQ timestamps
// for a single topic partition
type Ledger struct {
mu sync.RWMutex
entries []OffsetEntry // sorted by KafkaOffset
nextOffset int64 // next offset to assign
earliestTime int64 // timestamp of earliest message
latestTime int64 // timestamp of latest message
}
// NewLedger creates a new offset ledger starting from offset 0
func NewLedger() *Ledger {
return &Ledger{
entries: make([]OffsetEntry, 0, 1000), // pre-allocate for performance
nextOffset: 0,
}
}
// AssignOffsets reserves a range of consecutive Kafka offsets
// Returns the base offset of the reserved range
func (l *Ledger) AssignOffsets(count int64) int64 {
l.mu.Lock()
defer l.mu.Unlock()
baseOffset := l.nextOffset
l.nextOffset += count
return baseOffset
}
// AppendRecord adds a new offset entry to the ledger
// The kafkaOffset should be from a previous AssignOffsets call
func (l *Ledger) AppendRecord(kafkaOffset, timestamp int64, size int32) error {
l.mu.Lock()
defer l.mu.Unlock()
// Validate offset is in expected range
if kafkaOffset < 0 || kafkaOffset >= l.nextOffset {
return fmt.Errorf("invalid offset %d, expected 0 <= offset < %d", kafkaOffset, l.nextOffset)
}
// Check for duplicate offset (shouldn't happen in normal operation)
if len(l.entries) > 0 && l.entries[len(l.entries)-1].KafkaOffset >= kafkaOffset {
return fmt.Errorf("offset %d already exists or is out of order", kafkaOffset)
}
entry := OffsetEntry{
KafkaOffset: kafkaOffset,
Timestamp: timestamp,
Size: size,
}
l.entries = append(l.entries, entry)
// Update earliest/latest timestamps
if l.earliestTime == 0 || timestamp < l.earliestTime {
l.earliestTime = timestamp
}
if timestamp > l.latestTime {
l.latestTime = timestamp
}
return nil
}
// GetRecord retrieves the record information for a given Kafka offset
func (l *Ledger) GetRecord(kafkaOffset int64) (timestamp int64, size int32, err error) {
l.mu.RLock()
defer l.mu.RUnlock()
// Binary search for the offset
idx := sort.Search(len(l.entries), func(i int) bool {
return l.entries[i].KafkaOffset >= kafkaOffset
})
if idx >= len(l.entries) || l.entries[idx].KafkaOffset != kafkaOffset {
return 0, 0, fmt.Errorf("offset %d not found", kafkaOffset)
}
entry := l.entries[idx]
return entry.Timestamp, entry.Size, nil
}
// GetEarliestOffset returns the smallest Kafka offset in the ledger
func (l *Ledger) GetEarliestOffset() int64 {
l.mu.RLock()
defer l.mu.RUnlock()
if len(l.entries) == 0 {
return 0 // no messages yet, earliest is 0
}
return l.entries[0].KafkaOffset
}
// GetLatestOffset returns the largest Kafka offset in the ledger
func (l *Ledger) GetLatestOffset() int64 {
l.mu.RLock()
defer l.mu.RUnlock()
if len(l.entries) == 0 {
return 0 // no messages yet, latest is 0
}
return l.entries[len(l.entries)-1].KafkaOffset
}
// GetHighWaterMark returns the next offset that will be assigned
// (i.e., one past the latest offset)
func (l *Ledger) GetHighWaterMark() int64 {
l.mu.RLock()
defer l.mu.RUnlock()
return l.nextOffset
}
// FindOffsetByTimestamp returns the first offset with a timestamp >= target
// Used for timestamp-based offset lookup
func (l *Ledger) FindOffsetByTimestamp(targetTimestamp int64) int64 {
l.mu.RLock()
defer l.mu.RUnlock()
if len(l.entries) == 0 {
return 0
}
// Binary search for first entry with timestamp >= targetTimestamp
idx := sort.Search(len(l.entries), func(i int) bool {
return l.entries[i].Timestamp >= targetTimestamp
})
if idx >= len(l.entries) {
// Target timestamp is after all entries, return high water mark
return l.nextOffset
}
return l.entries[idx].KafkaOffset
}
// GetStats returns basic statistics about the ledger
func (l *Ledger) GetStats() (entryCount int, earliestTime, latestTime, nextOffset int64) {
l.mu.RLock()
defer l.mu.RUnlock()
return len(l.entries), l.earliestTime, l.latestTime, l.nextOffset
}
// GetTimestampRange returns the time range covered by this ledger
func (l *Ledger) GetTimestampRange() (earliest, latest int64) {
l.mu.RLock()
defer l.mu.RUnlock()
if len(l.entries) == 0 {
now := time.Now().UnixNano()
return now, now // stub values when no data
}
return l.earliestTime, l.latestTime
}
// GetEntries returns a copy of all offset entries in the ledger
func (l *Ledger) GetEntries() []OffsetEntry {
l.mu.RLock()
defer l.mu.RUnlock()
// Return a copy to prevent external modification
entries := make([]OffsetEntry, len(l.entries))
copy(entries, l.entries)
return entries
}