diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba8bd4..88a984d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +# 2.7.0 (June 2021) + +#46 : got panic + +A panic occured in a line that checks the maximum amount of items in the cache. While not definite root cause has been found, there is indeed the possibility of crashing an empty cache if the cache limit is set to 'zero' which codes for infinite. This would lead to removal of the first item in the cache which would panic on an empty cache. + +Fixed this by applying the global cache lock to all configuration options as well. + +# 2.6.0 (May 2021) + +#44 : There are no API changes, but a contribution was made to use https://pkg.go.dev/golang.org/x/sync/singleflight as a way to provide everybody waiting for a key with that key when it's fetched. + +This removes some complexity from the code and will make sure that all callers will get a return value even if there's high concurrency and low TTL (as proven by the test that was added). + +# 2.5.0 (May 2021) + +## API changes: + +* #39 : Allow custom loader function for each key via `GetByLoader` + +Introduce the `SimpleCache` interface for quick-start and basic usage. + +# 2.4.0 (April 2021) + +## API changes: + +* #42 : Add option to get list of keys +* #40: Allow 'Touch' on items without other operation + +// Touch resets the TTL of the key when it exists, returns ErrNotFound if the key is not present. +func (cache *Cache) Touch(key string) error + +// GetKeys returns all keys of items in the cache. Returns nil when the cache has been closed. +func (cache *Cache) GetKeys() []string + # 2.3.0 (February 2021) ## API changes: diff --git a/Readme.md b/Readme.md index 08ea517..2ab17e3 100644 --- a/Readme.md +++ b/Readme.md @@ -23,8 +23,39 @@ ## Usage -You can copy it as a full standalone demo program. +You can copy it as a full standalone demo program. The first snippet is basic usage, where the second exploits more options in the cache. +Basic: +```go +package main + +import ( + "fmt" + "time" + + "github.com/ReneKroon/ttlcache/v2" +) + +var notFound = ttlcache.ErrNotFound + +func main() { + var cache ttlcache.SimpleCache = ttlcache.NewCache() + + cache.SetTTL(time.Duration(10 * time.Second)) + cache.Set("MyKey", "MyValue") + cache.Set("MyNumber", 1000) + + if val, err := cache.Get("MyKey"); err != notFound { + fmt.Printf("Got it: %s\n", val) + } + + cache.Remove("MyNumber") + cache.Purge() + cache.Close() +} +``` + +Advanced: ```go package main diff --git a/bench/bench_test.go b/bench/bench_test.go new file mode 100644 index 0000000..b4e9d5e --- /dev/null +++ b/bench/bench_test.go @@ -0,0 +1,37 @@ +package bench + +import ( + "fmt" + "testing" + "time" + + ttlcache "github.com/ReneKroon/ttlcache/v2" +) + +func BenchmarkCacheSetWithoutTTL(b *testing.B) { + cache := ttlcache.NewCache() + defer cache.Close() + + for n := 0; n < b.N; n++ { + cache.Set(fmt.Sprint(n%1000000), "value") + } +} + +func BenchmarkCacheSetWithGlobalTTL(b *testing.B) { + cache := ttlcache.NewCache() + defer cache.Close() + + cache.SetTTL(time.Duration(50 * time.Millisecond)) + for n := 0; n < b.N; n++ { + cache.Set(fmt.Sprint(n%1000000), "value") + } +} + +func BenchmarkCacheSetWithTTL(b *testing.B) { + cache := ttlcache.NewCache() + defer cache.Close() + + for n := 0; n < b.N; n++ { + cache.SetWithTTL(fmt.Sprint(n%1000000), "value", time.Duration(50*time.Millisecond)) + } +} diff --git a/cache.go b/cache.go index 51ff261..53227a4 100644 --- a/cache.go +++ b/cache.go @@ -1,6 +1,7 @@ package ttlcache import ( + "golang.org/x/sync/singleflight" "sync" "time" ) @@ -17,13 +18,24 @@ // LoaderFunction can be supplied to retrieve an item where a cache miss occurs. Supply an item specific ttl or Duration.Zero type LoaderFunction func(key string) (data interface{}, ttl time.Duration, err error) + +// SimpleCache interface enables a quick-start. Interface for basic usage. +type SimpleCache interface { + Get(key string) (interface{}, error) + Set(key string, data interface{}) error + SetTTL(ttl time.Duration) error + SetWithTTL(key string, data interface{}, ttl time.Duration) error + Remove(key string) error + Close() error + Purge() error +} // Cache is a synchronized map of items that can auto-expire once stale type Cache struct { mutex sync.Mutex ttl time.Duration items map[string]*item - loaderLock map[string]*sync.Cond + loaderLock *singleflight.Group expireCallback ExpireCallback expireReasonCallback ExpireReasonCallback checkExpireCallback CheckExpireCallback @@ -266,6 +278,11 @@ // Get is a thread-safe way to lookup items // Every lookup, also touches the item, hence extending it's life func (cache *Cache) Get(key string) (interface{}, error) { + return cache.GetByLoader(key, nil) +} + +// GetByLoader can take a per key loader function (ie. to propagate context) +func (cache *Cache) GetByLoader(key string, customLoaderFunction LoaderFunction) (interface{}, error) { cache.mutex.Lock() if cache.isShutDown { cache.mutex.Unlock() @@ -286,38 +303,23 @@ cache.metrics.Misses++ err = ErrNotFound } - if cache.loaderFunction == nil || exists { - cache.mutex.Unlock() - } - - if cache.loaderFunction != nil && !exists { - if lock, ok := cache.loaderLock[key]; ok { - // if a lock is present then a fetch is in progress and we wait. - cache.mutex.Unlock() - lock.L.Lock() - lock.Wait() - lock.L.Unlock() - cache.mutex.Lock() - item, exists, triggerExpirationNotification = cache.getItem(key) - if exists { - dataToReturn = item.data - err = nil - } - cache.mutex.Unlock() - } else { - // if no lock is present we are the leader and should set the lock and fetch. - m := sync.NewCond(&sync.Mutex{}) - cache.loaderLock[key] = m - cache.mutex.Unlock() - // cache is not blocked during IO - dataToReturn, err = cache.invokeLoader(key) - cache.mutex.Lock() - m.Broadcast() - // cleanup so that we don't block consecutive access. - delete(cache.loaderLock, key) - cache.mutex.Unlock() - } - + + loaderFunction := cache.loaderFunction + if customLoaderFunction != nil { + loaderFunction = customLoaderFunction + } + + if loaderFunction == nil || exists { + cache.mutex.Unlock() + } + + if loaderFunction != nil && !exists { + cache.mutex.Unlock() + dataToReturn, err, _ = cache.loaderLock.Do(key, func() (interface{}, error) { + // cache is not blocked during io + invokeData, err := cache.invokeLoader(key, loaderFunction) + return invokeData, err + }) } if triggerExpirationNotification { @@ -327,10 +329,10 @@ return dataToReturn, err } -func (cache *Cache) invokeLoader(key string) (dataToReturn interface{}, err error) { +func (cache *Cache) invokeLoader(key string, loaderFunction LoaderFunction) (dataToReturn interface{}, err error) { var ttl time.Duration - dataToReturn, ttl, err = cache.loaderFunction(key) + dataToReturn, ttl, err = loaderFunction(key) if err == nil { err = cache.SetWithTTL(key, dataToReturn, ttl) if err != nil { @@ -402,22 +404,30 @@ // SetExpirationCallback sets a callback that will be called when an item expires func (cache *Cache) SetExpirationCallback(callback ExpireCallback) { + cache.mutex.Lock() + defer cache.mutex.Unlock() cache.expireCallback = callback } // SetExpirationReasonCallback sets a callback that will be called when an item expires, includes reason of expiry func (cache *Cache) SetExpirationReasonCallback(callback ExpireReasonCallback) { + cache.mutex.Lock() + defer cache.mutex.Unlock() cache.expireReasonCallback = callback } // SetCheckExpirationCallback sets a callback that will be called when an item is about to expire // in order to allow external code to decide whether the item expires or remains for another TTL cycle func (cache *Cache) SetCheckExpirationCallback(callback CheckExpireCallback) { + cache.mutex.Lock() + defer cache.mutex.Unlock() cache.checkExpireCallback = callback } // SetNewItemCallback sets a callback that will be called when a new item is added to the cache func (cache *Cache) SetNewItemCallback(callback ExpireCallback) { + cache.mutex.Lock() + defer cache.mutex.Unlock() cache.newItemCallback = callback } @@ -425,12 +435,16 @@ // no longer extend TTL of items when they are retrieved using Get, or when their expiration condition is evaluated // using SetCheckExpirationCallback. func (cache *Cache) SkipTTLExtensionOnHit(value bool) { + cache.mutex.Lock() + defer cache.mutex.Unlock() cache.skipTTLExtension = value } // SetLoaderFunction allows you to set a function to retrieve cache misses. The signature matches that of the Get function. // Additional Get calls on the same key block while fetching is in progress (groupcache style). func (cache *Cache) SetLoaderFunction(loader LoaderFunction) { + cache.mutex.Lock() + defer cache.mutex.Unlock() cache.loaderFunction = loader } @@ -451,6 +465,8 @@ // If a new item is getting cached, the closes item to being timed out will be replaced // Set to 0 to turn off func (cache *Cache) SetCacheSizeLimit(limit int) { + cache.mutex.Lock() + defer cache.mutex.Unlock() cache.sizeLimit = limit } @@ -461,7 +477,7 @@ cache := &Cache{ items: make(map[string]*item), - loaderLock: make(map[string]*sync.Cond), + loaderLock: &singleflight.Group{}, priorityQueue: newPriorityQueue(), expirationNotification: make(chan bool), expirationTime: time.Now(), diff --git a/cache_test.go b/cache_test.go index 6e770c4..58a04af 100644 --- a/cache_test.go +++ b/cache_test.go @@ -18,6 +18,57 @@ func TestMain(m *testing.M) { goleak.VerifyTestMain(m) +} + +// The SimpleCache interface enables quick-start. +func TestCache_SimpleCache(t *testing.T) { + t.Parallel() + var cache SimpleCache = NewCache() + + cache.SetTTL(time.Second) + cache.Set("k", "v") + cache.Get("k") + cache.Purge() + cache.Close() + +} + +// Issue / PR #39: add customer loader function for each Get() # +// some middleware prefers to define specific context's etc per Get. +// This is faciliated by supplying a loder function with Get's. +func TestCache_GetByLoader(t *testing.T) { + t.Parallel() + cache := NewCache() + defer cache.Close() + + globalLoader := func(key string) (data interface{}, ttl time.Duration, err error) { + return "global", 0, nil + } + cache.SetLoaderFunction(globalLoader) + + localLoader := func(key string) (data interface{}, ttl time.Duration, err error) { + return "local", 0, nil + } + + key, _ := cache.Get("test") + assert.Equal(t, "global", key) + + cache.Remove("test") + + localKey, _ := cache.GetByLoader("test", localLoader) + assert.Equal(t, "local", localKey) + + cache.Remove("test") + + globalKey, _ := cache.GetByLoader("test", globalLoader) + assert.Equal(t, "global", globalKey) + + cache.Remove("test") + + defaultKey, _ := cache.GetByLoader("test", nil) + assert.Equal(t, "global", defaultKey) + + cache.Remove("test") } // Issue #38: Feature request: ability to know why an expiry has occurred @@ -225,6 +276,37 @@ cache.Close() +} + +// Cache sometimes returns key not found under parallel access with a loader function +func TestCache_TestLoaderFunctionParallelKeyAccess(t *testing.T) { + t.Parallel() + cache := NewCache() + + cache.SetLoaderFunction(func(key string) (data interface{}, ttl time.Duration, err error) { + time.Sleep(time.Millisecond * 300) + return "1", 1 * time.Nanosecond, nil + }) + + wg := sync.WaitGroup{} + errCount := uint64(0) + for i := 0; i < 200; i++ { + wg.Add(1) + go func() { + defer wg.Done() + value, found := cache.Get("foo") + if value != "1" || found != nil { // Use an atomic to avoid spamming logs + atomic.AddUint64(&errCount, 1) + } + }() + + } + + wg.Wait() + + assert.Equalf(t, uint64(0), errCount, "expected 0 errs, got %d", errCount) + + cache.Close() } // Issue #28: call expirationCallback automatically on cache.Close() diff --git a/debian/changelog b/debian/changelog index 343b151..7d3c5aa 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +golang-github-renekroon-ttlcache (2.7.0-1) UNRELEASED; urgency=low + + * New upstream release. + + -- Debian Janitor Thu, 03 Jun 2021 21:33:45 -0000 + golang-github-renekroon-ttlcache (2.3.0+ds-1) unstable; urgency=medium * New upstream release. diff --git a/go.mod b/go.mod index 510ef4c..c39d9fd 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ go 1.15 require ( - github.com/alvaroloes/enumer v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/stretchr/testify v1.7.0 go.uber.org/goleak v1.1.10 golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064 // indirect ) diff --git a/go.sum b/go.sum index 168c7af..d1f5162 100644 --- a/go.sum +++ b/go.sum @@ -1,86 +1,57 @@ -github.com/alvaroloes/enumer v1.1.2 h1:5khqHB33TZy1GWCO/lZwcroBFh7u+0j40T83VUbfAMY= -github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU= -github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d h1:szSOL78iTCl0LF1AMjhSWJj8tIM0KixlUUnBtYXsmd8= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064 h1:BmCFkEH4nJrYcAc2L08yX5RhYGD4j58PTMkEUDkpz2I= golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=