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()
}