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 <noreply@anthropic.com>
This commit is contained in:
Graham McIntire 2026-02-09 09:20:08 -06:00
parent cedbc5abd1
commit 2b4cb5c3f5
No known key found for this signature in database

View file

@ -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