-
Notifications
You must be signed in to change notification settings - Fork 0
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
0 parents
commit 4cbb8d9
Showing
6 changed files
with
315 additions
and
0 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 |
---|---|---|
@@ -0,0 +1,17 @@ | ||
name: test | ||
|
||
on: push | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: actions/setup-go@v5 | ||
with: | ||
go-version: stable | ||
- uses: golangci/golangci-lint-action@v6 | ||
- name: go list | ||
run: go list -json -m all > go.list | ||
- uses: sonatype-nexus-community/nancy-github-action@main | ||
- run: go test -race ./... |
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,23 @@ | ||
# ExpiryMap | ||
|
||
This Go package provides a map that automatically removes entries after a given expiry delay. | ||
|
||
## Features | ||
|
||
* The map key can be any comparable type | ||
* The map value can be any type | ||
* The map is safe for concurrent use | ||
* The expiry delay is specified as a `time.Duration` value | ||
|
||
## Methods | ||
|
||
* NewExpiryMap - creates a new ExpiryMap | ||
* Get, Set, Delete - standard map operations | ||
* Len - returns the number of entries in the map | ||
* Iterate - iterates over all entries in the map | ||
* Clear - removes all entries from the map | ||
* Stop - stops the background goroutine that removes expired entries | ||
|
||
## Example | ||
|
||
See [example/main.go](./example/main.go) |
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,136 @@ | ||
package expirymap | ||
|
||
import ( | ||
"iter" | ||
"sync" | ||
"time" | ||
) | ||
|
||
// ExpiryMap is a map of K key and V values with a builtin garbage cleaner that automatically | ||
// deletes entries after a given expiry delay. | ||
type ExpiryMap[K comparable, V any] struct { | ||
storedMap map[K]*Content[V] | ||
expiryDelay time.Duration | ||
gargabeCleanInterval time.Duration | ||
mutex sync.Mutex | ||
stop chan bool | ||
} | ||
|
||
// Content is the value stored for a given key in the ExpiryMap. | ||
type Content[V any] struct { | ||
Data V | ||
|
||
lastUpdated time.Time | ||
} | ||
|
||
// New returns a new ExpiryMap. | ||
// It also starts a goroutine that periodically cleans up expired entries | ||
// according to the expiryDelay every gargabeCleanInterval. | ||
func New[K comparable, V any](expiryDelay, gargabeCleanInterval time.Duration) *ExpiryMap[K, V] { | ||
s := &ExpiryMap[K, V]{ | ||
storedMap: make(map[K]*Content[V]), | ||
expiryDelay: expiryDelay, | ||
gargabeCleanInterval: gargabeCleanInterval, | ||
stop: make(chan bool), | ||
} | ||
|
||
s.start() | ||
|
||
return s | ||
} | ||
|
||
// Get returns the value for a given key. | ||
func (s *ExpiryMap[K, V]) Get(key K) V { | ||
s.mutex.Lock() | ||
defer s.mutex.Unlock() | ||
|
||
content, found := s.storedMap[key] | ||
if !found { | ||
content = &Content[V]{} | ||
} | ||
|
||
return content.Data | ||
} | ||
|
||
// Set sets the value for a given key and reset its expiry time. | ||
func (s *ExpiryMap[K, V]) Set(key K, data V) { | ||
s.mutex.Lock() | ||
defer s.mutex.Unlock() | ||
|
||
content := &Content[V]{} | ||
content.lastUpdated = time.Now() | ||
content.Data = data | ||
s.storedMap[key] = content | ||
} | ||
|
||
// Delete deletes the value for a given key. | ||
func (s *ExpiryMap[K, V]) Delete(key K) { | ||
s.mutex.Lock() | ||
defer s.mutex.Unlock() | ||
|
||
delete(s.storedMap, key) | ||
} | ||
|
||
// Len returns the number of stored entries. | ||
func (s *ExpiryMap[K, V]) Len() int { | ||
s.mutex.Lock() | ||
defer s.mutex.Unlock() | ||
|
||
return len(s.storedMap) | ||
} | ||
|
||
// Iterate returns an iterator to loop over the stored entries. | ||
func (s *ExpiryMap[K, V]) Iterate() iter.Seq2[K, V] { | ||
return func(next func(K, V) bool) { | ||
s.mutex.Lock() | ||
defer s.mutex.Unlock() | ||
|
||
for k, v := range s.storedMap { | ||
if !next(k, v.Data) { | ||
return | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Clear deletes all stored entries. | ||
func (s *ExpiryMap[K, V]) Clear() { | ||
s.mutex.Lock() | ||
defer s.mutex.Unlock() | ||
|
||
clear(s.storedMap) | ||
} | ||
|
||
// Stop stops the garbage cleaner goroutine. | ||
func (s *ExpiryMap[K, V]) Stop() { | ||
s.mutex.Lock() | ||
defer s.mutex.Unlock() | ||
|
||
s.stop <- true | ||
} | ||
|
||
func (s *ExpiryMap[K, V]) start() { | ||
go func() { | ||
for { | ||
select { | ||
case <-s.stop: | ||
return | ||
case <-time.Tick(s.gargabeCleanInterval): | ||
s.gargabeClean() | ||
} | ||
} | ||
}() | ||
} | ||
|
||
func (s *ExpiryMap[K, V]) gargabeClean() { | ||
s.mutex.Lock() | ||
defer s.mutex.Unlock() | ||
|
||
expiredTime := time.Now().Add(-s.expiryDelay) | ||
|
||
for key, u := range s.storedMap { | ||
if u.lastUpdated.Before(expiredTime) { | ||
delete(s.storedMap, key) | ||
} | ||
} | ||
} |
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,136 @@ | ||
package expirymap | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func TestNew(t *testing.T) { | ||
m := New[int, bool](time.Minute, time.Minute) | ||
if m == nil { | ||
t.Fatalf("expected ExpiryMap, got nil") | ||
} | ||
} | ||
|
||
func TestGetUnset(t *testing.T) { | ||
key := 1 | ||
|
||
m := New[int, string](time.Minute, time.Minute) | ||
|
||
if v := m.Get(key); !reflect.DeepEqual(v, "") { | ||
t.Fatalf("expected %v, got %v", "", v) | ||
} | ||
} | ||
|
||
func TestSetGet(t *testing.T) { | ||
key := 1 | ||
value := "alice" | ||
|
||
m := New[int, string](time.Minute, time.Minute) | ||
m.Set(key, value) | ||
|
||
if v := m.Get(key); !reflect.DeepEqual(v, value) { | ||
t.Fatalf("expected %v, got %v", value, v) | ||
} | ||
} | ||
|
||
func TestSetDeleteGet(t *testing.T) { | ||
key := 2 | ||
value := "bob" | ||
|
||
m := New[int, string](time.Minute, time.Minute) | ||
m.Set(key, value) | ||
m.Delete(key) | ||
|
||
if v := m.Get(key); !reflect.DeepEqual(v, "") { | ||
t.Fatalf("expected %v, got %v", "", v) | ||
} | ||
} | ||
|
||
func TestDeleteUnset(t *testing.T) { | ||
key := 3 | ||
|
||
m := New[int, string](time.Minute, time.Minute) | ||
m.Delete(key) | ||
} | ||
|
||
func TestLen(t *testing.T) { | ||
key := 4 | ||
value := "charlie" | ||
|
||
m := New[int, string](time.Minute, time.Minute) | ||
|
||
if l := m.Len(); l != 0 { | ||
t.Fatalf("expected %v, got %v", 0, l) | ||
} | ||
|
||
m.Set(key, value) | ||
|
||
if l := m.Len(); l != 1 { | ||
t.Fatalf("expected %v, got %v", 1, l) | ||
} | ||
} | ||
|
||
func TestIterate(t *testing.T) { | ||
key := 5 | ||
value := "dave" | ||
|
||
m := New[int, string](time.Minute, time.Minute) | ||
m.Set(key, value) | ||
|
||
var count int | ||
var k int | ||
var v string | ||
for k, v = range m.Iterate() { | ||
count++ | ||
} | ||
|
||
if count != 1 { | ||
t.Fatalf("expected %v, got %v", 1, count) | ||
} | ||
|
||
if k != key { | ||
t.Fatalf("expected %v, got %v", key, k) | ||
} | ||
|
||
if v != value { | ||
t.Fatalf("expected %v, got %v", value, v) | ||
} | ||
} | ||
|
||
func TestClear(t *testing.T) { | ||
key := 6 | ||
value := "eve" | ||
|
||
m := New[int, string](time.Minute, time.Minute) | ||
m.Set(key, value) | ||
m.Clear() | ||
|
||
if l := m.Len(); l != 0 { | ||
t.Fatalf("expected %v, got %v", 0, l) | ||
} | ||
} | ||
|
||
func TestStop(t *testing.T) { | ||
m := New[int, string](time.Minute, time.Minute) | ||
m.Stop() | ||
} | ||
|
||
func TestGargabeClean(t *testing.T) { | ||
key := 7 | ||
value := "frank" | ||
|
||
m := New[int, string](time.Nanosecond, time.Millisecond) | ||
m.Set(key, value) | ||
|
||
if v := m.Get(key); !reflect.DeepEqual(v, value) { | ||
t.Fatalf("expected %v, got %v", value, v) | ||
} | ||
|
||
time.Sleep(time.Millisecond * 2) | ||
|
||
if v := m.Get(key); v != "" { | ||
t.Fatalf("expected nil, got %v", v) | ||
} | ||
} |
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/TheoBrigitte/expirymap | ||
|
||
go 1.23 |
Empty file.