aprs.me/lib/aprsme/release.ex
Graham McIntire 478e7b1936
Add deployment notification system to update timestamp in LiveViews
When a new deployment occurs in k8s, all connected LiveViews now receive
a notification to update their deployment timestamp display.

Changes:
- Added DeploymentNotifier GenServer that broadcasts deployment events
- LiveViews subscribe to "deployment_events" PubSub topic
- When DEPLOYED_AT env var is set (k8s deployments), a notification is
  sent after app startup
- LiveViews handle the deployment event and update their timestamp

This ensures that when a new version is deployed, users see the
deployment timestamp update in real-time without needing to refresh
their browser.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-28 13:19:38 -05:00

165 lines
4.7 KiB
Elixir

defmodule Aprsme.Release do
@moduledoc """
Used for executing DB release tasks when run in production without Mix
installed.
"""
@app :aprsme
def migrate do
require Logger
# Initialize deployment timestamp first
deployed_at = init()
Logger.info("Deployment timestamp: #{deployed_at}")
# Gettext translations are automatically compiled during Mix compilation
# Skip database creation when using PgBouncer
# The database should already exist
if System.get_env("SKIP_DB_CREATE") != "true" do
create_database()
end
# Run migrations with distributed lock
cluster_enabled = Application.get_env(:aprsme, :cluster_enabled, false)
# Configure longer timeouts for migration operations
# 2 hours default
migration_timeout = String.to_integer(System.get_env("MIGRATION_TIMEOUT", "7200000"))
if cluster_enabled do
Logger.info("Running migrations with distributed lock...")
# Ensure repo is started for advisory lock with extended timeout
repo_config =
Aprsme.Repo.config()
|> Keyword.put(:timeout, migration_timeout)
|> Keyword.put(:pool_timeout, 60_000)
{:ok, _} = Aprsme.Repo.start_link(repo_config)
Aprsme.MigrationLock.with_lock(Aprsme.Repo, fn ->
run_migrations_with_timeout(migration_timeout)
end)
else
Logger.info("Running migrations without lock (single node)...")
run_migrations_with_timeout(migration_timeout)
end
end
defp create_database do
require Logger
case Aprsme.Repo.__adapter__().storage_up(Aprsme.Repo.config()) do
:ok ->
Logger.info("Database created successfully")
{:error, :already_up} ->
Logger.info("Database already exists")
{:error, error} ->
Logger.error("Failed to create database: #{inspect(error)}")
raise "Database creation failed: #{inspect(error)}"
end
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
defp load_app do
Application.load(@app)
end
@doc """
Initialize release-specific configuration.
This is called during application startup.
"""
def init do
# Check if deployment timestamp is provided via environment variable
# This allows k8s deployments to set a persistent timestamp
deployed_at =
case System.get_env("DEPLOYED_AT") do
nil ->
# Fallback to current time for local development or non-k8s deployments
DateTime.utc_now()
timestamp_str ->
# Parse the ISO8601 timestamp from environment variable
case DateTime.from_iso8601(timestamp_str) do
{:ok, datetime, _offset} ->
datetime
{:error, _} ->
require Logger
Logger.warning("Invalid DEPLOYED_AT timestamp: #{timestamp_str}, using current time")
DateTime.utc_now()
end
end
# Store in application config
Application.put_env(:aprsme, :deployed_at, deployed_at)
# Notify about deployment after a short delay to ensure PubSub is started
# In k8s, this will notify all connected clients about the new deployment
if System.get_env("DEPLOYED_AT") do
spawn(fn ->
# Wait for application to start
Process.sleep(10_000)
try do
require Logger
Aprsme.DeploymentNotifier.notify_deployment(deployed_at)
Logger.info("Deployment notification sent for timestamp: #{deployed_at}")
rescue
error ->
require Logger
Logger.warning("Failed to send deployment notification: #{inspect(error)}")
end
end)
end
deployed_at
end
@doc """
Get the deployment timestamp.
"""
def deployed_at do
case Application.get_env(:aprsme, :deployed_at) do
nil ->
# If not initialized yet, initialize now
timestamp = DateTime.utc_now()
Application.put_env(:aprsme, :deployed_at, timestamp)
timestamp
timestamp ->
timestamp
end
end
defp run_migrations_with_timeout(timeout) do
require Logger
Logger.info("Running migrations with timeout: #{timeout}ms")
# Run with extended timeout configuration
_repo_config =
Aprsme.Repo.config()
|> Keyword.put(:timeout, timeout)
|> Keyword.put(:pool_timeout, 60_000)
{:ok, _, _} =
Ecto.Migrator.with_repo(
Aprsme.Repo,
fn repo ->
# Set session-level timeout for this connection
Ecto.Adapters.SQL.query!(repo, "SET statement_timeout = '#{div(timeout, 1000)}s'")
Ecto.Migrator.run(repo, :up, all: true)
end,
timeout: timeout
)
end
end