aprs.me/lib/aprsme/application.ex
Graham McIntire adaf9599e2
chore: update dependencies and fix pre-existing issues
Dep upgrades (mix deps.update --all):
- phoenix_live_view 1.1.27 → 1.1.28
- swoosh 1.23.1 → 1.25.0
- bandit 1.10.3 → 1.10.4
- credo 1.7.17 → 1.7.18
- hammer 7.2.0 → 7.3.0
- igniter 0.7.6 → 0.7.9
- and minor: fine, lazy_html, meck, mimerl
- removed stale lock entries: geocalc, gettext_pseudolocalize, gridsquare

Bug fixes surfaced during update:
- ETS cache tables were :protected (owned by Application master), preventing
  the Cache GenServer from writing; change to :public + write_concurrency
  so device/symbol/query caches actually work
- historical_dot_html returned Phoenix.HTML.safe tuple instead of plain string;
  symbol_html is JSON-encoded for JS so it must be binary
- String.slice always returns binary so the || "Unknown error" fallback was
  unreachable dead code (dialyzer guard_fail)
- Supervisor.which_children always returns a list so the _ -> branch in
  database_metrics was unreachable dead code (dialyzer pattern_match_cov)
- Add @type t :: %__MODULE__{} to User schema to resolve unknown_type in user_auth spec
- Extract nested if in leader_election to fix Credo nesting depth violation
- Update .dialyzer_ignore.exs: remove 2 stale entries, add false positives for
  Ecto.Multi/Gettext opaque types and Mix.Task PLT limitations
2026-04-15 14:07:29 -05:00

205 lines
6.4 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