From e86ec8c6ce8888c1c71791d46aa92904ade21cff Mon Sep 17 00:00:00 2001 From: Pratik Khadloya Date: Fri, 8 May 2026 00:33:42 +0530 Subject: [PATCH] fix: prevent EnforcerServer crash on duplicate casbin_rule insert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The casbin_rule table has a unique index casbin_rule_content_unique on (ptype, v0..v6) NULLS NOT DISTINCT. A bare repo.insert raises Ecto.ConstraintError when a duplicate rule is inserted — e.g. two nodes racing after an EnforcerServer restart, or in-memory and DB state diverging. That exception propagates out of handle_call and crashes the EnforcerServer GenServer, a critical process that is slow to recover. Patched add_policy/2 and insert_policy/3 (used by save_policies/2) to use on_conflict: :nothing, which silently skips the duplicate and returns {:ok, struct}. No data is lost — the rule already exists in the DB. Fixes Sentry issue BACKEND-2H2. Co-Authored-By: Claude Sonnet 4.6 --- lib/casbin/persist/ecto_adapter.ex | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/casbin/persist/ecto_adapter.ex b/lib/casbin/persist/ecto_adapter.ex index 3b4d8a0..7d3edac 100644 --- a/lib/casbin/persist/ecto_adapter.ex +++ b/lib/casbin/persist/ecto_adapter.ex @@ -302,7 +302,11 @@ defmodule Casbin.Persist.EctoAdapter do repo = Casbin.Persist.EctoAdapter.get_repo(adapter) changeset = CasbinRule.create_changeset(policy) - case repo.insert(changeset) do + # on_conflict: :nothing prevents Ecto.ConstraintError when a duplicate rule is + # inserted concurrently (e.g., two nodes racing, or memory/DB state diverging after + # an EnforcerServer restart). Without this, the bare insert raises and crashes the + # EnforcerServer GenServer — a critical process that takes time to restart. + case repo.insert(changeset, on_conflict: :nothing) do {:ok, _casbin} -> {:ok, adapter} {:error, changeset} -> {:error, changeset.errors} end @@ -375,7 +379,9 @@ defmodule Casbin.Persist.EctoAdapter do defp insert_policy(repo, adapter, policy) do changeset = CasbinRule.create_changeset(policy) - case repo.insert(changeset) do + # on_conflict: :nothing — same rationale as add_policy/2: save_policies/2 + # truncates then re-inserts all rules; a race or restart can cause duplicates. + case repo.insert(changeset, on_conflict: :nothing) do {:ok, _casbin} -> adapter {:error, changeset} -> {:error, changeset.errors} end