perf: sync megagroup unread derivation
This commit is contained in:
parent
4c0cc2b7a7
commit
41c7f1d018
5 changed files with 87 additions and 217 deletions
|
|
@ -902,14 +902,23 @@ func (s *ChannelStore) CountChannelArchiveUnread(ctx context.Context, userID int
|
|||
}
|
||||
var peers, messages int32
|
||||
// JOIN active member:退群残留的 channel_dialogs 行不计入归档徽章。
|
||||
// unread 读时动态派生(H4a):不再消费 channel_dialogs.unread_count 缓存列;归档集合
|
||||
// 有界(需显式归档建行),每行动态 COUNT 已被 cap 钳制。
|
||||
visibleTopID := "CASE WHEN c.top_message_id > m.available_min_id THEN c.top_message_id ELSE 0 END"
|
||||
visibleReadInbox := "GREATEST(COALESCE(d.read_inbox_max_id, 0), m.read_inbox_max_id)"
|
||||
if err := s.db.QueryRow(ctx, `
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE d.unread_count > 0 OR d.unread_mark)::int,
|
||||
COALESCE(SUM(d.unread_count), 0)::int
|
||||
FROM channel_dialogs d
|
||||
JOIN channel_members m ON m.channel_id = d.channel_id AND m.user_id = d.user_id AND m.status = 'active'
|
||||
WHERE d.user_id = $1
|
||||
AND COALESCE(d.folder_id, 0) = 1`, userID).Scan(&peers, &messages); err != nil {
|
||||
COUNT(*) FILTER (WHERE t.unread_count > 0 OR t.unread_mark)::int,
|
||||
COALESCE(SUM(t.unread_count), 0)::int
|
||||
FROM (
|
||||
SELECT `+channelDialogVisibleUnreadCountSQL(visibleReadInbox, visibleTopID)+` AS unread_count,
|
||||
COALESCE(d.unread_mark, m.unread_mark) AS unread_mark
|
||||
FROM channel_dialogs d
|
||||
JOIN channel_members m ON m.channel_id = d.channel_id AND m.user_id = d.user_id AND m.status = 'active'
|
||||
JOIN channels c ON c.id = d.channel_id AND NOT c.deleted
|
||||
WHERE d.user_id = $1
|
||||
AND COALESCE(d.folder_id, 0) = 1
|
||||
) t`, userID).Scan(&peers, &messages); err != nil {
|
||||
return 0, 0, fmt.Errorf("count channel archive unread: %w", err)
|
||||
}
|
||||
return int(peers), int(messages), nil
|
||||
|
|
@ -1112,61 +1121,6 @@ func cappedChannelUnreadCount(whereBody string) string {
|
|||
)`, whereBody, domain.MaxDialogUnreadCount)
|
||||
}
|
||||
|
||||
func refreshChannelDialogsAfterDeleteTx(ctx context.Context, tx pgx.Tx, channel domain.Channel) error {
|
||||
if channel.ID == 0 || !shouldSynchronouslyUpsertChannelDialogs(channel) {
|
||||
return nil
|
||||
}
|
||||
insertUnread := cappedChannelUnreadCount(`WHERE msg.channel_id = $1
|
||||
AND msg.id > GREATEST(active.read_inbox_max_id, active.available_min_id)
|
||||
AND msg.id <= active.visible_top_id
|
||||
AND NOT msg.deleted
|
||||
AND msg.sender_user_id <> active.user_id`)
|
||||
conflictUnread := cappedChannelUnreadCount(`WHERE msg.channel_id = channel_dialogs.channel_id
|
||||
AND msg.id > GREATEST(channel_dialogs.read_inbox_max_id, EXCLUDED.read_inbox_max_id)
|
||||
AND msg.id <= EXCLUDED.top_message_id
|
||||
AND NOT msg.deleted
|
||||
AND msg.sender_user_id <> channel_dialogs.user_id`)
|
||||
if _, err := tx.Exec(ctx, fmt.Sprintf(`
|
||||
WITH active AS (
|
||||
SELECT
|
||||
m.user_id,
|
||||
m.available_min_id,
|
||||
GREATEST(COALESCE(d.read_inbox_max_id, 0), m.read_inbox_max_id) AS read_inbox_max_id,
|
||||
LEAST(GREATEST(c.top_message_id, 0), GREATEST(COALESCE(d.read_outbox_max_id, 0), m.read_outbox_max_id, CASE WHEN c.read_inbox_top1_user_id = m.user_id THEN c.read_inbox_top2 ELSE c.read_inbox_top1 END)) AS read_outbox_max_id,
|
||||
COALESCE(d.unread_mark, m.unread_mark) AS unread_mark,
|
||||
CASE WHEN $2 > m.available_min_id THEN $2 ELSE 0 END AS visible_top_id
|
||||
FROM channel_members m
|
||||
JOIN channels c ON c.id = m.channel_id
|
||||
LEFT JOIN channel_dialogs d ON d.user_id = m.user_id AND d.channel_id = m.channel_id
|
||||
WHERE m.channel_id = $1
|
||||
AND m.status = 'active'
|
||||
AND NOT COALESCE((m.banned_rights->>'ViewMessages')::boolean, false)
|
||||
)
|
||||
INSERT INTO channel_dialogs (
|
||||
user_id, channel_id, top_message_id, top_message_date,
|
||||
read_inbox_max_id, read_outbox_max_id, unread_count, unread_mark
|
||||
)
|
||||
SELECT
|
||||
user_id, $1, visible_top_id,
|
||||
CASE WHEN visible_top_id > 0 THEN COALESCE(top_msg.message_date, $3) ELSE 0 END,
|
||||
read_inbox_max_id, read_outbox_max_id,
|
||||
%s,
|
||||
unread_mark
|
||||
FROM active
|
||||
LEFT JOIN channel_messages top_msg ON top_msg.channel_id = $1 AND top_msg.id = active.visible_top_id AND NOT top_msg.deleted
|
||||
ON CONFLICT (user_id, channel_id) DO UPDATE SET
|
||||
top_message_id = EXCLUDED.top_message_id,
|
||||
top_message_date = EXCLUDED.top_message_date,
|
||||
read_inbox_max_id = GREATEST(channel_dialogs.read_inbox_max_id, EXCLUDED.read_inbox_max_id),
|
||||
read_outbox_max_id = GREATEST(channel_dialogs.read_outbox_max_id, EXCLUDED.read_outbox_max_id),
|
||||
unread_count = %s,
|
||||
unread_mark = EXCLUDED.unread_mark,
|
||||
updated_at = now()`, insertUnread, conflictUnread), channel.ID, channel.TopMessageID, channel.Date); err != nil {
|
||||
return fmt.Errorf("refresh channel dialogs after delete: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertChannelDialogTx(ctx context.Context, tx pgx.Tx, userID int64, channel domain.Channel, top domain.ChannelMessage, readInboxMaxID, readOutboxMaxID int) error {
|
||||
topDate := top.Date
|
||||
if topDate == 0 {
|
||||
|
|
@ -1199,6 +1153,18 @@ ON CONFLICT (user_id, channel_id) DO UPDATE SET
|
|||
return nil
|
||||
}
|
||||
|
||||
// upsertChannelDialogsForMessageTx 在发送事务内处理消息的收件人侧读边界副作用。
|
||||
//
|
||||
// 性能审计 H4a:不再对全员(旧闸门为 megagroup ≤ MaxSynchronousChannelDialogFanout)
|
||||
// upsert channel_dialogs——发送事务 O(成员) 行写 + 行锁是放开大群的阻断项(P1-u 残余)。
|
||||
// 所有频道(broadcast / megagroup 不分大小)统一读时派生:unread 由可见 incoming 消息
|
||||
// 重算(channelDialogDynamicUnreadCountSQL,capped),top 由 channels.top_message_id 提供,
|
||||
// read boundary 真值在 channel_members,dialog 行仅承载 pin/folder/unread_mark/draft 等
|
||||
// 用户显式状态并按需惰性建行。
|
||||
//
|
||||
// 保留的唯一逐收件人写:skipDelivery(privacy bot)成员的 read_inbox 边界推进——这些
|
||||
// 成员被排除投递,必须同步抬边界避免其未读派生把被隐藏消息计成 unread;集合有界
|
||||
// (bot-only 候选)。
|
||||
func upsertChannelDialogsForMessageTx(ctx context.Context, tx pgx.Tx, channel domain.Channel, top domain.ChannelMessage, selfReadUserID int64, skipDeliveryUserIDs ...[]int64) error {
|
||||
if channel.ID == 0 || top.ID == 0 {
|
||||
return nil
|
||||
|
|
@ -1221,75 +1187,11 @@ WHERE channel_id = $1
|
|||
return fmt.Errorf("advance skipped channel delivery boundary: %w", err)
|
||||
}
|
||||
}
|
||||
if !shouldSynchronouslyUpsertChannelDialogs(channel) {
|
||||
return nil
|
||||
}
|
||||
topDate := top.Date
|
||||
if topDate == 0 {
|
||||
topDate = channel.Date
|
||||
}
|
||||
// 下界用 GREATEST(read_inbox, available_min_id) 与 delete/read 路径一致:当前
|
||||
// 全局不变量 read_inbox_max_id >= available_min_id 使其为 no-op,但该不变量未由
|
||||
// schema 强制,对齐后即便未来有路径只抬 available_min 不抬 read 也不会 over-count。
|
||||
insertUnread := cappedChannelUnreadCount(`WHERE msg.channel_id = $1
|
||||
AND msg.id > GREATEST(active.read_inbox_max_id, active.available_min_id)
|
||||
AND msg.id <= $2
|
||||
AND NOT msg.deleted
|
||||
AND msg.sender_user_id <> active.user_id`)
|
||||
conflictUnread := cappedChannelUnreadCount(`WHERE msg.channel_id = channel_dialogs.channel_id
|
||||
AND msg.id > GREATEST(channel_dialogs.read_inbox_max_id, EXCLUDED.read_inbox_max_id)
|
||||
AND msg.id <= GREATEST(channel_dialogs.top_message_id, EXCLUDED.top_message_id)
|
||||
AND NOT msg.deleted
|
||||
AND msg.sender_user_id <> channel_dialogs.user_id`)
|
||||
if _, err := tx.Exec(ctx, fmt.Sprintf(`
|
||||
WITH active AS (
|
||||
SELECT
|
||||
m.user_id,
|
||||
CASE
|
||||
WHEN m.user_id = $4 THEN GREATEST(m.read_inbox_max_id, $2)
|
||||
ELSE m.read_inbox_max_id
|
||||
END AS read_inbox_max_id,
|
||||
m.available_min_id,
|
||||
-- sender 的 outbox 回执水位绝不随自己发消息推进:read_outbox_max_id
|
||||
-- 语义是"已被其他成员读到的最大 ID",推进只能来自对端 readHistory。
|
||||
m.read_outbox_max_id
|
||||
FROM channel_members m
|
||||
WHERE m.channel_id = $1
|
||||
AND m.status = 'active'
|
||||
AND NOT COALESCE((m.banned_rights->>'ViewMessages')::boolean, false)
|
||||
AND $2 > m.available_min_id
|
||||
AND NOT (m.user_id = ANY($5::bigint[]))
|
||||
)
|
||||
INSERT INTO channel_dialogs (
|
||||
user_id, channel_id, top_message_id, top_message_date,
|
||||
read_inbox_max_id, read_outbox_max_id, unread_count, unread_mark
|
||||
)
|
||||
SELECT
|
||||
user_id, $1, $2, $3,
|
||||
read_inbox_max_id, read_outbox_max_id,
|
||||
%s,
|
||||
false
|
||||
FROM active
|
||||
ON CONFLICT (user_id, channel_id) DO UPDATE SET
|
||||
top_message_id = GREATEST(channel_dialogs.top_message_id, EXCLUDED.top_message_id),
|
||||
top_message_date = GREATEST(channel_dialogs.top_message_date, EXCLUDED.top_message_date),
|
||||
read_inbox_max_id = GREATEST(channel_dialogs.read_inbox_max_id, EXCLUDED.read_inbox_max_id),
|
||||
read_outbox_max_id = GREATEST(channel_dialogs.read_outbox_max_id, EXCLUDED.read_outbox_max_id),
|
||||
unread_count = %s,
|
||||
unread_mark = CASE WHEN channel_dialogs.user_id = $4 THEN false ELSE channel_dialogs.unread_mark END,
|
||||
updated_at = now()`, insertUnread, conflictUnread), channel.ID, top.ID, topDate, selfReadUserID, skipIDs); err != nil {
|
||||
return fmt.Errorf("upsert channel message dialogs: %w", err)
|
||||
}
|
||||
// skipDelivery 成员可能已有 channel_dialogs 行(pin/归档等惰性建行),其 read_inbox
|
||||
// 经 GREATEST(d, m) 读取——member 行已推进即可,dialog 行无需同步。
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldSynchronouslyUpsertChannelDialogs(channel domain.Channel) bool {
|
||||
if channel.Broadcast {
|
||||
return false
|
||||
}
|
||||
return channel.ParticipantsCount > 0 && channel.ParticipantsCount <= domain.MaxSynchronousChannelDialogFanout
|
||||
}
|
||||
|
||||
func scanChannelDialogRow(row rowScanner, userID int64) (domain.Channel, domain.Dialog, *domain.Peer, error) {
|
||||
var ch domain.Channel
|
||||
var rights, reactionPolicy string
|
||||
|
|
|
|||
|
|
@ -411,9 +411,8 @@ RETURNING pinned_message_id`, channel.ID, topID, pts).Scan(&latestPinned); err !
|
|||
channel.TopMessageID = topID
|
||||
channel.Pts = pts
|
||||
channel.PinnedMessageID = latestPinned
|
||||
if err := refreshChannelDialogsAfterDeleteTx(ctx, tx, channel); err != nil {
|
||||
return nil, domain.ChannelUpdateEvent{}, channel, err
|
||||
}
|
||||
// 删除后不再对全员刷新 channel_dialogs 缓存行(性能审计 H4a):unread 读时由
|
||||
// 可见 incoming 消息动态派生(deleted 自动出列),top 由 channels.top_message_id 提供。
|
||||
event := domain.ChannelUpdateEvent{
|
||||
ChannelID: channel.ID,
|
||||
Type: domain.ChannelUpdateDeleteMessages,
|
||||
|
|
|
|||
|
|
@ -65,28 +65,27 @@ func TestChannelStoreSendMessageFansOutDialogRows(t *testing.T) {
|
|||
t.Fatalf("send channel message: %v", err)
|
||||
}
|
||||
|
||||
var friendTop, friendReadInbox, friendUnread int
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT top_message_id, read_inbox_max_id, unread_count
|
||||
FROM channel_dialogs
|
||||
WHERE channel_id = $1 AND user_id = $2`, channelID, friend.ID).Scan(&friendTop, &friendReadInbox, &friendUnread); err != nil {
|
||||
t.Fatalf("read friend dialog row after send: %v", err)
|
||||
// H4a:发送事务不再对全员 upsert channel_dialogs——dialog 投影读时派生。
|
||||
// friend 视角:top 来自 channels.top_message_id,unread 由可见 incoming 消息动态重算。
|
||||
friendView, err := channels.GetChannelDialogs(ctx, friend.ID, []int64{channelID})
|
||||
if err != nil {
|
||||
t.Fatalf("get friend dialogs after send: %v", err)
|
||||
}
|
||||
if friendTop != sent.Message.ID || friendReadInbox != 0 || friendUnread != 2 {
|
||||
t.Fatalf("friend dialog row top=%d read=%d unread=%d, want top %d read 0 unread 2", friendTop, friendReadInbox, friendUnread, sent.Message.ID)
|
||||
if len(friendView.Dialogs) != 1 || friendView.Dialogs[0].TopMessage != sent.Message.ID ||
|
||||
friendView.Dialogs[0].ReadInboxMaxID != 0 || friendView.Dialogs[0].UnreadCount != 2 {
|
||||
t.Fatalf("friend derived dialog = %+v, want top %d read 0 unread 2", friendView.Dialogs, sent.Message.ID)
|
||||
}
|
||||
|
||||
var ownerTop, ownerReadInbox, ownerReadOutbox, ownerUnread int
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT top_message_id, read_inbox_max_id, read_outbox_max_id, unread_count
|
||||
FROM channel_dialogs
|
||||
WHERE channel_id = $1 AND user_id = $2`, channelID, owner.ID).Scan(&ownerTop, &ownerReadInbox, &ownerReadOutbox, &ownerUnread); err != nil {
|
||||
t.Fatalf("read owner dialog row after send: %v", err)
|
||||
}
|
||||
// 发送者自己的 outbox 回执水位不得随发送推进:同账号另一设备会把它当成
|
||||
// "对端已读"渲染双勾。推进只能来自 peer 的 readHistory(见下方断言)。
|
||||
if ownerTop != sent.Message.ID || ownerReadInbox != sent.Message.ID || ownerReadOutbox != 0 || ownerUnread != 0 {
|
||||
t.Fatalf("owner dialog row top=%d read_in=%d read_out=%d unread=%d, want top/read_in %d, read_out 0 before peer read, unread 0", ownerTop, ownerReadInbox, ownerReadOutbox, ownerUnread, sent.Message.ID)
|
||||
ownerViewBefore, err := channels.GetChannelDialogs(ctx, owner.ID, []int64{channelID})
|
||||
if err != nil {
|
||||
t.Fatalf("get owner dialogs after send: %v", err)
|
||||
}
|
||||
if len(ownerViewBefore.Dialogs) != 1 || ownerViewBefore.Dialogs[0].TopMessage != sent.Message.ID ||
|
||||
ownerViewBefore.Dialogs[0].ReadInboxMaxID != sent.Message.ID ||
|
||||
ownerViewBefore.Dialogs[0].ReadOutboxMaxID != 0 || ownerViewBefore.Dialogs[0].UnreadCount != 0 {
|
||||
t.Fatalf("owner derived dialog = %+v, want top/read_in %d, read_out 0 before peer read, unread 0", ownerViewBefore.Dialogs, sent.Message.ID)
|
||||
}
|
||||
|
||||
var ownerMemberReadInbox, ownerMemberReadOutbox int
|
||||
|
|
@ -581,15 +580,15 @@ func TestChannelStoreSendMessageSkipDeliveryAdvancesReadBoundary(t *testing.T) {
|
|||
t.Fatalf("hidden recipients = %+v, want bot skipped", hidden.Recipients)
|
||||
}
|
||||
|
||||
var botDialogTop, botReadInbox, botUnread int
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT top_message_id, read_inbox_max_id, unread_count
|
||||
FROM channel_dialogs
|
||||
WHERE channel_id = $1 AND user_id = $2`, channelID, bot.ID).Scan(&botDialogTop, &botReadInbox, &botUnread); err != nil {
|
||||
t.Fatalf("read bot dialog after hidden: %v", err)
|
||||
// H4a:skip 成员的读边界推进只写 channel_members 行;unread 派生自动把被隐藏
|
||||
// 消息排除(read boundary 已越过它)。
|
||||
hiddenView, err := channels.GetChannelDialogs(ctx, bot.ID, []int64{channelID})
|
||||
if err != nil {
|
||||
t.Fatalf("get bot dialogs after hidden: %v", err)
|
||||
}
|
||||
if botDialogTop != created.Message.ID {
|
||||
t.Fatalf("bot dialog after hidden top=%d, want unchanged create top %d", botDialogTop, created.Message.ID)
|
||||
if len(hiddenView.Dialogs) != 1 || hiddenView.Dialogs[0].UnreadCount != 0 ||
|
||||
hiddenView.Dialogs[0].ReadInboxMaxID != hidden.Message.ID {
|
||||
t.Fatalf("bot derived dialog after hidden = %+v, want unread 0 read %d", hiddenView.Dialogs, hidden.Message.ID)
|
||||
}
|
||||
var memberReadInbox int
|
||||
if err := pool.QueryRow(ctx, `
|
||||
|
|
@ -615,15 +614,14 @@ WHERE channel_id = $1 AND user_id = $2`, channelID, bot.ID).Scan(&memberReadInbo
|
|||
if !containsInt64ForPostgresTest(visible.Recipients, bot.ID) {
|
||||
t.Fatalf("visible recipients = %+v, want bot", visible.Recipients)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT top_message_id, read_inbox_max_id, unread_count
|
||||
FROM channel_dialogs
|
||||
WHERE channel_id = $1 AND user_id = $2`, channelID, bot.ID).Scan(&botDialogTop, &botReadInbox, &botUnread); err != nil {
|
||||
t.Fatalf("read bot dialog after visible: %v", err)
|
||||
visibleView, err := channels.GetChannelDialogs(ctx, bot.ID, []int64{channelID})
|
||||
if err != nil {
|
||||
t.Fatalf("get bot dialogs after visible: %v", err)
|
||||
}
|
||||
if botDialogTop != visible.Message.ID || botReadInbox != hidden.Message.ID || botUnread != 1 {
|
||||
t.Fatalf("bot dialog after visible top=%d read=%d unread=%d, want top %d read hidden %d unread visible-only",
|
||||
botDialogTop, botReadInbox, botUnread, visible.Message.ID, hidden.Message.ID)
|
||||
if len(visibleView.Dialogs) != 1 || visibleView.Dialogs[0].TopMessage != visible.Message.ID ||
|
||||
visibleView.Dialogs[0].ReadInboxMaxID != hidden.Message.ID || visibleView.Dialogs[0].UnreadCount != 1 {
|
||||
t.Fatalf("bot derived dialog after visible = %+v, want top %d read hidden %d unread visible-only",
|
||||
visibleView.Dialogs, visible.Message.ID, hidden.Message.ID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,22 +40,16 @@ func channelDialogDynamicUnreadExistsSQL(readInboxExpr, topIDExpr string) string
|
|||
)`, readInboxExpr, topIDExpr)
|
||||
}
|
||||
|
||||
// channelDialogVisibleUnreadCountSQL 恒走读时动态派生(性能审计 H4a):broadcast/大群
|
||||
// 自 P0-1 起即读时派生并经生产验证,≤1000 megagroup 也统一到同一路径后,发送事务不再
|
||||
// 对全员 upsert channel_dialogs,`channel_dialogs.unread_count` 缓存列不再被任何读路径
|
||||
// 消费(真值 = read boundary + 可见 incoming 消息重算)。
|
||||
func channelDialogVisibleUnreadCountSQL(readInboxExpr, topIDExpr string) string {
|
||||
dynamicCount := channelDialogDynamicUnreadCountSQL(readInboxExpr, topIDExpr)
|
||||
// LEAST 钳制缓存分支 d.unread_count(小群走缓存列,可能因逐条自增长过上界)到 P1-v 上界;
|
||||
// 动态分支已被 LIMIT 子查询钳过,外层 LEAST 对两分支统一封顶,保证下发角标 ≤ 上界。
|
||||
return fmt.Sprintf(`LEAST(CASE
|
||||
WHEN c.broadcast OR c.participants_count > %d THEN %s
|
||||
ELSE COALESCE(d.unread_count, %s)
|
||||
END, %d)`, domain.MaxSynchronousChannelDialogFanout, dynamicCount, dynamicCount, domain.MaxDialogUnreadCount)
|
||||
return channelDialogDynamicUnreadCountSQL(readInboxExpr, topIDExpr)
|
||||
}
|
||||
|
||||
func channelDialogHasUnreadSQL(readInboxExpr, topIDExpr string) string {
|
||||
dynamicUnread := channelDialogDynamicUnreadExistsSQL(readInboxExpr, topIDExpr)
|
||||
return fmt.Sprintf(`CASE
|
||||
WHEN c.broadcast OR c.participants_count > %d THEN %s
|
||||
ELSE COALESCE(d.unread_count > 0, %s)
|
||||
END`, domain.MaxSynchronousChannelDialogFanout, dynamicUnread, dynamicUnread)
|
||||
return channelDialogDynamicUnreadExistsSQL(readInboxExpr, topIDExpr)
|
||||
}
|
||||
|
||||
func (s *ChannelStore) SetChannelDialogUnreadMark(ctx context.Context, userID, channelID int64, unread bool) (bool, error) {
|
||||
|
|
|
|||
|
|
@ -79,15 +79,14 @@ func TestChannelStoreReadOutboxDoesNotRegressSenderDialogUnread(t *testing.T) {
|
|||
t.Fatalf("send member message: %v", err)
|
||||
}
|
||||
|
||||
var storedReadInbox, storedUnread int
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT read_inbox_max_id, unread_count
|
||||
FROM channel_dialogs
|
||||
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&storedReadInbox, &storedUnread); err != nil {
|
||||
t.Fatalf("read member dialog after self send: %v", err)
|
||||
// H4a:发送不再写全员 dialog 行;sender 自己的 read_inbox 在发送事务内推进
|
||||
// channel_members 行,读侧 GREATEST(dialog, member) 派生。
|
||||
memberView, err := channels.GetChannelDialogs(ctx, member.ID, []int64{channelID})
|
||||
if err != nil {
|
||||
t.Fatalf("get member dialogs after self send: %v", err)
|
||||
}
|
||||
if storedReadInbox != memberMsg.Message.ID || storedUnread != 0 {
|
||||
t.Fatalf("member dialog after self send read=%d unread=%d, want read %d unread 0", storedReadInbox, storedUnread, memberMsg.Message.ID)
|
||||
if len(memberView.Dialogs) != 1 || memberView.Dialogs[0].ReadInboxMaxID != memberMsg.Message.ID || memberView.Dialogs[0].UnreadCount != 0 {
|
||||
t.Fatalf("member derived dialog after self send = %+v, want read %d unread 0", memberView.Dialogs, memberMsg.Message.ID)
|
||||
}
|
||||
|
||||
read, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
|
||||
|
|
@ -102,14 +101,12 @@ WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&storedReadI
|
|||
if len(read.OutboxUpdates) != 1 || read.OutboxUpdates[0].UserID != member.ID || read.OutboxUpdates[0].MaxID != memberMsg.Message.ID {
|
||||
t.Fatalf("read outbox updates = %+v, want member max id %d", read.OutboxUpdates, memberMsg.Message.ID)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT read_inbox_max_id, unread_count
|
||||
FROM channel_dialogs
|
||||
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&storedReadInbox, &storedUnread); err != nil {
|
||||
t.Fatalf("read member dialog after owner read: %v", err)
|
||||
memberView, err = channels.GetChannelDialogs(ctx, member.ID, []int64{channelID})
|
||||
if err != nil {
|
||||
t.Fatalf("get member dialogs after owner read: %v", err)
|
||||
}
|
||||
if storedReadInbox != memberMsg.Message.ID || storedUnread != 0 {
|
||||
t.Fatalf("member dialog after owner read read=%d unread=%d, want read %d unread 0", storedReadInbox, storedUnread, memberMsg.Message.ID)
|
||||
if len(memberView.Dialogs) != 1 || memberView.Dialogs[0].ReadInboxMaxID != memberMsg.Message.ID || memberView.Dialogs[0].UnreadCount != 0 {
|
||||
t.Fatalf("member derived dialog after owner read = %+v, want read %d unread 0", memberView.Dialogs, memberMsg.Message.ID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -680,7 +677,7 @@ WHERE id = $1`, channelID, domain.MaxSynchronousChannelDialogFanout+1); err != n
|
|||
}
|
||||
}
|
||||
|
||||
func TestChannelStoreSmallMegagroupDeleteRefreshesDialogUnreadCache(t *testing.T) {
|
||||
func TestChannelStoreSmallMegagroupDeleteDerivesUnread(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
|
|
@ -761,15 +758,13 @@ func TestChannelStoreSmallMegagroupDeleteRefreshesDialogUnreadCache(t *testing.T
|
|||
t.Fatalf("send third message: %v", err)
|
||||
}
|
||||
|
||||
var cachedTop, cachedUnread int
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT top_message_id, unread_count
|
||||
FROM channel_dialogs
|
||||
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&cachedTop, &cachedUnread); err != nil {
|
||||
t.Fatalf("read cached dialog before delete: %v", err)
|
||||
// H4a:dialog unread 读时派生,不再依赖 channel_dialogs 缓存列刷新。
|
||||
beforeDelete, err := channels.ListChannelDialogs(ctx, member.ID, domain.DialogFilter{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("list dialogs before delete: %v", err)
|
||||
}
|
||||
if cachedTop != third.Message.ID || cachedUnread != 3 {
|
||||
t.Fatalf("cached dialog before delete top=%d unread=%d, want top %d unread 3", cachedTop, cachedUnread, third.Message.ID)
|
||||
if len(beforeDelete.Dialogs) != 1 || beforeDelete.Dialogs[0].TopMessage != third.Message.ID || beforeDelete.Dialogs[0].UnreadCount != 3 {
|
||||
t.Fatalf("derived dialog before delete = %+v, want top %d unread 3", beforeDelete.Dialogs, third.Message.ID)
|
||||
}
|
||||
|
||||
if _, err := channels.DeleteChannelMessages(ctx, domain.DeleteChannelMessagesRequest{
|
||||
|
|
@ -780,15 +775,6 @@ WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&cachedTop,
|
|||
}); err != nil {
|
||||
t.Fatalf("delete middle unread message: %v", err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT top_message_id, unread_count
|
||||
FROM channel_dialogs
|
||||
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&cachedTop, &cachedUnread); err != nil {
|
||||
t.Fatalf("read cached dialog after middle delete: %v", err)
|
||||
}
|
||||
if cachedTop != third.Message.ID || cachedUnread != 2 {
|
||||
t.Fatalf("cached dialog after middle delete top=%d unread=%d, want top %d unread 2", cachedTop, cachedUnread, third.Message.ID)
|
||||
}
|
||||
list, err := channels.ListChannelDialogs(ctx, member.ID, domain.DialogFilter{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("list dialogs after middle delete: %v", err)
|
||||
|
|
@ -805,15 +791,6 @@ WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&cachedTop,
|
|||
}); err != nil {
|
||||
t.Fatalf("delete top unread message: %v", err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT top_message_id, unread_count
|
||||
FROM channel_dialogs
|
||||
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&cachedTop, &cachedUnread); err != nil {
|
||||
t.Fatalf("read cached dialog after top delete: %v", err)
|
||||
}
|
||||
if cachedTop != first.Message.ID || cachedUnread != 1 {
|
||||
t.Fatalf("cached dialog after top delete top=%d unread=%d, want top %d unread 1", cachedTop, cachedUnread, first.Message.ID)
|
||||
}
|
||||
list, err = channels.ListChannelDialogs(ctx, member.ID, domain.DialogFilter{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("list dialogs after top delete: %v", err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue