Skip to content

Commit

Permalink
[Chore] Add the MaximumSize and MaximumWeight methods to the Builder
Browse files Browse the repository at this point in the history
  • Loading branch information
maypok86 committed Aug 31, 2024
1 parent 0dede52 commit 2ca5bbc
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 90 deletions.
92 changes: 76 additions & 16 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,54 @@ import (

// Builder is a one-shot builder for creating a cache instance.
type Builder[K comparable, V any] struct {
capacity *uint64
maximumSize *int
maximumWeight *uint64
initialCapacity *int
statsCollector StatsCollector
ttl *time.Duration
withVariableTTL bool
weigher func(key K, value V) uint32
withWeight bool
deletionListener func(key K, value V, cause DeletionCause)
logger Logger
}

// NewBuilder creates a builder and sets the future cache capacity.
func NewBuilder[K comparable, V any](capacity uint64) *Builder[K, V] {
func NewBuilder[K comparable, V any]() *Builder[K, V] {
return &Builder[K, V]{
capacity: &capacity,
weigher: func(key K, value V) uint32 {
return 1
},
statsCollector: noopStatsCollector{},
logger: noopLogger{},
}
}

// MaximumSize specifies the maximum number of entries the cache may contain.
//
// This option cannot be used in conjunction with MaximumWeight.
//
// NOTE: the cache may evict an entry before this limit is exceeded or temporarily exceed the threshold while evicting.
// As the cache size grows close to the maximum, the cache evicts entries that are less likely to be used again.
// For example, the cache may evict an entry because it hasn't been used recently or very often.
func (b *Builder[K, V]) MaximumSize(maximumSize int) *Builder[K, V] {
b.maximumSize = &maximumSize
return b
}

// MaximumWeight specifies the maximum weight of entries the cache may contain. Weight is determined using the
// callback specified with Weigher.
// Use of this method requires a corresponding call to Weigher prior to calling Build.
//
// This option cannot be used in conjunction with MaximumSize.
//
// NOTE: the cache may evict an entry before this limit is exceeded or temporarily exceed the threshold while evicting.
// As the cache size grows close to the maximum, the cache evicts entries that are less likely to be used again.
// For example, the cache may evict an entry because it hasn't been used recently or very often.
//
// NOTE: weight is only used to determine whether the cache is over capacity; it has no effect
// on selecting which entry should be evicted next.
func (b *Builder[K, V]) MaximumWeight(maximumWeight uint64) *Builder[K, V] {
b.maximumWeight = &maximumWeight
return b
}

// CollectStats enables the accumulation of statistics during the operation of the cache.
//
// NOTE: collecting statistics requires bookkeeping to be performed with each operation,
Expand All @@ -61,12 +86,13 @@ func (b *Builder[K, V]) InitialCapacity(initialCapacity int) *Builder[K, V] {
return b
}

// Weigher sets a function to dynamically calculate the weight of an item.
//
// By default, this function always returns 1.
// Weigher specifies the weigher to use in determining the weight of entries. Entry weight is taken into
// consideration by MaximumWeight when determining which entries to evict, and use
// of this method requires a corresponding call to MaximumWeight prior to calling Build.
// Weights are measured and recorded when entries are inserted into or updated in
// the cache, and are thus effectively static during the lifetime of a cache entry.
func (b *Builder[K, V]) Weigher(weigher func(key K, value V) uint32) *Builder[K, V] {
b.weigher = weigher
b.withWeight = true
return b
}

Expand Down Expand Up @@ -102,16 +128,50 @@ func (b *Builder[K, V]) Logger(logger Logger) *Builder[K, V] {
return b
}

func (b *Builder[K, V]) getMaximum() *uint64 {
if b.maximumSize != nil {
ms := uint64(*b.maximumSize)
return &ms
}
if b.maximumWeight != nil {
return b.maximumWeight
}
return nil
}

func (b *Builder[K, V]) getWeigher() func(key K, value V) uint32 {
if b.weigher == nil {
return func(key K, value V) uint32 {
return 1
}
}
return b.weigher
}

func (b *Builder[K, V]) validate() error {
if b.capacity == nil || *b.capacity <= 0 {
return errors.New("otter: not valid capacity")
if b.maximumSize != nil && b.maximumWeight != nil {
return errors.New("otter: both maximumSize and maximumWeight are set")
}
if b.maximumSize != nil && b.weigher != nil {
return errors.New("otter: both maximumSize and weigher are set")
}
if b.maximumSize != nil && *b.maximumSize <= 0 {
return errors.New("otter: maximumSize should be positive")
}

if b.maximumWeight != nil && *b.maximumWeight <= 0 {
return errors.New("otter: maximumWeight should be positive")
}
if b.maximumWeight != nil && b.weigher == nil {
return errors.New("otter: maximumWeight requires weigher")
}
if b.weigher != nil && b.maximumWeight == nil {
return errors.New("otter: weigher requires maximumWeight")
}

if b.initialCapacity != nil && *b.initialCapacity <= 0 {
return errors.New("otter: initial capacity should be positive")
}
if b.weigher == nil {
return errors.New("otter: weigher should not be nil")
}
if b.statsCollector == nil {
return errors.New("otter: stats collector should not be nil")
}
Expand Down
174 changes: 117 additions & 57 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,64 +21,124 @@ import (
"github.com/maypok86/otter/v2/stats"
)

func TestBuilder_NewFailed(t *testing.T) {
_, err := NewBuilder[int, int](0).Build()
if err == nil {
t.Fatalf("should fail with an error")
}

capacity := uint64(100)
// negative const ttl
_, err = NewBuilder[int, int](capacity).WithTTL(-1).Build()
if err == nil {
t.Fatalf("should fail with an error")
}

// negative initial capacity
_, err = NewBuilder[int, int](capacity).InitialCapacity(-2).Build()
if err == nil {
t.Fatalf("should fail with an error")
}

_, err = NewBuilder[int, int](capacity).WithTTL(time.Hour).InitialCapacity(0).Build()
if err == nil {
t.Fatalf("should fail with an error")
}

_, err = NewBuilder[int, int](capacity).WithVariableTTL().InitialCapacity(-5).Build()
if err == nil {
t.Fatalf("should fail with an error")
}

// nil weigher
_, err = NewBuilder[int, int](capacity).Weigher(nil).Build()
if err == nil {
t.Fatalf("should fail with an error")
}

// nil stats collector
_, err = NewBuilder[int, int](capacity).CollectStats(nil).Build()
if err == nil {
t.Fatalf("should fail with an error")
}

// nil logger
_, err = NewBuilder[int, int](capacity).Logger(nil).Build()
if err == nil {
t.Fatalf("should fail with an error")
}
func ptr[T any](t T) *T {
return &t
}

func TestBuilder_BuildSuccess(t *testing.T) {
_, err := NewBuilder[int, int](10).
CollectStats(stats.NewCounter()).
InitialCapacity(10).
Weigher(func(key int, value int) uint32 {
return 2
}).
WithTTL(time.Hour).
Build()
if err != nil {
t.Fatalf("builded cache with error: %v", err)
func TestBuilder_Build(t *testing.T) {
for _, test := range []struct {
fn func(b *Builder[string, string])
want *string
}{
{
fn: func(b *Builder[string, string]) {
b.MaximumSize(100).MaximumWeight(uint64(1000))
},
want: ptr("otter: both maximumSize and maximumWeight are set"),
},
{
fn: func(b *Builder[string, string]) {
b.MaximumSize(100).Weigher(func(key string, value string) uint32 {
return 1
})
},
want: ptr("otter: both maximumSize and weigher are set"),
},
{
fn: func(b *Builder[string, string]) {
b.MaximumSize(0)
},
want: ptr("otter: maximumSize should be positive"),
},
{
fn: func(b *Builder[string, string]) {
b.MaximumWeight(0).Weigher(func(key string, value string) uint32 {
return 1
})
},
want: ptr("otter: maximumWeight should be positive"),
},
{
fn: func(b *Builder[string, string]) {
b.MaximumWeight(10)
},
want: ptr("otter: maximumWeight requires weigher"),
},
{
fn: func(b *Builder[string, string]) {
b.Weigher(func(key string, value string) uint32 {
return 0
})
},
want: ptr("otter: weigher requires maximumWeight"),
},
{
fn: func(b *Builder[string, string]) {
b.Weigher(func(key string, value string) uint32 {
return 0
})
},
want: ptr("otter: weigher requires maximumWeight"),
},
{
fn: func(b *Builder[string, string]) {
b.Weigher(func(key string, value string) uint32 {
return 0
})
},
want: ptr("otter: weigher requires maximumWeight"),
},
{
fn: func(b *Builder[string, string]) {
b.InitialCapacity(0)
},
want: ptr("otter: initial capacity should be positive"),
},
{
fn: func(b *Builder[string, string]) {
b.InitialCapacity(0)
},
want: ptr("otter: initial capacity should be positive"),
},
{
fn: func(b *Builder[string, string]) {
b.CollectStats(nil)
},
want: ptr("otter: stats collector should not be nil"),
},
{
fn: func(b *Builder[string, string]) {
b.Logger(nil)
},
want: ptr("otter: logger should not be nil"),
},
{
fn: func(b *Builder[string, string]) {
b.WithTTL(-1)
},
want: ptr("otter: ttl should be positive"),
},
{
fn: func(b *Builder[string, string]) {
b.MaximumWeight(10).
CollectStats(stats.NewCounter()).
InitialCapacity(10).
Weigher(func(key string, value string) uint32 {
return 2
}).
WithTTL(time.Hour)
},
want: nil,
},
} {
b := NewBuilder[string, string]()
test.fn(b)
_, err := b.Build()
if test.want == nil && err != nil {
t.Fatalf("unexpected error: %v", err)
}
if test.want != nil && err == nil {
t.Fatalf("wanted error: %s, but got nil", *test.want)
}
}
}
6 changes: 3 additions & 3 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ type Cache[K comparable, V any] struct {
func newCache[K comparable, V any](b *Builder[K, V]) *Cache[K, V] {
nodeManager := node.NewManager[K, V](node.Config{
WithExpiration: b.ttl != nil || b.withVariableTTL,
WithWeight: b.withWeight,
WithWeight: b.weigher != nil,
})

stripedBuffer := make([]*lossy.Buffer[K, V], 0, maxStripedBufferSize)
Expand All @@ -136,11 +136,11 @@ func newCache[K comparable, V any](b *Builder[K, V]) *Cache[K, V] {
doneClose: make(chan struct{}, 1),
//nolint:gosec // there will never be an overflow
mask: uint32(maxStripedBufferSize - 1),
weigher: b.weigher,
weigher: b.getWeigher(),
deletionListener: b.deletionListener,
}

cache.policy = s3fifo.NewPolicy(*b.capacity, cache.evictNode)
cache.policy = s3fifo.NewPolicy(*b.getMaximum(), cache.evictNode)

switch {
case b.ttl != nil:
Expand Down
Loading

0 comments on commit 2ca5bbc

Please sign in to comment.