fix: critical subscriber matching and performance improvements

- Add missing wireless_mac and wireless_ip to schema validation
  (was causing highest-confidence matches to be rejected)
- Optimize account lookup to load once instead of 5x per refresh
  (eliminates N+1 query performance issue)
- Preserve all matching phases with proper signature updates
This commit is contained in:
Graham McIntire 2026-03-10 11:33:47 -05:00
parent a19155f3de
commit 1a9a6e0ef6
No known key found for this signature in database
2 changed files with 16 additions and 21 deletions

View file

@ -39,7 +39,7 @@ defmodule Towerops.Gaiia.DeviceSubscriberLink do
:subscriber_mac
])
|> validate_required([:organization_id, :device_id, :gaiia_account_id, :match_method, :confidence])
|> validate_inclusion(:match_method, ~w(arp_ip arp_mac subnet site_fallback))
|> validate_inclusion(:match_method, ~w(wireless_mac wireless_ip arp_ip arp_mac subnet site_fallback))
|> validate_inclusion(:confidence, ~w(high medium low))
|> unique_constraint([:device_id, :gaiia_account_id])
|> foreign_key_constraint(:organization_id)

View file

@ -69,6 +69,9 @@ defmodule Towerops.Gaiia.SubscriberMatching do
# Load all wireless client entries for this org's devices
wireless_clients = load_wireless_clients(organization_id)
# Load account lookup once for all phases (performance optimization)
account_lookup = load_account_id_lookup(organization_id)
# Build lookup maps
arp_by_ip = Map.new(arp_entries, fn e -> {e.ip_address, e} end)
arp_by_mac = Map.new(arp_entries, fn e -> {normalize_mac(e.mac_address), e} end)
@ -76,26 +79,28 @@ defmodule Towerops.Gaiia.SubscriberMatching do
wc_by_ip = wireless_clients |> Enum.filter(& &1.ip_address) |> Map.new(fn w -> {w.ip_address, w} end)
# Phase 1: Wireless client MAC match (highest confidence)
{wc_mac_matched, wc_mac_links} = match_by_wireless_mac(items, wc_by_mac, organization_id, now)
{wc_mac_matched, wc_mac_links} = match_by_wireless_mac(items, wc_by_mac, account_lookup, organization_id, now)
# Phase 2: Wireless client IP match (high confidence)
remaining_after_wc_mac = Enum.reject(items, &MapSet.member?(wc_mac_matched, &1.id))
{wc_ip_matched, wc_ip_links} = match_by_wireless_ip(remaining_after_wc_mac, wc_by_ip, organization_id, now)
{wc_ip_matched, wc_ip_links} =
match_by_wireless_ip(remaining_after_wc_mac, wc_by_ip, account_lookup, organization_id, now)
# Phase 3: ARP IP match (medium confidence)
matched_so_far = MapSet.union(wc_mac_matched, wc_ip_matched)
remaining_after_wc = Enum.reject(items, &MapSet.member?(matched_so_far, &1.id))
{ip_matched, ip_links} = match_by_arp_ip(remaining_after_wc, arp_by_ip, organization_id, now)
{ip_matched, ip_links} = match_by_arp_ip(remaining_after_wc, arp_by_ip, account_lookup, organization_id, now)
# Phase 4: ARP MAC match (medium confidence)
matched_so_far = MapSet.union(matched_so_far, ip_matched)
remaining_after_ip = Enum.reject(items, &MapSet.member?(matched_so_far, &1.id))
{mac_matched, mac_links} = match_by_arp_mac(remaining_after_ip, arp_by_mac, organization_id, now)
{mac_matched, mac_links} = match_by_arp_mac(remaining_after_ip, arp_by_mac, account_lookup, organization_id, now)
# Phase 5: Site fallback (low confidence)
matched_ids = MapSet.union(matched_so_far, mac_matched)
remaining = Enum.reject(items, &MapSet.member?(matched_ids, &1.id))
site_links = match_by_site_fallback(remaining, organization_id, now)
site_links = match_by_site_fallback(remaining, account_lookup, organization_id, now)
all_links = wc_mac_links ++ wc_ip_links ++ ip_links ++ mac_links ++ site_links
@ -259,9 +264,7 @@ defmodule Towerops.Gaiia.SubscriberMatching do
|> Map.new()
end
defp match_by_wireless_mac(items, wc_by_mac, org_id, now) do
account_lookup = load_account_id_lookup(org_id)
defp match_by_wireless_mac(items, wc_by_mac, account_lookup, org_id, now) do
{matched_ids, links} =
Enum.reduce(items, {MapSet.new(), []}, fn item, {matched, acc} ->
normalized = normalize_mac(item.mac_address)
@ -292,9 +295,7 @@ defmodule Towerops.Gaiia.SubscriberMatching do
{matched_ids, Enum.reverse(links)}
end
defp match_by_wireless_ip(items, wc_by_ip, org_id, now) do
account_lookup = load_account_id_lookup(org_id)
defp match_by_wireless_ip(items, wc_by_ip, account_lookup, org_id, now) do
{matched_ids, links} =
Enum.reduce(items, {MapSet.new(), []}, fn item, {matched, acc} ->
wc = if item.ip_address, do: Map.get(wc_by_ip, item.ip_address)
@ -324,9 +325,7 @@ defmodule Towerops.Gaiia.SubscriberMatching do
{matched_ids, Enum.reverse(links)}
end
defp match_by_arp_ip(items, arp_by_ip, org_id, now) do
account_lookup = load_account_id_lookup(org_id)
defp match_by_arp_ip(items, arp_by_ip, account_lookup, org_id, now) do
{matched_ids, links} =
Enum.reduce(items, {MapSet.new(), []}, fn item, {matched, acc} ->
arp = if item.ip_address, do: Map.get(arp_by_ip, item.ip_address)
@ -356,9 +355,7 @@ defmodule Towerops.Gaiia.SubscriberMatching do
{matched_ids, Enum.reverse(links)}
end
defp match_by_arp_mac(items, arp_by_mac, org_id, now) do
account_lookup = load_account_id_lookup(org_id)
defp match_by_arp_mac(items, arp_by_mac, account_lookup, org_id, now) do
{matched_ids, links} =
Enum.reduce(items, {MapSet.new(), []}, fn item, {matched, acc} ->
normalized = normalize_mac(item.mac_address)
@ -389,9 +386,7 @@ defmodule Towerops.Gaiia.SubscriberMatching do
{matched_ids, Enum.reverse(links)}
end
defp match_by_site_fallback(items, org_id, now) do
account_lookup = load_account_id_lookup(org_id)
defp match_by_site_fallback(items, account_lookup, org_id, now) do
# Load network site -> TowerOps site mappings
site_mappings =
NetworkSite