Class: PostTiming

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/post_timing.rb

Constant Summary collapse

MAX_READ_TIME_PER_BATCH =
60 * 1000.0

Class Method Summary collapse

Class Method Details

.destroy_for(user_id, topic_ids) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'app/models/post_timing.rb', line 103

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)

    set_minimum_first_unread!(user_id: user_id, date: date) if date
  end
end

.destroy_last_for(user, topic_id: nil, topic: nil) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'app/models/post_timing.rb', line 74

def self.destroy_last_for(user, topic_id: nil, topic: nil)
  topic ||= Topic.find(topic_id)
  post_number = user.whisperer? ? 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
    last_read = nil if last_read < 1

    TopicUser.where(user_id: user.id, topic_id: topic.id).update_all(
      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

.pretend_read(topic_id, actual_read_post_number, pretend_read_post_number, user_ids = nil) ⇒ Object



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'app/models/post_timing.rb', line 10

def self.pretend_read(topic_id, actual_read_post_number, pretend_read_post_number, user_ids = nil)
  # This is done in SQL cause the logic is quite tricky and we want to do this in one db hit
  #
  user_ids_condition = user_ids.present? ? "AND user_id = ANY(ARRAY[:user_ids]::int[])" : ""
  sql_query = <<-SQL
    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
                  #{user_ids_condition}
                  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
                  )
  SQL

  params = {
    pretend_read_post_number: pretend_read_post_number,
    topic_id: topic_id,
    actual_read_post_number: actual_read_post_number,
  }
  params[:user_ids] = user_ids if user_ids.present?

  DB.exec(sql_query, params)

  TopicUser.update_last_read_post_number(topic_id:)
end

.process_timings(current_user, topic_id, topic_time, timings, opts = {}) ⇒ Object



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'app/models/post_timing.rb', line 150

def self.process_timings(current_user, topic_id, topic_time, timings, opts = {})
  lookup_column = current_user.whisperer? ? "highest_staff_post_number" : "highest_post_number"
  highest_post_number = DB.query_single(<<~SQL, topic_id: topic_id).first
        SELECT #{lookup_column}
        FROM topics
        WHERE id = :topic_id
      SQL

  # does not exist log nothing
  return if highest_post_number.nil?

  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
    timings.delete_at(i) if timings[i][0] > highest_post_number
  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 = LEAST(t.msecs::bigint + x.msecs, 2^31 - 1)
    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|
      if existing.exclude?(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

.record_new_timing(args) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'app/models/post_timing.rb', line 41

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"

  return if Topic.exists?(id: args[:topic_id], archetype: Archetype.private_message)
  UserStat.where(user_id: args[:user_id]).update_all "posts_read_count = posts_read_count + 1"
end

.record_timing(args) ⇒ Object

Increases a timer if a row exists, otherwise create it



62
63
64
65
66
67
68
69
70
71
72
# File 'app/models/post_timing.rb', line 62

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

.set_minimum_first_unread!(user_id:, date:) ⇒ Object



139
140
141
142
143
144
145
146
# File 'app/models/post_timing.rb', line 139

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

.set_minimum_first_unread_pm!(topic:, user_id:, date:) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'app/models/post_timing.rb', line 117

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