Compare commits

...

48 Commits

Author SHA1 Message Date
Patrick Mylund Nielsen 46f4078530 Fix incorrect key in concurrent benchmarks
Fixes #111
2019-10-04 15:21:08 -04:00
Patrick Mylund Nielsen 8026b575a9 LICENSE: Update copyright years 2019-10-04 15:15:23 -04:00
Patrick Mylund Nielsen 5633e08626 LICENSE: Update years 2018-08-15 01:31:27 -04:00
Patrick Mylund Nielsen 9f6ff22cff Fix benchmark for-loop shadowing 2018-05-27 00:33:50 -04:00
Patrick Mylund Nielsen a3647f8e31 Merge pull request #64 from inf-rno/master
Fix janitor stop channel data race
2017-07-22 00:01:10 -04:00
Vivian Mathews 0640633ccc 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
2017-07-21 14:56:50 -04:00
Patrick Mylund Nielsen 7ac151875f Merge pull request #46 from alexedwards/master
Add GetWithExpiration
2017-04-18 19:29:47 -04:00
Patrick Mylund Nielsen ea4bd2a538 LICENSE: Update copyright years 2017-03-26 12:37:11 -04:00
Patrick Mylund Nielsen 96426d0c5b README.md: Remove the unprotected change example since it would actually need external synchronization 2017-03-26 12:36:28 -04:00
Patrick Mylund Nielsen dd1ed0ba63 README.md: Remove one level of indentation and increase 'recommended' cleanupInterval 2017-03-26 12:30:15 -04:00
Alex Edwards 8c11fe2df0 Add GetWithExpiration 2016-12-08 14:50:49 +01:00
Patrick Mylund Nielsen e7a9def80f Add SetDefault() for setting with the default expiration 2016-11-25 18:48:19 -05:00
Patrick Mylund Nielsen 52581776a3 LICENSE: Update copyright year 2016-11-25 14:18:09 -05:00
Patrick Mylund Nielsen 9e6d9117e7 Add 'inlining of expired' note to Items() 2016-11-25 13:57:39 -05:00
Patrick Mylund Nielsen a2d8b56f0c Make Items() return a copy rather than an unsynchronized reference to the underlying items map 2016-11-25 13:56:11 -05:00
Patrick Mylund Nielsen 1881a9bccb Merge pull request #29 from darrenmcc/master
added go syntax highlighting to README
2016-01-27 12:00:04 -05:00
Darren McCleary da6326cd69 added go syntax highlighting to README 2016-01-27 11:56:21 -05:00
Patrick Mylund Nielsen 5849ccb308 remove mu.RUnlock call from get 2016-01-08 15:02:42 -05:00
Patrick Mylund Nielsen 721cc9438c Add BenchmarkRWMutexInterfaceMapGetString 2015-12-03 09:55:58 -05:00
Patrick Mylund Nielsen 8c41258ef3 Add BenchmarkRWMutexInterfaceMapGet 2015-12-03 09:40:14 -05:00
Patrick Mylund Nielsen faf83836bd Change GitHub repository URLs in README 2015-12-02 14:32:12 -05:00
Patrick Mylund Nielsen 66bf7b7a45 Update README to point to new repository URL 2015-12-01 11:18:46 -05:00
Patrick Mylund Nielsen d461c5d2dd 'Inline' set in Set, and do time checks before the lock 2015-12-01 11:08:43 -05:00
Patrick Mylund Nielsen 76f1250a65 Make OnEvicted() a little faster 2015-11-30 16:18:49 -05:00
Patrick Mylund Nielsen 7c1e7f5829 go fmt 2015-11-30 16:04:57 -05:00
Patrick Mylund Nielsen 9fc6f9c73f Add expiring/notexpiring sharded cache benchmarks 2015-11-30 16:04:49 -05:00
Patrick Mylund Nielsen afadf13f9f Back to UnixNano(), syscall dependency isn't worth a few nanoseconds better performance 2015-11-30 15:12:19 -05:00
Patrick Mylund Nielsen f6cdd07cbb Merge branch 'timeval' 2015-11-30 15:02:11 -05:00
Patrick Mylund Nielsen 2f0c74ebb8 Use intermediary timevals 2015-11-30 15:02:02 -05:00
Patrick Mylund Nielsen 2f60853f80 No need for emptyTime anymore 2015-11-30 14:49:18 -05:00
Patrick Mylund Nielsen 01842a547c Use timevals 2015-11-30 14:47:22 -05:00
Patrick Mylund Nielsen 1924ec3baf Remove expired() since it's no longer used (because of the inlining) 2015-11-30 14:14:52 -05:00
Patrick Mylund Nielsen 8084bd02b5 Inline expiration checks manually for performance 2015-11-30 14:12:45 -05:00
Patrick Mylund Nielsen eb4f9f6b2f Use UnixNano int64s instead of Time 2015-11-30 13:54:01 -05:00
Patrick Mylund Nielsen 31c7be0bed 'Inline' Get and Expired 2015-11-30 13:50:17 -05:00
Patrick Mylund Nielsen 4e0d34ef00 Only get the current time once in the DeleteExpired loop 2015-11-30 13:39:27 -05:00
Patrick Mylund Nielsen a45ed98559 Add benchmarks that use expiring items (time.Now calls) and rename BenchmarkDeleteExpired to BenchmarkDeleteExpiredLoop for clarity 2015-11-30 10:45:30 -05:00
Patrick Mylund Nielsen 28ab885a1a Make BenchmarkDeleteExpired more meaningful 2015-11-28 15:13:26 -05:00
Patrick Mylund Nielsen cf4e165754 Add IncrementInt benchmark 2015-11-28 14:56:23 -05:00
Patrick Mylund Nielsen 901b2413ee Improve cache locality by removing Item-related pointers 2015-11-28 14:47:46 -05:00
Patrick Mylund Nielsen 3d4d09ca0b Add a benchmark for DeleteExpired() 2015-11-28 14:35:38 -05:00
Patrick Mylund Nielsen 0ba3e0049c Update copyright years 2015-11-28 14:21:44 -05:00
Patrick Mylund Nielsen ac0fcef49b Clarify that the OnEvicted function isn't called when an item is overwritten 2015-11-28 12:27:08 -05:00
Patrick Mylund Nielsen e9441b12e0 Add mutex-using test condition to TestOnEvicted 2015-11-28 12:22:52 -05:00
Patrick Mylund Nielsen 3f2c810ea1 Add OnEvicted() 2015-11-27 22:00:08 -05:00
Patrick Mylund Nielsen a0136a8980 Don't expose the cache mutex 2015-11-27 13:03:24 -05:00
Patrick Mylund Nielsen a122e14c4b Merge pull request #23 from databus23/patch-1
Fix leaking the janitor ticker when shutting down
2015-10-13 08:21:26 -04:00
Fabian Ruff fe045e4040 Fix leaking the janitor ticker when shutting down 2015-10-13 12:55:54 +02:00
7 changed files with 624 additions and 293 deletions

