owpengram-server/internal/mtprotoedge/session_membership_gen_test.go

157 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mtprotoedge
import (
"slices"
"testing"
"time"
"github.com/iamxvbaba/td/tlprofile"
"go.uber.org/zap/zaptest"
)
func TestOnlineChannelIDsSnapshotAndDiagnosticPagesStableAscending(t *testing.T) {
sm := NewSessionManager(zaptest.NewLogger(t))
raw := [8]byte{4, 5, 6}
c := &Conn{sessionID: 77, authKeyID: raw}
sm.Register(c)
sm.BindUserForAuthKey(raw, 77, 100)
sm.SetSessionChannelMemberships(raw, 77, 100, []int64{50, 10, 30, 20, 40}, sm.ChannelMembershipGeneration(raw, 77))
want := []int64{10, 20, 30, 40, 50}
snapshot := sm.OnlineChannelIDsSnapshot()
if !slices.Equal(snapshot, want) {
t.Fatalf("online channel snapshot = %v, want %v", snapshot, want)
}
var got []int64
after := int64(0)
for {
page := sm.OnlineChannelIDsAfter(after, 2)
if len(page) == 0 {
break
}
for _, channelID := range page {
if channelID <= after {
t.Fatalf("page %v not strictly after cursor %d", page, after)
}
after = channelID
got = append(got, channelID)
}
}
if !slices.Equal(got, want) {
t.Fatalf("paged online channels = %v, want %v", got, want)
}
// The recovery actor owns a stable copy: later membership changes are visible to the next
// generation, not spliced into the in-flight sorted snapshot.
sm.AddUserChannelMembership(100, 5)
if !slices.Equal(snapshot, want) {
t.Fatalf("owned snapshot mutated after membership insert: %v", snapshot)
}
if current := sm.OnlineChannelIDsSnapshot(); !slices.Equal(current, []int64{5, 10, 20, 30, 40, 50}) {
t.Fatalf("next online channel snapshot = %v", current)
}
// Removing the only live session must immediately remove all channel ids from the recovery
// enumeration; stale membership map entries are never enough without a live bySession key.
sm.Unregister(c)
if got := sm.OnlineChannelIDsAfter(0, 10); len(got) != 0 {
t.Fatalf("online channels after unregister = %v, want empty", got)
}
}
// TestSetSessionChannelMembershipsDetectsConcurrentIncrementalUpdates 验证全量
// membership 同步的丢失更新防护:同步方在读持久成员列表前采样修订号,读取窗口内
// 若发生增量 join/leave另一设备操作经 Add/RemoveUserChannelMembership 落索引),
// 携带过期修订号的全量替换必须改走并集合并(不得覆盖增量),并保持
// membershipsSynced=false 促使下一条 RPC 重试全量同步收敛。
func TestSetSessionChannelMembershipsDetectsConcurrentIncrementalUpdates(t *testing.T) {
sm := NewSessionManager(zaptest.NewLogger(t))
raw := [8]byte{1, 2, 3}
c := &Conn{sessionID: 42, authKeyID: raw}
if err := c.FreezeLayerProfile(tlprofile.Profile227); err != nil {
t.Fatal(err)
}
sm.Register(c)
sm.BindUserForAuthKey(raw, 42, 100)
sm.SetReceivesUpdatesForAuthKey(raw, 42, true)
// 同步方采样修订号后、全量列表落地前,用户在另一台设备加入了频道 7。
gen := sm.ChannelMembershipGeneration(raw, 42)
sm.AddUserChannelMembership(100, 7)
// 基于旧快照的全量列表(只有频道 5不含 7携带过期修订号落地。
sm.SetSessionChannelMemberships(raw, 42, 100, []int64{5}, gen)
if got := sm.OnlineChannelMemberUserIDs(7, 10); len(got) != 1 || got[0] != 100 {
t.Fatalf("channel 7 members = %v, want [100]: full replace overwrote the in-window incremental join", got)
}
if got := sm.OnlineChannelMemberUserIDs(5, 10); len(got) != 1 || got[0] != 100 {
t.Fatalf("channel 5 members = %v, want [100]: merge path must still apply the full list", got)
}
if sm.ReceivesUpdatesForAuthKey(raw, 42) {
t.Fatal("session fully ready despite raced membership sync; retry would never happen")
}
// 重试:新修订号下的全量同步正常替换并置就绪。
sm.SetSessionChannelMemberships(raw, 42, 100, []int64{5, 7}, sm.ChannelMembershipGeneration(raw, 42))
if !sm.ReceivesUpdatesForAuthKey(raw, 42) {
t.Fatal("session not ready after clean resync")
}
// 反方向:窗口内被移出频道 5stale 全量含 5 → 合并会短暂保留 stale 条目
// fan-out 前的 PG active 复核兜底),但必须保持未就绪等待重试。
gen = sm.ChannelMembershipGeneration(raw, 42)
sm.RemoveUserChannelMembership(100, 5)
sm.SetSessionChannelMemberships(raw, 42, 100, []int64{5, 7}, gen)
if sm.ReceivesUpdatesForAuthKey(raw, 42) {
t.Fatal("session ready despite raced removal during sync")
}
sm.SetSessionChannelMemberships(raw, 42, 100, []int64{7}, sm.ChannelMembershipGeneration(raw, 42))
if got := sm.OnlineChannelMemberUserIDs(5, 10); len(got) != 0 {
t.Fatalf("channel 5 members after resync = %v, want empty", got)
}
if !sm.ReceivesUpdatesForAuthKey(raw, 42) {
t.Fatal("session not ready after final resync")
}
}
// TestRegisterEvictsOldestSessionAtCap 验证同 raw auth_key session 数触顶时驱逐的是
// 建连最早的连接,而不是 map 迭代顺序下的随机一个(随机可能误杀刚建立的活跃连接)。
func TestRegisterEvictsOldestSessionAtCap(t *testing.T) {
sm := NewSessionManager(zaptest.NewLogger(t))
raw := [8]byte{9}
base := time.Unix(1_700_000_000, 0)
const oldestSession = int64(100)
oldestTransport := &closeCountingTransport{}
for i := 0; i < maxSessionsPerAuthKey; i++ {
sid := int64(i + 1)
created := base.Add(time.Duration(i+1) * time.Second)
if sid == oldestSession {
created = base // 唯一早于所有其它连接的时间戳,且故意不在注册顺序首位。
}
c := &Conn{sessionID: sid, authKeyID: raw, createdAt: created}
if sid == oldestSession {
c.transport = oldestTransport
}
sm.Register(c)
}
sm.Register(&Conn{sessionID: 9999, authKeyID: raw, createdAt: base.Add(time.Hour)})
sm.mu.RLock()
_, oldestAlive := sm.bySession[sessionKey{authKeyID: raw, sessionID: oldestSession}]
_, newestAlive := sm.bySession[sessionKey{authKeyID: raw, sessionID: 9999}]
total := len(sm.byAuthKey[raw])
sm.mu.RUnlock()
if oldestAlive {
t.Fatal("oldest session survived eviction at cap")
}
if !newestAlive {
t.Fatal("newly registered session missing after eviction")
}
if total != maxSessionsPerAuthKey {
t.Fatalf("sessions for auth key = %d, want cap %d", total, maxSessionsPerAuthKey)
}
if oldestTransport.closes != 1 {
t.Fatalf("evicted transport closes = %d, want 1", oldestTransport.closes)
}
}