adaptive_min_worker_test.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. package indexer
  2. import (
  3. "runtime"
  4. "testing"
  5. )
  6. // TestMinWorkerCalculation tests the minimum worker calculation for different CPU configurations
  7. func TestMinWorkerCalculation(t *testing.T) {
  8. testCases := []struct {
  9. name string
  10. maxProcs int
  11. expectedMin int
  12. description string
  13. }{
  14. {
  15. name: "Single core system",
  16. maxProcs: 1,
  17. expectedMin: 1,
  18. description: "max(1, 1/8) = max(1, 0) = 1",
  19. },
  20. {
  21. name: "Dual core system",
  22. maxProcs: 2,
  23. expectedMin: 1,
  24. description: "max(1, 2/8) = max(1, 0) = 1",
  25. },
  26. {
  27. name: "Quad core system",
  28. maxProcs: 4,
  29. expectedMin: 1,
  30. description: "max(1, 4/8) = max(1, 0) = 1",
  31. },
  32. {
  33. name: "8-core system",
  34. maxProcs: 8,
  35. expectedMin: 1,
  36. description: "max(1, 8/8) = max(1, 1) = 1",
  37. },
  38. {
  39. name: "16-core system",
  40. maxProcs: 16,
  41. expectedMin: 2,
  42. description: "max(1, 16/8) = max(1, 2) = 2",
  43. },
  44. {
  45. name: "24-core system",
  46. maxProcs: 24,
  47. expectedMin: 3,
  48. description: "max(1, 24/8) = max(1, 3) = 3",
  49. },
  50. }
  51. for _, tc := range testCases {
  52. t.Run(tc.name, func(t *testing.T) {
  53. // Simulate the min worker calculation logic
  54. calculatedMin := max(1, tc.maxProcs/8)
  55. if calculatedMin != tc.expectedMin {
  56. t.Errorf("Expected min workers %d, got %d for %d cores",
  57. tc.expectedMin, calculatedMin, tc.maxProcs)
  58. }
  59. t.Logf("✅ %s: %s -> min workers = %d",
  60. tc.name, tc.description, calculatedMin)
  61. })
  62. }
  63. }
  64. // TestCPUOptimizationScenario simulates the CPU over-utilization scenario
  65. func TestCPUOptimizationScenario(t *testing.T) {
  66. // Simulate a 2-core system (common in containers/VMs)
  67. simulatedGOMAXPROCS := 2
  68. // Create a mock config similar to production
  69. config := &Config{
  70. WorkerCount: 2, // Starting with 2 workers
  71. }
  72. // Create adaptive optimizer with simulated CPU configuration
  73. ao := &AdaptiveOptimizer{
  74. config: config,
  75. cpuMonitor: &CPUMonitor{
  76. targetUtilization: 0.75, // Target 75% CPU utilization
  77. measurementInterval: 5,
  78. adjustmentThreshold: 0.10, // 10% threshold
  79. maxWorkers: simulatedGOMAXPROCS * 3,
  80. minWorkers: max(1, simulatedGOMAXPROCS/8), // New formula
  81. measurements: make([]float64, 0, 12),
  82. },
  83. performanceHistory: &PerformanceHistory{
  84. samples: make([]PerformanceSample, 0, 120),
  85. },
  86. }
  87. // Mock worker count change callback
  88. workerAdjustments := []int{}
  89. ao.SetWorkerCountChangeCallback(func(oldCount, newCount int) {
  90. workerAdjustments = append(workerAdjustments, newCount)
  91. config.WorkerCount = newCount // Update the config
  92. t.Logf("🔧 Worker count adjusted from %d to %d", oldCount, newCount)
  93. })
  94. // Simulate CPU over-utilization scenario
  95. currentCPU := 0.95 // 95% CPU usage
  96. targetCPU := 0.75 // 75% target
  97. t.Logf("📊 Initial state: workers=%d, minWorkers=%d, CPU=%.1f%%, target=%.1f%%",
  98. config.WorkerCount, ao.cpuMonitor.minWorkers, currentCPU*100, targetCPU*100)
  99. // Test the worker decrease logic
  100. ao.suggestWorkerDecrease(currentCPU, targetCPU)
  101. // Verify that adjustment happened
  102. if len(workerAdjustments) == 0 {
  103. t.Errorf("Expected worker count adjustment, but none occurred")
  104. } else {
  105. originalWorkers := 2 // We started with 2 workers
  106. newCount := workerAdjustments[0]
  107. if newCount >= originalWorkers {
  108. t.Errorf("Expected worker count to decrease from %d, but got %d",
  109. originalWorkers, newCount)
  110. } else {
  111. t.Logf("✅ Successfully reduced workers from %d to %d", originalWorkers, newCount)
  112. }
  113. // Verify minimum constraint is respected
  114. if newCount < ao.cpuMonitor.minWorkers {
  115. t.Errorf("Worker count %d is below minimum %d", newCount, ao.cpuMonitor.minWorkers)
  116. }
  117. }
  118. // Test repeated optimization calls (simulating the loop issue)
  119. t.Logf("🔄 Testing repeated optimization calls...")
  120. initialWorkerCount := config.WorkerCount
  121. for i := 0; i < 5; i++ {
  122. ao.suggestWorkerDecrease(currentCPU, targetCPU)
  123. t.Logf("Iteration %d: workers=%d", i+1, config.WorkerCount)
  124. // If worker count reached minimum, it should stop decreasing
  125. if config.WorkerCount == ao.cpuMonitor.minWorkers {
  126. t.Logf("✅ Reached minimum worker count %d", config.WorkerCount)
  127. break
  128. }
  129. }
  130. // Verify we didn't get stuck in infinite loop
  131. if config.WorkerCount < initialWorkerCount {
  132. t.Logf("✅ Worker count successfully reduced from %d to %d",
  133. initialWorkerCount, config.WorkerCount)
  134. }
  135. }
  136. // TestCurrentSystemConfiguration tests with actual system GOMAXPROCS
  137. func TestCurrentSystemConfiguration(t *testing.T) {
  138. currentCores := runtime.GOMAXPROCS(0)
  139. minWorkers := max(1, currentCores/8)
  140. t.Logf("🖥️ Current system configuration:")
  141. t.Logf(" GOMAXPROCS(0): %d", currentCores)
  142. t.Logf(" Calculated min workers: %d", minWorkers)
  143. t.Logf(" Max workers: %d", currentCores*3)
  144. // Verify that we can always scale down to minimum
  145. if minWorkers >= 2 && currentCores <= 16 {
  146. t.Errorf("Min workers %d seems too high for %d cores - may prevent scaling down",
  147. minWorkers, currentCores)
  148. }
  149. // Test scaling scenarios
  150. scenarios := []struct {
  151. startWorkers int
  152. canScaleDown bool
  153. }{
  154. {startWorkers: 8, canScaleDown: true},
  155. {startWorkers: 4, canScaleDown: true},
  156. {startWorkers: 2, canScaleDown: minWorkers < 2},
  157. {startWorkers: 1, canScaleDown: false},
  158. }
  159. for _, scenario := range scenarios {
  160. actualCanScale := scenario.startWorkers > minWorkers
  161. if actualCanScale != scenario.canScaleDown {
  162. t.Logf("⚠️ Scenario mismatch: starting with %d workers, min=%d, expected canScale=%v, actual=%v",
  163. scenario.startWorkers, minWorkers, scenario.canScaleDown, actualCanScale)
  164. } else {
  165. t.Logf("✅ Scaling scenario: %d workers -> min %d (can scale down: %v)",
  166. scenario.startWorkers, minWorkers, actualCanScale)
  167. }
  168. }
  169. }