-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmemory.go
More file actions
184 lines (155 loc) · 4.42 KB
/
memory.go
File metadata and controls
184 lines (155 loc) · 4.42 KB
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
174
175
176
177
178
179
180
181
182
183
184
// Package fido provides a high-performance cache with optional persistence.
package fido
import (
"iter"
"sync"
"time"
"github.com/puzpuzpuz/xsync/v4"
)
// calculateExpiry returns the expiry time for a given TTL, falling back to defaultTTL.
// Returns zero Time (no expiry) if both TTL and defaultTTL are zero or negative.
func calculateExpiry(ttl, defaultTTL time.Duration) time.Time {
if ttl <= 0 {
ttl = defaultTTL
}
if ttl <= 0 {
return time.Time{}
}
return time.Now().Add(ttl)
}
// Cache is an in-memory cache. All operations are synchronous and infallible.
type Cache[K comparable, V any] struct {
flights *xsync.Map[K, *flightCall[V]]
memory *s3fifo[K, V]
defaultTTL time.Duration
}
// flightCall holds an in-flight computation for singleflight deduplication.
//
//nolint:govet // fieldalignment: semantic grouping preferred
type flightCall[V any] struct {
wg sync.WaitGroup
val V
err error
}
// New creates an in-memory cache.
func New[K comparable, V any](opts ...Option) *Cache[K, V] {
cfg := &config{size: 16384}
for _, opt := range opts {
opt(cfg)
}
return &Cache[K, V]{
flights: xsync.NewMap[K, *flightCall[V]](),
memory: newS3FIFO[K, V](cfg),
defaultTTL: cfg.defaultTTL,
}
}
// Get returns the value for key, or zero and false if not found.
func (c *Cache[K, V]) Get(key K) (V, bool) {
return c.memory.get(key)
}
// Set stores a value using the default TTL specified at cache creation.
// If no default TTL was set, the entry never expires.
func (c *Cache[K, V]) Set(key K, value V) {
c.SetTTL(key, value, c.defaultTTL)
}
// SetTTL stores a value with an explicit TTL.
// A zero or negative TTL means the entry never expires.
func (c *Cache[K, V]) SetTTL(key K, value V, ttl time.Duration) {
if ttl <= 0 {
c.memory.set(key, value, 0)
return
}
//nolint:gosec // G115: Unix seconds fit in uint32 until year 2106
c.memory.set(key, value, uint32(time.Now().Add(ttl).Unix()))
}
// Delete removes a key from the cache.
func (c *Cache[K, V]) Delete(key K) {
c.memory.del(key)
}
// Fetch returns cached value or calls loader to compute it.
// Concurrent calls for the same key share one loader invocation.
// Computed values are stored with the default TTL.
func (c *Cache[K, V]) Fetch(key K, loader func() (V, error)) (V, error) {
return c.getSet(key, loader, 0)
}
// FetchTTL is like Fetch but stores computed values with an explicit TTL.
func (c *Cache[K, V]) FetchTTL(key K, ttl time.Duration, loader func() (V, error)) (V, error) {
return c.getSet(key, loader, ttl)
}
func (c *Cache[K, V]) getSet(key K, loader func() (V, error), ttl time.Duration) (V, error) {
if val, ok := c.memory.get(key); ok {
return val, nil
}
call, loaded := c.flights.LoadOrCompute(key, func() (*flightCall[V], bool) {
fc := &flightCall[V]{}
fc.wg.Add(1)
return fc, false
})
if loaded {
call.wg.Wait()
return call.val, call.err
}
if val, ok := c.memory.get(key); ok {
call.val = val
c.flights.Delete(key)
call.wg.Done()
return val, nil
}
val, err := loader()
if err == nil {
if ttl <= 0 {
c.Set(key, val)
} else {
c.SetTTL(key, val, ttl)
}
}
call.val, call.err = val, err
c.flights.Delete(key)
call.wg.Done()
return val, err
}
// Len returns the number of entries.
func (c *Cache[K, V]) Len() int {
return c.memory.len()
}
// Flush removes all entries. Returns count removed.
func (c *Cache[K, V]) Flush() int {
return c.memory.flush()
}
// Range returns an iterator over all non-expired key-value pairs.
// Iteration order is undefined. Safe for concurrent use.
// Changes during iteration may or may not be reflected.
func (c *Cache[K, V]) Range() iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
//nolint:gosec // G115: Unix seconds fit in uint32 until year 2106
now := uint32(time.Now().Unix())
c.memory.entries.Range(func(key K, e *entry[K, V]) bool {
// Skip expired entries.
expiry := e.expirySec.Load()
if expiry != 0 && expiry < now {
return true
}
// Load value with seqlock.
v, ok := e.loadValue()
if !ok {
return true
}
// Yield to caller.
return yield(key, v)
})
}
}
type config struct {
size int
defaultTTL time.Duration
}
// Option configures a Cache.
type Option func(*config)
// Size sets maximum entries. Default 16384.
func Size(n int) Option {
return func(c *config) { c.size = n }
}
// TTL sets default expiration. Default 0 (none).
func TTL(d time.Duration) Option {
return func(c *config) { c.defaultTTL = d }
}