diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index fc84a03..fc3c563 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [1.13, 1.14, 1.15, ^1.16] + go-version: ["1.19", "1.20", "1.21", "^1.22"] steps: - name: Set up Go 1.x @@ -28,6 +28,9 @@ jobs: run: | go get -v -t -d ./... + - name: Run Tests with Race checks + run: go test -v -timeout 300s -race ./... + - name: Run Tests run: go test -v -timeout 300s -covermode atomic -coverprofile=covprofile ./... diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..30bab2a --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml new file mode 100644 index 0000000..02b915b --- /dev/null +++ b/.idea/git_toolbox_prj.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9fe311b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/timedmap.iml b/.idea/timedmap.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/timedmap.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file 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/README.md b/README.md index 3741829..e57f6a5 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ---
- go get -u github.com/zekroTJA/timedmap + go get -u github.com/zekroTJA/timedmap/v2
--- @@ -21,7 +21,16 @@ This package allows to set values to a map which will expire and disappear after a specified time. -[Here](https://pkg.go.dev/github.com/zekroTJA/timedmap) you can read the docs of this package, generated by pkg.go.dev. +[Here](https://pkg.go.dev/github.com/zekroTJA/timedmap/v2) you can read the docs of this package, generated by pkg.go.dev. + +> [!IMPORTANT] +> The package has been updated to `v2` which will introduce breaking changes to `v1`. +> - The package now requires a minimum Go version of `v1.19`. +> - `TimedMap` and corresponding constructor function now take type parameters for key and value types for improved type safety. +> - Sections have been removed in favor of performance and simplicity of the package. +> - Previously deprecated functions have been removed. +> +> If you experience issues with `v1`, please create an issue with the specific version mentioned. `v1` will still receive updates for bugs and incosistencies alongside `v2`. --- @@ -34,26 +43,37 @@ import ( "log" "time" - "github.com/zekroTJA/timedmap" + "github.com/zekroTJA/timedmap/v2" ) func main() { - // Create a timed map with a cleanup timer interval of 1 second - tm := timedmap.New(1 * time.Second) - // Set value of key "hey" to 213, which will expire after 3 seconds - tm.Set("hey", 213, 3*time.Second) - // Print the value of "hey" + + // Creates a new timed map which scans for + // expired keys every 1 second + tm := timedmap.New[string, int](1 * time.Second) + + // Add a key "hey" with the value 213, which should + // expire after 3 seconds and execute the callback, which + // prints that the key was expired + tm.Set("hey", 213, 3*time.Second, func(v int) { + log.Println("key-value pair of 'hey' has expired") + }) + + // Print key "hey" from timed map printKeyVal(tm, "hey") - // Block the main thread for 5 seconds - // After this time, the key-value pair "hey": 213 has expired + + // Wait for 5 seconds + // During this time the main thread is blocked, the + // key-value pair of "hey" will be expired time.Sleep(5 * time.Second) - // Now, this function should show that there is no key "hey" - // in the map, because it has been expired + + // Printing value of key "hey" wil lfail because the + // key-value pair does not exist anymore printKeyVal(tm, "hey") } -func printKeyVal(tm *timedmap.TimedMap, key interface{}) { - d, ok := tm.GetValue(key).(int) +func printKeyVal(tm *timedmap.TimedMap[string, int], key string) { + d, ok := tm.GetValue(key) if !ok { log.Println("data expired") return @@ -63,7 +83,7 @@ func printKeyVal(tm *timedmap.TimedMap, key interface{}) { } ``` -Further examples, you can find in the [example](examples) directory. +Further examples, you can find in the [examples](examples) directory. If you want to see this package in a practcal use case scenario, please take a look at the rate limiter implementation of the REST API of [myrunes.com](https://myrunes.com), where I have used `timedmap` for storing client-based limiter instances: https://github.com/myrunes/backend/blob/master/internal/ratelimit/ratelimit.go 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/examples/default/default.go b/examples/default/default.go index 8702d87..9e4b0fe 100644 --- a/examples/default/default.go +++ b/examples/default/default.go @@ -4,19 +4,19 @@ import ( "log" "time" - "github.com/zekroTJA/timedmap" + "github.com/zekroTJA/timedmap/v2" ) func main() { // Creates a new timed map which scans for // expired keys every 1 second - tm := timedmap.New(1 * time.Second) + tm := timedmap.New[string, int](1 * time.Second) // Add a key "hey" with the value 213, which should // expire after 3 seconds and execute the callback, which // prints that the key was expired - tm.Set("hey", 213, 3*time.Second, func(v interface{}) { + tm.Set("hey", 213, 3*time.Second, func(v int) { log.Println("key-value pair of 'hey' has expired") }) @@ -33,8 +33,8 @@ func main() { printKeyVal(tm, "hey") } -func printKeyVal(tm *timedmap.TimedMap, key interface{}) { - d, ok := tm.GetValue(key).(int) +func printKeyVal(tm *timedmap.TimedMap[string, int], key string) { + d, ok := tm.GetValue(key) if !ok { log.Println("data expired") return diff --git a/examples/sections/sections.go b/examples/sections/sections.go deleted file mode 100644 index 9738007..0000000 --- a/examples/sections/sections.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "fmt" - "log" - "time" - - "github.com/zekroTJA/timedmap" -) - -type myData struct { - data string -} - -func main() { - - // Creates a new timed map which scans for - // expired keys every 1 second - tm := timedmap.New(1 * time.Second) - - // Get sections 0 and 1 - sec0 := tm.Section(0) - sec1 := tm.Section(1) - - // set value for key 'hey' in section 0 - sec0.Set("hey", 213, 3*time.Second, func(v interface{}) { - log.Println("key-value pair of 'hey' has expired") - }) - - // set value for key 'ho' in section 1 - sec1.Set("ho", &myData{data: "ho"}, 4*time.Second, func(v interface{}) { - log.Println("key-value pair of 'ho' has expired") - }) - - // Print values - printKeyVal(sec0, "hey") - printKeyVal(sec0, "ho") - printKeyVal(sec1, "hey") - printKeyVal(sec1, "ho") - - fmt.Println("-----------------") - fmt.Println("► wait for 5 secs") - - // Wait for 5 seconds - // During this time the main thread is blocked, the - // key-value pairs of "hey" and "ho" will be expired - time.Sleep(5 * time.Second) - - fmt.Println("-----------------") - - // Print values after 5 seconds - printKeyVal(sec0, "hey") - printKeyVal(sec0, "ho") - printKeyVal(sec1, "hey") - printKeyVal(sec1, "ho") -} - -func printKeyVal(s timedmap.Section, key interface{}) { - d := s.GetValue(key) - if d == nil { - log.Printf( - "data expired or section [%d] does not contain a value for '%v'", - s.Ident(), key) - return - } - - log.Printf("[%d]%v = %v\n", s.Ident(), key, d) -} diff --git a/go.mod b/go.mod index 8e5faeb..8cfd33a 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,12 @@ -module github.com/zekroTJA/timedmap +module github.com/zekroTJA/timedmap/v2 -go 1.13 +go 1.19 -require github.com/stretchr/testify v1.7.0 // indirect +require github.com/stretchr/testify v1.9.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // 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 7b4dc41..d28e6f9 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +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/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/section.go b/section.go deleted file mode 100644 index f09e7e0..0000000 --- a/section.go +++ /dev/null @@ -1,136 +0,0 @@ -package timedmap - -import ( - "time" -) - -// Section defines a sectioned access -// wrapper of TimedMap. -type Section interface { - - // Ident returns the current sections identifier - Ident() int - - // Set appends a key-value pair to the map or sets the value of - // a key. expiresAfter sets the expire time after the key-value pair - // will automatically be removed from the map. - Set(key, value interface{}, expiresAfter time.Duration, cb ...callback) - - // GetValue returns an interface of the value of a key in the - // map. The returned value is nil if there is no value to the - // passed key or if the value was expired. - GetValue(key interface{}) interface{} - - // GetExpires returns the expire 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. - GetExpires(key interface{}) (time.Time, error) - - // SetExpires sets the expire time for a key-value - // pair to the passed duration. If there is no value - // to the key passed , this will return an error. - SetExpires(key interface{}, d time.Duration) error - - // Contains returns true, if the key exists in the map. - // false will be returned, if there is no value to the - // key or if the key-value pair was expired. - Contains(key interface{}) bool - - // Remove deletes a key-value pair in the map. - Remove(key interface{}) - - // Refresh extends the expire time for a key-value pair - // about the passed duration. If there is no value to - // the key passed, this will return an error. - Refresh(key interface{}, d time.Duration) error - - // Flush deletes all key-value pairs of the section - // in the map. - Flush() - - // Size returns the current number of key-value pairs - // existent in the section of the map. - Size() (i int) - - // Snapshot returns a new map which represents the - // current key-value state of the internal container. - Snapshot() map[interface{}]interface{} -} - -// section wraps access to a specific -// section of the map. -type section struct { - tm *TimedMap - sec int -} - -// newSection creates a new Section instance -// wrapping the given TimedMap instance and -// section identifier. -func newSection(tm *TimedMap, sec int) *section { - return §ion{ - tm: tm, - sec: sec, - } -} - -func (s *section) Ident() int { - return s.sec -} - -func (s *section) Set(key, value interface{}, expiresAfter time.Duration, cb ...callback) { - s.tm.set(key, s.sec, value, expiresAfter, cb...) -} - -func (s *section) GetValue(key interface{}) interface{} { - v := s.tm.get(key, s.sec) - if v == nil { - return nil - } - return v.value -} - -func (s *section) GetExpires(key interface{}) (time.Time, error) { - v := s.tm.get(key, s.sec) - if v == nil { - return time.Time{}, ErrKeyNotFound - } - return v.expires, nil -} - -func (s *section) SetExpires(key interface{}, d time.Duration) error { - return s.tm.setExpires(key, s.sec, d) -} - -func (s *section) Contains(key interface{}) bool { - return s.tm.get(key, s.sec) != nil -} - -func (s *section) Remove(key interface{}) { - s.tm.remove(key, s.sec) -} - -func (s *section) Refresh(key interface{}, d time.Duration) error { - return s.tm.refresh(key, s.sec, d) -} - -func (s *section) Flush() { - for k := range s.tm.container { - if k.sec == s.sec { - s.tm.remove(k.key, k.sec) - } - } -} - -func (s *section) Size() (i int) { - for k := range s.tm.container { - if k.sec == s.sec { - i++ - } - } - return -} - -func (s *section) Snapshot() map[interface{}]interface{} { - return s.tm.getSnapshot(s.sec) -} diff --git a/section_test.go b/section_test.go deleted file mode 100644 index a5398e6..0000000 --- a/section_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package timedmap - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestSectionFlush(t *testing.T) { - tm := New(dCleanupTick) - - for i := 0; i < 5; i++ { - tm.set(i, 0, 1, time.Hour) - } - for i := 0; i < 10; i++ { - tm.set(i, 1, 1, time.Hour) - } - for i := 0; i < 12; i++ { - tm.set(i, 2, 1, time.Hour) - } - tm.Section(2).Flush() - assert.EqualValues(t, 15, len(tm.container)) - - tm.Section(1).Flush() - assert.EqualValues(t, 5, len(tm.container)) - - tm.Section(0).Flush() - assert.EqualValues(t, 0, len(tm.container)) -} - -func TestSectionIdent(t *testing.T) { - tm := New(dCleanupTick) - - assert.EqualValues(t, 1, tm.Section(1).Ident()) - assert.EqualValues(t, 2, tm.Section(2).Ident()) - assert.EqualValues(t, 3, tm.Section(3).Ident()) -} - -func TestSectionSet(t *testing.T) { - const key = "tKeySet" - const val = "tValSet" - const sec = 1 - - tm := New(dCleanupTick) - - s := tm.Section(sec) - - s.Set(key, val, 20*time.Millisecond) - assert.Equal(t, val, tm.get(key, sec).value) - - time.Sleep(40 * time.Millisecond) - assert.Nil(t, tm.get(key, sec)) -} - -func TestSectionGetValue(t *testing.T) { - const key = "tKeyGetVal" - const val = "tValGetVal" - const sec = 1 - - tm := New(dCleanupTick) - - s := tm.Section(sec) - - s.Set(key, val, 50*time.Millisecond) - - assert.Nil(t, s.GetValue("keyNotExists")) - - assert.Equal(t, val, s.GetValue(key)) - - time.Sleep(60 * time.Millisecond) - - assert.Nil(t, s.GetValue(key)) - - s.Set(key, val, 1*time.Microsecond) - time.Sleep(2 * time.Millisecond) - assert.Nil(t, s.GetValue(key)) -} - -func TestSectionGetExpire(t *testing.T) { - const key = "tKeyGetExp" - const val = "tValGetExp" - const sec = 1 - - tm := New(dCleanupTick) - - s := tm.Section(sec) - - s.Set(key, val, 50*time.Millisecond) - ct := time.Now().Add(50 * time.Millisecond) - - _, err := s.GetExpires("keyNotExists") - assert.ErrorIs(t, err, ErrKeyNotFound) - - exp, err := s.GetExpires(key) - assert.Nil(t, err) - assert.Less(t, ct.Sub(exp), 1*time.Millisecond) - - tm.Flush() -} - -func TestSectionSetExpires(t *testing.T) { - const key = "tKeyRef" - const sec = 1 - - tm := New(dCleanupTick) - - s := tm.Section(sec) - - if err := tm.SetExpires("notExistentKey", 1*time.Second); err != ErrKeyNotFound { - t.Errorf("returned error should have been '%s', but was '%s'", - ErrKeyNotFound.Error(), err.Error()) - } - - s.Set(key, 1, 12*time.Millisecond) - assert.Nil(t, s.SetExpires(key, 50*time.Millisecond)) - - time.Sleep(30 * time.Millisecond) - assert.NotNil(t, tm.get(key, sec)) - - time.Sleep(51 * time.Millisecond) - assert.Nil(t, tm.get(key, sec)) -} - -func TestSectionContains(t *testing.T) { - const key = "tKeyCont" - const sec = 1 - - tm := New(dCleanupTick) - - s := tm.Section(sec) - - s.Set(key, 1, 30*time.Millisecond) - - assert.False(t, s.Contains("keyNotExists")) - assert.True(t, s.Contains(key)) - - time.Sleep(50 * time.Millisecond) - assert.False(t, s.Contains(key)) -} - -func TestSectionRemove(t *testing.T) { - const key = "tKeyRem" - const sec = 1 - - tm := New(dCleanupTick) - - s := tm.Section(sec) - - s.Set(key, 1, time.Hour) - s.Remove(key) - assert.Nil(t, tm.get(key, sec)) -} - -func TestSectionRefresh(t *testing.T) { - const key = "tKeyRef" - const sec = 1 - - tm := New(dCleanupTick) - - s := tm.Section(sec) - - assert.ErrorIs(t, s.Refresh("keyNotExists", time.Hour), ErrKeyNotFound) - - s.Set(key, 1, 12*time.Millisecond) - assert.Nil(t, s.Refresh(key, 50*time.Millisecond)) - - time.Sleep(30 * time.Millisecond) - assert.NotNil(t, tm.get(key, sec)) - - time.Sleep(100 * time.Millisecond) - assert.Nil(t, tm.get(key, sec)) -} - -func TestSectionSize(t *testing.T) { - tm := New(dCleanupTick) - - for i := 0; i < 20; i++ { - tm.set(i, 0, 1, 50*time.Millisecond) - } - for i := 0; i < 25; i++ { - tm.set(i, 1, 1, 50*time.Millisecond) - } - assert.EqualValues(t, 25, tm.Section(1).Size()) -} - -func TestSectionCallback(t *testing.T) { - cb := new(CB) - cb.On("Cb").Return() - - tm := New(dCleanupTick) - - tm.Section(1).Set(1, 3, 25*time.Millisecond, cb.Cb) - - time.Sleep(50 * time.Millisecond) - assert.Nil(t, tm.get(1, 0)) - cb.AssertCalled(t, "Cb") - assert.EqualValues(t, 3, cb.TestData().Get("v").Int()) -} - -func TestSectionSnapshot(t *testing.T) { - tm := New(1 * time.Minute) - - for i := 0; i < 10; i++ { - tm.set(i, i%2, i, 1*time.Minute) - } - - m := tm.Section(1).Snapshot() - - assert.Len(t, m, 5) - for i := 0; i < 10; i++ { - if i%2 == 1 { - assert.EqualValues(t, i, m[i]) - } else { - assert.Nil(t, m[i]) - } - } -} diff --git a/timedmap.go b/timedmap.go index b74a860..e0e0b69 100644 --- a/timedmap.go +++ b/timedmap.go @@ -1,52 +1,47 @@ package timedmap import ( - "reflect" "sync" "sync/atomic" "time" ) -type callback func(value interface{}) +// 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. -type TimedMap struct { +// 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[keyWrap]*element + container map[TKey]*Element[TVal] elementPool *sync.Pool cleanupTickTime time.Duration cleanerTicker *time.Ticker cleanerStopChan chan bool - cleanerRunning *uint32 + cleanerRunning atomic.Bool } -type keyWrap struct { - sec int - key interface{} -} - -// element contains the actual value as interface type, -// the thime when the value expires and an array of -// callbacks, which will be executed when the element +// 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 // expires. -type element struct { - value interface{} +type Element[TVal any] struct { + value TVal expires time.Time - cbs []callback + cbs []Callback[TVal] } // 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 syncronyzed timer or if you want to have more -// control over the cleanup loop. +// a single synchronized timer or if you want to have more +// granular control over the cleanup loop. // // When passing 0 as cleanupTickTime and no tickerChan, // the cleanup loop will not be started. You can call @@ -54,124 +49,96 @@ type element struct { // manually start the cleanup loop. These both methods // can also be used to re-define the specification of // the cleanup loop when already running if you want to. -func New(cleanupTickTime time.Duration, tickerChan ...<-chan time.Time) *TimedMap { - return newTimedMap(make(map[keyWrap]*element), cleanupTickTime, tickerChan) +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) } -func FromMap( - m interface{}, +// 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, cleanupTickTime time.Duration, tickerChan ...<-chan time.Time, -) (*TimedMap, error) { - mv := reflect.ValueOf(m) - if mv.Kind() != reflect.Map { +) (*TimedMap[TKey, TVal], error) { + if m == nil { return nil, ErrValueNoMap } exp := time.Now().Add(expiration) - container := make(map[keyWrap]*element) - - iter := mv.MapRange() - for iter.Next() { - key := iter.Key() - val := iter.Value() - kw := keyWrap{ - sec: 0, - key: key.Interface(), - } - el := &element{ - value: val.Interface(), + container := make(map[TKey]*Element[TVal]) + + for k, v := range m { + el := &Element[TVal]{ + value: v, expires: exp, } - container[kw] = el + container[k] = el } return newTimedMap(container, cleanupTickTime, tickerChan), nil } -// Section returns a sectioned subset of -// the timed map with the given section -// identifier i. -func (tm *TimedMap) Section(i int) Section { - if i == 0 { - return tm - } - return newSection(tm, i) -} - -// Ident returns the current sections ident. -// In the case of the root object TimedMap, -// this is always 0. -func (tm *TimedMap) Ident() int { - return 0 -} - // Set appends a key-value pair to the map or sets the value of -// a key. expiresAfter sets the expire time after the key-value pair +// a key. expiresAfter sets the expiry time after the key-value pair // will automatically be removed from the map. -func (tm *TimedMap) Set(key, value interface{}, expiresAfter time.Duration, cb ...callback) { - tm.set(key, 0, value, expiresAfter, cb...) +func (tm *TimedMap[TKey, TVal]) Set(key TKey, value TVal, expiresAfter time.Duration, cb ...Callback[TVal]) { + tm.set(key, value, expiresAfter, cb...) } // GetValue returns an interface of the value of a key in the // map. The returned value is nil if there is no value to the // passed key or if the value was expired. -func (tm *TimedMap) GetValue(key interface{}) interface{} { - v := tm.get(key, 0) +func (tm *TimedMap[TKey, TVal]) GetValue(key TKey) (val TVal, ok bool) { + v := tm.get(key) if v == nil { - return nil + return val, false } tm.mtx.RLock() defer tm.mtx.RUnlock() - return v.value + 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) GetExpires(key interface{}) (time.Time, error) { - v := tm.get(key, 0) +func (tm *TimedMap[TKey, TVal]) GetExpires(key TKey) (time.Time, error) { + v := tm.get(key) if v == nil { return time.Time{}, ErrKeyNotFound } return v.expires, nil } -// SetExpire is deprecated. -// Please use SetExpires instead. -func (tm *TimedMap) SetExpire(key interface{}, 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) SetExpires(key interface{}, d time.Duration) error { - return tm.setExpires(key, 0, d) +func (tm *TimedMap[TKey, TVal]) SetExpires(key TKey, d time.Duration) error { + return tm.setExpires(key, d) } // Contains returns true, if the key exists in the map. // false will be returned, if there is no value to the // key or if the key-value pair was expired. -func (tm *TimedMap) Contains(key interface{}) bool { - return tm.get(key, 0) != nil +func (tm *TimedMap[TKey, TVal]) Contains(key TKey) bool { + return tm.get(key) != nil } // Remove deletes a key-value pair in the map. -func (tm *TimedMap) Remove(key interface{}) { - tm.remove(key, 0) +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) Refresh(key interface{}, d time.Duration) error { - return tm.refresh(key, 0, d) +func (tm *TimedMap[TKey, TVal]) Refresh(key TKey, d time.Duration) error { + return tm.refresh(key, d) } // Flush deletes all key-value pairs of the map. -func (tm *TimedMap) Flush() { +func (tm *TimedMap[TKey, TVal]) Flush() { tm.mtx.Lock() defer tm.mtx.Unlock() @@ -183,7 +150,7 @@ func (tm *TimedMap) Flush() { // Size returns the current number of key-value pairs // existent in the map. -func (tm *TimedMap) Size() int { +func (tm *TimedMap[TKey, TVal]) Size() int { return len(tm.container) } @@ -192,8 +159,8 @@ func (tm *TimedMap) Size() int { // // If the cleanup loop is already running, it will be // stopped and restarted using the new specification. -func (tm *TimedMap) StartCleanerInternal(interval time.Duration) { - if atomic.LoadUint32(tm.cleanerRunning) != 0 { +func (tm *TimedMap[TKey, TVal]) StartCleanerInternal(interval time.Duration) { + if tm.cleanerRunning.Load() { tm.StopCleaner() } tm.cleanerTicker = time.NewTicker(interval) @@ -203,12 +170,12 @@ func (tm *TimedMap) 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. -func (tm *TimedMap) StartCleanerExternal(initiator <-chan time.Time) { - if atomic.LoadUint32(tm.cleanerRunning) != 0 { +func (tm *TimedMap[TKey, TVal]) StartCleanerExternal(initiator <-chan time.Time) { + if tm.cleanerRunning.Load() { tm.StopCleaner() } go tm.cleanupLoop(initiator) @@ -218,8 +185,8 @@ func (tm *TimedMap) StartCleanerExternal(initiator <-chan time.Time) { // This should always be called after exiting a scope // where TimedMap is used that the data can be cleaned // up correctly. -func (tm *TimedMap) StopCleaner() { - if atomic.LoadUint32(tm.cleanerRunning) == 0 { +func (tm *TimedMap[TKey, TVal]) StopCleaner() { + if !tm.cleanerRunning.Load() { return } tm.cleanerStopChan <- true @@ -230,16 +197,16 @@ func (tm *TimedMap) StopCleaner() { // Snapshot returns a new map which represents the // current key-value state of the internal container. -func (tm *TimedMap) Snapshot() map[interface{}]interface{} { - return tm.getSnapshot(0) +func (tm *TimedMap[TKey, TVal]) Snapshot() map[TKey]TVal { + return tm.getSnapshot() } // cleanupLoop holds the loop executing the cleanup // when initiated by tc. -func (tm *TimedMap) cleanupLoop(tc <-chan time.Time) { - atomic.StoreUint32(tm.cleanerRunning, 1) +func (tm *TimedMap[TKey, TVal]) cleanupLoop(tc <-chan time.Time) { + tm.cleanerRunning.Store(true) defer func() { - atomic.StoreUint32(tm.cleanerRunning, 0) + tm.cleanerRunning.Store(false) }() for { @@ -252,25 +219,20 @@ func (tm *TimedMap) cleanupLoop(tc <-chan time.Time) { } } -// expireElement removes the specified key-value element -// from the map and executes all defined callback functions -func (tm *TimedMap) expireElement(key interface{}, sec int, v *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]) { for _, cb := range v.cbs { cb(v.value) } - k := keyWrap{ - sec: sec, - key: key, - } - tm.elementPool.Put(v) - delete(tm.container, k) + 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) cleanUp() { +func (tm *TimedMap[TKey, TVal]) cleanUp() { now := time.Now() tm.mtx.Lock() @@ -278,16 +240,16 @@ func (tm *TimedMap) cleanUp() { for k, v := range tm.container { if now.After(v.expires) { - tm.expireElement(k.key, k.sec, v) + tm.expireElement(k, v) } } } // set sets the value for a key and section with the // given expiration parameters -func (tm *TimedMap) set(key interface{}, sec int, val interface{}, expiresAfter time.Duration, cb ...callback) { - // re-use element when existent on this key - if v := tm.getRaw(key, sec); v != nil { +func (tm *TimedMap[TKey, TVal]) set(key TKey, val TVal, expiresAfter time.Duration, cb ...Callback[TVal]) { + // re-use Element when existent on this key + if v := tm.getRaw(key); v != nil { tm.mtx.Lock() defer tm.mtx.Unlock() v.value = val @@ -296,25 +258,20 @@ func (tm *TimedMap) set(key interface{}, sec int, val interface{}, expiresAfter return } - k := keyWrap{ - sec: sec, - key: key, - } - tm.mtx.Lock() defer tm.mtx.Unlock() - v := tm.elementPool.Get().(*element) + v := tm.elementPool.Get().(*Element[TVal]) v.value = val v.expires = time.Now().Add(expiresAfter) v.cbs = cb - tm.container[k] = v + 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) get(key interface{}, sec int) *element { - v := tm.getRaw(key, sec) +func (tm *TimedMap[TKey, TVal]) get(key TKey) *Element[TVal] { + v := tm.getRaw(key) if v == nil { return nil @@ -324,23 +281,18 @@ func (tm *TimedMap) get(key interface{}, sec int) *element { defer tm.mtx.Unlock() if time.Now().After(v.expires) { - tm.expireElement(key, sec, v) + tm.expireElement(key, v) return nil } 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) getRaw(key interface{}, sec int) *element { - k := keyWrap{ - sec: sec, - key: key, - } - +func (tm *TimedMap[TKey, TVal]) getRaw(key TKey) *Element[TVal] { tm.mtx.RLock() - v, ok := tm.container[k] + v, ok := tm.container[key] tm.mtx.RUnlock() if !ok { @@ -350,30 +302,24 @@ func (tm *TimedMap) getRaw(key interface{}, sec int) *element { return v } -// remove removes an element from the map by giveb -// key and section -func (tm *TimedMap) remove(key interface{}, sec int) { - k := keyWrap{ - sec: sec, - key: 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() - v, ok := tm.container[k] + v, ok := tm.container[key] if !ok { return } tm.elementPool.Put(v) - delete(tm.container, k) + delete(tm.container, key) } // refresh extends the lifetime of the given key in the // given section by the duration d. -func (tm *TimedMap) refresh(key interface{}, sec int, d time.Duration) error { - v := tm.get(key, sec) +func (tm *TimedMap[TKey, TVal]) refresh(key TKey, d time.Duration) error { + v := tm.get(key) if v == nil { return ErrKeyNotFound } @@ -385,8 +331,8 @@ func (tm *TimedMap) refresh(key interface{}, sec int, d time.Duration) error { // setExpires sets the lifetime of the given key in the // given section to the duration d. -func (tm *TimedMap) setExpires(key interface{}, sec int, d time.Duration) error { - v := tm.get(key, sec) +func (tm *TimedMap[TKey, TVal]) setExpires(key TKey, d time.Duration) error { + v := tm.get(key) if v == nil { return ErrKeyNotFound } @@ -396,33 +342,30 @@ func (tm *TimedMap) setExpires(key interface{}, sec int, d time.Duration) error return nil } -func (tm *TimedMap) getSnapshot(sec int) (m map[interface{}]interface{}) { - m = make(map[interface{}]interface{}) +func (tm *TimedMap[TKey, TVal]) getSnapshot() (m map[TKey]TVal) { + m = make(map[TKey]TVal) tm.mtx.RLock() defer tm.mtx.RUnlock() for k, v := range tm.container { - if k.sec == sec { - m[k.key] = v.value - } + m[k] = v.value } return } -func newTimedMap( - container map[keyWrap]*element, +func newTimedMap[TKey comparable, TVal any]( + container map[TKey]*Element[TVal], cleanupTickTime time.Duration, tickerChan []<-chan time.Time, -) *TimedMap { - tm := &TimedMap{ +) *TimedMap[TKey, TVal] { + tm := &TimedMap[TKey, TVal]{ container: container, - cleanerRunning: new(uint32), cleanerStopChan: make(chan bool), elementPool: &sync.Pool{ - New: func() interface{} { - return new(element) + New: func() any { + return new(Element[TVal]) }, }, } diff --git a/timedmap_test.go b/timedmap_test.go index 2c63d6e..e1d7b4c 100644 --- a/timedmap_test.go +++ b/timedmap_test.go @@ -2,7 +2,6 @@ package timedmap import ( "sync" - "sync/atomic" "testing" "time" @@ -15,28 +14,33 @@ const ( ) func TestNew(t *testing.T) { - tm := New(dCleanupTick) + tm := New[int, int](dCleanupTick) assert.NotNil(t, tm) assert.EqualValues(t, 0, len(tm.container)) time.Sleep(10 * time.Millisecond) - assert.True(t, atomic.LoadUint32(tm.cleanerRunning) != 0) + assert.True(t, tm.cleanerRunning.Load()) } func TestFromMap(t *testing.T) { t.Run("map-string-string", func(t *testing.T) { tm, err := FromMap( - map[string]string{"foo": "bar", "bazz": "fuzz"}, + map[string]string{"foo": "bar", "baz": "fuzz"}, 200*time.Millisecond, 10*time.Millisecond) assert.Nil(t, err) - assert.EqualValues(t, "bar", tm.GetValue("foo")) - assert.EqualValues(t, "fuzz", tm.GetValue("bazz")) + v, ok := tm.GetValue("foo") + assert.True(t, ok) + assert.EqualValues(t, "bar", v) + + v, ok = tm.GetValue("baz") + assert.True(t, ok) + assert.EqualValues(t, "fuzz", v) time.Sleep(500 * time.Millisecond) assert.False(t, tm.Contains("foo")) - assert.False(t, tm.Contains("bazz")) + assert.False(t, tm.Contains("baz")) }) t.Run("map-int-interface", func(t *testing.T) { @@ -45,102 +49,81 @@ func TestFromMap(t *testing.T) { 200*time.Millisecond, 10*time.Millisecond) assert.Nil(t, err) - assert.EqualValues(t, "foo", tm.GetValue(1)) - assert.EqualValues(t, 3.456, tm.GetValue(2)) - - time.Sleep(500 * time.Millisecond) - - assert.False(t, tm.Contains(1)) - assert.False(t, tm.Contains(2)) - }) - - t.Run("map-interface-interface", func(t *testing.T) { - tm, err := FromMap( - map[interface{}]interface{}{1: "foo", "a": 3.456}, - 200*time.Millisecond, 10*time.Millisecond) - assert.Nil(t, err) + v, ok := tm.GetValue(1) + assert.True(t, ok) + assert.EqualValues(t, "foo", v) - assert.EqualValues(t, "foo", tm.GetValue(1)) - assert.EqualValues(t, 3.456, tm.GetValue("a")) + v, ok = tm.GetValue(2) + assert.True(t, ok) + assert.EqualValues(t, 3.456, v) time.Sleep(500 * time.Millisecond) assert.False(t, tm.Contains(1)) - assert.False(t, tm.Contains("a")) - }) - - t.Run("non-map", func(t *testing.T) { - _, err := FromMap( - "this is not a map", - 200*time.Millisecond, 10*time.Millisecond) - assert.ErrorIs(t, err, ErrValueNoMap) - - _, err = FromMap( - nil, - 200*time.Millisecond, 10*time.Millisecond) - assert.ErrorIs(t, err, ErrValueNoMap) + assert.False(t, tm.Contains(2)) }) } func TestFlush(t *testing.T) { - tm := New(dCleanupTick) + tm := New[int, int](dCleanupTick) for i := 0; i < 10; i++ { - tm.set(i, 0, 1, time.Hour) + tm.set(i, 1, time.Hour) } assert.EqualValues(t, 10, len(tm.container)) tm.Flush() assert.EqualValues(t, 0, len(tm.container)) } -func TestIdent(t *testing.T) { - tm := New(dCleanupTick) - assert.EqualValues(t, 0, tm.Ident()) -} - func TestSet(t *testing.T) { const key = "tKeySet" const val = "tValSet" - tm := New(dCleanupTick) + tm := New[string, string](dCleanupTick) tm.Set(key, val, 20*time.Millisecond) - if v := tm.get(key, 0); v == nil { + if v := tm.get(key); v == nil { t.Fatal("key was not set") - } else if v.value.(string) != val { + } else if v.value != val { t.Fatal("value was not like set") } - assert.Equal(t, val, tm.get(key, 0).value) + assert.Equal(t, val, tm.get(key).value) time.Sleep(40 * time.Millisecond) - assert.Nil(t, tm.get(key, 0)) + assert.Nil(t, tm.get(key)) } func TestGetValue(t *testing.T) { const key = "tKeyGetVal" const val = "tValGetVal" - tm := New(dCleanupTick) + tm := New[string, string](dCleanupTick) tm.Set(key, val, 50*time.Millisecond) - assert.Nil(t, tm.GetValue("keyNotExists")) + _, ok := tm.GetValue("keyNotExists") + assert.False(t, ok) - assert.Equal(t, val, tm.GetValue(key)) + v, ok := tm.GetValue(key) + assert.True(t, ok) + assert.Equal(t, val, v) time.Sleep(60 * time.Millisecond) - assert.Nil(t, tm.GetValue(key)) + _, ok = tm.GetValue(key) + assert.False(t, ok) tm.Set(key, val, 1*time.Microsecond) time.Sleep(2 * time.Millisecond) - assert.Nil(t, tm.GetValue(key)) + + _, ok = tm.GetValue(key) + assert.False(t, ok) } func TestGetExpire(t *testing.T) { const key = "tKeyGetExp" const val = "tValGetExp" - tm := New(dCleanupTick) + tm := New[string, string](dCleanupTick) tm.Set(key, val, 50*time.Millisecond) ct := time.Now().Add(50 * time.Millisecond) @@ -156,7 +139,7 @@ func TestGetExpire(t *testing.T) { func TestSetExpires(t *testing.T) { const key = "tKeyRef" - tm := New(dCleanupTick) + tm := New[string, int](dCleanupTick) err := tm.Refresh("keyNotExists", time.Hour) assert.ErrorIs(t, err, ErrKeyNotFound) @@ -169,16 +152,16 @@ func TestSetExpires(t *testing.T) { assert.Nil(t, err) time.Sleep(30 * time.Millisecond) - assert.NotNil(t, tm.get(key, 0)) + assert.NotNil(t, tm.get(key)) time.Sleep(52 * time.Millisecond) - assert.Nil(t, tm.get(key, 0)) + assert.Nil(t, tm.get(key)) } func TestContains(t *testing.T) { const key = "tKeyCont" - tm := New(dCleanupTick) + tm := New[string, int](dCleanupTick) tm.Set(key, 1, 30*time.Millisecond) @@ -192,18 +175,18 @@ func TestContains(t *testing.T) { func TestRemove(t *testing.T) { const key = "tKeyRem" - tm := New(dCleanupTick) + tm := New[string, int](dCleanupTick) tm.Set(key, 1, time.Hour) tm.Remove(key) - assert.Nil(t, tm.get(key, 0)) + assert.Nil(t, tm.get(key)) } func TestRefresh(t *testing.T) { const key = "tKeyRef" - tm := New(dCleanupTick) + tm := New[string, int](dCleanupTick) err := tm.Refresh("keyNotExists", time.Hour) assert.ErrorIs(t, err, ErrKeyNotFound) @@ -212,14 +195,14 @@ func TestRefresh(t *testing.T) { assert.Nil(t, tm.Refresh(key, 50*time.Millisecond)) time.Sleep(30 * time.Millisecond) - assert.NotNil(t, tm.get(key, 0)) + assert.NotNil(t, tm.get(key)) time.Sleep(100 * time.Millisecond) - assert.Nil(t, tm.get(key, 0)) + assert.Nil(t, tm.get(key)) } func TestSize(t *testing.T) { - tm := New(dCleanupTick) + tm := New[int, int](dCleanupTick) for i := 0; i < 25; i++ { tm.Set(i, 1, 50*time.Millisecond) @@ -228,26 +211,26 @@ func TestSize(t *testing.T) { } func TestCallback(t *testing.T) { - cb := new(CB) + cb := new(CB[int]) cb.On("Cb").Return() - tm := New(dCleanupTick) + tm := New[int, int](dCleanupTick) tm.Set(1, 3, 25*time.Millisecond, cb.Cb) time.Sleep(50 * time.Millisecond) - assert.Nil(t, tm.get(1, 0)) + assert.Nil(t, tm.get(1)) cb.AssertCalled(t, "Cb") assert.EqualValues(t, 3, cb.TestData().Get("v").Int()) } func TestStopCleaner(t *testing.T) { - tm := New(dCleanupTick) + tm := New[string, int](dCleanupTick) time.Sleep(10 * time.Millisecond) tm.StopCleaner() time.Sleep(10 * time.Millisecond) - assert.False(t, atomic.LoadUint32(tm.cleanerRunning) != 0) + assert.False(t, tm.cleanerRunning.Load()) assert.NotPanics(t, func() { tm.StopCleaner() @@ -257,29 +240,29 @@ func TestStopCleaner(t *testing.T) { func TestStartCleanerInternal(t *testing.T) { // Test functionality { - tm := New(0) + tm := New[int, int](0) time.Sleep(10 * time.Millisecond) - assert.False(t, atomic.LoadUint32(tm.cleanerRunning) != 0) + assert.False(t, tm.cleanerRunning.Load()) // Ensure cleanup timer is not running - tm.set(1, 0, 1, 0) + tm.set(1, 1, 0) time.Sleep(100 * time.Millisecond) - assert.EqualValues(t, 1, tm.getRaw(1, 0).value) + assert.EqualValues(t, 1, tm.getRaw(1).value) tm.StartCleanerInternal(dCleanupTick) time.Sleep(10 * time.Millisecond) - assert.True(t, atomic.LoadUint32(tm.cleanerRunning) != 0) + assert.True(t, tm.cleanerRunning.Load()) // Ensure cleanup timer is running - tm.set(1, 0, 1, 0) + tm.set(1, 0, 1) time.Sleep(100 * time.Millisecond) - assert.Nil(t, tm.getRaw(1, 0)) + assert.Nil(t, tm.getRaw(1)) } // Test ticker overwrite and cleaner stop { - tm := New(dCleanupTick) + tm := New[int, int](dCleanupTick) time.Sleep(10 * time.Millisecond) oldTicker := tm.cleanerTicker @@ -292,56 +275,56 @@ func TestStartCleanerInternal(t *testing.T) { func TestStartCleanerExternal(t *testing.T) { // Test functionality { - tm := New(0) + tm := New[int, int](0) time.Sleep(10 * time.Millisecond) - assert.False(t, atomic.LoadUint32(tm.cleanerRunning) != 0) + assert.False(t, tm.cleanerRunning.Load()) // Ensure cleanup timer is not running - tm.set(1, 0, 1, 0) + tm.set(1, 1, 0) time.Sleep(100 * time.Millisecond) - assert.EqualValues(t, 1, tm.getRaw(1, 0).value) + assert.EqualValues(t, 1, tm.getRaw(1).value) c := make(chan time.Time) tm.StartCleanerExternal(c) time.Sleep(10 * time.Millisecond) - assert.True(t, atomic.LoadUint32(tm.cleanerRunning) != 0) + assert.True(t, tm.cleanerRunning.Load()) // Ensure cleanup is controlled by c - tm.set(1, 0, 1, 0) + tm.set(1, 0, 1) time.Sleep(100 * time.Millisecond) - assert.NotNil(t, tm.getRaw(1, 0)) + assert.NotNil(t, tm.getRaw(1)) // Ensure cleanup is controlled by c c <- time.Now() time.Sleep(10 * time.Millisecond) - assert.Nil(t, tm.getRaw(1, 0)) + assert.Nil(t, tm.getRaw(1)) } // Ensure timer overwrite { - tm := New(dCleanupTick) + tm := New[int, int](dCleanupTick) time.Sleep(10 * time.Millisecond) - assert.True(t, atomic.LoadUint32(tm.cleanerRunning) != 0) + assert.True(t, tm.cleanerRunning.Load()) assert.NotNil(t, tm.cleanerTicker) c := make(chan time.Time) tm.StartCleanerExternal(c) // Ensure cleanup is controlled by c - tm.set(1, 0, 1, 0) + tm.set(1, 0, 1) time.Sleep(100 * time.Millisecond) - assert.NotNil(t, tm.getRaw(1, 0)) + assert.NotNil(t, tm.getRaw(1)) } } func TestSnapshot(t *testing.T) { - tm := New(1 * time.Minute) + tm := New[int, int](1 * time.Minute) for i := 0; i < 10; i++ { - tm.set(i, 0, i, 1*time.Minute) + tm.set(i, i, 1*time.Minute) } m := tm.Snapshot() @@ -353,7 +336,7 @@ func TestSnapshot(t *testing.T) { } func TestConcurrentReadWrite(t *testing.T) { - tm := New(dCleanupTick) + tm := New[int, int](dCleanupTick) go func() { for { @@ -370,7 +353,8 @@ func TestConcurrentReadWrite(t *testing.T) { go func() { for { for i := 0; i < 100; i++ { - v := tm.GetValue(i) + v, ok := tm.GetValue(i) + assert.True(t, ok) assert.EqualValues(t, i, v) } } @@ -380,7 +364,7 @@ func TestConcurrentReadWrite(t *testing.T) { } func TestGetExpiredConcurrent(t *testing.T) { - tm := New(dCleanupTick) + tm := New[int, int](dCleanupTick) wg := sync.WaitGroup{} for i := 0; i < 50000; i++ { @@ -405,25 +389,25 @@ func TestExternalTicker(t *testing.T) { const val = "tValSet" ticker := time.NewTicker(dCleanupTick) - tm := New(0, ticker.C) + tm := New[string, string](0, ticker.C) tm.Set(key, val, 20*time.Millisecond) - assert.Equal(t, val, tm.get(key, 0).value) + assert.Equal(t, val, tm.get(key).value) time.Sleep(40 * time.Millisecond) - assert.Nil(t, tm.get(key, 0)) + assert.Nil(t, tm.get(key)) } func TestBeforeCleanup(t *testing.T) { const key, value = 1, 2 - tm := New(1 * time.Hour) + tm := New[int, int](1 * time.Hour) tm.Set(key, value, 5*time.Millisecond) time.Sleep(10 * time.Millisecond) - _, ok := tm.GetValue(key).(int) + _, ok := tm.GetValue(key) assert.False(t, ok) } @@ -431,14 +415,14 @@ func TestBeforeCleanup(t *testing.T) { // --- BENCHMARKS --- func BenchmarkSetValues(b *testing.B) { - tm := New(1 * time.Minute) + tm := New[int, int](1 * time.Minute) for n := 0; n < b.N; n++ { tm.Set(n, n, 1*time.Hour) } } func BenchmarkSetGetValues(b *testing.B) { - tm := New(1 * time.Minute) + tm := New[int, int](1 * time.Minute) for n := 0; n < b.N; n++ { tm.Set(n, n, 1*time.Hour) tm.GetValue(n) @@ -446,7 +430,7 @@ func BenchmarkSetGetValues(b *testing.B) { } func BenchmarkSetGetRemoveValues(b *testing.B) { - tm := New(1 * time.Minute) + tm := New[int, int](1 * time.Minute) for n := 0; n < b.N; n++ { tm.Set(n, n, 1*time.Hour) tm.GetValue(n) @@ -455,7 +439,7 @@ func BenchmarkSetGetRemoveValues(b *testing.B) { } func BenchmarkSetGetSameKey(b *testing.B) { - tm := New(1 * time.Minute) + tm := New[int, int](1 * time.Minute) for n := 0; n < b.N; n++ { tm.Set(1, n, 1*time.Hour) tm.GetValue(1) @@ -465,11 +449,11 @@ func BenchmarkSetGetSameKey(b *testing.B) { // ---------------------------------------------------------- // --- UTILS --- -type CB struct { +type CB[TVal any] struct { mock.Mock } -func (cb *CB) Cb(v interface{}) { +func (cb *CB[TVal]) Cb(v TVal) { cb.TestData().Set("v", v) cb.Called() }