2017-05-10 10:27:17 +02:00
package cache
import (
"container/list"
"sync"
"time"
2017-05-10 11:03:49 +02:00
"github.com/ewhal/nyaa/common"
2017-05-10 10:27:17 +02:00
"github.com/ewhal/nyaa/model"
)
2017-05-10 12:06:32 +02:00
const expiryTime = time . Minute
2017-05-10 10:27:17 +02:00
var (
2017-05-10 11:03:49 +02:00
cache = make ( map [ common . SearchParam ] * list . Element , 10 )
2017-05-10 10:27:17 +02:00
ll = list . New ( )
totalUsed int
mu sync . Mutex
// Size sets the maximum size of the cache before evicting unread data in MB
Size float64 = 1 << 10
)
// 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 {
2017-05-10 11:03:49 +02:00
sync . Mutex // Controls general access to the contents of the struct
2017-05-10 10:27:17 +02:00
lastFetched time . Time
2017-05-10 11:03:49 +02:00
key common . SearchParam
data [ ] model . Torrent
2017-05-10 11:32:49 +02:00
count , size int
2017-05-10 10:27:17 +02:00
}
// Check the cache for and existing record. If miss, run fn to retrieve fresh
// values.
2017-05-10 11:32:49 +02:00
func Get ( key common . SearchParam , fn func ( ) ( [ ] model . Torrent , int , error ) ) (
data [ ] model . Torrent , count int , err error ,
2017-05-10 10:27:17 +02:00
) {
s := getStore ( key )
// Also keeps multiple requesters from simultaneously requesting the same
// data
s . Lock ( )
defer s . Unlock ( )
if s . isFresh ( ) {
2017-05-10 11:32:49 +02:00
return s . data , s . count , nil
2017-05-10 10:27:17 +02:00
}
2017-05-10 11:32:49 +02:00
data , count , err = fn ( )
2017-05-10 10:27:17 +02:00
if err != nil {
2017-05-10 11:32:49 +02:00
return
2017-05-10 10:27:17 +02:00
}
2017-05-10 11:32:49 +02:00
s . update ( data , count )
return
2017-05-10 10:27:17 +02:00
}
// Retrieve a store from the cache or create a new one
2017-05-10 11:03:49 +02:00
func getStore ( k common . SearchParam ) ( s * store ) {
2017-05-10 10:27:17 +02:00
mu . Lock ( )
defer mu . Unlock ( )
el := cache [ k ]
if el == nil {
s = & store { key : k }
cache [ k ] = ll . PushFront ( s )
} else {
ll . MoveToFront ( el )
s = el . Value . ( * store )
}
return s
}
// Clear the cache. Only used for testing.
func Clear ( ) {
mu . Lock ( )
defer mu . Unlock ( )
ll = list . New ( )
2017-05-10 11:03:49 +02:00
cache = make ( map [ common . SearchParam ] * list . Element , 10 )
2017-05-10 10:27:17 +02:00
}
// Update the total used memory counter and evict, if over limit
func updateUsedSize ( delta int ) {
mu . Lock ( )
defer mu . Unlock ( )
totalUsed += delta
2017-05-10 12:06:32 +02:00
for totalUsed > int ( Size ) << 20 {
2017-05-11 03:04:55 +02:00
e := ll . Back ( )
if e == nil {
break
}
s := ll . Remove ( e ) . ( * store )
2017-05-10 10:27:17 +02:00
delete ( cache , s . key )
totalUsed -= s . size
}
}
// Return, if the data can still be considered fresh, without querying the DB
func ( s * store ) isFresh ( ) bool {
2017-05-10 11:32:49 +02:00
if s . lastFetched . IsZero ( ) { // New store
return false
}
return s . lastFetched . Add ( expiryTime ) . After ( time . Now ( ) )
2017-05-10 10:27:17 +02:00
}
// Stores the new values of s. Calculates and stores the new size. Passes the
// delta to the central cache to fire eviction checks.
2017-05-10 11:32:49 +02:00
func ( s * store ) update ( data [ ] model . Torrent , count int ) {
2017-05-10 10:27:17 +02:00
newSize := 0
for _ , d := range data {
newSize += d . Size ( )
}
s . data = data
2017-05-10 11:32:49 +02:00
s . count = count
2017-05-10 10:27:17 +02:00
delta := newSize - s . size
s . size = newSize
2017-05-10 11:03:49 +02:00
s . lastFetched = time . Now ( )
2017-05-10 10:27:17 +02:00
// 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.
updateUsedSize ( delta )
}