You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							199 lines
						
					
					
						
							5.9 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							199 lines
						
					
					
						
							5.9 KiB
						
					
					
				| package broker | |
| 
 | |
| import ( | |
| 	"fmt" | |
| 	"sync" | |
| 	"time" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/mq/offset" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb" | |
| ) | |
| 
 | |
| // recordEntry holds a record with timestamp for TTL cleanup | |
| type recordEntry struct { | |
| 	exists    bool | |
| 	timestamp time.Time | |
| } | |
| 
 | |
| // InMemoryOffsetStorage provides an in-memory implementation of OffsetStorage for testing ONLY | |
| // This is a copy of the implementation in weed/mq/offset/memory_storage_test.go | |
| type InMemoryOffsetStorage struct { | |
| 	mu          sync.RWMutex | |
| 	checkpoints map[string]int64                  // partition key -> offset | |
| 	records     map[string]map[int64]*recordEntry // partition key -> offset -> entry with timestamp | |
|  | |
| 	// Memory leak protection | |
| 	maxRecordsPerPartition int           // Maximum records to keep per partition | |
| 	recordTTL              time.Duration // TTL for record entries | |
| 	lastCleanup            time.Time     // Last cleanup time | |
| 	cleanupInterval        time.Duration // How often to run cleanup | |
| } | |
| 
 | |
| // NewInMemoryOffsetStorage creates a new in-memory storage with memory leak protection | |
| // FOR TESTING ONLY - do not use in production | |
| func NewInMemoryOffsetStorage() *InMemoryOffsetStorage { | |
| 	return &InMemoryOffsetStorage{ | |
| 		checkpoints:            make(map[string]int64), | |
| 		records:                make(map[string]map[int64]*recordEntry), | |
| 		maxRecordsPerPartition: 10000,           // Limit to 10K records per partition | |
| 		recordTTL:              1 * time.Hour,   // Records expire after 1 hour | |
| 		cleanupInterval:        5 * time.Minute, // Cleanup every 5 minutes | |
| 		lastCleanup:            time.Now(), | |
| 	} | |
| } | |
| 
 | |
| // SaveCheckpoint saves the checkpoint for a partition | |
| func (s *InMemoryOffsetStorage) SaveCheckpoint(namespace, topicName string, partition *schema_pb.Partition, off int64) error { | |
| 	s.mu.Lock() | |
| 	defer s.mu.Unlock() | |
| 
 | |
| 	key := offset.PartitionKey(partition) | |
| 	s.checkpoints[key] = off | |
| 	return nil | |
| } | |
| 
 | |
| // LoadCheckpoint loads the checkpoint for a partition | |
| func (s *InMemoryOffsetStorage) LoadCheckpoint(namespace, topicName string, partition *schema_pb.Partition) (int64, error) { | |
| 	s.mu.RLock() | |
| 	defer s.mu.RUnlock() | |
| 
 | |
| 	key := offset.PartitionKey(partition) | |
| 	off, exists := s.checkpoints[key] | |
| 	if !exists { | |
| 		return -1, fmt.Errorf("no checkpoint found") | |
| 	} | |
| 
 | |
| 	return off, nil | |
| } | |
| 
 | |
| // GetHighestOffset finds the highest offset in storage for a partition | |
| func (s *InMemoryOffsetStorage) GetHighestOffset(namespace, topicName string, partition *schema_pb.Partition) (int64, error) { | |
| 	s.mu.RLock() | |
| 	defer s.mu.RUnlock() | |
| 
 | |
| 	key := offset.PartitionKey(partition) | |
| 	offsets, exists := s.records[key] | |
| 	if !exists || len(offsets) == 0 { | |
| 		return -1, fmt.Errorf("no records found") | |
| 	} | |
| 
 | |
| 	var highest int64 = -1 | |
| 	for off, entry := range offsets { | |
| 		if entry.exists && off > highest { | |
| 			highest = off | |
| 		} | |
| 	} | |
| 
 | |
| 	return highest, nil | |
| } | |
| 
 | |
| // AddRecord simulates storing a record with an offset (for testing) | |
| func (s *InMemoryOffsetStorage) AddRecord(partition *schema_pb.Partition, off int64) { | |
| 	s.mu.Lock() | |
| 	defer s.mu.Unlock() | |
| 
 | |
| 	key := offset.PartitionKey(partition) | |
| 	if s.records[key] == nil { | |
| 		s.records[key] = make(map[int64]*recordEntry) | |
| 	} | |
| 
 | |
| 	// Add record with current timestamp | |
| 	s.records[key][off] = &recordEntry{ | |
| 		exists:    true, | |
| 		timestamp: time.Now(), | |
| 	} | |
| 
 | |
| 	// Trigger cleanup if needed (memory leak protection) | |
| 	s.cleanupIfNeeded() | |
| } | |
| 
 | |
