package moduleconfmod import ( "errors" "fmt" "sort" "sync" "time" "91porn-server/common/log" "go.mongodb.org/mongo-driver/bson/primitive" "golang.org/x/sync/singleflight" ) const ( moduleSnapshotTTL = 15 * time.Second moduleSnapshotRetryDelay = time.Second ) var errModuleSnapshotInvalidated = errors.New("module configuration snapshot invalidated while loading") type moduleSnapshotLoader func() ([]ModuleConf, error) // moduleSnapshot is immutable after construction. Every accessor returns // copies so callers cannot mutate data shared by concurrent requests. type moduleSnapshot struct { modules []ModuleConf byID map[primitive.ObjectID]int } func newModuleSnapshot(modules []ModuleConf) *moduleSnapshot { copied := cloneModuleConfs(modules) byID := make(map[primitive.ObjectID]int, len(copied)) for i := range copied { byID[copied[i].ID] = i } return &moduleSnapshot{ modules: copied, byID: byID, } } func (s *moduleSnapshot) all() []ModuleConf { if s == nil { return nil } return cloneModuleConfs(s.modules) } func (s *moduleSnapshot) findByID(id primitive.ObjectID) (ModuleConf, bool) { if s == nil { return ModuleConf{}, false } index, ok := s.byID[id] if !ok { return ModuleConf{}, false } return cloneModuleConf(s.modules[index]), true } func (s *moduleSnapshot) findByIDs(ids []primitive.ObjectID) []ModuleConf { if s == nil || len(ids) == 0 { return nil } wanted := make(map[primitive.ObjectID]struct{}, len(ids)) for _, id := range ids { wanted[id] = struct{}{} } modules := make([]ModuleConf, 0, len(wanted)) for i := range s.modules { if _, ok := wanted[s.modules[i].ID]; ok { modules = append(modules, cloneModuleConf(s.modules[i])) } } return modules } func (s *moduleSnapshot) findByModuleName(moduleName string) []ModuleConf { modules := make([]ModuleConf, 0) for i := range s.modules { if s.modules[i].ModuleName == moduleName { modules = append(modules, cloneModuleConf(s.modules[i])) } } return modules } func (s *moduleSnapshot) findByType(moduleType int) []ModuleConf { modules := make([]ModuleConf, 0) for i := range s.modules { if s.modules[i].Type == moduleType { modules = append(modules, cloneModuleConf(s.modules[i])) } } return modules } func (s *moduleSnapshot) enabled() []ModuleConf { modules := make([]ModuleConf, 0) for i := range s.modules { if s.modules[i].Status == 1 && s.modules[i].DeletedAt == nil { modules = append(modules, cloneModuleConf(s.modules[i])) } } sortModuleConfs(modules) return modules } func (s *moduleSnapshot) activeAt(now time.Time) []ModuleConf { modules := make([]ModuleConf, 0) for i := range s.modules { if s.modules[i].IsActiveAt(now) { modules = append(modules, cloneModuleConf(s.modules[i])) } } sortModuleConfs(modules) return modules } func (s *moduleSnapshot) excludedVideoModuleIDs(now time.Time, recommend bool) []string { ids := make([]string, 0) for i := range s.modules { module := s.modules[i] excludedByScene := module.ExcludeLatest if recommend { excludedByScene = module.ExcludeRecommend } if excludedByScene || (module.SearchOnlyWhenInactive && !module.IsActiveAt(now)) { ids = append(ids, module.ID.Hex()) } } return ids } func (s *moduleSnapshot) excludedSearchModuleIDs() []string { ids := make([]string, 0) for i := range s.modules { if s.modules[i].ExcludeSearch { ids = append(ids, s.modules[i].ID.Hex()) } } return ids } func (s *moduleSnapshot) blockedOutsideSearchModuleIDs(moduleIDs []primitive.ObjectID, now time.Time) map[string]struct{} { blocked := make(map[string]struct{}) seen := make(map[primitive.ObjectID]struct{}, len(moduleIDs)) for _, moduleID := range moduleIDs { if moduleID.IsZero() { continue } if _, ok := seen[moduleID]; ok { continue } seen[moduleID] = struct{}{} index, ok := s.byID[moduleID] if !ok { continue } module := s.modules[index] if module.SearchOnlyWhenInactive && !module.IsActiveAt(now) { blocked[module.ID.Hex()] = struct{}{} } } return blocked } type moduleSnapshotCache struct { mu sync.RWMutex loadGroup singleflight.Group loader moduleSnapshotLoader now func() time.Time ttl time.Duration retryDelay time.Duration snapshot *moduleSnapshot refreshAt time.Time generation uint64 } func newModuleSnapshotCache( loader moduleSnapshotLoader, now func() time.Time, ttl time.Duration, retryDelay time.Duration, ) *moduleSnapshotCache { if now == nil { now = time.Now } if ttl <= 0 { ttl = moduleSnapshotTTL } if retryDelay <= 0 { retryDelay = moduleSnapshotRetryDelay } return &moduleSnapshotCache{ loader: loader, now: now, ttl: ttl, retryDelay: retryDelay, } } func (c *moduleSnapshotCache) get() (*moduleSnapshot, error) { for { now := c.now() c.mu.RLock() if c.snapshot != nil && now.Before(c.refreshAt) { snapshot := c.snapshot c.mu.RUnlock() return snapshot, nil } generation := c.generation c.mu.RUnlock() key := fmt.Sprintf("module-snapshot-%d", generation) value, err, _ := c.loadGroup.Do(key, func() (interface{}, error) { return c.load(generation) }) if errors.Is(err, errModuleSnapshotInvalidated) { continue } if err != nil { return nil, err } // A write may invalidate the cache after load() installs the snapshot // but before singleflight returns it to this reader. c.mu.RLock() currentGeneration := c.generation c.mu.RUnlock() if currentGeneration != generation { continue } return value.(*moduleSnapshot), nil } } func (c *moduleSnapshotCache) load(generation uint64) (*moduleSnapshot, error) { now := c.now() c.mu.RLock() if generation != c.generation { c.mu.RUnlock() return nil, errModuleSnapshotInvalidated } if c.snapshot != nil && now.Before(c.refreshAt) { snapshot := c.snapshot c.mu.RUnlock() return snapshot, nil } c.mu.RUnlock() modules, err := c.loader() loadedAt := c.now() c.mu.Lock() if generation != c.generation { c.mu.Unlock() return nil, errModuleSnapshotInvalidated } if err != nil { if c.snapshot == nil { c.mu.Unlock() return nil, err } // Avoid retrying Mongo on every request while still keeping retries // frequent enough for a transient outage to recover quickly. c.refreshAt = loadedAt.Add(c.retryDelay) snapshot := c.snapshot c.mu.Unlock() log.Warn("module configuration snapshot refresh failed; using last-known-good data", log.E(err)) return snapshot, nil } c.snapshot = newModuleSnapshot(modules) c.refreshAt = loadedAt.Add(c.ttl) snapshot := c.snapshot c.mu.Unlock() return snapshot, nil } // invalidate forces the next reader to reload. The previous snapshot remains // available as last-known-good if the refresh fails. func (c *moduleSnapshotCache) invalidate() { c.mu.Lock() c.generation++ c.refreshAt = time.Time{} c.mu.Unlock() } // reset is used when the package is rebound to a new Mongo client. func (c *moduleSnapshotCache) reset() { c.mu.Lock() c.generation++ c.snapshot = nil c.refreshAt = time.Time{} c.mu.Unlock() } func cloneModuleConfs(modules []ModuleConf) []ModuleConf { if modules == nil { return nil } copied := make([]ModuleConf, len(modules)) for i := range modules { copied[i] = cloneModuleConf(modules[i]) } return copied } func cloneModuleConf(module ModuleConf) ModuleConf { copied := module copied.OnlineAt = cloneTime(module.OnlineAt) copied.OfflineAt = cloneTime(module.OfflineAt) copied.DeletedAt = cloneTime(module.DeletedAt) if module.HaiJiaoStyle.SortRules != nil { copied.HaiJiaoStyle.SortRules = append([]SortItem(nil), module.HaiJiaoStyle.SortRules...) } return copied } func cloneTime(value *time.Time) *time.Time { if value == nil { return nil } copied := *value return &copied } func sortModuleConfs(modules []ModuleConf) { sort.SliceStable(modules, func(i, j int) bool { return modules[i].SortNum < modules[j].SortNum }) }