mirror of
https://github.com/discourse/discourse.git
synced 2024-12-01 03:03:44 +08:00
9b75d95fc6
This optimization helps to filter away topics so that the joins on related tables when querying for unread messages is not expensive.
230 lines
7.3 KiB
Ruby
230 lines
7.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class PostTiming < ActiveRecord::Base
|
|
belongs_to :topic
|
|
belongs_to :user
|
|
|
|
validates_presence_of :post_number
|
|
validates_presence_of :msecs
|
|
|
|
def self.pretend_read(topic_id, actual_read_post_number, pretend_read_post_number)
|
|
# This is done in SQL cause the logic is quite tricky and we want to do this in one db hit
|
|
#
|
|
DB.exec("INSERT INTO post_timings(topic_id, user_id, post_number, msecs)
|
|
SELECT :topic_id, user_id, :pretend_read_post_number, 1
|
|
FROM post_timings pt
|
|
WHERE topic_id = :topic_id AND
|
|
post_number = :actual_read_post_number AND
|
|
NOT EXISTS (
|
|
SELECT 1 FROM post_timings pt1
|
|
WHERE pt1.topic_id = pt.topic_id AND
|
|
pt1.post_number = :pretend_read_post_number AND
|
|
pt1.user_id = pt.user_id
|
|
)
|
|
",
|
|
pretend_read_post_number: pretend_read_post_number,
|
|
topic_id: topic_id,
|
|
actual_read_post_number: actual_read_post_number
|
|
)
|
|
|
|
TopicUser.ensure_consistency!(topic_id)
|
|
end
|
|
|
|
def self.record_new_timing(args)
|
|
row_count = DB.exec("INSERT INTO post_timings (topic_id, user_id, post_number, msecs)
|
|
SELECT :topic_id, :user_id, :post_number, :msecs
|
|
ON CONFLICT DO NOTHING",
|
|
args)
|
|
|
|
# concurrency is hard, we are not running serialized so this can possibly
|
|
# still happen, if it happens we just don't care, its an invalid record anyway
|
|
return if row_count == 0
|
|
Post.where(['topic_id = :topic_id and post_number = :post_number', args]).update_all 'reads = reads + 1'
|
|
UserStat.where(user_id: args[:user_id]).update_all 'posts_read_count = posts_read_count + 1'
|
|
end
|
|
|
|
# Increases a timer if a row exists, otherwise create it
|
|
def self.record_timing(args)
|
|
rows = DB.exec(<<~SQL, args)
|
|
UPDATE post_timings
|
|
SET msecs = msecs + :msecs
|
|
WHERE topic_id = :topic_id
|
|
AND user_id = :user_id
|
|
AND post_number = :post_number
|
|
SQL
|
|
|
|
record_new_timing(args) if rows == 0
|
|
end
|
|
|
|
def self.destroy_last_for(user, topic_id)
|
|
topic = Topic.find(topic_id)
|
|
post_number = user.staff? ? topic.highest_staff_post_number : topic.highest_post_number
|
|
|
|
last_read = post_number - 1
|
|
|
|
PostTiming.transaction do
|
|
PostTiming.where("topic_id = ? AND user_id = ? AND post_number > ?", topic.id, user.id, last_read).delete_all
|
|
if last_read < 1
|
|
last_read = nil
|
|
end
|
|
|
|
TopicUser.where(user_id: user.id, topic_id: topic.id).update_all(
|
|
highest_seen_post_number: last_read,
|
|
last_read_post_number: last_read
|
|
)
|
|
|
|
topic.posts.find_by(post_number: post_number).decrement!(:reads)
|
|
|
|
if topic.private_message?
|
|
set_minimum_first_unread_pm!(topic: topic, user_id: user.id, date: topic.updated_at)
|
|
else
|
|
set_minimum_first_unread!(user_id: user.id, date: topic.updated_at)
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.destroy_for(user_id, topic_ids)
|
|
PostTiming.transaction do
|
|
PostTiming
|
|
.where('user_id = ? and topic_id in (?)', user_id, topic_ids)
|
|
.delete_all
|
|
|
|
TopicUser
|
|
.where('user_id = ? and topic_id in (?)', user_id, topic_ids)
|
|
.delete_all
|
|
|
|
Post.where(topic_id: topic_ids).update_all('reads = reads - 1')
|
|
|
|
date = Topic.listable_topics.where(id: topic_ids).minimum(:updated_at)
|
|
|
|
if date
|
|
set_minimum_first_unread!(user_id: user_id, date: date)
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.set_minimum_first_unread_pm!(topic:, user_id:, date:)
|
|
if topic.topic_allowed_users.exists?(user_id: user_id)
|
|
UserStat.where("first_unread_pm_at > ? AND user_id = ?", date, user_id)
|
|
.update_all(first_unread_pm_at: date)
|
|
else
|
|
DB.exec(<<~SQL, date: date, user_id: user_id, topic_id: topic.id)
|
|
UPDATE group_users gu
|
|
SET first_unread_pm_at = :date
|
|
FROM (
|
|
SELECT
|
|
gu2.user_id,
|
|
gu2.group_id
|
|
FROM group_users gu2
|
|
INNER JOIN topic_allowed_groups tag ON tag.group_id = gu2.group_id AND tag.topic_id = :topic_id
|
|
WHERE gu2.user_id = :user_id
|
|
) Y
|
|
WHERE gu.user_id = Y.user_id AND gu.group_id = Y.group_id
|
|
SQL
|
|
end
|
|
end
|
|
|
|
def self.set_minimum_first_unread!(user_id:, date:)
|
|
DB.exec(<<~SQL, date: date, user_id: user_id)
|
|
UPDATE user_stats
|
|
SET first_unread_at = :date
|
|
WHERE first_unread_at > :date AND
|
|
user_id = :user_id
|
|
SQL
|
|
end
|
|
|
|
MAX_READ_TIME_PER_BATCH = 60 * 1000.0
|
|
|
|
def self.process_timings(current_user, topic_id, topic_time, timings, opts = {})
|
|
UserStat.update_time_read!(current_user.id)
|
|
|
|
max_time_per_post = ((Time.now - current_user.created_at) * 1000.0)
|
|
max_time_per_post = MAX_READ_TIME_PER_BATCH if max_time_per_post > MAX_READ_TIME_PER_BATCH
|
|
|
|
highest_seen = 1
|
|
new_posts_read = 0
|
|
|
|
join_table = []
|
|
|
|
i = timings.length
|
|
while i > 0
|
|
i -= 1
|
|
timings[i][1] = max_time_per_post if timings[i][1] > max_time_per_post
|
|
timings.delete_at(i) if timings[i][0] < 1
|
|
end
|
|
|
|
timings.each_with_index do |(post_number, time), index|
|
|
|
|
join_table << "SELECT #{topic_id.to_i} topic_id, #{post_number.to_i} post_number,
|
|
#{current_user.id.to_i} user_id, #{time.to_i} msecs, #{index} idx"
|
|
|
|
highest_seen = post_number.to_i > highest_seen ?
|
|
post_number.to_i : highest_seen
|
|
end
|
|
|
|
if join_table.length > 0
|
|
sql = <<~SQL
|
|
UPDATE post_timings t
|
|
SET msecs = t.msecs + x.msecs
|
|
FROM (#{join_table.join(" UNION ALL ")}) x
|
|
WHERE x.topic_id = t.topic_id AND
|
|
x.post_number = t.post_number AND
|
|
x.user_id = t.user_id
|
|
RETURNING x.idx
|
|
SQL
|
|
|
|
existing = Set.new(DB.query_single(sql))
|
|
|
|
sql = <<~SQL
|
|
SELECT 1 FROM topics
|
|
WHERE deleted_at IS NULL AND
|
|
archetype = 'regular' AND
|
|
id = :topic_id
|
|
SQL
|
|
|
|
is_regular = DB.exec(sql, topic_id: topic_id) == 1
|
|
new_posts_read = timings.size - existing.size if is_regular
|
|
|
|
timings.each_with_index do |(post_number, time), index|
|
|
unless existing.include?(index)
|
|
PostTiming.record_new_timing(topic_id: topic_id,
|
|
post_number: post_number,
|
|
user_id: current_user.id,
|
|
msecs: time)
|
|
end
|
|
end
|
|
end
|
|
|
|
total_changed = 0
|
|
if timings.length > 0
|
|
total_changed = Notification.mark_posts_read(current_user, topic_id, timings.map { |t| t[0] })
|
|
end
|
|
|
|
topic_time = max_time_per_post if topic_time > max_time_per_post
|
|
|
|
TopicUser.update_last_read(current_user, topic_id, highest_seen, new_posts_read, topic_time, opts)
|
|
TopicGroup.update_last_read(current_user, topic_id, highest_seen)
|
|
|
|
if total_changed > 0
|
|
current_user.reload
|
|
current_user.publish_notifications_state
|
|
end
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: post_timings
|
|
#
|
|
# topic_id :integer not null
|
|
# post_number :integer not null
|
|
# user_id :integer not null
|
|
# msecs :integer not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_post_timings_on_user_id (user_id)
|
|
# post_timings_summary (topic_id,post_number)
|
|
# post_timings_unique (topic_id,post_number,user_id) UNIQUE
|
|
#
|