Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
TheoBrigitte committed Oct 16, 2024
0 parents commit 4cbb8d9
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 0 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/test.yaml
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 ./...
23 changes: 23 additions & 0 deletions README.md
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)
136 changes: 136 additions & 0 deletions expiry-map.go
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)
}
}
}
136 changes: 136 additions & 0 deletions expiry-map_test.go
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)
}
}
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/TheoBrigitte/expirymap

go 1.23
Empty file added go.sum
Empty file.

0 comments on commit 4cbb8d9

Please sign in to comment.