fix: prevent deadlocks by detecting existing transactions

This commit is contained in:
Moritz 2025-12-15 12:32:05 +01:00
parent ba6b81e155
commit b81ed99571
Signed by: moritz
GPG key ID: 1020A035E5DD0824
2 changed files with 46 additions and 25 deletions

View file

@ -763,7 +763,7 @@ defmodule Mv.Membership.Member do
end end
# Regenerates cycles with new type/amount # Regenerates cycles with new type/amount
# CycleGenerator uses its own transaction with advisory lock # CycleGenerator detects if already in transaction and uses advisory lock accordingly
defp regenerate_cycles(member_id) do defp regenerate_cycles(member_id) do
case Mv.MembershipFees.CycleGenerator.generate_cycles_for_member(member_id) do case Mv.MembershipFees.CycleGenerator.generate_cycles_for_member(member_id) do
{:ok, _cycles} -> :ok {:ok, _cycles} -> :ok

View file

@ -188,36 +188,57 @@ defmodule Mv.MembershipFees.CycleGenerator do
# Convert UUID to integer for advisory lock (use hash) # Convert UUID to integer for advisory lock (use hash)
lock_key = :erlang.phash2(member_id) lock_key = :erlang.phash2(member_id)
result = # Check if we're already in a transaction (e.g., called from Ash action)
Repo.transaction(fn -> if Repo.in_transaction?() do
# Acquire advisory lock for this transaction # Already in transaction: use advisory lock directly without starting new transaction
Ecto.Adapters.SQL.query!(Repo, "SELECT pg_advisory_xact_lock($1)", [lock_key]) # This prevents nested transactions which can cause deadlocks
Ecto.Adapters.SQL.query!(Repo, "SELECT pg_advisory_xact_lock($1)", [lock_key])
case fun.() do case fun.() do
{:ok, result, notifications} when is_list(notifications) -> {:ok, result, notifications} when is_list(notifications) ->
# Return result and notifications separately # Notifications will be sent after the outer transaction commits
{result, notifications} # Return in same format as non-transaction case for consistency
{:ok, result}
{:ok, result} -> {:ok, result} ->
# Handle case where no notifications were returned (backward compatibility) {:ok, result}
{result, []}
{:error, reason} -> {:error, reason} ->
Repo.rollback(reason) {:error, reason}
end end
end) else
# Not in transaction: start new transaction with advisory lock
result =
Repo.transaction(fn ->
# Acquire advisory lock for this transaction
Ecto.Adapters.SQL.query!(Repo, "SELECT pg_advisory_xact_lock($1)", [lock_key])
# Extract result and notifications, send notifications after transaction case fun.() do
case result do {:ok, result, notifications} when is_list(notifications) ->
{:ok, {cycles, notifications}} -> # Return result and notifications separately
if Enum.any?(notifications) do {result, notifications}
Ash.Notifier.notify(notifications)
end
{:ok, cycles} {:ok, result} ->
# Handle case where no notifications were returned (backward compatibility)
{result, []}
{:error, reason} -> {:error, reason} ->
{:error, reason} Repo.rollback(reason)
end
end)
# Extract result and notifications, send notifications after transaction
case result do
{:ok, {cycles, notifications}} ->
if Enum.any?(notifications) do
Ash.Notifier.notify(notifications)
end
{:ok, cycles}
{:error, reason} ->
{:error, reason}
end
end end
end end