fix: throttle filtered unread count rebuilds (#14980)

Reduces database pressure from filtered unread-count cache rebuilds
during high-traffic account rollouts by serving stale snapshots longer
and limiting inline saved-filter rebuild fanout.

## Closes

None

## What changed

- Increase filtered unread-count refresh throttling from 30 seconds to 5
minutes.
- Increase the stale snapshot window from 30 minutes to 1 hour.
- Reduce inline saved-filter count rebuilds per request from 10 to 3.
- Update unread-count specs to assert refresh and stale behavior through
the shared constants.

## How to test

- Enable `conversation_unread_counts` and `unread_count_for_filters` for
an account with conversation custom filters.
- Open the dashboard and verify unread-count badges still return values.
- Mutate conversations and verify stale filtered counts are served while
rebuilds are throttled, instead of repeatedly rebuilding every 30
seconds.
This commit is contained in:
Sony Mathew
2026-07-10 17:23:00 +05:30
committed by GitHub
parent 56dc580f50
commit 848e94bcf2
3 changed files with 42 additions and 9 deletions
+3 -3
View File
@@ -2,9 +2,9 @@ module Conversations::UnreadCounts
READY_TTL = 24.hours.to_i
SET_TTL = 25.hours.to_i
FILTERED_COUNT_FRESH_TTL = 5.minutes.to_i
FILTERED_COUNT_STALE_WINDOW = 30.minutes.to_i
FILTERED_COUNT_STALE_WINDOW = 1.hour.to_i
FILTERED_COUNT_REDIS_TTL = FILTERED_COUNT_FRESH_TTL + FILTERED_COUNT_STALE_WINDOW
FILTERED_COUNT_VERSION_TTL = SET_TTL
FILTERED_COUNT_MIN_REFRESH_INTERVAL = 30.seconds.to_i
MAX_INLINE_FILTER_BUILDS = 10
FILTERED_COUNT_MIN_REFRESH_INTERVAL = 5.minutes.to_i
MAX_INLINE_FILTER_BUILDS = 3
end
@@ -96,7 +96,14 @@ RSpec.describe Conversations::UnreadCounts::FilteredCountStore do
described_class.bump_built_in_filter_version!(account_id: account_id, user_id: user_id)
expect(described_class.built_in_filter_counts_state(account_id: account_id, user_id: user_id, now: built_at + 2.minutes)).to be_stale
expect(described_class.built_in_filter_counts_state(account_id: account_id, user_id: user_id, now: built_at + 36.minutes)).to be_expired
expect(
described_class.built_in_filter_counts_state(
account_id: account_id,
user_id: user_id,
now: built_at + Conversations::UnreadCounts::FILTERED_COUNT_FRESH_TTL +
Conversations::UnreadCounts::FILTERED_COUNT_STALE_WINDOW + 1.second
)
).to be_expired
Redis::Alfred.delete(described_class.built_in_filter_counts_key(account_id, user_id))
expect(described_class.built_in_filter_counts_state(account_id: account_id, user_id: user_id)).to be_missing
@@ -202,8 +209,18 @@ RSpec.describe Conversations::UnreadCounts::FilteredCountStore do
)
snapshot = described_class.built_in_filter_counts(account_id: account_id, user_id: user_id)
expect(described_class.refresh_due?(snapshot, now: built_at + 10.seconds)).to be(false)
expect(described_class.refresh_due?(snapshot, now: built_at + 31.seconds)).to be(true)
expect(
described_class.refresh_due?(
snapshot,
now: built_at + Conversations::UnreadCounts::FILTERED_COUNT_MIN_REFRESH_INTERVAL - 1.second
)
).to be(false)
expect(
described_class.refresh_due?(
snapshot,
now: built_at + Conversations::UnreadCounts::FILTERED_COUNT_MIN_REFRESH_INTERVAL + 1.second
)
).to be(true)
expect(described_class.claim_built_in_filter_refresh!(account_id: account_id, user_id: user_id)).to be(true)
expect(described_class.claim_built_in_filter_refresh!(account_id: account_id, user_id: user_id)).to be(false)
@@ -48,10 +48,22 @@ RSpec.describe Conversations::UnreadCounts::FilteredCounter do
create(:mention, account: account, conversation: second_mention, user: agent)
store.bump_conversation_version!(account.id)
expect(described_class.new(account: account, user: agent, now: now + 10.seconds).perform[:mentions_count]).to eq(1)
expect(
described_class.new(
account: account,
user: agent,
now: now + Conversations::UnreadCounts::FILTERED_COUNT_MIN_REFRESH_INTERVAL - 1.second
).perform[:mentions_count]
).to eq(1)
Redis::Alfred.delete(store.built_in_filter_refresh_throttle_key(account.id, agent.id))
expect(described_class.new(account: account, user: agent, now: now + 31.seconds).perform[:mentions_count]).to eq(2)
expect(
described_class.new(
account: account,
user: agent,
now: now + Conversations::UnreadCounts::FILTERED_COUNT_MIN_REFRESH_INTERVAL + 1.second
).perform[:mentions_count]
).to eq(2)
end
it 'returns stale built-in counts when a refresh build hits a database error' do
@@ -62,7 +74,11 @@ RSpec.describe Conversations::UnreadCounts::FilteredCounter do
store.bump_conversation_version!(account.id)
Redis::Alfred.delete(store.built_in_filter_refresh_throttle_key(account.id, agent.id))
failing_counter = described_class.new(account: account, user: agent, now: now + 31.seconds)
failing_counter = described_class.new(
account: account,
user: agent,
now: now + Conversations::UnreadCounts::FILTERED_COUNT_MIN_REFRESH_INTERVAL + 1.second
)
allow(failing_counter).to receive(:built_in_counts_from_database).and_raise(ActiveRecord::StatementInvalid.new('statement timeout'))
expect(failing_counter.perform[:mentions_count]).to eq(1)