chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
248
internal/rpc/story_projection_cache.go
Normal file
248
internal/rpc/story_projection_cache.go
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/readmodelcache"
|
||||
)
|
||||
|
||||
const (
|
||||
storyProjectionCacheTTL = storyPinnedStoriesCacheTTL
|
||||
storyProjectionCacheMaxEntries = 4096
|
||||
)
|
||||
|
||||
type storyProjectionCacheKey struct {
|
||||
viewerUserID int64
|
||||
peer domain.Peer
|
||||
}
|
||||
|
||||
// storyProjectionValue 含负缓存:hasRecent=false 表示「查过、该 peer 无 recent 故事」(仍带
|
||||
// hidden 状态)。tg.RecentStory 按值视为 copy-safe(与原实现一致,不深拷)。
|
||||
type storyProjectionValue struct {
|
||||
recent tg.RecentStory
|
||||
hasRecent bool
|
||||
hidden bool
|
||||
}
|
||||
|
||||
type storyProjectionMaps struct {
|
||||
recent map[domain.Peer]tg.RecentStory
|
||||
hidden map[domain.Peer]bool
|
||||
}
|
||||
|
||||
// storyProjectionCache 缓存 (viewer,peer)→{recent, hidden},由统一缓存原语承载,批量经
|
||||
// GetOrLoadBatch(per-peer 查 + 合批 load misses + per-key 写回;纯 TTL,无版本闸门)。
|
||||
// GetOrLoadBatch 无批量级 singleflight,故此处保留按 (viewer,peer 集) 维度的 sf,防止并发相同
|
||||
// 投影请求对昂贵的 story 后端形成 thundering herd。
|
||||
type storyProjectionCache struct {
|
||||
cache *readmodelcache.Cache[storyProjectionCacheKey, storyProjectionValue]
|
||||
sf singleflight.Group
|
||||
}
|
||||
|
||||
func newStoryProjectionCache(now func() time.Time) *storyProjectionCache {
|
||||
return &storyProjectionCache{
|
||||
cache: readmodelcache.New[storyProjectionCacheKey, storyProjectionValue](readmodelcache.Config[storyProjectionCacheKey, storyProjectionValue]{
|
||||
MaxEntries: storyProjectionCacheMaxEntries,
|
||||
TTL: storyProjectionCacheTTL,
|
||||
Now: now,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// getMany 批量解析 (viewer, peers) 的 recent/hidden 投影。loadMissing 为缺失 peer 计算两张图
|
||||
// (与 storyProjectionFreshMaps 同形);每个 miss peer 都被缓存(含「无 recent」负结果与 hidden 态)。
|
||||
// 整体按 (viewer, 排序去重 peer 集) 走 singleflight,并发相同请求只打一次后端。
|
||||
func (c *storyProjectionCache) getMany(
|
||||
ctx context.Context,
|
||||
viewerUserID int64,
|
||||
peers []domain.Peer,
|
||||
loadMissing func(context.Context, []domain.Peer) (map[domain.Peer]tg.RecentStory, map[domain.Peer]bool),
|
||||
) (map[domain.Peer]tg.RecentStory, map[domain.Peer]bool) {
|
||||
if c == nil {
|
||||
return loadMissing(ctx, peers)
|
||||
}
|
||||
v, _, _ := c.sf.Do(storyProjectionSingleflightKey(viewerUserID, peers), func() (any, error) {
|
||||
recent, hidden := c.getManyUncached(ctx, viewerUserID, peers, loadMissing)
|
||||
return storyProjectionMaps{recent: recent, hidden: hidden}, nil
|
||||
})
|
||||
res := v.(storyProjectionMaps)
|
||||
return res.recent, res.hidden
|
||||
}
|
||||
|
||||
func (c *storyProjectionCache) getManyUncached(
|
||||
ctx context.Context,
|
||||
viewerUserID int64,
|
||||
peers []domain.Peer,
|
||||
loadMissing func(context.Context, []domain.Peer) (map[domain.Peer]tg.RecentStory, map[domain.Peer]bool),
|
||||
) (map[domain.Peer]tg.RecentStory, map[domain.Peer]bool) {
|
||||
keys := make([]storyProjectionCacheKey, 0, len(peers))
|
||||
for _, peer := range peers {
|
||||
keys = append(keys, storyProjectionCacheKey{viewerUserID: viewerUserID, peer: peer})
|
||||
}
|
||||
values, _ := c.cache.GetOrLoadBatch(ctx, keys,
|
||||
func(storyProjectionCacheKey) (int64, bool) { return 0, true }, // 纯 TTL,无版本闸门
|
||||
func(ctx context.Context, missing []storyProjectionCacheKey) (map[storyProjectionCacheKey]storyProjectionValue, error) {
|
||||
missPeers := make([]domain.Peer, len(missing))
|
||||
for i, k := range missing {
|
||||
missPeers[i] = k.peer
|
||||
}
|
||||
recent, hidden := loadMissing(ctx, missPeers)
|
||||
out := make(map[storyProjectionCacheKey]storyProjectionValue, len(missing))
|
||||
for _, k := range missing {
|
||||
v := storyProjectionValue{}
|
||||
if story, ok := recent[k.peer]; ok {
|
||||
v.recent = story
|
||||
v.hasRecent = true
|
||||
}
|
||||
if state, ok := hidden[k.peer]; ok {
|
||||
v.hidden = state
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
return out, nil
|
||||
})
|
||||
recentOut := make(map[domain.Peer]tg.RecentStory, len(values))
|
||||
hiddenOut := make(map[domain.Peer]bool, len(values))
|
||||
for k, v := range values {
|
||||
if v.hasRecent {
|
||||
recentOut[k.peer] = v.recent
|
||||
}
|
||||
hiddenOut[k.peer] = v.hidden
|
||||
}
|
||||
return recentOut, hiddenOut
|
||||
}
|
||||
|
||||
func (c *storyProjectionCache) Delete(viewerUserID int64, peer domain.Peer) {
|
||||
if c == nil || viewerUserID == 0 || peer.ID == 0 {
|
||||
return
|
||||
}
|
||||
c.cache.Invalidate(storyProjectionCacheKey{viewerUserID: viewerUserID, peer: peer})
|
||||
}
|
||||
|
||||
func (c *storyProjectionCache) DeleteViewer(viewerUserID int64) {
|
||||
if c == nil || viewerUserID == 0 {
|
||||
return
|
||||
}
|
||||
c.cache.InvalidateWhere(func(k storyProjectionCacheKey) bool { return k.viewerUserID == viewerUserID })
|
||||
}
|
||||
|
||||
func (c *storyProjectionCache) DeletePeer(peer domain.Peer) {
|
||||
if c == nil || peer.ID == 0 {
|
||||
return
|
||||
}
|
||||
c.cache.InvalidateWhere(func(k storyProjectionCacheKey) bool { return k.peer == peer })
|
||||
}
|
||||
|
||||
func (c *storyProjectionCache) Flush() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.cache.Flush()
|
||||
}
|
||||
|
||||
func (r *Router) storyProjectionMaps(ctx context.Context, viewerUserID int64, peers []domain.Peer) (map[domain.Peer]tg.RecentStory, map[domain.Peer]bool) {
|
||||
if r.deps.Stories == nil || viewerUserID == 0 || len(peers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if r.storyProjectionCache == nil {
|
||||
return r.storyProjectionFreshMaps(ctx, viewerUserID, peers)
|
||||
}
|
||||
return r.storyProjectionCache.getMany(ctx, viewerUserID, peers, func(ctx context.Context, missPeers []domain.Peer) (map[domain.Peer]tg.RecentStory, map[domain.Peer]bool) {
|
||||
return r.storyProjectionFreshMaps(ctx, viewerUserID, missPeers)
|
||||
})
|
||||
}
|
||||
|
||||
func storyProjectionSingleflightKey(viewerUserID int64, peers []domain.Peer) string {
|
||||
uniq := make(map[domain.Peer]struct{}, len(peers))
|
||||
keys := make([]domain.Peer, 0, len(peers))
|
||||
for _, peer := range peers {
|
||||
if peer.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := uniq[peer]; ok {
|
||||
continue
|
||||
}
|
||||
uniq[peer] = struct{}{}
|
||||
keys = append(keys, peer)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
if keys[i].Type != keys[j].Type {
|
||||
return keys[i].Type < keys[j].Type
|
||||
}
|
||||
return keys[i].ID < keys[j].ID
|
||||
})
|
||||
var b strings.Builder
|
||||
b.WriteString(strconv.FormatInt(viewerUserID, 10))
|
||||
for _, peer := range keys {
|
||||
b.WriteByte('|')
|
||||
b.WriteString(string(peer.Type))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(strconv.FormatInt(peer.ID, 10))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (r *Router) invalidateStoryProjectionCache(viewerUserID int64, peer domain.Peer) {
|
||||
if r.storyProjectionCache != nil {
|
||||
r.storyProjectionCache.Delete(viewerUserID, peer)
|
||||
}
|
||||
if r.storyPinnedCache != nil {
|
||||
r.storyPinnedCache.Delete(viewerUserID, peer)
|
||||
}
|
||||
if r.storyPinnedListCache != nil {
|
||||
r.storyPinnedListCache.Delete(viewerUserID, peer)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) invalidateStoryProjectionCacheForViewer(viewerUserID int64) {
|
||||
if r.storyProjectionCache != nil {
|
||||
r.storyProjectionCache.DeleteViewer(viewerUserID)
|
||||
}
|
||||
if r.storyPinnedCache != nil {
|
||||
r.storyPinnedCache.DeleteViewer(viewerUserID)
|
||||
}
|
||||
if r.storyPinnedListCache != nil {
|
||||
r.storyPinnedListCache.DeleteViewer(viewerUserID)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) invalidateStoryProjectionCacheForPeer(peer domain.Peer) {
|
||||
if r.storyProjectionCache != nil {
|
||||
r.storyProjectionCache.DeletePeer(peer)
|
||||
}
|
||||
if r.storyPinnedCache != nil {
|
||||
r.storyPinnedCache.DeletePeer(peer)
|
||||
}
|
||||
if r.storyPinnedListCache != nil {
|
||||
r.storyPinnedListCache.DeletePeer(peer)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) InvalidateStoryReadModelViewers(viewerUserIDs ...int64) {
|
||||
for _, id := range viewerUserIDs {
|
||||
r.invalidateStoryProjectionCacheForViewer(id)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) InvalidateStoryReadModelPeer(peer domain.Peer) {
|
||||
r.invalidateStoryProjectionCacheForPeer(peer)
|
||||
}
|
||||
|
||||
func (r *Router) FlushStoryReadModelCache() {
|
||||
if r.storyProjectionCache != nil {
|
||||
r.storyProjectionCache.Flush()
|
||||
}
|
||||
if r.storyPinnedCache != nil {
|
||||
r.storyPinnedCache.Flush()
|
||||
}
|
||||
if r.storyPinnedListCache != nil {
|
||||
r.storyPinnedListCache.Flush()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue