Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jftuga committed Oct 23, 2023
1 parent db39f85 commit 55bbdf1
Show file tree
Hide file tree
Showing 6 changed files with 402 additions and 2 deletions.
65 changes: 63 additions & 2 deletions README.md
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.
98 changes: 98 additions & 0 deletions example/example.go
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()
}
35 changes: 35 additions & 0 deletions example/small/small.go
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())
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/jftuga/ttlMap

go 1.21.3
101 changes: 101 additions & 0 deletions ttlMap.go
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
}
Loading

0 comments on commit 55bbdf1

Please sign in to comment.