Optimize list_agent_polling_targets to use SQL filtering

Rewrote list_agent_polling_targets/1 to filter equipment using SQL WHERE clause
instead of loading all SNMP-enabled equipment into memory and filtering in Elixir.

Previous implementation:
- Loaded ALL SNMP-enabled equipment with preloads (potentially 10,000+ records)
- Filtered in application code with equipment_matches_agent?/2 helper
- Called get_equipment_assignment/1 for each piece of equipment (N+1 queries)
- Memory-intensive and slow at scale

New implementation:
- Single SQL query with LEFT JOIN and compound WHERE clause
- Evaluates hierarchical assignment (equipment → site → organization) in SQL
- Only loads equipment assigned to the specific agent
- Scales efficiently to 10,000+ equipment

Performance improvement:
- With 10,000 equipment, 1000 assigned to agent:
  - Before: Load 10,000 records + 10,000 queries = massive memory + slow
  - After: Load 1,000 records in 1 query = 10x memory reduction + instant

Removed unused helper functions:
- equipment_matches_agent?/2
- site_matches_agent?/2

All 40 agent tests passing.
This commit is contained in:
Graham McIntire 2026-01-15 07:33:52 -06:00
parent 723f064315
commit bb5766f1a6
No known key found for this signature in database

View file

@ -239,55 +239,27 @@ defmodule Towerops.Agents do
"""
def list_agent_polling_targets(agent_token_id) do
# Get all equipment with necessary preloads
equipment_query =
# Use SQL to filter equipment by effective agent assignment
# This avoids loading all equipment into memory and filtering in application code
Repo.all(
from(e in Equipment,
where: e.snmp_enabled == true,
join: s in assoc(e, :site),
join: o in assoc(s, :organization),
left_join: aa in AgentAssignment,
on: aa.equipment_id == e.id and aa.enabled == true,
where:
e.snmp_enabled == true and
(aa.agent_token_id == ^agent_token_id or
(is_nil(aa.agent_token_id) and s.agent_token_id == ^agent_token_id) or
(is_nil(aa.agent_token_id) and is_nil(s.agent_token_id) and
o.default_agent_token_id == ^agent_token_id)),
preload: [
:agent_assignments,
site: :organization,
snmp_device: [:sensors, :interfaces]
]
)
equipment_list = Repo.all(equipment_query)
# Filter to only equipment that resolves to this agent
Enum.filter(equipment_list, fn equipment ->
equipment_matches_agent?(equipment, agent_token_id)
end)
end
defp equipment_matches_agent?(equipment, agent_token_id) do
# Check direct assignment first
case get_equipment_assignment(equipment.id) do
%AgentAssignment{agent_token_id: ^agent_token_id, enabled: true} ->
true
%AgentAssignment{} ->
# Has a different direct assignment
false
nil ->
# No direct assignment, check site then org
site = equipment.site
site_matches_agent?(site, agent_token_id)
end
end
defp site_matches_agent?(site, agent_token_id) do
cond do
# Check site agent
site.agent_token_id == agent_token_id ->
true
# Check org default (only if site doesn't have an agent set)
is_nil(site.agent_token_id) and site.organization.default_agent_token_id == agent_token_id ->
true
true ->
false
end
)
end
@doc """