Skip to content

Commit

Permalink
add cache builder and loading cache (Yiling-J#2)
Browse files Browse the repository at this point in the history
* add cache builder and loading cache

* update assert because github CI is slow

* singleflight group per shard

* remove optional singleflight and update readme

* update test

* update readme

* Update README.md
  • Loading branch information
Yiling-J authored Apr 16, 2023
1 parent 73d3710 commit e3e74c1
Show file tree
Hide file tree
Showing 9 changed files with 570 additions and 62 deletions.
53 changes: 42 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,43 +35,71 @@ go get github.com/Yiling-J/theine-go

## API

Key should be **comparable**, and value can be any.
**Builder API**

create client
Theine provides two types of client, simple cache and loading cache. Both of them are initialized from a builder. The difference between simple cache and loading cache is: loading cache's Get method will compute the value using loader function when there is a miss, while simple cache client only return false and do nothing.

Loading cache uses singleflight to prevent concurrency loading to same key(thundering herd).

simple cache:

```GO
import "github.com/Yiling-J/theine-go"

// key type string, value type string, max size 1000
// max size is the only required configuration to initialize a client
client, err := theine.New[string, string](1000)
// max size is the only required configuration to build a client
client, err := theine.NewBuilder[string, string](1000).Build()
if err != nil {
panic(err)
}

// optional
// builder also provide several optional configurations
// you can chain them together and call build once
// client, err := theine.NewBuilder[string, string](1000).Cost(...).Doorkeeper(...).Build()

// or create builder first
builder := theine.NewBuilder[string, string](1000)

// dynamic cost function based on value
// use 0 in Set will call this function to evaluate cost at runtime
client.Cost(func(v string) int64 {
builder.Cost(func(v string) int64 {
return int64(len(v))
})

// doorkeeper
// doorkeeper will drop Set if they are not in bloomfilter yet
// this can improve write peroformance, but may lower hit ratio
client.Doorkeeper(true)
builder.Doorkeeper(true)

// removal listener, this function will be called when entry is removed
// RemoveReason could be REMOVED/EVICTED/EXPIRED
// REMOVED: remove by API
// EVICTED: evicted by Window-TinyLFU policy
// EXPIRED: expired by timing wheel
client.RemovalListener(func(key K, value V, reason theine.RemoveReason) {})
builder.RemovalListener(func(key K, value V, reason theine.RemoveReason) {})

```
loading cache:

```go
import "github.com/Yiling-J/theine-go"

// loader function: func(ctx context.Context, key K) (theine.Loaded[V], error)
// Loaded struct should include cache value, cost and ttl, which required by Set method
client, err := theine.NewBuilder[string, string](1000).BuildWithLoader(
func(ctx context.Context, key string) (theine.Loaded[string], error) {
return theine.Loaded[string]{Value: key, Cost: 1, TTL: 0}, nil
},
)
if err != nil {
panic(err)
}

```
Other builder options are same as simple cache(cost, doorkeeper, removal listener).

use client

**Client API**

```Go
// set, key foo, value bar, cost 1
Expand All @@ -83,15 +111,18 @@ success := client.Set("foo", "bar", 1)
// set with ttl
success = client.SetWithTTL("foo", "bar", 1, 1*time.Second)

// get
// get(simple cache version)
value, ok := client.Get("foo")

// get(loading cache version)
value, err := client.Get(ctx, "foo")

// remove
client.Delete("foo")

```
## Benchmarks

### throughput

Source Code: https://github.com/Yiling-J/theine-go/blob/main/benchmark_test.go
Expand Down
6 changes: 3 additions & 3 deletions benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type bar struct {
type foo struct{}

func BenchmarkGetTheineParallel(b *testing.B) {
client, err := theine.New[string, foo](100000)
client, err := theine.NewBuilder[string, foo](100000).Build()
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -63,7 +63,7 @@ func BenchmarkGetRistrettoParallel(b *testing.B) {
}

func BenchmarkSetTheineParallel(b *testing.B) {
client, err := theine.New[string, bar](100000)
client, err := theine.NewBuilder[string, bar](100000).Build()
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -109,7 +109,7 @@ func BenchmarkSetRistrettoParallel(b *testing.B) {
}

func BenchmarkZipfTheineParallel(b *testing.B) {
client, err := theine.New[string, bar](100000)
client, err := theine.NewBuilder[string, bar](100000).Build()
if err != nil {
panic(err)
}
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/clients/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type Theine[K comparable, V any] struct {
}

func (c *Theine[K, V]) Init(cap int) {
client, err := theine.New[K, V](int64(cap))
client, err := theine.NewBuilder[K, V](int64(cap)).Build()
if err != nil {
panic(err)
}
Expand Down
123 changes: 103 additions & 20 deletions cache.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package theine

import (
"context"
"errors"
"time"

Expand All @@ -11,43 +12,97 @@ const (
ZERO_TTL = 0 * time.Second
)

type Cache[K comparable, V any] struct {
store *internal.Store[K, V]
type RemoveReason = internal.RemoveReason

type Loaded[V any] struct {
Value V
Cost int64
TTL time.Duration
}

type RemoveReason = internal.RemoveReason
func (l *Loaded[V]) internal() internal.Loaded[V] {
return internal.Loaded[V]{
Value: l.Value,
Cost: l.Cost,
TTL: l.TTL,
}
}

const (
REMOVED = internal.REMOVED
EVICTED = internal.EVICTED
EXPIRED = internal.EXPIRED
)

func New[K comparable, V any](maxsize int64) (*Cache[K, V], error) {
if maxsize <= 0 {
return nil, errors.New("size must be positive")
}
type Builder[K comparable, V any] struct {
maxsize int64
cost func(V) int64
doorkeeper bool
removalListener func(key K, value V, reason RemoveReason)
}

return &Cache[K, V]{
store: internal.NewStore[K, V](maxsize),
}, nil
func NewBuilder[K comparable, V any](maxsize int64) *Builder[K, V] {
return &Builder[K, V]{maxsize: maxsize}
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
return c.store.Get(key)
func (b *Builder[K, V]) Cost(cost func(v V) int64) *Builder[K, V] {
b.cost = cost
return b
}
func (b *Builder[K, V]) Doorkeeper(enabled bool) *Builder[K, V] {
b.doorkeeper = true
return b
}

func (c *Cache[K, V]) Cost(cost func(v V) int64) *Cache[K, V] {
c.store.Cost(cost)
return c
func (b *Builder[K, V]) RemovalListener(listener func(key K, value V, reason RemoveReason)) *Builder[K, V] {
b.removalListener = listener
return b
}

func (b *Builder[K, V]) Build() (*Cache[K, V], error) {
if b.maxsize <= 0 {
return nil, errors.New("size must be positive")
}
store := internal.NewStore[K, V](b.maxsize)
if b.cost != nil {
store.Cost(b.cost)
}
if b.removalListener != nil {
store.RemovalListener(b.removalListener)
}
store.Doorkeeper(b.doorkeeper)
return &Cache[K, V]{store: store}, nil
}

func (b *Builder[K, V]) BuildWithLoader(loader func(ctx context.Context, key K) (Loaded[V], error)) (*LoadingCache[K, V], error) {
if b.maxsize <= 0 {
return nil, errors.New("size must be positive")
}
if loader == nil {
return nil, errors.New("loader function required")
}
store := internal.NewStore[K, V](b.maxsize)
if b.cost != nil {
store.Cost(b.cost)
}
if b.removalListener != nil {
store.RemovalListener(b.removalListener)
}
store.Doorkeeper(b.doorkeeper)
loadingStore := internal.NewLoadingStore(store)
loadingStore.Loader(func(ctx context.Context, key K) (internal.Loaded[V], error) {
v, err := loader(ctx, key)
return v.internal(), err
})
return &LoadingCache[K, V]{store: loadingStore}, nil
}
func (c *Cache[K, V]) Doorkeeper(enabled bool) *Cache[K, V] {
c.store.Doorkeeper(enabled)
return c

type Cache[K comparable, V any] struct {
store *internal.Store[K, V]
}

func (c *Cache[K, V]) RemovalListener(listener func(key K, value V, reason RemoveReason)) {
c.store.RemovalListener(listener)
func (c *Cache[K, V]) Get(key K) (V, bool) {
return c.store.Get(key)
}

func (c *Cache[K, V]) SetWithTTL(key K, value V, cost int64, ttl time.Duration) bool {
Expand All @@ -69,3 +124,31 @@ func (c *Cache[K, V]) Len() int {
func (c *Cache[K, V]) Close() {
c.store.Close()
}

type LoadingCache[K comparable, V any] struct {
store *internal.LoadingStore[K, V]
}

func (c *LoadingCache[K, V]) Get(ctx context.Context, key K) (V, error) {
return c.store.Get(ctx, key)
}

func (c *LoadingCache[K, V]) SetWithTTL(key K, value V, cost int64, ttl time.Duration) bool {
return c.store.Set(key, value, cost, ttl)
}

func (c *LoadingCache[K, V]) Set(key K, value V, cost int64) bool {
return c.SetWithTTL(key, value, cost, ZERO_TTL)
}

func (c *LoadingCache[K, V]) Delete(key K) {
c.store.Delete(key)
}

func (c *LoadingCache[K, V]) Len() int {
return c.store.Len()
}

func (c *LoadingCache[K, V]) Close() {
c.store.Close()
}
Loading

0 comments on commit e3e74c1

Please sign in to comment.