144 lignes
3,1 Kio
Go
144 lignes
3,1 Kio
Go
|
package native
|
||
|
|
||
|
import (
|
||
|
"container/list"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"github.com/ewhal/nyaa/common"
|
||
|
"github.com/ewhal/nyaa/model"
|
||
|
)
|
||
|
|
||
|
const expiryTime = time.Minute
|
||
|
|
||
|
// NativeCache implements cache.Cache
|
||
|
type NativeCache struct {
|
||
|
cache map[common.SearchParam]*list.Element
|
||
|
ll *list.List
|
||
|
totalUsed int
|
||
|
mu sync.Mutex
|
||
|
|
||
|
// Size sets the maximum size of the cache before evicting unread data in MB
|
||
|
Size float64
|
||
|
}
|
||
|
|
||
|
// New Creates New Native Cache instance
|
||
|
func New(sz float64) *NativeCache {
|
||
|
return &NativeCache{
|
||
|
cache: make(map[common.SearchParam]*list.Element, 10),
|
||
|
Size: sz,
|
||
|
ll: list.New(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Key stores the ID of either a thread or board page
|
||
|
type Key struct {
|
||
|
LastN uint8
|
||
|
Board string
|
||
|
ID uint64
|
||
|
}
|
||
|
|
||
|
// Single cache entry
|
||
|
type store struct {
|
||
|
sync.Mutex // Controls general access to the contents of the struct
|
||
|
lastFetched time.Time
|
||
|
key common.SearchParam
|
||
|
data []model.Torrent
|
||
|
count, size int
|
||
|
n *NativeCache
|
||
|
}
|
||
|
|
||
|
// Check the cache for and existing record. If miss, run fn to retrieve fresh
|
||
|
// values.
|
||
|
func (n *NativeCache) Get(key common.SearchParam, fn func() ([]model.Torrent, int, error)) (
|
||
|
data []model.Torrent, count int, err error,
|
||
|
) {
|
||
|
s := n.getStore(key)
|
||
|
|
||
|
// Also keeps multiple requesters from simultaneously requesting the same
|
||
|
// data
|
||
|
s.Lock()
|
||
|
defer s.Unlock()
|
||
|
|
||
|
if s.isFresh() {
|
||
|
return s.data, s.count, nil
|
||
|
}
|
||
|
|
||
|
data, count, err = fn()
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
s.update(data, count)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Retrieve a store from the cache or create a new one
|
||
|
func (n *NativeCache) getStore(k common.SearchParam) (s *store) {
|
||
|
n.mu.Lock()
|
||
|
defer n.mu.Unlock()
|
||
|
|
||
|
el := n.cache[k]
|
||
|
if el == nil {
|
||
|
s = &store{key: k, n: n}
|
||
|
n.cache[k] = n.ll.PushFront(s)
|
||
|
} else {
|
||
|
n.ll.MoveToFront(el)
|
||
|
s = el.Value.(*store)
|
||
|
}
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
// Clear the cache. Only used for testing.
|
||
|
func (n *NativeCache) ClearAll() {
|
||
|
n.mu.Lock()
|
||
|
defer n.mu.Unlock()
|
||
|
|
||
|
n.ll = list.New()
|
||
|
n.cache = make(map[common.SearchParam]*list.Element, 10)
|
||
|
}
|
||
|
|
||
|
// Update the total used memory counter and evict, if over limit
|
||
|
func (n *NativeCache) updateUsedSize(delta int) {
|
||
|
n.mu.Lock()
|
||
|
defer n.mu.Unlock()
|
||
|
|
||
|
n.totalUsed += delta
|
||
|
|
||
|
for n.totalUsed > int(n.Size)<<20 {
|
||
|
e := n.ll.Back()
|
||
|
if e == nil {
|
||
|
break
|
||
|
}
|
||
|
s := n.ll.Remove(e).(*store)
|
||
|
delete(n.cache, s.key)
|
||
|
n.totalUsed -= s.size
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Return, if the data can still be considered fresh, without querying the DB
|
||
|
func (s *store) isFresh() bool {
|
||
|
if s.lastFetched.IsZero() { // New store
|
||
|
return false
|
||
|
}
|
||
|
return s.lastFetched.Add(expiryTime).After(time.Now())
|
||
|
}
|
||
|
|
||
|
// Stores the new values of s. Calculates and stores the new size. Passes the
|
||
|
// delta to the central cache to fire eviction checks.
|
||
|
func (s *store) update(data []model.Torrent, count int) {
|
||
|
newSize := 0
|
||
|
for _, d := range data {
|
||
|
newSize += d.Size()
|
||
|
}
|
||
|
s.data = data
|
||
|
s.count = count
|
||
|
delta := newSize - s.size
|
||
|
s.size = newSize
|
||
|
s.lastFetched = time.Now()
|
||
|
|
||
|
// Technically it is possible to update the size even when the store is
|
||
|
// already evicted, but that should never happen, unless you have a very
|
||
|
// small cache, very large stored datasets and a lot of traffic.
|
||
|
s.n.updateUsedSize(delta)
|
||
|
}
|