discourse/app/models/user_stat.rb
Sam 236c755d62 FIX: do not store key tracking last seen time indefinitely
UserStat has some special logic to keep adding time read if repeat calls
are made in intervals less than 100 seconds. This is called regularly
when we update read timings on a topic.

We only need to cache this key in redis for 100 seconds, however previously
we would keep it forever, 1 key per user. This has potential of bloating
a very large amount of keys for no longer active users in redis.
2018-12-03 08:35:26 +11:00

137 lines
4.3 KiB
Ruby

# frozen_string_literal: true
class UserStat < ActiveRecord::Base
belongs_to :user
after_save :trigger_badges
def self.ensure_consistency!(last_seen = 1.hour.ago)
reset_bounce_scores
update_view_counts(last_seen)
end
def self.reset_bounce_scores
UserStat.where("reset_bounce_score_after < now()")
.where("bounce_score > 0")
.update_all(bounce_score: 0)
end
# Updates the denormalized view counts for all users
def self.update_view_counts(last_seen = 1.hour.ago)
# NOTE: we only update the counts for users we have seen in the last hour
# this avoids a very expensive query that may run on the entire user base
# we also ensure we only touch the table if data changes
# Update denormalized topics_entered
DB.exec(<<~SQL, seen_at: last_seen)
UPDATE user_stats SET topics_entered = X.c
FROM
(SELECT v.user_id, COUNT(topic_id) AS c
FROM topic_views AS v
WHERE v.user_id IN (
SELECT u1.id FROM users u1 where u1.last_seen_at > :seen_at
)
GROUP BY v.user_id) AS X
WHERE
X.user_id = user_stats.user_id AND
X.c <> topics_entered
SQL
# Update denormalzied posts_read_count
DB.exec(<<~SQL, seen_at: last_seen)
UPDATE user_stats SET posts_read_count = X.c
FROM
(SELECT pt.user_id,
COUNT(*) AS c
FROM users AS u
JOIN post_timings AS pt ON pt.user_id = u.id
JOIN topics t ON t.id = pt.topic_id
WHERE u.last_seen_at > :seen_at AND
t.archetype = 'regular' AND
t.deleted_at IS NULL
GROUP BY pt.user_id) AS X
WHERE X.user_id = user_stats.user_id AND
X.c <> posts_read_count
SQL
end
# topic_reply_count is a count of posts in other users' topics
def update_topic_reply_count
self.topic_reply_count =
Topic
.where(['id in (
SELECT topic_id FROM posts p
JOIN topics t2 ON t2.id = p.topic_id
WHERE p.deleted_at IS NULL AND
t2.user_id <> p.user_id AND
p.user_id = ?
)', self.user_id])
.count
end
MAX_TIME_READ_DIFF = 100
# attempt to add total read time to user based on previous time this was called
def self.update_time_read!(id)
if last_seen = last_seen_cached(id)
diff = (Time.now.to_f - last_seen.to_f).round
if diff > 0 && diff < MAX_TIME_READ_DIFF
update_args = ["time_read = time_read + ?", diff]
UserStat.where(user_id: id).update_all(update_args)
UserVisit.where(user_id: id, visited_at: Time.zone.now.to_date).update_all(update_args)
end
end
cache_last_seen(id, Time.now.to_f)
end
def update_time_read!
UserStat.update_time_read!(id)
end
def reset_bounce_score!
update_columns(reset_bounce_score_after: nil, bounce_score: 0)
end
def self.last_seen_key(id)
# frozen
-"user-last-seen:#{id}"
end
def self.last_seen_cached(id)
$redis.get(last_seen_key(id))
end
def self.cache_last_seen(id, val)
$redis.setex(last_seen_key(id), MAX_TIME_READ_DIFF, val)
end
protected
def trigger_badges
BadgeGranter.queue_badge_grant(Badge::Trigger::UserChange, user: self.user)
end
end
# == Schema Information
#
# Table name: user_stats
#
# user_id :integer not null, primary key
# topics_entered :integer default(0), not null
# time_read :integer default(0), not null
# days_visited :integer default(0), not null
# posts_read_count :integer default(0), not null
# likes_given :integer default(0), not null
# likes_received :integer default(0), not null
# topic_reply_count :integer default(0), not null
# new_since :datetime not null
# read_faq :datetime
# first_post_created_at :datetime
# post_count :integer default(0), not null
# topic_count :integer default(0), not null
# bounce_score :float default(0.0), not null
# reset_bounce_score_after :datetime
# flags_agreed :integer default(0), not null
# flags_disagreed :integer default(0), not null
# flags_ignored :integer default(0), not null
#