diff --git a/Gemfile b/Gemfile index 1ae6cf093..2d789ba1d 100644 --- a/Gemfile +++ b/Gemfile @@ -12,6 +12,7 @@ gem 'bootsnap', require: false gem 'acts-as-taggable-on' gem 'attr_extras' gem 'browser' +gem 'counter_culture' gem 'hashie' gem 'jbuilder' gem 'kaminari' diff --git a/Gemfile.lock b/Gemfile.lock index 1cdfabee0..96ce85f3b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -191,6 +191,9 @@ GEM commonmarker (0.23.10) concurrent-ruby (1.3.5) connection_pool (2.5.3) + counter_culture (3.12.1) + activerecord (>= 4.2) + activesupport (>= 4.2) crack (1.0.0) bigdecimal rexml @@ -1039,6 +1042,7 @@ DEPENDENCIES byebug climate_control commonmarker + counter_culture csv-safe database_cleaner datadog (~> 2.0) diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index 458881367..5a10ddc13 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -2,6 +2,9 @@ class ConversationFinder attr_reader :current_user, :current_account, :params DEFAULT_STATUS = 'open'.freeze + # Filter params that affect conversation count + # NOTE: When adding a new filter, add it here to prevent incorrect cache usage + FILTER_PARAMS = %i[inbox_id team_id labels q source_id conversation_type].freeze SORT_OPTIONS = { 'last_activity_at_asc' => %w[sort_on_last_activity_at asc], 'last_activity_at_desc' => %w[sort_on_last_activity_at desc], @@ -167,10 +170,25 @@ class ConversationFinder end def set_count_for_all_conversations + # Check if filtering by status only (no other filters applied) + filtering_by_status_only = FILTER_PARAMS.none? { |key| params[key].present? } + + # Use cache if feature is enabled, filtering by status only, and status is not 'all' + use_cache = current_account.feature_enabled?(:counter_cache_optimization) && + filtering_by_status_only && + params[:status] != 'all' + + all_count = if use_cache + status = params[:status] || DEFAULT_STATUS + current_account.public_send("#{status}_conversations_count") + else + @conversations.count + end + [ @conversations.assigned_to(current_user).count, @conversations.unassigned.count, - current_account.conversations_count + all_count ] end diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 583c046a1..aa5cd10dd 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -97,7 +97,9 @@ class Conversation < ApplicationRecord ).sort_on_last_user_message_at } - belongs_to :account, counter_cache: true + belongs_to :account + counter_culture :account, + column_name: proc { |model| "#{model.status}_conversations_count" } belongs_to :inbox belongs_to :assignee, class_name: 'User', optional: true, inverse_of: :assigned_conversations belongs_to :assignee_agent_bot, class_name: 'AgentBot', optional: true diff --git a/config/features.yml b/config/features.yml index e03690d74..14f2ff33b 100644 --- a/config/features.yml +++ b/config/features.yml @@ -241,3 +241,7 @@ display_name: Required Conversation Attributes enabled: false premium: true +- name: counter_cache_optimization + display_name: Counter Cache Optimization + enabled: false + chatwoot_internal: true diff --git a/db/migrate/20260203062506_add_conversations_count_to_accounts.rb b/db/migrate/20260203062506_add_conversations_count_to_accounts.rb deleted file mode 100644 index 52571e577..000000000 --- a/db/migrate/20260203062506_add_conversations_count_to_accounts.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddConversationsCountToAccounts < ActiveRecord::Migration[7.1] - def change - add_column :accounts, :conversations_count, :integer, default: 0, null: false - end -end diff --git a/db/migrate/20260203062518_backfill_conversations_count.rb b/db/migrate/20260203062518_backfill_conversations_count.rb deleted file mode 100644 index 27b112593..000000000 --- a/db/migrate/20260203062518_backfill_conversations_count.rb +++ /dev/null @@ -1,11 +0,0 @@ -class BackfillConversationsCount < ActiveRecord::Migration[7.1] - def up - Account.find_each do |account| - Account.reset_counters(account.id, :conversations) - end - end - - def down - # No-op - end -end diff --git a/db/migrate/20260203125142_add_status_conversation_counters_to_accounts.rb b/db/migrate/20260203125142_add_status_conversation_counters_to_accounts.rb new file mode 100644 index 000000000..d60898ebb --- /dev/null +++ b/db/migrate/20260203125142_add_status_conversation_counters_to_accounts.rb @@ -0,0 +1,8 @@ +class AddStatusConversationCountersToAccounts < ActiveRecord::Migration[7.1] + def change + add_column :accounts, :open_conversations_count, :integer, default: 0, null: false + add_column :accounts, :resolved_conversations_count, :integer, default: 0, null: false + add_column :accounts, :pending_conversations_count, :integer, default: 0, null: false + add_column :accounts, :snoozed_conversations_count, :integer, default: 0, null: false + end +end diff --git a/db/migrate/20260203125200_backfill_status_conversation_counters.rb b/db/migrate/20260203125200_backfill_status_conversation_counters.rb new file mode 100644 index 000000000..80ea2c245 --- /dev/null +++ b/db/migrate/20260203125200_backfill_status_conversation_counters.rb @@ -0,0 +1,25 @@ +class BackfillStatusConversationCounters < ActiveRecord::Migration[7.1] + def up + Account.find_each do |account| + # Count conversations by status for this account + open_count = account.conversations.open.count + resolved_count = account.conversations.resolved.count + pending_count = account.conversations.pending.count + snoozed_count = account.conversations.snoozed.count + + # Update the counter cache columns + # rubocop:disable Rails/SkipsModelValidations + account.update_columns( + open_conversations_count: open_count, + resolved_conversations_count: resolved_count, + pending_conversations_count: pending_count, + snoozed_conversations_count: snoozed_count + ) + # rubocop:enable Rails/SkipsModelValidations + end + end + + def down + # No-op + end +end diff --git a/db/schema.rb b/db/schema.rb index aede09f12..7d91702fb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2026_02_03_062518) do +ActiveRecord::Schema[7.1].define(version: 2026_02_03_125200) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -73,7 +73,10 @@ ActiveRecord::Schema[7.1].define(version: 2026_02_03_062518) do t.integer "status", default: 0 t.jsonb "internal_attributes", default: {}, null: false t.jsonb "settings", default: {} - t.integer "conversations_count", default: 0, null: false + t.integer "open_conversations_count", default: 0, null: false + t.integer "resolved_conversations_count", default: 0, null: false + t.integer "pending_conversations_count", default: 0, null: false + t.integer "snoozed_conversations_count", default: 0, null: false t.index ["status"], name: "index_accounts_on_status" end