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.
116 lines
2.9 KiB
Elixir
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
|