defmodule SnmpKit.SnmpLib.PoolTest do use ExUnit.Case, async: false alias SnmpKit.SnmpLib.Pool doctest Pool @moduletag :pool_test # Helper function for safe pool cleanup defp safe_stop_pool(pool_name, timeout \\ 100) do if Process.whereis(pool_name), do: Pool.stop_pool(pool_name, timeout) rescue _ -> :ok catch :exit, _ -> :ok end setup do # Clean up any existing pools with better error handling pool_names = [ :test_pool, :custom_pool, :duplicate_test, :affinity_pool, :fifo_pool, :rr_pool, :overflow_pool, :max_overflow_pool, :health_pool, :cleanup_pool, :stats_pool, :operation_stats_pool, :error_pool, :recovery_pool, :lifecycle_pool, :timeout_pool, :perf_pool, :reuse_pool ] Enum.each(pool_names, fn pool_name -> try do if Process.whereis(pool_name) do Pool.stop_pool(pool_name, 100) end rescue _ -> :ok catch :exit, _ -> :ok end end) # Small delay to ensure cleanup :timer.sleep(10) :ok end describe "Pool.start_pool/2" do test "starts a basic pool with default options" do assert {:ok, pid} = Pool.start_pool(:test_pool) assert Process.alive?(pid) # Check pool stats stats = Pool.get_stats(:test_pool) assert stats.name == :test_pool assert stats.strategy == :fifo # Default size assert stats.size == 10 Pool.stop_pool(:test_pool) end test "starts a pool with custom configuration" do opts = [ strategy: :device_affinity, size: 5, max_overflow: 3, health_check_interval: 10_000 ] assert {:ok, pid} = Pool.start_pool(:custom_pool, opts) assert Process.alive?(pid) stats = Pool.get_stats(:custom_pool) assert stats.strategy == :device_affinity assert stats.size == 5 Pool.stop_pool(:custom_pool) end test "prevents duplicate pool names" do assert {:ok, _pid} = Pool.start_pool(:duplicate_test) # Second pool with same name should fail assert {:error, _reason} = Pool.start_pool(:duplicate_test) Pool.stop_pool(:duplicate_test) end end describe "Pool.with_connection/4" do setup do {:ok, _pid} = Pool.start_pool(:test_pool, size: 3) on_exit(fn -> safe_stop_pool(:test_pool) end) :ok end test "executes function with pooled connection" do result = Pool.with_connection(:test_pool, "192.168.1.1", fn conn -> assert is_map(conn) assert Map.has_key?(conn, :socket) assert Map.has_key?(conn, :device) assert conn.device == "192.168.1.1" {:ok, :test_result} end) assert result == {:ok, :test_result} end test "handles function errors gracefully" do result = Pool.with_connection(:test_pool, "192.168.1.1", fn _conn -> raise "Test error" end) assert match?({:error, {:operation_failed, _}}, result) end test "returns connection to pool after use" do initial_stats = Pool.get_stats(:test_pool) initial_idle = initial_stats.idle_connections Pool.with_connection(:test_pool, "192.168.1.1", fn _conn -> # Connection should be checked out during_stats = Pool.get_stats(:test_pool) assert during_stats.idle_connections == initial_idle - 1 assert during_stats.active_connections == 1 :ok end) # Connection should be returned final_stats = Pool.get_stats(:test_pool) assert final_stats.idle_connections == initial_idle assert final_stats.active_connections == 0 end test "supports concurrent operations" do tasks = Enum.map(1..5, fn i -> Task.async(fn -> Pool.with_connection(:test_pool, "device_#{i}", fn conn -> # No sleep needed for testing connection logic {:ok, conn.device} end) end) end) results = Task.await_many(tasks, 1000) # All should succeed assert Enum.all?(results, fn result -> match?({:ok, _}, result) end) # Check that connections were properly managed stats = Pool.get_stats(:test_pool) assert stats.total_checkouts >= 5 # All returned assert stats.active_connections == 0 end end describe "Pool.checkout_connection/3 and checkin_connection/2" do setup do {:ok, _pid} = Pool.start_pool(:test_pool, size: 2, max_overflow: 0) on_exit(fn -> safe_stop_pool(:test_pool) end) :ok end test "manually checks out and returns connections" do assert {:ok, conn1} = Pool.checkout_connection(:test_pool, "device1") assert {:ok, conn2} = Pool.checkout_connection(:test_pool, "device2") # Pool should be exhausted stats = Pool.get_stats(:test_pool) assert stats.idle_connections == 0 assert stats.active_connections == 2 # Return connections assert :ok = Pool.checkin_connection(:test_pool, conn1) assert :ok = Pool.checkin_connection(:test_pool, conn2) # Pool should be restored final_stats = Pool.get_stats(:test_pool) assert final_stats.idle_connections == 2 assert final_stats.active_connections == 0 end test "handles checkout timeout when pool is exhausted" do # Checkout all connections {:ok, conn1} = Pool.checkout_connection(:test_pool, "device1") {:ok, conn2} = Pool.checkout_connection(:test_pool, "device2") # Next checkout should fail when pool is exhausted (no overflow allowed) assert {:error, :no_connections} = Pool.checkout_connection(:test_pool, "device3", timeout: 100) # Return one connection Pool.checkin_connection(:test_pool, conn1) # Should be able to checkout again assert {:ok, _conn3} = Pool.checkout_connection(:test_pool, "device3") # Cleanup Pool.checkin_connection(:test_pool, conn2) end end describe "Pool strategies" do test "FIFO strategy processes connections in order" do {:ok, _pid} = Pool.start_pool(:fifo_pool, strategy: :fifo, size: 3) # Strategy should be FIFO stats = Pool.get_stats(:fifo_pool) assert stats.strategy == :fifo Pool.stop_pool(:fifo_pool) end test "round-robin strategy distributes connections evenly" do {:ok, _pid} = Pool.start_pool(:rr_pool, strategy: :round_robin, size: 3) stats = Pool.get_stats(:rr_pool) assert stats.strategy == :round_robin Pool.stop_pool(:rr_pool) end test "device-affinity strategy maintains device associations" do {:ok, _pid} = Pool.start_pool(:affinity_pool, strategy: :device_affinity, size: 3) stats = Pool.get_stats(:affinity_pool) assert stats.strategy == :device_affinity # Test device affinity behavior {:ok, conn1} = Pool.checkout_connection(:affinity_pool, "device1") Pool.checkin_connection(:affinity_pool, conn1) {:ok, conn2} = Pool.checkout_connection(:affinity_pool, "device1") # With device affinity, we should get connections that work for the device # The exact socket might be different but the device should be properly set assert conn2.device == "device1" Pool.checkin_connection(:affinity_pool, conn2) Pool.stop_pool(:affinity_pool) end end describe "Pool overflow handling" do test "creates overflow connections when pool is exhausted" do {:ok, _pid} = Pool.start_pool(:overflow_pool, size: 2, max_overflow: 2) # Checkout all base connections {:ok, conn1} = Pool.checkout_connection(:overflow_pool, "device1") {:ok, conn2} = Pool.checkout_connection(:overflow_pool, "device2") initial_stats = Pool.get_stats(:overflow_pool) assert initial_stats.idle_connections == 0 assert initial_stats.active_connections == 2 assert initial_stats.overflow_connections == 0 # Checkout overflow connections {:ok, conn3} = Pool.checkout_connection(:overflow_pool, "device3") {:ok, conn4} = Pool.checkout_connection(:overflow_pool, "device4") overflow_stats = Pool.get_stats(:overflow_pool) assert overflow_stats.overflow_connections >= 2 # Return all connections Pool.checkin_connection(:overflow_pool, conn1) Pool.checkin_connection(:overflow_pool, conn2) Pool.checkin_connection(:overflow_pool, conn3) Pool.checkin_connection(:overflow_pool, conn4) Pool.stop_pool(:overflow_pool) end test "rejects connections when max overflow is reached" do {:ok, _pid} = Pool.start_pool(:max_overflow_pool, size: 1, max_overflow: 1) # Checkout base and overflow {:ok, conn1} = Pool.checkout_connection(:max_overflow_pool, "device1") {:ok, conn2} = Pool.checkout_connection(:max_overflow_pool, "device2") # Should reject further checkouts assert {:error, :no_connections} = Pool.checkout_connection(:max_overflow_pool, "device3", timeout: 100) Pool.checkin_connection(:max_overflow_pool, conn1) Pool.checkin_connection(:max_overflow_pool, conn2) Pool.stop_pool(:max_overflow_pool) end end describe "Pool health monitoring" do test "tracks connection health status" do {:ok, _pid} = Pool.start_pool(:health_pool, size: 2, health_check_interval: 100) # Force health check Pool.health_check(:health_pool) stats = Pool.get_stats(:health_pool) assert Map.has_key?(stats, :health_status) Pool.stop_pool(:health_pool) end test "cleans up unhealthy connections" do {:ok, _pid} = Pool.start_pool(:cleanup_pool, size: 2) # Simulate unhealthy connections Pool.cleanup_unhealthy(:cleanup_pool) # Pool should still be functional stats = Pool.get_stats(:cleanup_pool) assert stats.idle_connections >= 0 Pool.stop_pool(:cleanup_pool) end end describe "Pool statistics" do test "provides comprehensive pool statistics" do {:ok, _pid} = Pool.start_pool(:stats_pool, size: 3, max_overflow: 2) stats = Pool.get_stats(:stats_pool) # Check required fields assert Map.has_key?(stats, :name) assert Map.has_key?(stats, :strategy) assert Map.has_key?(stats, :size) assert Map.has_key?(stats, :active_connections) assert Map.has_key?(stats, :idle_connections) assert Map.has_key?(stats, :overflow_connections) assert Map.has_key?(stats, :total_checkouts) assert Map.has_key?(stats, :total_checkins) assert Map.has_key?(stats, :health_status) assert Map.has_key?(stats, :average_response_time) # Check initial values assert stats.name == :stats_pool assert stats.size == 3 assert stats.idle_connections == 3 assert stats.active_connections == 0 assert stats.total_checkouts == 0 assert stats.total_checkins == 0 Pool.stop_pool(:stats_pool) end test "updates statistics during operation" do {:ok, _pid} = Pool.start_pool(:operation_stats_pool, size: 2) initial_stats = Pool.get_stats(:operation_stats_pool) assert initial_stats.total_checkouts == 0 # Perform some operations Pool.with_connection(:operation_stats_pool, "device1", fn _conn -> :ok end) Pool.with_connection(:operation_stats_pool, "device2", fn _conn -> :ok end) updated_stats = Pool.get_stats(:operation_stats_pool) assert updated_stats.total_checkouts >= 2 assert updated_stats.total_checkins >= 2 Pool.stop_pool(:operation_stats_pool) end end describe "Pool error handling" do test "handles worker process failures gracefully" do {:ok, _pid} = Pool.start_pool(:error_pool, size: 2) # Pool should remain functional even with simulated errors result = Pool.with_connection(:error_pool, "device1", fn conn -> assert is_map(conn) :ok end) assert result == :ok Pool.stop_pool(:error_pool) end test "recovers from connection failures" do {:ok, _pid} = Pool.start_pool(:recovery_pool, size: 2) # Simulate connection failure and recovery Pool.cleanup_unhealthy(:recovery_pool) # Pool should still work result = Pool.with_connection(:recovery_pool, "device1", fn _conn -> :success end) assert result == :success Pool.stop_pool(:recovery_pool) end end describe "Pool lifecycle" do test "stops gracefully and cleans up resources" do {:ok, pid} = Pool.start_pool(:lifecycle_pool, size: 2) assert Process.alive?(pid) # Use the pool Pool.with_connection(:lifecycle_pool, "device1", fn _conn -> :ok end) # Stop should succeed assert :ok = Pool.stop_pool(:lifecycle_pool, 1000) refute Process.alive?(pid) end test "handles stop timeout appropriately" do {:ok, _pid} = Pool.start_pool(:timeout_pool, size: 1) # Should stop within timeout assert :ok = Pool.stop_pool(:timeout_pool, 100) end end # Performance tests describe "Pool performance" do @tag :performance test "handles high-frequency operations efficiently" do {:ok, _pid} = Pool.start_pool(:perf_pool, size: 10, max_overflow: 15) # Measure time for many operations {time_microseconds, results} = :timer.tc(fn -> tasks = Enum.map(1..20, fn i -> Task.async(fn -> Pool.with_connection(:perf_pool, "device_#{rem(i, 10)}", fn _conn -> # Minimal work :timer.sleep(1) :ok end) end) end) Task.await_many(tasks, 5000) end) # All operations should succeed assert Enum.all?(results, fn result -> result == :ok end) # Should complete reasonably quickly (less than 2 seconds) assert time_microseconds < 2_000_000 stats = Pool.get_stats(:perf_pool) assert stats.total_checkouts >= 20 Pool.stop_pool(:perf_pool) end @tag :performance test "connection reuse provides performance benefit" do {:ok, _pid} = Pool.start_pool(:reuse_pool, size: 3) # Simulate repeated operations to same device device = "performance.test.device" {time_microseconds, _} = :timer.tc(fn -> Enum.each(1..20, fn _i -> Pool.with_connection(:reuse_pool, device, fn _conn -> # Simulate SNMP operation :timer.sleep(5) :ok end) end) end) # Should complete efficiently with connection reuse # Less than 1 second assert time_microseconds < 1_000_000 stats = Pool.get_stats(:reuse_pool) assert stats.total_checkouts == 20 Pool.stop_pool(:reuse_pool) end end end