- Delete dead config keys and test file (typo'd aprsme_is_*, vacuous disable test) - Remove duplicate function clauses and identical function pairs - Rename misleading one_hour_ago variable to one_day_ago - Strip stale Oban comment - Remove TestHelpers time function duplicates - Inline Aprsme.Schema module (only 1 caller) - Deduplicate prod esbuild config - Thin SymbolRenderer delegations, inline TimeUtils wrappers - Remove redundant DeploymentNotifier GenServer polling - Replace random fallback in get_callsign_key with sentinel
201 lines
6.7 KiB
Elixir
201 lines
6.7 KiB
Elixir
defmodule Aprsme.Application do
|
|
# See https://hexdocs.pm/elixir/Application.html
|
|
# for more information on OTP Applications
|
|
@moduledoc false
|
|
|
|
use Application
|
|
|
|
require Logger
|
|
|
|
@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 Prometheus metrics exporter
|
|
Aprsme.PromEx,
|
|
# 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 ReplayRegistry for unique replay sessions per user
|
|
{Registry, keys: :unique, name: Aprsme.ReplayRegistry, partitions: System.schedulers_online()},
|
|
# Start cleanup scheduler for periodic packet cleanup
|
|
Aprsme.CleanupScheduler,
|
|
Aprsme.PostgresNotifier,
|
|
# 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
|
|
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 ->
|
|
Logger.error("Failed to run migrations: #{inspect(error)}. Application may be in an inconsistent state.")
|
|
# 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
|
|
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
|
|
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(:weather_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
|