Skip to content

Commit 0f94bc3

Browse files
committed
Fixing unread message counting bug
We assumed that with message ranges (id1,id2) we can count the number of messages in the range by id2-id1. This is of course wrong because some of the ids in the range can belong to other threads, causing the code to significantly overcount read messages. To fix this problem this commit introduces a new column to the ranges, so that we store ranges in the form of (id1,id2,count), and can count the read message count reliable for threads by a simple group by thread_id sum(count).
1 parent 74ef4d9 commit 0f94bc3

File tree

5 files changed

+63
-21
lines changed

5 files changed

+63
-21
lines changed

app/controllers/topics_controller.rb

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -313,11 +313,9 @@ def preload_topic_states
313313
aware_map = ThreadAwareness.where(user: current_user, topic_id: topic_ids)
314314
.pluck(:topic_id, :aware_until_message_id)
315315
.to_h
316-
read_rows = MessageReadRange.where(user: current_user, topic_id: topic_ids)
317-
.pluck(:topic_id, :range_start_message_id, :range_end_message_id)
318-
read_ranges = read_rows.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(tid, s, e), acc|
319-
acc[tid] << [s, e]
320-
end
316+
read_counts = MessageReadRange.where(user: current_user, topic_id: topic_ids)
317+
.group(:topic_id)
318+
.sum(:message_count)
321319
global_aware_before = current_user.aware_before
322320

323321
team_readers = preload_team_reader_states(topic_ids, last_ids)
@@ -327,19 +325,19 @@ def preload_topic_states
327325
@topics.each do |topic|
328326
last_id = last_ids[topic.id]
329327
last_time = last_times[topic.id]
330-
if user_signed_in?
331-
aware_until = aware_map[topic.id]
332-
total = total_counts[topic.id].to_i
333-
ranges = read_ranges[topic.id] || []
334-
read_count = ranges.sum do |(s, e)|
335-
next 0 unless s && e
336-
(e - s + 1)
328+
if user_signed_in?
329+
aware_until = aware_map[topic.id]
330+
total = total_counts[topic.id].to_i
331+
read_count = read_counts[topic.id].to_i
332+
status = compute_topic_status(total:, last_time:, aware_until:, read_count:, global_aware_before:)
333+
progress = compute_progress(total:, read_count:)
334+
else
335+
status = :new
336+
aware_until = nil
337+
read_count = 0
338+
progress = 0
337339
end
338-
status = compute_topic_status(total:, last_time:, aware_until:, read_count:, global_aware_before:)
339-
progress = compute_progress(total:, read_count:)
340-
else
341-
status = :new
342-
end
340+
343341
@topic_states[topic.id] = { status:, aware_until:, read_count:, last_id:, last_time:, progress:, team_readers: team_readers[topic.id] || [] }
344342
end
345343
end

app/models/message_read_range.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,16 @@ def self.add_range(user:, topic:, start_id:, end_id:, read_at: Time.current)
2424
overlapping.delete_all
2525
end
2626

27-
create!(user:, topic:, range_start_message_id: s, range_end_message_id: e, read_at:)
27+
count = Message.where(topic_id: topic.id, id: s..e).count
28+
29+
create!(
30+
user: user,
31+
topic: topic,
32+
range_start_message_id: s,
33+
range_end_message_id: e,
34+
message_count: count,
35+
read_at: read_at
36+
)
2837
end
2938
end
3039

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
class AddMessageCountToMessageReadRanges < ActiveRecord::Migration[7.1]
2+
disable_ddl_transaction!
3+
4+
class MigrationMessageReadRange < ApplicationRecord
5+
self.table_name = "message_read_ranges"
6+
end
7+
8+
class MigrationMessage < ApplicationRecord
9+
self.table_name = "messages"
10+
end
11+
12+
def up
13+
add_column :message_read_ranges, :message_count, :integer, default: 0, null: false
14+
15+
say_with_time "Backfilling message_count for existing message_read_ranges" do
16+
MigrationMessageReadRange.reset_column_information
17+
18+
MigrationMessageReadRange.find_in_batches(batch_size: 500) do |batch|
19+
updates = batch.map do |range|
20+
count = MigrationMessage.where(topic_id: range.topic_id, id: range.range_start_message_id..range.range_end_message_id).count
21+
[range.id, count]
22+
end
23+
24+
updates.each_slice(100) do |slice|
25+
slice.each do |id, count|
26+
MigrationMessageReadRange.where(id: id).update_all(message_count: count)
27+
end
28+
end
29+
end
30+
end
31+
end
32+
33+
def down
34+
remove_column :message_read_ranges, :message_count
35+
end
36+
end

db/schema.rb

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/requests/topics_spec.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828

2929
it "shows topics with most recent activity first" do
3030
get topics_path
31-
# topic2 has more recent message, should appear first
3231
topic1_position = response.body.index(topic1.title)
3332
topic2_position = response.body.index(topic2.title)
3433
expect(topic2_position).to be < topic1_position
@@ -76,7 +75,6 @@
7675
it "shows flat view (oldest first)" do
7776
get topic_path(topic)
7877
expect(response.body).to include('messages-container flat')
79-
# root should appear before reply
8078
root_position = response.body.index(root_message.body)
8179
reply_position = response.body.index(reply_message.body)
8280
expect(root_position).to be < reply_position

0 commit comments

Comments
 (0)