all repos

mugit @ 75a25c9f1746e2a04b8748ac97fdceac8caf87f6

馃惍 git server that your cow will love
3 files changed, 125 insertions(+), 61 deletions(-)
cache: refactor; tests
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
        +}