88 lines
2.8 KiB
Go
88 lines
2.8 KiB
Go
package postgres
|
||
|
||
import (
|
||
"context"
|
||
"sync/atomic"
|
||
"testing"
|
||
|
||
"telesrv/internal/domain"
|
||
)
|
||
|
||
// staleChannelIDAllocator 模拟落后的 channel id 计数器:从 start 起按 1
|
||
// 步进,必然撞上已存在的 channel 主键。
|
||
type staleChannelIDAllocator struct {
|
||
next atomic.Int64
|
||
atLeast atomic.Int64 // 记录 NextChannelIDAtLeast 被调用时的 floor
|
||
}
|
||
|
||
func (a *staleChannelIDAllocator) NextChannelID(_ context.Context) (int64, error) {
|
||
return a.next.Add(1), nil
|
||
}
|
||
|
||
func (a *staleChannelIDAllocator) CurrentChannelID(_ context.Context) (int64, error) {
|
||
return a.next.Load(), nil
|
||
}
|
||
|
||
func (a *staleChannelIDAllocator) NextChannelIDAtLeast(_ context.Context, floor int64) (int64, error) {
|
||
a.atLeast.Store(floor)
|
||
for {
|
||
cur := a.next.Load()
|
||
if cur < floor {
|
||
if !a.next.CompareAndSwap(cur, floor) {
|
||
continue
|
||
}
|
||
}
|
||
return a.next.Add(1), nil
|
||
}
|
||
}
|
||
|
||
// TestChannelStoreCreateChannelRecoversFromStaleIDCounter 验证计数器落后
|
||
// (Redis 快照回退 / 测试 fallback 分配器绕过 Redis 直写同一库)时,
|
||
// CreateChannel 经预检自愈拿到空闲 id,而非撞主键 500。
|
||
func TestChannelStoreCreateChannelRecoversFromStaleIDCounter(t *testing.T) {
|
||
pool := testPool(t)
|
||
ctx := context.Background()
|
||
suffix := randomSuffix(t)
|
||
|
||
users := NewUserStore(pool)
|
||
creator, err := users.Create(ctx, domain.User{AccessHash: 91, Phone: "+1673" + suffix + "01", FirstName: "StaleIDCreator"})
|
||
if err != nil {
|
||
t.Fatalf("create user: %v", err)
|
||
}
|
||
t.Cleanup(func() {
|
||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", creator.ID)
|
||
})
|
||
|
||
// 先用默认 PG fallback 分配器造一个真实 channel(占住 max id)。
|
||
seedStore := NewChannelStore(pool)
|
||
seed, err := seedStore.CreateChannel(ctx, domain.CreateChannelRequest{
|
||
CreatorUserID: creator.ID,
|
||
Title: "StaleSeed " + suffix,
|
||
Megagroup: true,
|
||
Date: 1700000700,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("seed channel: %v", err)
|
||
}
|
||
|
||
// 落后计数器从 seed id 之前很远处开始(模拟回退),首次分配必撞。
|
||
stale := &staleChannelIDAllocator{}
|
||
stale.next.Store(seed.Channel.ID - 3)
|
||
staleStore := NewChannelStore(pool, WithChannelAllocators(stale, nil))
|
||
// msgIDs 传 nil 会被 fallback 覆盖;channel pts 由 PG 事务直接维护。
|
||
created, err := staleStore.CreateChannel(ctx, domain.CreateChannelRequest{
|
||
CreatorUserID: creator.ID,
|
||
Title: "StaleRecovered " + suffix,
|
||
Megagroup: true,
|
||
Date: 1700000701,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("create channel with stale counter: %v", err)
|
||
}
|
||
if created.Channel.ID <= seed.Channel.ID {
|
||
t.Fatalf("recovered channel id = %d, want > seed id %d", created.Channel.ID, seed.Channel.ID)
|
||
}
|
||
if stale.atLeast.Load() < seed.Channel.ID {
|
||
t.Fatalf("NextChannelIDAtLeast floor = %d, want >= seed id %d(应按表内最大 id 对账)", stale.atLeast.Load(), seed.Channel.ID)
|
||
}
|
||
}
|