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



96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'app/models/post_timing.rb', line 96

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



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'app/models/post_timing.rb', line 67

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) ⇒ Object



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

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

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



143
144
145
146
147
148
149
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
# File 'app/models/post_timing.rb', line 143

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 = 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|
      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

.record_new_timing(args) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'app/models/post_timing.rb', line 34

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



55
56
57
58
59
60
61
62
63
64
65
# File 'app/models/post_timing.rb', line 55

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



132
133
134
135
136
137
138
139
# File 'app/models/post_timing.rb', line 132

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



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'app/models/post_timing.rb', line 110

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