| // Reset removes all data (implements resettable interface for shutdown) | |
| func (s *InMemoryOffsetStorage) Reset() error { | |
| 	s.mu.Lock() | |
| 	defer s.mu.Unlock() | |
| 
 | |
| 	s.checkpoints = make(map[string]int64) | |
| 	s.records = make(map[string]map[int64]*recordEntry) | |
| 	s.lastCleanup = time.Now() | |
| 	return nil | |
| } | |
| 
 | |
| // cleanupIfNeeded performs memory leak protection cleanup | |
| // This method assumes the caller already holds the write lock | |
| func (s *InMemoryOffsetStorage) cleanupIfNeeded() { | |
| 	now := time.Now() | |
| 
 | |
| 	// Only cleanup if enough time has passed | |
| 	if now.Sub(s.lastCleanup) < s.cleanupInterval { | |
| 		return | |
| 	} | |
| 
 | |
| 	s.lastCleanup = now | |
| 	cutoff := now.Add(-s.recordTTL) | |
| 
 | |
| 	// Clean up expired records and enforce size limits | |
| 	for partitionKey, offsets := range s.records { | |
| 		// Remove expired records | |
| 		for offset, entry := range offsets { | |
| 			if entry.timestamp.Before(cutoff) { | |
| 				delete(offsets, offset) | |
| 			} | |
| 		} | |
| 
 | |
| 		// Enforce size limit per partition | |
| 		if len(offsets) > s.maxRecordsPerPartition { | |
| 			// Keep only the most recent records | |
| 			type offsetTime struct { | |
| 				offset int64 | |
| 				time   time.Time | |
| 			} | |
| 
 | |
| 			var entries []offsetTime | |
| 			for offset, entry := range offsets { | |
| 				entries = append(entries, offsetTime{offset: offset, time: entry.timestamp}) | |
| 			} | |
| 
 | |
| 			// Sort by timestamp (newest first) | |
| 			for i := 0; i < len(entries)-1; i++ { | |
| 				for j := i + 1; j < len(entries); j++ { | |
| 					if entries[i].time.Before(entries[j].time) { | |
| 						entries[i], entries[j] = entries[j], entries[i] | |
| 					} | |
| 				} | |
| 			} | |
| 
 | |
| 			// Keep only the newest maxRecordsPerPartition entries | |
| 			newOffsets := make(map[int64]*recordEntry) | |
| 			for i := 0; i < s.maxRecordsPerPartition && i < len(entries); i++ { | |
| 				offset := entries[i].offset | |
| 				newOffsets[offset] = offsets[offset] | |
| 			} | |
| 
 | |
| 			s.records[partitionKey] = newOffsets | |
| 		} | |
| 
 | |
| 		// Remove empty partition maps | |
| 		if len(offsets) == 0 { | |
| 			delete(s.records, partitionKey) | |
| 		} | |
| 	} | |
| } | |
| 
 | |
| // NewInMemoryOffsetStorageForTesting creates an InMemoryOffsetStorage for testing purposes | |
| func NewInMemoryOffsetStorageForTesting() offset.OffsetStorage { | |
| 	return NewInMemoryOffsetStorage() | |
| } | |
| 
 | |
| // NewBrokerOffsetManagerWithStorage creates a new broker offset manager with custom storage | |
| // FOR TESTING ONLY - moved from production code since it's only used in tests | |
| func NewBrokerOffsetManagerWithStorage(storage offset.OffsetStorage) *BrokerOffsetManager { | |
| 	if storage == nil { | |
| 		panic("BrokerOffsetManager requires a storage implementation. Use NewBrokerOffsetManagerWithFiler() or provide FilerOffsetStorage/SQLOffsetStorage. InMemoryOffsetStorage is only for testing.") | |
| 	} | |
| 
 | |
| 	return &BrokerOffsetManager{ | |
| 		offsetIntegration:    offset.NewSMQOffsetIntegration(storage), | |
| 		storage:              storage, | |
| 		consumerGroupStorage: nil, // Will be set separately if needed | |
| 	} | |
| }
 |