defmodule Towerops.Application do @moduledoc """ OTP Application module for Towerops. Supervises the Repo, PubSub, Telemetry, Endpoint, and monitoring workers. """ use Application # Capture the build timestamp at compile time (fallback for development) @build_timestamp DateTime.utc_now() @impl true def start(_type, _args) do # Run migrations on startup (Ecto handles locking for concurrent runs) _ = if Application.get_env(:towerops, Towerops.Repo)[:database] != "towerops_test" do Towerops.Release.migrate() end topologies = Application.get_env(:libcluster, :topologies, []) children = [ {Cluster.Supervisor, [topologies, [name: Towerops.ClusterSupervisor]]}, ToweropsWeb.Telemetry, Towerops.Repo, {Oban, Application.fetch_env!(:towerops, Oban)}, {DNSCluster, query: Application.get_env(:towerops, :dns_cluster_query) || :ignore}, pubsub_spec(), # Load YAML profiles into ETS cache Towerops.Profiles.YamlProfiles, # SnmpKit services for SNMP operations SnmpKit.SnmpMgr.Config, SnmpKit.SnmpMgr.MIB ] ++ dev_only_workers() ++ background_workers() ++ [ # Start to serve requests, typically the last entry ToweropsWeb.Endpoint ] # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: Towerops.Supervisor] Supervisor.start_link(children, opts) end # Tell Phoenix to update the endpoint configuration # whenever the application is updated. @impl true def config_change(changed, _new, removed) do ToweropsWeb.Endpoint.config_change(changed, removed) :ok end # Configure background workers - don't start in test environment defp background_workers do if Application.get_env(:towerops, :env) == :test do [] else [ # Start event logger (subscribes to PubSub) Towerops.Devices.EventLogger # Note: NeighborCleanupWorker, StaleAgentWorker, AgentLatencyEvaluator, # and JobHealthCheckWorker are now Oban Cron jobs (see config/runtime.exs) ] end end # Configure dev-only workers - profile watcher for auto-reload defp dev_only_workers do if Application.get_env(:towerops, :env) == :dev do [ # Watch YAML profiles for changes and auto-reload Towerops.Profiles.ProfileWatcher ] else [] end end # Returns PubSub spec - uses Redis in production/clustered environments, # falls back to default PG2 adapter for Mix tasks and development defp pubsub_spec do redis_config = Application.get_env(:towerops, :redis, []) node_name = node() # Use Redis adapter only when: # 1. Redis config exists with host # 2. Node has a name (not nonode@nohost) use_redis? = Keyword.has_key?(redis_config, :host) && node_name != :nonode@nohost if use_redis? do base_config = [name: Towerops.PubSub, adapter: Phoenix.PubSub.Redis] redis_opts = redis_pubsub_config() {Phoenix.PubSub, Keyword.merge(base_config, redis_opts)} else # Default PG2 adapter for Mix tasks and development {Phoenix.PubSub, name: Towerops.PubSub} end end defp redis_pubsub_config do redis_config = Application.get_env(:towerops, :redis, []) # Base configuration for Phoenix.PubSub.Redis base_opts = [ host: Keyword.get(redis_config, :host, "localhost"), port: Keyword.get(redis_config, :port, 6379), node_name: node() ] # Add password if configured (read from application config set by runtime.exs) base_opts = case Keyword.get(redis_config, :password) do nil -> base_opts "" -> base_opts password -> Keyword.put(base_opts, :password, password) end # Add resilience options for better reconnection handling resilience_opts = [ # Redix connection options for better resilience socket_opts: [ # TCP keepalive to detect dead connections keepalive: true ], # Connection timeout (5 seconds) timeout: 5_000, # Backoff settings for reconnection backoff_initial: 500, backoff_max: 30_000, # Don't exit process on disconnection - let PubSub handle it exit_on_disconnection: false, # Sync connect to fail fast if Redis unavailable at startup sync_connect: false ] Keyword.merge(base_opts, resilience_opts) end @doc """ Returns the deployment timestamp. In production (Kubernetes), this reads from the DEPLOY_TIMESTAMP environment variable which is automatically set by GitLab CI during deployment. Falls back to compile time in development. The timestamp is set by GitLab CI using: ```bash kubectl set env deployment/towerops DEPLOY_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") -n towerops ``` This ensures all pods in the deployment show the same deployment time, regardless of when individual pods were created or restarted. """ @spec build_timestamp() :: DateTime.t() def build_timestamp do case System.get_env("DEPLOY_TIMESTAMP") do nil -> # Development/fallback: use compile time @build_timestamp timestamp_string -> # Production: parse from environment variable case DateTime.from_iso8601(timestamp_string) do {:ok, datetime, _offset} -> datetime {:error, _} -> @build_timestamp end end end end