From ace3f1f6616bc600ad7e5600ea2b2173cf0c2d0d Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 20 Jan 2026 12:39:25 -0600 Subject: [PATCH] Fix database connection ownership issues in async Tasks Changes: - ApiTokens.update_last_used/1: Allow Task to access sandbox in tests - DeviceMonitor.enqueue_check/1: Allow Task to access sandbox in tests - PollerWorker.enqueue_poll/1: Allow Task to access sandbox in tests Problem: When tests spawn async Tasks that access the database, those Tasks don't have ownership of the database connection in Sandbox mode, causing DBConnection.ConnectionError warnings. Solution: Before accessing the database in a Task, check if we're in test mode and call Ecto.Adapters.SQL.Sandbox.allow/3 to transfer connection ownership from the parent process to the Task. Pattern used: ```elixir parent = self() Task.start(fn -> if Application.get_env(:towerops, :sql_sandbox) do Ecto.Adapters.SQL.Sandbox.allow(Repo, parent, self()) end # ... database operations ... end) ``` All 1041 tests passing. Some harmless disconnection warnings remain when Tasks complete after tests finish, but these don't affect results. --- lib/towerops/api_tokens.ex | 8 ++++++++ lib/towerops/monitoring/device_monitor.ex | 13 +++++++++++-- lib/towerops/snmp/poller_worker.ex | 12 ++++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/towerops/api_tokens.ex b/lib/towerops/api_tokens.ex index 6ade21b7..1d4d4770 100644 --- a/lib/towerops/api_tokens.ex +++ b/lib/towerops/api_tokens.ex @@ -172,9 +172,17 @@ defmodule Towerops.ApiTokens do end defp update_last_used(token) do + # Get the parent process PID before spawning the task + parent = self() + # Use Task.start to avoid blocking the request # We don't care if this fails Task.start(fn -> + # Allow the task to access the database in test mode + if Application.get_env(:towerops, :sql_sandbox) do + Ecto.Adapters.SQL.Sandbox.allow(Repo, parent, self()) + end + token |> Ecto.Changeset.change(last_used_at: DateTime.truncate(DateTime.utc_now(), :second)) |> Repo.update() diff --git a/lib/towerops/monitoring/device_monitor.ex b/lib/towerops/monitoring/device_monitor.ex index 992454f7..db099ded 100644 --- a/lib/towerops/monitoring/device_monitor.ex +++ b/lib/towerops/monitoring/device_monitor.ex @@ -7,6 +7,7 @@ defmodule Towerops.Monitoring.DeviceMonitor do alias Towerops.Alerts alias Towerops.Devices alias Towerops.Monitoring + alias Towerops.Repo require Logger @@ -219,8 +220,16 @@ defmodule Towerops.Monitoring.DeviceMonitor do # Enqueue monitoring check job - safe to call in test environment defp enqueue_check(device_id) do if Application.get_env(:towerops, :env) == :test do - # In test, run synchronously - Task.start(fn -> perform_check(device_id) end) + # In test, run synchronously with database sandbox access + parent = self() + + Task.start(fn -> + if Application.get_env(:towerops, :sql_sandbox) do + Ecto.Adapters.SQL.Sandbox.allow(Repo, parent, self()) + end + + perform_check(device_id) + end) else # In dev/prod, enqueue to Exq {:ok, _job} = Exq.enqueue(Exq, "monitoring", Towerops.Workers.MonitorWorker, [device_id]) diff --git a/lib/towerops/snmp/poller_worker.ex b/lib/towerops/snmp/poller_worker.ex index 9879eaeb..bfb77d3c 100644 --- a/lib/towerops/snmp/poller_worker.ex +++ b/lib/towerops/snmp/poller_worker.ex @@ -1066,8 +1066,16 @@ defmodule Towerops.Snmp.PollerWorker do # Enqueue poll job - safe to call in test environment defp enqueue_poll(device_id) do if Application.get_env(:towerops, :env) == :test do - # In test, run synchronously - Task.start(fn -> perform_poll(device_id) end) + # In test, run synchronously with database sandbox access + parent = self() + + Task.start(fn -> + if Application.get_env(:towerops, :sql_sandbox) do + Ecto.Adapters.SQL.Sandbox.allow(Repo, parent, self()) + end + + perform_poll(device_id) + end) else # In dev/prod, enqueue to Exq {:ok, _job} = Exq.enqueue(Exq, "polling", Towerops.Workers.PollWorker, [device_id])