Skip to content

Commit

Permalink
ttl is time.Duration
Browse files Browse the repository at this point in the history
  • Loading branch information
parMaster committed Apr 26, 2024
1 parent f152816 commit 5d18afb
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 52 deletions.
28 changes: 12 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ import "github.com/parMaster/mcache"
Create a new cache instance using the `NewCache` constructor, and use it to perform cache operations:

```go
cache := mcache.NewCache()
cache := mcache.NewCache[string]()
data, err := cache.Get("key")
if err != nil {
data = ExpensiveFunctionCall()
cache.Set("key", data, 5*60) // cache data for 5 minutes
cache.Set("key", data, 5*time.Minute) // cache data for 5 minutes
}
```
## Examples
Expand All @@ -45,7 +45,7 @@ See the [examples](https://github.com/parMaster/mcache/tree/main/examples) direc
## API Reference
### Set

Set a key-value pair in the cache. The key must be a string, and the value can be any type that implements the `interface{}` interface, ttl is int64 value in seconds:
Set a key-value pair in the cache. The key must be a string, and the value can be any type, ttl is int64 value in seconds:

```go
err := cache.Set("key", "value", 0)
Expand All @@ -59,16 +59,14 @@ If the key already exists and is not expired, an error `mcache.ErrKeyExists` wil
You can also set a key-value pair with an expiration time (in seconds):

```go
err := cache.Set("key", "value", 60)
err := cache.Set("key", "value", time.Minute)
if err != nil {
// handle error
}
```

The value will automatically expire after the specified duration.

_Note that int64 is used instead of time.Duration for the expiration time. This choise is deliberate to simplify the API. It can be changed in the future, I can break the API but only in the next major version._

### Get

Retrieve a value from the cache by key:
Expand Down Expand Up @@ -136,7 +134,7 @@ cache.Cleanup()
WithCleanup is a functional option to the NewCache constructor that allows you to specify a cleanup interval:

```go
cache := mcache.NewCache(mcache.WithCleanup(60)) // cleanup every 60 seconds
cache := mcache.NewCache(mcache.WithCleanup[string](60)) // cleanup every 60 seconds
```
It will basically run a Cleanup method in a goroutine with a time interval.

Expand All @@ -145,25 +143,23 @@ It will basically run a Cleanup method in a goroutine with a time interval.
100% test coverage:

```shell
$ go test -race .
$ go test -cover -race .

ok github.com/parMaster/mcache 8.239s coverage: 100.0% of statements
```
Blinding fast and efficient:

```shell
$ go test -bench . -benchmem

goos: linux
goos: darwin
goarch: amd64
pkg: github.com/parMaster/mcache
cpu: Intel(R) Core(TM) i5-4308U CPU @ 2.80GHz
BenchmarkWrite-4 1797061 815.2 ns/op 247 B/op 3 allocs/op
BenchmarkRead-4 4516329 260.7 ns/op 48 B/op 3 allocs/op
BenchmarkRWD-4 1761019 686.6 ns/op 56 B/op 6 allocs/op
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkWrite-4 1333594 772.8 ns/op 196 B/op 2 allocs/op
BenchmarkRead-4 2578328 490.7 ns/op 15 B/op 1 allocs/op
BenchmarkRWD-4 1277389 939.7 ns/op 47 B/op 5 allocs/op
PASS
github.com/parMaster/mcache coverage: 83.0% of statements
ok github.com/parMaster/mcache 7.190s
ok github.com/parMaster/mcache 19.769s
```

## Contributing
Expand Down
7 changes: 4 additions & 3 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package mcache
import (
"fmt"
"testing"
"time"
)

// BenchmarkWrite
Expand All @@ -11,7 +12,7 @@ func BenchmarkWrite(b *testing.B) {

b.ResetTimer()
for i := 0; i < b.N; i++ {
mcache.Set(fmt.Sprintf("%d", i), i, int64(i))
mcache.Set(fmt.Sprintf("%d", i), i, time.Second)
}
b.StopTimer()
mcache.Cleanup()
Expand All @@ -22,7 +23,7 @@ func BenchmarkRead(b *testing.B) {
mcache := NewCache[int]()

for i := 0; i < b.N; i++ {
mcache.Set(fmt.Sprintf("%d", i), i, int64(i))
mcache.Set(fmt.Sprintf("%d", i), i, time.Minute)
}

b.ResetTimer()
Expand All @@ -39,7 +40,7 @@ func BenchmarkRWD(b *testing.B) {

b.ResetTimer()
for i := 0; i < b.N; i++ {
mcache.Set(fmt.Sprintf("%d", i), i, int64(i))
mcache.Set(fmt.Sprintf("%d", i), i, time.Hour)
mcache.Get(fmt.Sprintf("%d", i))
mcache.Del(fmt.Sprintf("%d", i))
}
Expand Down
4 changes: 2 additions & 2 deletions examples/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ func demo() {

cache := mcache.NewCache[string]()

cache.Set("save indefinitely", "value without expiration", 0) // set value without expiration
cache.Set("save for 1 second", "value will expire in 1 second", 1) // set value with expiration in 1 second
cache.Set("save indefinitely", "value without expiration", 0) // set value without expiration
cache.Set("save for 1 second", "value will expire in 1 second", 1*time.Second) // set value with expiration in 1 second

exists, err := cache.Has("no such key")
// either exists or error can be checked
Expand Down
3 changes: 2 additions & 1 deletion examples/readme_example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"time"

"github.com/parMaster/mcache"
)
Expand All @@ -10,7 +11,7 @@ func main() {

cache := mcache.NewCache[string]()

cache.Set("key", "value", 5*60) // set value with expiration in 5 minutes
cache.Set("key", "value", time.Minute*5) // set value with expiration in 5 minutes

v, err := cache.Get("key")
if err != nil {
Expand Down
30 changes: 17 additions & 13 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const (
// CacheItem is a struct for cache item
type CacheItem[T any] struct {
value T
expiration int64
expiration time.Time
}

// Cache is a struct for cache
Expand Down Expand Up @@ -53,20 +53,22 @@ func NewCache[T any](options ...func(*Cache[T])) *Cache[T] {
// 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[T]) Set(key string, value T, ttl int64) error {
func (c *Cache[T]) Set(key string, value T, ttl time.Duration) error {
var zeroTime time.Time

c.mx.RLock()
d, ok := c.data[key]
cached, ok := c.data[key]
c.mx.RUnlock()
if ok {
if d.expiration == 0 || d.expiration > time.Now().Unix() {
if cached.expiration == zeroTime || cached.expiration.After(time.Now().Add(ttl)) {
return fmt.Errorf(ErrKeyExists)
}
}

var expiration int64
var expiration time.Time

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

c.mx.Lock()
Expand All @@ -80,7 +82,7 @@ func (c *Cache[T]) Set(key string, value T, ttl int64) error {

// 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, but it's expired, delete key, return zeroa value and error
// If key exists and it's not expired, return value
func (c *Cache[T]) Get(key string) (T, error) {
var none T
Expand Down Expand Up @@ -109,7 +111,8 @@ func (c *Cache[T]) Has(key string) (bool, error) {
return false, fmt.Errorf(ErrKeyNotFound)
}

if d.expiration != 0 && d.expiration < time.Now().Unix() {
var zeroTime time.Time
if d.expiration != zeroTime && d.expiration.Before(time.Now()) {
c.mx.Lock()
delete(c.data, key)
c.mx.Unlock()
Expand Down Expand Up @@ -143,21 +146,22 @@ func (c *Cache[T]) Clear() error {
// Cleanup is a method for deleting expired keys
func (c *Cache[T]) Cleanup() {
c.mx.Lock()
var zeroTime time.Time
for k, v := range c.data {
if v.expiration != 0 && v.expiration < time.Now().Unix() {
if v.expiration != zeroTime && v.expiration.Before(time.Now()) {
delete(c.data, k)
}
}
c.mx.Unlock()
}

// WithCleanup is a functional option for setting interval and starting Cleanup goroutine
func WithCleanup[T any](ttl int64) func(*Cache[T]) {
// WithCleanup is a functional option for setting interval to run Cleanup goroutine
func WithCleanup[T any](ttl time.Duration) func(*Cache[T]) {
return func(c *Cache[T]) {
go func() {
for {
c.Cleanup()
time.Sleep(time.Duration(ttl) * time.Second)
time.Sleep(ttl)
}
}()
}
Expand Down
35 changes: 18 additions & 17 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
type testItem struct {
key string
value string
ttl int64
ttl time.Duration
}

func Test_SimpleTest_Mcache(t *testing.T) {
Expand All @@ -22,14 +22,14 @@ func Test_SimpleTest_Mcache(t *testing.T) {
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},
{"key0", "value0", time.Second * 0},
{"key1", "value1", time.Second * 1},
{"key2", "value2", time.Second * 20},
{"key3", "value3", time.Second * 30},
{"key4", "value4", time.Second * 40},
{"key5", "value5", time.Second * 50},
{"key6", "value6", time.Second * 60},
{"key7", "value7", time.Second * 70000000},
}
noSuchKey := "noSuchKey"

Expand All @@ -40,8 +40,8 @@ func Test_SimpleTest_Mcache(t *testing.T) {

for _, item := range testItems {
value, err := c.Get(item.key)
assert.NoError(t, err)
assert.Equal(t, item.value, value)
assert.NoError(t, err, fmt.Sprintf("key:%s; val:%s; ttl:%d", item.key, item.value, item.ttl))
assert.Equal(t, item.value, value, fmt.Sprintf("key:%s; val:%s; ttl:%d", item.key, item.value, item.ttl))
}

_, err := c.Get(noSuchKey)
Expand Down Expand Up @@ -74,9 +74,9 @@ func Test_SimpleTest_Mcache(t *testing.T) {
assert.Equal(t, ErrKeyNotFound, err.Error())
}

c.Set("key", "value", 1)
c.Set("key", "value", time.Second*1)
time.Sleep(time.Second * 2)
err = c.Set("key", "newvalue", 1)
err = c.Set("key", "newvalue", time.Second*1)
assert.NoError(t, err)

// old value should be rewritten
Expand All @@ -85,10 +85,11 @@ func Test_SimpleTest_Mcache(t *testing.T) {
assert.Equal(t, "newvalue", value)

err = c.Set("key", "not a newer value", 1)
assert.Equal(t, ErrKeyExists, err.Error())

if err != nil {
assert.Equal(t, ErrKeyExists, err.Error())
}
time.Sleep(time.Second * 2)
err = c.Set("key", "even newer value", 1)
err = c.Set("key", "even newer value", time.Second*1)
// key should be silently rewritten
assert.NoError(t, err)
value, err = c.Get("key")
Expand Down Expand Up @@ -148,7 +149,7 @@ func TestConcurrentSetAndGet(t *testing.T) {

// TestWithCleanup tests that the cleanup goroutine is working
func TestWithCleanup(t *testing.T) {
cache := NewCache(WithCleanup[string](1))
cache := NewCache(WithCleanup[string](time.Second * 1))

// Set a value with a TTL of 1 second
err := cache.Set("key", "value", 1)
Expand Down

0 comments on commit 5d18afb

Please sign in to comment.