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
|
||||
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
|
||||
Logger.info("Running migrations with distributed lock...")
|
||||
# Ensure repo is started for advisory lock
|
||||
{:ok, _} = Aprsme.Repo.start_link()
|
||||
# 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 ->
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(Aprsme.Repo, &Ecto.Migrator.run(&1, :up, all: true))
|
||||
run_migrations_with_timeout(migration_timeout)
|
||||
end)
|
||||
else
|
||||
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
|
||||
|
||||
|
|
@ -82,6 +89,23 @@ defmodule Aprsme.Release do
|
|||
Application.get_env(:aprsme, :deployed_at) || DateTime.utc_now()
|
||||
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
|
||||
case File.read("/app/deployed_at.txt") do
|
||||
{:ok, timestamp} ->
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@ defmodule Aprsme.Repo.Migrations.AddOptimizedIndexes do
|
|||
@disable_ddl_transaction true
|
||||
@disable_migration_lock true
|
||||
|
||||
# Increase timeout for large table operations
|
||||
@migration_timeout :timer.minutes(30)
|
||||
|
||||
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
|
||||
# This replaces the need for upper() in queries
|
||||
execute """
|
||||
|
|
@ -54,25 +60,32 @@ defmodule Aprsme.Repo.Migrations.AddOptimizedIndexes do
|
|||
WHERE lat IS NOT NULL AND lon IS NOT NULL
|
||||
"""
|
||||
|
||||
# 7. Add has_weather computed column for better query performance
|
||||
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
|
||||
"""
|
||||
# 7. Skip the generated column for now - it requires full table rewrite
|
||||
# Will be added in a separate migration with better handling
|
||||
# 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
|
||||
# """
|
||||
|
||||
# 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 """
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_packets_has_weather
|
||||
ON packets (has_weather, sender, received_at DESC)
|
||||
WHERE has_weather = true
|
||||
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)
|
||||
"""
|
||||
|
||||
# 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
|
||||
execute "ANALYZE packets;"
|
||||
|
||||
# Reset statement timeout
|
||||
execute "RESET statement_timeout;"
|
||||
execute "RESET lock_timeout;"
|
||||
end
|
||||
|
||||
def down do
|
||||
# Drop indexes in reverse order
|
||||
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_has_weather;"
|
||||
execute "ALTER TABLE packets DROP COLUMN IF EXISTS has_weather;"
|
||||
execute "DROP INDEX IF EXISTS idx_packets_has_weather_functional;"
|
||||
# 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_weather;"
|
||||
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