aprs.me/lib/aprsme/release.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

168 lines
4.7 KiB
Elixir

defmodule Aprsme.Release do
@moduledoc """
Used for executing DB release tasks when run in production without Mix
installed.
"""
alias Ecto.Adapters.SQL
@app :aprsme
def migrate do
require Logger
# Initialize deployment timestamp first
deployed_at = init()
Logger.info("Deployment timestamp: #{deployed_at}")
# Gettext translations are automatically compiled during Mix compilation
# Skip database creation when using PgBouncer
# The database should already exist
if System.get_env("SKIP_DB_CREATE") != "true" do
create_database()
end
# Run migrations with distributed lock
cluster_enabled = Application.get_env(:aprsme, :cluster_enabled, false)
# Configure longer timeouts for migration operations
# 2 hours default
migration_timeout = String.to_integer(System.get_env("MIGRATION_TIMEOUT", "7200000"))
if cluster_enabled do
Logger.info("Running migrations with distributed lock...")
# Ensure repo is started for advisory lock with extended timeout
repo_config =
Aprsme.Repo.config()
|> Keyword.put(:timeout, migration_timeout)
|> Keyword.put(:pool_timeout, 60_000)
{:ok, _} = Aprsme.Repo.start_link(repo_config)
Aprsme.MigrationLock.with_lock(Aprsme.Repo, fn ->
run_migrations_with_timeout(migration_timeout)
end)
else
Logger.info("Running migrations without lock (single node)...")
run_migrations_with_timeout(migration_timeout)
end
end
defp create_database do
require Logger
case Aprsme.Repo.__adapter__().storage_up(Aprsme.Repo.config()) do
:ok ->
Logger.info("Database created successfully")
{:error, :already_up} ->
Logger.info("Database already exists")
{:error, error} ->
Logger.error("Failed to create database: #{inspect(error)}")
raise "Database creation failed: #{inspect(error)}"
end
end
def rollback(repo, version) do
_ = load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
defp load_app do
Application.load(@app)
end
@doc """
Initialize release-specific configuration.
This is called during application startup.
"""
def init do
# Check if deployment timestamp is provided via environment variable
# This allows k8s deployments to set a persistent timestamp
deployed_at =
case System.get_env("DEPLOYED_AT") do
nil ->
# Fallback to current time for local development or non-k8s deployments
DateTime.utc_now()
timestamp_str ->
# Parse the ISO8601 timestamp from environment variable
case DateTime.from_iso8601(timestamp_str) do
{:ok, datetime, _offset} ->
datetime
{:error, _} ->
require Logger
Logger.warning("Invalid DEPLOYED_AT timestamp: #{timestamp_str}, using current time")
DateTime.utc_now()
end
end
# Store in application config
Application.put_env(:aprsme, :deployed_at, deployed_at)
# Notify about deployment after a short delay to ensure PubSub is started
# In k8s, this will notify all connected clients about the new deployment
_ =
if System.get_env("DEPLOYED_AT") do
Task.start(fn ->
# Wait for application to start
Process.sleep(10_000)
try do
require Logger
_ = Aprsme.DeploymentNotifier.notify_deployment(deployed_at)
Logger.info("Deployment notification sent for timestamp: #{deployed_at}")
rescue
error ->
require Logger
Logger.warning("Failed to send deployment notification: #{inspect(error)}")
end
end)
end
deployed_at
end
@doc """
Get the deployment timestamp.
"""
def deployed_at do
case Application.get_env(:aprsme, :deployed_at) do
nil ->
# If not initialized yet, initialize now
timestamp = DateTime.utc_now()
Application.put_env(:aprsme, :deployed_at, timestamp)
timestamp
timestamp ->
timestamp
end
end
defp run_migrations_with_timeout(timeout) do
require Logger
Logger.info("Running migrations with timeout: #{timeout}ms")
# Run with extended timeout configuration
_repo_config =
Aprsme.Repo.config()
|> Keyword.put(:timeout, timeout)
|> Keyword.put(:pool_timeout, 60_000)
{:ok, _, _} =
Ecto.Migrator.with_repo(
Aprsme.Repo,
fn repo ->
timeout_seconds = div(timeout, 1000)
_ = SQL.query!(repo, "SET statement_timeout = '#{timeout_seconds}s'", [])
Ecto.Migrator.run(repo, :up, all: true)
end,
timeout: timeout
)
end
end