From 2b4cb5c3f57d344a788b883d51df2882bd5718ec Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 9 Feb 2026 09:20:08 -0600 Subject: [PATCH] fix: handle duplicate MAC address entries gracefully Problem: upsert_mac_address was using Repo.one() which crashes with Ecto.MultipleResultsError when duplicate MAC address records exist in the database. Root Cause: Race condition or historical data corruption created duplicate entries for the same (device_id, mac_address, vlan_id) tuple, violating the unique constraint. Fix: - Changed Repo.one() to Repo.all() to handle duplicates gracefully - When duplicates found: keep first entry, delete extras, log warning - Includes device_id, mac_address, vlan_id, and duplicate IDs in log - Auto-cleans corrupted data while maintaining service availability Example Log Output: Found 1 duplicate MAC address entries, cleaning up device_id=f3ff19d5-b519-4327-9f7d-dc416e494cfa mac_address=78:9a:18:52:b1:c6 vlan_id=nil duplicate_ids=[...] Impact: - Prevents polling crashes when duplicate MAC entries exist - Self-healing: automatically cleans up database corruption - Maintains data integrity while preserving service availability Related: Issue 2.9 from bugs analysis (ARP/MAC parsing issues) Co-Authored-By: Claude Sonnet 4.5 --- lib/towerops/snmp.ex | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/towerops/snmp.ex b/lib/towerops/snmp.ex index 401e0c74..c7ab33d8 100644 --- a/lib/towerops/snmp.ex +++ b/lib/towerops/snmp.ex @@ -1570,6 +1570,8 @@ defmodule Towerops.Snmp do def upsert_mac_address(attrs) do import Ecto.Query + require Logger + # Build query to find existing MAC address entry query = from m in MacAddress, @@ -1583,16 +1585,36 @@ defmodule Towerops.Snmp do vlan_id -> where(query, [m], m.vlan_id == ^vlan_id) end - case Repo.one(query) do - nil -> + # Use Repo.all to handle potential duplicates gracefully + # If duplicates exist, keep the first one and delete the rest + case Repo.all(query) do + [] -> %MacAddress{} |> MacAddress.changeset(attrs) |> Repo.insert() - mac_address -> + [mac_address] -> mac_address |> MacAddress.changeset(attrs) |> Repo.update() + + [first | duplicates] -> + # Handle duplicates: delete extras, update first + Logger.warning("Found #{length(duplicates)} duplicate MAC address entries, cleaning up", + device_id: attrs.device_id, + mac_address: attrs.mac_address, + vlan_id: attrs[:vlan_id], + duplicate_ids: Enum.map(duplicates, & &1.id) + ) + + # Delete duplicate entries + duplicate_ids = Enum.map(duplicates, & &1.id) + Repo.delete_all(from m in MacAddress, where: m.id in ^duplicate_ids) + + # Update the remaining entry + first + |> MacAddress.changeset(attrs) + |> Repo.update() end end