View File

@ -6,3 +6,4 @@ code was contributed.)
Dustin Sallings <dustin@spy.net>
Jason Mooberry <jasonmoo@me.com>
Sergey Shepelev <temotor@gmail.com>
Alex Edwards <ajmedwards@gmail.com>

View File

@ -1,4 +1,4 @@
Copyright (c) 2012-2014 Patrick Mylund Nielsen and the go-cache contributors
Copyright (c) 2012-2019 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

131
README.md
View File

@ -15,92 +15,69 @@ 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"
"time"
)
```go
import (
"fmt"
"github.com/patrickmn/go-cache"
"time"
)
func main() {
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)
// 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 "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
// 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
// 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
// Want performance? Store pointers!
c.Set("foo", &MyStruct, cache.DefaultExpiration)
if x, found := c.Get("foo"); found {
foo := x.(*MyStruct)
// ...
}
}
```
### 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)

505
cache.go

File diff suppressed because it is too large Load Diff

View File

@ -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: 0,
},
"b": &Item{
"b": Item{
Object: 2,
Expiration: nil,
Expiration: 0,
},
}
tc := NewFrom(DefaultExpiration, 0, m)
@ -1224,6 +1224,29 @@ 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.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) {
tc := New(DefaultExpiration, 0)
testFillAndSerialize(t, tc)
@ -1402,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++ {
@ -1426,9 +1457,46 @@ func BenchmarkRWMutexMapGet(b *testing.B) {
}
}
func BenchmarkCacheGetConcurrent(b *testing.B) {
func BenchmarkRWMutexInterfaceMapGetStruct(b *testing.B) {
b.StopTimer()
tc := New(DefaultExpiration, 0)
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 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)
}
func BenchmarkCacheGetConcurrentNotExpiring(b *testing.B) {
benchmarkCacheGetConcurrent(b, NoExpiration)
}
func benchmarkCacheGetConcurrent(b *testing.B, exp time.Duration) {
b.StopTimer()
tc := New(exp, 0)
tc.Set("foo", "bar", DefaultExpiration)
wg := new(sync.WaitGroup)
workers := runtime.NumCPU()
@ -1470,16 +1538,24 @@ 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)
k := "foo" + strconv.Itoa(i)
keys[i] = k
tc.Set(k, "bar", DefaultExpiration)
}
@ -1487,20 +1563,28 @@ func BenchmarkCacheGetManyConcurrent(b *testing.B) {
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()
}
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)
@ -1549,10 +1633,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()
}
}
@ -1568,3 +1652,120 @@ func BenchmarkRWMutexMapSetDeleteSingleLock(b *testing.B) {
mu.Unlock()
}
}
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 BenchmarkDeleteExpiredLoop(b *testing.B) {
b.StopTimer()
tc := New(5*time.Minute, 0)
tc.mu.Lock()
for i := 0; i < 100000; i++ {
tc.set(strconv.Itoa(i), "bar", DefaultExpiration)
}
tc.mu.Unlock()
b.StartTimer()
for i := 0; i < b.N; i++ {
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")
}
}

View File

@ -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
}

View File

@ -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,13 +51,21 @@ 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)
k := "foo" + strconv.Itoa(i)
keys[i] = k
tsc.Set(k, "bar", DefaultExpiration)
}
@ -56,12 +73,12 @@ func BenchmarkShardedCacheGetManyConcurrent(b *testing.B) {
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()