-
Notifications
You must be signed in to change notification settings - Fork 10
/
tinylfu.go
147 lines (121 loc) · 3.05 KB
/
tinylfu.go
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
package cache
import (
"context"
"errors"
"sync"
"time"
"github.com/vmihailenco/go-tinylfu"
"golang.org/x/exp/rand"
)
const (
maxOffset = 10 * time.Second
defaultOffset = -1
)
type tinyLFU struct {
lfu *tinylfu.T
// tinyLFU is not thread-safe, it needs a lock
mut sync.Mutex
rand *rand.Rand
offset time.Duration
}
// NewTinyLFU generates Adapter with tinylfu
func NewTinyLFU(size int, options ...TinyLFUOptions) Adapter {
// samples are the number of keys to track frequency
// TinyLFU works best for small number of keys (~ 100k)
// Ref: https://github.com/vmihailenco/go-cache-benchmark
// consider the discussing in (https://github.com/ben-manes/caffeine/issues/106),
// choose ~10x the cache size as the default value.
samples := size * 10
o := loadtinyLFUOptions(options...)
if o.offset != defaultOffset && o.offset < 0 {
panic(errors.New("invalid offset"))
}
return &tinyLFU{
lfu: tinylfu.New(size, samples),
rand: rand.New(rand.NewSource(uint64(time.Now().UnixNano()))),
offset: o.offset,
}
}
// TinyLFUOptions is an alias for functional argument.
type TinyLFUOptions func(opts *tinyLFUOptions)
// tinyLFUOptions contains all options which will be applied when calling New().
type tinyLFUOptions struct {
offset time.Duration
}
// WithOffset sets up the offset which is used to randomize TTL preventing
// expiring at the same time.
func WithOffset(offset time.Duration) TinyLFUOptions {
return func(opts *tinyLFUOptions) {
opts.offset = offset
}
}
func loadtinyLFUOptions(options ...TinyLFUOptions) *tinyLFUOptions {
opts := &tinyLFUOptions{offset: defaultOffset}
for _, option := range options {
option(opts)
}
return opts
}
func (lfu *tinyLFU) MSet(
ctx context.Context, keyVals map[string][]byte, ttl time.Duration, options ...MSetOptions,
) error {
if len(keyVals) == 0 {
return nil
}
// load options
o := loadMSetOptions(options...)
// offset is used to adjust the ttl preventing expiring at the same time
offset := lfu.offset
if offset == defaultOffset {
offset = ttl / 10
if offset > maxOffset {
offset = maxOffset
}
}
lfu.mut.Lock()
defer lfu.mut.Unlock()
for key, b := range keyVals {
t := ttl
if offset > 0 {
t += time.Duration(lfu.rand.Int63n(int64(offset)))
}
cost := len(b)
if o.onCostAdd != nil {
o.onCostAdd(key, cost)
}
lfu.lfu.Set(&tinylfu.Item{
Key: key,
Value: b,
ExpireAt: time.Now().Add(t),
OnEvict: func() {
if o.onCostEvict != nil {
o.onCostEvict(key, cost)
}
},
})
}
return nil
}
func (lfu *tinyLFU) MGet(ctx context.Context, keys []string) ([]Value, error) {
lfu.mut.Lock()
defer lfu.mut.Unlock()
vals := make([]Value, len(keys))
for i, key := range keys {
val, ok := lfu.lfu.Get(key)
if !ok {
vals[i] = Value{Valid: false, Bytes: nil}
continue
}
b, ok := val.([]byte)
vals[i] = Value{Valid: ok, Bytes: b}
}
return vals, nil
}
func (lfu *tinyLFU) Del(ctx context.Context, keys ...string) error {
lfu.mut.Lock()
defer lfu.mut.Unlock()
for _, key := range keys {
lfu.lfu.Del(key)
}
return nil
}