owpengram-server/internal/app/privacy/cache.go
2026-07-24 11:57:00 +08:00

268 lines
7.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
// 数据写入已提交预热失败不能伪装成写失败LISTEN/NOTIFY 也会在每个
// 实例上再次失效并预热,覆盖本实例通知晚于这里到达的时序。
_ = c.WarmOwners(ctx, rules.OwnerUserID)
return nil
}
// WarmOwners 在低频写/变更通知路径一次性装入 owner 的完整规则集。调用方必须先
// InvalidateOwnersepoch 保证预热期间若又发生失效,不会把旧快照写回。
func (c *CachedPrivacyStore) WarmOwners(ctx context.Context, ownerUserIDs ...int64) error {
owners := dedupPrivacyOwnerIDs(ownerUserIDs)
if len(owners) == 0 || c == nil || c.cache == nil {
return nil
}
// 隐私规则变更很少、读取极热。必须重建全部 key
// 不能只塞本次 key否则会把 owner 的其它持久规则误当成默认规则。
loadEpoch := c.cache.LoadEpoch()
list, err := c.inner.ListPrivacyRules(ctx, owners, allPrivacyRuleKeys)
if err != nil {
return err
}
snapshots := buildPrivacyRulesByOwner(list, owners)
for _, ownerUserID := range owners {
c.cache.StoreIfEpoch(ownerUserID, snapshots[ownerUserID], loadEpoch)
}
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()
}
// InvalidateOwners lets Service be registered as the single privacy read-model
// cache group: rule snapshots and relationship facts then share one invalidation
// lifecycle.
func (s *Service) InvalidateOwners(ids ...int64) {
if s == nil || s.rules == nil {
return
}
if cache, ok := s.rules.(interface{ InvalidateOwners(...int64) }); ok {
cache.InvalidateOwners(ids...)
}
}
func (s *Service) WarmOwners(ctx context.Context, ids ...int64) error {
if s == nil || s.rules == nil {
return nil
}
if cache, ok := s.rules.(interface {
WarmOwners(context.Context, ...int64) error
}); ok {
return cache.WarmOwners(ctx, ids...)
}
return nil
}
func (s *Service) FlushReadModelCache() {
if s == nil {
return
}
if cache, ok := s.rules.(interface{ FlushReadModelCache() }); ok {
cache.FlushReadModelCache()
}
s.flushFactCaches()
}
// 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
}