-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d1f0898
Showing
9 changed files
with
975 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Oops, something went wrong.