mirror of
https://github.com/discourse/discourse.git
synced 2025-06-02 16:29:32 +08:00
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:
@ -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
|
||||
|
82
plugins/chat/app/models/concerns/chat/thread_cache.rb
Normal file
82
plugins/chat/app/models/concerns/chat/thread_cache.rb
Normal 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
|
Reference in New Issue
Block a user