From 5666aa17ff578bbf40bb2c1c5f956b32824c1729 Mon Sep 17 00:00:00 2001 From: xxj <346944475@qq.com> Date: Mon, 26 Sep 2022 16:57:06 +0800 Subject: [PATCH] get the value but not update the expiration time --- go.mod | 1 - go.sum | 1 - mycache/cache2go/benchmark_test.go | 49 ++ mycache/cache2go/cache.go | 42 ++ mycache/cache2go/cache_test.go | 517 ++++++++++++++++++ mycache/cache2go/cacheitem.go | 132 +++++ mycache/cache2go/cachetable.go | 398 ++++++++++++++ mycache/cache2go/errors.go | 20 + .../cache2go/examples/callbacks/callbacks.go | 51 ++ .../examples/dataloader/dataloader.go | 34 ++ .../examples/mycachedapp/mycachedapp.go | 53 ++ mycache/mycache.go | 7 +- mycache1/mycache.go | 20 +- 13 files changed, 1309 insertions(+), 16 deletions(-) create mode 100644 mycache/cache2go/benchmark_test.go create mode 100644 mycache/cache2go/cache.go create mode 100644 mycache/cache2go/cache_test.go create mode 100644 mycache/cache2go/cacheitem.go create mode 100644 mycache/cache2go/cachetable.go create mode 100644 mycache/cache2go/errors.go create mode 100644 mycache/cache2go/examples/callbacks/callbacks.go create mode 100644 mycache/cache2go/examples/dataloader/dataloader.go create mode 100644 mycache/cache2go/examples/mycachedapp/mycachedapp.go diff --git a/go.mod b/go.mod index a0f0a40..247e1e0 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,6 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/jroimartin/gocui v0.4.0 github.com/kardianos/service v1.2.1 - github.com/muesli/cache2go v0.0.0-20200423001931-a100c5aac93f github.com/nicksnyder/go-i18n/v2 v2.0.3 github.com/nsqio/go-nsq v1.0.8 github.com/olivere/elastic v6.2.31+incompatible diff --git a/go.sum b/go.sum index 992a598..0ca6df4 100644 --- a/go.sum +++ b/go.sum @@ -144,7 +144,6 @@ github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/muesli/cache2go v0.0.0-20200423001931-a100c5aac93f h1:dBzTzAKOh89fTvdQ3XlXupMExvLty90V1rrpChTAQAY= github.com/muesli/cache2go v0.0.0-20200423001931-a100c5aac93f/go.mod h1:414R+qZrt4f9S2TO/s6YVQMNAXR2KdwqQ7pW+O4oYzU= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nicksnyder/go-i18n/v2 v2.0.3 h1:ks/JkQiOEhhuF6jpNvx+Wih1NIiXzUnZeZVnJuI8R8M= diff --git a/mycache/cache2go/benchmark_test.go b/mycache/cache2go/benchmark_test.go new file mode 100644 index 0000000..64961e0 --- /dev/null +++ b/mycache/cache2go/benchmark_test.go @@ -0,0 +1,49 @@ +/* + * Simple caching library with expiration capabilities + * Copyright (c) 2013-2017, Christian Muehlhaeuser + * + * For license see LICENSE.txt + */ + +package cache2go + +import ( + "sync" + "sync/atomic" + "testing" + "time" +) + +func BenchmarkNotFoundAdd(b *testing.B) { + table := Cache("testNotFoundAdd") + + var finish sync.WaitGroup + var added int32 + var idle int32 + + fn := func(id int) { + for i := 0; i < b.N; 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() + +} diff --git a/mycache/cache2go/cache.go b/mycache/cache2go/cache.go new file mode 100644 index 0000000..232c2ae --- /dev/null +++ b/mycache/cache2go/cache.go @@ -0,0 +1,42 @@ +/* + * Simple caching library with expiration capabilities + * Copyright (c) 2012, Radu Ioan Fericean + * 2013-2017, Christian Muehlhaeuser + * + * For license see LICENSE.txt + */ + +package cache2go + +import ( + "sync" +) + +var ( + cache = make(map[string]*CacheTable) + mutex sync.RWMutex +) + +// Cache returns the existing cache table with given name or creates a new one +// if the table does not exist yet. +func Cache(table string) *CacheTable { + mutex.RLock() + t, ok := cache[table] + mutex.RUnlock() + + if !ok { + mutex.Lock() + t, ok = cache[table] + // Double check whether the table exists or not. + if !ok { + t = &CacheTable{ + name: table, + items: make(map[interface{}]*CacheItem), + } + cache[table] = t + } + mutex.Unlock() + } + + return t +} diff --git a/mycache/cache2go/cache_test.go b/mycache/cache2go/cache_test.go new file mode 100644 index 0000000..1c7234a --- /dev/null +++ b/mycache/cache2go/cache_test.go @@ -0,0 +1,517 @@ +/* + * Simple caching library with expiration capabilities + * Copyright (c) 2013-2017, Christian Muehlhaeuser + * + * 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") + } +} diff --git a/mycache/cache2go/cacheitem.go b/mycache/cache2go/cacheitem.go new file mode 100644 index 0000000..232c44c --- /dev/null +++ b/mycache/cache2go/cacheitem.go @@ -0,0 +1,132 @@ +/* + * Simple caching library with expiration capabilities + * Copyright (c) 2013-2017, Christian Muehlhaeuser + * + * For license see LICENSE.txt + */ + +package cache2go + +import ( + "sync" + "time" +) + +// CacheItem is an individual cache item +// Parameter data contains the user-set value in the cache. +type CacheItem struct { + sync.RWMutex + + // The item's key. + key interface{} + // The item's data. + data interface{} + // How long will the item live in the cache when not being accessed/kept alive. + lifeSpan time.Duration + + // Creation timestamp. + createdOn time.Time + // Last access timestamp. + accessedOn time.Time + // How often the item was accessed. + accessCount int64 + + // Callback method triggered right before removing the item from the cache + aboutToExpire []func(key interface{}) +} + +// NewCacheItem returns a newly created CacheItem. +// Parameter key is the item's cache-key. +// Parameter lifeSpan determines after which time period without an access the item +// will get removed from the cache. +// Parameter data is the item's value. +func NewCacheItem(key interface{}, lifeSpan time.Duration, data interface{}) *CacheItem { + t := time.Now() + return &CacheItem{ + key: key, + lifeSpan: lifeSpan, + createdOn: t, + accessedOn: t, + accessCount: 0, + aboutToExpire: nil, + data: data, + } +} + +// AddAccessCount only add one access count. +func (item *CacheItem) AddAccessCount() { + item.Lock() + defer item.Unlock() + item.accessCount++ +} + +// KeepAlive marks an item to be kept for another expireDuration period. +func (item *CacheItem) KeepAlive() { + item.Lock() + defer item.Unlock() + item.accessedOn = time.Now() + item.accessCount++ +} + +// LifeSpan returns this item's expiration duration. +func (item *CacheItem) LifeSpan() time.Duration { + // immutable + return item.lifeSpan +} + +// AccessedOn returns when this item was last accessed. +func (item *CacheItem) AccessedOn() time.Time { + item.RLock() + defer item.RUnlock() + return item.accessedOn +} + +// CreatedOn returns when this item was added to the cache. +func (item *CacheItem) CreatedOn() time.Time { + // immutable + return item.createdOn +} + +// AccessCount returns how often this item has been accessed. +func (item *CacheItem) AccessCount() int64 { + item.RLock() + defer item.RUnlock() + return item.accessCount +} + +// Key returns the key of this cached item. +func (item *CacheItem) Key() interface{} { + // immutable + return item.key +} + +// Data returns the value of this cached item. +func (item *CacheItem) Data() interface{} { + // immutable + return item.data +} + +// SetAboutToExpireCallback configures a callback, which will be called right +// before the item is about to be removed from the cache. +func (item *CacheItem) SetAboutToExpireCallback(f func(interface{})) { + if len(item.aboutToExpire) > 0 { + item.RemoveAboutToExpireCallback() + } + item.Lock() + defer item.Unlock() + item.aboutToExpire = append(item.aboutToExpire, f) +} + +// AddAboutToExpireCallback appends a new callback to the AboutToExpire queue +func (item *CacheItem) AddAboutToExpireCallback(f func(interface{})) { + item.Lock() + defer item.Unlock() + item.aboutToExpire = append(item.aboutToExpire, f) +} + +// RemoveAboutToExpireCallback empties the about to expire callback queue +func (item *CacheItem) RemoveAboutToExpireCallback() { + item.Lock() + defer item.Unlock() + item.aboutToExpire = nil +} diff --git a/mycache/cache2go/cachetable.go b/mycache/cache2go/cachetable.go new file mode 100644 index 0000000..5e40f32 --- /dev/null +++ b/mycache/cache2go/cachetable.go @@ -0,0 +1,398 @@ +/* + * Simple caching library with expiration capabilities + * Copyright (c) 2013-2017, Christian Muehlhaeuser + * + * For license see LICENSE.txt + */ + +package cache2go + +import ( + "log" + "sort" + "sync" + "time" +) + +// CacheTable is a table within the cache +type CacheTable struct { + sync.RWMutex + + // The table's name. + name string + // All cached items. + items map[interface{}]*CacheItem + + // Timer responsible for triggering cleanup. + cleanupTimer *time.Timer + // Current timer duration. + cleanupInterval time.Duration + + // The logger used for this table. + logger *log.Logger + + // Callback method triggered when trying to load a non-existing key. + loadData func(key interface{}, args ...interface{}) *CacheItem + // Callback method triggered when adding a new item to the cache. + addedItem []func(item *CacheItem) + // Callback method triggered before deleting an item from the cache. + aboutToDeleteItem []func(item *CacheItem) +} + +// Count returns how many items are currently stored in the cache. +func (table *CacheTable) Count() int { + table.RLock() + defer table.RUnlock() + return len(table.items) +} + +// Foreach all items +func (table *CacheTable) Foreach(trans func(key interface{}, item *CacheItem)) { + table.RLock() + defer table.RUnlock() + + for k, v := range table.items { + trans(k, v) + } +} + +// SetDataLoader configures a data-loader callback, which will be called when +// trying to access a non-existing key. The key and 0...n additional arguments +// are passed to the callback function. +func (table *CacheTable) SetDataLoader(f func(interface{}, ...interface{}) *CacheItem) { + table.Lock() + defer table.Unlock() + table.loadData = f +} + +// SetAddedItemCallback configures a callback, which will be called every time +// a new item is added to the cache. +func (table *CacheTable) SetAddedItemCallback(f func(*CacheItem)) { + if len(table.addedItem) > 0 { + table.RemoveAddedItemCallbacks() + } + table.Lock() + defer table.Unlock() + table.addedItem = append(table.addedItem, f) +} + +// AddAddedItemCallback appends a new callback to the addedItem queue +func (table *CacheTable) AddAddedItemCallback(f func(*CacheItem)) { + table.Lock() + defer table.Unlock() + table.addedItem = append(table.addedItem, f) +} + +// RemoveAddedItemCallbacks empties the added item callback queue +func (table *CacheTable) RemoveAddedItemCallbacks() { + table.Lock() + defer table.Unlock() + table.addedItem = nil +} + +// SetAboutToDeleteItemCallback configures a callback, which will be called +// every time an item is about to be removed from the cache. +func (table *CacheTable) SetAboutToDeleteItemCallback(f func(*CacheItem)) { + if len(table.aboutToDeleteItem) > 0 { + table.RemoveAboutToDeleteItemCallback() + } + table.Lock() + defer table.Unlock() + table.aboutToDeleteItem = append(table.aboutToDeleteItem, f) +} + +// AddAboutToDeleteItemCallback appends a new callback to the AboutToDeleteItem queue +func (table *CacheTable) AddAboutToDeleteItemCallback(f func(*CacheItem)) { + table.Lock() + defer table.Unlock() + table.aboutToDeleteItem = append(table.aboutToDeleteItem, f) +} + +// RemoveAboutToDeleteItemCallback empties the about to delete item callback queue +func (table *CacheTable) RemoveAboutToDeleteItemCallback() { + table.Lock() + defer table.Unlock() + table.aboutToDeleteItem = nil +} + +// SetLogger sets the logger to be used by this cache table. +func (table *CacheTable) SetLogger(logger *log.Logger) { + table.Lock() + defer table.Unlock() + table.logger = logger +} + +// Expiration check loop, triggered by a self-adjusting timer. +func (table *CacheTable) expirationCheck() { + table.Lock() + if table.cleanupTimer != nil { + table.cleanupTimer.Stop() + } + if table.cleanupInterval > 0 { + table.log("Expiration check triggered after", table.cleanupInterval, "for table", table.name) + } else { + table.log("Expiration check installed for table", table.name) + } + + // To be more accurate with timers, we would need to update 'now' on every + // loop iteration. Not sure it's really efficient though. + now := time.Now() + smallestDuration := 0 * time.Second + for key, item := range table.items { + // Cache values so we don't keep blocking the mutex. + item.RLock() + lifeSpan := item.lifeSpan + accessedOn := item.accessedOn + item.RUnlock() + + if lifeSpan == 0 { + continue + } + if now.Sub(accessedOn) >= lifeSpan { + // Item has excessed its lifespan. + table.deleteInternal(key) + } else { + // Find the item chronologically closest to its end-of-lifespan. + if smallestDuration == 0 || lifeSpan-now.Sub(accessedOn) < smallestDuration { + smallestDuration = lifeSpan - now.Sub(accessedOn) + } + } + } + + // Setup the interval for the next cleanup run. + table.cleanupInterval = smallestDuration + if smallestDuration > 0 { + table.cleanupTimer = time.AfterFunc(smallestDuration, func() { + go table.expirationCheck() + }) + } + table.Unlock() +} + +func (table *CacheTable) addInternal(item *CacheItem) { + // Careful: do not run this method unless the table-mutex is locked! + // It will unlock it for the caller before running the callbacks and checks + table.log("Adding item with key", item.key, "and lifespan of", item.lifeSpan, "to table", table.name) + table.items[item.key] = item + + // Cache values so we don't keep blocking the mutex. + expDur := table.cleanupInterval + addedItem := table.addedItem + table.Unlock() + + // Trigger callback after adding an item to cache. + if addedItem != nil { + for _, callback := range addedItem { + callback(item) + } + } + + // If we haven't set up any expiration check timer or found a more imminent item. + if item.lifeSpan > 0 && (expDur == 0 || item.lifeSpan < expDur) { + table.expirationCheck() + } +} + +// Add adds a key/value pair to the cache. +// Parameter key is the item's cache-key. +// Parameter lifeSpan determines after which time period without an access the item +// will get removed from the cache. +// Parameter data is the item's value. +func (table *CacheTable) Add(key interface{}, lifeSpan time.Duration, data interface{}) *CacheItem { + item := NewCacheItem(key, lifeSpan, data) + + // Add item to cache. + table.Lock() + table.addInternal(item) + + return item +} + +func (table *CacheTable) deleteInternal(key interface{}) (*CacheItem, error) { + r, ok := table.items[key] + if !ok { + return nil, ErrKeyNotFound + } + + // Cache value so we don't keep blocking the mutex. + aboutToDeleteItem := table.aboutToDeleteItem + table.Unlock() + + // Trigger callbacks before deleting an item from cache. + if aboutToDeleteItem != nil { + for _, callback := range aboutToDeleteItem { + callback(r) + } + } + + r.RLock() + defer r.RUnlock() + if r.aboutToExpire != nil { + for _, callback := range r.aboutToExpire { + callback(key) + } + } + + table.Lock() + table.log("Deleting item with key", key, "created on", r.createdOn, "and hit", r.accessCount, "times from table", table.name) + delete(table.items, key) + + return r, nil +} + +// Delete an item from the cache. +func (table *CacheTable) Delete(key interface{}) (*CacheItem, error) { + table.Lock() + defer table.Unlock() + + return table.deleteInternal(key) +} + +// Exists returns whether an item exists in the cache. Unlike the Value method +// Exists neither tries to fetch data via the loadData callback nor does it +// keep the item alive in the cache. +func (table *CacheTable) Exists(key interface{}) bool { + table.RLock() + defer table.RUnlock() + _, ok := table.items[key] + + return ok +} + +// NotFoundAdd checks whether an item is not yet cached. Unlike the Exists +// method this also adds data if the key could not be found. +func (table *CacheTable) NotFoundAdd(key interface{}, lifeSpan time.Duration, data interface{}) bool { + table.Lock() + + if _, ok := table.items[key]; ok { + table.Unlock() + return false + } + + item := NewCacheItem(key, lifeSpan, data) + table.addInternal(item) + + return true +} + +// Value returns an item from the cache and marks it to be kept alive. You can +// pass additional arguments to your DataLoader callback function. +func (table *CacheTable) Value(key interface{}, args ...interface{}) (*CacheItem, error) { + table.RLock() + r, ok := table.items[key] + loadData := table.loadData + table.RUnlock() + + if ok { + // Update access counter and timestamp. + r.KeepAlive() + return r, nil + } + + // Item doesn't exist in cache. Try and fetch it with a data-loader. + if loadData != nil { + item := loadData(key, args...) + if item != nil { + table.Add(key, item.lifeSpan, item.data) + return item, nil + } + + return nil, ErrKeyNotFoundOrLoadable + } + + return nil, ErrKeyNotFound +} + +// Peek returns an item from the cache, but does not mark its active status, i.e. does not update the active time. +// You can also pass additional arguments to your DataLoader callback function. +func (table *CacheTable) Peek(key interface{}, args ...interface{}) (*CacheItem, error) { + table.RLock() + r, ok := table.items[key] + loadData := table.loadData + table.RUnlock() + + if ok { + r.AddAccessCount() + return r, nil + } + + // Item doesn't exist in cache. Try and fetch it with a data-loader. + if loadData != nil { + item := loadData(key, args...) + if item != nil { + table.Add(key, item.lifeSpan, item.data) + return item, nil + } + + return nil, ErrKeyNotFoundOrLoadable + } + + return nil, ErrKeyNotFound +} + +// Flush deletes all items from this cache table. +func (table *CacheTable) Flush() { + table.Lock() + defer table.Unlock() + + table.log("Flushing table", table.name) + + table.items = make(map[interface{}]*CacheItem) + table.cleanupInterval = 0 + if table.cleanupTimer != nil { + table.cleanupTimer.Stop() + } +} + +// CacheItemPair maps key to access counter +type CacheItemPair struct { + Key interface{} + AccessCount int64 +} + +// CacheItemPairList is a slice of CacheItemPairs that implements sort. +// Interface to sort by AccessCount. +type CacheItemPairList []CacheItemPair + +func (p CacheItemPairList) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p CacheItemPairList) Len() int { return len(p) } +func (p CacheItemPairList) Less(i, j int) bool { return p[i].AccessCount > p[j].AccessCount } + +// MostAccessed returns the most accessed items in this cache table +func (table *CacheTable) MostAccessed(count int64) []*CacheItem { + table.RLock() + defer table.RUnlock() + + p := make(CacheItemPairList, len(table.items)) + i := 0 + for k, v := range table.items { + p[i] = CacheItemPair{k, v.accessCount} + i++ + } + sort.Sort(p) + + var r []*CacheItem + c := int64(0) + for _, v := range p { + if c >= count { + break + } + + item, ok := table.items[v.Key] + if ok { + r = append(r, item) + } + c++ + } + + return r +} + +// Internal logging method for convenience. +func (table *CacheTable) log(v ...interface{}) { + if table.logger == nil { + return + } + + table.logger.Println(v...) +} diff --git a/mycache/cache2go/errors.go b/mycache/cache2go/errors.go new file mode 100644 index 0000000..b2b422a --- /dev/null +++ b/mycache/cache2go/errors.go @@ -0,0 +1,20 @@ +/* + * Simple caching library with expiration capabilities + * Copyright (c) 2013-2017, Christian Muehlhaeuser + * + * For license see LICENSE.txt + */ + +package cache2go + +import ( + "errors" +) + +var ( + // ErrKeyNotFound gets returned when a specific key couldn't be found + ErrKeyNotFound = errors.New("Key not found in cache") + // ErrKeyNotFoundOrLoadable gets returned when a specific key couldn't be + // found and loading via the data-loader callback also failed + ErrKeyNotFoundOrLoadable = errors.New("Key not found and could not be loaded into cache") +) diff --git a/mycache/cache2go/examples/callbacks/callbacks.go b/mycache/cache2go/examples/callbacks/callbacks.go new file mode 100644 index 0000000..03441be --- /dev/null +++ b/mycache/cache2go/examples/callbacks/callbacks.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "time" + + "github.com/xxjwxc/public/mycache/cache2go" +) + +func main() { + cache := cache2go.Cache("myCache") + + // This callback will be triggered every time a new item + // gets added to the cache. + cache.SetAddedItemCallback(func(entry *cache2go.CacheItem) { + fmt.Println("Added Callback 1:", entry.Key(), entry.Data(), entry.CreatedOn()) + }) + cache.AddAddedItemCallback(func(entry *cache2go.CacheItem) { + fmt.Println("Added Callback 2:", entry.Key(), entry.Data(), entry.CreatedOn()) + }) + // This callback will be triggered every time an item + // is about to be removed from the cache. + cache.SetAboutToDeleteItemCallback(func(entry *cache2go.CacheItem) { + fmt.Println("Deleting:", entry.Key(), entry.Data(), entry.CreatedOn()) + }) + + // Caching a new item will execute the AddedItem callback. + cache.Add("someKey", 0, "This is a test!") + + // Let's retrieve the item from the cache + res, err := cache.Value("someKey") + if err == nil { + fmt.Println("Found value in cache:", res.Data()) + } else { + fmt.Println("Error retrieving value from cache:", err) + } + + // Deleting the item will execute the AboutToDeleteItem callback. + cache.Delete("someKey") + + cache.RemoveAddedItemCallbacks() + // Caching a new item that expires in 3 seconds + res = cache.Add("anotherKey", 3*time.Second, "This is another test") + + // This callback will be triggered when the item is about to expire + res.SetAboutToExpireCallback(func(key interface{}) { + fmt.Println("About to expire:", key.(string)) + }) + + time.Sleep(5 * time.Second) +} diff --git a/mycache/cache2go/examples/dataloader/dataloader.go b/mycache/cache2go/examples/dataloader/dataloader.go new file mode 100644 index 0000000..cfd8342 --- /dev/null +++ b/mycache/cache2go/examples/dataloader/dataloader.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "strconv" + + "github.com/xxjwxc/public/mycache/cache2go" +) + +func main() { + cache := cache2go.Cache("myCache") + + // The data loader gets called automatically whenever something + // tries to retrieve a non-existing key from the cache. + cache.SetDataLoader(func(key interface{}, args ...interface{}) *cache2go.CacheItem { + // Apply some clever loading logic here, e.g. read values for + // this key from database, network or file. + val := "This is a test with key " + key.(string) + + // This helper method creates the cached item for us. Yay! + item := cache2go.NewCacheItem(key, 0, val) + return item + }) + + // Let's retrieve a few auto-generated items from the cache. + for i := 0; i < 10; i++ { + res, err := cache.Value("someKey_" + strconv.Itoa(i)) + if err == nil { + fmt.Println("Found value in cache:", res.Data()) + } else { + fmt.Println("Error retrieving value from cache:", err) + } + } +} diff --git a/mycache/cache2go/examples/mycachedapp/mycachedapp.go b/mycache/cache2go/examples/mycachedapp/mycachedapp.go new file mode 100644 index 0000000..15952ac --- /dev/null +++ b/mycache/cache2go/examples/mycachedapp/mycachedapp.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "time" + + "github.com/xxjwxc/public/mycache/cache2go" +) + +// Keys & values in cache2go can be of arbitrary types, e.g. a struct. +type myStruct struct { + text string + moreData []byte +} + +func main() { + // Accessing a new cache table for the first time will create it. + cache := cache2go.Cache("myCache") + + // We will put a new item in the cache. It will expire after + // not being accessed via Value(key) for more than 5 seconds. + val := myStruct{"This is a test!", []byte{}} + cache.Add("someKey", 5*time.Second, &val) + + // Let's retrieve the item from the cache. + res, err := cache.Value("someKey") + if err == nil { + fmt.Println("Found value in cache:", res.Data().(*myStruct).text) + } else { + fmt.Println("Error retrieving value from cache:", err) + } + + // Wait for the item to expire in cache. + time.Sleep(6 * time.Second) + res, err = cache.Value("someKey") + if err != nil { + fmt.Println("Item is not cached (anymore).") + } + + // Add another item that never expires. + cache.Add("someKey", 0, &val) + + // cache2go supports a few handy callbacks and loading mechanisms. + cache.SetAboutToDeleteItemCallback(func(e *cache2go.CacheItem) { + fmt.Println("Deleting:", e.Key(), e.Data().(*myStruct).text, e.CreatedOn()) + }) + + // Remove the item from the cache. + cache.Delete("someKey") + + // And wipe the entire cache table. + cache.Flush() +} diff --git a/mycache/mycache.go b/mycache/mycache.go index dcebf25..ca79e4f 100644 --- a/mycache/mycache.go +++ b/mycache/mycache.go @@ -1,12 +1,12 @@ /* - key/value 内存缓存,支持基于超时的自动无效功能 +key/value 内存缓存,支持基于超时的自动无效功能 */ package mycache import ( "time" - "github.com/muesli/cache2go" + "github.com/xxjwxc/public/mycache/cache2go" "github.com/xxjwxc/public/serializing" ) @@ -21,7 +21,6 @@ type CacheIFS interface { Close() (err error) // 关闭连接 } - // MyCache 内存缓存 type MyCache struct { cache *cache2go.CacheTable @@ -47,7 +46,7 @@ func (mc *MyCache) Add(key interface{}, value interface{}, lifeSpan time.Duratio // Value 查找一个cache func (mc *MyCache) Value(key interface{}, value interface{}) error { - res, err := mc.cache.Value(key) + res, err := mc.cache.Peek(key) if err == nil { bt := res.Data().([]byte) return decodeValue(bt, value) diff --git a/mycache1/mycache.go b/mycache1/mycache.go index 9222a6e..496b1f0 100644 --- a/mycache1/mycache.go +++ b/mycache1/mycache.go @@ -1,12 +1,12 @@ /* - key/value 内存缓存,支持基于超时的自动无效功能 +key/value 内存缓存,支持基于超时的自动无效功能 */ package mycache import ( "time" - "github.com/muesli/cache2go" + "github.com/xxjwxc/public/mycache/cache2go" ) // MyCache 内存缓存 @@ -15,8 +15,8 @@ type MyCache struct { } /* - 初始化一个cache - cachename 缓存名字 +初始化一个cache +cachename 缓存名字 */ func NewCache(cachename string) (mc *MyCache) { mc = &MyCache{} @@ -25,8 +25,8 @@ func NewCache(cachename string) (mc *MyCache) { } /* - 添加一个缓存 - lifeSpan:缓存时间,0表示永不超时 +添加一个缓存 +lifeSpan:缓存时间,0表示永不超时 */ func (mc *MyCache) Add(key interface{}, value interface{}, lifeSpan time.Duration) *cache2go.CacheItem { return mc.cache.Add(key, lifeSpan, value) @@ -49,14 +49,14 @@ func (mc *MyCache) Value(key interface{}) (value interface{}, b bool) { } /* - 判断key是否存在 +判断key是否存在 */ func (mc *MyCache) IsExist(key interface{}) bool { return mc.cache.Exists(key) } /* - 删除一个cache +删除一个cache */ func (mc *MyCache) Delete(key interface{}) error { _, err := mc.cache.Delete(key) @@ -64,14 +64,14 @@ func (mc *MyCache) Delete(key interface{}) error { } /* - 获取原始cache2go操作类 +获取原始cache2go操作类 */ func (mc *MyCache) GetCache2go() *cache2go.CacheTable { return mc.cache } /* - 清空表內容 +清空表內容 */ func (mc *MyCache) Clear() bool { mc.cache.Flush()