268 lines
7.7 KiB
Go
268 lines
7.7 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)
|
||
// 数据写入已提交,预热失败不能伪装成写失败;LISTEN/NOTIFY 也会在每个
|
||
// 实例上再次失效并预热,覆盖本实例通知晚于这里到达的时序。
|
||
_ = c.WarmOwners(ctx, rules.OwnerUserID)
|
||
return nil
|
||
}
|
||
|
||
// WarmOwners 在低频写/变更通知路径一次性装入 owner 的完整规则集。调用方必须先
|
||
// InvalidateOwners;epoch 保证预热期间若又发生失效,不会把旧快照写回。
|
||
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
|
||
}
|