Skip to content

Instantly share code, notes, and snippets.

@suhodolskiy
Last active February 25, 2025 08:39
Show Gist options
  • Select an option

  • Save suhodolskiy/0fa959c10fc744f43504e611c868e1dd to your computer and use it in GitHub Desktop.

Select an option

Save suhodolskiy/0fa959c10fc744f43504e611c868e1dd to your computer and use it in GitHub Desktop.
Cache Loader
package cacheloader
import (
"bytes"
"context"
"crypto/sha256"
"encoding/gob"
"fmt"
"sync"
"time"
)
type CacheLoader[T any] struct {
registry map[string][]chan T
store Store[T]
mu sync.Mutex
tags []string
}
func New[T any](store Store[T], tags []string) *CacheLoader[T] {
return &CacheLoader[T]{
registry: make(map[string][]chan T),
mu: sync.Mutex{},
store: store,
tags: tags,
}
}
func (c *CacheLoader[T]) key(key any) string {
if value, ok := key.(string); ok {
return value
}
var b bytes.Buffer
if err := gob.NewEncoder(&b).Encode(key); err != nil {
panic(err)
}
hash := sha256.Sum256(b.Bytes())
return fmt.Sprintf("%x", hash)
}
func (c *CacheLoader[T]) Get(ctx context.Context, key any) (T, func()) {
k := c.key(key)
// Return the value if found in the store.
if value, err := c.store.Get(ctx, k); err == nil {
return value, nil
}
// Lock the mutex to access the registry.
c.mu.Lock()
if _, ok := c.registry[k]; ok {
ch := make(chan T, 1)
c.registry[k] = append(c.registry[k], ch)
c.mu.Unlock()
// Wait for the value to be set and return it.
return <-ch, nil
} else {
// Initialize a new channel list if the key is not present.
c.registry[k] = []chan T{}
c.mu.Unlock()
}
var data T
unlock := func() {
c.mu.Lock()
defer c.mu.Unlock()
// Remove the key from the registry when done.
delete(c.registry, k)
}
return data, unlock
}
func (c *CacheLoader[T]) Set(key any, value T) {
k := c.key(key)
c.mu.Lock()
channels, exists := c.registry[k]
c.mu.Unlock()
if exists {
for _, ch := range channels {
// Send the value to waiting channels.
select {
case ch <- value:
// Close the channel after sending the value.
close(ch)
default:
}
}
}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
if err := c.store.Set(ctx, k, value, c.tags); err != nil {
// TODO: Log error
}
}()
}
func (c *CacheLoader[T]) Invalidate(ctx context.Context) error {
return c.store.Invalidate(ctx, c.tags...)
}
package cacheloader
import "context"
type Store[T any] interface {
Get(ctx context.Context, key string) (T, error)
Set(ctx context.Context, key string, value any, tags []string) error
Invalidate(ctx context.Context, tags ...string) error
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment