fix: sync session membership routing fixes
This commit is contained in:
parent
7e64d9c30e
commit
56d995474c
20 changed files with 639 additions and 102 deletions
|
|
@ -101,8 +101,8 @@ func TestAuthLoginTokenAcceptedByAndroidBindsTargetSession(t *testing.T) {
|
|||
}
|
||||
|
||||
snap := sessions.snapshot()
|
||||
if sessions.scopedAuthKeyID != targetRawAuthKeyID {
|
||||
t.Fatalf("scoped raw auth key = %x, want %x", sessions.scopedAuthKeyID, targetRawAuthKeyID)
|
||||
if got := sessions.scopedAuthKey(); got != targetRawAuthKeyID {
|
||||
t.Fatalf("scoped raw auth key = %x, want %x", got, targetRawAuthKeyID)
|
||||
}
|
||||
if snap.sessionID != targetSession || snap.userID != scannerUserID || !snap.userResolved {
|
||||
t.Fatalf("target session snapshot = %+v, want session/user/resolved %d/%d/true", snap, targetSession, scannerUserID)
|
||||
|
|
@ -110,7 +110,7 @@ func TestAuthLoginTokenAcceptedByAndroidBindsTargetSession(t *testing.T) {
|
|||
if snap.messageType != proto.MessageFromServer {
|
||||
t.Fatalf("push message type = %v, want MessageFromServer", snap.messageType)
|
||||
}
|
||||
if !sessions.immediatePush {
|
||||
if !sessions.immediatePushSeen() {
|
||||
t.Fatal("login token update was not pushed through the immediate pre-auth path")
|
||||
}
|
||||
short, ok := snap.message.(*tg.UpdateShort)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ func (r *Router) syncSessionChannelMemberships(ctx context.Context, userID int64
|
|||
if !ok {
|
||||
return
|
||||
}
|
||||
// 在读取持久成员列表之前采样修订号:读取窗口内若发生增量 join/leave
|
||||
// (AddUserChannelMembership/RemoveUserChannelMembership),全量替换会覆盖增量,
|
||||
// SetSessionChannelMemberships 据此改走合并路径并保持未就绪重试。
|
||||
expectedGen := provider.ChannelMembershipGeneration(rawAuthKeyID, sessionID)
|
||||
channelIDs := make([]int64, 0, channelMembershipSyncPageSize)
|
||||
after := int64(0)
|
||||
for {
|
||||
|
|
@ -82,7 +86,7 @@ func (r *Router) syncSessionChannelMemberships(ctx context.Context, userID int64
|
|||
break
|
||||
}
|
||||
}
|
||||
provider.SetSessionChannelMemberships(rawAuthKeyID, sessionID, userID, channelIDs)
|
||||
provider.SetSessionChannelMemberships(rawAuthKeyID, sessionID, userID, channelIDs, expectedGen)
|
||||
}
|
||||
|
||||
func (r *Router) addOnlineChannelMemberships(channelID int64, userIDs ...int64) {
|
||||
|
|
|
|||
|
|
@ -902,6 +902,70 @@ func TestChannelEditBannedKickNotifiesKickedViewer(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestChannelEditBannedMaintainsOnlineMembershipIndex 验证 editBanned 与 editAdmin 对称
|
||||
// 维护在线成员推送路由索引:踢出/封禁后必须立即摘除(否则被踢在线成员保留 stale
|
||||
// byMemberChannel 条目直到断线,占用实时 fan-out cap 名额,且群通话推送会继续投递);
|
||||
// 仅限制权限(仍为 active 成员)时索引保留。
|
||||
func TestChannelEditBannedMaintainsOnlineMembershipIndex(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 61, Phone: "15550002261", FirstName: "Owner"})
|
||||
target, _ := userStore.Create(ctx, domain.User{AccessHash: 62, Phone: "15550002262", FirstName: "Target"})
|
||||
sessions := &captureSessions{}
|
||||
r := New(Config{}, Deps{
|
||||
Users: appusers.NewService(userStore),
|
||||
Channels: appchannels.NewService(memory.NewChannelStore()),
|
||||
Sessions: sessions,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
created, err := r.onMessagesCreateChat(WithUserID(ctx, owner.ID), &tg.MessagesCreateChatRequest{
|
||||
Users: []tg.InputUserClass{&tg.InputUser{UserID: target.ID, AccessHash: target.AccessHash}},
|
||||
Title: "Kick Index",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create chat: %v", err)
|
||||
}
|
||||
channel := created.Updates.(*tg.Updates).Chats[0].(*tg.Channel)
|
||||
input := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||||
contains := func(ids []int64, id int64) bool {
|
||||
for _, v := range ids {
|
||||
if v == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if !contains(sessions.onlineChannelMemberIDs(channel.ID), target.ID) {
|
||||
t.Fatalf("membership index after create = %v, want target %d", sessions.onlineChannelMemberIDs(channel.ID), target.ID)
|
||||
}
|
||||
|
||||
// 仅限制发言(仍为 active 成员):索引保留。
|
||||
if _, err := r.onChannelsEditBanned(WithUserID(ctx, owner.ID), &tg.ChannelsEditBannedRequest{
|
||||
Channel: input,
|
||||
Participant: &tg.InputPeerUser{UserID: target.ID, AccessHash: target.AccessHash},
|
||||
BannedRights: tg.ChatBannedRights{SendMessages: true},
|
||||
}); err != nil {
|
||||
t.Fatalf("restrict member: %v", err)
|
||||
}
|
||||
if !contains(sessions.onlineChannelMemberIDs(channel.ID), target.ID) {
|
||||
t.Fatalf("membership index after restrict = %v, restricted member must stay routed", sessions.onlineChannelMemberIDs(channel.ID))
|
||||
}
|
||||
|
||||
// 踢出(view_messages):索引必须立即摘除,其他成员不受影响。
|
||||
if _, err := r.onChannelsEditBanned(WithUserID(ctx, owner.ID), &tg.ChannelsEditBannedRequest{
|
||||
Channel: input,
|
||||
Participant: &tg.InputPeerUser{UserID: target.ID, AccessHash: target.AccessHash},
|
||||
BannedRights: tg.ChatBannedRights{ViewMessages: true},
|
||||
}); err != nil {
|
||||
t.Fatalf("kick member: %v", err)
|
||||
}
|
||||
if contains(sessions.onlineChannelMemberIDs(channel.ID), target.ID) {
|
||||
t.Fatalf("membership index after kick = %v, kicked member must be removed", sessions.onlineChannelMemberIDs(channel.ID))
|
||||
}
|
||||
if !contains(sessions.onlineChannelMemberIDs(channel.ID), owner.ID) {
|
||||
t.Fatalf("membership index after kick = %v, owner must survive", sessions.onlineChannelMemberIDs(channel.ID))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelInviteKickedMemberRPC(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
|
|
|
|||
|
|
@ -450,6 +450,14 @@ func (r *Router) onChannelsEditBanned(ctx context.Context, req *tg.ChannelsEditB
|
|||
return nil, channelAdminErr(err)
|
||||
}
|
||||
r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID)
|
||||
// 与 onChannelsEditAdmin 对称维护在线成员路由索引:踢出/封禁后立即摘除,
|
||||
// 否则被踢在线成员会留着 stale byMemberChannel 条目直到断线(占用实时
|
||||
// fan-out cap 名额,且群通话推送等未过 PG 复核的路径会继续投递)。
|
||||
if res.Participant.Status == domain.ChannelMemberActive {
|
||||
r.addOnlineChannelMemberships(res.Channel.ID, res.Participant.UserID)
|
||||
} else {
|
||||
r.removeOnlineChannelMemberships(res.Channel.ID, res.Participant.UserID)
|
||||
}
|
||||
if res.Participant.Status == domain.ChannelMemberKicked && res.Previous.Status == domain.ChannelMemberActive {
|
||||
r.recordChannelStateForUser(ctx, res.Participant.UserID, res.Channel.ID, false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ func TestChannelSendHistoryAndDifferenceRPC(t *testing.T) {
|
|||
if pushed.userID != friend.ID || pushed.sessionID != 77 || pushed.messageType != proto.MessageFromServer {
|
||||
t.Fatalf("pushed channel update = user %d exclude session %d type %v, want friend/exclude/from_server", pushed.userID, pushed.sessionID, pushed.messageType)
|
||||
}
|
||||
if gotAuthKeyID := sessions.scopedAuthKeyID; gotAuthKeyID != authKeyID {
|
||||
if gotAuthKeyID := sessions.scopedAuthKey(); gotAuthKeyID != authKeyID {
|
||||
t.Fatalf("exclude auth_key_id = %x, want %x", gotAuthKeyID, authKeyID)
|
||||
}
|
||||
pushedUpdates, ok := pushed.message.(*tg.Updates)
|
||||
|
|
@ -156,7 +156,7 @@ func TestChannelSendHistoryAndDifferenceRPC(t *testing.T) {
|
|||
if contentPush.userID != friend.ID || contentPush.sessionID != 88 || contentPush.messageType != proto.MessageFromServer {
|
||||
t.Fatalf("content-read push = user %d exclude session %d type %v, want friend/exclude/from_server", contentPush.userID, contentPush.sessionID, contentPush.messageType)
|
||||
}
|
||||
if gotAuthKeyID := sessions.scopedAuthKeyID; gotAuthKeyID != contentAuthKeyID {
|
||||
if gotAuthKeyID := sessions.scopedAuthKey(); gotAuthKeyID != contentAuthKeyID {
|
||||
t.Fatalf("content-read exclude auth_key_id = %x, want %x", gotAuthKeyID, contentAuthKeyID)
|
||||
}
|
||||
contentUpdates, ok := contentPush.message.(*tg.Updates)
|
||||
|
|
@ -379,7 +379,7 @@ func TestChannelsReadMessageContentsClearsUnreadReactionAndPushesUpdate(t *testi
|
|||
if pushed.userID != owner.ID || pushed.sessionID != 99 || pushed.messageType != proto.MessageFromServer {
|
||||
t.Fatalf("reaction read push = user %d session %d type %v, want owner/exclude/from_server", pushed.userID, pushed.sessionID, pushed.messageType)
|
||||
}
|
||||
if gotAuthKeyID := sessions.scopedAuthKeyID; gotAuthKeyID != contentAuthKeyID {
|
||||
if gotAuthKeyID := sessions.scopedAuthKey(); gotAuthKeyID != contentAuthKeyID {
|
||||
t.Fatalf("reaction read exclude auth_key_id = %x, want %x", gotAuthKeyID, contentAuthKeyID)
|
||||
}
|
||||
pushedUpdates, ok := pushed.message.(*tg.Updates)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,14 @@ type SessionUpdatesStateProvider interface {
|
|||
ReceivesUpdatesForAuthKey(rawAuthKeyID [8]byte, sessionID int64) bool
|
||||
}
|
||||
|
||||
// ClientLayerBinder 把协商 TL layer 即时下推到连接(可选能力)。
|
||||
// invokeWithLayer 在 Dispatch 入口被观测到时立即调用,使同一请求 handler 执行期间
|
||||
// 触发的 pending flush / 并发 push 就已按正确 layer 降级;不实现时连接层只能靠
|
||||
// Dispatch 返回后的兜底刷新,重连老客户端首条 RPC 期间的推送会漏降级。
|
||||
type ClientLayerBinder interface {
|
||||
SetClientLayerForAuthKey(rawAuthKeyID [8]byte, sessionID int64, layer int)
|
||||
}
|
||||
|
||||
// SessionTerminator 暴露按业务 auth_key 强制断开活跃连接的能力(可选)。
|
||||
// 授权撤销(被踢设备)必须断开连接:出站推送用连接持有的密钥加密、不回查授权,
|
||||
// perm-key 连接的授权缓存也只有断开重连才会重新回查授权表。
|
||||
|
|
@ -130,7 +138,11 @@ type OnlineUserProvider interface {
|
|||
TrackChannelInterest(rawAuthKeyID [8]byte, sessionID, userID int64, channelIDs []int64)
|
||||
ClearChannelInterest(rawAuthKeyID [8]byte, sessionID, userID int64)
|
||||
OnlineChannelUserIDs(channelID int64, limit int) []int64
|
||||
SetSessionChannelMemberships(rawAuthKeyID [8]byte, sessionID, userID int64, channelIDs []int64)
|
||||
// ChannelMembershipGeneration / SetSessionChannelMemberships 配对使用:调用方在读取
|
||||
// 持久成员列表前采样修订号,落地时带回;期间发生增量 Add/Remove 时 Set 走合并路径
|
||||
// 并保持未就绪,由下一条 RPC 重试全量同步(防全量替换覆盖窗口内的增量 join/leave)。
|
||||
ChannelMembershipGeneration(rawAuthKeyID [8]byte, sessionID int64) int64
|
||||
SetSessionChannelMemberships(rawAuthKeyID [8]byte, sessionID, userID int64, channelIDs []int64, expectedGen int64)
|
||||
AddUserChannelMembership(userID, channelID int64)
|
||||
RemoveUserChannelMembership(userID, channelID int64)
|
||||
OnlineChannelMemberUserIDs(channelID int64, limit int) []int64
|
||||
|
|
|
|||
|
|
@ -477,7 +477,7 @@ func TestMessagesSaveDraftPushesDraftUpdateToOtherSessions(t *testing.T) {
|
|||
if got.userID != userID || got.sessionID != 66 || got.messageType != proto.MessageFromServer {
|
||||
t.Fatalf("push = user %d exclude session %d type %v, want self/exclude/from_server", got.userID, got.sessionID, got.messageType)
|
||||
}
|
||||
if gotAuthKeyID := sessions.scopedAuthKeyID; gotAuthKeyID != authKeyID {
|
||||
if gotAuthKeyID := sessions.scopedAuthKey(); gotAuthKeyID != authKeyID {
|
||||
t.Fatalf("exclude auth_key_id = %x, want %x", gotAuthKeyID, authKeyID)
|
||||
}
|
||||
updates, ok := got.message.(*tg.Updates)
|
||||
|
|
|
|||
|
|
@ -516,7 +516,7 @@ func TestMessagesSetTypingPushesUserTypingUpdate(t *testing.T) {
|
|||
if got.userID != 1000000002 || got.sessionID != 55 || got.messageType != proto.MessageFromServer {
|
||||
t.Fatalf("push = user %d exclude session %d type %v, want target/exclude/from_server", got.userID, got.sessionID, got.messageType)
|
||||
}
|
||||
if gotAuthKeyID := sessions.scopedAuthKeyID; gotAuthKeyID != authKeyID {
|
||||
if gotAuthKeyID := sessions.scopedAuthKey(); gotAuthKeyID != authKeyID {
|
||||
t.Fatalf("exclude auth_key_id = %x, want %x", gotAuthKeyID, authKeyID)
|
||||
}
|
||||
updateShort, ok := got.message.(*tg.UpdateShort)
|
||||
|
|
@ -623,7 +623,7 @@ func TestMessagesSetTypingPushesChannelTypingTopMsgID(t *testing.T) {
|
|||
if got.userID != memberID || got.sessionID != 77 || got.messageType != proto.MessageFromServer {
|
||||
t.Fatalf("channel typing push = user %d exclude session %d type %v, want member/exclude/from_server", got.userID, got.sessionID, got.messageType)
|
||||
}
|
||||
if gotAuthKeyID := sessions.scopedAuthKeyID; gotAuthKeyID != authKeyID {
|
||||
if gotAuthKeyID := sessions.scopedAuthKey(); gotAuthKeyID != authKeyID {
|
||||
t.Fatalf("exclude auth_key_id = %x, want %x", gotAuthKeyID, authKeyID)
|
||||
}
|
||||
updates, ok := got.message.(*tg.Updates)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -97,8 +98,8 @@ func TestOutboxDispatcherUsesScopedAuthKeyExclusion(t *testing.T) {
|
|||
dispatcher := NewOutboxDispatcher(events, outbox, sessions, zaptest.NewLogger(t))
|
||||
dispatcher.DispatchOnce(context.Background())
|
||||
|
||||
if sessions.scopedAuthKeyID != excludeAuthKeyID || sessions.sessionID != 99 || sessions.userID != 1000000002 {
|
||||
t.Fatalf("scoped push = auth %x session %d user %d, want precise outbox exclusion", sessions.scopedAuthKeyID, sessions.sessionID, sessions.userID)
|
||||
if sessions.scopedAuthKey() != excludeAuthKeyID || sessions.sessionID != 99 || sessions.userID != 1000000002 {
|
||||
t.Fatalf("scoped push = auth %x session %d user %d, want precise outbox exclusion", sessions.scopedAuthKey(), sessions.sessionID, sessions.userID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -731,13 +732,34 @@ type captureDispatchOutbox struct {
|
|||
|
||||
type captureScopedSessions struct {
|
||||
*captureSessions
|
||||
// scopedMu 保护本层扩展字段:presence 等异步推送 goroutine 会并发写
|
||||
// scopedAuthKeyID,测试主 goroutine 并发读(race detector 抓过这里)。
|
||||
scopedMu sync.Mutex
|
||||
scopedAuthKeyID [8]byte
|
||||
immediatePush bool
|
||||
}
|
||||
|
||||
func (s *captureScopedSessions) setScopedAuthKeyID(rawAuthKeyID [8]byte) {
|
||||
s.scopedMu.Lock()
|
||||
s.scopedAuthKeyID = rawAuthKeyID
|
||||
s.scopedMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *captureScopedSessions) scopedAuthKey() [8]byte {
|
||||
s.scopedMu.Lock()
|
||||
defer s.scopedMu.Unlock()
|
||||
return s.scopedAuthKeyID
|
||||
}
|
||||
|
||||
func (s *captureScopedSessions) immediatePushSeen() bool {
|
||||
s.scopedMu.Lock()
|
||||
defer s.scopedMu.Unlock()
|
||||
return s.immediatePush
|
||||
}
|
||||
|
||||
func (s *captureScopedSessions) BindAuthKeyForSession(rawAuthKeyID [8]byte, sessionID int64, authKeyID [8]byte) {
|
||||
s.BindAuthKey(sessionID, authKeyID)
|
||||
s.scopedAuthKeyID = rawAuthKeyID
|
||||
s.setScopedAuthKeyID(rawAuthKeyID)
|
||||
}
|
||||
|
||||
func (s *captureScopedSessions) AuthKeyIDForSession([8]byte, int64) ([8]byte, bool) {
|
||||
|
|
@ -746,7 +768,7 @@ func (s *captureScopedSessions) AuthKeyIDForSession([8]byte, int64) ([8]byte, bo
|
|||
|
||||
func (s *captureScopedSessions) BindUserForAuthKey(rawAuthKeyID [8]byte, sessionID, userID int64) {
|
||||
s.BindUser(sessionID, userID)
|
||||
s.scopedAuthKeyID = rawAuthKeyID
|
||||
s.setScopedAuthKeyID(rawAuthKeyID)
|
||||
}
|
||||
|
||||
func (s *captureScopedSessions) UserIDForAuthKey([8]byte, int64) (int64, bool) {
|
||||
|
|
@ -760,18 +782,20 @@ func (s *captureScopedSessions) UserIDResolvedForAuthKey([8]byte, int64) (int64,
|
|||
func (s *captureScopedSessions) SetReceivesUpdatesForAuthKey([8]byte, int64, bool) {}
|
||||
|
||||
func (s *captureScopedSessions) PushToSessionForAuthKey(_ context.Context, rawAuthKeyID [8]byte, sessionID int64, t proto.MessageType, msg bin.Encoder) error {
|
||||
s.scopedAuthKeyID = rawAuthKeyID
|
||||
s.setScopedAuthKeyID(rawAuthKeyID)
|
||||
return s.PushToSession(context.Background(), sessionID, t, msg)
|
||||
}
|
||||
|
||||
func (s *captureScopedSessions) PushToSessionForAuthKeyImmediate(_ context.Context, rawAuthKeyID [8]byte, sessionID int64, t proto.MessageType, msg bin.Encoder) error {
|
||||
s.scopedMu.Lock()
|
||||
s.immediatePush = true
|
||||
s.scopedAuthKeyID = rawAuthKeyID
|
||||
s.scopedMu.Unlock()
|
||||
return s.PushToSession(context.Background(), sessionID, t, msg)
|
||||
}
|
||||
|
||||
func (s *captureScopedSessions) PushToUserExceptAuthKeySession(_ context.Context, userID int64, excludeAuthKeyID [8]byte, excludeSessionID int64, t proto.MessageType, msg bin.Encoder) (int, error) {
|
||||
s.scopedAuthKeyID = excludeAuthKeyID
|
||||
s.setScopedAuthKeyID(excludeAuthKeyID)
|
||||
return s.PushToUserExceptSession(context.Background(), userID, excludeSessionID, t, msg)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,12 +40,13 @@ func (s *groupCallSessions) OnlineUserIDsForCandidates(candidateUserIDs []int64,
|
|||
}
|
||||
return out
|
||||
}
|
||||
func (s *groupCallSessions) TrackChannelInterest([8]byte, int64, int64, []int64) {}
|
||||
func (s *groupCallSessions) ClearChannelInterest([8]byte, int64, int64) {}
|
||||
func (s *groupCallSessions) OnlineChannelUserIDs(int64, int) []int64 { return nil }
|
||||
func (s *groupCallSessions) SetSessionChannelMemberships([8]byte, int64, int64, []int64) {}
|
||||
func (s *groupCallSessions) AddUserChannelMembership(int64, int64) {}
|
||||
func (s *groupCallSessions) RemoveUserChannelMembership(int64, int64) {}
|
||||
func (s *groupCallSessions) TrackChannelInterest([8]byte, int64, int64, []int64) {}
|
||||
func (s *groupCallSessions) ClearChannelInterest([8]byte, int64, int64) {}
|
||||
func (s *groupCallSessions) OnlineChannelUserIDs(int64, int) []int64 { return nil }
|
||||
func (s *groupCallSessions) ChannelMembershipGeneration([8]byte, int64) int64 { return 0 }
|
||||
func (s *groupCallSessions) SetSessionChannelMemberships([8]byte, int64, int64, []int64, int64) {}
|
||||
func (s *groupCallSessions) AddUserChannelMembership(int64, int64) {}
|
||||
func (s *groupCallSessions) RemoveUserChannelMembership(int64, int64) {}
|
||||
func (s *groupCallSessions) OnlineChannelMemberUserIDs(channelID int64, limit int) []int64 {
|
||||
return append([]int64(nil), s.online...)
|
||||
}
|
||||
|
|
@ -148,6 +149,49 @@ func findUpdate[T tg.UpdateClass](t *testing.T, updates tg.UpdatesClass) T {
|
|||
return zero
|
||||
}
|
||||
|
||||
// TestGroupCallPushSkipsNonMemberOnlineCandidates 验证群通话推送收件人必须过一次
|
||||
// PG active 成员复核:在线成员索引 stale(如踢人窗口内、或索引维护缺口)时,
|
||||
// 非成员绝不能收到通话状态/参与者名单(信息泄漏),真实成员照常收到。
|
||||
func TestGroupCallPushSkipsNonMemberOnlineCandidates(t *testing.T) {
|
||||
f := newGroupCallFixture(t)
|
||||
ownerCtx := f.userCtx(f.owner, 11)
|
||||
// 模拟 stale 在线索引:outsider 不是频道成员却出现在候选里。
|
||||
f.sessions.online = []int64{f.owner.ID, f.member.ID, f.outsider.ID}
|
||||
|
||||
createRes, err := f.router.onPhoneCreateGroupCall(ownerCtx, &tg.PhoneCreateGroupCallRequest{
|
||||
Peer: &tg.InputPeerChannel{ChannelID: f.channel.ID, AccessHash: f.channel.AccessHash},
|
||||
RandomID: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("createGroupCall: %v", err)
|
||||
}
|
||||
callUpdate := findUpdate[*tg.UpdateGroupCall](t, createRes)
|
||||
call := callUpdate.Call.(*tg.GroupCall)
|
||||
f.sessions.reset()
|
||||
|
||||
if _, err := f.router.onPhoneJoinGroupCall(ownerCtx, &tg.PhoneJoinGroupCallRequest{
|
||||
Call: &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash},
|
||||
JoinAs: &tg.InputPeerSelf{},
|
||||
Params: groupCallJoinParams(t, 1001),
|
||||
}); err != nil {
|
||||
t.Fatalf("joinGroupCall: %v", err)
|
||||
}
|
||||
|
||||
pushes := f.sessions.records()
|
||||
memberGot := false
|
||||
for _, rec := range pushes {
|
||||
if rec.userID == f.outsider.ID {
|
||||
t.Fatalf("group call push leaked to non-member outsider %d: %+v", f.outsider.ID, rec)
|
||||
}
|
||||
if rec.userID == f.member.ID {
|
||||
memberGot = true
|
||||
}
|
||||
}
|
||||
if !memberGot {
|
||||
t.Fatalf("member %d received no group call push, fan-out broken: %+v", f.member.ID, pushes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupCallM0Lifecycle(t *testing.T) {
|
||||
f := newGroupCallFixture(t)
|
||||
ownerCtx := f.userCtx(f.owner, 11)
|
||||
|
|
|
|||
|
|
@ -17,12 +17,24 @@ import (
|
|||
|
||||
// groupCallOnlineRecipients 返回在线群成员(含 originUserID 本人,推送时由
|
||||
// pushUserMessage 的 ctx except 排除发起 session、保留其它设备)。
|
||||
func (r *Router) groupCallOnlineRecipients(channelID int64) []int64 {
|
||||
// 在线索引候选必须过一次 PG active 复核(与 channelFanoutRecipients 同款纵深):
|
||||
// 索引 stale(如踢人窗口)时不得把通话状态/参与者名单继续推给已非成员者。
|
||||
func (r *Router) groupCallOnlineRecipients(ctx context.Context, channelID int64) []int64 {
|
||||
provider, ok := r.deps.Sessions.(OnlineUserProvider)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return provider.OnlineChannelMemberUserIDs(channelID, domain.MaxChannelRealtimeFanout)
|
||||
online := provider.OnlineChannelMemberUserIDs(channelID, domain.MaxChannelRealtimeFanout)
|
||||
if len(online) == 0 || r.deps.Channels == nil {
|
||||
return online
|
||||
}
|
||||
active, err := r.deps.Channels.FilterActiveMemberIDs(ctx, channelID, online)
|
||||
if err != nil {
|
||||
// 复核失败宁可漏推不误推:群通话 update 无 pts,客户端靠 checkGroupCall/
|
||||
// version 跳号自愈,漏推不丢一致性。
|
||||
return nil
|
||||
}
|
||||
return active
|
||||
}
|
||||
|
||||
// groupCallUpdateContainer 把单条 update 包进 viewer 视角的 Updates 容器。
|
||||
|
|
@ -46,7 +58,7 @@ func (r *Router) pushGroupCallUpdate(ctx context.Context, channel domain.Channel
|
|||
r.pushConferenceGroupCallUpdate(ctx, call)
|
||||
return
|
||||
}
|
||||
recipients := r.groupCallOnlineRecipients(channel.ID)
|
||||
recipients := r.groupCallOnlineRecipients(ctx, channel.ID)
|
||||
for _, viewerID := range recipients {
|
||||
update := &tg.UpdateGroupCall{Call: tgGroupCall(call, viewerID, false)}
|
||||
update.SetPeer(&tg.PeerChannel{ChannelID: channel.ID})
|
||||
|
|
@ -69,7 +81,7 @@ func (r *Router) pushGroupCallParticipantsUpdate(ctx context.Context, channel do
|
|||
for _, p := range rows {
|
||||
userIDs = append(userIDs, p.UserID)
|
||||
}
|
||||
recipients := r.groupCallOnlineRecipients(channel.ID)
|
||||
recipients := r.groupCallOnlineRecipients(ctx, channel.ID)
|
||||
for _, viewerID := range recipients {
|
||||
update := &tg.UpdateGroupCallParticipants{
|
||||
Call: &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash},
|
||||
|
|
|
|||
|
|
@ -219,6 +219,16 @@ func (r *Router) Dispatch(ctx context.Context, authKeyID [8]byte, sessionID int6
|
|||
hasClientMetadata = true
|
||||
r.rememberClientSessionInfo(ctx, info)
|
||||
clientMetadataStored = true
|
||||
// 冷启动回填:授权表恢复的 layer 即时下推到连接,防止本条 RPC handler
|
||||
// 执行期间的 push 仍按 canonical 227 发给老客户端(该分支只在元数据
|
||||
// 尚未入缓存时走到,稳态 RPC 不经过这里)。
|
||||
if info.layer != 0 {
|
||||
if binder, okBinder := r.deps.Sessions.(ClientLayerBinder); okBinder {
|
||||
if rawAuthKeyID, okRaw := RawAuthKeyIDFrom(ctx); okRaw {
|
||||
binder.SetClientLayerForAuthKey(rawAuthKeyID, sessionID, info.layer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 前置鉴权阶段(auth key 解析 / user 重校验 / client info)慢路径告警:超阈值才记,避免刷屏。
|
||||
|
|
@ -703,6 +713,12 @@ func (r *Router) rememberClientLayer(ctx context.Context, layer int) {
|
|||
}
|
||||
sessionKey := clientInfoSessionKey{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID}
|
||||
sessionInfo, exists := r.clientInfo[sessionKey]
|
||||
// session 级记录缺失或 layer 变化时把新值即时下推到连接:invokeWithLayer 在
|
||||
// Dispatch 入口被处理(早于鉴权门与 updates 就绪置位),此时下推能保证同一请求
|
||||
// handler 执行期间触发的 pending flush / 并发 push 已按正确 layer 降级。记录已
|
||||
// 存在且相同(如客户端每条请求都带 wrapper)时跳过——连接侧已由注册播种或此前
|
||||
// 下推持有同值。
|
||||
notifyConn := !exists || sessionInfo.layer != layer
|
||||
sessionInfo.layer = layer
|
||||
if !exists {
|
||||
evictMapEntryIfFullLocked(r.clientInfo, maxClientInfoEntries)
|
||||
|
|
@ -713,6 +729,11 @@ func (r *Router) rememberClientLayer(ctx context.Context, layer int) {
|
|||
r.rememberAuthClientLayerLocked(authKeyID, layer)
|
||||
}
|
||||
r.clientInfoMu.Unlock()
|
||||
if notifyConn {
|
||||
if binder, ok := r.deps.Sessions.(ClientLayerBinder); ok {
|
||||
binder.SetClientLayerForAuthKey(rawAuthKeyID, sessionID, layer)
|
||||
}
|
||||
}
|
||||
if persistAuthLayer && r.deps.Auth != nil {
|
||||
if err := r.deps.Auth.UpdateAuthorizationLayer(ctx, authKeyID, layer); err != nil {
|
||||
r.log.Warn("update authorization layer failed",
|
||||
|
|
|
|||
82
internal/rpc/router_layer_binder_test.go
Normal file
82
internal/rpc/router_layer_binder_test.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
"github.com/gotd/td/bin"
|
||||
"github.com/gotd/td/clock"
|
||||
"github.com/gotd/td/tg"
|
||||
)
|
||||
|
||||
type layerBinderCall struct {
|
||||
rawAuthKeyID [8]byte
|
||||
sessionID int64
|
||||
layer int
|
||||
}
|
||||
|
||||
// layerCaptureSessions 在 captureSessions 基础上实现可选的 ClientLayerBinder。
|
||||
type layerCaptureSessions struct {
|
||||
captureSessions
|
||||
layerMu sync.Mutex
|
||||
layerCalls []layerBinderCall
|
||||
}
|
||||
|
||||
func (s *layerCaptureSessions) SetClientLayerForAuthKey(rawAuthKeyID [8]byte, sessionID int64, layer int) {
|
||||
s.layerMu.Lock()
|
||||
defer s.layerMu.Unlock()
|
||||
s.layerCalls = append(s.layerCalls, layerBinderCall{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID, layer: layer})
|
||||
}
|
||||
|
||||
func (s *layerCaptureSessions) layerCallsSnapshot() []layerBinderCall {
|
||||
s.layerMu.Lock()
|
||||
defer s.layerMu.Unlock()
|
||||
return append([]layerBinderCall(nil), s.layerCalls...)
|
||||
}
|
||||
|
||||
// TestDispatchInvokeWithLayerPushesLayerToSessionBinder 验证 invokeWithLayer 在
|
||||
// Dispatch 入口把新观测的 layer 即时下推到连接层(ClientLayerBinder),且仅在
|
||||
// 首次观测或 layer 变化时下推——每条请求都带 wrapper 的客户端不会造成逐 RPC 下推。
|
||||
func TestDispatchInvokeWithLayerPushesLayerToSessionBinder(t *testing.T) {
|
||||
sessions := &layerCaptureSessions{}
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{Sessions: sessions}, zaptest.NewLogger(t), clock.System)
|
||||
rawAuthKeyID := [8]byte{1, 2, 3, 4, 5, 6, 7, 8}
|
||||
const sessionID = int64(42)
|
||||
|
||||
dispatchWithLayer := func(layer int) {
|
||||
t.Helper()
|
||||
var in bin.Buffer
|
||||
req := &tg.InvokeWithLayerRequest{Layer: layer, Query: &tg.HelpGetConfigRequest{}}
|
||||
if err := req.Encode(&in); err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, sessionID, &in); err != nil {
|
||||
t.Fatalf("dispatch layer %d: %v", layer, err)
|
||||
}
|
||||
}
|
||||
|
||||
dispatchWithLayer(225)
|
||||
calls := sessions.layerCallsSnapshot()
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("layer binder calls = %d, want 1", len(calls))
|
||||
}
|
||||
if calls[0] != (layerBinderCall{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID, layer: 225}) {
|
||||
t.Fatalf("layer binder call = %+v", calls[0])
|
||||
}
|
||||
|
||||
// 同 layer 重复 wrapper:不再下推。
|
||||
dispatchWithLayer(225)
|
||||
if calls := sessions.layerCallsSnapshot(); len(calls) != 1 {
|
||||
t.Fatalf("layer binder calls after repeat = %d, want 1", len(calls))
|
||||
}
|
||||
|
||||
// layer 变化:再次下推。
|
||||
dispatchWithLayer(226)
|
||||
calls = sessions.layerCallsSnapshot()
|
||||
if len(calls) != 2 || calls[1].layer != 226 {
|
||||
t.Fatalf("layer binder calls after change = %+v, want second call with layer 226", calls)
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +58,13 @@ func (s *captureSessions) pushedUserIDs() []int64 {
|
|||
return append([]int64(nil), s.pushUserIDs...)
|
||||
}
|
||||
|
||||
// onlineChannelMemberIDs 返回当前登记的频道在线成员索引快照(测试断言用)。
|
||||
func (s *captureSessions) onlineChannelMemberIDs(channelID int64) []int64 {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return append([]int64(nil), s.channelMembers[channelID]...)
|
||||
}
|
||||
|
||||
// lastUserPush 返回最近一次 PushToUser* 的消息,独立于 message(后者也会被
|
||||
// pushOnlinePeerStatusesToCurrentSession 经 PushToSession 覆盖成对端状态)。
|
||||
func (s *captureSessions) lastUserPush() bin.Encoder {
|
||||
|
|
@ -239,7 +246,9 @@ func (s *captureSessions) OnlineChannelUserIDs(channelID int64, limit int) []int
|
|||
return limitIDs(s.channelViewers[channelID], limit)
|
||||
}
|
||||
|
||||
func (s *captureSessions) SetSessionChannelMemberships(_ [8]byte, _ int64, userID int64, channelIDs []int64) {
|
||||
func (s *captureSessions) ChannelMembershipGeneration(_ [8]byte, _ int64) int64 { return 0 }
|
||||
|
||||
func (s *captureSessions) SetSessionChannelMemberships(_ [8]byte, _ int64, userID int64, channelIDs []int64, _ int64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.channelMembers == nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue