2017-05-11 15:01:53 +02:00
package native
import (
"container/list"
"sync"
"time"
2017-05-17 07:58:40 +02:00
"github.com/NyaaPantsu/nyaa/common"
"github.com/NyaaPantsu/nyaa/model"
2017-05-11 15:01:53 +02:00
)
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 )
2017-05-12 21:32:24 +02:00
s . Lock ( )
2017-05-11 15:01:53 +02:00
n . totalUsed -= s . size
2017-05-12 21:32:24 +02:00
s . Unlock ( )
2017-05-11 15:01:53 +02:00
}
}
// 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 ( )
2017-05-12 21:32:24 +02:00
// In a separate goroutine, to ensure there is never any lock intersection
go s . n . updateUsedSize ( delta )
2017-05-11 15:01:53 +02:00
}