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.
206 lines
6.5 KiB
Elixir
206 lines
6.5 KiB
Elixir
defmodule Aprsme.Application do
|
|
# See https://hexdocs.pm/elixir/Application.html
|
|
# for more information on OTP Applications
|
|
@moduledoc false
|
|
|
|
use Application
|
|
|
|
# Configure Oban for background jobs
|
|
|
|
@impl true
|
|
def start(_type, _args) do
|
|
# Initialize deployment timestamp
|
|
_ = Aprsme.Release.init()
|
|
|
|
# Run migrations on startup
|
|
migrate()
|
|
|
|
children = [
|
|
# Start the Telemetry supervisor
|
|
AprsmeWeb.Telemetry,
|
|
# Start the Ecto repository
|
|
Aprsme.Repo,
|
|
# Start the PubSub system
|
|
pubsub_config(),
|
|
# Start Redis-based rate limiter and caches (only if Redis is available)
|
|
# Start circuit breaker
|
|
Aprsme.CircuitBreaker,
|
|
# Start regex cache for performance
|
|
Aprsme.RegexCache,
|
|
# Start cache manager
|
|
Aprsme.Cache,
|
|
# Start device cache manager
|
|
Aprsme.DeviceCache,
|
|
# Start broadcast task supervisor for async operations
|
|
Aprsme.BroadcastTaskSupervisor,
|
|
# Start spatial PubSub for viewport-based filtering
|
|
Aprsme.SpatialPubSub,
|
|
# Start global streaming packets PubSub
|
|
Aprsme.StreamingPacketsPubSub,
|
|
# Manage daily partitions for the packets table
|
|
Aprsme.PartitionManager,
|
|
# Start the Endpoint (http/https)
|
|
AprsmeWeb.Endpoint,
|
|
# Start a worker by calling: Aprsme.Worker.start_link(arg)
|
|
# {Aprsme.Worker, arg}
|
|
{Registry, keys: :duplicate, name: Registry.PubSub, partitions: System.schedulers_online()},
|
|
# Start cleanup scheduler for periodic packet cleanup
|
|
Aprsme.CleanupScheduler,
|
|
Aprsme.PostgresNotifier,
|
|
# Start deployment notifier
|
|
Aprsme.DeploymentNotifier,
|
|
# Start the packet processing pipeline
|
|
Aprsme.PacketPipelineSupervisor
|
|
]
|
|
|
|
# Skip partition manager and packet pipeline in test to avoid Sandbox ownership errors
|
|
children =
|
|
if Application.get_env(:aprsme, :env) == :test do
|
|
children
|
|
|> List.delete(Aprsme.PartitionManager)
|
|
|> List.delete(Aprsme.PacketPipelineSupervisor)
|
|
else
|
|
children
|
|
end
|
|
|
|
children = children ++ redis_children()
|
|
|
|
# Add shutdown handlers at the end, after everything else is started
|
|
children =
|
|
children ++
|
|
[
|
|
Aprsme.SignalHandler,
|
|
Aprsme.ShutdownHandler
|
|
]
|
|
|
|
children = maybe_add_cluster_components(children)
|
|
children = maybe_add_is_supervisor(children, Application.get_env(:aprsme, :env))
|
|
# Exq is now started automatically via config, not in supervision tree
|
|
|
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
|
# for other strategies and supported options
|
|
opts = [strategy: :one_for_one, name: Aprsme.Supervisor]
|
|
{:ok, sup} = Supervisor.start_link(children, opts)
|
|
|
|
# Attach error notification telemetry handler
|
|
_ = Aprsme.ErrorNotifier.attach()
|
|
|
|
# Now that the Repo is started, run the refresh in a background task
|
|
# Skip in test environment to avoid DBConnection.OwnershipError
|
|
env = Application.get_env(:aprsme, :env)
|
|
|
|
_ =
|
|
if env != :test do
|
|
Aprsme.BroadcastTaskSupervisor.async_execute(fn ->
|
|
Aprsme.DeviceIdentification.maybe_refresh_devices()
|
|
end)
|
|
end
|
|
|
|
{:ok, sup}
|
|
end
|
|
|
|
# Tell Phoenix to update the endpoint configuration
|
|
# whenever the application is updated.
|
|
@impl true
|
|
def config_change(changed, _new, removed) do
|
|
AprsmeWeb.Endpoint.config_change(changed, removed)
|
|
:ok
|
|
end
|
|
|
|
defp migrate do
|
|
auto_migrate = Application.get_env(:aprsme, :auto_migrate, true)
|
|
cluster_enabled = Application.get_env(:aprsme, :cluster_enabled, false)
|
|
|
|
# In cluster mode, prefer init containers or manual migration
|
|
# to avoid race conditions between nodes
|
|
if auto_migrate and not cluster_enabled do
|
|
do_migrate(true)
|
|
else
|
|
require Logger
|
|
|
|
if cluster_enabled do
|
|
Logger.info("Skipping auto-migration in cluster mode")
|
|
else
|
|
Logger.info("Auto-migration disabled")
|
|
end
|
|
end
|
|
|
|
# Gettext translations are automatically compiled during Mix compilation
|
|
rescue
|
|
error ->
|
|
require Logger
|
|
|
|
Logger.error("Failed to run migrations: #{inspect(error)}")
|
|
# Don't crash the application, just log the error
|
|
:ok
|
|
end
|
|
|
|
defp maybe_add_cluster_components(children) do
|
|
if Application.get_env(:aprsme, :cluster_enabled, false) do
|
|
topologies = Application.get_env(:libcluster, :topologies, [])
|
|
|
|
# Packet receiver for distributed packets on non-leader nodes
|
|
cluster_children =
|
|
if topologies == [] do
|
|
[]
|
|
else
|
|
[
|
|
{Cluster.Supervisor, [topologies, [name: Aprsme.ClusterSupervisor]]},
|
|
Aprsme.DynamicSupervisor,
|
|
Aprsme.Cluster.LeaderElection,
|
|
Aprsme.Cluster.ConnectionManager,
|
|
Aprsme.Cluster.PacketReceiver,
|
|
Aprsme.Cluster.PacketDistributor,
|
|
Aprsme.ConnectionMonitor
|
|
]
|
|
end
|
|
|
|
children ++ cluster_children
|
|
else
|
|
children
|
|
end
|
|
end
|
|
|
|
defp maybe_add_is_supervisor(children, env) do
|
|
disable_connection = Application.get_env(:aprsme, :disable_aprs_connection, false)
|
|
cluster_enabled = Application.get_env(:aprsme, :cluster_enabled, false)
|
|
|
|
# Only add Is.IsSupervisor directly if clustering is disabled
|
|
if env in [:prod, :dev] and not disable_connection and not cluster_enabled do
|
|
children ++ [Aprsme.Is.IsSupervisor]
|
|
else
|
|
children
|
|
end
|
|
end
|
|
|
|
defp do_migrate(true) do
|
|
require Logger
|
|
|
|
Logger.info("Running database migrations...")
|
|
Aprsme.Release.migrate()
|
|
Logger.info("Database migrations completed")
|
|
end
|
|
|
|
defp pubsub_config do
|
|
{Phoenix.PubSub, name: Aprsme.PubSub}
|
|
end
|
|
|
|
defp redis_children do
|
|
require Logger
|
|
|
|
Logger.info("Starting ETS-based caching and rate limiting")
|
|
|
|
# Create ETS tables for caching — use :public so the Cache GenServer (a separate process)
|
|
# can write to these tables; write_concurrency improves throughput under concurrent writes.
|
|
_ = :ets.new(:query_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true])
|
|
_ = :ets.new(:device_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true])
|
|
_ = :ets.new(:symbol_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true])
|
|
_ = :ets.new(:aprsme, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true])
|
|
_ = :ets.insert(:aprsme, {:message_number, 0})
|
|
|
|
[
|
|
# ETS-based rate limiter
|
|
Aprsme.RateLimiter
|
|
]
|
|
end
|
|
end
|