Packages

A comprehensive SNMP toolkit for Elixir featuring a unified API, pure Elixir implementation, and powerful device simulation. Perfect for network monitoring, testing, and development with support for SNMP operations, MIB management, and realistic device simulation.

Current section

Files

Jump to
snmpkit lib snmpkit snmp_sim performance optimized_udp_server.ex
Raw

lib/snmpkit/snmp_sim/performance/optimized_udp_server.ex

defmodule SnmpKit.SnmpSim.Performance.OptimizedUdpServer do
@moduledoc """
High-performance UDP server optimized for 100K+ requests/second throughput.
Features:
- Multi-socket architecture for load distribution
- Worker pool for concurrent packet processing
- Ring buffer for packet queuing
- Socket-level optimizations for minimal latency
- Adaptive backpressure management
- Direct response path bypassing GenServer for hot paths
"""
use GenServer
require Logger
@dialyzer [
# Ignore "Function will never be called" warnings for worker functions
{:nowarn_function, start_worker_pool: 3},
{:nowarn_function, worker_loop: 3},
{:nowarn_function, process_packet_optimized: 7},
{:nowarn_function, initialize_server_stats: 0},
# Ignore "no local return" warnings for GenServer callbacks
{:nowarn_function, init: 1},
{:nowarn_function, create_multi_socket_setup: 3}
]
alias SnmpKit.SnmpLib.PDU
alias SnmpKit.SnmpSim.Performance.PerformanceMonitor
alias SnmpKit.SnmpSim.Performance.OptimizedDevicePool
# Performance optimization constants
# Multi-socket for load distribution
@default_socket_count 4
# Concurrent packet processors
@default_worker_pool_size 16
# Socket buffer size
@default_buffer_size 65536
# Socket optimization options
@socket_opts [
:binary,
{:active, :once},
{:reuseaddr, true},
{:reuseport, true},
{:buffer, @default_buffer_size},
{:recbuf, @default_buffer_size},
{:sndbuf, @default_buffer_size},
{:priority, 6},
{:tos, 16},
{:nodelay, true}
]
defstruct [
:port,
:sockets,
:worker_pool,
:packet_queue,
:socket_supervisors,
:device_handler,
:community,
:stats,
:backpressure_state,
:optimization_level
]
# Client API
def start_link(port, opts \\ []) do
GenServer.start_link(__MODULE__, {port, opts}, name: via_tuple(port))
end
@doc """
Start optimized UDP server with performance tuning.
"""
def start_optimized(port, opts \\ []) do
optimization_opts = [
socket_count: Keyword.get(opts, :socket_count, @default_socket_count),
worker_pool_size: Keyword.get(opts, :worker_pool_size, @default_worker_pool_size),
buffer_size: Keyword.get(opts, :buffer_size, @default_buffer_size),
optimization_level: Keyword.get(opts, :optimization_level, :high)
]
merged_opts = Keyword.merge(opts, optimization_opts)
start_link(port, merged_opts)
end
@doc """
Get comprehensive server performance statistics.
"""
def get_performance_stats(port) do
GenServer.call(via_tuple(port), :get_performance_stats)
end
@doc """
Update server optimization settings at runtime.
"""
def update_optimization(port, opts) do
GenServer.call(via_tuple(port), {:update_optimization, opts})
end
@doc """
Force immediate packet processing (drain queue).
"""
def force_packet_processing(port) do
GenServer.cast(via_tuple(port), :force_packet_processing)
end
# Server callbacks
@impl true
def init({port, opts}) do
Process.flag(:trap_exit, true)
socket_count = Keyword.get(opts, :socket_count, @default_socket_count)
worker_pool_size = Keyword.get(opts, :worker_pool_size, @default_worker_pool_size)
buffer_size = Keyword.get(opts, :buffer_size, @default_buffer_size)
optimization_level = Keyword.get(opts, :optimization_level, :medium)
community = Keyword.get(opts, :community, "public")
device_handler = Keyword.get(opts, :device_handler, &default_device_handler/3)
# Apply system-level optimizations
apply_system_optimizations(optimization_level)
# Create multi-socket setup for load distribution
case create_multi_socket_setup(port, socket_count, buffer_size) do
{:ok, sockets} ->
# Start worker pool for concurrent processing
{:ok, worker_pool} = start_worker_pool(worker_pool_size, device_handler, community)
# Initialize packet queue with ring buffer
packet_queue = :queue.new()
state = %__MODULE__{
port: port,
sockets: sockets,
worker_pool: worker_pool,
packet_queue: packet_queue,
device_handler: device_handler,
community: community,
stats: initialize_server_stats(),
backpressure_state: :normal,
optimization_level: optimization_level
}
Logger.info(
"OptimizedUdpServer started on port #{port} with #{socket_count} sockets, #{worker_pool_size} workers (#{optimization_level} optimization)"
)
{:ok, state}
{:error, reason} ->
Logger.error("Failed to create sockets: #{inspect(reason)}")
{:stop, reason}
end
end
@impl true
def handle_call(:get_performance_stats, _from, state) do
stats = %{
port: state.port,
socket_count: length(state.sockets),
worker_pool_size: length(state.worker_pool),
queue_size: :queue.len(state.packet_queue),
backpressure_state: state.backpressure_state,
optimization_level: state.optimization_level,
server_stats: state.stats,
system_metrics: get_socket_system_metrics(state.sockets)
}
{:reply, stats, state}
end
@impl true
def handle_call({:update_optimization, opts}, _from, state) do
# Apply runtime optimization updates
new_optimization_level = Keyword.get(opts, :optimization_level, state.optimization_level)
if new_optimization_level != state.optimization_level do
apply_system_optimizations(new_optimization_level)
end
new_state = %{state | optimization_level: new_optimization_level}
Logger.info("Updated optimization level to: #{new_optimization_level}")
{:reply, :ok, new_state}
end
@impl true
def handle_cast(:force_packet_processing, state) do
# Process all queued packets immediately
{processed_count, new_queue} =
process_packet_queue_batch(
state.packet_queue,
state.worker_pool,
:queue.len(state.packet_queue)
)
new_stats = update_server_stats(state.stats, :packets_processed, processed_count)
{:noreply, %{state | packet_queue: new_queue, stats: new_stats}}
end
# ... rest of the code remains the same ...
defp via_tuple(port) do
{:via, Registry, {SnmpSim.ServerRegistry, port}}
end
defp update_socket_opts(base_opts, buffer_size) do
base_opts
|> Keyword.put(:buffer, buffer_size)
|> Keyword.put(:recbuf, buffer_size)
|> Keyword.put(:sndbuf, buffer_size)
end
defp create_multi_socket_setup(port, socket_count, buffer_size) do
# Create multiple sockets on the same port using SO_REUSEPORT
socket_opts = update_socket_opts(@socket_opts, buffer_size)
sockets =
Enum.reduce_while(1..socket_count, [], fn _i, acc ->
case :gen_udp.open(port, socket_opts) do
{:ok, socket} ->
{:cont, [socket | acc]}
{:error, reason} ->
Logger.error("Failed to create socket: #{inspect(reason)}")
{:halt, {:error, reason}}
end
end)
case sockets do
{:error, reason} -> {:error, reason}
sockets_list -> {:ok, Enum.reverse(sockets_list)}
end
end
defp start_worker_pool(pool_size, device_handler, community) do
workers =
Enum.map(1..pool_size, fn worker_id ->
{:ok, pid} =
Task.start_link(fn ->
worker_loop(worker_id, device_handler, community)
end)
{worker_id, pid}
end)
{:ok, workers}
end
defp worker_loop(worker_id, device_handler, community) do
receive do
{:process_packet, socket, ip, port, packet, server_pid, start_time} ->
processing_time =
process_packet_optimized(
socket,
ip,
port,
packet,
device_handler,
community,
start_time
)
send(server_pid, {:packet_processed, worker_id, processing_time})
worker_loop(worker_id, device_handler, community)
:terminate ->
:ok
_other ->
worker_loop(worker_id, device_handler, community)
end
end
defp apply_system_optimizations(optimization_level) do
case optimization_level do
:high ->
# Apply aggressive optimizations
:erlang.system_flag(:schedulers_online, :erlang.system_info(:logical_processors))
:erlang.system_flag(
:dirty_cpu_schedulers_online,
:erlang.system_info(:dirty_cpu_schedulers)
)
:medium ->
# Balanced optimizations
online_schedulers = max(2, div(:erlang.system_info(:logical_processors), 2))
:erlang.system_flag(:schedulers_online, online_schedulers)
:low ->
# Minimal optimizations
:ok
end
end
defp process_packet_queue_batch(queue, worker_pool, batch_size) do
process_batch(queue, worker_pool, batch_size, 0)
end
defp process_batch(queue, worker_pool, batch_size, processed_count) do
if batch_size == 0 do
{processed_count, queue}
else
case :queue.out(queue) do
{{:value, packet_info}, new_queue} ->
# Assign to least loaded worker
worker = select_least_loaded_worker(worker_pool)
send(elem(worker, 1), {:process_packet, packet_info})
process_batch(new_queue, worker_pool, batch_size - 1, processed_count + 1)
{:empty, queue} ->
{processed_count, queue}
end
end
end
defp select_least_loaded_worker(worker_pool) do
# Simple round-robin selection (could be improved with load tracking)
Enum.random(worker_pool)
end
defp process_packet_optimized(socket, ip, port, packet, device_handler, community, start_time) do
try do
case PDU.decode_message(packet) do
{:ok, message} ->
# Validate community
if message.community == community do
# Get device for this port (optimized lookup)
case OptimizedDevicePool.get_device(port) do
{:ok, device_pid} ->
# Create complete PDU structure for device handler
variable_bindings =
case message.pdu.varbinds do
varbinds when is_list(varbinds) ->
Enum.map(varbinds, fn
{oid, _type, value} -> {oid, value}
{oid, value} -> {oid, value}
end)
end
complete_pdu = %{
version: message.version,
community: message.community,
type: message.pdu.type,
request_id: message.pdu.request_id,
error_status: message.pdu[:error_status] || 0,
error_index: message.pdu[:error_index] || 0,
varbinds: variable_bindings,
max_repetitions: message.pdu[:max_repetitions] || 0,
non_repeaters: message.pdu[:non_repeaters] || 0
}
# Process request
case device_handler.(device_pid, complete_pdu, %{ip: ip, port: port}) do
{:ok, response_pdu} ->
# Build response message
response_message =
case response_pdu do
%{
type: _pdu_type,
request_id: request_id,
error_status: error_status,
error_index: error_index,
varbinds: varbinds
} ->
pdu =
PDU.build_response(request_id, error_status, error_index, varbinds)
PDU.build_message(pdu, message.community, message.version)
# other ->
# # Create error response
# # genErr
# pdu = PDU.build_response(0, 5, 0, [])
# PDU.build_message(pdu, message.community, message.version)
end
# Encode and send response
{:ok, response_packet} = PDU.encode_message(response_message)
:gen_udp.send(socket, ip, port, response_packet)
# Record performance metrics
processing_time = System.monotonic_time(:microsecond) - start_time
PerformanceMonitor.record_request_timing(
port,
hd(variable_bindings).oid,
processing_time,
true
)
processing_time
{:error, error_code} ->
# Send error response
error_pdu = PDU.build_response(complete_pdu.request_id, error_code, 0, [])
error_message =
PDU.build_message(error_pdu, message.community, message.version)
{:ok, error_packet} = PDU.encode_message(error_message)
:gen_udp.send(socket, ip, port, error_packet)
processing_time = System.monotonic_time(:microsecond) - start_time
PerformanceMonitor.record_request_timing(
port,
hd(variable_bindings).oid,
processing_time,
false
)
processing_time
end
{:error, :resource_limit_exceeded} ->
# Send resource error
error_pdu =
PDU.build_response(message.pdu.request_id, :resourceUnavailable, 0, [])
error_message = PDU.build_message(error_pdu, message.community, message.version)
{:ok, error_packet} = PDU.encode_message(error_message)
:gen_udp.send(socket, ip, port, error_packet)
System.monotonic_time(:microsecond) - start_time
end
else
# Invalid community - silently drop
System.monotonic_time(:microsecond) - start_time
end
{:error, _reason} ->
# Malformed packet - drop
System.monotonic_time(:microsecond) - start_time
end
rescue
error ->
Logger.error("Packet processing error: #{inspect(error)}")
System.monotonic_time(:microsecond) - start_time
end
end
defp get_socket_system_metrics(sockets) do
socket_stats =
Enum.map(sockets, fn socket ->
case :inet.getstat(socket) do
{:ok, stats} -> stats
{:error, _} -> []
end
end)
total_recv_oct = Enum.sum(Enum.map(socket_stats, &Keyword.get(&1, :recv_oct, 0)))
total_send_oct = Enum.sum(Enum.map(socket_stats, &Keyword.get(&1, :send_oct, 0)))
total_recv_cnt = Enum.sum(Enum.map(socket_stats, &Keyword.get(&1, :recv_cnt, 0)))
total_send_cnt = Enum.sum(Enum.map(socket_stats, &Keyword.get(&1, :send_cnt, 0)))
%{
total_bytes_received: total_recv_oct,
total_bytes_sent: total_send_oct,
total_packets_received: total_recv_cnt,
total_packets_sent: total_send_cnt,
socket_count: length(sockets)
}
end
defp initialize_server_stats() do
%{
packets_processed: 0,
packets_queued: 0,
packets_dropped: 0,
hot_path_requests: 0,
avg_processing_time_us: 0,
max_processing_time_us: 0,
min_processing_time_us: :infinity,
worker_stats: %{},
start_time: System.monotonic_time(:millisecond)
}
end
defp update_server_stats(stats, metric, value) do
Map.update!(stats, metric, &(&1 + value))
end
defp default_device_handler(device_pid, pdu, _context) do
# Default device handler - delegates to device process
GenServer.call(device_pid, {:handle_snmp_request, pdu})
end
end