Fix migration timeout issues for large tables
- Add statement_timeout and lock_timeout settings to migration - Remove generated column addition that requires full table rewrite - Replace with functional index to avoid table rewrite - Create separate migration for generated column with 2-hour timeout - Add MIGRATION_TIMEOUT environment variable support (default 2 hours) - Configure extended timeouts in Release.migrate for large operations The main migration now uses only CONCURRENTLY operations that don't lock the table. The generated column addition is deferred to a separate migration that can be run during a maintenance window. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9189579a02
commit
4f7aabbce9
3 changed files with 142 additions and 23 deletions
|
|
@ -22,17 +22,24 @@ defmodule Aprsme.Release do
|
||||||
# Run migrations with distributed lock
|
# Run migrations with distributed lock
|
||||||
cluster_enabled = Application.get_env(:aprsme, :cluster_enabled, false)
|
cluster_enabled = Application.get_env(:aprsme, :cluster_enabled, false)
|
||||||
|
|
||||||
|
# Configure longer timeouts for migration operations
|
||||||
|
migration_timeout = String.to_integer(System.get_env("MIGRATION_TIMEOUT", "7200000")) # 2 hours default
|
||||||
|
|
||||||
if cluster_enabled do
|
if cluster_enabled do
|
||||||
Logger.info("Running migrations with distributed lock...")
|
Logger.info("Running migrations with distributed lock...")
|
||||||
# Ensure repo is started for advisory lock
|
# Ensure repo is started for advisory lock with extended timeout
|
||||||
{:ok, _} = Aprsme.Repo.start_link()
|
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 ->
|
Aprsme.MigrationLock.with_lock(Aprsme.Repo, fn ->
|
||||||
{:ok, _, _} = Ecto.Migrator.with_repo(Aprsme.Repo, &Ecto.Migrator.run(&1, :up, all: true))
|
run_migrations_with_timeout(migration_timeout)
|
||||||
end)
|
end)
|
||||||
else
|
else
|
||||||
Logger.info("Running migrations without lock (single node)...")
|
Logger.info("Running migrations without lock (single node)...")
|
||||||
{:ok, _, _} = Ecto.Migrator.with_repo(Aprsme.Repo, &Ecto.Migrator.run(&1, :up, all: true))
|
run_migrations_with_timeout(migration_timeout)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -82,6 +89,23 @@ defmodule Aprsme.Release do
|
||||||
Application.get_env(:aprsme, :deployed_at) || DateTime.utc_now()
|
Application.get_env(:aprsme, :deployed_at) || DateTime.utc_now()
|
||||||
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
|
||||||
|
|
||||||
defp read_deployment_timestamp do
|
defp read_deployment_timestamp do
|
||||||
case File.read("/app/deployed_at.txt") do
|
case File.read("/app/deployed_at.txt") do
|
||||||
{:ok, timestamp} ->
|
{:ok, timestamp} ->
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,13 @@ defmodule Aprsme.Repo.Migrations.AddOptimizedIndexes do
|
||||||
@disable_ddl_transaction true
|
@disable_ddl_transaction true
|
||||||
@disable_migration_lock true
|
@disable_migration_lock true
|
||||||
|
|
||||||
|
# Increase timeout for large table operations
|
||||||
|
@migration_timeout :timer.minutes(30)
|
||||||
|
|
||||||
def up do
|
def up do
|
||||||
|
# Set statement timeout for this migration session
|
||||||
|
execute "SET statement_timeout = '30min';"
|
||||||
|
execute "SET lock_timeout = '10s';"
|
||||||
# 1. Functional index for case-insensitive callsign searches
|
# 1. Functional index for case-insensitive callsign searches
|
||||||
# This replaces the need for upper() in queries
|
# This replaces the need for upper() in queries
|
||||||
execute """
|
execute """
|
||||||
|
|
@ -54,25 +60,32 @@ defmodule Aprsme.Repo.Migrations.AddOptimizedIndexes do
|
||||||
WHERE lat IS NOT NULL AND lon IS NOT NULL
|
WHERE lat IS NOT NULL AND lon IS NOT NULL
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 7. Add has_weather computed column for better query performance
|
# 7. Skip the generated column for now - it requires full table rewrite
|
||||||
execute """
|
# Will be added in a separate migration with better handling
|
||||||
ALTER TABLE packets
|
# execute """
|
||||||
ADD COLUMN IF NOT EXISTS has_weather boolean
|
# ALTER TABLE packets
|
||||||
GENERATED ALWAYS AS (
|
# ADD COLUMN IF NOT EXISTS has_weather boolean
|
||||||
temperature IS NOT NULL
|
# GENERATED ALWAYS AS (
|
||||||
OR humidity IS NOT NULL
|
# temperature IS NOT NULL
|
||||||
OR pressure IS NOT NULL
|
# OR humidity IS NOT NULL
|
||||||
OR wind_speed IS NOT NULL
|
# OR pressure IS NOT NULL
|
||||||
OR wind_direction IS NOT NULL
|
# OR wind_speed IS NOT NULL
|
||||||
OR rain_1h IS NOT NULL
|
# OR wind_direction IS NOT NULL
|
||||||
) STORED
|
# OR rain_1h IS NOT NULL
|
||||||
"""
|
# ) STORED
|
||||||
|
# """
|
||||||
|
|
||||||
# 8. Index on the new has_weather column
|
# 8. Create functional index instead of relying on generated column
|
||||||
|
# This avoids the need for a full table rewrite
|
||||||
execute """
|
execute """
|
||||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_packets_has_weather
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_packets_has_weather_functional
|
||||||
ON packets (has_weather, sender, received_at DESC)
|
ON packets (sender, received_at DESC)
|
||||||
WHERE has_weather = true
|
WHERE (temperature IS NOT NULL
|
||||||
|
OR humidity IS NOT NULL
|
||||||
|
OR pressure IS NOT NULL
|
||||||
|
OR wind_speed IS NOT NULL
|
||||||
|
OR wind_direction IS NOT NULL
|
||||||
|
OR rain_1h IS NOT NULL)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 9. Composite index for region-based queries with position
|
# 9. Composite index for region-based queries with position
|
||||||
|
|
@ -90,14 +103,18 @@ defmodule Aprsme.Repo.Migrations.AddOptimizedIndexes do
|
||||||
|
|
||||||
# Analyze the table to update statistics after adding indexes
|
# Analyze the table to update statistics after adding indexes
|
||||||
execute "ANALYZE packets;"
|
execute "ANALYZE packets;"
|
||||||
|
|
||||||
|
# Reset statement timeout
|
||||||
|
execute "RESET statement_timeout;"
|
||||||
|
execute "RESET lock_timeout;"
|
||||||
end
|
end
|
||||||
|
|
||||||
def down do
|
def down do
|
||||||
# Drop indexes in reverse order
|
# Drop indexes in reverse order
|
||||||
execute "DROP INDEX IF EXISTS idx_packets_datatype_sender_time;"
|
execute "DROP INDEX IF EXISTS idx_packets_datatype_sender_time;"
|
||||||
execute "DROP INDEX IF EXISTS idx_packets_region_position_time;"
|
execute "DROP INDEX IF EXISTS idx_packets_region_position_time;"
|
||||||
execute "DROP INDEX IF EXISTS idx_packets_has_weather;"
|
execute "DROP INDEX IF EXISTS idx_packets_has_weather_functional;"
|
||||||
execute "ALTER TABLE packets DROP COLUMN IF EXISTS has_weather;"
|
# execute "ALTER TABLE packets DROP COLUMN IF EXISTS has_weather;"
|
||||||
execute "DROP INDEX IF EXISTS idx_packets_sender_position;"
|
execute "DROP INDEX IF EXISTS idx_packets_sender_position;"
|
||||||
execute "DROP INDEX IF EXISTS idx_packets_weather;"
|
execute "DROP INDEX IF EXISTS idx_packets_weather;"
|
||||||
execute "DROP INDEX IF EXISTS idx_packets_base_callsign_time;"
|
execute "DROP INDEX IF EXISTS idx_packets_base_callsign_time;"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
defmodule Aprsme.Repo.Migrations.AddHasWeatherColumn do
|
||||||
|
use Ecto.Migration
|
||||||
|
@disable_ddl_transaction true
|
||||||
|
@disable_migration_lock true
|
||||||
|
|
||||||
|
def up do
|
||||||
|
# This migration adds a generated column which requires a full table rewrite
|
||||||
|
# It should be run during a maintenance window or when load is low
|
||||||
|
|
||||||
|
# Set aggressive timeout for very large table
|
||||||
|
execute "SET statement_timeout = '2h';"
|
||||||
|
execute "SET lock_timeout = '30s';"
|
||||||
|
|
||||||
|
# Log progress
|
||||||
|
execute "SELECT NOW() AS start_time, 'Starting has_weather column addition' AS status;"
|
||||||
|
|
||||||
|
# Add the column - this will rewrite the entire table
|
||||||
|
# Consider running this during off-peak hours
|
||||||
|
execute """
|
||||||
|
ALTER TABLE packets
|
||||||
|
ADD COLUMN IF NOT EXISTS has_weather boolean
|
||||||
|
GENERATED ALWAYS AS (
|
||||||
|
temperature IS NOT NULL
|
||||||
|
OR humidity IS NOT NULL
|
||||||
|
OR pressure IS NOT NULL
|
||||||
|
OR wind_speed IS NOT NULL
|
||||||
|
OR wind_direction IS NOT NULL
|
||||||
|
OR rain_1h IS NOT NULL
|
||||||
|
) STORED
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create index on the new column
|
||||||
|
execute """
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_packets_has_weather
|
||||||
|
ON packets (has_weather, sender, received_at DESC)
|
||||||
|
WHERE has_weather = true
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Drop the functional index if it exists (replaced by column-based index)
|
||||||
|
execute "DROP INDEX IF EXISTS idx_packets_has_weather_functional;"
|
||||||
|
|
||||||
|
# Update statistics
|
||||||
|
execute "ANALYZE packets (has_weather);"
|
||||||
|
|
||||||
|
# Reset timeouts
|
||||||
|
execute "RESET statement_timeout;"
|
||||||
|
execute "RESET lock_timeout;"
|
||||||
|
|
||||||
|
# Log completion
|
||||||
|
execute "SELECT NOW() AS end_time, 'Completed has_weather column addition' AS status;"
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute "SET statement_timeout = '2h';"
|
||||||
|
execute "SET lock_timeout = '30s';"
|
||||||
|
|
||||||
|
# Drop the index first
|
||||||
|
execute "DROP INDEX IF EXISTS idx_packets_has_weather;"
|
||||||
|
|
||||||
|
# Drop the column (this will also rewrite the table)
|
||||||
|
execute "ALTER TABLE packets DROP COLUMN IF EXISTS has_weather;"
|
||||||
|
|
||||||
|
# Recreate the functional index
|
||||||
|
execute """
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_packets_has_weather_functional
|
||||||
|
ON packets (sender, received_at DESC)
|
||||||
|
WHERE (temperature IS NOT NULL
|
||||||
|
OR humidity IS NOT NULL
|
||||||
|
OR pressure IS NOT NULL
|
||||||
|
OR wind_speed IS NOT NULL
|
||||||
|
OR wind_direction IS NOT NULL
|
||||||
|
OR rain_1h IS NOT NULL)
|
||||||
|
"""
|
||||||
|
|
||||||
|
execute "RESET statement_timeout;"
|
||||||
|
execute "RESET lock_timeout;"
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Reference in a new issue