Skip to content

Commit 55bbdf1

Browse files
committed
initial commit
1 parent db39f85 commit 55bbdf1

File tree

6 files changed

+402
-2
lines changed

6 files changed

+402
-2
lines changed

README.md

+63-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,63 @@
1-
# ttlmap
2-
A map in which entries expire after given time period
1+
# ttlMap
2+
3+
`ttlMap` is golang package that implements a *time-to-live* map such that after a given amount of time, items in the map are deleted.
4+
* The default map key uses a type of `string`, but this can be modified be changing `CustomKeyType` in [ttlMap.go](ttlMap.go).
5+
* Any data type can be used as a map value. Internally, `interface{}` is used for this.
6+
7+
## Example
8+
9+
[Full example using many data types](example/example.go)
10+
11+
Small example:
12+
13+
```go
14+
package main
15+
16+
import (
17+
"fmt"
18+
"time"
19+
20+
"github.com/jftuga/ttlMap"
21+
)
22+
23+
func main() {
24+
maxTTL := 4 // a key's time to live in seconds
25+
startSize := 3 // initial number of items in map
26+
pruneInterval := 1 // search for expired items every 'pruneInterval' seconds
27+
refreshLastAccessOnGet := true // update item's 'lastAccessTime' on a .Get()
28+
t := ttlMap.New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
29+
30+
// populate the ttlMap
31+
t.Put("myString", "a b c")
32+
t.Put("int_array", []int{1, 2, 3})
33+
fmt.Println("ttlMap length:", t.Len())
34+
35+
// display all items in ttlMap
36+
all := t.All()
37+
for k, v := range all {
38+
fmt.Printf("[%9s] %v\n", k, v.Value)
39+
}
40+
fmt.Println()
41+
42+
sleepTime := maxTTL + pruneInterval
43+
fmt.Printf("Sleeping %v seconds, items should be 'nil' after this time\n", sleepTime)
44+
time.Sleep(time.Second * time.Duration(sleepTime))
45+
fmt.Printf("[%9s] %v\n", "myString", t.Get("myString"))
46+
fmt.Printf("[%9s] %v\n", "int_array", t.Get("int_array"))
47+
fmt.Println("ttlMap length:", t.Len())
48+
}
49+
```
50+
51+
## Performance
52+
* Searching for expired items runs in O(n) time, where n = number of items in the `ttlMap`.
53+
* * This inefficiency can be somewhat mitigated by increasing the value of the `pruneInterval` time.
54+
* In most cases you want `pruneInterval > maxTTL`; otherwise expired items will stay in the map longer than expected.
55+
56+
## Acknowledgments
57+
* Adopted from: [Map with TTL option in Go](https://stackoverflow.com/a/25487392/452281)
58+
* * Answer created by: [OneOfOne](https://stackoverflow.com/users/145587/oneofone)
59+
* [/u/skeeto](https://old.reddit.com/user/skeeto): suggestions for the `New` function
60+
61+
## Disclosure Notification
62+
63+
This program was completely developed on my own personal time, for my own personal benefit, and on my personally owned equipment.

example/example.go

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
example.go
3+
-John Taylor
4+
2023-10-21
5+
6+
This is an example on how to use the ttlMap package. Notice the variety of data types used.
7+
*/
8+
9+
package main
10+
11+
import (
12+
"fmt"
13+
"time"
14+
15+
"github.com/jftuga/ttlMap"
16+
)
17+
18+
type User struct {
19+
Name string
20+
Level uint
21+
}
22+
23+
func main() {
24+
maxTTL := 4 // time in seconds
25+
startSize := 3 // initial number of items in map
26+
pruneInterval := 1 // search for expired items every 'pruneInterval' seconds
27+
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
28+
t := ttlMap.New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
29+
30+
// populate the ttlMap
31+
t.Put("string", "a b c")
32+
t.Put("int", 3)
33+
t.Put("float", 4.4)
34+
t.Put("int_array", []int{1, 2, 3})
35+
t.Put("bool", false)
36+
t.Put("rune", '{')
37+
t.Put("byte", 0x7b)
38+
var u = uint64(123456789)
39+
t.Put("uint64", u)
40+
var c = complex(3.14, -4.321)
41+
t.Put("complex", c)
42+
43+
allUsers := []User{{Name: "abc", Level: 123}, {Name: "def", Level: 456}}
44+
t.Put("all_users", allUsers)
45+
46+
fmt.Println()
47+
fmt.Println("ttlMap length:", t.Len())
48+
49+
// extract entry from struct array
50+
a := t.Get("all_users").([]User)
51+
fmt.Printf("second user: %v, %v\n", a[1].Name, a[1].Level)
52+
53+
// display all items in ttlMap
54+
fmt.Println()
55+
fmt.Println("vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv")
56+
all := t.All()
57+
for k, v := range all {
58+
fmt.Printf("[%9s] %v\n", k, v.Value)
59+
}
60+
fmt.Println("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^")
61+
fmt.Println()
62+
63+
// by executing Get(), the 'dontExpireKey' lastAccessTime will be updated
64+
// therefore, this item will not expire
65+
dontExpireKey := "float"
66+
go func() {
67+
for range time.Tick(time.Second) {
68+
t.Get(ttlMap.CustomKeyType(dontExpireKey))
69+
}
70+
}()
71+
72+
// ttlMap has an expiration time, wait until this amount of time passes
73+
sleepTime := maxTTL + pruneInterval
74+
fmt.Println()
75+
fmt.Printf("Sleeping %v seconds, items should be removed after this time, except for the '%v' key\n", sleepTime, dontExpireKey)
76+
fmt.Println()
77+
time.Sleep(time.Second * time.Duration(sleepTime))
78+
79+
// these items have expired and therefore should be nil, except for 'dontExpireKey'
80+
fmt.Printf("[%9s] %v\n", "string", t.Get("string"))
81+
fmt.Printf("[%9s] %v\n", "int", t.Get("int"))
82+
fmt.Printf("[%9s] %v\n", "float", t.Get("float"))
83+
fmt.Printf("[%9s] %v\n", "int_array", t.Get("int_array"))
84+
fmt.Printf("[%9s] %v\n", "bool", t.Get("bool"))
85+
fmt.Printf("[%9s] %v\n", "rune", t.Get("rune"))
86+
fmt.Printf("[%9s] %v\n", "byte", t.Get("byte"))
87+
fmt.Printf("[%9s] %v\n", "uint64", t.Get("uint64"))
88+
fmt.Printf("[%9s] %v\n", "complex", t.Get("complex"))
89+
fmt.Printf("[%9s] %v\n", "all_users", t.Get("all_users"))
90+
91+
// sanity check, this comparison should be true
92+
fmt.Println()
93+
if t.Get("int") == nil {
94+
fmt.Println("[int] is nil")
95+
}
96+
fmt.Println("ttlMap length:", t.Len())
97+
fmt.Println()
98+
}

example/small/small.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/jftuga/ttlMap"
8+
)
9+
10+
func main() {
11+
maxTTL := 4 // time in seconds
12+
startSize := 3 // initial number of items in map
13+
pruneInterval := 1 // search for expired items every 'pruneInterval' seconds
14+
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
15+
t := ttlMap.New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
16+
17+
// populate the ttlMap
18+
t.Put("myString", "a b c")
19+
t.Put("int_array", []int{1, 2, 3})
20+
fmt.Println("ttlMap length:", t.Len())
21+
22+
// display all items in ttlMap
23+
all := t.All()
24+
for k, v := range all {
25+
fmt.Printf("[%9s] %v\n", k, v.Value)
26+
}
27+
fmt.Println()
28+
29+
sleepTime := maxTTL + pruneInterval
30+
fmt.Printf("Sleeping %v seconds, items should be 'nil' after this time\n", sleepTime)
31+
time.Sleep(time.Second * time.Duration(sleepTime))
32+
fmt.Printf("[%9s] %v\n", "myString", t.Get("myString"))
33+
fmt.Printf("[%9s] %v\n", "int_array", t.Get("int_array"))
34+
fmt.Println("ttlMap length:", t.Len())
35+
}

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/jftuga/ttlMap
2+
3+
go 1.21.3

ttlMap.go

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
ttlMap.go
3+
-John Taylor
4+
2023-10-21
5+
6+
ttlMap is a "time-to-live" map such that after a given amount of time, items
7+
in the map are deleted.
8+
9+
When a Put() occurs, the lastAccess time is set to time.Now().Unix()
10+
When a Get() occurs, the lastAccess time is updated to time.Now().Unix()
11+
Therefore, only items that are not called by Get() will be deleted after the TTL occurs.
12+
13+
Adopted from: https://stackoverflow.com/a/25487392/452281
14+
15+
Changes from the referenced implementation
16+
==========================================
17+
1) the may key is user definable by setting CustomKeyType (defaults to string)
18+
2) use interface{} instead of string as the map value so that any data type can be used
19+
3) added All() function
20+
4) use item.Value instead of item.value so that it can be externally referenced
21+
5) added user configurable prune interval - search for expired items every 'pruneInterval' seconds
22+
6) toggle for refreshLastAccessOnGet - update item's lastAccessTime on a .Get() when set to true
23+
24+
*/
25+
26+
package ttlMap
27+
28+
import (
29+
"sync"
30+
"time"
31+
)
32+
33+
const version string = "1.0.0"
34+
35+
type CustomKeyType string
36+
37+
type item struct {
38+
Value interface{}
39+
lastAccess int64
40+
}
41+
42+
type ttlMap struct {
43+
m map[CustomKeyType]*item
44+
l sync.Mutex
45+
refresh bool
46+
}
47+
48+
func New(maxTTL int, ln int, pruneInterval int, refreshLastAccessOnGet bool) (m *ttlMap) {
49+
// if pruneInterval > maxTTL {
50+
// print("WARNING: ttlMap: pruneInterval > maxTTL\n")
51+
// }
52+
m = &ttlMap{m: make(map[CustomKeyType]*item, ln)}
53+
m.refresh = refreshLastAccessOnGet
54+
go func() {
55+
for now := range time.Tick(time.Second * time.Duration(pruneInterval)) {
56+
currentTime := now.Unix()
57+
m.l.Lock()
58+
for k, v := range m.m {
59+
// print("TICK:", currentTime, " ", v.lastAccess, " ", (currentTime - v.lastAccess), " ", maxTTL, " ", k, "\n")
60+
if currentTime-v.lastAccess >= int64(maxTTL) {
61+
delete(m.m, k)
62+
// print("deleting: ", k, "\n")
63+
}
64+
}
65+
// print("\n")
66+
m.l.Unlock()
67+
}
68+
}()
69+
return
70+
}
71+
72+
func (m *ttlMap) Len() int {
73+
return len(m.m)
74+
}
75+
76+
func (m *ttlMap) Put(k CustomKeyType, v interface{}) {
77+
m.l.Lock()
78+
it, ok := m.m[k]
79+
if !ok {
80+
it = &item{Value: v}
81+
m.m[k] = it
82+
}
83+
it.lastAccess = time.Now().Unix()
84+
m.l.Unlock()
85+
}
86+
87+
func (m *ttlMap) Get(k CustomKeyType) (v interface{}) {
88+
m.l.Lock()
89+
if it, ok := m.m[k]; ok {
90+
v = it.Value
91+
if m.refresh {
92+
m.m[k].lastAccess = time.Now().Unix()
93+
}
94+
}
95+
m.l.Unlock()
96+
return
97+
}
98+
99+
func (m *ttlMap) All() map[CustomKeyType]*item {
100+
return m.m
101+
}

0 commit comments

Comments
 (0)