@@ -0,0 +1,334 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user