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.
		
		
		
		
		
			
		
			
				
					
					
						
							228 lines
						
					
					
						
							6.6 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							228 lines
						
					
					
						
							6.6 KiB
						
					
					
				
								package offset
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"fmt"
							 | 
						|
									"sync"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"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
							 | 
						|
								// WARNING: This should NEVER be used in production - use FilerOffsetStorage or SQLOffsetStorage instead
							 | 
						|
								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, offset int64) error {
							 | 
						|
									s.mu.Lock()
							 | 
						|
									defer s.mu.Unlock()
							 | 
						|
								
							 | 
						|
									// Use TopicPartitionKey for consistency with other storage implementations
							 | 
						|
									key := TopicPartitionKey(namespace, topicName, partition)
							 | 
						|
									s.checkpoints[key] = offset
							 | 
						|
									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()
							 | 
						|
								
							 | 
						|
									// Use TopicPartitionKey to match SaveCheckpoint
							 | 
						|
									key := TopicPartitionKey(namespace, topicName, partition)
							 | 
						|
									offset, exists := s.checkpoints[key]
							 | 
						|
									if !exists {
							 | 
						|
										return -1, fmt.Errorf("no checkpoint found")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return offset, 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()
							 | 
						|
								
							 | 
						|
									// Use TopicPartitionKey to match SaveCheckpoint
							 | 
						|
									key := TopicPartitionKey(namespace, topicName, partition)
							 | 
						|
									offsets, exists := s.records[key]
							 | 
						|
									if !exists || len(offsets) == 0 {
							 | 
						|
										return -1, fmt.Errorf("no records found")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var highest int64 = -1
							 | 
						|
									for offset, entry := range offsets {
							 | 
						|
										if entry.exists && offset > highest {
							 | 
						|
											highest = offset
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return highest, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// AddRecord simulates storing a record with an offset (for testing)
							 | 
						|
								func (s *InMemoryOffsetStorage) AddRecord(namespace, topicName string, partition *schema_pb.Partition, offset int64) {
							 | 
						|
									s.mu.Lock()
							 | 
						|
									defer s.mu.Unlock()
							 | 
						|
								
							 | 
						|
									// Use TopicPartitionKey to match GetHighestOffset
							 | 
						|
									key := TopicPartitionKey(namespace, topicName, partition)
							 | 
						|
									if s.records[key] == nil {
							 | 
						|
										s.records[key] = make(map[int64]*recordEntry)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Add record with current timestamp
							 | 
						|
									s.records[key][offset] = &recordEntry{
							 | 
						|
										exists:    true,
							 | 
						|
										timestamp: time.Now(),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Trigger cleanup if needed (memory leak protection)
							 | 
						|
									s.cleanupIfNeeded()
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GetRecordCount returns the number of records for a partition (for testing)
							 | 
						|
								func (s *InMemoryOffsetStorage) GetRecordCount(namespace, topicName string, partition *schema_pb.Partition) int {
							 | 
						|
									s.mu.RLock()
							 | 
						|
									defer s.mu.RUnlock()
							 | 
						|
								
							 | 
						|
									// Use TopicPartitionKey to match GetHighestOffset
							 | 
						|
									key := TopicPartitionKey(namespace, topicName, partition)
							 | 
						|
									if offsets, exists := s.records[key]; exists {
							 | 
						|
										count := 0
							 | 
						|
										for _, entry := range offsets {
							 | 
						|
											if entry.exists {
							 | 
						|
												count++
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
										return count
							 | 
						|
									}
							 | 
						|
									return 0
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Clear removes all data (for testing)
							 | 
						|
								func (s *InMemoryOffsetStorage) Clear() {
							 | 
						|
									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()
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Reset removes all data (implements resettable interface for shutdown)
							 | 
						|
								func (s *InMemoryOffsetStorage) Reset() error {
							 | 
						|
									s.Clear()
							 | 
						|
									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)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GetMemoryStats returns memory usage statistics for monitoring
							 | 
						|
								func (s *InMemoryOffsetStorage) GetMemoryStats() map[string]interface{} {
							 | 
						|
									s.mu.RLock()
							 | 
						|
									defer s.mu.RUnlock()
							 | 
						|
								
							 | 
						|
									totalRecords := 0
							 | 
						|
									partitionCount := len(s.records)
							 | 
						|
								
							 | 
						|
									for _, offsets := range s.records {
							 | 
						|
										totalRecords += len(offsets)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return map[string]interface{}{
							 | 
						|
										"total_partitions":          partitionCount,
							 | 
						|
										"total_records":             totalRecords,
							 | 
						|
										"max_records_per_partition": s.maxRecordsPerPartition,
							 | 
						|
										"record_ttl_hours":          s.recordTTL.Hours(),
							 | 
						|
										"last_cleanup":              s.lastCleanup,
							 | 
						|
									}
							 | 
						|
								}
							 |