magnusvk/counter_culture

after_destroy callbacks sometimes not called with execute_after_commit and transaction

Closed this issue · 4 comments

Hello 👋

I've recently started using this gem's execute_after_commit feature to successfully fix MySQL LockWaitTimeout issues, but now I've stumbled upon a nasty bug. In some cases, the counter is not updated after destroy with execute_after_commit.

Let's say we have:

class Graph < ApplicationRecord
  has_many :graph_nodes, class_name: 'Node', primary_key: 'root_id'
end
class Node < ApplicationRecord
  belongs_to :graph,  optional: true, foreign_key: 'graph_id', primary_key: 'root_id'
  counter_culture :graph, execute_after_commit: true # column is 'nodes_count'
end

And then we have a transaction where an update before destroy is happening:

ActiveRecord::Base.transaction do
  Node.last.touch
  Node.last.destroy
end
# => no decrement query

ActiveRecord::Base.transaction do
  Node.last.touch
  Node.last.destroy
  Node.first.destroy
end
# => only the last destroy will trigger one decrement query

Works well without execute_after_commit: true or by changing the code (not possible in my case) to:

ActiveRecord::Base.transaction do
  node = Node.last
  node.touch
  node.destroy
end

I suspect the update request somehow prevents the after_destroy callback.

Hey @marinfr thanks for reporting this. I did some digging, see #316. However, my take-away from that work is that the after_commit hook simply is not called at all for your issue here. So at first glance, this looks like more of a Rails problem than a counter_culture issue. Can you take a look at the output in #316 and let me know what you think?

@magnusvk, thanks for looking into this!

I've tried to reproduce this behaviour with basic Rails by defining an after_commit action that just prints AFTER_COMMIT. It's for a different model that doesn't have anything to do with counter columns. Trying both syntaxes of updating+destroying, AFTER_COMMIT was always and only printed once at the end:

   (2.2ms)  COMMIT
"AFTER COMMIT"

Does this mean that after_commit not being called is not the issue here? It looks like it's the execute_after_commit logic in this or the after_commit_action gems.

Ok, you might be right!

I've defined this in the model described in the issue:

after_commit :test_after_commit

def test_after_commit
  actions = {}

  actions[:create]  = transaction_include_any_action?([:create])  || nil
  actions[:update]  = transaction_include_any_action?([:update])  || nil
  actions[:destroy] = transaction_include_any_action?([:destroy]) || nil

  p "AFTER COMMIT EXECUTED FOR #{actions.compact.keys.join(", ")}"
end

The output with the first approach:

   (2.4ms)  COMMIT
"AFTER COMMIT EXECUTED FOR update"

The output with the second approach:

   (2.0ms)  COMMIT
"AFTER COMMIT EXECUTED FOR destroy"

Feel free to close this issue as it looks like it's intended for the Rails team.
Thanks!

Bummer, I personally don’t think that’s the right behavior, but also wouldn’t know what to do to fix it!