-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
402 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,63 @@ | ||
# ttlmap | ||
A map in which entries expire after given time period | ||
# ttlMap | ||
|
||
`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. | ||
* The default map key uses a type of `string`, but this can be modified be changing `CustomKeyType` in [ttlMap.go](ttlMap.go). | ||
* Any data type can be used as a map value. Internally, `interface{}` is used for this. | ||
|
||
## Example | ||
|
||
[Full example using many data types](example/example.go) | ||
|
||
Small example: | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"time" | ||
|
||
"github.com/jftuga/ttlMap" | ||
) | ||
|
||
func main() { | ||
maxTTL := 4 // a key's time to live in seconds | ||
startSize := 3 // initial number of items in map | ||
pruneInterval := 1 // search for expired items every 'pruneInterval' seconds | ||
refreshLastAccessOnGet := true // update item's 'lastAccessTime' on a .Get() | ||
t := ttlMap.New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet) | ||
|
||
// populate the ttlMap | ||
t.Put("myString", "a b c") | ||
t.Put("int_array", []int{1, 2, 3}) | ||
fmt.Println("ttlMap length:", t.Len()) | ||
|
||
// display all items in ttlMap | ||
all := t.All() | ||
for k, v := range all { | ||
fmt.Printf("[%9s] %v\n", k, v.Value) | ||
} | ||
fmt.Println() | ||
|
||
sleepTime := maxTTL + pruneInterval | ||
fmt.Printf("Sleeping %v seconds, items should be 'nil' after this time\n", sleepTime) | ||
time.Sleep(time.Second * time.Duration(sleepTime)) | ||
fmt.Printf("[%9s] %v\n", "myString", t.Get("myString")) | ||
fmt.Printf("[%9s] %v\n", "int_array", t.Get("int_array")) | ||
fmt.Println("ttlMap length:", t.Len()) | ||
} | ||
``` | ||
|
||
## Performance | ||
* Searching for expired items runs in O(n) time, where n = number of items in the `ttlMap`. | ||
* * This inefficiency can be somewhat mitigated by increasing the value of the `pruneInterval` time. | ||
* In most cases you want `pruneInterval > maxTTL`; otherwise expired items will stay in the map longer than expected. | ||
|
||
## Acknowledgments | ||
* Adopted from: [Map with TTL option in Go](https://stackoverflow.com/a/25487392/452281) | ||
* * Answer created by: [OneOfOne](https://stackoverflow.com/users/145587/oneofone) | ||
* [/u/skeeto](https://old.reddit.com/user/skeeto): suggestions for the `New` function | ||
|
||
## Disclosure Notification | ||
|
||
This program was completely developed on my own personal time, for my own personal benefit, and on my personally owned equipment. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/* | ||
example.go | ||
-John Taylor | ||
2023-10-21 | ||
This is an example on how to use the ttlMap package. Notice the variety of data types used. | ||
*/ | ||
|
||
package main | ||
|
||
import ( | ||
"fmt" | ||
"time" | ||
|
||
"github.com/jftuga/ttlMap" | ||
) | ||
|
||
type User struct { | ||
Name string | ||
Level uint | ||
} | ||
|
||
func main() { | ||
maxTTL := 4 // time in seconds | ||
startSize := 3 // initial number of items in map | ||
pruneInterval := 1 // search for expired items every 'pruneInterval' seconds | ||
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get() | ||
t := ttlMap.New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet) | ||
|
||
// populate the ttlMap | ||
t.Put("string", "a b c") | ||
t.Put("int", 3) | ||
t.Put("float", 4.4) | ||
t.Put("int_array", []int{1, 2, 3}) | ||
t.Put("bool", false) | ||
t.Put("rune", '{') | ||
t.Put("byte", 0x7b) | ||
var u = uint64(123456789) | ||
t.Put("uint64", u) | ||
var c = complex(3.14, -4.321) | ||
t.Put("complex", c) | ||
|
||
allUsers := []User{{Name: "abc", Level: 123}, {Name: "def", Level: 456}} | ||
t.Put("all_users", allUsers) | ||
|
||
fmt.Println() | ||
fmt.Println("ttlMap length:", t.Len()) | ||
|
||
// extract entry from struct array | ||
a := t.Get("all_users").([]User) | ||
fmt.Printf("second user: %v, %v\n", a[1].Name, a[1].Level) | ||
|
||
// display all items in ttlMap | ||
fmt.Println() | ||
fmt.Println("vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv") | ||
all := t.All() | ||
for k, v := range all { | ||
fmt.Printf("[%9s] %v\n", k, v.Value) | ||
} | ||
fmt.Println("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^") | ||
fmt.Println() | ||
|
||
// by executing Get(), the 'dontExpireKey' lastAccessTime will be updated | ||
// therefore, this item will not expire | ||
dontExpireKey := "float" | ||
go func() { | ||
for range time.Tick(time.Second) { | ||
t.Get(ttlMap.CustomKeyType(dontExpireKey)) | ||
} | ||
}() | ||
|
||
// ttlMap has an expiration time, wait until this amount of time passes | ||
sleepTime := maxTTL + pruneInterval | ||
fmt.Println() | ||
fmt.Printf("Sleeping %v seconds, items should be removed after this time, except for the '%v' key\n", sleepTime, dontExpireKey) | ||
fmt.Println() | ||
time.Sleep(time.Second * time.Duration(sleepTime)) | ||
|
||
// these items have expired and therefore should be nil, except for 'dontExpireKey' | ||
fmt.Printf("[%9s] %v\n", "string", t.Get("string")) | ||
fmt.Printf("[%9s] %v\n", "int", t.Get("int")) | ||
fmt.Printf("[%9s] %v\n", "float", t.Get("float")) | ||
fmt.Printf("[%9s] %v\n", "int_array", t.Get("int_array")) | ||
fmt.Printf("[%9s] %v\n", "bool", t.Get("bool")) | ||
fmt.Printf("[%9s] %v\n", "rune", t.Get("rune")) | ||
fmt.Printf("[%9s] %v\n", "byte", t.Get("byte")) | ||
fmt.Printf("[%9s] %v\n", "uint64", t.Get("uint64")) | ||
fmt.Printf("[%9s] %v\n", "complex", t.Get("complex")) | ||
fmt.Printf("[%9s] %v\n", "all_users", t.Get("all_users")) | ||
|
||
// sanity check, this comparison should be true | ||
fmt.Println() | ||
if t.Get("int") == nil { | ||
fmt.Println("[int] is nil") | ||
} | ||
fmt.Println("ttlMap length:", t.Len()) | ||
fmt.Println() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"time" | ||
|
||
"github.com/jftuga/ttlMap" | ||
) | ||
|
||
func main() { | ||
maxTTL := 4 // time in seconds | ||
startSize := 3 // initial number of items in map | ||
pruneInterval := 1 // search for expired items every 'pruneInterval' seconds | ||
refreshLastAccessOnGet := true // update item's lastAccessTime on a .Get() | ||
t := ttlMap.New(maxTTL, startSize, pruneInterval, refreshLastAccessOnGet) | ||
|
||
// populate the ttlMap | ||
t.Put("myString", "a b c") | ||
t.Put("int_array", []int{1, 2, 3}) | ||
fmt.Println("ttlMap length:", t.Len()) | ||
|
||
// display all items in ttlMap | ||
all := t.All() | ||
for k, v := range all { | ||
fmt.Printf("[%9s] %v\n", k, v.Value) | ||
} | ||
fmt.Println() | ||
|
||
sleepTime := maxTTL + pruneInterval | ||
fmt.Printf("Sleeping %v seconds, items should be 'nil' after this time\n", sleepTime) | ||
time.Sleep(time.Second * time.Duration(sleepTime)) | ||
fmt.Printf("[%9s] %v\n", "myString", t.Get("myString")) | ||
fmt.Printf("[%9s] %v\n", "int_array", t.Get("int_array")) | ||
fmt.Println("ttlMap length:", t.Len()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/jftuga/ttlMap | ||
|
||
go 1.21.3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/* | ||
ttlMap.go | ||
-John Taylor | ||
2023-10-21 | ||
ttlMap is a "time-to-live" map such that after a given amount of time, items | ||
in the map are deleted. | ||
When a Put() occurs, the lastAccess time is set to time.Now().Unix() | ||
When a Get() occurs, the lastAccess time is updated to time.Now().Unix() | ||
Therefore, only items that are not called by Get() will be deleted after the TTL occurs. | ||
Adopted from: https://stackoverflow.com/a/25487392/452281 | ||
Changes from the referenced implementation | ||
========================================== | ||
1) the may key is user definable by setting CustomKeyType (defaults to string) | ||
2) use interface{} instead of string as the map value so that any data type can be used | ||
3) added All() function | ||
4) use item.Value instead of item.value so that it can be externally referenced | ||
5) added user configurable prune interval - search for expired items every 'pruneInterval' seconds | ||
6) toggle for refreshLastAccessOnGet - update item's lastAccessTime on a .Get() when set to true | ||
*/ | ||
|
||
package ttlMap | ||
|
||
import ( | ||
"sync" | ||
"time" | ||
) | ||
|
||
const version string = "1.0.0" | ||
|
||
type CustomKeyType string | ||
|
||
type item struct { | ||
Value interface{} | ||
lastAccess int64 | ||
} | ||
|
||
type ttlMap struct { | ||
m map[CustomKeyType]*item | ||
l sync.Mutex | ||
refresh bool | ||
} | ||
|
||
func New(maxTTL int, ln int, pruneInterval int, refreshLastAccessOnGet bool) (m *ttlMap) { | ||
// if pruneInterval > maxTTL { | ||
// print("WARNING: ttlMap: pruneInterval > maxTTL\n") | ||
// } | ||
m = &ttlMap{m: make(map[CustomKeyType]*item, ln)} | ||
m.refresh = refreshLastAccessOnGet | ||
go func() { | ||
for now := range time.Tick(time.Second * time.Duration(pruneInterval)) { | ||
currentTime := now.Unix() | ||
m.l.Lock() | ||
for k, v := range m.m { | ||
// print("TICK:", currentTime, " ", v.lastAccess, " ", (currentTime - v.lastAccess), " ", maxTTL, " ", k, "\n") | ||
if currentTime-v.lastAccess >= int64(maxTTL) { | ||
delete(m.m, k) | ||
// print("deleting: ", k, "\n") | ||
} | ||
} | ||
// print("\n") | ||
m.l.Unlock() | ||
} | ||
}() | ||
return | ||
} | ||
|
||
func (m *ttlMap) Len() int { | ||
return len(m.m) | ||
} | ||
|
||
func (m *ttlMap) Put(k CustomKeyType, v interface{}) { | ||
m.l.Lock() | ||
it, ok := m.m[k] | ||
if !ok { | ||
it = &item{Value: v} | ||
m.m[k] = it | ||
} | ||
it.lastAccess = time.Now().Unix() | ||
m.l.Unlock() | ||
} | ||
|
||
func (m *ttlMap) Get(k CustomKeyType) (v interface{}) { | ||
m.l.Lock() | ||
if it, ok := m.m[k]; ok { | ||
v = it.Value | ||
if m.refresh { | ||
m.m[k].lastAccess = time.Now().Unix() | ||
} | ||
} | ||
m.l.Unlock() | ||
return | ||
} | ||
|
||
func (m *ttlMap) All() map[CustomKeyType]*item { | ||
return m.m | ||
} |
Oops, something went wrong.