chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
141
internal/app/peerview/cache.go
Normal file
141
internal/app/peerview/cache.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package peerview
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// UserResolver resolves users after applying viewer-specific projection.
|
||||
type UserResolver interface {
|
||||
ByIDs(ctx context.Context, viewerUserID int64, userIDs []int64) ([]domain.User, error)
|
||||
}
|
||||
|
||||
// BatchCache caches projected users only for one update-building batch.
|
||||
// The cache key includes viewerUserID because contacts, privacy, phone visibility
|
||||
// and personal/fallback photos are viewer-specific.
|
||||
type BatchCache struct {
|
||||
users UserResolver
|
||||
|
||||
byViewer map[int64]map[int64]domain.User
|
||||
missing map[int64]map[int64]struct{}
|
||||
}
|
||||
|
||||
func NewBatchCache(users UserResolver) *BatchCache {
|
||||
return &BatchCache{
|
||||
users: users,
|
||||
byViewer: make(map[int64]map[int64]domain.User),
|
||||
missing: make(map[int64]map[int64]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BatchCache) UsersForView(ctx context.Context, viewerUserID int64, ids []int64) ([]domain.User, error) {
|
||||
unique := uniqueIDs(ids)
|
||||
if len(unique) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
byID := c.viewerUsers(viewerUserID)
|
||||
missing := c.viewerMissing(viewerUserID)
|
||||
load := make([]int64, 0, len(unique))
|
||||
for _, id := range unique {
|
||||
if _, ok := byID[id]; ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := missing[id]; ok {
|
||||
continue
|
||||
}
|
||||
if u, ok := domain.SystemUserByID(id); ok {
|
||||
byID[id] = u
|
||||
continue
|
||||
}
|
||||
if c.users == nil || viewerUserID == 0 {
|
||||
missing[id] = struct{}{}
|
||||
continue
|
||||
}
|
||||
load = append(load, id)
|
||||
}
|
||||
var err error
|
||||
if len(load) > 0 {
|
||||
var resolved []domain.User
|
||||
resolved, err = c.users.ByIDs(ctx, viewerUserID, load)
|
||||
if err == nil {
|
||||
found := make(map[int64]struct{}, len(resolved))
|
||||
for _, u := range resolved {
|
||||
if u.ID == 0 {
|
||||
continue
|
||||
}
|
||||
byID[u.ID] = u
|
||||
found[u.ID] = struct{}{}
|
||||
}
|
||||
for _, id := range load {
|
||||
if _, ok := found[id]; !ok {
|
||||
missing[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out := make([]domain.User, 0, len(unique))
|
||||
for _, id := range unique {
|
||||
if u, ok := byID[id]; ok {
|
||||
out = append(out, u)
|
||||
}
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Prime 预热某 viewer 的已投影用户(fan-out 跨 viewer 模板化把 per-recipient ForViewer 折叠成
|
||||
// 一次 ForViewers 后回填)。已存在的 id 不覆盖(保留同批已解析结果),避免预热与按需解析互相打架。
|
||||
// 预热的是「投影后、未叠加实时 presence」的用户——与 UsersForView 缓存层一致,presence 仍由
|
||||
// 上层在输出端叠加,故预热不破坏 presence 新鲜度。
|
||||
func (c *BatchCache) Prime(viewerUserID int64, users []domain.User) {
|
||||
if c == nil || viewerUserID == 0 || len(users) == 0 {
|
||||
return
|
||||
}
|
||||
byID := c.viewerUsers(viewerUserID)
|
||||
for _, u := range users {
|
||||
if u.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := byID[u.ID]; ok {
|
||||
continue
|
||||
}
|
||||
byID[u.ID] = u
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BatchCache) viewerUsers(viewerUserID int64) map[int64]domain.User {
|
||||
if byID, ok := c.byViewer[viewerUserID]; ok {
|
||||
return byID
|
||||
}
|
||||
byID := make(map[int64]domain.User)
|
||||
c.byViewer[viewerUserID] = byID
|
||||
return byID
|
||||
}
|
||||
|
||||
func (c *BatchCache) viewerMissing(viewerUserID int64) map[int64]struct{} {
|
||||
if missing, ok := c.missing[viewerUserID]; ok {
|
||||
return missing
|
||||
}
|
||||
missing := make(map[int64]struct{})
|
||||
c.missing[viewerUserID] = missing
|
||||
return missing
|
||||
}
|
||||
|
||||
func uniqueIDs(ids []int64) []int64 {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]int64, 0, len(ids))
|
||||
seen := make(map[int64]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
out = append(out, id)
|
||||
}
|
||||
return out
|
||||
}
|
||||
108
internal/app/peerview/cache_test.go
Normal file
108
internal/app/peerview/cache_test.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package peerview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestBatchCacheCachesPerViewer(t *testing.T) {
|
||||
resolver := &captureUserResolver{
|
||||
users: map[int64]domain.User{
|
||||
1000000001: {ID: 1000000001, FirstName: "Alice"},
|
||||
},
|
||||
}
|
||||
cache := NewBatchCache(resolver)
|
||||
|
||||
got, err := cache.UsersForView(context.Background(), 1000000002, []int64{1000000001, 1000000001})
|
||||
if err != nil {
|
||||
t.Fatalf("UsersForView: %v", err)
|
||||
}
|
||||
if len(got) != 1 || got[0].ID != 1000000001 {
|
||||
t.Fatalf("users = %+v, want Alice once", got)
|
||||
}
|
||||
got, err = cache.UsersForView(context.Background(), 1000000002, []int64{1000000001})
|
||||
if err != nil {
|
||||
t.Fatalf("UsersForView cached: %v", err)
|
||||
}
|
||||
got, err = cache.UsersForView(context.Background(), 1000000003, []int64{1000000001})
|
||||
if err != nil {
|
||||
t.Fatalf("UsersForView other viewer: %v", err)
|
||||
}
|
||||
|
||||
wantCalls := []resolverCall{
|
||||
{viewerUserID: 1000000002, ids: []int64{1000000001}},
|
||||
{viewerUserID: 1000000003, ids: []int64{1000000001}},
|
||||
}
|
||||
if !reflect.DeepEqual(resolver.calls, wantCalls) {
|
||||
t.Fatalf("resolver calls = %+v, want %+v", resolver.calls, wantCalls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatchCachePrimeServesWithoutResolver:Prime 预热的 viewer 用户被 UsersForView 直接命中,
|
||||
// 不再回落 resolver(fan-out 跨 viewer 投影预热把 per-recipient ByIDs 折叠成一次 ForViewers 的前提);
|
||||
// 且 Prime 不覆盖已解析的同 id(按需解析优先)。
|
||||
func TestBatchCachePrimeServesWithoutResolver(t *testing.T) {
|
||||
resolver := &captureUserResolver{
|
||||
users: map[int64]domain.User{
|
||||
1000000001: {ID: 1000000001, FirstName: "Resolved"},
|
||||
},
|
||||
}
|
||||
cache := NewBatchCache(resolver)
|
||||
|
||||
const viewer = int64(1000000002)
|
||||
cache.Prime(viewer, []domain.User{{ID: 1000000001, FirstName: "Primed"}, {ID: 1000000009, FirstName: "PrimedOnly"}})
|
||||
|
||||
got, err := cache.UsersForView(context.Background(), viewer, []int64{1000000001, 1000000009})
|
||||
if err != nil {
|
||||
t.Fatalf("UsersForView: %v", err)
|
||||
}
|
||||
byID := map[int64]domain.User{}
|
||||
for _, u := range got {
|
||||
byID[u.ID] = u
|
||||
}
|
||||
if byID[1000000001].FirstName != "Primed" || byID[1000000009].FirstName != "PrimedOnly" {
|
||||
t.Fatalf("primed users = %+v, want Primed/PrimedOnly served from cache", got)
|
||||
}
|
||||
if len(resolver.calls) != 0 {
|
||||
t.Fatalf("resolver called %d times, want 0 (all served from prime)", len(resolver.calls))
|
||||
}
|
||||
|
||||
// 已解析的 id 不被后续 Prime 覆盖。
|
||||
other := &captureUserResolver{users: map[int64]domain.User{1000000003: {ID: 1000000003, FirstName: "Resolved3"}}}
|
||||
c2 := NewBatchCache(other)
|
||||
if _, err := c2.UsersForView(context.Background(), viewer, []int64{1000000003}); err != nil {
|
||||
t.Fatalf("resolve 3: %v", err)
|
||||
}
|
||||
c2.Prime(viewer, []domain.User{{ID: 1000000003, FirstName: "ShouldNotOverwrite"}})
|
||||
got2, err := c2.UsersForView(context.Background(), viewer, []int64{1000000003})
|
||||
if err != nil {
|
||||
t.Fatalf("UsersForView after prime: %v", err)
|
||||
}
|
||||
if len(got2) != 1 || got2[0].FirstName != "Resolved3" {
|
||||
t.Fatalf("after prime = %+v, want resolved value preserved (no overwrite)", got2)
|
||||
}
|
||||
}
|
||||
|
||||
type resolverCall struct {
|
||||
viewerUserID int64
|
||||
ids []int64
|
||||
}
|
||||
|
||||
type captureUserResolver struct {
|
||||
users map[int64]domain.User
|
||||
calls []resolverCall
|
||||
}
|
||||
|
||||
func (r *captureUserResolver) ByIDs(_ context.Context, viewerUserID int64, userIDs []int64) ([]domain.User, error) {
|
||||
r.calls = append(r.calls, resolverCall{viewerUserID: viewerUserID, ids: append([]int64(nil), userIDs...)})
|
||||
out := make([]domain.User, 0, len(userIDs))
|
||||
for _, id := range userIDs {
|
||||
if u, ok := r.users[id]; ok {
|
||||
out = append(out, u)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue