mirror of
https://github.com/xxjwxc/public.git
synced 2025-10-05 15:56:54 +08:00
518 lines
12 KiB
Go
518 lines
12 KiB
Go
/*
|
|
* Simple caching library with expiration capabilities
|
|
* Copyright (c) 2013-2017, Christian Muehlhaeuser <muesli@gmail.com>
|
|
*
|
|
* For license see LICENSE.txt
|
|
*/
|
|
|
|
package cache2go
|
|
|
|
import (
|
|
"bytes"
|
|
"log"
|
|
"strconv"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
k = "testkey"
|
|
v = "testvalue"
|
|
)
|
|
|
|
func TestCache(t *testing.T) {
|
|
// add an expiring item after a non-expiring one to
|
|
// trigger expirationCheck iterating over non-expiring items
|
|
table := Cache("testCache")
|
|
table.Add(k+"_1", 0*time.Second, v)
|
|
table.Add(k+"_2", 1*time.Second, v)
|
|
|
|
// check if both items are still there
|
|
p, err := table.Value(k + "_1")
|
|
if err != nil || p == nil || p.Data().(string) != v {
|
|
t.Error("Error retrieving non expiring data from cache", err)
|
|
}
|
|
p, err = table.Value(k + "_2")
|
|
if err != nil || p == nil || p.Data().(string) != v {
|
|
t.Error("Error retrieving data from cache", err)
|
|
}
|
|
|
|
// sanity checks
|
|
if p.AccessCount() != 1 {
|
|
t.Error("Error getting correct access count")
|
|
}
|
|
if p.LifeSpan() != 1*time.Second {
|
|
t.Error("Error getting correct life-span")
|
|
}
|
|
if p.AccessedOn().Unix() == 0 {
|
|
t.Error("Error getting access time")
|
|
}
|
|
if p.CreatedOn().Unix() == 0 {
|
|
t.Error("Error getting creation time")
|
|
}
|
|
}
|
|
|
|
func TestCacheExpire(t *testing.T) {
|
|
table := Cache("testCache")
|
|
|
|
table.Add(k+"_1", 250*time.Millisecond, v+"_1")
|
|
table.Add(k+"_2", 200*time.Millisecond, v+"_2")
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// check key `1` is still alive
|
|
_, err := table.Value(k + "_1")
|
|
if err != nil {
|
|
t.Error("Error retrieving value from cache:", err)
|
|
}
|
|
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
// check key `1` again, it should still be alive since we just accessed it
|
|
_, err = table.Value(k + "_1")
|
|
if err != nil {
|
|
t.Error("Error retrieving value from cache:", err)
|
|
}
|
|
|
|
// check key `2`, it should have been removed by now
|
|
_, err = table.Value(k + "_2")
|
|
if err == nil {
|
|
t.Error("Found key which should have been expired by now")
|
|
}
|
|
}
|
|
|
|
func TestExists(t *testing.T) {
|
|
// add an expiring item
|
|
table := Cache("testExists")
|
|
table.Add(k, 0, v)
|
|
// check if it exists
|
|
if !table.Exists(k) {
|
|
t.Error("Error verifying existing data in cache")
|
|
}
|
|
}
|
|
|
|
func TestNotFoundAdd(t *testing.T) {
|
|
table := Cache("testNotFoundAdd")
|
|
|
|
if !table.NotFoundAdd(k, 0, v) {
|
|
t.Error("Error verifying NotFoundAdd, data not in cache")
|
|
}
|
|
|
|
if table.NotFoundAdd(k, 0, v) {
|
|
t.Error("Error verifying NotFoundAdd data in cache")
|
|
}
|
|
}
|
|
|
|
func TestNotFoundAddConcurrency(t *testing.T) {
|
|
table := Cache("testNotFoundAdd")
|
|
|
|
var finish sync.WaitGroup
|
|
var added int32
|
|
var idle int32
|
|
|
|
fn := func(id int) {
|
|
for i := 0; i < 100; i++ {
|
|
if table.NotFoundAdd(i, 0, i+id) {
|
|
atomic.AddInt32(&added, 1)
|
|
} else {
|
|
atomic.AddInt32(&idle, 1)
|
|
}
|
|
time.Sleep(0)
|
|
}
|
|
finish.Done()
|
|
}
|
|
|
|
finish.Add(10)
|
|
go fn(0x0000)
|
|
go fn(0x1100)
|
|
go fn(0x2200)
|
|
go fn(0x3300)
|
|
go fn(0x4400)
|
|
go fn(0x5500)
|
|
go fn(0x6600)
|
|
go fn(0x7700)
|
|
go fn(0x8800)
|
|
go fn(0x9900)
|
|
finish.Wait()
|
|
|
|
t.Log(added, idle)
|
|
|
|
table.Foreach(func(key interface{}, item *CacheItem) {
|
|
v, _ := item.Data().(int)
|
|
k, _ := key.(int)
|
|
t.Logf("%02x %04x\n", k, v)
|
|
})
|
|
}
|
|
|
|
func TestCacheKeepAlive(t *testing.T) {
|
|
// add an expiring item
|
|
table := Cache("testKeepAlive")
|
|
p := table.Add(k, 250*time.Millisecond, v)
|
|
|
|
// keep it alive before it expires
|
|
time.Sleep(100 * time.Millisecond)
|
|
p.KeepAlive()
|
|
|
|
// check it's still alive after it was initially supposed to expire
|
|
time.Sleep(150 * time.Millisecond)
|
|
if !table.Exists(k) {
|
|
t.Error("Error keeping item alive")
|
|
}
|
|
|
|
// check it expires eventually
|
|
time.Sleep(300 * time.Millisecond)
|
|
if table.Exists(k) {
|
|
t.Error("Error expiring item after keeping it alive")
|
|
}
|
|
}
|
|
|
|
func TestPeek(t *testing.T) {
|
|
// add an expiring item
|
|
table := Cache("TestPeek")
|
|
_ = table.Add(k, 250*time.Millisecond, v)
|
|
_ = table.Add(k+"_", 250*time.Millisecond, v)
|
|
|
|
// test peek item
|
|
time.Sleep(150 * time.Millisecond)
|
|
p, _ := table.Peek(k)
|
|
if p.Data() != v {
|
|
t.Error("Error peek item")
|
|
}
|
|
|
|
// test k is expired
|
|
time.Sleep(150 * time.Millisecond)
|
|
if table.Exists(k) {
|
|
t.Error("Error peek but expired time updated")
|
|
}
|
|
|
|
// test peek nil
|
|
_, err := table.Peek(k)
|
|
if err != ErrKeyNotFound {
|
|
t.Error("Error peek nil value but not return ErrKeyNotFound")
|
|
}
|
|
|
|
// test DataLoader
|
|
table.SetDataLoader(func(key interface{}, args ...interface{}) *CacheItem {
|
|
if key == k {
|
|
return NewCacheItem(key, 150*time.Millisecond, "new value")
|
|
}
|
|
return nil
|
|
})
|
|
p, _ = table.Peek(k)
|
|
if p.Data() != "new value" {
|
|
t.Error("Error peek DataLoader callback did not take effect")
|
|
}
|
|
|
|
_, err = table.Peek(k + "_")
|
|
if err != ErrKeyNotFoundOrLoadable {
|
|
t.Error("Error peek DataLoader err")
|
|
}
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
// add an item to the cache
|
|
table := Cache("testDelete")
|
|
table.Add(k, 0, v)
|
|
// check it's really cached
|
|
p, err := table.Value(k)
|
|
if err != nil || p == nil || p.Data().(string) != v {
|
|
t.Error("Error retrieving data from cache", err)
|
|
}
|
|
// try to delete it
|
|
table.Delete(k)
|
|
// verify it has been deleted
|
|
p, err = table.Value(k)
|
|
if err == nil || p != nil {
|
|
t.Error("Error deleting data")
|
|
}
|
|
|
|
// test error handling
|
|
_, err = table.Delete(k)
|
|
if err == nil {
|
|
t.Error("Expected error deleting item")
|
|
}
|
|
}
|
|
|
|
func TestFlush(t *testing.T) {
|
|
// add an item to the cache
|
|
table := Cache("testFlush")
|
|
table.Add(k, 10*time.Second, v)
|
|
// flush the entire table
|
|
table.Flush()
|
|
|
|
// try to retrieve the item
|
|
p, err := table.Value(k)
|
|
if err == nil || p != nil {
|
|
t.Error("Error flushing table")
|
|
}
|
|
// make sure there's really nothing else left in the cache
|
|
if table.Count() != 0 {
|
|
t.Error("Error verifying count of flushed table")
|
|
}
|
|
}
|
|
|
|
func TestCount(t *testing.T) {
|
|
// add a huge amount of items to the cache
|
|
table := Cache("testCount")
|
|
count := 100000
|
|
for i := 0; i < count; i++ {
|
|
key := k + strconv.Itoa(i)
|
|
table.Add(key, 10*time.Second, v)
|
|
}
|
|
// confirm every single item has been cached
|
|
for i := 0; i < count; i++ {
|
|
key := k + strconv.Itoa(i)
|
|
p, err := table.Value(key)
|
|
if err != nil || p == nil || p.Data().(string) != v {
|
|
t.Error("Error retrieving data")
|
|
}
|
|
}
|
|
// make sure the item count matches (no dupes etc.)
|
|
if table.Count() != count {
|
|
t.Error("Data count mismatch")
|
|
}
|
|
}
|
|
|
|
func TestDataLoader(t *testing.T) {
|
|
// setup a cache with a configured data-loader
|
|
table := Cache("testDataLoader")
|
|
table.SetDataLoader(func(key interface{}, args ...interface{}) *CacheItem {
|
|
var item *CacheItem
|
|
if key.(string) != "nil" {
|
|
val := k + key.(string)
|
|
i := NewCacheItem(key, 500*time.Millisecond, val)
|
|
item = i
|
|
}
|
|
|
|
return item
|
|
})
|
|
|
|
// make sure data-loader works as expected and handles unloadable keys
|
|
_, err := table.Value("nil")
|
|
if err == nil || table.Exists("nil") {
|
|
t.Error("Error validating data loader for nil values")
|
|
}
|
|
|
|
// retrieve a bunch of items via the data-loader
|
|
for i := 0; i < 10; i++ {
|
|
key := k + strconv.Itoa(i)
|
|
vp := k + key
|
|
p, err := table.Value(key)
|
|
if err != nil || p == nil || p.Data().(string) != vp {
|
|
t.Error("Error validating data loader")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAccessCount(t *testing.T) {
|
|
// add 100 items to the cache
|
|
count := 100
|
|
table := Cache("testAccessCount")
|
|
for i := 0; i < count; i++ {
|
|
table.Add(i, 10*time.Second, v)
|
|
}
|
|
// never access the first item, access the second item once, the third
|
|
// twice and so on...
|
|
for i := 0; i < count; i++ {
|
|
for j := 0; j < i; j++ {
|
|
table.Value(i)
|
|
}
|
|
}
|
|
|
|
// check MostAccessed returns the items in correct order
|
|
ma := table.MostAccessed(int64(count))
|
|
for i, item := range ma {
|
|
if item.Key() != count-1-i {
|
|
t.Error("Most accessed items seem to be sorted incorrectly")
|
|
}
|
|
}
|
|
|
|
// check MostAccessed returns the correct amount of items
|
|
ma = table.MostAccessed(int64(count - 1))
|
|
if len(ma) != count-1 {
|
|
t.Error("MostAccessed returns incorrect amount of items")
|
|
}
|
|
}
|
|
|
|
func TestCallbacks(t *testing.T) {
|
|
var m sync.Mutex
|
|
addedKey := ""
|
|
removedKey := ""
|
|
calledAddedItem := false
|
|
calledRemoveItem := false
|
|
expired := false
|
|
calledExpired := false
|
|
|
|
// setup a cache with AddedItem & SetAboutToDelete handlers configured
|
|
table := Cache("testCallbacks")
|
|
table.SetAddedItemCallback(func(item *CacheItem) {
|
|
m.Lock()
|
|
addedKey = item.Key().(string)
|
|
m.Unlock()
|
|
})
|
|
table.SetAddedItemCallback(func(item *CacheItem) {
|
|
m.Lock()
|
|
calledAddedItem = true
|
|
m.Unlock()
|
|
})
|
|
table.SetAboutToDeleteItemCallback(func(item *CacheItem) {
|
|
m.Lock()
|
|
removedKey = item.Key().(string)
|
|
m.Unlock()
|
|
})
|
|
|
|
table.SetAboutToDeleteItemCallback(func(item *CacheItem) {
|
|
m.Lock()
|
|
calledRemoveItem = true
|
|
m.Unlock()
|
|
})
|
|
// add an item to the cache and setup its AboutToExpire handler
|
|
i := table.Add(k, 500*time.Millisecond, v)
|
|
i.SetAboutToExpireCallback(func(key interface{}) {
|
|
m.Lock()
|
|
expired = true
|
|
m.Unlock()
|
|
})
|
|
|
|
i.SetAboutToExpireCallback(func(key interface{}) {
|
|
m.Lock()
|
|
calledExpired = true
|
|
m.Unlock()
|
|
})
|
|
|
|
// verify the AddedItem handler works
|
|
time.Sleep(250 * time.Millisecond)
|
|
m.Lock()
|
|
if addedKey == k && !calledAddedItem {
|
|
t.Error("AddedItem callback not working")
|
|
}
|
|
m.Unlock()
|
|
// verify the AboutToDelete handler works
|
|
time.Sleep(500 * time.Millisecond)
|
|
m.Lock()
|
|
if removedKey == k && !calledRemoveItem {
|
|
t.Error("AboutToDeleteItem callback not working:" + k + "_" + removedKey)
|
|
}
|
|
// verify the AboutToExpire handler works
|
|
if expired && !calledExpired {
|
|
t.Error("AboutToExpire callback not working")
|
|
}
|
|
m.Unlock()
|
|
|
|
}
|
|
|
|
func TestCallbackQueue(t *testing.T) {
|
|
var m sync.Mutex
|
|
addedKey := ""
|
|
addedkeyCallback2 := ""
|
|
secondCallbackResult := "second"
|
|
removedKey := ""
|
|
removedKeyCallback := ""
|
|
expired := false
|
|
calledExpired := false
|
|
// setup a cache with AddedItem & SetAboutToDelete handlers configured
|
|
table := Cache("testCallbacks")
|
|
|
|
// test callback queue
|
|
table.AddAddedItemCallback(func(item *CacheItem) {
|
|
m.Lock()
|
|
addedKey = item.Key().(string)
|
|
m.Unlock()
|
|
})
|
|
table.AddAddedItemCallback(func(item *CacheItem) {
|
|
m.Lock()
|
|
addedkeyCallback2 = secondCallbackResult
|
|
m.Unlock()
|
|
})
|
|
|
|
table.AddAboutToDeleteItemCallback(func(item *CacheItem) {
|
|
m.Lock()
|
|
removedKey = item.Key().(string)
|
|
m.Unlock()
|
|
})
|
|
table.AddAboutToDeleteItemCallback(func(item *CacheItem) {
|
|
m.Lock()
|
|
removedKeyCallback = secondCallbackResult
|
|
m.Unlock()
|
|
})
|
|
|
|
i := table.Add(k, 500*time.Millisecond, v)
|
|
i.AddAboutToExpireCallback(func(key interface{}) {
|
|
m.Lock()
|
|
expired = true
|
|
m.Unlock()
|
|
})
|
|
i.AddAboutToExpireCallback(func(key interface{}) {
|
|
m.Lock()
|
|
calledExpired = true
|
|
m.Unlock()
|
|
})
|
|
|
|
time.Sleep(250 * time.Millisecond)
|
|
m.Lock()
|
|
if addedKey != k && addedkeyCallback2 != secondCallbackResult {
|
|
t.Error("AddedItem callback queue not working")
|
|
}
|
|
m.Unlock()
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
m.Lock()
|
|
if removedKey != k && removedKeyCallback != secondCallbackResult {
|
|
t.Error("Item removed callback queue not working")
|
|
}
|
|
m.Unlock()
|
|
|
|
// test removing of the callbacks
|
|
table.RemoveAddedItemCallbacks()
|
|
table.RemoveAboutToDeleteItemCallback()
|
|
secondItemKey := "itemKey02"
|
|
expired = false
|
|
i = table.Add(secondItemKey, 500*time.Millisecond, v)
|
|
i.SetAboutToExpireCallback(func(key interface{}) {
|
|
m.Lock()
|
|
expired = true
|
|
m.Unlock()
|
|
})
|
|
i.RemoveAboutToExpireCallback()
|
|
|
|
// verify if the callbacks were removed
|
|
time.Sleep(250 * time.Millisecond)
|
|
m.Lock()
|
|
if addedKey == secondItemKey {
|
|
t.Error("AddedItemCallbacks were not removed")
|
|
}
|
|
m.Unlock()
|
|
|
|
// verify the AboutToDelete handler works
|
|
time.Sleep(500 * time.Millisecond)
|
|
m.Lock()
|
|
if removedKey == secondItemKey {
|
|
t.Error("AboutToDeleteItem not removed")
|
|
}
|
|
// verify the AboutToExpire handler works
|
|
if !expired && !calledExpired {
|
|
t.Error("AboutToExpire callback not working")
|
|
}
|
|
m.Unlock()
|
|
}
|
|
|
|
func TestLogger(t *testing.T) {
|
|
// setup a logger
|
|
out := new(bytes.Buffer)
|
|
l := log.New(out, "cache2go ", log.Ldate|log.Ltime)
|
|
|
|
// setup a cache with this logger
|
|
table := Cache("testLogger")
|
|
table.SetLogger(l)
|
|
table.Add(k, 0, v)
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// verify the logger has been used
|
|
if out.Len() == 0 {
|
|
t.Error("Logger is empty")
|
|
}
|
|
}
|