From fe045e40400351ddef4d0617908f72ebb8d1cf86 Mon Sep 17 00:00:00 2001 From: Fabian Ruff Date: Tue, 13 Oct 2015 12:55:54 +0200 Subject: [PATCH 01/40] Fix leaking the janitor ticker when shutting down --- cache.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cache.go b/cache.go index 12f7f0b..68458ab 100644 --- a/cache.go +++ b/cache.go @@ -941,12 +941,13 @@ type janitor struct { func (j *janitor) Run(c *cache) { j.stop = make(chan bool) - tick := time.Tick(j.Interval) + ticker := time.NewTicker(j.Interval) for { select { - case <-tick: + case <-ticker.C: c.DeleteExpired() case <-j.stop: + ticker.Stop() return } } From a0136a89800c7096efe52a676f68d4eca56fa021 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Fri, 27 Nov 2015 13:03:24 -0500 Subject: [PATCH 02/40] Don't expose the cache mutex --- cache.go | 290 +++++++++++++++++++++++++------------------------- cache_test.go | 4 +- 2 files changed, 147 insertions(+), 147 deletions(-) diff --git a/cache.go b/cache.go index 68458ab..ecddbbb 100644 --- a/cache.go +++ b/cache.go @@ -38,9 +38,9 @@ type Cache struct { } type cache struct { - sync.RWMutex defaultExpiration time.Duration items map[string]*Item + mu sync.RWMutex janitor *janitor } @@ -48,11 +48,11 @@ type cache struct { // (DefaultExpiration), the cache's default expiration time is used. If it is -1 // (NoExpiration), the item never expires. func (c *cache) Set(k string, x interface{}, d time.Duration) { - c.Lock() + c.mu.Lock() c.set(k, x, d) // TODO: Calls to mu.Unlock are currently not deferred because defer // adds ~200 ns (as of go1.) - c.Unlock() + c.mu.Unlock() } func (c *cache) set(k string, x interface{}, d time.Duration) { @@ -73,37 +73,37 @@ func (c *cache) set(k string, x interface{}, d time.Duration) { // Add an item to the cache only if an item doesn't already exist for the given // key, or if the existing item has expired. Returns an error otherwise. func (c *cache) Add(k string, x interface{}, d time.Duration) error { - c.Lock() + c.mu.Lock() _, found := c.get(k) if found { - c.Unlock() + c.mu.Unlock() return fmt.Errorf("Item %s already exists", k) } c.set(k, x, d) - c.Unlock() + c.mu.Unlock() return nil } // Set a new value for the cache key only if it already exists, and the existing // item hasn't expired. Returns an error otherwise. func (c *cache) Replace(k string, x interface{}, d time.Duration) error { - c.Lock() + c.mu.Lock() _, found := c.get(k) if !found { - c.Unlock() + c.mu.Unlock() return fmt.Errorf("Item %s doesn't exist", k) } c.set(k, x, d) - c.Unlock() + c.mu.Unlock() return nil } // Get an item from the cache. Returns the item or nil, and a bool indicating // whether the key was found. func (c *cache) Get(k string) (interface{}, bool) { - c.RLock() + c.mu.RLock() x, found := c.get(k) - c.RUnlock() + c.mu.RUnlock() return x, found } @@ -121,10 +121,10 @@ func (c *cache) get(k string) (interface{}, bool) { // possible to increment it by n. To retrieve the incremented value, use one // of the specialized methods, e.g. IncrementInt64. func (c *cache) Increment(k string, n int64) error { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return fmt.Errorf("Item %s not found", k) } switch v.Object.(type) { @@ -155,10 +155,10 @@ func (c *cache) Increment(k string, n int64) error { case float64: v.Object = v.Object.(float64) + float64(n) default: - c.Unlock() + c.mu.Unlock() return fmt.Errorf("The value for %s is not an integer", k) } - c.Unlock() + c.mu.Unlock() return nil } @@ -168,10 +168,10 @@ func (c *cache) Increment(k string, n int64) error { // value. To retrieve the incremented value, use one of the specialized methods, // e.g. IncrementFloat64. func (c *cache) IncrementFloat(k string, n float64) error { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return fmt.Errorf("Item %s not found", k) } switch v.Object.(type) { @@ -180,10 +180,10 @@ func (c *cache) IncrementFloat(k string, n float64) error { case float64: v.Object = v.Object.(float64) + n default: - c.Unlock() + c.mu.Unlock() return fmt.Errorf("The value for %s does not have type float32 or float64", k) } - c.Unlock() + c.mu.Unlock() return nil } @@ -191,20 +191,20 @@ func (c *cache) IncrementFloat(k string, n float64) error { // not an int, or if it was not found. If there is no error, the incremented // value is returned. func (c *cache) IncrementInt(k string, n int) (int, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(int) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an int", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -212,20 +212,20 @@ func (c *cache) IncrementInt(k string, n int) (int, error) { // not an int8, or if it was not found. If there is no error, the incremented // value is returned. func (c *cache) IncrementInt8(k string, n int8) (int8, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(int8) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an int8", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -233,20 +233,20 @@ func (c *cache) IncrementInt8(k string, n int8) (int8, error) { // not an int16, or if it was not found. If there is no error, the incremented // value is returned. func (c *cache) IncrementInt16(k string, n int16) (int16, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(int16) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an int16", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -254,20 +254,20 @@ func (c *cache) IncrementInt16(k string, n int16) (int16, error) { // not an int32, or if it was not found. If there is no error, the incremented // value is returned. func (c *cache) IncrementInt32(k string, n int32) (int32, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(int32) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an int32", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -275,20 +275,20 @@ func (c *cache) IncrementInt32(k string, n int32) (int32, error) { // not an int64, or if it was not found. If there is no error, the incremented // value is returned. func (c *cache) IncrementInt64(k string, n int64) (int64, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(int64) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an int64", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -296,20 +296,20 @@ func (c *cache) IncrementInt64(k string, n int64) (int64, error) { // not an uint, or if it was not found. If there is no error, the incremented // value is returned. func (c *cache) IncrementUint(k string, n uint) (uint, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(uint) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an uint", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -317,20 +317,20 @@ func (c *cache) IncrementUint(k string, n uint) (uint, error) { // is not an uintptr, or if it was not found. If there is no error, the // incremented value is returned. func (c *cache) IncrementUintptr(k string, n uintptr) (uintptr, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(uintptr) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an uintptr", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -338,20 +338,20 @@ func (c *cache) IncrementUintptr(k string, n uintptr) (uintptr, error) { // is not an uint8, or if it was not found. If there is no error, the // incremented value is returned. func (c *cache) IncrementUint8(k string, n uint8) (uint8, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(uint8) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an uint8", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -359,20 +359,20 @@ func (c *cache) IncrementUint8(k string, n uint8) (uint8, error) { // is not an uint16, or if it was not found. If there is no error, the // incremented value is returned. func (c *cache) IncrementUint16(k string, n uint16) (uint16, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(uint16) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an uint16", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -380,20 +380,20 @@ func (c *cache) IncrementUint16(k string, n uint16) (uint16, error) { // is not an uint32, or if it was not found. If there is no error, the // incremented value is returned. func (c *cache) IncrementUint32(k string, n uint32) (uint32, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(uint32) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an uint32", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -401,20 +401,20 @@ func (c *cache) IncrementUint32(k string, n uint32) (uint32, error) { // is not an uint64, or if it was not found. If there is no error, the // incremented value is returned. func (c *cache) IncrementUint64(k string, n uint64) (uint64, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(uint64) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an uint64", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -422,20 +422,20 @@ func (c *cache) IncrementUint64(k string, n uint64) (uint64, error) { // is not an float32, or if it was not found. If there is no error, the // incremented value is returned. func (c *cache) IncrementFloat32(k string, n float32) (float32, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(float32) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an float32", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -443,20 +443,20 @@ func (c *cache) IncrementFloat32(k string, n float32) (float32, error) { // is not an float64, or if it was not found. If there is no error, the // incremented value is returned. func (c *cache) IncrementFloat64(k string, n float64) (float64, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(float64) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an float64", k) } nv := rv + n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -468,10 +468,10 @@ func (c *cache) IncrementFloat64(k string, n float64) (float64, error) { func (c *cache) Decrement(k string, n int64) error { // TODO: Implement Increment and Decrement more cleanly. // (Cannot do Increment(k, n*-1) for uints.) - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return fmt.Errorf("Item not found") } switch v.Object.(type) { @@ -502,10 +502,10 @@ func (c *cache) Decrement(k string, n int64) error { case float64: v.Object = v.Object.(float64) - float64(n) default: - c.Unlock() + c.mu.Unlock() return fmt.Errorf("The value for %s is not an integer", k) } - c.Unlock() + c.mu.Unlock() return nil } @@ -515,10 +515,10 @@ func (c *cache) Decrement(k string, n int64) error { // value. To retrieve the decremented value, use one of the specialized methods, // e.g. DecrementFloat64. func (c *cache) DecrementFloat(k string, n float64) error { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return fmt.Errorf("Item %s not found", k) } switch v.Object.(type) { @@ -527,10 +527,10 @@ func (c *cache) DecrementFloat(k string, n float64) error { case float64: v.Object = v.Object.(float64) - n default: - c.Unlock() + c.mu.Unlock() return fmt.Errorf("The value for %s does not have type float32 or float64", k) } - c.Unlock() + c.mu.Unlock() return nil } @@ -538,20 +538,20 @@ func (c *cache) DecrementFloat(k string, n float64) error { // not an int, or if it was not found. If there is no error, the decremented // value is returned. func (c *cache) DecrementInt(k string, n int) (int, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(int) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an int", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -559,20 +559,20 @@ func (c *cache) DecrementInt(k string, n int) (int, error) { // not an int8, or if it was not found. If there is no error, the decremented // value is returned. func (c *cache) DecrementInt8(k string, n int8) (int8, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(int8) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an int8", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -580,20 +580,20 @@ func (c *cache) DecrementInt8(k string, n int8) (int8, error) { // not an int16, or if it was not found. If there is no error, the decremented // value is returned. func (c *cache) DecrementInt16(k string, n int16) (int16, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(int16) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an int16", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -601,20 +601,20 @@ func (c *cache) DecrementInt16(k string, n int16) (int16, error) { // not an int32, or if it was not found. If there is no error, the decremented // value is returned. func (c *cache) DecrementInt32(k string, n int32) (int32, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(int32) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an int32", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -622,20 +622,20 @@ func (c *cache) DecrementInt32(k string, n int32) (int32, error) { // not an int64, or if it was not found. If there is no error, the decremented // value is returned. func (c *cache) DecrementInt64(k string, n int64) (int64, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(int64) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an int64", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -643,20 +643,20 @@ func (c *cache) DecrementInt64(k string, n int64) (int64, error) { // not an uint, or if it was not found. If there is no error, the decremented // value is returned. func (c *cache) DecrementUint(k string, n uint) (uint, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(uint) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an uint", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -664,20 +664,20 @@ func (c *cache) DecrementUint(k string, n uint) (uint, error) { // is not an uintptr, or if it was not found. If there is no error, the // decremented value is returned. func (c *cache) DecrementUintptr(k string, n uintptr) (uintptr, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(uintptr) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an uintptr", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -685,20 +685,20 @@ func (c *cache) DecrementUintptr(k string, n uintptr) (uintptr, error) { // not an uint8, or if it was not found. If there is no error, the decremented // value is returned. func (c *cache) DecrementUint8(k string, n uint8) (uint8, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(uint8) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an uint8", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -706,20 +706,20 @@ func (c *cache) DecrementUint8(k string, n uint8) (uint8, error) { // is not an uint16, or if it was not found. If there is no error, the // decremented value is returned. func (c *cache) DecrementUint16(k string, n uint16) (uint16, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(uint16) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an uint16", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -727,20 +727,20 @@ func (c *cache) DecrementUint16(k string, n uint16) (uint16, error) { // is not an uint32, or if it was not found. If there is no error, the // decremented value is returned. func (c *cache) DecrementUint32(k string, n uint32) (uint32, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(uint32) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an uint32", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -748,20 +748,20 @@ func (c *cache) DecrementUint32(k string, n uint32) (uint32, error) { // is not an uint64, or if it was not found. If there is no error, the // decremented value is returned. func (c *cache) DecrementUint64(k string, n uint64) (uint64, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(uint64) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an uint64", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -769,20 +769,20 @@ func (c *cache) DecrementUint64(k string, n uint64) (uint64, error) { // is not an float32, or if it was not found. If there is no error, the // decremented value is returned. func (c *cache) DecrementFloat32(k string, n float32) (float32, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(float32) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an float32", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } @@ -790,28 +790,28 @@ func (c *cache) DecrementFloat32(k string, n float32) (float32, error) { // is not an float64, or if it was not found. If there is no error, the // decremented value is returned. func (c *cache) DecrementFloat64(k string, n float64) (float64, error) { - c.Lock() + c.mu.Lock() v, found := c.items[k] if !found || v.Expired() { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } rv, ok := v.Object.(float64) if !ok { - c.Unlock() + c.mu.Unlock() return 0, fmt.Errorf("The value for %s is not an float64", k) } nv := rv - n v.Object = nv - c.Unlock() + c.mu.Unlock() return nv, nil } // Delete an item from the cache. Does nothing if the key is not in the cache. func (c *cache) Delete(k string) { - c.Lock() + c.mu.Lock() c.delete(k) - c.Unlock() + c.mu.Unlock() } func (c *cache) delete(k string) { @@ -820,13 +820,13 @@ func (c *cache) delete(k string) { // Delete all expired items from the cache. func (c *cache) DeleteExpired() { - c.Lock() + c.mu.Lock() for k, v := range c.items { if v.Expired() { c.delete(k) } } - c.Unlock() + c.mu.Unlock() } // Write the cache's items (using Gob) to an io.Writer. @@ -840,8 +840,8 @@ func (c *cache) Save(w io.Writer) (err error) { err = fmt.Errorf("Error registering item types with Gob library") } }() - c.RLock() - defer c.RUnlock() + c.mu.RLock() + defer c.mu.RUnlock() for _, v := range c.items { gob.Register(v.Object) } @@ -877,8 +877,8 @@ func (c *cache) Load(r io.Reader) error { items := map[string]*Item{} err := dec.Decode(&items) if err == nil { - c.Lock() - defer c.Unlock() + c.mu.Lock() + defer c.mu.Unlock() for k, v := range items { ov, found := c.items[k] if !found || ov.Expired() { @@ -913,25 +913,25 @@ func (c *cache) LoadFile(fname string) error { // is needed to use a cache and its corresponding Items() return value at // the same time, as the map is shared. func (c *cache) Items() map[string]*Item { - c.RLock() - defer c.RUnlock() + c.mu.RLock() + defer c.mu.RUnlock() return c.items } // Returns the number of items in the cache. This may include items that have // expired, but have not yet been cleaned up. Equivalent to len(c.Items()). func (c *cache) ItemCount() int { - c.RLock() + c.mu.RLock() n := len(c.items) - c.RUnlock() + c.mu.RUnlock() return n } // Delete all items from the cache. func (c *cache) Flush() { - c.Lock() + c.mu.Lock() c.items = map[string]*Item{} - c.Unlock() + c.mu.Unlock() } type janitor struct { diff --git a/cache_test.go b/cache_test.go index d5b2a60..8b308c8 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1549,10 +1549,10 @@ func BenchmarkCacheSetDeleteSingleLock(b *testing.B) { tc := New(DefaultExpiration, 0) b.StartTimer() for i := 0; i < b.N; i++ { - tc.Lock() + tc.mu.Lock() tc.set("foo", "bar", DefaultExpiration) tc.delete("foo") - tc.Unlock() + tc.mu.Unlock() } } From 3f2c810ea19ac967a5435d943ce437b446a84b4c Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Fri, 27 Nov 2015 22:00:08 -0500 Subject: [PATCH 03/40] Add OnEvicted() --- cache.go | 38 +++++++++++++++++++++++++++++++++++--- cache_test.go | 18 ++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/cache.go b/cache.go index ecddbbb..7fec2b6 100644 --- a/cache.go +++ b/cache.go @@ -41,6 +41,7 @@ type cache struct { defaultExpiration time.Duration items map[string]*Item mu sync.RWMutex + onEvicted func(string, interface{}) janitor *janitor } @@ -810,23 +811,54 @@ func (c *cache) DecrementFloat64(k string, n float64) (float64, error) { // Delete an item from the cache. Does nothing if the key is not in the cache. func (c *cache) Delete(k string) { c.mu.Lock() - c.delete(k) + v, evicted := c.delete(k) c.mu.Unlock() + if evicted { + c.onEvicted(k, v) + } } -func (c *cache) delete(k string) { +func (c *cache) delete(k string) (interface{}, bool) { + if c.onEvicted != nil { + if v, found := c.items[k]; found { + delete(c.items, k) + return v.Object, true + } + } delete(c.items, k) + return nil, false +} + +type keyAndValue struct { + key string + value interface{} } // Delete all expired items from the cache. func (c *cache) DeleteExpired() { + var evictedItems []keyAndValue c.mu.Lock() for k, v := range c.items { if v.Expired() { - c.delete(k) + ov, evicted := c.delete(k) + if evicted { + evictedItems = append(evictedItems, keyAndValue{k, ov}) + } } } c.mu.Unlock() + for _, v := range evictedItems { + c.onEvicted(v.key, v.value) + } +} + +// Sets an (optional) function that is called with the key and value when an +// item is evicted from the cache. (Including when it is deleted manually.) +// Set to nil to disable. +func (c *cache) OnEvicted(f func(string, interface{})) { + c.mu.Lock() + defer c.mu.Unlock() + c.onEvicted = f } // Write the cache's items (using Gob) to an io.Writer. diff --git a/cache_test.go b/cache_test.go index 8b308c8..c357cb0 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1224,6 +1224,24 @@ func TestDecrementUnderflowUint(t *testing.T) { } } +func TestOnEvicted(t *testing.T) { + tc := New(DefaultExpiration, 0) + tc.Set("foo", 3, DefaultExpiration) + if tc.onEvicted != nil { + t.Fatal("tc.onEvicted is not nil") + } + works := false + tc.OnEvicted(func(k string, v interface{}) { + if k == "foo" && v.(int) == 3 { + works = true + } + }) + tc.Delete("foo") + if !works { + t.Error("works bool not true") + } +} + func TestCacheSerialization(t *testing.T) { tc := New(DefaultExpiration, 0) testFillAndSerialize(t, tc) From e9441b12e0692f1420509316d661dc731be11a28 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Sat, 28 Nov 2015 12:22:52 -0500 Subject: [PATCH 04/40] Add mutex-using test condition to TestOnEvicted --- cache_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cache_test.go b/cache_test.go index c357cb0..13f17ae 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1235,11 +1235,16 @@ func TestOnEvicted(t *testing.T) { if k == "foo" && v.(int) == 3 { works = true } + tc.Set("bar", 4, DefaultExpiration) }) tc.Delete("foo") + x, _ := tc.Get("bar") if !works { t.Error("works bool not true") } + if x.(int) != 4 { + t.Error("bar was not 4") + } } func TestCacheSerialization(t *testing.T) { From ac0fcef49b2720e0bdbbf39c0c923c1299479247 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Sat, 28 Nov 2015 12:27:08 -0500 Subject: [PATCH 05/40] Clarify that the OnEvicted function isn't called when an item is overwritten --- cache.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cache.go b/cache.go index 7fec2b6..bbc936a 100644 --- a/cache.go +++ b/cache.go @@ -853,8 +853,8 @@ func (c *cache) DeleteExpired() { } // Sets an (optional) function that is called with the key and value when an -// item is evicted from the cache. (Including when it is deleted manually.) -// Set to nil to disable. +// item is evicted from the cache. (Including when it is deleted manually, but +// not when it is overwritten.) Set to nil to disable. func (c *cache) OnEvicted(f func(string, interface{})) { c.mu.Lock() defer c.mu.Unlock() From 0ba3e0049c4b6452c81558d65854c8e1b259be95 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Sat, 28 Nov 2015 14:21:44 -0500 Subject: [PATCH 06/40] Update copyright years --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 2061d72..159e1e7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2014 Patrick Mylund Nielsen and the go-cache contributors +Copyright (c) 2012-2015 Patrick Mylund Nielsen and the go-cache contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 3d4d09ca0bf8df26cd38a90bdc7968177f976464 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Sat, 28 Nov 2015 14:35:38 -0500 Subject: [PATCH 07/40] Add a benchmark for DeleteExpired() --- cache_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cache_test.go b/cache_test.go index 13f17ae..cc6b511 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1591,3 +1591,19 @@ func BenchmarkRWMutexMapSetDeleteSingleLock(b *testing.B) { mu.Unlock() } } + +func BenchmarkDeleteExpired(b *testing.B) { + b.StopTimer() + tc := New(1, 0) + tc.mu.Lock() + for i := 0; i < b.N; i++ { + tc.set(strconv.Itoa(i), "bar", DefaultExpiration) + } + tc.mu.Unlock() + time.Sleep(100) + if _, found := tc.Get("0"); found { + b.Fatal("0 found") + } + b.StartTimer() + tc.DeleteExpired() +} From 901b2413eec5582e8dfaff57e290ae210be96d6b Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Sat, 28 Nov 2015 14:47:46 -0500 Subject: [PATCH 08/40] Improve cache locality by removing Item-related pointers --- cache.go | 63 ++++++++++++++++++++++++++++++++++++++------------- cache_test.go | 10 ++++---- sharded.go | 6 ++--- 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/cache.go b/cache.go index bbc936a..61040b8 100644 --- a/cache.go +++ b/cache.go @@ -10,14 +10,16 @@ import ( "time" ) +var emptyTime = time.Time{} + type Item struct { Object interface{} - Expiration *time.Time + Expiration time.Time } // Returns true if the item has expired. -func (item *Item) Expired() bool { - if item.Expiration == nil { +func (item Item) Expired() bool { + if item.Expiration == emptyTime { return false } return item.Expiration.Before(time.Now()) @@ -39,7 +41,7 @@ type Cache struct { type cache struct { defaultExpiration time.Duration - items map[string]*Item + items map[string]Item mu sync.RWMutex onEvicted func(string, interface{}) janitor *janitor @@ -57,15 +59,14 @@ func (c *cache) Set(k string, x interface{}, d time.Duration) { } func (c *cache) set(k string, x interface{}, d time.Duration) { - var e *time.Time + e := emptyTime if d == DefaultExpiration { d = c.defaultExpiration } if d > 0 { - t := time.Now().Add(d) - e = &t + e = time.Now().Add(d) } - c.items[k] = &Item{ + c.items[k] = Item{ Object: x, Expiration: e, } @@ -159,6 +160,7 @@ func (c *cache) Increment(k string, n int64) error { c.mu.Unlock() return fmt.Errorf("The value for %s is not an integer", k) } + c.items[k] = v c.mu.Unlock() return nil } @@ -184,6 +186,7 @@ func (c *cache) IncrementFloat(k string, n float64) error { c.mu.Unlock() return fmt.Errorf("The value for %s does not have type float32 or float64", k) } + c.items[k] = v c.mu.Unlock() return nil } @@ -205,6 +208,7 @@ func (c *cache) IncrementInt(k string, n int) (int, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -226,6 +230,7 @@ func (c *cache) IncrementInt8(k string, n int8) (int8, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -247,6 +252,7 @@ func (c *cache) IncrementInt16(k string, n int16) (int16, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -268,6 +274,7 @@ func (c *cache) IncrementInt32(k string, n int32) (int32, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -289,6 +296,7 @@ func (c *cache) IncrementInt64(k string, n int64) (int64, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -310,6 +318,7 @@ func (c *cache) IncrementUint(k string, n uint) (uint, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -331,6 +340,7 @@ func (c *cache) IncrementUintptr(k string, n uintptr) (uintptr, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -352,6 +362,7 @@ func (c *cache) IncrementUint8(k string, n uint8) (uint8, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -373,6 +384,7 @@ func (c *cache) IncrementUint16(k string, n uint16) (uint16, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -394,6 +406,7 @@ func (c *cache) IncrementUint32(k string, n uint32) (uint32, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -415,6 +428,7 @@ func (c *cache) IncrementUint64(k string, n uint64) (uint64, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -436,6 +450,7 @@ func (c *cache) IncrementFloat32(k string, n float32) (float32, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -457,6 +472,7 @@ func (c *cache) IncrementFloat64(k string, n float64) (float64, error) { } nv := rv + n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -506,6 +522,7 @@ func (c *cache) Decrement(k string, n int64) error { c.mu.Unlock() return fmt.Errorf("The value for %s is not an integer", k) } + c.items[k] = v c.mu.Unlock() return nil } @@ -531,6 +548,7 @@ func (c *cache) DecrementFloat(k string, n float64) error { c.mu.Unlock() return fmt.Errorf("The value for %s does not have type float32 or float64", k) } + c.items[k] = v c.mu.Unlock() return nil } @@ -552,6 +570,7 @@ func (c *cache) DecrementInt(k string, n int) (int, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -573,6 +592,7 @@ func (c *cache) DecrementInt8(k string, n int8) (int8, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -594,6 +614,7 @@ func (c *cache) DecrementInt16(k string, n int16) (int16, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -615,6 +636,7 @@ func (c *cache) DecrementInt32(k string, n int32) (int32, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -636,6 +658,7 @@ func (c *cache) DecrementInt64(k string, n int64) (int64, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -657,6 +680,7 @@ func (c *cache) DecrementUint(k string, n uint) (uint, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -678,6 +702,7 @@ func (c *cache) DecrementUintptr(k string, n uintptr) (uintptr, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -699,6 +724,7 @@ func (c *cache) DecrementUint8(k string, n uint8) (uint8, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -720,6 +746,7 @@ func (c *cache) DecrementUint16(k string, n uint16) (uint16, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -741,6 +768,7 @@ func (c *cache) DecrementUint32(k string, n uint32) (uint32, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -762,6 +790,7 @@ func (c *cache) DecrementUint64(k string, n uint64) (uint64, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -783,6 +812,7 @@ func (c *cache) DecrementFloat32(k string, n float32) (float32, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -804,6 +834,7 @@ func (c *cache) DecrementFloat64(k string, n float64) (float64, error) { } nv := rv - n v.Object = nv + c.items[k] = v c.mu.Unlock() return nv, nil } @@ -906,7 +937,7 @@ func (c *cache) SaveFile(fname string) error { // documentation for NewFrom().) func (c *cache) Load(r io.Reader) error { dec := gob.NewDecoder(r) - items := map[string]*Item{} + items := map[string]Item{} err := dec.Decode(&items) if err == nil { c.mu.Lock() @@ -944,7 +975,7 @@ func (c *cache) LoadFile(fname string) error { // fields of the items should be checked. Note that explicit synchronization // is needed to use a cache and its corresponding Items() return value at // the same time, as the map is shared. -func (c *cache) Items() map[string]*Item { +func (c *cache) Items() map[string]Item { c.mu.RLock() defer c.mu.RUnlock() return c.items @@ -962,7 +993,7 @@ func (c *cache) ItemCount() int { // Delete all items from the cache. func (c *cache) Flush() { c.mu.Lock() - c.items = map[string]*Item{} + c.items = map[string]Item{} c.mu.Unlock() } @@ -997,7 +1028,7 @@ func runJanitor(c *cache, ci time.Duration) { go j.Run(c) } -func newCache(de time.Duration, m map[string]*Item) *cache { +func newCache(de time.Duration, m map[string]Item) *cache { if de == 0 { de = -1 } @@ -1008,7 +1039,7 @@ func newCache(de time.Duration, m map[string]*Item) *cache { return c } -func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]*Item) *Cache { +func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache { c := newCache(de, m) // This trick ensures that the janitor goroutine (which--granted it // was enabled--is running DeleteExpired on c forever) does not keep @@ -1029,7 +1060,7 @@ func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]*Item) // manually. If the cleanup interval is less than one, expired items are not // deleted from the cache before calling c.DeleteExpired(). func New(defaultExpiration, cleanupInterval time.Duration) *Cache { - items := make(map[string]*Item) + items := make(map[string]Item) return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) } @@ -1042,7 +1073,7 @@ func New(defaultExpiration, cleanupInterval time.Duration) *Cache { // NewFrom() also accepts an items map which will serve as the underlying map // for the cache. This is useful for starting from a deserialized cache // (serialized using e.g. gob.Encode() on c.Items()), or passing in e.g. -// make(map[string]*Item, 500) to improve startup performance when the cache +// make(map[string]Item, 500) to improve startup performance when the cache // is expected to reach a certain minimum size. // // Only the cache's methods synchronize access to this map, so it is not @@ -1054,6 +1085,6 @@ func New(defaultExpiration, cleanupInterval time.Duration) *Cache { // gob.Register() the individual types stored in the cache before encoding a // map retrieved with c.Items(), and to register those same types before // decoding a blob containing an items map. -func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]*Item) *Cache { +func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache { return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) } diff --git a/cache_test.go b/cache_test.go index cc6b511..0cccf5f 100644 --- a/cache_test.go +++ b/cache_test.go @@ -107,14 +107,14 @@ func TestCacheTimes(t *testing.T) { } func TestNewFrom(t *testing.T) { - m := map[string]*Item{ - "a": &Item{ + m := map[string]Item{ + "a": Item{ Object: 1, - Expiration: nil, + Expiration: emptyTime, }, - "b": &Item{ + "b": Item{ Object: 2, - Expiration: nil, + Expiration: emptyTime, }, } tc := NewFrom(DefaultExpiration, 0, m) diff --git a/sharded.go b/sharded.go index 7ae92af..bcc0538 100644 --- a/sharded.go +++ b/sharded.go @@ -109,8 +109,8 @@ func (sc *shardedCache) DeleteExpired() { // fields of the items should be checked. Note that explicit synchronization // is needed to use a cache and its corresponding Items() return values at // the same time, as the maps are shared. -func (sc *shardedCache) Items() []map[string]*Item { - res := make([]map[string]*Item, len(sc.cs)) +func (sc *shardedCache) Items() []map[string]Item { + res := make([]map[string]Item, len(sc.cs)) for i, v := range sc.cs { res[i] = v.Items() } @@ -171,7 +171,7 @@ func newShardedCache(n int, de time.Duration) *shardedCache { for i := 0; i < n; i++ { c := &cache{ defaultExpiration: de, - items: map[string]*Item{}, + items: map[string]Item{}, } sc.cs[i] = c } From cf4e16575494047186a7f4279a1e423fc2f638df Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Sat, 28 Nov 2015 14:56:23 -0500 Subject: [PATCH 09/40] Add IncrementInt benchmark --- cache_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cache_test.go b/cache_test.go index 0cccf5f..86a5283 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1592,6 +1592,16 @@ func BenchmarkRWMutexMapSetDeleteSingleLock(b *testing.B) { } } +func BenchmarkIncrementInt(b *testing.B) { + b.StopTimer() + tc := New(DefaultExpiration, 0) + tc.Set("foo", 0, DefaultExpiration) + b.StartTimer() + for i := 0; i < b.N; i++ { + tc.IncrementInt("foo", 1) + } +} + func BenchmarkDeleteExpired(b *testing.B) { b.StopTimer() tc := New(1, 0) From 28ab885a1a99d5b142901e78c44d1b85f9d01ef0 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Sat, 28 Nov 2015 15:13:26 -0500 Subject: [PATCH 10/40] Make BenchmarkDeleteExpired more meaningful --- cache_test.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/cache_test.go b/cache_test.go index 86a5283..f998d43 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1604,16 +1604,14 @@ func BenchmarkIncrementInt(b *testing.B) { func BenchmarkDeleteExpired(b *testing.B) { b.StopTimer() - tc := New(1, 0) + tc := New(5 * time.Minute, 0) tc.mu.Lock() - for i := 0; i < b.N; i++ { + for i := 0; i < 100000; i++ { tc.set(strconv.Itoa(i), "bar", DefaultExpiration) } tc.mu.Unlock() - time.Sleep(100) - if _, found := tc.Get("0"); found { - b.Fatal("0 found") - } b.StartTimer() - tc.DeleteExpired() + for i := 0; i < b.N; i++ { + tc.DeleteExpired() + } } From a45ed98559a8a57870f724b765bb47d7126af96b Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 10:45:30 -0500 Subject: [PATCH 11/40] Add benchmarks that use expiring items (time.Now calls) and rename BenchmarkDeleteExpired to BenchmarkDeleteExpiredLoop for clarity --- cache_test.go | 50 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/cache_test.go b/cache_test.go index f998d43..2ff220e 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1425,9 +1425,17 @@ func TestSerializeUnserializable(t *testing.T) { } } -func BenchmarkCacheGet(b *testing.B) { +func BenchmarkCacheGetExpiring(b *testing.B) { + benchmarkCacheGet(b, 5 * time.Minute) +} + +func BenchmarkCacheGetNotExpiring(b *testing.B) { + benchmarkCacheGet(b, NoExpiration) +} + +func benchmarkCacheGet(b *testing.B, exp time.Duration) { b.StopTimer() - tc := New(DefaultExpiration, 0) + tc := New(exp, 0) tc.Set("foo", "bar", DefaultExpiration) b.StartTimer() for i := 0; i < b.N; i++ { @@ -1449,9 +1457,17 @@ func BenchmarkRWMutexMapGet(b *testing.B) { } } -func BenchmarkCacheGetConcurrent(b *testing.B) { +func BenchmarkCacheGetConcurrentExpiring(b *testing.B) { + benchmarkCacheGetConcurrent(b, 5 * time.Minute) +} + +func BenchmarkCacheGetConcurrentNotExpiring(b *testing.B) { + benchmarkCacheGetConcurrent(b, NoExpiration) +} + +func benchmarkCacheGetConcurrent(b *testing.B, exp time.Duration) { b.StopTimer() - tc := New(DefaultExpiration, 0) + tc := New(exp, 0) tc.Set("foo", "bar", DefaultExpiration) wg := new(sync.WaitGroup) workers := runtime.NumCPU() @@ -1493,13 +1509,21 @@ func BenchmarkRWMutexMapGetConcurrent(b *testing.B) { wg.Wait() } -func BenchmarkCacheGetManyConcurrent(b *testing.B) { +func BenchmarkCacheGetManyConcurrentExpiring(b *testing.B) { + benchmarkCacheGetManyConcurrent(b, 5 * time.Minute) +} + +func BenchmarkCacheGetManyConcurrentNotExpiring(b *testing.B) { + benchmarkCacheGetManyConcurrent(b, NoExpiration) +} + +func benchmarkCacheGetManyConcurrent(b *testing.B, exp time.Duration) { // This is the same as BenchmarkCacheGetConcurrent, but its result // can be compared against BenchmarkShardedCacheGetManyConcurrent // in sharded_test.go. b.StopTimer() n := 10000 - tc := New(DefaultExpiration, 0) + tc := New(exp, 0) keys := make([]string, n) for i := 0; i < n; i++ { k := "foo" + strconv.Itoa(n) @@ -1521,9 +1545,17 @@ func BenchmarkCacheGetManyConcurrent(b *testing.B) { wg.Wait() } -func BenchmarkCacheSet(b *testing.B) { +func BenchmarkCacheSetExpiring(b *testing.B) { + benchmarkCacheSet(b, 5 * time.Minute) +} + +func BenchmarkCacheSetNotExpiring(b *testing.B) { + benchmarkCacheSet(b, NoExpiration) +} + +func benchmarkCacheSet(b *testing.B, exp time.Duration) { b.StopTimer() - tc := New(DefaultExpiration, 0) + tc := New(exp, 0) b.StartTimer() for i := 0; i < b.N; i++ { tc.Set("foo", "bar", DefaultExpiration) @@ -1602,7 +1634,7 @@ func BenchmarkIncrementInt(b *testing.B) { } } -func BenchmarkDeleteExpired(b *testing.B) { +func BenchmarkDeleteExpiredLoop(b *testing.B) { b.StopTimer() tc := New(5 * time.Minute, 0) tc.mu.Lock() From 4e0d34ef0010c94c55fbdc0ef08ea751979e7ed9 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 13:39:27 -0500 Subject: [PATCH 12/40] Only get the current time once in the DeleteExpired loop --- cache.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cache.go b/cache.go index 61040b8..2599f1a 100644 --- a/cache.go +++ b/cache.go @@ -17,12 +17,16 @@ type Item struct { Expiration time.Time } -// Returns true if the item has expired. -func (item Item) Expired() bool { +func (item Item) expired(now time.Time) bool { if item.Expiration == emptyTime { return false } - return item.Expiration.Before(time.Now()) + return item.Expiration.Before(now) +} + +// Returns true if the item has expired. +func (item Item) Expired() bool { + return item.expired(time.Now()) } const ( @@ -868,9 +872,10 @@ type keyAndValue struct { // Delete all expired items from the cache. func (c *cache) DeleteExpired() { var evictedItems []keyAndValue + now := time.Now() c.mu.Lock() for k, v := range c.items { - if v.Expired() { + if v.expired(now) { ov, evicted := c.delete(k) if evicted { evictedItems = append(evictedItems, keyAndValue{k, ov}) From 31c7be0bed73008bb6b43fc60c23711455d66614 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 13:50:17 -0500 Subject: [PATCH 13/40] 'Inline' Get and Expired --- cache.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cache.go b/cache.go index 2599f1a..a94bbd0 100644 --- a/cache.go +++ b/cache.go @@ -26,7 +26,11 @@ func (item Item) expired(now time.Time) bool { // Returns true if the item has expired. func (item Item) Expired() bool { - return item.expired(time.Now()) + // "Inlining" of expired + if item.Expiration == emptyTime { + return false + } + return item.Expiration.Before(time.Now()) } const ( @@ -108,9 +112,14 @@ func (c *cache) Replace(k string, x interface{}, d time.Duration) error { // whether the key was found. func (c *cache) Get(k string) (interface{}, bool) { c.mu.RLock() - x, found := c.get(k) + // "Inlining" of get + item, found := c.items[k] + if !found || item.Expired() { + c.mu.RUnlock() + return nil, false + } c.mu.RUnlock() - return x, found + return item.Object, found } func (c *cache) get(k string) (interface{}, bool) { From eb4f9f6b2f44f94b24f25173e58ffb8fa5c4f37e Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 13:54:01 -0500 Subject: [PATCH 14/40] Use UnixNano int64s instead of Time --- cache.go | 18 +++++++++--------- cache_test.go | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cache.go b/cache.go index a94bbd0..5839ac1 100644 --- a/cache.go +++ b/cache.go @@ -14,23 +14,23 @@ var emptyTime = time.Time{} type Item struct { Object interface{} - Expiration time.Time + Expiration int64 } -func (item Item) expired(now time.Time) bool { - if item.Expiration == emptyTime { +func (item Item) expired(now int64) bool { + if item.Expiration == 0 { return false } - return item.Expiration.Before(now) + return now > item.Expiration } // Returns true if the item has expired. func (item Item) Expired() bool { // "Inlining" of expired - if item.Expiration == emptyTime { + if item.Expiration == 0 { return false } - return item.Expiration.Before(time.Now()) + return time.Now().UnixNano() > item.Expiration } const ( @@ -67,12 +67,12 @@ func (c *cache) Set(k string, x interface{}, d time.Duration) { } func (c *cache) set(k string, x interface{}, d time.Duration) { - e := emptyTime + var e int64 if d == DefaultExpiration { d = c.defaultExpiration } if d > 0 { - e = time.Now().Add(d) + e = time.Now().Add(d).UnixNano() } c.items[k] = Item{ Object: x, @@ -881,7 +881,7 @@ type keyAndValue struct { // Delete all expired items from the cache. func (c *cache) DeleteExpired() { var evictedItems []keyAndValue - now := time.Now() + now := time.Now().UnixNano() c.mu.Lock() for k, v := range c.items { if v.expired(now) { diff --git a/cache_test.go b/cache_test.go index 2ff220e..f604794 100644 --- a/cache_test.go +++ b/cache_test.go @@ -110,11 +110,11 @@ func TestNewFrom(t *testing.T) { m := map[string]Item{ "a": Item{ Object: 1, - Expiration: emptyTime, + Expiration: 0, }, "b": Item{ Object: 2, - Expiration: emptyTime, + Expiration: 0, }, } tc := NewFrom(DefaultExpiration, 0, m) From 8084bd02b5d65f4bb74b5080254ad08faf15b336 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 14:12:45 -0500 Subject: [PATCH 15/40] Inline expiration checks manually for performance --- cache.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cache.go b/cache.go index 5839ac1..1a6c4e2 100644 --- a/cache.go +++ b/cache.go @@ -112,9 +112,9 @@ func (c *cache) Replace(k string, x interface{}, d time.Duration) error { // whether the key was found. func (c *cache) Get(k string) (interface{}, bool) { c.mu.RLock() - // "Inlining" of get + // "Inlining" of get and expired item, found := c.items[k] - if !found || item.Expired() { + if !found || (item.Expiration > 0 && time.Now().UnixNano() > item.Expiration) { c.mu.RUnlock() return nil, false } @@ -124,7 +124,8 @@ func (c *cache) Get(k string) (interface{}, bool) { func (c *cache) get(k string) (interface{}, bool) { item, found := c.items[k] - if !found || item.Expired() { + // "Inlining" of expired + if !found || (item.Expiration > 0 && time.Now().UnixNano() > item.Expiration) { return nil, false } return item.Object, true @@ -884,7 +885,8 @@ func (c *cache) DeleteExpired() { now := time.Now().UnixNano() c.mu.Lock() for k, v := range c.items { - if v.expired(now) { + // "Inlining" of expired + if v.Expiration > 0 && now > v.Expiration { ov, evicted := c.delete(k) if evicted { evictedItems = append(evictedItems, keyAndValue{k, ov}) From 1924ec3baf9c7cb78662282fbec44fbb7f3d6d86 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 14:14:52 -0500 Subject: [PATCH 16/40] Remove expired() since it's no longer used (because of the inlining) --- cache.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/cache.go b/cache.go index 1a6c4e2..cea90c5 100644 --- a/cache.go +++ b/cache.go @@ -17,16 +17,8 @@ type Item struct { Expiration int64 } -func (item Item) expired(now int64) bool { - if item.Expiration == 0 { - return false - } - return now > item.Expiration -} - // Returns true if the item has expired. func (item Item) Expired() bool { - // "Inlining" of expired if item.Expiration == 0 { return false } From 01842a547caf6a5d39d1db580f08923b840ea7fe Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 14:47:22 -0500 Subject: [PATCH 17/40] Use timevals --- cache.go | 44 +++++++++++++++++++++++++++++++------------- cache_test.go | 5 +++-- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/cache.go b/cache.go index cea90c5..b9bf974 100644 --- a/cache.go +++ b/cache.go @@ -7,22 +7,23 @@ import ( "os" "runtime" "sync" + "syscall" "time" ) -var emptyTime = time.Time{} - type Item struct { Object interface{} - Expiration int64 + Expiration syscall.Timeval } // Returns true if the item has expired. func (item Item) Expired() bool { - if item.Expiration == 0 { + if item.Expiration.Sec == 0 { return false } - return time.Now().UnixNano() > item.Expiration + var tv syscall.Timeval + syscall.Gettimeofday(&tv) + return tv.Sec > item.Expiration.Sec || (tv.Sec == item.Expiration.Sec && tv.Usec > item.Expiration.Usec) } const ( @@ -59,12 +60,12 @@ func (c *cache) Set(k string, x interface{}, d time.Duration) { } func (c *cache) set(k string, x interface{}, d time.Duration) { - var e int64 + var e syscall.Timeval if d == DefaultExpiration { d = c.defaultExpiration } if d > 0 { - e = time.Now().Add(d).UnixNano() + e = syscall.NsecToTimeval(time.Now().Add(d).UnixNano()) } c.items[k] = Item{ Object: x, @@ -106,20 +107,36 @@ func (c *cache) Get(k string) (interface{}, bool) { c.mu.RLock() // "Inlining" of get and expired item, found := c.items[k] - if !found || (item.Expiration > 0 && time.Now().UnixNano() > item.Expiration) { + if !found { c.mu.RUnlock() return nil, false } + if item.Expiration.Sec > 0 { + var tv syscall.Timeval + syscall.Gettimeofday(&tv) + if tv.Sec > item.Expiration.Sec || (tv.Sec == item.Expiration.Sec && tv.Usec > item.Expiration.Usec) { + c.mu.RUnlock() + return nil, false + } + } c.mu.RUnlock() - return item.Object, found + return item.Object, true } func (c *cache) get(k string) (interface{}, bool) { item, found := c.items[k] - // "Inlining" of expired - if !found || (item.Expiration > 0 && time.Now().UnixNano() > item.Expiration) { + if !found { return nil, false } + // "Inlining" of Expired + if item.Expiration.Sec > 0 { + var tv syscall.Timeval + syscall.Gettimeofday(&tv) + if tv.Sec > item.Expiration.Sec || (tv.Sec == item.Expiration.Sec && tv.Usec > item.Expiration.Usec) { + c.mu.RUnlock() + return nil, false + } + } return item.Object, true } @@ -874,11 +891,12 @@ type keyAndValue struct { // Delete all expired items from the cache. func (c *cache) DeleteExpired() { var evictedItems []keyAndValue - now := time.Now().UnixNano() + var now syscall.Timeval + syscall.Gettimeofday(&now) c.mu.Lock() for k, v := range c.items { // "Inlining" of expired - if v.Expiration > 0 && now > v.Expiration { + if v.Expiration.Sec > 0 && (now.Sec > v.Expiration.Sec || (now.Sec == v.Expiration.Sec && now.Usec > v.Expiration.Usec)) { ov, evicted := c.delete(k) if evicted { evictedItems = append(evictedItems, keyAndValue{k, ov}) diff --git a/cache_test.go b/cache_test.go index f604794..62b2854 100644 --- a/cache_test.go +++ b/cache_test.go @@ -6,6 +6,7 @@ import ( "runtime" "strconv" "sync" + "syscall" "testing" "time" ) @@ -110,11 +111,11 @@ func TestNewFrom(t *testing.T) { m := map[string]Item{ "a": Item{ Object: 1, - Expiration: 0, + Expiration: syscall.Timeval{}, }, "b": Item{ Object: 2, - Expiration: 0, + Expiration: syscall.Timeval{}, }, } tc := NewFrom(DefaultExpiration, 0, m) From 2f60853f802f7846ab59906612e15fcde066e217 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 14:49:18 -0500 Subject: [PATCH 18/40] No need for emptyTime anymore --- cache.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cache.go b/cache.go index cea90c5..e934361 100644 --- a/cache.go +++ b/cache.go @@ -10,8 +10,6 @@ import ( "time" ) -var emptyTime = time.Time{} - type Item struct { Object interface{} Expiration int64 From 2f0c74ebb8a6bd87a32958cdfff1124e527d4718 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 15:02:02 -0500 Subject: [PATCH 19/40] Use intermediary timevals --- cache.go | 31 +++++++++++++++++-------------- cache_test.go | 5 ++--- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/cache.go b/cache.go index b9bf974..a573611 100644 --- a/cache.go +++ b/cache.go @@ -13,17 +13,17 @@ import ( type Item struct { Object interface{} - Expiration syscall.Timeval + Expiration int64 } // Returns true if the item has expired. func (item Item) Expired() bool { - if item.Expiration.Sec == 0 { + if item.Expiration == 0 { return false } var tv syscall.Timeval syscall.Gettimeofday(&tv) - return tv.Sec > item.Expiration.Sec || (tv.Sec == item.Expiration.Sec && tv.Usec > item.Expiration.Usec) + return tv.Nano() > item.Expiration } const ( @@ -60,12 +60,12 @@ func (c *cache) Set(k string, x interface{}, d time.Duration) { } func (c *cache) set(k string, x interface{}, d time.Duration) { - var e syscall.Timeval + var e int64 if d == DefaultExpiration { d = c.defaultExpiration } if d > 0 { - e = syscall.NsecToTimeval(time.Now().Add(d).UnixNano()) + e = time.Now().Add(d).UnixNano() } c.items[k] = Item{ Object: x, @@ -105,16 +105,16 @@ func (c *cache) Replace(k string, x interface{}, d time.Duration) error { // whether the key was found. func (c *cache) Get(k string) (interface{}, bool) { c.mu.RLock() - // "Inlining" of get and expired + // "Inlining" of get and Expired item, found := c.items[k] if !found { c.mu.RUnlock() return nil, false } - if item.Expiration.Sec > 0 { + if item.Expiration > 0 { var tv syscall.Timeval syscall.Gettimeofday(&tv) - if tv.Sec > item.Expiration.Sec || (tv.Sec == item.Expiration.Sec && tv.Usec > item.Expiration.Usec) { + if tv.Nano() > item.Expiration { c.mu.RUnlock() return nil, false } @@ -129,10 +129,10 @@ func (c *cache) get(k string) (interface{}, bool) { return nil, false } // "Inlining" of Expired - if item.Expiration.Sec > 0 { + if item.Expiration > 0 { var tv syscall.Timeval syscall.Gettimeofday(&tv) - if tv.Sec > item.Expiration.Sec || (tv.Sec == item.Expiration.Sec && tv.Usec > item.Expiration.Usec) { + if tv.Nano() > item.Expiration { c.mu.RUnlock() return nil, false } @@ -890,13 +890,16 @@ type keyAndValue struct { // Delete all expired items from the cache. func (c *cache) DeleteExpired() { - var evictedItems []keyAndValue - var now syscall.Timeval - syscall.Gettimeofday(&now) + var ( + evictedItems []keyAndValue + tv syscall.Timeval + ) + syscall.Gettimeofday(&tv) + now := tv.Nano() c.mu.Lock() for k, v := range c.items { // "Inlining" of expired - if v.Expiration.Sec > 0 && (now.Sec > v.Expiration.Sec || (now.Sec == v.Expiration.Sec && now.Usec > v.Expiration.Usec)) { + if v.Expiration > 0 && now > v.Expiration { ov, evicted := c.delete(k) if evicted { evictedItems = append(evictedItems, keyAndValue{k, ov}) diff --git a/cache_test.go b/cache_test.go index 62b2854..f604794 100644 --- a/cache_test.go +++ b/cache_test.go @@ -6,7 +6,6 @@ import ( "runtime" "strconv" "sync" - "syscall" "testing" "time" ) @@ -111,11 +110,11 @@ func TestNewFrom(t *testing.T) { m := map[string]Item{ "a": Item{ Object: 1, - Expiration: syscall.Timeval{}, + Expiration: 0, }, "b": Item{ Object: 2, - Expiration: syscall.Timeval{}, + Expiration: 0, }, } tc := NewFrom(DefaultExpiration, 0, m) From afadf13f9f1c09f0c5e1b080662aa93cc12c5141 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 15:12:19 -0500 Subject: [PATCH 20/40] Back to UnixNano(), syscall dependency isn't worth a few nanoseconds better performance --- cache.go | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/cache.go b/cache.go index a573611..4f44ba2 100644 --- a/cache.go +++ b/cache.go @@ -7,7 +7,6 @@ import ( "os" "runtime" "sync" - "syscall" "time" ) @@ -21,9 +20,7 @@ func (item Item) Expired() bool { if item.Expiration == 0 { return false } - var tv syscall.Timeval - syscall.Gettimeofday(&tv) - return tv.Nano() > item.Expiration + return time.Now().UnixNano() > item.Expiration } const ( @@ -112,9 +109,7 @@ func (c *cache) Get(k string) (interface{}, bool) { return nil, false } if item.Expiration > 0 { - var tv syscall.Timeval - syscall.Gettimeofday(&tv) - if tv.Nano() > item.Expiration { + if time.Now().UnixNano() > item.Expiration { c.mu.RUnlock() return nil, false } @@ -130,9 +125,7 @@ func (c *cache) get(k string) (interface{}, bool) { } // "Inlining" of Expired if item.Expiration > 0 { - var tv syscall.Timeval - syscall.Gettimeofday(&tv) - if tv.Nano() > item.Expiration { + if time.Now().UnixNano() > item.Expiration { c.mu.RUnlock() return nil, false } @@ -890,12 +883,8 @@ type keyAndValue struct { // Delete all expired items from the cache. func (c *cache) DeleteExpired() { - var ( - evictedItems []keyAndValue - tv syscall.Timeval - ) - syscall.Gettimeofday(&tv) - now := tv.Nano() + var evictedItems []keyAndValue + now := time.Now().UnixNano() c.mu.Lock() for k, v := range c.items { // "Inlining" of expired From 9fc6f9c73f5599519846febf41781f43e2b9efca Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 16:04:49 -0500 Subject: [PATCH 21/40] Add expiring/notexpiring sharded cache benchmarks --- sharded_test.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/sharded_test.go b/sharded_test.go index 6ef5eb3..51088d9 100644 --- a/sharded_test.go +++ b/sharded_test.go @@ -4,6 +4,7 @@ import ( "strconv" "sync" "testing" + "time" ) // func TestDjb33(t *testing.T) { @@ -32,9 +33,17 @@ func TestShardedCache(t *testing.T) { } } -func BenchmarkShardedCacheGet(b *testing.B) { +func BenchmarkShardedCacheGetExpiring(b *testing.B) { + benchmarkShardedCacheGet(b, 5 * time.Minute) +} + +func BenchmarkShardedCacheGetNotExpiring(b *testing.B) { + benchmarkShardedCacheGet(b, NoExpiration) +} + +func benchmarkShardedCacheGet(b *testing.B, exp time.Duration) { b.StopTimer() - tc := unexportedNewSharded(DefaultExpiration, 0, 10) + tc := unexportedNewSharded(exp, 0, 10) tc.Set("foobarba", "zquux", DefaultExpiration) b.StartTimer() for i := 0; i < b.N; i++ { @@ -42,10 +51,18 @@ func BenchmarkShardedCacheGet(b *testing.B) { } } -func BenchmarkShardedCacheGetManyConcurrent(b *testing.B) { +func BenchmarkShardedCacheGetManyConcurrentExpiring(b *testing.B) { + benchmarkShardedCacheGetManyConcurrent(b, 5 * time.Minute) +} + +func BenchmarkShardedCacheGetManyConcurrentNotExpiring(b *testing.B) { + benchmarkShardedCacheGetManyConcurrent(b, NoExpiration) +} + +func benchmarkShardedCacheGetManyConcurrent(b *testing.B, exp time.Duration) { b.StopTimer() n := 10000 - tsc := unexportedNewSharded(DefaultExpiration, 0, 20) + tsc := unexportedNewSharded(exp, 0, 20) keys := make([]string, n) for i := 0; i < n; i++ { k := "foo" + strconv.Itoa(n) From 7c1e7f5829f0aef6c8d26dcdc76871584e9ab62f Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 16:04:57 -0500 Subject: [PATCH 22/40] go fmt --- cache_test.go | 10 +++++----- sharded_test.go | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cache_test.go b/cache_test.go index f604794..5a3b396 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1426,7 +1426,7 @@ func TestSerializeUnserializable(t *testing.T) { } func BenchmarkCacheGetExpiring(b *testing.B) { - benchmarkCacheGet(b, 5 * time.Minute) + benchmarkCacheGet(b, 5*time.Minute) } func BenchmarkCacheGetNotExpiring(b *testing.B) { @@ -1458,7 +1458,7 @@ func BenchmarkRWMutexMapGet(b *testing.B) { } func BenchmarkCacheGetConcurrentExpiring(b *testing.B) { - benchmarkCacheGetConcurrent(b, 5 * time.Minute) + benchmarkCacheGetConcurrent(b, 5*time.Minute) } func BenchmarkCacheGetConcurrentNotExpiring(b *testing.B) { @@ -1510,7 +1510,7 @@ func BenchmarkRWMutexMapGetConcurrent(b *testing.B) { } func BenchmarkCacheGetManyConcurrentExpiring(b *testing.B) { - benchmarkCacheGetManyConcurrent(b, 5 * time.Minute) + benchmarkCacheGetManyConcurrent(b, 5*time.Minute) } func BenchmarkCacheGetManyConcurrentNotExpiring(b *testing.B) { @@ -1546,7 +1546,7 @@ func benchmarkCacheGetManyConcurrent(b *testing.B, exp time.Duration) { } func BenchmarkCacheSetExpiring(b *testing.B) { - benchmarkCacheSet(b, 5 * time.Minute) + benchmarkCacheSet(b, 5*time.Minute) } func BenchmarkCacheSetNotExpiring(b *testing.B) { @@ -1636,7 +1636,7 @@ func BenchmarkIncrementInt(b *testing.B) { func BenchmarkDeleteExpiredLoop(b *testing.B) { b.StopTimer() - tc := New(5 * time.Minute, 0) + tc := New(5*time.Minute, 0) tc.mu.Lock() for i := 0; i < 100000; i++ { tc.set(strconv.Itoa(i), "bar", DefaultExpiration) diff --git a/sharded_test.go b/sharded_test.go index 51088d9..aef8597 100644 --- a/sharded_test.go +++ b/sharded_test.go @@ -34,7 +34,7 @@ func TestShardedCache(t *testing.T) { } func BenchmarkShardedCacheGetExpiring(b *testing.B) { - benchmarkShardedCacheGet(b, 5 * time.Minute) + benchmarkShardedCacheGet(b, 5*time.Minute) } func BenchmarkShardedCacheGetNotExpiring(b *testing.B) { @@ -52,7 +52,7 @@ func benchmarkShardedCacheGet(b *testing.B, exp time.Duration) { } func BenchmarkShardedCacheGetManyConcurrentExpiring(b *testing.B) { - benchmarkShardedCacheGetManyConcurrent(b, 5 * time.Minute) + benchmarkShardedCacheGetManyConcurrent(b, 5*time.Minute) } func BenchmarkShardedCacheGetManyConcurrentNotExpiring(b *testing.B) { From 76f1250a65318992f63b2fc75baafbe62e63a7a6 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 30 Nov 2015 16:18:49 -0500 Subject: [PATCH 23/40] Make OnEvicted() a little faster --- cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cache.go b/cache.go index 4f44ba2..d58c603 100644 --- a/cache.go +++ b/cache.go @@ -906,8 +906,8 @@ func (c *cache) DeleteExpired() { // not when it is overwritten.) Set to nil to disable. func (c *cache) OnEvicted(f func(string, interface{})) { c.mu.Lock() - defer c.mu.Unlock() c.onEvicted = f + c.mu.Unlock() } // Write the cache's items (using Gob) to an io.Writer. From d461c5d2dd1e09fecf8be90a136289fadebbeef1 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Tue, 1 Dec 2015 11:08:43 -0500 Subject: [PATCH 24/40] 'Inline' set in Set, and do time checks before the lock --- cache.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cache.go b/cache.go index d58c603..ff2c1e2 100644 --- a/cache.go +++ b/cache.go @@ -49,8 +49,19 @@ type cache struct { // (DefaultExpiration), the cache's default expiration time is used. If it is -1 // (NoExpiration), the item never expires. func (c *cache) Set(k string, x interface{}, d time.Duration) { + // "Inlining" of set + var e int64 + if d == DefaultExpiration { + d = c.defaultExpiration + } + if d > 0 { + e = time.Now().Add(d).UnixNano() + } c.mu.Lock() - c.set(k, x, d) + c.items[k] = Item{ + Object: x, + Expiration: e, + } // TODO: Calls to mu.Unlock are currently not deferred because defer // adds ~200 ns (as of go1.) c.mu.Unlock() From 66bf7b7a4586ad87161abca30cd43c2ecbc9b1cb Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Tue, 1 Dec 2015 11:18:46 -0500 Subject: [PATCH 25/40] Update README to point to new repository URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92e8a74..9cc4cc4 100644 --- a/README.md +++ b/README.md @@ -103,4 +103,4 @@ one) to recover from downtime quickly. (See the docs for `NewFrom()` for caveats ### Reference -`godoc` or [http://godoc.org/github.com/pmylund/go-cache](http://godoc.org/github.com/pmylund/go-cache) +`godoc` or [http://godoc.org/github.com/patrickmn/go-cache](http://godoc.org/github.com/patrickmn/go-cache) From faf83836bdb705862d053d3ddd08bc05183cb2dd Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Wed, 2 Dec 2015 14:32:12 -0500 Subject: [PATCH 26/40] Change GitHub repository URLs in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9cc4cc4..91f0087 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,13 @@ one) to recover from downtime quickly. (See the docs for `NewFrom()` for caveats ### Installation -`go get github.com/pmylund/go-cache` +`go get github.com/patrickmn/go-cache` ### Usage import ( "fmt" - "github.com/pmylund/go-cache" + "github.com/patrickmn/go-cache" "time" ) From 8c41258ef3755975cf45e9a1dcde8586a6656cd7 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Thu, 3 Dec 2015 09:40:14 -0500 Subject: [PATCH 27/40] Add BenchmarkRWMutexInterfaceMapGet --- cache_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cache_test.go b/cache_test.go index 5a3b396..e30adc7 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1457,6 +1457,21 @@ func BenchmarkRWMutexMapGet(b *testing.B) { } } +func BenchmarkRWMutexInterfaceMapGet(b *testing.B) { + b.StopTimer() + s := struct{name string}{name: "foo"} + m := map[interface{}]string{ + s: "bar", + } + mu := sync.RWMutex{} + b.StartTimer() + for i := 0; i < b.N; i++ { + mu.RLock() + _, _ = m[s] + mu.RUnlock() + } +} + func BenchmarkCacheGetConcurrentExpiring(b *testing.B) { benchmarkCacheGetConcurrent(b, 5*time.Minute) } From 721cc9438c91df8997d39584a6740e2aba3100a7 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Thu, 3 Dec 2015 09:55:58 -0500 Subject: [PATCH 28/40] Add BenchmarkRWMutexInterfaceMapGetString --- cache_test.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/cache_test.go b/cache_test.go index e30adc7..6e81693 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1457,7 +1457,7 @@ func BenchmarkRWMutexMapGet(b *testing.B) { } } -func BenchmarkRWMutexInterfaceMapGet(b *testing.B) { +func BenchmarkRWMutexInterfaceMapGetStruct(b *testing.B) { b.StopTimer() s := struct{name string}{name: "foo"} m := map[interface{}]string{ @@ -1472,6 +1472,20 @@ func BenchmarkRWMutexInterfaceMapGet(b *testing.B) { } } +func BenchmarkRWMutexInterfaceMapGetString(b *testing.B) { + b.StopTimer() + m := map[interface{}]string{ + "foo": "bar", + } + mu := sync.RWMutex{} + b.StartTimer() + for i := 0; i < b.N; i++ { + mu.RLock() + _, _ = m["foo"] + mu.RUnlock() + } +} + func BenchmarkCacheGetConcurrentExpiring(b *testing.B) { benchmarkCacheGetConcurrent(b, 5*time.Minute) } From 5849ccb30876239377aca783d5d9546f3876020e Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Fri, 8 Jan 2016 15:02:42 -0500 Subject: [PATCH 29/40] remove mu.RUnlock call from get --- cache.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cache.go b/cache.go index ff2c1e2..3562543 100644 --- a/cache.go +++ b/cache.go @@ -137,7 +137,6 @@ func (c *cache) get(k string) (interface{}, bool) { // "Inlining" of Expired if item.Expiration > 0 { if time.Now().UnixNano() > item.Expiration { - c.mu.RUnlock() return nil, false } } From da6326cd6988896b4c13f071cdd6d969223290b9 Mon Sep 17 00:00:00 2001 From: Darren McCleary Date: Wed, 27 Jan 2016 11:56:21 -0500 Subject: [PATCH 30/40] added go syntax highlighting to README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 91f0087..168ff7b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ one) to recover from downtime quickly. (See the docs for `NewFrom()` for caveats ### Usage +```go import ( "fmt" "github.com/patrickmn/go-cache" @@ -99,7 +100,7 @@ one) to recover from downtime quickly. (See the docs for `NewFrom()` for caveats // 2 } - +``` ### Reference From a2d8b56f0c21c5562c23717b6137ed6254ddc6d4 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Fri, 25 Nov 2016 13:56:11 -0500 Subject: [PATCH 31/40] Make Items() return a copy rather than an unsynchronized reference to the underlying items map --- cache.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/cache.go b/cache.go index 3562543..4eab3d0 100644 --- a/cache.go +++ b/cache.go @@ -998,19 +998,25 @@ func (c *cache) LoadFile(fname string) error { return fp.Close() } -// Returns the items in the cache. This may include items that have expired, -// but have not yet been cleaned up. If this is significant, the Expiration -// fields of the items should be checked. Note that explicit synchronization -// is needed to use a cache and its corresponding Items() return value at -// the same time, as the map is shared. +// Copies all unexpired items in the cache into a new map and returns it. func (c *cache) Items() map[string]Item { c.mu.RLock() defer c.mu.RUnlock() - return c.items + m := make(map[string]Item, len(c.items)) + now := time.Now().UnixNano() + for k, v := range c.items { + if v.Expiration > 0 { + if now > v.Expiration { + continue + } + } + m[k] = v + } + return m } // Returns the number of items in the cache. This may include items that have -// expired, but have not yet been cleaned up. Equivalent to len(c.Items()). +// expired, but have not yet been cleaned up. func (c *cache) ItemCount() int { c.mu.RLock() n := len(c.items) From 9e6d9117e747d8a136ae985ebcc46fbe4c293198 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Fri, 25 Nov 2016 13:57:39 -0500 Subject: [PATCH 32/40] Add 'inlining of expired' note to Items() --- cache.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cache.go b/cache.go index 4eab3d0..8c440d4 100644 --- a/cache.go +++ b/cache.go @@ -1005,6 +1005,7 @@ func (c *cache) Items() map[string]Item { m := make(map[string]Item, len(c.items)) now := time.Now().UnixNano() for k, v := range c.items { + // "Inlining" of Expired if v.Expiration > 0 { if now > v.Expiration { continue From 52581776a332a467d34d727f700f046f693c576a Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Fri, 25 Nov 2016 14:18:09 -0500 Subject: [PATCH 33/40] LICENSE: Update copyright year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 159e1e7..f9fe271 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2015 Patrick Mylund Nielsen and the go-cache contributors +Copyright (c) 2012-2016 Patrick Mylund Nielsen and the go-cache contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From e7a9def80f35fe1b170b7b8b68871d59dea117e1 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Fri, 25 Nov 2016 18:48:19 -0500 Subject: [PATCH 34/40] Add SetDefault() for setting with the default expiration --- cache.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cache.go b/cache.go index 8c440d4..70e4dad 100644 --- a/cache.go +++ b/cache.go @@ -81,6 +81,12 @@ func (c *cache) set(k string, x interface{}, d time.Duration) { } } +// Add an item to the cache, replacing any existing item, using the default +// expiration. +func (c *cache) SetDefault(k string, x interface{}) { + c.Set(k, x, DefaultExpiration) +} + // Add an item to the cache only if an item doesn't already exist for the given // key, or if the existing item has expired. Returns an error otherwise. func (c *cache) Add(k string, x interface{}, d time.Duration) error { From 8c11fe2df051d9977a0010807d3f9dbb58c889f4 Mon Sep 17 00:00:00 2001 From: Alex Edwards Date: Thu, 8 Dec 2016 14:50:49 +0100 Subject: [PATCH 35/40] Add GetWithExpiration --- CONTRIBUTORS | 1 + cache.go | 30 ++++++++++++++++ cache_test.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 8a4da4e..2b16e99 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -6,3 +6,4 @@ code was contributed.) Dustin Sallings Jason Mooberry Sergey Shepelev +Alex Edwards diff --git a/cache.go b/cache.go index 70e4dad..30b1ea2 100644 --- a/cache.go +++ b/cache.go @@ -135,6 +135,36 @@ func (c *cache) Get(k string) (interface{}, bool) { return item.Object, true } +// GetWithExpiration returns an item and its expiration time from the cache. +// It returns the item or nil, the expiration time if one is set (if the item +// never expires a zero value for time.Time is returned), and a bool indicating +// whether the key was found. +func (c *cache) GetWithExpiration(k string) (interface{}, time.Time, bool) { + c.mu.RLock() + // "Inlining" of get and Expired + item, found := c.items[k] + if !found { + c.mu.RUnlock() + return nil, time.Time{}, false + } + + if item.Expiration > 0 { + if time.Now().UnixNano() > item.Expiration { + c.mu.RUnlock() + return nil, time.Time{}, false + } + + // Return the item and the expiration time + c.mu.RUnlock() + return item.Object, time.Unix(0, item.Expiration), true + } + + // If expiration <= 0 (i.e. no expiration time set) then return the item + // and a zeroed time.Time + c.mu.RUnlock() + return item.Object, time.Time{}, true +} + func (c *cache) get(k string) (interface{}, bool) { item, found := c.items[k] if !found { diff --git a/cache_test.go b/cache_test.go index 6e81693..47a3d53 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1459,7 +1459,7 @@ func BenchmarkRWMutexMapGet(b *testing.B) { func BenchmarkRWMutexInterfaceMapGetStruct(b *testing.B) { b.StopTimer() - s := struct{name string}{name: "foo"} + s := struct{ name string }{name: "foo"} m := map[interface{}]string{ s: "bar", } @@ -1676,3 +1676,96 @@ func BenchmarkDeleteExpiredLoop(b *testing.B) { tc.DeleteExpired() } } + +func TestGetWithExpiration(t *testing.T) { + tc := New(DefaultExpiration, 0) + + a, expiration, found := tc.GetWithExpiration("a") + if found || a != nil || !expiration.IsZero() { + t.Error("Getting A found value that shouldn't exist:", a) + } + + b, expiration, found := tc.GetWithExpiration("b") + if found || b != nil || !expiration.IsZero() { + t.Error("Getting B found value that shouldn't exist:", b) + } + + c, expiration, found := tc.GetWithExpiration("c") + if found || c != nil || !expiration.IsZero() { + t.Error("Getting C found value that shouldn't exist:", c) + } + + tc.Set("a", 1, DefaultExpiration) + tc.Set("b", "b", DefaultExpiration) + tc.Set("c", 3.5, DefaultExpiration) + tc.Set("d", 1, NoExpiration) + tc.Set("e", 1, 50*time.Millisecond) + + x, expiration, found := tc.GetWithExpiration("a") + if !found { + t.Error("a was not found while getting a2") + } + if x == nil { + t.Error("x for a is nil") + } else if a2 := x.(int); a2+2 != 3 { + t.Error("a2 (which should be 1) plus 2 does not equal 3; value:", a2) + } + if !expiration.IsZero() { + t.Error("expiration for a is not a zeroed time") + } + + x, expiration, found = tc.GetWithExpiration("b") + if !found { + t.Error("b was not found while getting b2") + } + if x == nil { + t.Error("x for b is nil") + } else if b2 := x.(string); b2+"B" != "bB" { + t.Error("b2 (which should be b) plus B does not equal bB; value:", b2) + } + if !expiration.IsZero() { + t.Error("expiration for b is not a zeroed time") + } + + x, expiration, found = tc.GetWithExpiration("c") + if !found { + t.Error("c was not found while getting c2") + } + if x == nil { + t.Error("x for c is nil") + } else if c2 := x.(float64); c2+1.2 != 4.7 { + t.Error("c2 (which should be 3.5) plus 1.2 does not equal 4.7; value:", c2) + } + if !expiration.IsZero() { + t.Error("expiration for c is not a zeroed time") + } + + x, expiration, found = tc.GetWithExpiration("d") + if !found { + t.Error("d was not found while getting d2") + } + if x == nil { + t.Error("x for d is nil") + } else if d2 := x.(int); d2+2 != 3 { + t.Error("d (which should be 1) plus 2 does not equal 3; value:", d2) + } + if !expiration.IsZero() { + t.Error("expiration for d is not a zeroed time") + } + + x, expiration, found = tc.GetWithExpiration("e") + if !found { + t.Error("e was not found while getting e2") + } + if x == nil { + t.Error("x for e is nil") + } else if e2 := x.(int); e2+2 != 3 { + t.Error("e (which should be 1) plus 2 does not equal 3; value:", e2) + } + if expiration.UnixNano() != tc.items["e"].Expiration { + t.Error("expiration for e is not the correct time") + } + if expiration.UnixNano() < time.Now().UnixNano() { + t.Error("expiration for e is in the past") + } +} From dd1ed0ba63f345ac734aaab8a27d14407380779d Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Sun, 26 Mar 2017 12:30:15 -0400 Subject: [PATCH 36/40] README.md: Remove one level of indentation and increase 'recommended' cleanupInterval --- README.md | 151 +++++++++++++++++++++++++++--------------------------- 1 file changed, 75 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 168ff7b..372c9b7 100644 --- a/README.md +++ b/README.md @@ -20,86 +20,85 @@ one) to recover from downtime quickly. (See the docs for `NewFrom()` for caveats ### Usage ```go - import ( - "fmt" - "github.com/patrickmn/go-cache" - "time" - ) - - func main() { - - // Create a cache with a default expiration time of 5 minutes, and which - // purges expired items every 30 seconds - c := cache.New(5*time.Minute, 30*time.Second) - - // Set the value of the key "foo" to "bar", with the default expiration time - c.Set("foo", "bar", cache.DefaultExpiration) - - // Set the value of the key "baz" to 42, with no expiration time - // (the item won't be removed until it is re-set, or removed using - // c.Delete("baz") - c.Set("baz", 42, cache.NoExpiration) - - // Get the string associated with the key "foo" from the cache - foo, found := c.Get("foo") - if found { - fmt.Println(foo) - } - - // Since Go is statically typed, and cache values can be anything, type - // assertion is needed when values are being passed to functions that don't - // take arbitrary types, (i.e. interface{}). The simplest way to do this for - // values which will only be used once--e.g. for passing to another - // function--is: - foo, found := c.Get("foo") - if found { - MyFunction(foo.(string)) - } - - // This gets tedious if the value is used several times in the same function. - // You might do either of the following instead: - if x, found := c.Get("foo"); found { - foo := x.(string) - // ... - } - // or - var foo string - if x, found := c.Get("foo"); found { - foo = x.(string) - } - // ... - // foo can then be passed around freely as a string +import ( + "fmt" + "github.com/patrickmn/go-cache" + "time" +) + +func main() { + // Create a cache with a default expiration time of 5 minutes, and which + // purges expired items every 10 minutes + c := cache.New(5*time.Minute, 10*time.Minute) + + // Set the value of the key "foo" to "bar", with the default expiration time + c.Set("foo", "bar", cache.DefaultExpiration) + + // Set the value of the key "baz" to 42, with no expiration time + // (the item won't be removed until it is re-set, or removed using + // c.Delete("baz") + c.Set("baz", 42, cache.NoExpiration) + + // Get the string associated with the key "foo" from the cache + foo, found := c.Get("foo") + if found { + fmt.Println(foo) + } - // Want performance? Store pointers! - c.Set("foo", &MyStruct, cache.DefaultExpiration) - if x, found := c.Get("foo"); found { - foo := x.(*MyStruct) - // ... - } - - // If you store a reference type like a pointer, slice, map or channel, you - // do not need to run Set if you modify the underlying data. The cached - // reference points to the same memory, so if you modify a struct whose - // pointer you've stored in the cache, retrieving that pointer with Get will - // point you to the same data: - foo := &MyStruct{Num: 1} - c.Set("foo", foo, cache.DefaultExpiration) - // ... - x, _ := c.Get("foo") - foo := x.(*MyStruct) - fmt.Println(foo.Num) - // ... - foo.Num++ - // ... - x, _ := c.Get("foo") - foo := x.(*MyStruct) - foo.Println(foo.Num) + // Since Go is statically typed, and cache values can be anything, type + // assertion is needed when values are being passed to functions that don't + // take arbitrary types, (i.e. interface{}). The simplest way to do this for + // values which will only be used once--e.g. for passing to another + // function--is: + foo, found := c.Get("foo") + if found { + MyFunction(foo.(string)) + } - // will print: - // 1 - // 2 + // This gets tedious if the value is used several times in the same function. + // You might do either of the following instead: + if x, found := c.Get("foo"); found { + foo := x.(string) + // ... + } + // or + var foo string + if x, found := c.Get("foo"); found { + foo = x.(string) + } + // ... + // foo can then be passed around freely as a string + // Want performance? Store pointers! + c.Set("foo", &MyStruct, cache.DefaultExpiration) + if x, found := c.Get("foo"); found { + foo := x.(*MyStruct) + // ... } + + // If you store a reference type like a pointer, slice, map or channel, you + // do not need to run Set if you modify the underlying data. The cached + // reference points to the same memory, so if you modify a struct whose + // pointer you've stored in the cache, retrieving that pointer with Get will + // point you to the same data: + foo := &MyStruct{Num: 1} + c.Set("foo", foo, cache.DefaultExpiration) + // ... + x, _ := c.Get("foo") + foo := x.(*MyStruct) + fmt.Println(foo.Num) + // ... + foo.Num++ + // ... + x, _ := c.Get("foo") + foo := x.(*MyStruct) + foo.Println(foo.Num) + + // will print: + // 1 + // 2 + +} ``` ### Reference From 96426d0c5b53b58dee9d0c88e391281dfa28297f Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Sun, 26 Mar 2017 12:36:28 -0400 Subject: [PATCH 37/40] README.md: Remove the unprotected change example since it would actually need external synchronization --- README.md | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/README.md b/README.md index 372c9b7..c5789cc 100644 --- a/README.md +++ b/README.md @@ -75,29 +75,6 @@ func main() { foo := x.(*MyStruct) // ... } - - // If you store a reference type like a pointer, slice, map or channel, you - // do not need to run Set if you modify the underlying data. The cached - // reference points to the same memory, so if you modify a struct whose - // pointer you've stored in the cache, retrieving that pointer with Get will - // point you to the same data: - foo := &MyStruct{Num: 1} - c.Set("foo", foo, cache.DefaultExpiration) - // ... - x, _ := c.Get("foo") - foo := x.(*MyStruct) - fmt.Println(foo.Num) - // ... - foo.Num++ - // ... - x, _ := c.Get("foo") - foo := x.(*MyStruct) - foo.Println(foo.Num) - - // will print: - // 1 - // 2 - } ``` From ea4bd2a538d816ad2bd3426ced3a99f2382103ba Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Sun, 26 Mar 2017 12:37:11 -0400 Subject: [PATCH 38/40] LICENSE: Update copyright years --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index f9fe271..db9903c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2016 Patrick Mylund Nielsen and the go-cache contributors +Copyright (c) 2012-2017 Patrick Mylund Nielsen and the go-cache contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 0640633ccc0b5a5a7c01b466a6c1702b8523b241 Mon Sep 17 00:00:00 2001 From: Vivian Mathews Date: Fri, 21 Jul 2017 14:56:50 -0400 Subject: [PATCH 39/40] Fix race condition - the gc finalize for an object races with the janitor.Run goroutine - because the janitor.stop channel is created in the Run() goroutine this leads to a data race. - fix by creating the channel when the janitor is created --- cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cache.go b/cache.go index 30b1ea2..db88d2f 100644 --- a/cache.go +++ b/cache.go @@ -1074,7 +1074,6 @@ type janitor struct { } func (j *janitor) Run(c *cache) { - j.stop = make(chan bool) ticker := time.NewTicker(j.Interval) for { select { @@ -1094,6 +1093,7 @@ func stopJanitor(c *Cache) { func runJanitor(c *cache, ci time.Duration) { j := &janitor{ Interval: ci, + stop: make(chan bool), } c.janitor = j go j.Run(c) From 9f6ff22cfff829561052f5886ec80a2ac148b4eb Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Sun, 27 May 2018 00:33:50 -0400 Subject: [PATCH 40/40] Fix benchmark for-loop shadowing --- cache_test.go | 6 +++--- sharded_test.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cache_test.go b/cache_test.go index 47a3d53..cb80b38 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1563,12 +1563,12 @@ func benchmarkCacheGetManyConcurrent(b *testing.B, exp time.Duration) { wg := new(sync.WaitGroup) wg.Add(n) for _, v := range keys { - go func() { + go func(k string) { for j := 0; j < each; j++ { - tc.Get(v) + tc.Get(k) } wg.Done() - }() + }(v) } b.StartTimer() wg.Wait() diff --git a/sharded_test.go b/sharded_test.go index aef8597..4bc26dd 100644 --- a/sharded_test.go +++ b/sharded_test.go @@ -73,12 +73,12 @@ func benchmarkShardedCacheGetManyConcurrent(b *testing.B, exp time.Duration) { wg := new(sync.WaitGroup) wg.Add(n) for _, v := range keys { - go func() { + go func(k string) { for j := 0; j < each; j++ { - tsc.Get(v) + tsc.Get(k) } wg.Done() - }() + }(v) } b.StartTimer() wg.Wait()