poll device immediately after discovery completes

newly discovered devices sat idle with no metric data until the
next scheduling cycle. now the channel sends a poll job (and ping
job if monitoring enabled) right after discovery succeeds, using
send(self(), ...) so discovery DB writes commit first.

also reduces k8s replicas from 3 to 2.
This commit is contained in:
Graham McIntire 2026-02-11 13:24:55 -06:00
parent 10d1157ec3
commit 9e36d5030a
No known key found for this signature in database
2 changed files with 137 additions and 0 deletions

View file

@ -145,6 +145,38 @@ defmodule ToweropsWeb.AgentChannel do
{:noreply, socket}
end
# Poll a device immediately after discovery completes.
# Uses send(self(), ...) so discovery DB writes commit first.
def handle_info({:poll_after_discovery, device_id}, socket) do
case Devices.get_device_with_details(device_id) do
nil ->
Logger.error("Device not found for post-discovery poll", device_id: device_id)
device ->
jobs = [build_polling_job(device)]
jobs =
if device.monitoring_enabled do
jobs ++ [build_ping_job(device)]
else
jobs
end
job_list = %AgentJobList{jobs: jobs}
binary = AgentJobList.encode(job_list)
Logger.info("Sending immediate poll after discovery",
device_id: device_id,
device_name: device.name,
job_count: length(jobs)
)
push(socket, "jobs", %{binary: Base.encode64(binary)})
end
{:noreply, socket}
end
# Periodic heartbeat check — close channel if agent hasn't sent a heartbeat recently
def handle_info(:check_heartbeat, socket) do
seconds_since = DateTime.diff(DateTime.utc_now(), socket.assigns.last_heartbeat_at, :second)
@ -1249,6 +1281,9 @@ defmodule ToweropsWeb.AgentChannel do
{:discovery_completed, device.id}
)
# Poll the device immediately so it doesn't sit idle until the next cycle
send(self(), {:poll_after_discovery, device.id})
:ok
{:error, reason} ->

View file

@ -0,0 +1,102 @@
defmodule ToweropsWeb.AgentChannelTest do
use Towerops.DataCase, async: false
import Phoenix.ChannelTest
alias Towerops.AccountsFixtures
alias Towerops.Agent.AgentJobList
alias Towerops.Agent.SnmpResult
alias Towerops.AgentsFixtures
alias Towerops.DevicesFixtures
alias Towerops.OrganizationsFixtures
alias ToweropsWeb.AgentSocket
@endpoint ToweropsWeb.Endpoint
setup do
# Create organization, site, device, and agent token
user = AccountsFixtures.user_fixture()
organization = OrganizationsFixtures.organization_fixture(user.id)
device =
DevicesFixtures.device_fixture(%{
organization_id: organization.id,
snmp_version: "2c",
snmp_community: "public"
})
{:ok, agent_token, token_string} =
AgentsFixtures.agent_token_fixture(organization.id)
# Assign device to agent
{:ok, _assignment} =
AgentsFixtures.agent_assignment_fixture(agent_token.id, device.id)
# Connect socket and join channel
{:ok, socket} = connect(AgentSocket, %{})
{:ok, _, socket} =
subscribe_and_join(socket, "agent:#{agent_token.id}", %{"token" => token_string})
# Drain the initial :send_jobs message pushed on join
assert_push "jobs", _initial_jobs
%{
socket: socket,
device: device,
agent_token: agent_token,
organization: organization
}
end
describe "poll after discovery" do
test "pushes poll jobs after successful discovery result", %{
socket: socket,
device: device
} do
# Build a minimal discovery result with system info OIDs
# that AgentDiscovery.process_agent_discovery can handle
discovery_result = %SnmpResult{
device_id: device.id,
job_type: :DISCOVER,
job_id: "discover:#{device.id}",
timestamp: DateTime.to_unix(DateTime.utc_now()),
oid_values: %{
# sysDescr
"1.3.6.1.2.1.1.1.0" => "Test Device Description",
# sysObjectID
"1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.9.1.1",
# sysUpTime
"1.3.6.1.2.1.1.3.0" => "123456",
# sysContact
"1.3.6.1.2.1.1.4.0" => "admin@test.com",
# sysName
"1.3.6.1.2.1.1.5.0" => "test-device",
# sysLocation
"1.3.6.1.2.1.1.6.0" => "Test Location"
}
}
binary = SnmpResult.encode(discovery_result)
payload = %{"binary" => Base.encode64(binary)}
# Send the discovery result to the channel
push(socket, "result", payload)
# Should receive a "jobs" push with poll job(s) for the discovered device
assert_push "jobs", %{binary: jobs_binary}, 5_000
# Decode the job list and verify it contains a POLL job for our device
{:ok, decoded_binary} = Base.decode64(jobs_binary)
job_list = AgentJobList.decode(decoded_binary)
poll_job =
Enum.find(job_list.jobs, fn job ->
job.job_type == :POLL and job.device_id == device.id
end)
assert poll_job != nil, "Expected a POLL job for device #{device.id}"
assert poll_job.job_id == "poll:#{device.id}"
end
end
end