fix: sync public channel preview updates

This commit is contained in:
iamxvbaba 2026-07-24 14:50:17 +08:00
parent 2289a31f46
commit b4aaf57d6b
27 changed files with 877 additions and 128 deletions

View file

@ -634,9 +634,9 @@ func (d *channelFanoutDispatcher) Enqueue(reqCtx context.Context, job channelFan
d.releaseQueuedJob(job)
}
d.dropped.Add(1)
if job.scope != channelFanoutMembers || job.pts <= 0 {
// 当前所有 enqueue 入口均为 members + durable pts;若未来新增其它 scope,必须先
// 定义其 overflow 恢复面,不能误把 viewer-only/no-pts 更新伪装成 channel nudge。
if job.scope != channelFanoutMembers && job.scope != channelFanoutMessageBox || job.pts <= 0 {
// 只有 durable member/message-box payload 可折叠为 channel nudge;viewer-only/no-pts
// 更新没有 difference 恢复契约,不能伪装成 channel PTS 水位。
d.log.Error("channel fanout queue full for non-coalescible job; overflow contract violated",
zap.Int64("channel_id", job.channelID), zap.Int("pts", job.pts), zap.Int("scope", int(job.scope)))
d.enqueueMu.RUnlock()
@ -867,7 +867,7 @@ func (r *Router) runChannelFanoutOverflowNudge(ctx context.Context, channelID in
if r.deps.Sessions == nil || channelID == 0 || pts <= 0 {
return true
}
return r.nudgeBeyondCapChannelMembers(ctx, channelID, pts, nil)
return r.nudgeBeyondCapChannelMessageAudience(ctx, channelID, pts, nil)
}
// runChannelFanoutJob 执行一条 fan-out:与同步 pushChannelUpdatesWithScope 等价,区别是
@ -918,6 +918,8 @@ func (r *Router) runChannelFanoutJob(ctx context.Context, job channelFanoutJob)
// 仅对会推进客户端 channel PtsWaiter 的真实 payload(members scope + 带 channel pts)做。
if job.scope == channelFanoutMembers && job.pts > 0 {
r.nudgeBeyondCapChannelMembers(pushCtx, job.channelID, job.pts, seen)
} else if job.scope == channelFanoutMessageBox && job.pts > 0 {
r.nudgeBeyondCapChannelMessageAudience(pushCtx, job.channelID, job.pts, seen)
}
}
@ -975,7 +977,7 @@ func (r *Router) enqueueChannelMessageFanout(ctx context.Context, originUserID i
fanoutCache := newViewerPeerCache(r)
ownerIDs := channelMessageFanoutOwnerIDs(res, extraUserIDs)
skip := skipDeliverySet(res.SkipDeliveryUserIDs)
r.enqueueChannelFanoutWithPrefetch(ctx, channelFanoutMembers, originUserID, res.Channel.ID, res.Event.Pts, res.Recipients,
r.enqueueChannelFanoutWithPrefetch(ctx, channelFanoutMessageBox, originUserID, res.Channel.ID, res.Event.Pts, res.Recipients,
0,
func(bgCtx context.Context, viewers []int64) {
r.prefetchChannelFanoutUsers(bgCtx, fanoutCache, viewers, ownerIDs)
@ -1056,7 +1058,7 @@ func (r *Router) enqueueChannelEditMessageFanout(ctx context.Context, originUser
fanoutCache := newViewerPeerCache(r)
ownerIDs := channelEditMessageFanoutOwnerIDs(res)
nudgePts := max(res.Event.Pts, res.ServiceEvent.Pts)
r.enqueueChannelFanoutWithPrefetch(ctx, channelFanoutMembers, originUserID, res.Channel.ID, nudgePts, res.Recipients,
r.enqueueChannelFanoutWithPrefetch(ctx, channelFanoutMessageBox, originUserID, res.Channel.ID, nudgePts, res.Recipients,
0,
func(bgCtx context.Context, viewers []int64) {
r.prefetchChannelFanoutUsers(bgCtx, fanoutCache, viewers, ownerIDs)
@ -1073,7 +1075,7 @@ func (r *Router) enqueueChannelMessagesFanout(ctx context.Context, originUserID,
r.enqueueBotAPIChannelMessagesUpdate(ctx, originUserID, results)
fanoutCache := newViewerPeerCache(r)
ownerIDs := channelMessagesFanoutOwnerIDs(results, extraUserIDs)
r.enqueueChannelFanoutWithPrefetch(ctx, channelFanoutMembers, originUserID, channelID, pts, recipients,
r.enqueueChannelFanoutWithPrefetch(ctx, channelFanoutMessageBox, originUserID, channelID, pts, recipients,
int64(len(results))*(64<<10),
func(bgCtx context.Context, viewers []int64) {
r.prefetchChannelFanoutUsers(bgCtx, fanoutCache, viewers, ownerIDs)
@ -1136,3 +1138,81 @@ func (r *Router) nudgeBeyondCapChannelMembers(ctx context.Context, channelID int
}
return ctx.Err() == nil
}
// nudgeBeyondCapChannelMessageAudience extends member recovery to users with an
// unexpired public-channel short-poll subscription. Subscribers are selected
// first (normally a tiny set), then the remaining bounded capacity is filled
// with joined members. One authoritative batched audience check prevents stale
// runtime indexes from leaking even the channel id/pts to revoked viewers.
func (r *Router) nudgeBeyondCapChannelMessageAudience(ctx context.Context, channelID int64, pts int, delivered map[int64]struct{}) bool {
if r.deps.Sessions == nil || channelID == 0 || pts <= 0 {
return true
}
if r.deps.Channels == nil {
return r.nudgeBeyondCapChannelMembers(ctx, channelID, pts, delivered)
}
audience, ok := r.deps.Channels.(ChannelMessageAudienceService)
if !ok {
return r.nudgeBeyondCapChannelMembers(ctx, channelID, pts, delivered)
}
limit := r.channelNudgeMaxTargets()
excluded := make(map[int64]struct{}, len(delivered)+16)
for userID := range delivered {
excluded[userID] = struct{}{}
}
candidates := make([]int64, 0, min(limit, 64))
if subscriptions, ok := r.deps.Sessions.(ChannelSubscriptionProvider); ok {
for _, userID := range subscriptions.OnlineChannelSubscriberUserIDsExcluding(channelID, excluded, limit) {
if userID == 0 {
continue
}
excluded[userID] = struct{}{}
candidates = append(candidates, userID)
if len(candidates) >= limit {
break
}
}
}
if len(candidates) < limit {
if members, ok := r.deps.Sessions.(ChannelNudgeProvider); ok {
for _, userID := range members.OnlineChannelMemberUserIDsExcluding(channelID, excluded, limit-len(candidates)) {
if userID == 0 {
continue
}
excluded[userID] = struct{}{}
candidates = append(candidates, userID)
}
}
}
if len(candidates) == 0 {
return true
}
targets, err := audience.FilterMessageAudienceIDs(ctx, channelID, candidates)
if err != nil {
r.log.Warn("channel message audience nudge authorization failed",
zap.Int64("channel_id", channelID), zap.Int("pts", pts), zap.Error(err))
return false
}
if len(targets) == 0 {
return true
}
date := int(r.clock.Now().Unix())
tooLong := &tg.UpdateChannelTooLong{ChannelID: channelID}
tooLong.SetPts(pts)
updates := &tg.Updates{
Updates: []tg.UpdateClass{tooLong},
Users: []tg.UserClass{},
Chats: []tg.ChatClass{},
Date: date,
Seq: 0,
}
for _, userID := range targets {
select {
case <-ctx.Done():
return false
default:
}
r.pushUserUpdates(ctx, userID, updates)
}
return true
}