144 lines
3.9 KiB
Elixir
144 lines
3.9 KiB
Elixir
defmodule Towerops.Profiles.MibCache do
|
|
@moduledoc """
|
|
GenServer that manages a cache of MIB names to numeric OIDs.
|
|
|
|
Pre-resolves MIB names from device profiles at startup (asynchronously)
|
|
and provides fast lookup with runtime fallback for cache misses.
|
|
|
|
This prevents blocking application startup while still providing fast
|
|
MIB name resolution during profile matching.
|
|
"""
|
|
|
|
use GenServer
|
|
|
|
require Logger
|
|
|
|
@table :mib_cache
|
|
|
|
def start_link(opts \\ []) do
|
|
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
|
end
|
|
|
|
@doc """
|
|
Looks up a MIB name in the cache and returns the numeric OID.
|
|
|
|
If the name is not in cache, it will be resolved at runtime and cached
|
|
for future lookups.
|
|
|
|
Returns the numeric OID string, or the original input if resolution fails.
|
|
"""
|
|
def lookup(mib_name) do
|
|
case :ets.lookup(@table, mib_name) do
|
|
[{^mib_name, oid}] ->
|
|
# Cache hit - return immediately
|
|
oid
|
|
|
|
[] ->
|
|
# Cache miss - resolve at runtime and cache result
|
|
resolve_and_cache(mib_name)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Pre-resolves a list of MIB names asynchronously.
|
|
|
|
This is called by YamlProfiles after loading profiles to warm the cache.
|
|
"""
|
|
def pre_resolve(mib_names) when is_list(mib_names) do
|
|
GenServer.cast(__MODULE__, {:pre_resolve, mib_names})
|
|
end
|
|
|
|
@doc """
|
|
Returns the current cache size (number of cached MIB names).
|
|
"""
|
|
def size do
|
|
:ets.info(@table, :size)
|
|
end
|
|
|
|
# GenServer callbacks
|
|
|
|
@impl true
|
|
def init(_opts) do
|
|
# Create ETS table for MIB cache
|
|
_ = :ets.new(@table, [:named_table, :set, :public, read_concurrency: true])
|
|
|
|
Logger.info("MIB cache initialized (empty)")
|
|
|
|
{:ok, %{}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_cast({:pre_resolve, mib_names}, state) do
|
|
# Pre-resolve MIB names in the background
|
|
# This happens asynchronously and doesn't block callers
|
|
Logger.info("Pre-resolving #{length(mib_names)} unique MIB names from profiles...")
|
|
|
|
resolved_count =
|
|
Enum.reduce(mib_names, 0, fn mib_name, acc ->
|
|
case resolve_oid_with_timeout(mib_name) do
|
|
{:ok, oid} when is_binary(oid) ->
|
|
:ets.insert(@table, {mib_name, oid})
|
|
acc + 1
|
|
|
|
{:error, _reason} ->
|
|
Logger.warning("Failed to resolve MIB name: #{mib_name}")
|
|
acc
|
|
end
|
|
end)
|
|
|
|
Logger.info("Successfully pre-resolved #{resolved_count}/#{length(mib_names)} MIB names")
|
|
|
|
{:noreply, state}
|
|
end
|
|
|
|
# Private helpers
|
|
|
|
defp resolve_and_cache(mib_name) do
|
|
Logger.info("Cache miss for MIB name #{mib_name}, resolving at runtime (fast)")
|
|
|
|
case resolve_oid_with_timeout(mib_name) do
|
|
{:ok, oid} when is_binary(oid) ->
|
|
:ets.insert(@table, {mib_name, oid})
|
|
oid
|
|
|
|
{:error, _reason} ->
|
|
# Resolution failed or timeout - at minimum, strip module prefix
|
|
# Handle MODULE-NAME::objectName format by returning just objectName
|
|
stripped =
|
|
if String.contains?(mib_name, "::") do
|
|
mib_name |> String.split("::") |> List.last()
|
|
else
|
|
mib_name
|
|
end
|
|
|
|
stripped
|
|
end
|
|
end
|
|
|
|
defp resolve_oid_with_timeout(mib_name, timeout \\ 5000) do
|
|
# In test environment, skip resolution to avoid hanging
|
|
# Tests should stub ToweropsNative if they need specific behavior
|
|
if Application.get_env(:towerops, :env) == :test do
|
|
{:error, :test_environment}
|
|
else
|
|
# Add timeout to prevent hanging when snmptranslate is unavailable
|
|
task =
|
|
Task.async(fn ->
|
|
ToweropsNative.resolve_oid(mib_name)
|
|
end)
|
|
|
|
case Task.yield(task, timeout) || Task.shutdown(task) do
|
|
{:ok, oid} when is_binary(oid) ->
|
|
{:ok, oid}
|
|
|
|
{:ok, {:error, reason}} ->
|
|
{:error, reason}
|
|
|
|
nil ->
|
|
# Timeout
|
|
Logger.warning("MIB name resolution timeout for #{mib_name}")
|
|
{:error, :timeout}
|
|
end
|
|
end
|
|
end
|
|
end
|