Skip to content

Commit

Permalink
init wfcache
Browse files Browse the repository at this point in the history
  • Loading branch information
juliaqiuxy committed May 31, 2021
0 parents commit d1f0898
Show file tree
Hide file tree
Showing 9 changed files with 975 additions and 0 deletions.
9 changes: 9 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The MIT License (MIT)

Copyright (c) 2021 Julia Qiu

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 copies 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.
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Waterfall Cache

[![GoDoc](https://godoc.org/github.com/juliaqiuxy/wfcache?status.svg)](https://godoc.org/github.com/juliaqiuxy/wfcache) [![npm](https://img.shields.io/github/license/juliaqiuxy/wfcache.svg?style=flat-square)](https://github.com/juliaqiuxy/wfcache/blob/master/LICENSE.md)

wfcache is a multi-layered cache with waterfall hit propagation and built-in storage adapters for DynamoDB, Redis, BigCache (in-memory)

> This project is under active development. Use at your own risk.
wfcache is effective for read-heavy workloads and it can be used both as a side-cache or a read-through/write-through cache.

## Built-in Storage Adapters

| Package | Description | Eviction strategy
| --- | --- | --- |
| [Basic](basic/basic.go) | Basic in-memory storage | TTL (enforced on get) |
| [BigCache](https://github.com/allegro/bigcache) | BigCache | TTL/LRU |
| [Redis](https://github.com/go-redis/redis) | Redis | TTL/LRU |
| [DynamoDB](https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb) | DynamoDB | [TTL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/howitworks-ttl.html) |

## Installation

To retrieve wfcache, run:

```sh
$ go get github.com/juliaqiuxy/wfcache
```

To update to the latest version, run:

```sh
$ go get -u github.com/juliaqiuxy/wfcache
```

### Usage

```go
import "github.com/juliaqiuxy/wfcache"

wfcache.Create(
onStartOperation,
onFinishOperation,
basicAdapter.Create(time.Minute),
bigCacheAdapter.Create(time.Hour),
dynamodbAdapter.Create(dynamodbClient, "my-cache-table", 3, 3, 24 * time.Hour),
)
```

## How it works

The following steps outline how reads from wfcache work:

- When getting a value, wfcache tries to read it from the first storage layer (e.g. BigCache).
- If the storage layer is not populated with the requested key-value pair (cache miss), transparent to the application, wfcache notes the missing key and moves on to the next layer. This continues until all configured storage options are exhausted.
- When there is a cache hit, wfcache then primes each storage layer with a previously reported cache miss to make the data available for any subsequent reads.
- wfcache returns the key-value pair back to the application

If you want to use wfcache as read-through cache, you can implement a [custom adapter](#implementing-custom-adapters) for your source database and configure it as the last storage layer. In this setup, a cache miss only ever happens in intermediate storage layers (which are then primed as your source storage resolves values) but wfcache would always yield data.

When mutating wfcache, key-value pairs are written and removed from all storage layers. To mutate a specific storage layer in isolation, you can keep a refernece to it. However, this is not recommended as the interface is subject to change.

### Cache eviction

wfcache leaves it up to each storage layer to implement their eviction strategy. Built-in adapters use a combination of Time-to-Live (TTL) and Least Recently Used (LRU) algorithm to decide which items to evict.

Also note that the built-in Basic storage is not meant for production use as the TTL enforcement only happens if and when a "stale" item is requested form the storage layer.

## Implementing Custom Adapters

For use cases where:

- you require a stroge adapter which is not [included](#built-in-storage-adapters) in wfcache, or
- you want to use wfcache as a read-through/write-through cache

it is trivial to extend wfcache by implementing the following adapter interface:

```go
type Storage interface {
Get(ctx context.Context, key string) *Metadata
BatchGet(ctx context.Context, keys []string) []*Metadata
Set(ctx context.Context, key string, value []byte) error
BatchSet(ctx context.Context, pairs map[string][]byte) error
Del(ctx context.Context, key string) error
}
```
107 changes: 107 additions & 0 deletions basic/basic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package memory

import (
"context"
"errors"
"sync"
"time"

"github.com/juliaqiuxy/wfcache"
)

const NoTTL time.Duration = -1

type BasicStorage struct {
pairs map[string]*wfcache.CacheItem
ttl time.Duration

mutex sync.RWMutex
}

func Create(ttl time.Duration) wfcache.StorageMaker {
return func() (wfcache.Storage, error) {
if ttl == 0 {
return nil, errors.New("basic: storage requires a ttl")
}

s := &BasicStorage{
pairs: make(map[string]*wfcache.CacheItem),
ttl: ttl,
}

return s, nil
}
}

func (s *BasicStorage) TimeToLive() time.Duration {
return s.ttl
}

func (s *BasicStorage) Get(ctx context.Context, key string) *wfcache.CacheItem {
s.mutex.RLock()
defer s.mutex.RUnlock()

m, found := s.pairs[key]

if found {
if s.ttl == NoTTL || time.Now().UTC().Before(time.Unix(m.ExpiresAt, 0)) {
return m
} else {
s.Del(ctx, key)
}
}

return nil
}

func (s *BasicStorage) BatchGet(ctx context.Context, keys []string) (results []*wfcache.CacheItem) {
s.mutex.RLock()
defer s.mutex.RUnlock()

for _, key := range keys {
m := s.Get(ctx, key)

if m != nil {
results = append(results, m)
}
}

return results
}

func (s *BasicStorage) Set(ctx context.Context, key string, data []byte) error {
s.mutex.Lock()
defer s.mutex.Unlock()

s.pairs[key] = &wfcache.CacheItem{
Key: key,
Value: data,
ExpiresAt: time.Now().UTC().Add(s.ttl).Unix(),
}

return nil
}

func (s *BasicStorage) BatchSet(ctx context.Context, pairs map[string][]byte) error {
s.mutex.Lock()
defer s.mutex.Unlock()

for key, data := range pairs {
s.pairs[key] = &wfcache.CacheItem{
Key: key,
Value: data,
ExpiresAt: time.Now().UTC().Add(s.ttl).Unix(),
}
}

return nil
}

func (s *BasicStorage) Del(ctx context.Context, key string) error {
s.mutex.Lock()
defer s.mutex.Unlock()

delete(s.pairs, key)

return nil
}
44 changes: 44 additions & 0 deletions bigcache/bigcache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package bigcache

// TODO(juliaqiuxy) Implement using https://github.com/allegro/bigcache

import (
"context"
"time"

"github.com/juliaqiuxy/wfcache"
)

type BigCacheStorage struct{}

func Create(ttl time.Duration) wfcache.StorageMaker {
return func() (wfcache.Storage, error) {
s := &BigCacheStorage{}

return s, nil
}
}

func (s *BigCacheStorage) TimeToLive() time.Duration {
return 0
}

func (s *BigCacheStorage) Get(ctx context.Context, key string) *wfcache.CacheItem {
panic("bigcache: unimplemented")
}

func (s *BigCacheStorage) BatchGet(ctx context.Context, keys []string) (results []*wfcache.CacheItem) {
panic("bigcache: unimplemented")
}

func (s *BigCacheStorage) Set(ctx context.Context, key string, data []byte) error {
panic("bigcache: unimplemented")
}

func (s *BigCacheStorage) BatchSet(ctx context.Context, pairs map[string][]byte) error {
panic("bigcache: unimplemented")
}

func (s *BigCacheStorage) Del(ctx context.Context, key string) error {
panic("bigcache: unimplemented")
}
Loading

0 comments on commit d1f0898

Please sign in to comment.