owpengram-server/internal/app/privacy/cache.go

209 lines
5.8 KiB
Go

package privacy
import (
"context"
"time"
"telesrv/internal/domain"
"telesrv/internal/readmodelcache"
"telesrv/internal/store"
)
const (
DefaultPrivacyRulesCacheTTL = 24 * time.Hour
privacySnapshotMaxOwners = 8192
)
var allPrivacyRuleKeys = []domain.PrivacyKey{
domain.PrivacyKeyStatusTimestamp,
domain.PrivacyKeyChatInvite,
domain.PrivacyKeyPhoneCall,
domain.PrivacyKeyPhoneP2P,
domain.PrivacyKeyForwards,
domain.PrivacyKeyProfilePhoto,
domain.PrivacyKeyPhoneNumber,
domain.PrivacyKeyAddedByPhone,
domain.PrivacyKeyVoiceMessages,
domain.PrivacyKeyAbout,
domain.PrivacyKeyBirthday,
domain.PrivacyKeyStarGiftsAutoSave,
domain.PrivacyKeyNoPaidMessages,
domain.PrivacyKeySavedMusic,
}
// privacyRulesMap 是单个 owner 的全部隐私规则(空 map = 查过且无规则,即负缓存)。
type privacyRulesMap map[domain.PrivacyKey]domain.PrivacyRules
// CachedPrivacyStore 是 account privacy rules 的 owner 级 read-model 缓存,由统一缓存原语承载
// (LRU 单条驱逐 / epoch 守卫 / clone)。owner 级、变更稀少:一次性装入某 owner 全部 key,让
// projectBatch/CanSeeMatrix 在内存里判 phone/status/photo 可见性,免去反复规划 account_privacy_rules。
// 单 owner 走 GetOrLoad,多 owner 走 GetOrLoadBatch(一次 LoadEpoch + 合批 ListPrivacyRules + 写回)。
type CachedPrivacyStore struct {
inner store.PrivacyStore
cache *readmodelcache.Cache[int64, privacyRulesMap]
}
func NewCachedPrivacyStore(inner store.PrivacyStore, ttl time.Duration) *CachedPrivacyStore {
if inner == nil {
return nil
}
if ttl <= 0 {
ttl = DefaultPrivacyRulesCacheTTL
}
return &CachedPrivacyStore{
inner: inner,
cache: readmodelcache.New[int64, privacyRulesMap](readmodelcache.Config[int64, privacyRulesMap]{
MaxEntries: privacySnapshotMaxOwners,
TTL: ttl,
Clone: clonePrivacyRulesMap,
}),
}
}
func (c *CachedPrivacyStore) GetPrivacyRules(ctx context.Context, ownerUserID int64, key domain.PrivacyKey) (domain.PrivacyRules, bool, error) {
if ownerUserID == 0 {
return domain.PrivacyRules{}, false, nil
}
rules, err := c.ownerRules(ctx, ownerUserID)
if err != nil {
return domain.PrivacyRules{}, false, err
}
r, ok := rules[key]
if !ok {
return domain.PrivacyRules{}, false, nil
}
return cloneRules(r), true, nil
}
func (c *CachedPrivacyStore) SetPrivacyRules(ctx context.Context, rules domain.PrivacyRules) error {
if err := c.inner.SetPrivacyRules(ctx, rules); err != nil {
return err
}
c.InvalidateOwners(rules.OwnerUserID)
return nil
}
func (c *CachedPrivacyStore) ListPrivacyRules(ctx context.Context, ownerUserIDs []int64, keys []domain.PrivacyKey) ([]domain.PrivacyRules, error) {
owners := dedupPrivacyOwnerIDs(ownerUserIDs)
if len(owners) == 0 || len(keys) == 0 {
return nil, nil
}
byOwner, err := c.ownerRulesBatch(ctx, owners)
if err != nil {
return nil, err
}
keySet := make(map[domain.PrivacyKey]struct{}, len(keys))
for _, key := range keys {
keySet[key] = struct{}{}
}
out := make([]domain.PrivacyRules, 0, len(owners)*len(keys))
for _, owner := range owners {
for key, rules := range byOwner[owner] {
if _, want := keySet[key]; !want {
continue
}
out = append(out, cloneRules(rules))
}
}
return out, nil
}
func (c *CachedPrivacyStore) ownerRules(ctx context.Context, ownerUserID int64) (privacyRulesMap, error) {
load := func() (privacyRulesMap, error) {
list, err := c.inner.ListPrivacyRules(ctx, []int64{ownerUserID}, allPrivacyRuleKeys)
if err != nil {
return nil, err
}
return buildPrivacyRulesByOwner(list, []int64{ownerUserID})[ownerUserID], nil
}
if c == nil || c.cache == nil {
return load()
}
return c.cache.GetOrLoad(ctx, ownerUserID, load)
}
func (c *CachedPrivacyStore) ownerRulesBatch(ctx context.Context, owners []int64) (map[int64]privacyRulesMap, error) {
loadMissing := func(ctx context.Context, missing []int64) (map[int64]privacyRulesMap, error) {
list, err := c.inner.ListPrivacyRules(ctx, missing, allPrivacyRuleKeys)
if err != nil {
return nil, err
}
return buildPrivacyRulesByOwner(list, missing), nil
}
if c == nil || c.cache == nil {
return loadMissing(ctx, owners)
}
return c.cache.GetOrLoadBatch(ctx, owners,
func(int64) (int64, bool) { return 0, true }, // 纯 TTL,无版本闸门
loadMissing,
)
}
func (c *CachedPrivacyStore) InvalidateOwners(ids ...int64) {
if c == nil || c.cache == nil || len(ids) == 0 {
return
}
nonZero := make([]int64, 0, len(ids))
for _, id := range ids {
if id != 0 {
nonZero = append(nonZero, id)
}
}
c.cache.Invalidate(nonZero...)
}
func (c *CachedPrivacyStore) FlushReadModelCache() {
if c == nil || c.cache == nil {
return
}
c.cache.Flush()
}
// buildPrivacyRulesByOwner 把扁平规则按 owner 归组;每个 owner 都建一个条目(无规则即空 map),
// 这样「查过且无规则」的 owner 也被负缓存,不会反复打后端。
func buildPrivacyRulesByOwner(list []domain.PrivacyRules, owners []int64) map[int64]privacyRulesMap {
out := make(map[int64]privacyRulesMap, len(owners))
for _, owner := range owners {
out[owner] = make(privacyRulesMap)
}
for _, item := range list {
if item.OwnerUserID == 0 || item.Key == "" {
continue
}
m, ok := out[item.OwnerUserID]
if !ok {
m = make(privacyRulesMap)
out[item.OwnerUserID] = m
}
m[item.Key] = cloneRules(item)
}
return out
}
func clonePrivacyRulesMap(in privacyRulesMap) privacyRulesMap {
if in == nil {
return nil
}
out := make(privacyRulesMap, len(in))
for key, rules := range in {
out[key] = cloneRules(rules)
}
return out
}
func dedupPrivacyOwnerIDs(ids []int64) []int64 {
seen := make(map[int64]struct{}, len(ids))
out := make([]int64, 0, 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
}