towerops/lib/towerops/workers/agent_latency_evaluator.ex
Graham McIntire 6c1f344b5e
fix: resolve all dialyzer unmatched return and pattern match warnings
Fixed dialyzer warnings across multiple files by properly handling return
values from functions that are called for side effects.

Changes:
- lib/towerops/agents.ex: Explicitly match broadcast and if-expression returns
- lib/towerops/devices.ex: Match worker start function returns in if-expressions
- lib/towerops/workers/agent_latency_evaluator.ex: Match broadcast return
- lib/towerops/workers/device_monitor_worker.ex: Match perform_check and handle_status_change returns, handle schedule_next_check errors
- lib/towerops/workers/device_poller_worker.ex: Match all PubSub broadcast returns, handle schedule_next_poll errors, remove unreachable pattern
- lib/towerops/workers/stale_agent_worker.ex: Match process_stale_agents and broadcast returns
- lib/towerops_web/channels/agent_channel.ex: Match PubSub.subscribe return, improve get_addr pattern matching, remove unreachable format_ip clause
- lib/towerops_web/live/device_live/form.ex: Match enqueue_discovery return, replace compile-time @dev_env with runtime check

Pattern applied:
- PubSub broadcasts: `_ = Phoenix.PubSub.broadcast(...)`
- If-expressions with side effects: `_ = if condition do ... end`
- Critical operations: Added error logging with case statements

All 3625 tests passing. No dialyzer warnings remaining in lib/towerops or lib/towerops_web.

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-24 17:34:37 -06:00

130 lines
3.7 KiB
Elixir

defmodule Towerops.Workers.AgentLatencyEvaluator do
@moduledoc """
Oban worker that periodically evaluates device-to-agent latency and performs automatic reassignments.
Runs every 5 minutes via Oban.Plugins.Cron to:
1. Find devices where an alternative agent has significantly better latency (20%+ improvement)
2. Reassign devices based on their assignment source (site, organization, global, or device-level)
3. Broadcast PubSub events for UI updates
4. Log all reassignment decisions with improvement metrics
Only considers devices with automatic assignments (site, organization, global, or none).
Manual device-level assignments are never automatically changed.
"""
use Oban.Worker, queue: :maintenance
alias Towerops.Agents
alias Towerops.Agents.Stats
alias Towerops.Devices.Device
alias Towerops.Organizations
alias Towerops.Repo
alias Towerops.Sites
require Logger
# Minimum improvement required for reassignment (20%)
@min_improvement_percent 20
# Minimum checks per agent for statistical validity
@min_checks_per_agent 10
@impl Oban.Worker
def perform(%Oban.Job{}) do
start_time = System.monotonic_time(:millisecond)
candidates =
Stats.find_reassignment_candidates(
min_improvement_percent: @min_improvement_percent,
min_checks_per_agent: @min_checks_per_agent
)
reassignment_count =
candidates
|> Enum.map(&reassign_device/1)
|> Enum.count(&(&1 == :ok))
duration_ms = System.monotonic_time(:millisecond) - start_time
Logger.info(
"Latency evaluation completed: #{length(candidates)} candidate(s) found, " <>
"#{reassignment_count} device(s) reassigned in #{duration_ms}ms"
)
:ok
end
defp reassign_device(candidate) do
Logger.info(
"Reassigning device '#{candidate.device_name}' " <>
"(#{candidate.improvement_percent}% improvement: " <>
"#{candidate.current_latency_ms}ms → #{candidate.best_latency_ms}ms)",
device_id: candidate.device_id,
device_name: candidate.device_name,
old_agent: candidate.current_agent_token_id,
new_agent: candidate.best_agent_token_id,
improvement_percent: candidate.improvement_percent,
assignment_source: candidate.assignment_source
)
result =
case candidate.assignment_source do
:site ->
reassign_at_site_level(candidate)
:organization ->
reassign_at_organization_level(candidate)
source when source in [:global, :none] ->
reassign_at_device_level(candidate)
end
case result do
{:ok, _} ->
_ = broadcast_reassignment(candidate)
:ok
{:error, reason} ->
Logger.error(
"Failed to reassign device '#{candidate.device_name}': #{inspect(reason)}",
device_id: candidate.device_id,
error: reason
)
:error
end
end
defp reassign_at_site_level(candidate) do
device = Device |> Repo.get!(candidate.device_id) |> Repo.preload(:site)
Sites.update_site(device.site, %{agent_token_id: candidate.best_agent_token_id})
end
defp reassign_at_organization_level(candidate) do
device =
Device
|> Repo.get!(candidate.device_id)
|> Repo.preload(site: :organization)
organization = device.site.organization
Organizations.update_organization(organization, %{
default_agent_token_id: candidate.best_agent_token_id
})
end
defp reassign_at_device_level(candidate) do
Agents.update_device_assignment(
candidate.device_id,
candidate.best_agent_token_id
)
end
defp broadcast_reassignment(candidate) do
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"devices:latency",
{:device_reassigned, candidate}
)
end
end