Skip to content

Commit

Permalink
v2: ttl is time.Duration, generics instead of interface{}
Browse files Browse the repository at this point in the history
  • Loading branch information
parMaster committed Apr 27, 2024
1 parent 034f528 commit 6e9cc8c
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 81 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](time.Minute)) // 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
21 changes: 16 additions & 5 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,47 @@ package mcache
import (
"fmt"
"testing"
"time"
)

var mcache *Cache

// BenchmarkWrite
func BenchmarkWrite(b *testing.B) {
mcache = NewCache()
mcache := NewCache[int]()

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()
}

// BenchmarkRead
func BenchmarkRead(b *testing.B) {
mcache := NewCache[int]()

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

b.ResetTimer()
for i := 0; i < b.N; i++ {
mcache.Get(fmt.Sprintf("%d", i))
}
b.StopTimer()
mcache.Clear()
}

// BenchmarkRW
func BenchmarkRWD(b *testing.B) {
mcache := NewCache[int]()

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))
}
b.StopTimer()
mcache.Clear()
}
6 changes: 3 additions & 3 deletions examples/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ func demo() {
fmt.Println("\r\n Demo 1:")
fmt.Println("------------------------------------------------------------")

cache := mcache.NewCache()
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
6 changes: 3 additions & 3 deletions examples/hit_or_miss.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/parMaster/mcache"
)

func expensive_func() interface{} {
func expensive_func() string {
time.Sleep(1 * time.Second)
return "expensive result"
}
Expand All @@ -16,7 +16,7 @@ func demo_hit_or_miss() {
fmt.Println("\r\n Hit or Miss:")
fmt.Println("------------------------------------------------------------")

cache := mcache.NewCache()
cache := mcache.NewCache[string]()
// expensive_func will be called only once
// because result will be saved in cache
for i := 0; i < 10; i++ {
Expand All @@ -27,6 +27,6 @@ func demo_hit_or_miss() {
cache.Set("expensive value", v, 0)
continue
}
fmt.Println("cache hit - " + v.(string))
fmt.Println("cache hit - " + v)
}
}
9 changes: 5 additions & 4 deletions examples/readme_example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@ package main

import (
"fmt"
"time"

"github.com/parMaster/mcache"
)

func main() {

cache := mcache.NewCache()
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 {
// either error can be checked
fmt.Println(err)
}
if v != nil {
// or value can be checked for nil
if v != "" {
// or value can be checked for "empty" type value
fmt.Println("key =", v)
}

Expand Down
63 changes: 34 additions & 29 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ const (
)

// CacheItem is a struct for cache item
type CacheItem struct {
value interface{}
expiration int64
type CacheItem[T any] struct {
value T
expiration time.Time
}

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

Expand All @@ -36,9 +36,9 @@ type Cacher interface {
}

// NewCache is a constructor for Cache
func NewCache(options ...func(*Cache)) *Cache {
c := &Cache{
data: make(map[string]CacheItem),
func NewCache[T any](options ...func(*Cache[T])) *Cache[T] {
c := &Cache[T]{
data: make(map[string]CacheItem[T]),
}

for _, option := range options {
Expand All @@ -53,24 +53,26 @@ func NewCache(options ...func(*Cache)) *Cache {
// 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 {
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()
c.data[key] = CacheItem{
c.data[key] = CacheItem[T]{
value: value,
expiration: expiration,
}
Expand All @@ -80,13 +82,14 @@ func (c *Cache) Set(key string, value interface{}, 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) Get(key string) (interface{}, error) {
func (c *Cache[T]) Get(key string) (T, error) {
var none T

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

// safe return?
Expand All @@ -100,15 +103,16 @@ func (c *Cache) Get(key string) (interface{}, error) {
// 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) {
func (c *Cache[T]) 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() {
var zeroTime time.Time
if d.expiration != zeroTime && d.expiration.Before(time.Now()) {
c.mx.Lock()
delete(c.data, key)
c.mx.Unlock()
Expand All @@ -119,7 +123,7 @@ func (c *Cache) Has(key string) (bool, error) {
}

// Del is a method for deleting key-value pair
func (c *Cache) Del(key string) error {
func (c *Cache[T]) Del(key string) error {
_, err := c.Has(key)
if err != nil {
return err
Expand All @@ -132,31 +136,32 @@ func (c *Cache) Del(key string) error {
}

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

// Cleanup is a method for deleting expired keys
func (c *Cache) Cleanup() {
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(ttl int64) func(*Cache) {
return func(c *Cache) {
// 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
Loading

0 comments on commit 6e9cc8c

Please sign in to comment.