owpengram-server/internal/rpc/story_pinned_stories_cache.go

180 lines
5.1 KiB
Go

package rpc
import (
"context"
"time"
"telesrv/internal/domain"
"telesrv/internal/readmodelcache"
)
const storyPinnedStoriesCacheTTL = 30 * time.Minute
type storyPinnedStoriesCacheKey struct {
viewerUserID int64
peer domain.Peer
offsetID int
limit int
}
// storyPinnedStoriesCache 缓存某 viewer 对某 peer 的置顶故事分页结果,由统一缓存原语承载。
// key 含 (offsetID,limit) 分页维度——LRU 单条驱逐终于给这个 page-key 上界,消掉原先无界增长。
type storyPinnedStoriesCache struct {
cache *readmodelcache.Cache[storyPinnedStoriesCacheKey, domain.StoryList]
}
func newStoryPinnedStoriesCache(now func() time.Time) *storyPinnedStoriesCache {
return &storyPinnedStoriesCache{
cache: readmodelcache.New[storyPinnedStoriesCacheKey, domain.StoryList](readmodelcache.Config[storyPinnedStoriesCacheKey, domain.StoryList]{
MaxEntries: storyProjectionCacheMaxEntries,
TTL: storyPinnedStoriesCacheTTL,
Now: now,
Clone: cloneStoryListForCache,
}),
}
}
func storyPinnedStoriesKey(viewerUserID int64, peer domain.Peer, offsetID, limit int) storyPinnedStoriesCacheKey {
if offsetID < 0 {
offsetID = 0
}
if limit <= 0 || limit > domain.MaxStoryListLimit {
limit = domain.MaxStoryListLimit
}
return storyPinnedStoriesCacheKey{
viewerUserID: viewerUserID,
peer: peer,
offsetID: offsetID,
limit: limit,
}
}
func (c *storyPinnedStoriesCache) getOrLoad(ctx context.Context, key storyPinnedStoriesCacheKey, load func() (domain.StoryList, error)) (domain.StoryList, error) {
if c == nil || key.viewerUserID == 0 || key.peer.ID == 0 {
return load()
}
return c.cache.GetOrLoad(ctx, key, load)
}
func (c *storyPinnedStoriesCache) Delete(viewerUserID int64, peer domain.Peer) {
if c == nil || viewerUserID == 0 || peer.ID == 0 {
return
}
c.cache.InvalidateWhere(func(k storyPinnedStoriesCacheKey) bool {
return k.viewerUserID == viewerUserID && k.peer == peer
})
}
func (c *storyPinnedStoriesCache) DeleteViewer(viewerUserID int64) {
if c == nil || viewerUserID == 0 {
return
}
c.cache.InvalidateWhere(func(k storyPinnedStoriesCacheKey) bool { return k.viewerUserID == viewerUserID })
}
func (c *storyPinnedStoriesCache) DeletePeer(peer domain.Peer) {
if c == nil || peer.ID == 0 {
return
}
c.cache.InvalidateWhere(func(k storyPinnedStoriesCacheKey) bool { return k.peer == peer })
}
func (c *storyPinnedStoriesCache) Flush() {
if c == nil {
return
}
c.cache.Flush()
}
func cloneStoryListForCache(in domain.StoryList) domain.StoryList {
in.Stories = cloneStoriesForCache(in.Stories)
in.PinnedToTop = append([]int(nil), in.PinnedToTop...)
in.Peers = clonePeerStoriesForCache(in.Peers)
in.Users = append([]domain.User(nil), in.Users...)
in.Channels = append([]domain.Channel(nil), in.Channels...)
return in
}
func clonePeerStoriesForCache(in []domain.PeerStories) []domain.PeerStories {
if len(in) == 0 {
return nil
}
out := make([]domain.PeerStories, len(in))
for i, item := range in {
out[i] = item
out[i].Stories = cloneStoriesForCache(item.Stories)
out[i].Users = append([]domain.User(nil), item.Users...)
out[i].Channels = append([]domain.Channel(nil), item.Channels...)
}
return out
}
func cloneStoriesForCache(in []domain.Story) []domain.Story {
if len(in) == 0 {
return nil
}
out := make([]domain.Story, len(in))
for i, story := range in {
out[i] = cloneStoryForCache(story)
}
return out
}
func cloneStoryForCache(story domain.Story) domain.Story {
story.PrivacyRules = clonePrivacyRulesForCache(story.PrivacyRules)
story.AllowUserIDs = append([]int64(nil), story.AllowUserIDs...)
story.DisallowUserIDs = append([]int64(nil), story.DisallowUserIDs...)
story.Entities = append([]domain.MessageEntity(nil), story.Entities...)
story.MediaAreas = cloneStoryMediaAreasForCache(story.MediaAreas)
if story.Forward != nil {
forward := *story.Forward
story.Forward = &forward
}
story.Views.Reactions = append([]domain.ChannelMessageReactionCount(nil), story.Views.Reactions...)
story.Views.RecentViewers = append([]int64(nil), story.Views.RecentViewers...)
if story.SentReaction != nil {
reaction := *story.SentReaction
story.SentReaction = &reaction
}
return story
}
func clonePrivacyRulesForCache(in []domain.PrivacyRule) []domain.PrivacyRule {
if len(in) == 0 {
return nil
}
out := make([]domain.PrivacyRule, len(in))
for i, rule := range in {
out[i] = rule
out[i].UserIDs = append([]int64(nil), rule.UserIDs...)
out[i].ChatIDs = append([]int64(nil), rule.ChatIDs...)
}
return out
}
func cloneStoryMediaAreasForCache(in []domain.StoryMediaArea) []domain.StoryMediaArea {
if len(in) == 0 {
return nil
}
out := make([]domain.StoryMediaArea, len(in))
for i, area := range in {
out[i] = area
if area.Reaction != nil {
reaction := *area.Reaction
out[i].Reaction = &reaction
}
if area.Geo != nil {
geo := *area.Geo
out[i].Geo = &geo
}
if area.GeoAddress != nil {
address := *area.GeoAddress
out[i].GeoAddress = &address
}
if area.Venue != nil {
venue := *area.Venue
out[i].Venue = &venue
}
}
return out
}