Skip to content

RavenUser.after_delete calls user.save() during the parent User's deletion, re-entering the full on_update hook cascade #2240

Description

@miao-Q777

Environment

  • Frappe v16.24.3 (identical flow confirmed on version-16 HEAD 16.32.0)
  • Raven co-installed with frappe/gameplan (both auto-provision per-User records)

Summary

When a User is deleted, raven's remove_user_from_raven hook deletes the Raven User record, and RavenUser.after_delete then calls user.save(). That nested save re-enters the complete post-save lifecycle (run_post_save_methodsrun_method("on_update") → all on_update doc_events) while the outer document is still mid-deletion. With other apps installing reactive on_update hooks, this composition can make User deletion fail entirely.

Reproduction (frappe v16 + gameplan + raven)

  1. Create a User — Raven User and GP User Profile records are auto-created by their respective hooks
  2. Delete the User
  3. frappe.exceptions.LinkExistsError: Cannot delete or cancel because User ... is linked with GP User Profile ...

The GP User Profile was already deleted by gameplan's own on_trash hook earlier in the flow; it is re-created mid-flow (the profile carries a fresh creation timestamp after the failed delete — this is in-flow recreation, not a rollback artifact). Probe-captured chain:

delete_doc.py:165   doc.run_method("on_trash")
  → gameplan delete_user_profile         # deletes GP User Profile ✓
  → raven remove_user_from_raven
    → RavenUser.delete()
      → RavenUser.after_delete
        → user.save()                     # ← re-enters on_update
          → gp_user_profile.py on_user_update
            → create_user_profile(doc)    # ← GP User Profile resurrected
delete_doc.py:172   check_if_doc_is_linked # ← now fails: profile exists again

Why this is a lifecycle re-entry issue (not "save is illegal in delete hooks")

Frappe core itself performs nested saves inside User.on_trash (note.save(), invite_doc.save()), so nested saves are an established pattern. The problem is amplification: each nested save() re-enters run_post_save_methods() and fires every installed app's on_update doc_events, letting auto-provisioning hooks in other apps react to a document that is being deleted.

Suggested fix

In RavenUser.after_delete, persist the role removal without re-entering the full save lifecycle — e.g. delete the Has Role child row directly (frappe.db.delete("Has Role", {"role": "Raven User", "parent": self.user, "parenttype": "User"})) + frappe.clear_cache(user=self.user), instead of user.remove_roles(...); user.save().

Verification

Deleting a User that has a Raven User record should succeed with no error; Raven User record, role, and dependent records should all be cleaned up.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions