fix(repo): make member/user foreign keys deferrable to avoid create_member deadlock

Concurrent create_member transactions took FK FOR KEY SHARE (MultiXact) locks
on shared rows across members/users/membership_fee_types and could form a
cross-transaction cycle, producing intermittent PostgreSQL deadlocks (40P01)
under load. Making the three foreign keys DEFERRABLE INITIALLY DEFERRED moves
the check to commit time and breaks the cycle, without weakening integrity
(NOT NULL and ON DELETE RESTRICT are unaffected).
This commit is contained in:
Moritz 2026-06-16 17:52:51 +02:00
parent cb54c2c46e
commit ef94d2ef10

View file

@ -0,0 +1,32 @@
defmodule Mv.Repo.Migrations.MakeMemberUserFksDeferrable do
@moduledoc """
Makes the members/users foreign keys DEFERRABLE INITIALLY DEFERRED.
Concurrent `create_member` transactions take FK `FOR KEY SHARE` (MultiXact)
locks on these foreign keys at statement time and can form a cross-transaction
lock cycle, producing a PostgreSQL `deadlock_detected` (40P01). Deferring the
FK checks to commit time breaks the cycle.
Constraint deferrability is not tracked by AshPostgres resource snapshots, so
this is a hand-written migration with raw `execute/2`. Do not regenerate it
via `mix ash_postgres.generate_migrations`.
"""
use Ecto.Migration
def change do
execute(
"ALTER TABLE users ALTER CONSTRAINT users_member_id_fkey DEFERRABLE INITIALLY DEFERRED",
"ALTER TABLE users ALTER CONSTRAINT users_member_id_fkey NOT DEFERRABLE"
)
execute(
"ALTER TABLE users ALTER CONSTRAINT users_role_id_fkey DEFERRABLE INITIALLY DEFERRED",
"ALTER TABLE users ALTER CONSTRAINT users_role_id_fkey NOT DEFERRABLE"
)
execute(
"ALTER TABLE members ALTER CONSTRAINT members_membership_fee_type_id_fkey DEFERRABLE INITIALLY DEFERRED",
"ALTER TABLE members ALTER CONSTRAINT members_membership_fee_type_id_fkey NOT DEFERRABLE"
)
end
end