493 lines
15 KiB
Elixir
493 lines
15 KiB
Elixir
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 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 %{socket: _} = 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
|