Skip to content

Commit f770ab3

Browse files
committed
The TtlMap key can now be any comparable data type
1 parent fa1c90b commit f770ab3

File tree

5 files changed

+107
-45
lines changed

5 files changed

+107
-45
lines changed

README.md

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# TtlMap
22

33
`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).
4+
* The map key can be any [comparable](https://go.dev/ref/spec#Comparison_operators) data type, via Generics.
55
* Any data type can be used as a map value. Internally, `interface{}` is used for this.
66

77
## Example
@@ -24,7 +24,7 @@ func main() {
2424
startSize := 3 // initial number of items in map
2525
pruneInterval := time.Duration(time.Second * 1) // search for expired items every 'pruneInterval' seconds
2626
refreshLastAccessOnGet := true // update item's 'lastAccessTime' on a .Get()
27-
t := TtlMap.New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
27+
t := TtlMap.New[string](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
2828
defer t.Close()
2929

3030
// populate the TtlMap
@@ -46,7 +46,6 @@ func main() {
4646
fmt.Printf("[%9s] %v\n", "int_array", t.Get("int_array"))
4747
fmt.Println("TtlMap length:", t.Len())
4848
}
49-
5049
```
5150

5251
Output:

example/example.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func main() {
2525
startSize := 3 // initial number of items in map
2626
pruneInterval := time.Duration(time.Second * 1) // search for expired items every 'pruneInterval' seconds
2727
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
28-
t := TtlMap.New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
28+
t := TtlMap.New[string](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
2929
defer t.Close()
3030

3131
// populate the TtlMap
@@ -66,7 +66,7 @@ func main() {
6666
dontExpireKey := "float"
6767
go func() {
6868
for range time.Tick(time.Second) {
69-
t.Get(TtlMap.CustomKeyType(dontExpireKey))
69+
t.Get(dontExpireKey)
7070
}
7171
}()
7272

@@ -99,10 +99,10 @@ func main() {
9999

100100
fmt.Println()
101101
fmt.Printf("Manually deleting '%v' key; should be successful\n", dontExpireKey)
102-
success := t.Delete(TtlMap.CustomKeyType(dontExpireKey))
102+
success := t.Delete(dontExpireKey)
103103
fmt.Printf(" successful? %v\n", success)
104104
fmt.Printf("Manually deleting '%v' key again; should NOT be successful this time\n", dontExpireKey)
105-
success = t.Delete(TtlMap.CustomKeyType(dontExpireKey))
105+
success = t.Delete(dontExpireKey)
106106
fmt.Printf(" successful? %v\n", success)
107107
fmt.Println("TtlMap length:", t.Len(), " (should equal 0)")
108108
fmt.Println()

example/small/small.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func main() {
1212
startSize := 3 // initial number of items in map
1313
pruneInterval := time.Duration(time.Second * 1) // search for expired items every 'pruneInterval' seconds
1414
refreshLastAccessOnGet := true // update item's 'lastAccessTime' on a .Get()
15-
t := TtlMap.New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
15+
t := TtlMap.New[string](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
1616
defer t.Close()
1717

1818
// populate the TtlMap

ttlMap.go

+17-25
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,9 @@ in the map are deleted.
99
When a Put() occurs, the lastAccess time is set to time.Now().Unix()
1010
When a Get() occurs, the lastAccess time is updated to time.Now().Unix()
1111
Therefore, only items that are not called by Get() will be deleted after the TTL occurs.
12+
A GetNoUpdate() can be used in which case the lastAccess time will NOT be updated.
1213
1314
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-
7) add Delete() and Clear() functions
24-
2515
*/
2616

2717
package TtlMap
@@ -34,25 +24,27 @@ import (
3424

3525
const version string = "1.4.0"
3626

37-
type CustomKeyType string
27+
type CustomKeyType interface {
28+
comparable
29+
}
3830

3931
type item struct {
4032
Value interface{}
4133
lastAccess int64
4234
}
4335

44-
type TtlMap struct {
45-
m map[CustomKeyType]*item
36+
type TtlMap[T CustomKeyType] struct {
37+
m map[T]*item
4638
l sync.Mutex
4739
refresh bool
4840
stop chan bool
4941
}
5042

51-
func New(maxTTL time.Duration, ln int, pruneInterval time.Duration, refreshLastAccessOnGet bool) (m *TtlMap) {
43+
func New[T CustomKeyType](maxTTL time.Duration, ln int, pruneInterval time.Duration, refreshLastAccessOnGet bool) (m *TtlMap[T]) {
5244
// if pruneInterval > maxTTL {
5345
// print("WARNING: TtlMap: pruneInterval > maxTTL\n")
5446
// }
55-
m = &TtlMap{m: make(map[CustomKeyType]*item, ln), stop: make(chan bool)}
47+
m = &TtlMap[T]{m: make(map[T]*item, ln), stop: make(chan bool)}
5648
m.refresh = refreshLastAccessOnGet
5749
maxTTL /= 1000000000
5850
// print("maxTTL: ", maxTTL, "\n")
@@ -79,11 +71,11 @@ func New(maxTTL time.Duration, ln int, pruneInterval time.Duration, refreshLastA
7971
return
8072
}
8173

82-
func (m *TtlMap) Len() int {
74+
func (m *TtlMap[T]) Len() int {
8375
return len(m.m)
8476
}
8577

86-
func (m *TtlMap) Put(k CustomKeyType, v interface{}) {
78+
func (m *TtlMap[T]) Put(k T, v interface{}) {
8779
m.l.Lock()
8880
it, ok := m.m[k]
8981
if !ok {
@@ -94,7 +86,7 @@ func (m *TtlMap) Put(k CustomKeyType, v interface{}) {
9486
m.l.Unlock()
9587
}
9688

97-
func (m *TtlMap) Get(k CustomKeyType) (v interface{}) {
89+
func (m *TtlMap[T]) Get(k T) (v interface{}) {
9890
m.l.Lock()
9991
if it, ok := m.m[k]; ok {
10092
v = it.Value
@@ -106,7 +98,7 @@ func (m *TtlMap) Get(k CustomKeyType) (v interface{}) {
10698
return
10799
}
108100

109-
func (m *TtlMap) GetNoUpdate(k CustomKeyType) (v interface{}) {
101+
func (m *TtlMap[T]) GetNoUpdate(k T) (v interface{}) {
110102
m.l.Lock()
111103
if it, ok := m.m[k]; ok {
112104
v = it.Value
@@ -115,7 +107,7 @@ func (m *TtlMap) GetNoUpdate(k CustomKeyType) (v interface{}) {
115107
return
116108
}
117109

118-
func (m *TtlMap) Delete(k CustomKeyType) bool {
110+
func (m *TtlMap[T]) Delete(k T) bool {
119111
m.l.Lock()
120112
_, ok := m.m[k]
121113
if !ok {
@@ -127,20 +119,20 @@ func (m *TtlMap) Delete(k CustomKeyType) bool {
127119
return true
128120
}
129121

130-
func (m *TtlMap) Clear() {
122+
func (m *TtlMap[T]) Clear() {
131123
m.l.Lock()
132124
clear(m.m)
133125
m.l.Unlock()
134126
}
135127

136-
func (m *TtlMap) All() map[CustomKeyType]*item {
128+
func (m *TtlMap[T]) All() map[T]*item {
137129
m.l.Lock()
138-
dst := make(map[CustomKeyType]*item, len(m.m))
130+
dst := make(map[T]*item, len(m.m))
139131
maps.Copy(dst, m.m)
140132
m.l.Unlock()
141133
return dst
142134
}
143135

144-
func (m *TtlMap) Close() {
136+
func (m *TtlMap[T]) Close() {
145137
m.stop <- true
146138
}

ttlMap_test.go

+83-12
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ func TestAllItemsExpired(t *testing.T) {
1111
startSize := 3 // initial number of items in map
1212
pruneInterval := time.Duration(time.Second * 1) // search for expired items every 'pruneInterval' seconds
1313
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
14-
tm := New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
14+
tm := New[string](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
1515
defer tm.Close()
1616

1717
// populate the TtlMap
@@ -30,7 +30,7 @@ func TestNoItemsExpired(t *testing.T) {
3030
startSize := 3 // initial number of items in map
3131
pruneInterval := time.Duration(time.Second * 3) // search for expired items every 'pruneInterval' seconds
3232
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
33-
tm := New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
33+
tm := New[string](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
3434
defer tm.Close()
3535

3636
// populate the TtlMap
@@ -49,7 +49,7 @@ func TestKeepFloat(t *testing.T) {
4949
startSize := 3 // initial number of items in map
5050
pruneInterval := time.Duration(time.Second * 1) // search for expired items every 'pruneInterval' seconds
5151
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
52-
tm := New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
52+
tm := New[string](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
5353
defer tm.Close()
5454

5555
// populate the TtlMap
@@ -60,7 +60,7 @@ func TestKeepFloat(t *testing.T) {
6060
dontExpireKey := "int"
6161
go func() {
6262
for range time.Tick(time.Second) {
63-
tm.Get(CustomKeyType(dontExpireKey))
63+
tm.Get(dontExpireKey)
6464
}
6565
}()
6666

@@ -69,19 +69,19 @@ func TestKeepFloat(t *testing.T) {
6969
t.Fatalf("t.Len should equal 1, but actually equals %v\n", tm.Len())
7070
}
7171
all := tm.All()
72-
if all[CustomKeyType(dontExpireKey)].Value != 1234 {
73-
t.Errorf("Value should equal 1234 but actually equals %v\n", all[CustomKeyType(dontExpireKey)].Value)
72+
if all[dontExpireKey].Value != 1234 {
73+
t.Errorf("Value should equal 1234 but actually equals %v\n", all[dontExpireKey].Value)
7474
}
7575
t.Logf("tm.Len: %v\n", tm.Len())
76-
t.Logf("%v Value: %v\n", dontExpireKey, all[CustomKeyType(dontExpireKey)].Value)
76+
t.Logf("%v Value: %v\n", dontExpireKey, all[dontExpireKey].Value)
7777
}
7878

7979
func TestWithNoRefresh(t *testing.T) {
8080
maxTTL := time.Duration(time.Second * 4) // time in seconds
8181
startSize := 3 // initial number of items in map
8282
pruneInterval := time.Duration(time.Second * 1) // search for expired items every 'pruneInterval' seconds
8383
refreshLastAccessOnGet := false // do NOT update item's lastAccessTime on a .Get()
84-
tm := New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
84+
tm := New[string](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
8585
defer tm.Close()
8686

8787
// populate the TtlMap
@@ -107,7 +107,7 @@ func TestDelete(t *testing.T) {
107107
startSize := 3 // initial number of items in map
108108
pruneInterval := time.Duration(time.Second * 4) // search for expired items every 'pruneInterval' seconds
109109
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
110-
tm := New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
110+
tm := New[string](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
111111
defer tm.Close()
112112

113113
// populate the TtlMap
@@ -132,7 +132,7 @@ func TestClear(t *testing.T) {
132132
startSize := 3 // initial number of items in map
133133
pruneInterval := time.Duration(time.Second * 4) // search for expired items every 'pruneInterval' seconds
134134
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
135-
tm := New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
135+
tm := New[string](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
136136
defer tm.Close()
137137

138138
// populate the TtlMap
@@ -155,7 +155,7 @@ func TestAllFunc(t *testing.T) {
155155
startSize := 3 // initial number of items in map
156156
pruneInterval := time.Duration(time.Second * 4) // search for expired items every 'pruneInterval' seconds
157157
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
158-
tm := New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
158+
tm := New[string](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
159159
defer tm.Close()
160160

161161
// populate the TtlMap
@@ -186,7 +186,7 @@ func TestGetNoUpdate(t *testing.T) {
186186
startSize := 3 // initial number of items in map
187187
pruneInterval := time.Duration(time.Second * 4) // search for expired items every 'pruneInterval' seconds
188188
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
189-
tm := New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
189+
tm := New[string](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
190190
defer tm.Close()
191191

192192
// populate the TtlMap
@@ -209,3 +209,74 @@ func TestGetNoUpdate(t *testing.T) {
209209
t.Errorf("t.Len should be 0, but actually equals %v\n", tm.Len())
210210
}
211211
}
212+
213+
func TestUInt64Key(t *testing.T) {
214+
maxTTL := time.Duration(time.Second * 2) // time in seconds
215+
startSize := 3 // initial number of items in map
216+
pruneInterval := time.Duration(time.Second * 4) // search for expired items every 'pruneInterval' seconds
217+
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
218+
tm := New[uint64](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
219+
defer tm.Close()
220+
221+
tm.Put(18446744073709551615, "largest")
222+
tm.Put(9223372036854776000, "mid")
223+
tm.Put(0, "zero")
224+
225+
allItems := tm.All()
226+
for k, v := range allItems {
227+
t.Logf("k: %v v: %v\n", k, v.Value)
228+
}
229+
230+
time.Sleep(maxTTL + pruneInterval)
231+
t.Logf("tm.Len: %v\n", tm.Len())
232+
if tm.Len() != 0 {
233+
t.Errorf("t.Len should be 0, but actually equals %v\n", tm.Len())
234+
}
235+
}
236+
237+
func TestUFloat32Key(t *testing.T) {
238+
maxTTL := time.Duration(time.Second * 2) // time in seconds
239+
startSize := 3 // initial number of items in map
240+
pruneInterval := time.Duration(time.Second * 4) // search for expired items every 'pruneInterval' seconds
241+
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
242+
tm := New[float32](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
243+
defer tm.Close()
244+
245+
tm.Put(34000000000.12345, "largest")
246+
tm.Put(12312312312.98765, "mid")
247+
tm.Put(0.001, "tiny")
248+
249+
allItems := tm.All()
250+
for k, v := range allItems {
251+
t.Logf("k: %v v: %v\n", k, v.Value)
252+
}
253+
t.Logf("k: 0.001 v:%v (verified)\n", tm.Get(0.001))
254+
255+
time.Sleep(maxTTL + pruneInterval)
256+
t.Logf("tm.Len: %v\n", tm.Len())
257+
if tm.Len() != 0 {
258+
t.Errorf("t.Len should be 0, but actually equals %v\n", tm.Len())
259+
}
260+
}
261+
262+
func TestByteKey(t *testing.T) {
263+
maxTTL := time.Duration(time.Second * 2) // time in seconds
264+
startSize := 3 // initial number of items in map
265+
pruneInterval := time.Duration(time.Second * 4) // search for expired items every 'pruneInterval' seconds
266+
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get()
267+
tm := New[byte](maxTTL, startSize, pruneInterval, refreshLastAccessOnGet)
268+
defer tm.Close()
269+
270+
tm.Put(0x41, "A")
271+
tm.Put(0x7a, "z")
272+
273+
allItems := tm.All()
274+
for k, v := range allItems {
275+
t.Logf("k: %x v: %v\n", k, v.Value)
276+
}
277+
time.Sleep(maxTTL + pruneInterval)
278+
t.Logf("tm.Len: %v\n", tm.Len())
279+
if tm.Len() != 0 {
280+
t.Errorf("t.Len should be 0, but actually equals %v\n", tm.Len())
281+
}
282+
}

0 commit comments

Comments
 (0)