aprs.me/lib/aprsme_web/plugs/health_check.ex
Graham McIntire b8a9b8a465
chore(dialyzer): enable stricter flags and fix 97 resulting findings
Turned on :error_handling, :underspecs, and :unmatched_returns in
mix.exs dialyzer config. The 97 warnings this surfaced were fixed in
place rather than suppressed:

- unmatched_return (79): explicit discard with `_ = ...` for
  fire-and-forget side effects (Process.cancel_timer, :ets.new,
  send/2), and pattern-matched `:ok = ...` for control-plane
  Phoenix.PubSub subscribe/unsubscribe/broadcast calls so a future
  return-shape change fails loud.

- contract_supertype (18): tightened @spec arg and return types on
  data_builder, historical_loader, url_params, packet_utils,
  encoding_utils, aprs_symbol, weather_controller, packet_replay to
  match each function's actual success typing.

No behavioural change. mix compile clean, 1008 tests pass, dialyzer
count is now 0.
2026-04-21 10:07:01 -05:00

116 lines
2.9 KiB
Elixir

defmodule AprsmeWeb.Plugs.HealthCheck do
@moduledoc """
Health check plug that returns appropriate status based on application state.
Used by Kubernetes liveness and readiness probes.
"""
import Plug.Conn
alias Ecto.Adapters.SQL
require Logger
def init(opts), do: opts
def call(%{request_path: "/health"} = conn, opts) do
probe_type = opts[:probe_type] || :readiness
case check_health(probe_type) do
{:ok, message} ->
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, message)
|> halt()
{:error, message} ->
conn
|> put_resp_content_type("text/plain")
|> send_resp(503, message)
|> halt()
end
end
def call(conn, _opts), do: conn
defp check_health(:liveness) do
# Liveness probe - only fails if the application is truly broken
# Continue returning OK even during shutdown to prevent unnecessary restarts
case basic_health_checks() do
:ok -> {:ok, "OK"}
{:error, reason} -> {:error, "Liveness check failed: #{reason}"}
end
end
defp check_health(:readiness) do
# Readiness probe - fails when shutting down to stop new traffic
health_status = Application.get_env(:aprsme, :health_status, :healthy)
cond do
health_status == :draining ->
{:error, "Application is draining connections"}
shutting_down?() ->
{:error, "Application is shutting down"}
true ->
case full_health_checks() do
:ok -> {:ok, "OK"}
{:error, reason} -> {:error, "Readiness check failed: #{reason}"}
end
end
end
# Basic checks for liveness
# Check if the application is running
defp basic_health_checks do
_ = Application.get_env(:aprsme, :env)
:ok
rescue
_ -> {:error, "Application not responding"}
end
defp full_health_checks do
# Comprehensive checks for readiness
with :ok <- check_database_connection() do
check_pubsub()
end
end
defp check_database_connection do
case SQL.query(Aprsme.Repo, "SELECT 1", [], timeout: 1000) do
{:ok, _} -> :ok
_ -> {:error, "Database connection failed"}
end
rescue
_ -> {:error, "Database check failed"}
end
defp check_pubsub do
_ = Phoenix.PubSub.broadcast(Aprsme.PubSub, "health_check", :ping)
:ok
rescue
_ -> {:error, "PubSub check failed"}
end
defp shutting_down? do
# Check if ShutdownHandler process exists and is shutting down
case Process.whereis(Aprsme.ShutdownHandler) do
nil ->
# Process doesn't exist, not shutting down
false
pid when is_pid(pid) ->
# Process exists, check if alive and call it
if Process.alive?(pid) do
try do
GenServer.call(pid, :shutting_down?, 5000)
catch
:exit, _ -> false
_, _ -> false
end
else
false
end
end
end
end