Compare commits

...

7 commits

Author SHA1 Message Date
9d6d9b9dff
chore: finalize v0.1.0
Some checks failed
/ tests (push) Has been cancelled
/ linting (push) Has been cancelled
2025-01-18 01:11:06 +01:00
5ebd13ed76
feat: add ci config for forgejo
tmp
2025-01-18 01:11:06 +01:00
d61b3d6865
feat: use generics 2025-01-18 01:08:59 +01:00
cb38484486
fix: use pointers for expiration date 2025-01-18 01:08:59 +01:00
a3b5b9b1d9 Add code navigation to CI 2021-11-20 10:39:16 +01:00
20415d82ed Add changelog, license and readme 2021-11-20 10:37:11 +01:00
5611558820 Add CI configuration 2021-11-20 10:37:02 +01:00
9 changed files with 2271 additions and 671 deletions

View file

@ -0,0 +1,20 @@
on: [push]
jobs:
linting:
runs-on: docker
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- uses: https://github.com/golangci/golangci-lint-action@v6
tests:
runs-on: docker
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: Run tests
run: go test

File diff suppressed because it is too large Load diff

7
CHANGELOG.md Normal file
View file

@ -0,0 +1,7 @@
# Changelog
## Unreleased
## v0.1.0 (2025-01-18)
* First version

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
MIT License Copyright (c) 2021 Bruno Carlin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell c
pies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

51
README.md Normal file
View file

