1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
|
/*
* SPDX-FileCopyrightText: © Hypermode Inc. <hello@hypermode.com>
* SPDX-License-Identifier: Apache-2.0
*/
package ristretto
import (
"sync"
"time"
)
var (
// TODO: find the optimal value or make it configurable.
bucketDurationSecs = int64(5)
)
func storageBucket(t time.Time) int64 {
return (t.Unix() / bucketDurationSecs) + 1
}
func cleanupBucket(t time.Time) int64 {
// The bucket to cleanup is always behind the storage bucket by one so that
// no elements in that bucket (which might not have expired yet) are deleted.
return storageBucket(t) - 1
}
// bucket type is a map of key to conflict.
type bucket map[uint64]uint64
// expirationMap is a map of bucket number to the corresponding bucket.
type expirationMap[V any] struct {
sync.RWMutex
buckets map[int64]bucket
lastCleanedBucketNum int64
}
func newExpirationMap[V any]() *expirationMap[V] {
return &expirationMap[V]{
buckets: make(map[int64]bucket),
lastCleanedBucketNum: cleanupBucket(time.Now()),
}
}
func (m *expirationMap[_]) add(key, conflict uint64, expiration time.Time) {
if m == nil {
return
}
// Items that don't expire don't need to be in the expiration map.
if expiration.IsZero() {
return
}
bucketNum := storageBucket(expiration)
m.Lock()
defer m.Unlock()
b, ok := m.buckets[bucketNum]
if !ok {
b = make(bucket)
m.buckets[bucketNum] = b
}
b[key] = conflict
}
func (m *expirationMap[_]) update(key, conflict uint64, oldExpTime, newExpTime time.Time) {
if m == nil {
return
}
m.Lock()
defer m.Unlock()
oldBucketNum := storageBucket(oldExpTime)
oldBucket, ok := m.buckets[oldBucketNum]
if ok {
delete(oldBucket, key)
}
// Items that don't expire don't need to be in the expiration map.
if newExpTime.IsZero() {
return
}
newBucketNum := storageBucket(newExpTime)
newBucket, ok := m.buckets[newBucketNum]
if !ok {
newBucket = make(bucket)
m.buckets[newBucketNum] = newBucket
}
newBucket[key] = conflict
}
func (m *expirationMap[_]) del(key uint64, expiration time.Time) {
if m == nil {
return
}
bucketNum := storageBucket(expiration)
m.Lock()
defer m.Unlock()
_, ok := m.buckets[bucketNum]
if !ok {
return
}
delete(m.buckets[bucketNum], key)
}
// cleanup removes all the items in the bucket that was just completed. It deletes
// those items from the store, and calls the onEvict function on those items.
// This function is meant to be called periodically.
func (m *expirationMap[V]) cleanup(store store[V], policy *defaultPolicy[V], onEvict func(item *Item[V])) int {
if m == nil {
return 0
}
m.Lock()
now := time.Now()
currentBucketNum := cleanupBucket(now)
// Clean up all buckets up to and including currentBucketNum, starting from
// (but not including) the last one that was cleaned up
var buckets []bucket
for bucketNum := m.lastCleanedBucketNum + 1; bucketNum <= currentBucketNum; bucketNum++ {
// With an empty bucket, we don't need to add it to the Clean list
if b := m.buckets[bucketNum]; b != nil {
buckets = append(buckets, b)
}
delete(m.buckets, bucketNum)
}
m.lastCleanedBucketNum = currentBucketNum
m.Unlock()
for _, keys := range buckets {
for key, conflict := range keys {
expr := store.Expiration(key)
// Sanity check. Verify that the store agrees that this key is expired.
if expr.After(now) {
continue
}
cost := policy.Cost(key)
policy.Del(key)
_, value := store.Del(key, conflict)
if onEvict != nil {
onEvict(&Item[V]{Key: key,
Conflict: conflict,
Value: value,
Cost: cost,
Expiration: expr,
})
}
}
}
cleanedBucketsCount := len(buckets)
return cleanedBucketsCount
}
// clear clears the expirationMap, the caller is responsible for properly
// evicting the referenced items
func (m *expirationMap[V]) clear() {
if m == nil {
return
}
m.Lock()
m.buckets = make(map[int64]bucket)
m.lastCleanedBucketNum = cleanupBucket(time.Now())
m.Unlock()
}
|