diff --git a/CHANGELOG.md b/CHANGELOG.md index cc3fcf4..093862b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## v2.0.0 + +- Add type parameters for the key and value type of a TimedMap and corresponding constructor functions. +- Remove the section system for better simplicity and usability. +- Remove deprecated `SetExpire` method. +- Update documentation. +- Update minimum required Go version to v1.19.0. + +## v1.5.2 + +- Multiple race conditions have been fixed (by @ShivamKumar2002 in https://github.com/zekroTJA/timedmap/pull/8) + +## v1.5.1 + +- Add [`FromMap`](https://pkg.go.dev/github.com/zekroTJA/timedmap#FromMap) constructor which can be used to create a `TimedMap` from an existing map with the given expiration values for each key-value pair. + ## v1.4.0 - Add `SetExpires` method to match `Section` interface and match naming scheme of the other expire-related endpoints. diff --git a/benchmarks/v1.5.2.txt b/benchmarks/v1.5.2.txt new file mode 100644 index 0000000..d593b8e --- /dev/null +++ b/benchmarks/v1.5.2.txt @@ -0,0 +1,46 @@ +goos: windows +goarch: amd64 +pkg: github.com/zekroTJA/timedmap +cpu: AMD Ryzen 7 5800X 8-Core Processor +BenchmarkSetValues-16 1882472 678.8 ns/op 252 B/op 3 allocs/op +BenchmarkSetValues-16 1968472 670.1 ns/op 245 B/op 3 allocs/op +BenchmarkSetValues-16 2001894 655.4 ns/op 242 B/op 3 allocs/op +BenchmarkSetValues-16 2135914 635.2 ns/op 232 B/op 3 allocs/op +BenchmarkSetValues-16 2148526 631.1 ns/op 231 B/op 3 allocs/op +BenchmarkSetValues-16 2146839 633.8 ns/op 231 B/op 3 allocs/op +BenchmarkSetValues-16 2160771 630.3 ns/op 230 B/op 3 allocs/op +BenchmarkSetValues-16 2159397 634.7 ns/op 230 B/op 3 allocs/op +BenchmarkSetValues-16 2115090 636.0 ns/op 233 B/op 3 allocs/op +BenchmarkSetValues-16 2125656 633.5 ns/op 232 B/op 3 allocs/op +BenchmarkSetGetValues-16 1862566 729.9 ns/op 254 B/op 3 allocs/op +BenchmarkSetGetValues-16 1837540 723.3 ns/op 256 B/op 3 allocs/op +BenchmarkSetGetValues-16 1820694 702.8 ns/op 258 B/op 3 allocs/op +BenchmarkSetGetValues-16 1830153 708.0 ns/op 257 B/op 3 allocs/op +BenchmarkSetGetValues-16 1831124 716.2 ns/op 257 B/op 3 allocs/op +BenchmarkSetGetValues-16 1837466 709.6 ns/op 256 B/op 3 allocs/op +BenchmarkSetGetValues-16 1820491 710.9 ns/op 258 B/op 3 allocs/op +BenchmarkSetGetValues-16 1851866 724.8 ns/op 255 B/op 3 allocs/op +BenchmarkSetGetValues-16 1834436 720.6 ns/op 257 B/op 3 allocs/op +BenchmarkSetGetValues-16 1760607 670.2 ns/op 264 B/op 3 allocs/op +BenchmarkSetGetRemoveValues-16 3977371 300.9 ns/op 15 B/op 1 allocs/op +BenchmarkSetGetRemoveValues-16 3949048 300.1 ns/op 16 B/op 1 allocs/op +BenchmarkSetGetRemoveValues-16 3930261 301.7 ns/op 15 B/op 1 allocs/op +BenchmarkSetGetRemoveValues-16 3964603 302.3 ns/op 15 B/op 1 allocs/op +BenchmarkSetGetRemoveValues-16 3970574 301.3 ns/op 15 B/op 1 allocs/op +BenchmarkSetGetRemoveValues-16 3970753 303.3 ns/op 16 B/op 1 allocs/op +BenchmarkSetGetRemoveValues-16 3957939 302.4 ns/op 16 B/op 1 allocs/op +BenchmarkSetGetRemoveValues-16 3965145 301.8 ns/op 16 B/op 1 allocs/op +BenchmarkSetGetRemoveValues-16 3981002 302.6 ns/op 15 B/op 1 allocs/op +BenchmarkSetGetRemoveValues-16 3960450 301.5 ns/op 16 B/op 1 allocs/op +BenchmarkSetGetSameKey-16 8956666 132.5 ns/op 8 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 9140398 132.4 ns/op 8 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 9072358 133.2 ns/op 8 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 9228426 130.8 ns/op 8 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 9120446 132.2 ns/op 8 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 9067176 132.3 ns/op 8 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 9152479 132.0 ns/op 8 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 9019464 131.8 ns/op 8 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 9069512 131.6 ns/op 8 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 8987463 133.1 ns/op 8 B/op 0 allocs/op +PASS +ok github.com/zekroTJA/timedmap 575.199s diff --git a/benchmarks/v2.0.0.txt b/benchmarks/v2.0.0.txt new file mode 100644 index 0000000..da6ca41 --- /dev/null +++ b/benchmarks/v2.0.0.txt @@ -0,0 +1,46 @@ +goos: windows +goarch: amd64 +pkg: github.com/zekroTJA/timedmap +cpu: AMD Ryzen 7 5800X 8-Core Processor +BenchmarkSetValues-16 3184940 339.4 ns/op 120 B/op 1 allocs/op +BenchmarkSetValues-16 3601794 438.6 ns/op 159 B/op 1 allocs/op +BenchmarkSetValues-16 3893646 375.8 ns/op 152 B/op 1 allocs/op +BenchmarkSetValues-16 4121847 372.0 ns/op 147 B/op 1 allocs/op +BenchmarkSetValues-16 4067539 368.1 ns/op 148 B/op 1 allocs/op +BenchmarkSetValues-16 4130247 367.4 ns/op 147 B/op 1 allocs/op +BenchmarkSetValues-16 3752602 376.5 ns/op 155 B/op 1 allocs/op +BenchmarkSetValues-16 3900349 372.1 ns/op 151 B/op 1 allocs/op +BenchmarkSetValues-16 4099213 373.1 ns/op 147 B/op 1 allocs/op +BenchmarkSetValues-16 4141642 371.3 ns/op 146 B/op 1 allocs/op +BenchmarkSetGetValues-16 3391387 406.2 ns/op 117 B/op 1 allocs/op +BenchmarkSetGetValues-16 3330207 389.4 ns/op 118 B/op 1 allocs/op +BenchmarkSetGetValues-16 3308163 381.0 ns/op 118 B/op 1 allocs/op +BenchmarkSetGetValues-16 3349912 383.9 ns/op 118 B/op 1 allocs/op +BenchmarkSetGetValues-16 3350439 382.1 ns/op 118 B/op 1 allocs/op +BenchmarkSetGetValues-16 3322348 384.1 ns/op 118 B/op 1 allocs/op +BenchmarkSetGetValues-16 3327039 384.1 ns/op 118 B/op 1 allocs/op +BenchmarkSetGetValues-16 3311404 385.7 ns/op 118 B/op 1 allocs/op +BenchmarkSetGetValues-16 3230353 402.7 ns/op 119 B/op 1 allocs/op +BenchmarkSetGetValues-16 3298658 387.8 ns/op 118 B/op 1 allocs/op +BenchmarkSetGetRemoveValues-16 8977050 133.6 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetRemoveValues-16 8609907 134.7 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetRemoveValues-16 8937481 133.7 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetRemoveValues-16 8970057 135.2 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetRemoveValues-16 9006141 133.3 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetRemoveValues-16 8969152 134.2 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetRemoveValues-16 8832172 133.9 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetRemoveValues-16 8906787 133.8 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetRemoveValues-16 9071329 136.9 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetRemoveValues-16 8991464 132.3 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 17527305 69.23 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 17328144 68.63 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 17363221 68.14 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 17746070 69.00 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 17612041 69.11 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 17479461 69.11 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 17608268 68.99 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 17113422 67.34 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 17349764 69.17 ns/op 0 B/op 0 allocs/op +BenchmarkSetGetSameKey-16 17481498 68.93 ns/op 0 B/op 0 allocs/op +PASS +ok github.com/zekroTJA/timedmap/v2 621.443s diff --git a/go.mod b/go.mod index e2e4622..8cfd33a 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,11 @@ module github.com/zekroTJA/timedmap/v2 go 1.19 -require ( - github.com/stretchr/testify v1.7.0 - github.com/zekroTJA/timedmap v1.5.2 -) +require github.com/stretchr/testify v1.9.0 require ( - github.com/davecgh/go-spew v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.1.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + github.com/stretchr/objx v0.5.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3ee4ac0..d28e6f9 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,20 @@ 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/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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/zekroTJA/timedmap v1.5.2 h1:5bhWBdyvekLHLrZu8cNJB6iCpIQl4bGaG4HTmPbTNKY= -github.com/zekroTJA/timedmap v1.5.2/go.mod h1:Go4uPxMN1Wjl5IgO6HYD1tM9IQhkYEVqcrrdsI4ljXo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/timedmap.go b/timedmap.go index 49c0992..e0e0b69 100644 --- a/timedmap.go +++ b/timedmap.go @@ -6,14 +6,15 @@ import ( "time" ) +// Callback is a function which can be called when a key-value-pair has expired. type Callback[TVal any] func(value TVal) -// TimedMap contains a map with all key-value pairs, -// and a timer, which cleans the map in the set -// tick durations from expired keys. +// TimedMap is a key-value map with lifetimes attached to values. +// Expired values are removed on access or via a cleanup coroutine, +// which can be enabled via the StartCleanerInternal method. type TimedMap[TKey comparable, TVal any] struct { mtx sync.RWMutex - container map[TKey]*element[TVal] + container map[TKey]*Element[TVal] elementPool *sync.Pool cleanupTickTime time.Duration @@ -22,11 +23,11 @@ type TimedMap[TKey comparable, TVal any] struct { cleanerRunning atomic.Bool } -// element contains the actual value as interface type, +// Element contains the actual value as interface type, // the time when the value expires and an array of -// callbacks, which will be executed when the element +// callbacks, which will be executed when the Element // expires. -type element[TVal any] struct { +type Element[TVal any] struct { value TVal expires time.Time cbs []Callback[TVal] @@ -35,12 +36,12 @@ type element[TVal any] struct { // New creates and returns a new instance of TimedMap. // The passed cleanupTickTime will be passed to the // cleanup ticker, which iterates through the map and -// deletes expired key-value pairs. +// deletes expired key-value pairs on each iteration. // -// Optionally, you can also pass a custom <-chan time.Time +// Optionally, you can also pass a custom <-chan time.Time, // which controls the cleanup cycle if you want to use // a single synchronized timer or if you want to have more -// control over the cleanup loop. +// granular control over the cleanup loop. // // When passing 0 as cleanupTickTime and no tickerChan, // the cleanup loop will not be started. You can call @@ -49,9 +50,12 @@ type element[TVal any] struct { // can also be used to re-define the specification of // the cleanup loop when already running if you want to. func New[TKey comparable, TVal any](cleanupTickTime time.Duration, tickerChan ...<-chan time.Time) *TimedMap[TKey, TVal] { - return newTimedMap[TKey, TVal](make(map[TKey]*element[TVal]), cleanupTickTime, tickerChan) + return newTimedMap[TKey, TVal](make(map[TKey]*Element[TVal]), cleanupTickTime, tickerChan) } +// FromMap creates a new TimedMap containing all entries from +// the passed map m. Each entry will get assigned the passed +// expiration duration. func FromMap[TKey comparable, TVal any]( m map[TKey]TVal, expiration time.Duration, @@ -63,10 +67,10 @@ func FromMap[TKey comparable, TVal any]( } exp := time.Now().Add(expiration) - container := make(map[TKey]*element[TVal]) + container := make(map[TKey]*Element[TVal]) for k, v := range m { - el := &element[TVal]{ + el := &Element[TVal]{ value: v, expires: exp, } @@ -96,7 +100,7 @@ func (tm *TimedMap[TKey, TVal]) GetValue(key TKey) (val TVal, ok bool) { return v.value, true } -// GetExpires returns the expire time of a key-value pair. +// GetExpires returns the expiry time of a key-value pair. // If the key-value pair does not exist in the map or // was expired, this will return an error object. func (tm *TimedMap[TKey, TVal]) GetExpires(key TKey) (time.Time, error) { @@ -107,13 +111,7 @@ func (tm *TimedMap[TKey, TVal]) GetExpires(key TKey) (time.Time, error) { return v.expires, nil } -// SetExpire is deprecated. -// Please use SetExpires instead. -func (tm *TimedMap[TKey, TVal]) SetExpire(key TKey, d time.Duration) error { - return tm.SetExpires(key, d) -} - -// SetExpires sets the expire time for a key-value +// SetExpires sets the expiry time for a key-value // pair to the passed duration. If there is no value // to the key passed , this will return an error. func (tm *TimedMap[TKey, TVal]) SetExpires(key TKey, d time.Duration) error { @@ -132,7 +130,7 @@ func (tm *TimedMap[TKey, TVal]) Remove(key TKey) { tm.remove(key) } -// Refresh extends the expire time for a key-value pair +// Refresh extends the expiry time for a key-value pair // about the passed duration. If there is no value to // the key passed, this will return an error object. func (tm *TimedMap[TKey, TVal]) Refresh(key TKey, d time.Duration) error { @@ -172,7 +170,7 @@ func (tm *TimedMap[TKey, TVal]) StartCleanerInternal(interval time.Duration) { // StartCleanerExternal starts the cleanup loop controlled // by the given initiator channel. This is useful if you // want to have more control over the cleanup loop or if -// you want to sync up multiple timedmaps. +// you want to sync up multiple TimedMaps. // // If the cleanup loop is already running, it will be // stopped and restarted using the new specification. @@ -221,9 +219,9 @@ func (tm *TimedMap[TKey, TVal]) cleanupLoop(tc <-chan time.Time) { } } -// expireElement removes the specified key-value element +// expireElement removes the specified key-value Element // from the map and executes all defined Callback functions -func (tm *TimedMap[TKey, TVal]) expireElement(key TKey, v *element[TVal]) { +func (tm *TimedMap[TKey, TVal]) expireElement(key TKey, v *Element[TVal]) { for _, cb := range v.cbs { cb(v.value) } @@ -232,7 +230,7 @@ func (tm *TimedMap[TKey, TVal]) expireElement(key TKey, v *element[TVal]) { delete(tm.container, key) } -// cleanUp iterates trhough the map and expires all key-value +// cleanUp iterates through the map and expires all key-value // pairs which expire time after the current time func (tm *TimedMap[TKey, TVal]) cleanUp() { now := time.Now() @@ -250,7 +248,7 @@ func (tm *TimedMap[TKey, TVal]) cleanUp() { // set sets the value for a key and section with the // given expiration parameters func (tm *TimedMap[TKey, TVal]) set(key TKey, val TVal, expiresAfter time.Duration, cb ...Callback[TVal]) { - // re-use element when existent on this key + // re-use Element when existent on this key if v := tm.getRaw(key); v != nil { tm.mtx.Lock() defer tm.mtx.Unlock() @@ -263,16 +261,16 @@ func (tm *TimedMap[TKey, TVal]) set(key TKey, val TVal, expiresAfter time.Durati tm.mtx.Lock() defer tm.mtx.Unlock() - v := tm.elementPool.Get().(*element[TVal]) + v := tm.elementPool.Get().(*Element[TVal]) v.value = val v.expires = time.Now().Add(expiresAfter) v.cbs = cb tm.container[key] = v } -// get returns an element object by key and section +// get returns an Element object by key and section // if the value has not already expired -func (tm *TimedMap[TKey, TVal]) get(key TKey) *element[TVal] { +func (tm *TimedMap[TKey, TVal]) get(key TKey) *Element[TVal] { v := tm.getRaw(key) if v == nil { @@ -290,9 +288,9 @@ func (tm *TimedMap[TKey, TVal]) get(key TKey) *element[TVal] { return v } -// getRaw returns the raw element object by key, +// getRaw returns the raw Element object by key, // not depending on expiration time -func (tm *TimedMap[TKey, TVal]) getRaw(key TKey) *element[TVal] { +func (tm *TimedMap[TKey, TVal]) getRaw(key TKey) *Element[TVal] { tm.mtx.RLock() v, ok := tm.container[key] tm.mtx.RUnlock() @@ -304,7 +302,7 @@ func (tm *TimedMap[TKey, TVal]) getRaw(key TKey) *element[TVal] { return v } -// remove removes an element from the map by give back the key +// remove removes an Element from the map by give back the key func (tm *TimedMap[TKey, TVal]) remove(key TKey) { tm.mtx.Lock() defer tm.mtx.Unlock() @@ -358,7 +356,7 @@ func (tm *TimedMap[TKey, TVal]) getSnapshot() (m map[TKey]TVal) { } func newTimedMap[TKey comparable, TVal any]( - container map[TKey]*element[TVal], + container map[TKey]*Element[TVal], cleanupTickTime time.Duration, tickerChan []<-chan time.Time, ) *TimedMap[TKey, TVal] { @@ -367,7 +365,7 @@ func newTimedMap[TKey comparable, TVal any]( cleanerStopChan: make(chan bool), elementPool: &sync.Pool{ New: func() any { - return new(element[TVal]) + return new(Element[TVal]) }, }, }