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.
		
		
		
		
		
			
		
			
				
					
					
						
							308 lines
						
					
					
						
							9.0 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							308 lines
						
					
					
						
							9.0 KiB
						
					
					
				
								package filer
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"container/heap"
							 | 
						|
									"testing"
							 | 
						|
									"time"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								func TestDeletionRetryQueue_AddAndRetrieve(t *testing.T) {
							 | 
						|
									queue := NewDeletionRetryQueue()
							 | 
						|
								
							 | 
						|
									// Add items
							 | 
						|
									queue.AddOrUpdate("file1", "is read only")
							 | 
						|
									queue.AddOrUpdate("file2", "connection reset")
							 | 
						|
								
							 | 
						|
									if queue.Size() != 2 {
							 | 
						|
										t.Errorf("Expected queue size 2, got %d", queue.Size())
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Items not ready yet (initial delay is 5 minutes)
							 | 
						|
									readyItems := queue.GetReadyItems(10)
							 | 
						|
									if len(readyItems) != 0 {
							 | 
						|
										t.Errorf("Expected 0 ready items, got %d", len(readyItems))
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Size should remain unchanged
							 | 
						|
									if queue.Size() != 2 {
							 | 
						|
										t.Errorf("Expected queue size 2 after checking ready items, got %d", queue.Size())
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func TestDeletionRetryQueue_ExponentialBackoff(t *testing.T) {
							 | 
						|
									queue := NewDeletionRetryQueue()
							 | 
						|
								
							 | 
						|
									// Create an item
							 | 
						|
									item := &DeletionRetryItem{
							 | 
						|
										FileId:      "test-file",
							 | 
						|
										RetryCount:  0,
							 | 
						|
										NextRetryAt: time.Now(),
							 | 
						|
										LastError:   "test error",
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Requeue multiple times to test backoff
							 | 
						|
									delays := []time.Duration{}
							 | 
						|
								
							 | 
						|
									for i := 0; i < 5; i++ {
							 | 
						|
										beforeTime := time.Now()
							 | 
						|
										queue.RequeueForRetry(item, "error")
							 | 
						|
								
							 | 
						|
										// Calculate expected delay for this retry count
							 | 
						|
										expectedDelay := InitialRetryDelay * time.Duration(1<<uint(i))
							 | 
						|
										if expectedDelay > MaxRetryDelay {
							 | 
						|
											expectedDelay = MaxRetryDelay
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Verify NextRetryAt is approximately correct
							 | 
						|
										actualDelay := item.NextRetryAt.Sub(beforeTime)
							 | 
						|
										delays = append(delays, actualDelay)
							 | 
						|
								
							 | 
						|
										// Allow small timing variance
							 | 
						|
										timeDiff := actualDelay - expectedDelay
							 | 
						|
										if timeDiff < 0 {
							 | 
						|
											timeDiff = -timeDiff
							 | 
						|
										}
							 | 
						|
										if timeDiff > 100*time.Millisecond {
							 | 
						|
											t.Errorf("Retry %d: expected delay ~%v, got %v (diff: %v)", i+1, expectedDelay, actualDelay, timeDiff)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Verify retry count incremented
							 | 
						|
										if item.RetryCount != i+1 {
							 | 
						|
											t.Errorf("Expected RetryCount %d, got %d", i+1, item.RetryCount)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Reset the heap for the next isolated test iteration
							 | 
						|
										queue.lock.Lock()
							 | 
						|
										queue.heap = retryHeap{}
							 | 
						|
										queue.lock.Unlock()
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									t.Logf("Exponential backoff delays: %v", delays)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func TestDeletionRetryQueue_OverflowProtection(t *testing.T) {
							 | 
						|
									queue := NewDeletionRetryQueue()
							 | 
						|
								
							 | 
						|
									// Create an item with very high retry count
							 | 
						|
									item := &DeletionRetryItem{
							 | 
						|
										FileId:      "test-file",
							 | 
						|
										RetryCount:  60, // High count that would cause overflow without protection
							 | 
						|
										NextRetryAt: time.Now(),
							 | 
						|
										LastError:   "test error",
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Should not panic and should cap at MaxRetryDelay
							 | 
						|
									queue.RequeueForRetry(item, "error")
							 | 
						|
								
							 | 
						|
									delay := time.Until(item.NextRetryAt)
							 | 
						|
									if delay > MaxRetryDelay+time.Second {
							 | 
						|
										t.Errorf("Delay exceeded MaxRetryDelay: %v > %v", delay, MaxRetryDelay)
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func TestDeletionRetryQueue_MaxAttemptsReached(t *testing.T) {
							 | 
						|
									queue := NewDeletionRetryQueue()
							 | 
						|
								
							 | 
						|
									// Add item
							 | 
						|
									queue.AddOrUpdate("file1", "error")
							 | 
						|
								
							 | 
						|
									// Manually set retry count to max
							 | 
						|
									queue.lock.Lock()
							 | 
						|
									item, exists := queue.itemIndex["file1"]
							 | 
						|
									if !exists {
							 | 
						|
										queue.lock.Unlock()
							 | 
						|
										t.Fatal("Item not found in queue")
							 | 
						|
									}
							 | 
						|
									item.RetryCount = MaxRetryAttempts
							 | 
						|
									item.NextRetryAt = time.Now().Add(-1 * time.Second) // Ready now
							 | 
						|
									heap.Fix(&queue.heap, item.heapIndex)
							 | 
						|
									queue.lock.Unlock()
							 | 
						|
								
							 | 
						|
									// Try to get ready items - should be returned for the last retry (attempt #10)
							 | 
						|
									readyItems := queue.GetReadyItems(10)
							 | 
						|
									if len(readyItems) != 1 {
							 | 
						|
										t.Fatalf("Expected 1 item for last retry, got %d", len(readyItems))
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Requeue it, which will increment its retry count beyond the max
							 | 
						|
									queue.RequeueForRetry(readyItems[0], "final error")
							 | 
						|
								
							 | 
						|
									// Manually make it ready again
							 | 
						|
									queue.lock.Lock()
							 | 
						|
									item, exists = queue.itemIndex["file1"]
							 | 
						|
									if !exists {
							 | 
						|
										queue.lock.Unlock()
							 | 
						|
										t.Fatal("Item not found in queue after requeue")
							 | 
						|
									}
							 | 
						|
									item.NextRetryAt = time.Now().Add(-1 * time.Second)
							 | 
						|
									heap.Fix(&queue.heap, item.heapIndex)
							 | 
						|
									queue.lock.Unlock()
							 | 
						|
								
							 | 
						|
									// Now it should be discarded (retry count is 11, exceeds max of 10)
							 | 
						|
									readyItems = queue.GetReadyItems(10)
							 | 
						|
									if len(readyItems) != 0 {
							 | 
						|
										t.Errorf("Expected 0 items (max attempts exceeded), got %d", len(readyItems))
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Should be removed from queue
							 | 
						|
									if queue.Size() != 0 {
							 | 
						|
										t.Errorf("Expected queue size 0 after max attempts exceeded, got %d", queue.Size())
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func TestCalculateBackoff(t *testing.T) {
							 | 
						|
									testCases := []struct {
							 | 
						|
										retryCount    int
							 | 
						|
										expectedDelay time.Duration
							 | 
						|
										description   string
							 | 
						|
									}{
							 | 
						|
										{1, InitialRetryDelay, "first retry"},
							 | 
						|
										{2, InitialRetryDelay * 2, "second retry"},
							 | 
						|
										{3, InitialRetryDelay * 4, "third retry"},
							 | 
						|
										{4, InitialRetryDelay * 8, "fourth retry"},
							 | 
						|
										{5, InitialRetryDelay * 16, "fifth retry"},
							 | 
						|
										{10, MaxRetryDelay, "capped at max delay"},
							 | 
						|
										{65, MaxRetryDelay, "overflow protection (shift > 63)"},
							 | 
						|
										{100, MaxRetryDelay, "very high retry count"},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, tc := range testCases {
							 | 
						|
										result := calculateBackoff(tc.retryCount)
							 | 
						|
										if result != tc.expectedDelay {
							 | 
						|
											t.Errorf("%s (retry %d): expected %v, got %v",
							 | 
						|
												tc.description, tc.retryCount, tc.expectedDelay, result)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func TestIsRetryableError(t *testing.T) {
							 | 
						|
									testCases := []struct {
							 | 
						|
										error       string
							 | 
						|
										retryable   bool
							 | 
						|
										description string
							 | 
						|
									}{
							 | 
						|
										{"volume 123 is read only", true, "read-only volume"},
							 | 
						|
										{"connection reset by peer", true, "connection reset"},
							 | 
						|
										{"timeout exceeded", true, "timeout"},
							 | 
						|
										{"deadline exceeded", true, "deadline exceeded"},
							 | 
						|
										{"context canceled", true, "context canceled"},
							 | 
						|
										{"lookup error: volume not found", true, "lookup error"},
							 | 
						|
										{"connection refused", true, "connection refused"},
							 | 
						|
										{"too many requests", true, "rate limiting"},
							 | 
						|
										{"service unavailable", true, "service unavailable"},
							 | 
						|
										{"i/o timeout", true, "I/O timeout"},
							 | 
						|
										{"broken pipe", true, "broken pipe"},
							 | 
						|
										{"not found", false, "not found (not retryable)"},
							 | 
						|
										{"invalid file id", false, "invalid input (not retryable)"},
							 | 
						|
										{"", false, "empty error"},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, tc := range testCases {
							 | 
						|
										result := isRetryableError(tc.error)
							 | 
						|
										if result != tc.retryable {
							 | 
						|
											t.Errorf("%s: expected retryable=%v, got %v for error: %q",
							 | 
						|
												tc.description, tc.retryable, result, tc.error)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func TestDeletionRetryQueue_HeapOrdering(t *testing.T) {
							 | 
						|
									queue := NewDeletionRetryQueue()
							 | 
						|
								
							 | 
						|
									now := time.Now()
							 | 
						|
								
							 | 
						|
									// Add items with different retry times (out of order)
							 | 
						|
									items := []*DeletionRetryItem{
							 | 
						|
										{FileId: "file3", RetryCount: 1, NextRetryAt: now.Add(30 * time.Second), LastError: "error3"},
							 | 
						|
										{FileId: "file1", RetryCount: 1, NextRetryAt: now.Add(10 * time.Second), LastError: "error1"},
							 | 
						|
										{FileId: "file2", RetryCount: 1, NextRetryAt: now.Add(20 * time.Second), LastError: "error2"},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Add items directly (simulating internal state)
							 | 
						|
									for _, item := range items {
							 | 
						|
										queue.lock.Lock()
							 | 
						|
										queue.itemIndex[item.FileId] = item
							 | 
						|
										queue.heap = append(queue.heap, item)
							 | 
						|
										queue.lock.Unlock()
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Use container/heap.Init to establish heap property
							 | 
						|
									queue.lock.Lock()
							 | 
						|
									heap.Init(&queue.heap)
							 | 
						|
									queue.lock.Unlock()
							 | 
						|
								
							 | 
						|
									// Verify heap maintains min-heap property (earliest time at top)
							 | 
						|
									queue.lock.Lock()
							 | 
						|
									if queue.heap[0].FileId != "file1" {
							 | 
						|
										t.Errorf("Expected file1 at heap top (earliest time), got %s", queue.heap[0].FileId)
							 | 
						|
									}
							 | 
						|
									queue.lock.Unlock()
							 | 
						|
								
							 | 
						|
									// Set all items to ready while preserving their relative order
							 | 
						|
									queue.lock.Lock()
							 | 
						|
									for _, item := range queue.itemIndex {
							 | 
						|
										// Shift all times back by 40 seconds to make them ready, but preserve order
							 | 
						|
										item.NextRetryAt = item.NextRetryAt.Add(-40 * time.Second)
							 | 
						|
									}
							 | 
						|
									heap.Init(&queue.heap) // Re-establish heap property after modification
							 | 
						|
									queue.lock.Unlock()
							 | 
						|
								
							 | 
						|
									// GetReadyItems should return in NextRetryAt order
							 | 
						|
									readyItems := queue.GetReadyItems(10)
							 | 
						|
									expectedOrder := []string{"file1", "file2", "file3"}
							 | 
						|
								
							 | 
						|
									if len(readyItems) != 3 {
							 | 
						|
										t.Fatalf("Expected 3 ready items, got %d", len(readyItems))
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for i, item := range readyItems {
							 | 
						|
										if item.FileId != expectedOrder[i] {
							 | 
						|
											t.Errorf("Item %d: expected %s, got %s", i, expectedOrder[i], item.FileId)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func TestDeletionRetryQueue_DuplicateFileIds(t *testing.T) {
							 | 
						|
									queue := NewDeletionRetryQueue()
							 | 
						|
								
							 | 
						|
									// Add same file ID twice with retryable error - simulates duplicate in batch
							 | 
						|
									queue.AddOrUpdate("file1", "timeout error")
							 | 
						|
								
							 | 
						|
									// Verify only one item exists in queue
							 | 
						|
									if queue.Size() != 1 {
							 | 
						|
										t.Fatalf("Expected queue size 1 after first add, got %d", queue.Size())
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Get initial retry count
							 | 
						|
									queue.lock.Lock()
							 | 
						|
									item1, exists := queue.itemIndex["file1"]
							 | 
						|
									if !exists {
							 | 
						|
										queue.lock.Unlock()
							 | 
						|
										t.Fatal("Item not found in queue after first add")
							 | 
						|
									}
							 | 
						|
									initialRetryCount := item1.RetryCount
							 | 
						|
									queue.lock.Unlock()
							 | 
						|
								
							 | 
						|
									// Add same file ID again - should NOT increment retry count (just update error)
							 | 
						|
									queue.AddOrUpdate("file1", "timeout error again")
							 | 
						|
								
							 | 
						|
									// Verify still only one item exists in queue (not duplicated)
							 | 
						|
									if queue.Size() != 1 {
							 | 
						|
										t.Errorf("Expected queue size 1 after duplicate add, got %d (duplicates detected)", queue.Size())
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Verify retry count did NOT increment (AddOrUpdate only updates error, not count)
							 | 
						|
									queue.lock.Lock()
							 | 
						|
									item2, exists := queue.itemIndex["file1"]
							 | 
						|
									queue.lock.Unlock()
							 | 
						|
								
							 | 
						|
									if !exists {
							 | 
						|
										t.Fatal("Item not found in queue after second add")
							 | 
						|
									}
							 | 
						|
									if item2.RetryCount != initialRetryCount {
							 | 
						|
										t.Errorf("Expected RetryCount to stay at %d after duplicate add (should not increment), got %d", initialRetryCount, item2.RetryCount)
							 | 
						|
									}
							 | 
						|
									if item2.LastError != "timeout error again" {
							 | 
						|
										t.Errorf("Expected LastError to be updated to 'timeout error again', got %q", item2.LastError)
							 | 
						|
									}
							 | 
						|
								}
							 |