From 8729e36aff248b17d38d040ae25378ea8827775f Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 1 May 2026 16:40:10 -0500 Subject: [PATCH] fix: use runtime env config in YamlProfiles init (broken in releases) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mix isn't loaded in mix releases, so Mix.env() raised UndefinedFunctionError inside YamlProfiles.init/1, crashing the GenServer at boot. That made Supervisor.start_link return {:error, ...} in production, which the application-start callback then masked by exiting on the unrelated Task.Supervisor.start_child noproc — leaving the pod in CrashLoopBackoff with no clear log of the real failure. Switch to Application.get_env(:towerops, :env), which is set in all three environments (dev/test/runtime). Also harden the post-startup callback so a transient TaskSupervisor noproc cannot mask a real Supervisor.start_link error in the future. --- lib/towerops/application.ex | 34 +++++++++++++++++--------- lib/towerops/profiles/yaml_profiles.ex | 3 ++- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/towerops/application.ex b/lib/towerops/application.ex index c3335111..669030d0 100644 --- a/lib/towerops/application.ex +++ b/lib/towerops/application.ex @@ -142,21 +142,33 @@ defmodule Towerops.Application do opts = [strategy: :one_for_one, name: Towerops.Supervisor] result = Supervisor.start_link(children, opts) - # Attach telemetry filter to suppress noisy health check logs - ToweropsWeb.TelemetryFilter.attach() - - # Run post-startup cleanup tasks (production only) - # This ensures all polling jobs use the latest worker code after deployment - _ = - Task.Supervisor.start_child(Towerops.TaskSupervisor, fn -> - # Wait for supervisor to fully initialize - Process.sleep(2000) - JobCleanupTask.run() - end) + if match?({:ok, _}, result), do: post_startup() result end + # Post-startup work runs only when the supervisor came up cleanly. The Task + # supervisor call is wrapped in try/catch so a transient noproc (the named + # supervisor may not be ready or may already be dying) cannot exit the boot + # process and mask the real error from Supervisor.start_link. + defp post_startup do + ToweropsWeb.TelemetryFilter.attach() + + _ = + try do + Task.Supervisor.start_child(Towerops.TaskSupervisor, fn -> + Process.sleep(2000) + JobCleanupTask.run() + end) + catch + :exit, reason -> + Logger.warning("Skipping JobCleanupTask: #{inspect(reason)}") + :error + end + + :ok + end + # Tell Phoenix to update the endpoint configuration # whenever the application is updated. @impl true diff --git a/lib/towerops/profiles/yaml_profiles.ex b/lib/towerops/profiles/yaml_profiles.ex index 1920d184..7c76dfb9 100644 --- a/lib/towerops/profiles/yaml_profiles.ex +++ b/lib/towerops/profiles/yaml_profiles.ex @@ -89,7 +89,8 @@ defmodule Towerops.Profiles.YamlProfiles do def init(_opts) do _ = :ets.new(@table, [:named_table, :set, :public, read_concurrency: true]) - if Mix.env() == :test do + # Mix isn't loaded in releases — use the runtime app env instead. + if Application.get_env(:towerops, :env) == :test do # Load synchronously so tests don't race with deferred loading. raw_profiles = load_all_profiles() mib_names = extract_mib_names_from_profiles(raw_profiles)