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.
		
		
		
		
		
			
		
			
				
					
					
						
							473 lines
						
					
					
						
							13 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							473 lines
						
					
					
						
							13 KiB
						
					
					
				
								package offset
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"fmt"
							 | 
						|
									"os"
							 | 
						|
									"testing"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									_ "github.com/mattn/go-sqlite3"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// TestEndToEndOffsetFlow tests the complete offset management flow
							 | 
						|
								func TestEndToEndOffsetFlow(t *testing.T) {
							 | 
						|
									// Create temporary database
							 | 
						|
									tmpFile, err := os.CreateTemp("", "e2e_offset_test_*.db")
							 | 
						|
									if err != nil {
							 | 
						|
										t.Fatalf("Failed to create temp database: %v", err)
							 | 
						|
									}
							 | 
						|
									tmpFile.Close()
							 | 
						|
									defer os.Remove(tmpFile.Name())
							 | 
						|
								
							 | 
						|
									// Create database with migrations
							 | 
						|
									db, err := CreateDatabase(tmpFile.Name())
							 | 
						|
									if err != nil {
							 | 
						|
										t.Fatalf("Failed to create database: %v", err)
							 | 
						|
									}
							 | 
						|
									defer db.Close()
							 | 
						|
								
							 | 
						|
									// Create SQL storage
							 | 
						|
									storage, err := NewSQLOffsetStorage(db)
							 | 
						|
									if err != nil {
							 | 
						|
										t.Fatalf("Failed to create SQL storage: %v", err)
							 | 
						|
									}
							 | 
						|
									defer storage.Close()
							 | 
						|
								
							 | 
						|
									// Create SMQ offset integration
							 | 
						|
									integration := NewSMQOffsetIntegration(storage)
							 | 
						|
								
							 | 
						|
									// Test partition
							 | 
						|
									partition := &schema_pb.Partition{
							 | 
						|
										RingSize:   1024,
							 | 
						|
										RangeStart: 0,
							 | 
						|
										RangeStop:  31,
							 | 
						|
										UnixTimeNs: time.Now().UnixNano(),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									t.Run("PublishAndAssignOffsets", func(t *testing.T) {
							 | 
						|
										// Simulate publishing messages with offset assignment
							 | 
						|
										records := []PublishRecordRequest{
							 | 
						|
											{Key: []byte("user1"), Value: &schema_pb.RecordValue{}},
							 | 
						|
											{Key: []byte("user2"), Value: &schema_pb.RecordValue{}},
							 | 
						|
											{Key: []byte("user3"), Value: &schema_pb.RecordValue{}},
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										response, err := integration.PublishRecordBatch("test-namespace", "test-topic", partition, records)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to publish record batch: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if response.BaseOffset != 0 {
							 | 
						|
											t.Errorf("Expected base offset 0, got %d", response.BaseOffset)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if response.LastOffset != 2 {
							 | 
						|
											t.Errorf("Expected last offset 2, got %d", response.LastOffset)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Verify high water mark
							 | 
						|
										hwm, err := integration.GetHighWaterMark("test-namespace", "test-topic", partition)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to get high water mark: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if hwm != 3 {
							 | 
						|
											t.Errorf("Expected high water mark 3, got %d", hwm)
							 | 
						|
										}
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("CreateAndUseSubscription", func(t *testing.T) {
							 | 
						|
										// Create subscription from earliest
							 | 
						|
										sub, err := integration.CreateSubscription(
							 | 
						|
											"e2e-test-sub",
							 | 
						|
											"test-namespace", "test-topic",
							 | 
						|
											partition,
							 | 
						|
											schema_pb.OffsetType_RESET_TO_EARLIEST,
							 | 
						|
											0,
							 | 
						|
										)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to create subscription: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Subscribe to records
							 | 
						|
										responses, err := integration.SubscribeRecords(sub, 2)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to subscribe to records: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if len(responses) != 2 {
							 | 
						|
											t.Errorf("Expected 2 responses, got %d", len(responses))
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Check subscription advancement
							 | 
						|
										if sub.CurrentOffset != 2 {
							 | 
						|
											t.Errorf("Expected current offset 2, got %d", sub.CurrentOffset)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Get subscription lag
							 | 
						|
										lag, err := sub.GetLag()
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to get lag: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if lag != 1 { // 3 (hwm) - 2 (current) = 1
							 | 
						|
											t.Errorf("Expected lag 1, got %d", lag)
							 | 
						|
										}
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("OffsetSeekingAndRanges", func(t *testing.T) {
							 | 
						|
										// Create subscription at specific offset
							 | 
						|
										sub, err := integration.CreateSubscription(
							 | 
						|
											"seek-test-sub",
							 | 
						|
											"test-namespace", "test-topic",
							 | 
						|
											partition,
							 | 
						|
											schema_pb.OffsetType_EXACT_OFFSET,
							 | 
						|
											1,
							 | 
						|
										)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to create subscription at offset 1: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Verify starting position
							 | 
						|
										if sub.CurrentOffset != 1 {
							 | 
						|
											t.Errorf("Expected current offset 1, got %d", sub.CurrentOffset)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Get offset range
							 | 
						|
										offsetRange, err := sub.GetOffsetRange(2)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to get offset range: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if offsetRange.StartOffset != 1 {
							 | 
						|
											t.Errorf("Expected start offset 1, got %d", offsetRange.StartOffset)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if offsetRange.Count != 2 {
							 | 
						|
											t.Errorf("Expected count 2, got %d", offsetRange.Count)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Seek to different offset
							 | 
						|
										err = sub.SeekToOffset(0)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to seek to offset 0: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if sub.CurrentOffset != 0 {
							 | 
						|
											t.Errorf("Expected current offset 0 after seek, got %d", sub.CurrentOffset)
							 | 
						|
										}
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("PartitionInformationAndMetrics", func(t *testing.T) {
							 | 
						|
										// Get partition offset info
							 | 
						|
										info, err := integration.GetPartitionOffsetInfo("test-namespace", "test-topic", partition)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to get partition offset info: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if info.EarliestOffset != 0 {
							 | 
						|
											t.Errorf("Expected earliest offset 0, got %d", info.EarliestOffset)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if info.LatestOffset != 2 {
							 | 
						|
											t.Errorf("Expected latest offset 2, got %d", info.LatestOffset)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if info.HighWaterMark != 3 {
							 | 
						|
											t.Errorf("Expected high water mark 3, got %d", info.HighWaterMark)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if info.ActiveSubscriptions != 2 { // Two subscriptions created above
							 | 
						|
											t.Errorf("Expected 2 active subscriptions, got %d", info.ActiveSubscriptions)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Get offset metrics
							 | 
						|
										metrics := integration.GetOffsetMetrics()
							 | 
						|
										if metrics.PartitionCount != 1 {
							 | 
						|
											t.Errorf("Expected 1 partition, got %d", metrics.PartitionCount)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if metrics.ActiveSubscriptions != 2 {
							 | 
						|
											t.Errorf("Expected 2 active subscriptions in metrics, got %d", metrics.ActiveSubscriptions)
							 | 
						|
										}
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestOffsetPersistenceAcrossRestarts tests that offsets persist across system restarts
							 | 
						|
								func TestOffsetPersistenceAcrossRestarts(t *testing.T) {
							 | 
						|
									// Create temporary database
							 | 
						|
									tmpFile, err := os.CreateTemp("", "persistence_test_*.db")
							 | 
						|
									if err != nil {
							 | 
						|
										t.Fatalf("Failed to create temp database: %v", err)
							 | 
						|
									}
							 | 
						|
									tmpFile.Close()
							 | 
						|
									defer os.Remove(tmpFile.Name())
							 | 
						|
								
							 | 
						|
									partition := &schema_pb.Partition{
							 | 
						|
										RingSize:   1024,
							 | 
						|
										RangeStart: 0,
							 | 
						|
										RangeStop:  31,
							 | 
						|
										UnixTimeNs: time.Now().UnixNano(),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var lastOffset int64
							 | 
						|
								
							 | 
						|
									// First session: Create database and assign offsets
							 | 
						|
									{
							 | 
						|
										db, err := CreateDatabase(tmpFile.Name())
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to create database: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										storage, err := NewSQLOffsetStorage(db)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to create SQL storage: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										integration := NewSMQOffsetIntegration(storage)
							 | 
						|
								
							 | 
						|
										// Publish some records
							 | 
						|
										records := []PublishRecordRequest{
							 | 
						|
											{Key: []byte("msg1"), Value: &schema_pb.RecordValue{}},
							 | 
						|
											{Key: []byte("msg2"), Value: &schema_pb.RecordValue{}},
							 | 
						|
											{Key: []byte("msg3"), Value: &schema_pb.RecordValue{}},
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										response, err := integration.PublishRecordBatch("test-namespace", "test-topic", partition, records)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to publish records: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										lastOffset = response.LastOffset
							 | 
						|
								
							 | 
						|
										// Close connections - Close integration first to trigger final checkpoint
							 | 
						|
										integration.Close()
							 | 
						|
										storage.Close()
							 | 
						|
										db.Close()
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Second session: Reopen database and verify persistence
							 | 
						|
									{
							 | 
						|
										db, err := CreateDatabase(tmpFile.Name())
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to reopen database: %v", err)
							 | 
						|
										}
							 | 
						|
										defer db.Close()
							 | 
						|
								
							 | 
						|
										storage, err := NewSQLOffsetStorage(db)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to create SQL storage: %v", err)
							 | 
						|
										}
							 | 
						|
										defer storage.Close()
							 | 
						|
								
							 | 
						|
										integration := NewSMQOffsetIntegration(storage)
							 | 
						|
								
							 | 
						|
										// Verify high water mark persisted
							 | 
						|
										hwm, err := integration.GetHighWaterMark("test-namespace", "test-topic", partition)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to get high water mark after restart: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if hwm != lastOffset+1 {
							 | 
						|
											t.Errorf("Expected high water mark %d after restart, got %d", lastOffset+1, hwm)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Assign new offsets and verify continuity
							 | 
						|
										newResponse, err := integration.PublishRecord("test-namespace", "test-topic", partition, []byte("msg4"), &schema_pb.RecordValue{})
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to publish new record after restart: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										expectedNextOffset := lastOffset + 1
							 | 
						|
										if newResponse.BaseOffset != expectedNextOffset {
							 | 
						|
											t.Errorf("Expected next offset %d after restart, got %d", expectedNextOffset, newResponse.BaseOffset)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestConcurrentOffsetOperations tests concurrent offset operations
							 | 
						|
								func TestConcurrentOffsetOperations(t *testing.T) {
							 | 
						|
									// Create temporary database
							 | 
						|
									tmpFile, err := os.CreateTemp("", "concurrent_test_*.db")
							 | 
						|
									if err != nil {
							 | 
						|
										t.Fatalf("Failed to create temp database: %v", err)
							 | 
						|
									}
							 | 
						|
									tmpFile.Close()
							 | 
						|
									defer os.Remove(tmpFile.Name())
							 | 
						|
								
							 | 
						|
									db, err := CreateDatabase(tmpFile.Name())
							 | 
						|
									if err != nil {
							 | 
						|
										t.Fatalf("Failed to create database: %v", err)
							 | 
						|
									}
							 | 
						|
									defer db.Close()
							 | 
						|
								
							 | 
						|
									storage, err := NewSQLOffsetStorage(db)
							 | 
						|
									if err != nil {
							 | 
						|
										t.Fatalf("Failed to create SQL storage: %v", err)
							 | 
						|
									}
							 | 
						|
									defer storage.Close()
							 | 
						|
								
							 | 
						|
									integration := NewSMQOffsetIntegration(storage)
							 | 
						|
								
							 | 
						|
									partition := &schema_pb.Partition{
							 | 
						|
										RingSize:   1024,
							 | 
						|
										RangeStart: 0,
							 | 
						|
										RangeStop:  31,
							 | 
						|
										UnixTimeNs: time.Now().UnixNano(),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Concurrent publishers
							 | 
						|
									const numPublishers = 5
							 | 
						|
									const recordsPerPublisher = 10
							 | 
						|
								
							 | 
						|
									done := make(chan bool, numPublishers)
							 | 
						|
								
							 | 
						|
									for i := 0; i < numPublishers; i++ {
							 | 
						|
										go func(publisherID int) {
							 | 
						|
											defer func() { done <- true }()
							 | 
						|
								
							 | 
						|
											for j := 0; j < recordsPerPublisher; j++ {
							 | 
						|
												key := fmt.Sprintf("publisher-%d-msg-%d", publisherID, j)
							 | 
						|
												_, err := integration.PublishRecord("test-namespace", "test-topic", partition, []byte(key), &schema_pb.RecordValue{})
							 | 
						|
												if err != nil {
							 | 
						|
													t.Errorf("Publisher %d failed to publish message %d: %v", publisherID, j, err)
							 | 
						|
													return
							 | 
						|
												}
							 | 
						|
											}
							 | 
						|
										}(i)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Wait for all publishers to complete
							 | 
						|
									for i := 0; i < numPublishers; i++ {
							 | 
						|
										<-done
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Verify total records
							 | 
						|
									hwm, err := integration.GetHighWaterMark("test-namespace", "test-topic", partition)
							 | 
						|
									if err != nil {
							 | 
						|
										t.Fatalf("Failed to get high water mark: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									expectedTotal := int64(numPublishers * recordsPerPublisher)
							 | 
						|
									if hwm != expectedTotal {
							 | 
						|
										t.Errorf("Expected high water mark %d, got %d", expectedTotal, hwm)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Verify no duplicate offsets
							 | 
						|
									info, err := integration.GetPartitionOffsetInfo("test-namespace", "test-topic", partition)
							 | 
						|
									if err != nil {
							 | 
						|
										t.Fatalf("Failed to get partition info: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if info.RecordCount != expectedTotal {
							 | 
						|
										t.Errorf("Expected record count %d, got %d", expectedTotal, info.RecordCount)
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestOffsetValidationAndErrorHandling tests error conditions and validation
							 | 
						|
								func TestOffsetValidationAndErrorHandling(t *testing.T) {
							 | 
						|
									// Create temporary database
							 | 
						|
									tmpFile, err := os.CreateTemp("", "validation_test_*.db")
							 | 
						|
									if err != nil {
							 | 
						|
										t.Fatalf("Failed to create temp database: %v", err)
							 | 
						|
									}
							 | 
						|
									tmpFile.Close()
							 | 
						|
									defer os.Remove(tmpFile.Name())
							 | 
						|
								
							 | 
						|
									db, err := CreateDatabase(tmpFile.Name())
							 | 
						|
									if err != nil {
							 | 
						|
										t.Fatalf("Failed to create database: %v", err)
							 | 
						|
									}
							 | 
						|
									defer db.Close()
							 | 
						|
								
							 | 
						|
									storage, err := NewSQLOffsetStorage(db)
							 | 
						|
									if err != nil {
							 | 
						|
										t.Fatalf("Failed to create SQL storage: %v", err)
							 | 
						|
									}
							 | 
						|
									defer storage.Close()
							 | 
						|
								
							 | 
						|
									integration := NewSMQOffsetIntegration(storage)
							 | 
						|
								
							 | 
						|
									partition := &schema_pb.Partition{
							 | 
						|
										RingSize:   1024,
							 | 
						|
										RangeStart: 0,
							 | 
						|
										RangeStop:  31,
							 | 
						|
										UnixTimeNs: time.Now().UnixNano(),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									t.Run("InvalidOffsetSubscription", func(t *testing.T) {
							 | 
						|
										// Try to create subscription with invalid offset
							 | 
						|
										_, err := integration.CreateSubscription(
							 | 
						|
											"invalid-sub",
							 | 
						|
											"test-namespace", "test-topic",
							 | 
						|
											partition,
							 | 
						|
											schema_pb.OffsetType_EXACT_OFFSET,
							 | 
						|
											100, // Beyond any existing data
							 | 
						|
										)
							 | 
						|
										if err == nil {
							 | 
						|
											t.Error("Expected error for subscription beyond high water mark")
							 | 
						|
										}
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("NegativeOffsetValidation", func(t *testing.T) {
							 | 
						|
										// Try to create subscription with negative offset
							 | 
						|
										_, err := integration.CreateSubscription(
							 | 
						|
											"negative-sub",
							 | 
						|
											"test-namespace", "test-topic",
							 | 
						|
											partition,
							 | 
						|
											schema_pb.OffsetType_EXACT_OFFSET,
							 | 
						|
											-1,
							 | 
						|
										)
							 | 
						|
										if err == nil {
							 | 
						|
											t.Error("Expected error for negative offset")
							 | 
						|
										}
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("DuplicateSubscriptionID", func(t *testing.T) {
							 | 
						|
										// Create first subscription
							 | 
						|
										_, err := integration.CreateSubscription(
							 | 
						|
											"duplicate-id",
							 | 
						|
											"test-namespace", "test-topic",
							 | 
						|
											partition,
							 | 
						|
											schema_pb.OffsetType_RESET_TO_EARLIEST,
							 | 
						|
											0,
							 | 
						|
										)
							 | 
						|
										if err != nil {
							 | 
						|
											t.Fatalf("Failed to create first subscription: %v", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Try to create duplicate
							 | 
						|
										_, err = integration.CreateSubscription(
							 | 
						|
											"duplicate-id",
							 | 
						|
											"test-namespace", "test-topic",
							 | 
						|
											partition,
							 | 
						|
											schema_pb.OffsetType_RESET_TO_EARLIEST,
							 | 
						|
											0,
							 | 
						|
										)
							 | 
						|
										if err == nil {
							 | 
						|
											t.Error("Expected error for duplicate subscription ID")
							 | 
						|
										}
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("OffsetRangeValidation", func(t *testing.T) {
							 | 
						|
										// Add some data first
							 | 
						|
										integration.PublishRecord("test-namespace", "test-topic", partition, []byte("test"), &schema_pb.RecordValue{})
							 | 
						|
								
							 | 
						|
										// Test invalid range validation
							 | 
						|
										err := integration.ValidateOffsetRange("test-namespace", "test-topic", partition, 5, 10) // Beyond high water mark
							 | 
						|
										if err == nil {
							 | 
						|
											t.Error("Expected error for range beyond high water mark")
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										err = integration.ValidateOffsetRange("test-namespace", "test-topic", partition, 10, 5) // End before start
							 | 
						|
										if err == nil {
							 | 
						|
											t.Error("Expected error for end offset before start offset")
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										err = integration.ValidateOffsetRange("test-namespace", "test-topic", partition, -1, 5) // Negative start
							 | 
						|
										if err == nil {
							 | 
						|
											t.Error("Expected error for negative start offset")
							 | 
						|
										}
							 | 
						|
									})
							 | 
						|
								}
							 |