Skip to content

Commit

Permalink
simple memory cache with ttl
Browse files Browse the repository at this point in the history
  • Loading branch information
parMaster committed Jul 10, 2023
1 parent 5b7ad0e commit c593a5c
Show file tree
Hide file tree
Showing 47 changed files with 20,048 additions and 0 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Go

on:
push:
branches: [ "workflows", "main" ]
pull_request:
branches: [ "workflows", "main" ]

jobs:

build:
runs-on: ubuntu-latest
steps:

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: "1.20"

- name: Checkout
uses: actions/checkout@v3

- name: Test
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
env:
GOFLAGS: "-mod=vendor"
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module github.com/parMaster/mcache

go 1.20

require github.com/stretchr/testify v1.8.4

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
145 changes: 145 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package mcache

import (
"fmt"
"sync"
"time"
)

// Errors for cache
const (
ErrKeyNotFound = "Key not found"
ErrKeyExists = "Key already exists"
ErrExpired = "Key expired"
)

// CacheItem is a struct for cache item
type CacheItem struct {
value interface{}
expiration int64
}

// Cache is a struct for cache
type Cache struct {
data map[string]CacheItem
mx sync.RWMutex
}

// Cacher is an interface for cache
type Cacher interface {
Set(key string, value interface{}, ttl int64) error
Get(key string) (interface{}, error)
Has(key string) (bool, error)
Del(key string) error
Cleanup()
Clear() error
}

// NewCache is a constructor for Cache
func NewCache() *Cache {
return &Cache{
data: make(map[string]CacheItem),
}
}

// Set is a method for setting key-value pair
// If key already exists, and it's not expired, return error
// If key already exists, but it's expired, set new value and return nil
// If key doesn't exist, set new value and return nil
// If ttl is 0, set value without expiration
func (c *Cache) Set(key string, value interface{}, ttl int64) error {
c.mx.RLock()
d, ok := c.data[key]
c.mx.RUnlock()
if ok {
if d.expiration == 0 || d.expiration > time.Now().Unix() {
return fmt.Errorf(ErrKeyExists)
}
}

var expiration int64

if ttl > 0 {
expiration = time.Now().Unix() + ttl
}

c.mx.Lock()
c.data[key] = CacheItem{
value: value,
expiration: expiration,
}
c.mx.Unlock()
return nil
}

// Get is a method for getting value by key
// If key doesn't exist, return error
// If key exists, but it's expired, return error and delete key
// If key exists and it's not expired, return value
func (c *Cache) Get(key string) (interface{}, error) {

_, err := c.Has(key)
if err != nil {
return nil, err
}

// safe return?
c.mx.RLock()
defer c.mx.RUnlock()

return c.data[key].value, nil
}

// Has is a method for checking if key exists.
// If key doesn't exist, return false.
// If key exists, but it's expired, return false and delete key.
// If key exists and it's not expired, return true.
func (c *Cache) Has(key string) (bool, error) {
c.mx.RLock()
d, ok := c.data[key]
c.mx.RUnlock()
if !ok {
return false, fmt.Errorf(ErrKeyNotFound)
}

if d.expiration != 0 && d.expiration < time.Now().Unix() {
c.mx.Lock()
delete(c.data, key)
c.mx.Unlock()
return false, fmt.Errorf(ErrExpired)
}

return true, nil
}

// Del is a method for deleting key-value pair
func (c *Cache) Del(key string) error {
_, err := c.Has(key)
if err != nil {
return err
}

c.mx.Lock()
delete(c.data, key)
c.mx.Unlock()
return nil
}

// Clear is a method for clearing cache
func (c *Cache) Clear() error {
c.mx.Lock()
c.data = make(map[string]CacheItem)
c.mx.Unlock()
return nil
}

// Cleanup is a method for deleting expired keys
func (c *Cache) Cleanup() {
c.mx.Lock()
for k, v := range c.data {
if v.expiration != 0 && v.expiration < time.Now().Unix() {
delete(c.data, k)
}
}
c.mx.Unlock()
}
122 changes: 122 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package mcache

import (
"fmt"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

type testItem struct {
key string
value interface{}
ttl int64
}

func Test_SimpleTest_Mcache(t *testing.T) {
c := NewCache()

assert.NotNil(t, c)
assert.IsType(t, &Cache{}, c)
assert.NotNil(t, c.data)

testItems := []testItem{
{"key0", "value0", 0},
{"key1", "value1", 1},
{"key2", "value2", 20},
{"key3", "value3", 30},
{"key4", "value4", 40},
{"key5", "value5", 50},
{"key6", "value6", 60},
{"key7", "value7", 70000000},
}
noSuchKey := "noSuchKey"

for _, item := range testItems {
err := c.Set(item.key, item.value, item.ttl)
assert.NoError(t, err)
}

for _, item := range testItems {
value, err := c.Get(item.key)
assert.NoError(t, err)
assert.Equal(t, item.value, value)
}

_, err := c.Get(noSuchKey)
assert.Error(t, err)
assert.Equal(t, ErrKeyNotFound, err.Error())

for _, item := range testItems {
has, err := c.Has(item.key)
assert.NoError(t, err)
assert.True(t, has)
}

time.Sleep(time.Second * 2)

has, err := c.Has(testItems[1].key)
assert.Error(t, err)
assert.Equal(t, ErrExpired, err.Error())
assert.False(t, has)

testItems = append(testItems[2:], testItems[0])
for _, item := range testItems {
err := c.Del(item.key)
assert.NoError(t, err)
}

for _, item := range testItems {
has, err := c.Has(item.key)
assert.False(t, has)
assert.Error(t, err)
assert.Equal(t, ErrKeyNotFound, err.Error())
}

c.Cleanup()

err = c.Clear()
assert.NoError(t, err)
}

func TestConcurrentSetAndGet(t *testing.T) {
cache := NewCache()

// Start multiple goroutines to concurrently set and get values
numGoroutines := 10000
done := make(chan bool)

for i := 0; i < numGoroutines; i++ {
go func(index int) {
key := fmt.Sprintf("key-%d", index)
value := fmt.Sprintf("value-%d", index)

err := cache.Set(key, value, 0)
if err != nil {
t.Errorf("Error setting value for key %s: %s", key, err)
}

result, err := cache.Get(key)
if err != nil {
t.Errorf("Error getting value for key %s: %s", key, err)
}

if result != value {
t.Errorf("Expected value %s for key %s, but got %s", value, key, result)
}

done <- true
}(i)
}

// Wait for all goroutines to finish
for i := 0; i < numGoroutines; i++ {
<-done
}
}

func TestMain(m *testing.M) {
// Enable the race detector
m.Run()
}
15 changes: 15 additions & 0 deletions vendor/github.com/davecgh/go-spew/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c593a5c

Please sign in to comment.