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 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 public ETS tables so callers can use concurrent reads and writes directly. _ = :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