Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#27] Add SetIfAbsent function #42

Merged
merged 1 commit into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ func (c Cache[K, V]) Set(key K, value V) bool {
return c.cache.Set(key, value)
}

// SetIfAbsent if the specified key is not already associated with a value associates it with the given value.
//
// If the specified key is not already associated with a value, then it returns false.
//
// Also, it returns false if the key-value item had too much cost and the SetIfAbsent was dropped.
func (c Cache[K, V]) SetIfAbsent(key K, value V) bool {
return c.cache.SetIfAbsent(key, value)
}

// CacheWithVariableTTL is a structure performs a best-effort bounding of a hash table using eviction algorithm
// to determine which entries to evict when the capacity is exceeded.
type CacheWithVariableTTL[K comparable, V any] struct {
Expand All @@ -143,3 +152,13 @@ func newCacheWithVariableTTL[K comparable, V any](c core.Config[K, V]) CacheWith
func (c CacheWithVariableTTL[K, V]) Set(key K, value V, ttl time.Duration) bool {
return c.cache.SetWithTTL(key, value, ttl)
}

// SetIfAbsent if the specified key is not already associated with a value associates it with the given value
// and sets the custom ttl for this key-value item.
//
// If the specified key is not already associated with a value, then it returns false.
//
// Also, it returns false if the key-value item had too much cost and the SetIfAbsent was dropped.
func (c CacheWithVariableTTL[K, V]) SetIfAbsent(key K, value V, ttl time.Duration) bool {
return c.cache.SetIfAbsentWithTTL(key, value, ttl)
}
67 changes: 65 additions & 2 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ import (
)

func TestCache_Set(t *testing.T) {
c, err := MustBuilder[int, int](100).WithTTL(time.Minute).CollectStats().Build()
const size = 100
c, err := MustBuilder[int, int](size).WithTTL(time.Minute).CollectStats().Build()
if err != nil {
t.Fatalf("can not create cache: %v", err)
}

for i := 0; i < 100; i++ {
for i := 0; i < size; i++ {
c.Set(i, i)
}

// update
for i := 0; i < size; i++ {
c.Set(i, i)
}

Expand Down Expand Up @@ -68,6 +74,63 @@ func TestCache_Set(t *testing.T) {
}
}

func TestCache_SetIfAbsent(t *testing.T) {
const size = 100
c, err := MustBuilder[int, int](size).WithTTL(time.Minute).CollectStats().Build()
if err != nil {
t.Fatalf("can not create cache: %v", err)
}

for i := 0; i < size; i++ {
if !c.SetIfAbsent(i, i) {
t.Fatalf("set was dropped. key: %d", i)
}
}

for i := 0; i < size; i++ {
if !c.Has(i) {
t.Fatalf("key should exists: %d", i)
}
}

for i := 0; i < size; i++ {
if c.SetIfAbsent(i, i) {
t.Fatalf("set wasn't dropped. key: %d", i)
}
}

c.Clear()

cc, err := MustBuilder[int, int](size).WithVariableTTL().CollectStats().Build()
if err != nil {
t.Fatalf("can not create cache: %v", err)
}

for i := 0; i < size; i++ {
if !cc.SetIfAbsent(i, i, time.Hour) {
t.Fatalf("set was dropped. key: %d", i)
}
}

for i := 0; i < size; i++ {
if !cc.Has(i) {
t.Fatalf("key should exists: %d", i)
}
}

for i := 0; i < size; i++ {
if cc.SetIfAbsent(i, i, time.Second) {
t.Fatalf("set wasn't dropped. key: %d", i)
}
}

if hits := cc.Stats().Hits(); hits != size {
t.Fatalf("hit ratio should be 100%%. Hits: %d", hits)
}

cc.Close()
}

func TestCache_SetWithTTL(t *testing.T) {
size := 256
c, err := MustBuilder[int, int](size).WithTTL(time.Second).Build()
Expand Down
42 changes: 37 additions & 5 deletions internal/core/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ func zeroValue[V any]() V {
return zero
}

func getExpiration(ttl time.Duration) uint32 {
ttlSecond := (ttl + time.Second - 1) / time.Second
return unixtime.Now() + uint32(ttlSecond)
}

// Config is a set of cache settings.
type Config[K comparable, V any] struct {
Capacity int
Expand Down Expand Up @@ -147,7 +152,7 @@ func (c *Cache[K, V]) afterGet(got *node.Node[K, V]) {
//
// If it returns false, then the key-value item had too much cost and the Set was dropped.
func (c *Cache[K, V]) Set(key K, value V) bool {
return c.set(key, value, c.defaultExpiration())
return c.set(key, value, c.defaultExpiration(), false)
}

func (c *Cache[K, V]) defaultExpiration() uint32 {
Expand All @@ -162,18 +167,45 @@ func (c *Cache[K, V]) defaultExpiration() uint32 {
//
// If it returns false, then the key-value item had too much cost and the SetWithTTL was dropped.
func (c *Cache[K, V]) SetWithTTL(key K, value V, ttl time.Duration) bool {
ttl = (ttl + time.Second - 1) / time.Second
expiration := unixtime.Now() + uint32(ttl)
return c.set(key, value, expiration)
return c.set(key, value, getExpiration(ttl), false)
}

// SetIfAbsent if the specified key is not already associated with a value associates it with the given value.
//
// If the specified key is not already associated with a value, then it returns false.
//
// Also, it returns false if the key-value item had too much cost and the SetIfAbsent was dropped.
func (c *Cache[K, V]) SetIfAbsent(key K, value V) bool {
return c.set(key, value, c.defaultExpiration(), true)
}

func (c *Cache[K, V]) set(key K, value V, expiration uint32) bool {
// SetIfAbsentWithTTL if the specified key is not already associated with a value associates it with the given value
// and sets the custom ttl for this key-value item.
//
// If the specified key is not already associated with a value, then it returns false.
//
// Also, it returns false if the key-value item had too much cost and the SetIfAbsent was dropped.
func (c *Cache[K, V]) SetIfAbsentWithTTL(key K, value V, ttl time.Duration) bool {
return c.set(key, value, getExpiration(ttl), true)
}

func (c *Cache[K, V]) set(key K, value V, expiration uint32, onlyIfAbsent bool) bool {
cost := c.costFunc(key, value)
if cost > c.policy.MaxAvailableCost() {
return false
}

n := node.New(key, value, expiration, cost)
if onlyIfAbsent {
res := c.hashmap.SetIfAbsent(n)
if res == nil {
// insert
c.writeBuffer.Insert(node.NewAddTask(n))
return true
}
return false
}

evicted := c.hashmap.Set(n)
if evicted != nil {
// update
Expand Down
17 changes: 16 additions & 1 deletion internal/hashtable/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,17 @@ func (m *Map[K, V]) Get(key K) (got *node.Node[K, V], ok bool) {
// Set sets the *node.Node for the key.
//
// Returns the evicted node or nil if the node was inserted.
func (m *Map[K, V]) Set(n *node.Node[K, V]) (evicted *node.Node[K, V]) {
func (m *Map[K, V]) Set(n *node.Node[K, V]) *node.Node[K, V] {
return m.set(n, false)
}

// SetIfAbsent sets the *node.Node if the specified key is not already associated with a value (or is mapped to null)
// associates it with the given value and returns null, else returns the current node.
func (m *Map[K, V]) SetIfAbsent(n *node.Node[K, V]) *node.Node[K, V] {
return m.set(n, true)
}

func (m *Map[K, V]) set(n *node.Node[K, V], onlyIfAbsent bool) *node.Node[K, V] {
for {
RETRY:
var (
Expand Down Expand Up @@ -231,6 +241,11 @@ func (m *Map[K, V]) Set(n *node.Node[K, V]) (evicted *node.Node[K, V]) {
if n.Key() != prev.Key() {
continue
}
if onlyIfAbsent {
// found node, drop set
rootBucket.mutex.Unlock()
return n
}
// in-place update.
// We get a copy of the value via an interface{} on each call,
// thus the live value pointers are unique. Otherwise atomic
Expand Down
30 changes: 29 additions & 1 deletion internal/hashtable/map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,34 @@ func TestMap_Set(t *testing.T) {
}
}

func TestMap_SetIfAbsent(t *testing.T) {
const numberOfNodes = 128
m := New[string, int]()
for i := 0; i < numberOfNodes; i++ {
res := m.SetIfAbsent(newNode[string, int](strconv.Itoa(i), i))
if res != nil {
t.Fatalf("set was dropped. got: %+v", res)
}
}
for i := 0; i < numberOfNodes; i++ {
n := newNode[string, int](strconv.Itoa(i), i)
res := m.SetIfAbsent(n)
if res == nil {
t.Fatalf("set was not dropped. node that was set: %+v", res)
}
}

for i := 0; i < numberOfNodes; i++ {
n, ok := m.Get(strconv.Itoa(i))
if !ok {
t.Fatalf("value not found for %d", i)
}
if n.Value() != i {
t.Fatalf("values do not match for %d: %v", i, n.Value())
}
}
}

// this code may break if the maphash.Hasher[k] structure changes.
type hasher struct {
hash func(pointer unsafe.Pointer, seed uintptr) uintptr
Expand Down Expand Up @@ -372,7 +400,7 @@ func parallelTypedRangeDeleter(t *testing.T, m *Map[int, int], numNodes int, sto
cdone <- true
}

func TestMapOfParallelRange(t *testing.T) {
func TestMap_ParallelRange(t *testing.T) {
const numNodes = 10_000
m := New[int, int]()
for i := 0; i < numNodes; i++ {
Expand Down
2 changes: 1 addition & 1 deletion internal/s3fifo/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (p *Policy[K, V]) evict(deleted []*node.Node[K, V]) []*node.Node[K, V] {
}

func (p *Policy[K, V]) isFull() bool {
return p.small.cost+p.main.cost >= p.maxCost
return p.small.cost+p.main.cost > p.maxCost
}

// Write updates the eviction policy based on node updates.
Expand Down
2 changes: 1 addition & 1 deletion internal/s3fifo/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func TestPolicy_Update(t *testing.T) {

p.Read([]*node.Node[int, int]{n1, n1})

n2 := node.New[int, int](2, 1, 0, 91)
n2 := node.New[int, int](2, 1, 0, 92)
deleted := p.Write(nil, []node.WriteTask[int, int]{
node.NewAddTask(n2),
})
Expand Down
Loading