335 lines
7.8 KiB
Go
335 lines
7.8 KiB
Go
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
|
|
})
|
|
}
|