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.
This commit is contained in:
Vishnu Narayanan
2026-04-10 17:32:13 +05:30
committed by GitHub
parent 224b1f98b0
commit de0bd8e71b

View File

@@ -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