FEATURE: Reintroduce better thread reply counter cache (#21197)

This was reverted in 38cebd3ed509524ad635adb107163d0496d0c550.
The issue was that I was using Discourse.redis.delete_prefixed
which does a slow redis KEYS lookup, which is not advised in
production. This commit removes that, and also ensures the periodical
thread count update only happens if threading is enabled.

I changed to use a redis INCR/DECR for reply count
cache. This avoids a round trip to redis to GET the current
count, and also avoids multi-process issues, where
if there's two processes trying to increment at the
same time, they may both receive the same value, add one
to it, then both write the same value back.
Then, it's only n+1 instead of n+2.

This also prevents almost all chat scheduled jobs from
running if chat is disabled, the only one remaining is
the message retention job.
This commit is contained in:
Martin Brennan
2023-04-24 09:32:04 +10:00
committed by GitHub
parent 21f93731a3
commit 24ec06ff85
27 changed files with 694 additions and 233 deletions

View File

@ -4,6 +4,8 @@ module Chat
class Thread < ActiveRecord::Base
EXCERPT_LENGTH = 150
include Chat::ThreadCache
self.table_name = "chat_threads"
belongs_to :channel, foreign_key: "channel_id", class_name: "Chat::Channel"
@ -11,7 +13,11 @@ module Chat
belongs_to :original_message, foreign_key: "original_message_id", class_name: "Chat::Message"
has_many :chat_messages,
-> { order("chat_messages.created_at ASC, chat_messages.id ASC") },
-> {
where("deleted_at IS NULL").order(
"chat_messages.created_at ASC, chat_messages.id ASC",
)
},
foreign_key: :thread_id,
primary_key: :id,
class_name: "Chat::Message"
@ -50,6 +56,7 @@ module Chat
end
def self.ensure_consistency!
return if !SiteSetting.enable_experimental_chat_threaded_discussions
update_counts
end
@ -61,7 +68,7 @@ module Chat
#
# It is updated eventually via Jobs::Chat::PeriodicalUpdates. In
# future we may want to update this more frequently.
DB.exec <<~SQL
updated_thread_ids = DB.query_single <<~SQL
UPDATE chat_threads threads
SET replies_count = subquery.replies_count
FROM (
@ -72,7 +79,10 @@ module Chat
) subquery
WHERE threads.id = subquery.thread_id
AND subquery.replies_count != threads.replies_count
RETURNING threads.id AS thread_id;
SQL
return if updated_thread_ids.empty?
self.clear_caches!(updated_thread_ids)
end
end
end

View File

@ -0,0 +1,82 @@
# frozen_string_literal: true
module Chat
module ThreadCache
extend ActiveSupport::Concern
class_methods do
def replies_count_cache_updated_at_redis_key(id)
"chat_thread:replies_count_cache_updated_at:#{id}"
end
def replies_count_cache_redis_key(id)
"chat_thread:replies_count_cache:#{id}"
end
def clear_caches!(ids)
ids = Array.wrap(ids)
keys_to_delete =
ids
.map do |id|
[replies_count_cache_redis_key(id), replies_count_cache_updated_at_redis_key(id)]
end
.flatten
Discourse.redis.del(keys_to_delete)
end
end
def replies_count_cache_recently_updated?
replies_count_cache_updated_at.after?(5.minutes.ago)
end
def replies_count_cache_updated_at
Time.at(
Discourse.redis.get(Chat::Thread.replies_count_cache_updated_at_redis_key(self.id)).to_i,
in: Time.zone,
)
end
def replies_count_cache
redis_cache = Discourse.redis.get(Chat::Thread.replies_count_cache_redis_key(self.id))&.to_i
# If the cache is not present for whatever reason, set it to the current value,
# otherwise INCR/DECR will be way off. No need to enqueue the job or publish,
# since this is likely fetched by a serializer.
if !redis_cache.present?
set_replies_count_redis_cache(self.replies_count)
self.replies_count
else
redis_cache != self.replies_count ? redis_cache : self.replies_count
end
end
def set_replies_count_cache(value, update_db: false)
self.update!(replies_count: value) if update_db
set_replies_count_redis_cache(value)
thread_reply_count_cache_changed
end
def set_replies_count_redis_cache(value)
Discourse.redis.setex(
Chat::Thread.replies_count_cache_redis_key(self.id),
5.minutes.from_now.to_i,
value,
)
end
def increment_replies_count_cache
Discourse.redis.incr(Chat::Thread.replies_count_cache_redis_key(self.id))
thread_reply_count_cache_changed
end
def decrement_replies_count_cache
Discourse.redis.decr(Chat::Thread.replies_count_cache_redis_key(self.id))
thread_reply_count_cache_changed
end
def thread_reply_count_cache_changed
Jobs.enqueue_in(5.seconds, Jobs::Chat::UpdateThreadReplyCount, thread_id: self.id)
::Chat::Publisher.publish_thread_original_message_metadata!(self)
end
end
end