owpengram-server/internal/store/postgres/message_concurrent_integration_test.go

113 lines
3.5 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 postgres
import (
"context"
"sync"
"testing"
"time"
"telesrv/internal/domain"
)
// TestSendPrivateTextConcurrentNoPtsGap 是「PG 事务内分配 pts」的正确性核心证明
// N 条消息并发 sender→recipient 发送,全部成功提交后,接收方账号事件 pts 必须严格连续 1..N
// (无空洞、无重复、无丢失),且 MaxContiguousPts == N。
// box id 仍可由外部计数器提供pts 则由 user_update_watermarks 在同一 PG tx 内推进。
func TestSendPrivateTextConcurrentNoPtsGap(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
sender, err := users.Create(ctx, domain.User{
AccessHash: 51,
Phone: "+1999" + suffix + "01",
FirstName: "ConcSender",
})
if err != nil {
t.Fatalf("create sender: %v", err)
}
recipient, err := users.Create(ctx, domain.User{
AccessHash: 52,
Phone: "+1999" + suffix + "02",
FirstName: "ConcRecipient",
})
if err != nil {
t.Fatalf("create recipient: %v", err)
}
ids := []int64{sender.ID, recipient.ID}
t.Cleanup(func() {
// 按 FK 依赖序清理子表message_boxes.from_user_id 为 RESTRICT再删用户。
_, _ = pool.Exec(ctx, "DELETE FROM dispatch_outbox WHERE target_user_id = ANY($1::bigint[])", ids)
_, _ = pool.Exec(ctx, "DELETE FROM user_update_events WHERE user_id = ANY($1::bigint[])", ids)
_, _ = pool.Exec(ctx, "DELETE FROM message_boxes WHERE owner_user_id = ANY($1::bigint[])", ids)
_, _ = pool.Exec(ctx, "DELETE FROM private_messages WHERE sender_user_id = ANY($1::bigint[])", ids)
_, _ = pool.Exec(ctx, "DELETE FROM dialogs WHERE user_id = ANY($1::bigint[])", ids)
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", ids)
})
messages := NewMessageStore(pool, WithMessageAllocators(&perUserCounterAllocator{}))
const n = 200
base := time.Now().UnixNano()
date := int(time.Now().Unix())
errs := make([]error, n)
recipPts := make([]int, n)
sem := make(chan struct{}, 32)
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
sem <- struct{}{}
go func(i int) {
defer wg.Done()
defer func() { <-sem }()
res, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: sender.ID,
RecipientUserID: recipient.ID,
RandomID: base + int64(i),
Message: "concurrent body",
Date: date,
})
errs[i] = err
recipPts[i] = res.RecipientMessage.Pts
}(i)
}
wg.Wait()
for i, err := range errs {
if err != nil {
t.Fatalf("send %d: %v", i, err)
}
}
// 进程内检测 allocator 是否给接收方分配了重复 pts。
seen := map[int]int{}
for i := 0; i < n; i++ {
if prev, ok := seen[recipPts[i]]; ok {
t.Errorf("recipient pts %d 被发送 #%d 和 #%d 同时分配allocator 并发重复)", recipPts[i], prev, i)
}
seen[recipPts[i]] = i
}
// 接收方应有 n 条事件pts 连续 1..n无空洞无重复无丢失
events := NewUpdateEventStore(pool)
got, err := events.ListAfter(ctx, recipient.ID, 0, n+10)
if err != nil {
t.Fatalf("list recipient events: %v", err)
}
if len(got) != n {
t.Fatalf("recipient events = %d, want %d无丢失无重复", len(got), n)
}
for i, ev := range got {
if ev.Pts != i+1 {
t.Fatalf("recipient event[%d].Pts = %d, want %dpts 必须连续无洞)", i, ev.Pts, i+1)
}
}
contig, err := events.MaxContiguousPts(ctx, recipient.ID)
if err != nil {
t.Fatalf("MaxContiguousPts: %v", err)
}
if contig != n {
t.Fatalf("MaxContiguousPts(recipient) = %d, want %d", contig, n)
}
}