From de0bd8e71b380bbaffded2fed9b0297d1f3564b7 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Fri, 10 Apr 2026 17:32:13 +0530 Subject: [PATCH] fix(perf): disable tags counter cache to prevent label deadlocks (#14021) Label attach/detach against a shared label no longer deadlocks under parallel load. During high-concurrency label writes (for example, a broadcast script attaching a campaign label to many conversations at once), Chatwoot previously hit periodic `ActiveRecord::Deadlocked` errors and tail-latency spikes on the tags table. This PR removes the contention by disabling the `acts-as-taggable-on` counter cache, which Chatwoot never reads. ## Closes Fixes [INF-68](https://linear.app/chatwoot/issue/INF-68) (event 2) ## How to reproduce 1. Seed an account with ~20 conversations and 5 labels. 2. Spawn 20 parallel threads, each calling `conversation.update!(label_list: shared_labels.shuffle)` against different conversations. 3. Observe `ActiveRecord::Deadlocked` exceptions and p99 label-write latency well above 1s. With the counter cache disabled, the deadlock cycle cannot form. ## How this was tested - Ran a 20-thread synthetic load test locally, each thread attaching 5 shared labels (shuffled per request) to different conversations. With the counter cache enabled: 8 deadlocks across 300 attempts, p99 ~2.2s. With the counter cache disabled: zero deadlocks, p99 ~306ms (roughly 85% tail-latency reduction). The `UPDATE tags SET taggings_count = ...` statement disappears from the SQL log entirely. - Verified at boot via `rails runner` that `ActsAsTaggableOn::Tagging.reflect_on_association(:tag).options[:counter_cache]` returns `false` after the initializer runs. The gem wires `belongs_to :tag, counter_cache: ActsAsTaggableOn.tags_counter` at class-load time, so the initializer must sit ahead of the `Tagging` autoload path; this confirms it does. --- config/initializers/acts_as_taggable_on.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 config/initializers/acts_as_taggable_on.rb diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb new file mode 100644 index 000000000..9b0297bfa --- /dev/null +++ b/config/initializers/acts_as_taggable_on.rb @@ -0,0 +1,10 @@ +# Disable the taggings counter cache on tags. +# +# Each tagging INSERT/DELETE would otherwise issue +# `UPDATE tags SET taggings_count = ... WHERE id = ?`, which serialises +# concurrent label writes on the same tag row and deadlocks under +# parallel multi-label updates (refer INF-68) +# +# Safe because Chatwoot does not read `tags.taggings_count` anywhere; +# label reports compute counts directly via GROUP BY on taggings. +ActsAsTaggableOn.tags_counter = false