@ -0,0 +1,51 @@
# go/cache
Cache provides an in-memory key-value store used to cache arbitrary data.
It supports entry expiration.
## Quickstart
You can add `cache` to your project using `go get`:
go get code.bcarlin.xyz/go/cache
You can then import it in your code:
import "code.bcarlin.xyz/go/cache"
## Tests
To run the tests, run the command:
go test
## Usage
The API is very light and straight forward:
```go
// Initialize a cache
c := cache.New()
// Store some values. Keys must be strings.
c.Put("foo", "my data")
c.Put("the-answer", 42)
// Store some more data with an ewpiration date
c.PutTTL("not-the-answer", 144, 1*time.Hour)
// Retrieve some data
val, ok := c.Get("the-answer")
if !ok {
fmt.Println("cache missed")
}
// Delete entries
c.Del("foo")
c.Del("does-not-exist") // does not fail
```
## License
[MIT](https://choosealicense.com/licenses/mit/)

View file

@ -1,7 +1,9 @@
// Package cache defines an in-memory key-value store.
//
// It supports exîration dates andcan store arbitrary values of any type.
// It supports exîration dates and can store arbitrary values of any type.
// Keys must be strings.
//
// Cache values are safe to share between goroutines.
package cache
import (
@ -9,75 +11,83 @@ import (
"time"
)
type entry struct {
expirationDate time.Time
value interface{}
type entry[V any] struct {
expirationDate *time.Time
value V
}
func (e entry[V]) isExpired() bool {
return e.expirationDate != nil && e.expirationDate.Before(time.Now())
}
// Cache is an in-memory key-value store.
type Cache struct {
data map[string]entry
type Cache[K comparable, V any] struct {
data map[K]entry[V]
mu *sync.RWMutex
}
// New instantiate a new cache.
func New() *Cache {
return &Cache{
data: make(map[string]entry),
func New[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{
data: make(map[K]entry[V]),
mu: &sync.RWMutex{},
}
}
// Put stores a value in the cache under the given key.
func (c *Cache) Put(key string, val interface{}) {
func (c *Cache[K, V]) Put(key K, val V) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = entry{time.Time{}, val}
c.data[key] = entry[V]{nil, val}
}
// PutTTL stores a value in the cache under the given key. The value will
// be expired after the given ttl.
//
// A 0 ttl value disables the expiration of the value.
func (c *Cache) PutTTL(key string, val interface{}, ttl time.Duration) {
var exp time.Time
if ttl == 0 {
exp = time.Time{}
} else {
exp = time.Now().Add(ttl)
}
func (c *Cache[K, V]) PutTTL(key K, val V, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = entry{exp, val}
if ttl == 0 {
c.Put(key, val)
return
}
exp := time.Now().Add(ttl)
c.data[key] = entry[V]{&exp, val}
}
// Get returns the value asspciated with the given key.
// The second return values indicates if the cache hs been hit or not.
func (c Cache) Get(key string) (interface{}, bool) {
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
v, ok := c.data[key]
entry, ok := c.data[key]
c.mu.RUnlock()
if !ok {
return nil, false
var t V
return t, false
}
if !v.expirationDate.IsZero() && v.expirationDate.Before(time.Now()) {
if entry.isExpired() {
c.Del(key)
return nil, false
var t V
return t, false
}
return v.value, ok
return entry.value, ok
}
// Del deletes the entry for the givzn key.
// Del deletes the entry for the given key.
// It does not fail if the key does not exist.
func (c *Cache) Del(key string) {
func (c *Cache[K, V]) Del(key K) {
c.mu.Lock()
defer c.mu.Unlock()
@ -85,7 +95,7 @@ func (c *Cache) Del(key string) {
}
// Count returns the total number of entries in the cache (vamid and expired).
func (c Cache) Count() int {
func (c *Cache[K, V]) Count() int {
c.mu.RLock()
defer c.mu.RUnlock()

View file

@ -1,156 +1,156 @@
package cache
package cache_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"code.bcarlin.net/go/cache"
)
func TestCache(t *testing.T) {
key := "test"
t.Parallel()
c := New()
const key = "test"
t.Log("simple put and get")
t.Run("simple put and get", func(t *testing.T) {
t.Parallel()
value := "foo"
c := cache.New[string, string]()
c.Put(key, value)
value := "foo"
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
c.Put(key, value)
val, ok := c.Get(key)
if !ok {
t.Fatalf("Expected cache to have an entry for key %q, but it did not", key)
}
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
val2, ok := val.(string)
if !ok {
t.Fatalf("Expected '%v' to have type string, but it has type %T", val, val)
}
got, ok := c.Get(key)
assert.True(t, ok, "Expected cache to have an entry for key %q, but it did not", key)
if val2 != value {
t.Fatalf("Expected '%v' to equal 'foo', but it does not", val2)
}
assert.Equal(t, value, got)
})
t.Log("Using the same key overwrites the value")
t.Run("Using the same key overwrites the value", func(t *testing.T) {
t.Parallel()
value = "bar"
c := cache.New[string, string]()
c.Put(key, value)
c.Put(key, "foo")
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
value := "bar"
c.Put(key, value)
val, ok = c.Get(key)
if !ok {
t.Fatalf("Expected cache to have an entry for key %q, but it did not", key)
}
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
val2, ok = val.(string)
if !ok {
t.Fatalf("Expected '%v' to have type string, but it has type %T", val, val)
}
got, ok := c.Get(key)
assert.True(t, ok, "Expected cache to have an entry for key %q, but it did not", key)
if val2 != value {
t.Fatalf("Expected '%v' to equal 'foo', but it does not", val2)
}
assert.Equal(t, value, got)
})
t.Log("It should tell if the cache is missed")
t.Run("cache is missed", func(t *testing.T) {
t.Parallel()
val, ok = c.Get("does-not-exist")
if ok {
t.Fatalf("Expected cache to not have an entry for key %q, but it did", key)
}
c := cache.New[string, string]()
if val != nil {
t.Fatalf("Expected '%v' to be nil, but it was not", val2)
}
val, ok := c.Get("does-not-exist")
assert.False(t, ok, "Expected cache to not have an entry for key 'does-not-exist', but it did")
assert.Zero(t, val)
})
}
func TestCacheTTL(t *testing.T) {
key := "test"
value := "foo"
t.Parallel()
c := New()
const (
key = "test"
value = "foo"
)
t.Log("Simple put and get a non-expired entry")
t.Run("simple put and get a non-expired entry", func(t *testing.T) {
t.Parallel()
c.PutTTL(key, value, 1*time.Hour)
c := cache.New[string, string]()
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
c.PutTTL(key, value, 1*time.Hour)
_, ok := c.Get(key)
if !ok {
t.Fatalf("Expected cache to have an entry for key %q, but it did not", key)
}
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
t.Log("Simple put and get a 0 TTL")
_, ok := c.Get(key)
assert.True(t, ok, "Expected cache to have an entry for key %q, but it did not", key)
})
c.PutTTL(key, value, 0)
t.Run("simple put and get a 0 TTL", func(t *testing.T) {
t.Parallel()
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
c := cache.New[string, string]()
_, ok = c.Get(key)
if !ok {
t.Fatalf("Expected cache to have an entry for key %q, but it did not", key)
}
c.PutTTL(key, value, 0)
t.Log("Simple put and get an expired entry")
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
c.PutTTL(key, value, -1*time.Hour)
_, ok := c.Get(key)
assert.True(t, ok, "Expected cache to have an entry for key %q, but it did not", key)
})
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
t.Run("simple put and get an expired entry", func(t *testing.T) {
t.Parallel()
_, ok = c.Get(key)
if ok {
t.Fatalf("Expected cache to not have an entry for key %q, but it did", key)
}
c := cache.New[string, string]()
if n := c.Count(); n != 0 {
t.Fatalf("Expected %d entries in the cache, got %d", 0, n)
}
c.PutTTL(key, value, -1*time.Hour)
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
_, ok := c.Get(key)
assert.False(t, ok, "Expected cache to not have an entry for key %q, but it did", key)
n = c.Count()
assert.Equal(t, 0, n, "Expected %d entries in the cache, got %d", 0, n)
})
}
func TestCacheDel(t *testing.T) {
key := "test"
value := "foo"
t.Parallel()
c := New()
const (
key = "test"
value = "foo"
)
c.Put(key, value)
t.Run("delete an existing key", func(t *testing.T) {
t.Parallel()
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
c := cache.New[string, string]()
t.Log("Delete an existing key from the cache")
c.Put(key, value)
c.Del(key)
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
if n := c.Count(); n != 0 {
t.Fatalf("Expected %d entries in the cache, got %d", 0, n)
}
c.Del(key)
c.Put(key, value)
n = c.Count()
assert.Equal(t, 0, n, "Expected %d entries in the cache, got %d", 0, n)
})
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
t.Run("delete a non-existing key", func(t *testing.T) {
t.Parallel()
t.Log("Delete an existing key from the cache")
c := cache.New[string, string]()
c.Del("does-not-exist")
c.Put(key, value)
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
c.Del("does-not-exist")
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
})
}

14
go.mod
View file

@ -1,3 +1,13 @@
module code.bcarlin.xyz/go/cache
module code.bcarlin.net/go/cache
go 1.17
go 1.22.0
toolchain go1.23.4
require github.com/stretchr/testify v1.10.0
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
go.sum Normal file
View file

@ -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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=