chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,110 @@
package postgres
import (
"context"
"os"
"testing"
"time"
"telesrv/internal/domain"
)
// TestChannelRowCacheInvalidatesOnNotify 端到端验证channels 触发器(0117) → LISTEN/NOTIFY →
// ChannelChangeListener → ChannelRowCache 失效闭环。门控于 TELESRV_TEST_POSTGRES_DSN。
//
// 用「listener 连接时的 flush」作为就绪信号(轮询缓存被清空即确认 listener 已 LISTEN),避免靠
// sleep 猜时序导致竞态。
func TestChannelRowCacheInvalidatesOnNotify(t *testing.T) {
pool := testPool(t) // 未设 DSN 会 t.Skip
dsn := os.Getenv("TELESRV_TEST_POSTGRES_DSN")
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
owner, err := users.Create(ctx, domain.User{
AccessHash: 451,
Phone: "+1889" + suffix + "01",
FirstName: "CacheOwner",
})
if err != nil {
t.Fatalf("create owner: %v", err)
}
cache := NewChannelRowCache(1000)
channels := NewChannelStore(pool, WithChannelRowCache(cache))
var channelID int64
t.Cleanup(func() {
if channelID != 0 {
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
}
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
})
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: owner.ID,
Title: "CacheChan " + suffix,
Megagroup: true,
Date: 1700000700,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
channelID = created.Channel.ID
// 先填充缓存(此时 listener 未起)。
if _, err := channels.GetChannel(ctx, owner.ID, channelID); err != nil {
t.Fatalf("warm GetChannel: %v", err)
}
if _, ok := cache.get(channelID); !ok {
t.Fatalf("GetChannel 后频道行应已缓存")
}
// 起 listener它连接后会 flush 整表——用缓存被清空作为「listener 已就绪」的信号。
lctx, cancel := context.WithCancel(ctx)
defer cancel()
listener := NewChannelChangeListener(dsn, cache, nil)
go listener.Run(lctx)
if !waitUntil(2*time.Second, func() bool { _, ok := cache.get(channelID); return !ok }) {
t.Fatalf("listener 未在预期内连接并 flush 缓存")
}
// listener 就绪后重新填充缓存。
if _, err := channels.GetChannel(ctx, owner.ID, channelID); err != nil {
t.Fatalf("re-warm GetChannel: %v", err)
}
if _, ok := cache.get(channelID); !ok {
t.Fatalf("re-warm 后频道行应已缓存")
}
// 模拟任意写点直接改 channels 表(触发器会 NOTIFY)。
const newTitle = "Renamed By Trigger"
if _, err := pool.Exec(ctx, "UPDATE channels SET title = $2, updated_at = now() WHERE id = $1", channelID, newTitle); err != nil {
t.Fatalf("update title: %v", err)
}
// 触发器 → NOTIFY → listener 应实时失效该频道。
if !waitUntil(3*time.Second, func() bool { _, ok := cache.get(channelID); return !ok }) {
t.Fatalf("UPDATE 后缓存未在预期内被 NOTIFY 失效")
}
// 失效后再读应拿到新值(并回填缓存)。
view, err := channels.GetChannel(ctx, owner.ID, channelID)
if err != nil {
t.Fatalf("post-invalidation GetChannel: %v", err)
}
if view.Channel.Title != newTitle {
t.Fatalf("失效后标题 = %q, want %q", view.Channel.Title, newTitle)
}
}
func waitUntil(timeout time.Duration, cond func() bool) bool {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if cond() {
return true
}
time.Sleep(20 * time.Millisecond)
}
return cond()
}