From 0b9942832a5e86ba762d9760a81b96c0f39c1ecd Mon Sep 17 00:00:00 2001 From: Tordarus Date: Mon, 24 Mar 2025 18:13:13 +0100 Subject: [PATCH] initial commit --- go.mod | 5 ++ go.sum | 2 + map.go | 137 ++++++++++++++++++++++++++++++++++ utils.go | 222 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 366 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 map.go create mode 100644 utils.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d1ee13b --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.tordarus.net/tordarus/cachemap + +go 1.24.1 + +require git.tordarus.net/tordarus/cmap v0.0.5 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..da14bf8 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +git.tordarus.net/tordarus/cmap v0.0.5 h1:g2STu3iMXCLn96JZ/f0iN5hbzct9cI5B8+V8ATB4ryE= +git.tordarus.net/tordarus/cmap v0.0.5/go.mod h1:Wx2snnIzdDrmONR9X5bt5aFq8E8EDCK5Zy50DmRwK1M= diff --git a/map.go b/map.go new file mode 100644 index 0000000..46d8d5a --- /dev/null +++ b/map.go @@ -0,0 +1,137 @@ +package cachemap + +import ( + "context" + "iter" + "time" + + "git.tordarus.net/tordarus/cmap" +) + +// CacheMap represents a map for caching values for a specific amount of time. +// CacheMap is thread-safe. +type CacheMap[K comparable, V any] struct { + data *cmap.Map[K, cacheMapEntry[V]] + defaultCacheDuration time.Duration + autoCleanUpEnabled bool +} + +// New returns a CacheMap which caches its items for the given cache duration (if not specified otherwise). +// It does not collect its own garbage until Get() or CleanUp() is called +func New[K comparable, V any](defaultCacheDuration time.Duration) *CacheMap[K, V] { + return &CacheMap[K, V]{ + data: cmap.New[K, cacheMapEntry[V]](), + defaultCacheDuration: defaultCacheDuration, + } +} + +// New returns a CacheMap which caches its items for the given cache duration (if not specified otherwise). +// It collects its own garbage periodically. Memory leaks are not possible but the map is locked for the cleanup process +func NewSelfCleaning[K comparable, V any](ctx context.Context, defaultCacheDuration, cleanUpInterval time.Duration) *CacheMap[K, V] { + cacheMap := New[K, V](defaultCacheDuration) + cacheMap.AutoCleanUp(ctx, cleanUpInterval) + return cacheMap +} + +// Put puts the given key-value pair into the cache map with the CacheMaps default cache duration +func (cm *CacheMap[K, V]) Put(key K, value V) { + cm.data.Put(key, cacheMapEntry[V]{ + Value: value, + CacheTime: time.Now(), + CacheDuration: cm.defaultCacheDuration, + }) +} + +// Put puts the given key-value pair into the cache map with the given cache duration +func (cm *CacheMap[K, V]) PutFor(key K, value V, cacheDuration time.Duration) { + cm.data.Put(key, cacheMapEntry[V]{ + Value: value, + CacheTime: time.Now(), + CacheDuration: cacheDuration, + }) +} + +// Get returns the value associated with the given key and a boolean indicating an existing entry. +// If the entry associated with the key is expired, the key is deleted from the map immediately. +// KeepAlive is called for the entry if it is not expired yet. +func (cm *CacheMap[K, V]) Get(key K) (V, bool) { + entry, ok := cm.data.GetHas(key) + if !ok { + return *new(V), false + } + + if !entry.IsAlive() { + cm.data.Delete(key) + return *new(V), false + } + + cm.KeepAlive(key) + return entry.Value, true +} + +// KeepAlive restarts the cache duration for the entry associated with the given key. +// This is the same as deleting and re-adding the entry with the same cache duration. +func (cm *CacheMap[K, V]) KeepAlive(key K) { + entry, ok := cm.data.GetHas(key) + if ok { + entry.CacheTime = time.Now() + cm.data.Put(key, entry) + } +} + +// Iterate returns an iterator for the CacheMap to use in for-range loops +func (cm *CacheMap[K, V]) Iterate() iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for key, value := range cm.data.Iterate() { + if !yield(key, value.Value) { + return + } + } + } +} + +// CleanUp iterates through the CacheMap and deletes all expired entries. +// The CacheMap is locked during the cleanup process. +func (cm *CacheMap[K, V]) CleanUp() { + cm.data.Do(func(m map[K]cacheMapEntry[V]) { + for key, entry := range m { + if !entry.IsAlive() { + delete(m, key) + } + } + }) +} + +// AutoCleanUp periodically calls CleanUp() to ensure that no garbage is accumulating +// over long periods of time without Get calls. +func (cm *CacheMap[K, V]) AutoCleanUp(ctx context.Context, interval time.Duration) { + if cm.autoCleanUpEnabled { + return + } + cm.autoCleanUpEnabled = true + + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + defer func() { cm.autoCleanUpEnabled = false }() + + for { + select { + case <-ticker.C: + cm.CleanUp() + case <-ctx.Done(): + return + } + } + }() +} + +type cacheMapEntry[T any] struct { + Value T + CacheTime time.Time + CacheDuration time.Duration +} + +func (e cacheMapEntry[T]) IsAlive() bool { + return time.Since(e.CacheTime) < e.CacheDuration +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..884e020 --- /dev/null +++ b/utils.go @@ -0,0 +1,222 @@ +package cachemap + +import ( + "context" + "time" +) + +func CacheFunc11[K1 comparable, V1 any](ctx context.Context, cacheDuration, cleanUpInterval time.Duration, f func(K1) V1) func(K1) V1 { + cacheMap := NewSelfCleaning[K1, V1](ctx, cacheDuration, cleanUpInterval) + return func(key K1) V1 { + if value, ok := cacheMap.Get(key); ok { + return value + } + + value := f(key) + cacheMap.Put(key, value) + return value + } +} + +func CacheFunc12[K1 comparable, V1, V2 any](ctx context.Context, cacheDuration, cleanUpInterval time.Duration, f func(K1) (V1, V2)) func(K1) (V1, V2) { + type Value struct { + Value1 V1 + Value2 V2 + } + + cacheMap := NewSelfCleaning[K1, Value](ctx, cacheDuration, cleanUpInterval) + return func(key K1) (V1, V2) { + if value, ok := cacheMap.Get(key); ok { + return value.Value1, value.Value2 + } + + value1, value2 := f(key) + cacheMap.Put(key, Value{ + Value1: value1, + Value2: value2, + }) + return value1, value2 + } +} + +func CacheFunc13[K1 comparable, V1, V2, V3 any](ctx context.Context, cacheDuration, cleanUpInterval time.Duration, f func(K1) (V1, V2, V3)) func(K1) (V1, V2, V3) { + type Value struct { + Value1 V1 + Value2 V2 + Value3 V3 + } + + cacheMap := NewSelfCleaning[K1, Value](ctx, cacheDuration, cleanUpInterval) + return func(key K1) (V1, V2, V3) { + if value, ok := cacheMap.Get(key); ok { + return value.Value1, value.Value2, value.Value3 + } + + value1, value2, value3 := f(key) + cacheMap.Put(key, Value{ + Value1: value1, + Value2: value2, + Value3: value3, + }) + return value1, value2, value3 + } +} + +func CacheFunc21[K1, K2 comparable, V1 any](ctx context.Context, cacheDuration, cleanUpInterval time.Duration, f func(K1, K2) V1) func(K1, K2) V1 { + type Key struct { + Key1 K1 + Key2 K2 + } + + cacheMap := NewSelfCleaning[Key, V1](ctx, cacheDuration, cleanUpInterval) + return func(key1 K1, key2 K2) V1 { + key := Key{Key1: key1, Key2: key2} + + if value, ok := cacheMap.Get(key); ok { + return value + } + + value := f(key1, key2) + cacheMap.Put(key, value) + return value + } +} + +func CacheFunc22[K1, K2 comparable, V1, V2 any](ctx context.Context, cacheDuration, cleanUpInterval time.Duration, f func(K1, K2) (V1, V2)) func(K1, K2) (V1, V2) { + type Key struct { + Key1 K1 + Key2 K2 + } + + type Value struct { + Value1 V1 + Value2 V2 + } + + cacheMap := NewSelfCleaning[Key, Value](ctx, cacheDuration, cleanUpInterval) + return func(key1 K1, key2 K2) (V1, V2) { + key := Key{Key1: key1, Key2: key2} + + if value, ok := cacheMap.Get(key); ok { + return value.Value1, value.Value2 + } + + value1, value2 := f(key1, key2) + cacheMap.Put(key, Value{ + Value1: value1, + Value2: value2, + }) + return value1, value2 + } +} + +func CacheFunc23[K1, K2 comparable, V1, V2, V3 any](ctx context.Context, cacheDuration, cleanUpInterval time.Duration, f func(K1, K2) (V1, V2, V3)) func(K1, K2) (V1, V2, V3) { + type Key struct { + Key1 K1 + Key2 K2 + } + + type Value struct { + Value1 V1 + Value2 V2 + Value3 V3 + } + + cacheMap := NewSelfCleaning[Key, Value](ctx, cacheDuration, cleanUpInterval) + return func(key1 K1, key2 K2) (V1, V2, V3) { + key := Key{Key1: key1, Key2: key2} + + if value, ok := cacheMap.Get(key); ok { + return value.Value1, value.Value2, value.Value3 + } + + value1, value2, value3 := f(key1, key2) + cacheMap.Put(key, Value{ + Value1: value1, + Value2: value2, + Value3: value3, + }) + return value1, value2, value3 + } +} + +func CacheFunc31[K1, K2, K3 comparable, V1 any](ctx context.Context, cacheDuration, cleanUpInterval time.Duration, f func(K1, K2, K3) V1) func(K1, K2, K3) V1 { + type Key struct { + Key1 K1 + Key2 K2 + Key3 K3 + } + + cacheMap := NewSelfCleaning[Key, V1](ctx, cacheDuration, cleanUpInterval) + return func(key1 K1, key2 K2, key3 K3) V1 { + key := Key{Key1: key1, Key2: key2, Key3: key3} + + if value, ok := cacheMap.Get(key); ok { + return value + } + + value := f(key1, key2, key3) + cacheMap.Put(key, value) + return value + } +} + +func CacheFunc32[K1, K2, K3 comparable, V1, V2 any](ctx context.Context, cacheDuration, cleanUpInterval time.Duration, f func(K1, K2, K3) (V1, V2)) func(K1, K2, K3) (V1, V2) { + type Key struct { + Key1 K1 + Key2 K2 + Key3 K3 + } + + type Value struct { + Value1 V1 + Value2 V2 + } + + cacheMap := NewSelfCleaning[Key, Value](ctx, cacheDuration, cleanUpInterval) + return func(key1 K1, key2 K2, key3 K3) (V1, V2) { + key := Key{Key1: key1, Key2: key2} + + if value, ok := cacheMap.Get(key); ok { + return value.Value1, value.Value2 + } + + value1, value2 := f(key1, key2, key3) + cacheMap.Put(key, Value{ + Value1: value1, + Value2: value2, + }) + return value1, value2 + } +} + +func CacheFunc33[K1, K2, K3 comparable, V1, V2, V3 any](ctx context.Context, cacheDuration, cleanUpInterval time.Duration, f func(K1, K2, K3) (V1, V2, V3)) func(K1, K2, K3) (V1, V2, V3) { + type Key struct { + Key1 K1 + Key2 K2 + Key3 K3 + } + + type Value struct { + Value1 V1 + Value2 V2 + Value3 V3 + } + + cacheMap := NewSelfCleaning[Key, Value](ctx, cacheDuration, cleanUpInterval) + return func(key1 K1, key2 K2, key3 K3) (V1, V2, V3) { + key := Key{Key1: key1, Key2: key2} + + if value, ok := cacheMap.Get(key); ok { + return value.Value1, value.Value2, value.Value3 + } + + value1, value2, value3 := f(key1, key2, key3) + cacheMap.Put(key, Value{ + Value1: value1, + Value2: value2, + Value3: value3, + }) + return value1, value2, value3 + } +}