3 files changed,
125 insertions(+),
61 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-02-21 19:09:30 +0200
Authored at:
2026-02-21 19:06:50 +0200
Change ID:
kzxlnzxxplyowxpzrzvzzmxmylstrsvz
Parent:
9d07e8a
M
internal/cache/cache.go
路路路 2 2 3 3 import ( 4 4 "errors" 5 - "sync" 6 - "time" 7 5 ) 8 6 9 7 var ErrNotFound = errors.New("not found") 路路路 12 10 Set(key string, val T) 13 11 Get(key string) (val T, found bool) 14 12 } 15 - 16 -type item[T any] struct { 17 - v T 18 - expiry time.Time 19 -} 20 - 21 -func (i item[T]) isExpired() bool { 22 - return time.Now().After(i.expiry) 23 -} 24 - 25 -type InMemory[T any] struct { 26 - mu sync.RWMutex 27 - ttl time.Duration 28 - data map[string]item[T] 29 -} 30 - 31 -func NewInMemory[T any](ttl time.Duration) *InMemory[T] { 32 - c := &InMemory[T]{ 33 - data: make(map[string]item[T]), 34 - ttl: ttl, 35 - } 36 - 37 - go c.clean() 38 - return c 39 -} 40 - 41 -func (m *InMemory[T]) Set(key string, val T) { 42 - m.mu.Lock() 43 - defer m.mu.Unlock() 44 - m.data[key] = item[T]{ 45 - v: val, 46 - expiry: time.Now().Add(m.ttl), 47 - } 48 -} 49 - 50 -func (m *InMemory[T]) Get(key string) (T, bool) { 51 - m.mu.Lock() 52 - defer m.mu.Unlock() 53 - 54 - val, found := m.data[key] 55 - if !found || val.isExpired() { 56 - var t T 57 - return t, false 58 - } 59 - return val.v, true 60 -} 61 - 62 -func (m *InMemory[T]) clean() { 63 - for range time.Tick(5 * time.Second) { 64 - m.mu.Lock() 65 - for k, v := range m.data { 66 - if v.isExpired() { 67 - delete(m.data, k) 68 - } 69 - } 70 - 71 - m.mu.Unlock() 72 - } 73 -}
A
internal/cache/in_memory.go
路路路 1 +package cache 2 + 3 +import ( 4 + "sync" 5 + "time" 6 +) 7 + 8 +type item[T any] struct { 9 + v T 10 + expiry time.Time 11 +} 12 + 13 +func (i item[T]) isExpired() bool { 14 + return time.Now().After(i.expiry) 15 +} 16 + 17 +type InMemory[T any] struct { 18 + mu sync.RWMutex 19 + ttl time.Duration 20 + data map[string]item[T] 21 +} 22 + 23 +func NewInMemory[T any](ttl time.Duration) *InMemory[T] { 24 + c := &InMemory[T]{ 25 + data: make(map[string]item[T]), 26 + ttl: ttl, 27 + } 28 + 29 + go c.clean() 30 + return c 31 +} 32 + 33 +func (m *InMemory[T]) Set(key string, val T) { 34 + m.mu.Lock() 35 + defer m.mu.Unlock() 36 + m.data[key] = item[T]{ 37 + v: val, 38 + expiry: time.Now().Add(m.ttl), 39 + } 40 +} 41 + 42 +func (m *InMemory[T]) Get(key string) (T, bool) { 43 + m.mu.Lock() 44 + defer m.mu.Unlock() 45 + 46 + val, found := m.data[key] 47 + if !found || val.isExpired() { 48 + var t T 49 + return t, false 50 + } 51 + return val.v, true 52 +} 53 + 54 +func (m *InMemory[T]) clean() { 55 + for range time.Tick(5 * time.Second) { 56 + m.mu.Lock() 57 + for k, v := range m.data { 58 + if v.isExpired() { 59 + delete(m.data, k) 60 + } 61 + } 62 + m.mu.Unlock() 63 + } 64 +}
A
internal/cache/in_memory_test.go
路路路 1 +package cache 2 + 3 +import ( 4 + "fmt" 5 + "sync" 6 + "testing" 7 + "testing/synctest" 8 + "time" 9 + 10 + "olexsmir.xyz/x/is" 11 +) 12 + 13 +func TestInMemory_Set(t *testing.T) { 14 + c := NewInMemory[string](time.Minute) 15 + t.Run("sets", func(t *testing.T) { 16 + c.Set("asdf", "qwer") 17 + is.Equal(t, c.data["asdf"].v, "qwer") 18 + }) 19 + t.Run("overwrites prev value", func(t *testing.T) { 20 + c.Set("asdf", "one") 21 + c.Set("asdf", "two") 22 + is.Equal(t, c.data["asdf"].v, "two") 23 + }) 24 +} 25 + 26 +func TestInMemory_Get(t *testing.T) { 27 + c := NewInMemory[string](time.Minute) 28 + 29 + t.Run("hit", func(t *testing.T) { 30 + c.Set("asdf", "qwer") 31 + v, found := c.Get("asdf") 32 + is.Equal(t, true, found) 33 + is.Equal(t, "qwer", v) 34 + }) 35 + t.Run("miss", func(t *testing.T) { 36 + _, found := c.Get("missing") 37 + is.Equal(t, false, found) 38 + }) 39 + t.Run("expired item", func(t *testing.T) { 40 + synctest.Test(t, func(t *testing.T) { 41 + c.Set("asdf", "qwer") 42 + time.Sleep(2 * time.Minute) 43 + v, found := c.Get("asdf") 44 + is.Equal(t, false, found) 45 + is.Equal(t, "", v) 46 + }) 47 + }) 48 +} 49 + 50 +func TestInMemory_ConcurrentSetGet(t *testing.T) { 51 + c := NewInMemory[int](time.Minute) 52 + synctest.Test(t, func(t *testing.T) { 53 + var wg sync.WaitGroup 54 + for i := range 50 { 55 + key := fmt.Sprintf("key-%d", i) 56 + wg.Go(func() { c.Set(key, i) }) 57 + wg.Go(func() { c.Get(key) }) 58 + } 59 + wg.Wait() 60 + }) 61 +}