mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-16 11:08:39 +02:00
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
183 lines
4.8 KiB
Go
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
|
|
}
|