Files
backend/internal/cache/lru_striped.go
lafay 98f0c9f2b6
All checks were successful
Build Backend / build (push) Successful in 5m54s
Build Backend / build-docker (push) Successful in 4m7s
feat: add hook system, QR code login, and layered cache
- Implement extensible hook system for content moderation with
  builtin and AI-powered moderation hooks
- Add QR code login feature with SSE for real-time status updates
  and scan/confirm/cancel workflow
- Introduce layered cache with local LRU + Redis backend for
  improved read performance
- Refactor post and comment services to use hook-based moderation
  instead of direct AI service calls
- Add local cache configuration options (size, buckets, ttl)
2026-03-20 12:23:28 +08:00

168 lines
3.4 KiB
Go

package cache
import (
"math"
"runtime"
"time"
"github.com/cespare/xxhash/v2"
)
type LRUStriped struct {
buckets []*LRU
name string
}
type LRUStripedOptions struct {
Size int
DefaultExpiry time.Duration
Name string
Buckets int
}
func NewLRUStriped(opts *LRUStripedOptions) *LRUStriped {
if opts.Buckets <= 0 {
opts.Buckets = runtime.NumCPU()
}
if opts.Buckets < 4 {
opts.Buckets = 4
}
if opts.Size < opts.Buckets {
opts.Size = opts.Buckets * 100
}
opts.Size += int(math.Ceil(float64(opts.Size) * 10.0 / 100.0))
bucketSize := (opts.Size / opts.Buckets) + (opts.Size % opts.Buckets)
buckets := make([]*LRU, opts.Buckets)
for i := 0; i < opts.Buckets; i++ {
buckets[i] = NewLRU(&LRUOptions{
Size: bucketSize,
DefaultExpiry: opts.DefaultExpiry,
Name: opts.Name,
})
}
return &LRUStriped{
buckets: buckets,
name: opts.Name,
}
}
func (l *LRUStriped) hashKey(key string) uint64 {
return xxhash.Sum64String(key)
}
func (l *LRUStriped) keyBucket(key string) *LRU {
return l.buckets[l.hashKey(key)%uint64(len(l.buckets))]
}
func (l *LRUStriped) Set(key string, value any, ttl time.Duration) {
l.keyBucket(key).Set(key, value, ttl)
}
func (l *LRUStriped) Get(key string) (any, bool) {
return l.keyBucket(key).Get(key)
}
func (l *LRUStriped) Delete(key string) {
l.keyBucket(key).Delete(key)
}
func (l *LRUStriped) DeleteByPrefix(prefix string) {
for _, bucket := range l.buckets {
bucket.DeleteByPrefix(prefix)
}
}
func (l *LRUStriped) Clear() {
for _, bucket := range l.buckets {
bucket.Clear()
}
}
func (l *LRUStriped) Exists(key string) bool {
return l.keyBucket(key).Exists(key)
}
func (l *LRUStriped) Increment(key string) int64 {
return l.keyBucket(key).Increment(key)
}
func (l *LRUStriped) IncrementBy(key string, value int64) int64 {
return l.keyBucket(key).IncrementBy(key, value)
}
func (l *LRUStriped) Len() int {
var total int
for _, bucket := range l.buckets {
total += bucket.Len()
}
return total
}
func (l *LRUStriped) Name() string {
return l.name
}
func (l *LRUStriped) DeleteMulti(keys []string) error {
var err error
for _, key := range keys {
l.Delete(key)
}
return err
}
func (l *LRUStriped) GetMulti(keys []string, values []any) []error {
errs := make([]error, len(values))
for i, key := range keys {
if val, ok := l.Get(key); ok {
values[i] = val
} else {
errs[i] = ErrKeyNotFound
}
}
return errs
}
func (l *LRUStriped) SetMulti(items map[string]any, ttl time.Duration) error {
for key, value := range items {
l.Set(key, value, ttl)
}
return nil
}
func (l *LRUStriped) Keys() []string {
keys := make([]string, 0)
for _, bucket := range l.buckets {
bucket.lock.RLock()
for key, ent := range bucket.items {
e := ent.Value.(*lruEntry)
if e.generation == bucket.currentGeneration && (e.expires.IsZero() || !time.Now().After(e.expires)) {
keys = append(keys, key)
}
}
bucket.lock.RUnlock()
}
return keys
}
type LRUStripedInterface interface {
Set(key string, value any, ttl time.Duration)
Get(key string) (any, bool)
Delete(key string)
DeleteByPrefix(prefix string)
Clear()
Exists(key string) bool
Increment(key string) int64
IncrementBy(key string, value int64) int64
Len() int
Name() string
DeleteMulti(keys []string) error
GetMulti(keys []string, values []any) []error
SetMulti(items map[string]any, ttl time.Duration) error
Keys() []string
}
var _ LRUStripedInterface = (*LRUStriped)(nil)