Repository navigation
Expand file tree
/
Copy pathexample_test.go
More file actions
363 lines (306 loc) · 9.58 KB
/
Copy pathexample_test.go
File metadata and controls
363 lines (306 loc) · 9.58 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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
package sanecache_test
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/andared/sanecache"
)
type article struct {
ID string
Body string
}
type season struct {
Num int
}
func Example() {
c := sanecache.New(sanecache.Options[string, *article]{
TTL: 10 * time.Minute,
Jitter: 10,
MaxBytes: 1 << 20,
// Resident size, not serialized size: a decoded struct costs several
// times its payload. Measure the ratio rather than guessing it.
Cost: func(a *article) int64 { return int64(len(a.Body)) * 3 },
})
defer c.Close()
if err := c.Set("a1", &article{ID: "a1", Body: "hello"}); err != nil {
fmt.Println("set:", err)
return
}
if a, ok := c.Get("a1"); ok {
fmt.Println("cached:", a.Body)
}
// Output:
// cached: hello
}
// Telling "nobody asked yet" apart from "the upstream says it does not exist".
func ExampleCache_Lookup() {
c := sanecache.New(sanecache.Options[string, string]{
TTL: time.Minute,
NegativeTTL: 30 * time.Second,
})
defer c.Close()
_, status := c.Lookup("gone")
fmt.Println("before asking the upstream:", status)
// The upstream answered "no such key". Remember that, or every render of a
// template naming this id will ask again.
if err := c.SetNegative("gone"); err != nil {
fmt.Println("set negative:", err)
return
}
_, status = c.Lookup("gone")
fmt.Println("after:", status)
// Get collapses the two, because both mean "you have no value".
_, ok := c.Get("gone")
fmt.Println("Get reports a hit:", ok)
// Output:
// before asking the upstream: miss
// after: negative
// Get reports a hit: false
}
// A value that cannot fit is refused rather than accepted and dropped later.
func ExampleCache_Set() {
c := sanecache.New(sanecache.Options[string, []byte]{
MaxBytes: 1024,
Cost: func(b []byte) int64 { return int64(len(b)) },
})
defer c.Close()
err := c.Set("huge", make([]byte, 4096))
fmt.Println("rejected:", errors.Is(err, sanecache.ErrTooLarge))
// The rejection is counted, so keys that can never be cached show up as
// their own signal instead of an unexplained miss rate.
_, ok := c.Get("huge")
fmt.Println("cached:", ok, "rejections:", c.Stats().Rejections)
// Output:
// rejected: true
// cached: false rejections: 1
}
// All carries what a cache holds into another one, here with a larger budget,
// or out to wherever a warm start is read back from.
func ExampleCache_All() {
old := sanecache.New(sanecache.Options[string, int]{MaxEntries: 100})
defer old.Close()
_ = old.Set("a", 1)
_ = old.Set("b", 2)
bigger := sanecache.New(sanecache.Options[string, int]{MaxEntries: 1000})
defer bigger.Close()
for k, v := range old.All() {
_ = bigger.Set(k, v)
}
v, _ := bigger.Get("b")
fmt.Println(bigger.Len(), v)
// Output:
// 2 2
}
func ExampleCache_Stats() {
c := sanecache.New(sanecache.Options[int, string]{
TTL: time.Minute,
NegativeTTL: time.Minute,
})
defer c.Close()
_ = c.Set(1, "one")
_ = c.SetNegative(2)
c.Get(1) // hit
c.Lookup(2) // negative
c.Get(3) // miss
s := c.Stats()
// A cached negative counts towards the hit rate: it saved the same upstream
// call a positive answer would have.
fmt.Printf("hits=%d negatives=%d misses=%d rate=%.2f\n",
s.Hits, s.Negatives, s.Misses, s.HitRate())
// Output:
// hits=1 negatives=1 misses=1 rate=0.67
}
// A cold key is fetched once however many callers want it at the same moment.
func ExampleCache_GetOrLoad() {
var upstreamCalls atomic.Int64
c := sanecache.New(sanecache.Options[string, *article]{
TTL: time.Minute,
NegativeTTL: 10 * time.Second,
Loader: func(_ context.Context, id string) (*article, error) {
upstreamCalls.Add(1)
if id == "gone" {
// The upstream's "no such row" is translated once, here, and
// callers see the same error from the negative entry later.
return nil, sanecache.ErrNotFound
}
return &article{ID: id, Body: "hello"}, nil
},
})
defer c.Close()
var wg sync.WaitGroup
for range 10 {
wg.Add(1)
go func() {
defer wg.Done()
if _, err := c.GetOrLoad(context.Background(), "a1"); err != nil {
fmt.Println("load:", err)
}
}()
}
wg.Wait()
_, err := c.GetOrLoad(context.Background(), "gone")
fmt.Println("missing:", errors.Is(err, sanecache.ErrNotFound))
// Ten callers, one upstream call — plus the one that came back empty.
fmt.Println("upstream calls:", upstreamCalls.Load())
// Output:
// missing: true
// upstream calls: 2
}
// A search page is cached under a key that only summarises the query. The request
// itself travels with the call instead of being parsed back out of the key.
func ExampleCache_GetOrLoadFunc() {
type query struct {
Text string
Page int
}
var upstreamCalls atomic.Int64
search := func(_ context.Context, q query) ([]string, error) {
upstreamCalls.Add(1)
return []string{fmt.Sprintf("%s, page %d", q.Text, q.Page)}, nil
}
c := sanecache.New(sanecache.Options[string, []string]{TTL: time.Minute})
defer c.Close()
q := query{Text: "sanecache", Page: 2}
key := fmt.Sprintf("%s|%d", q.Text, q.Page)
for range 3 {
results, err := c.GetOrLoadFunc(context.Background(), key, func(ctx context.Context) ([]string, error) {
return search(ctx, q)
})
if err != nil {
fmt.Println("search:", err)
}
fmt.Println(results[0])
}
fmt.Println("upstream calls:", upstreamCalls.Load())
// Output:
// sanecache, page 2
// sanecache, page 2
// sanecache, page 2
// upstream calls: 1
}
// Fetching many keys from an upstream that answers many at once: one call for
// whatever the cache does not already hold.
func ExampleCache_GetManyOrLoad() {
var upstreamCalls atomic.Int64
c := sanecache.New(sanecache.Options[string, *article]{
TTL: time.Minute,
NegativeTTL: 10 * time.Second,
BatchLoader: func(_ context.Context, ids []string) (map[string]*article, error) {
upstreamCalls.Add(1)
// One round trip, say SELECT ... WHERE id IN (...). A requested id
// missing from the answer is an id the upstream does not have.
found := make(map[string]*article, len(ids))
for _, id := range ids {
if id != "gone" {
found[id] = &article{ID: id, Body: "hello"}
}
}
return found, nil
},
})
defer c.Close()
if err := c.Set("a1", &article{ID: "a1", Body: "cached"}); err != nil {
fmt.Println("set:", err)
return
}
got, err := c.GetManyOrLoad(context.Background(), []string{"a1", "a2", "a3", "gone"})
if err != nil {
fmt.Println("load:", err)
return
}
fmt.Println("found:", len(got), got["a1"].Body, got["a2"].Body)
// The same batch again is answered from the cache, "gone" included.
_, _ = c.GetManyOrLoad(context.Background(), []string{"a1", "a2", "a3", "gone"})
fmt.Println("upstream calls:", upstreamCalls.Load())
// Output:
// found: 3 cached hello
// upstream calls: 1
}
// Several value types under one byte budget, which is the only kind of budget
// that does not need dividing up in advance.
func ExampleNewView() {
c := sanecache.New(sanecache.Options[string, any]{
TTL: 10 * time.Minute,
MaxBytes: 64 << 20,
// The fallback for views that do not bring their own Cost. With several
// types sharing a budget, this is where the type switch would go.
Cost: func(any) int64 { return 256 },
})
defer c.Close()
articles := sanecache.NewView(c, sanecache.ViewOptions[*article]{
Name: "article",
Cost: func(a *article) int64 { return int64(len(a.Body)) * 3 },
})
seasons := sanecache.NewView(c, sanecache.ViewOptions[*season]{
Name: "season",
TTL: time.Hour, // seasons change less often than articles do
})
_ = articles.Set("1", &article{ID: "1", Body: "hello"})
_ = seasons.Set("1", &season{Num: 7})
a, _ := articles.Get("1")
s, _ := seasons.Get("1")
fmt.Println(a.Body, s.Num)
// The same key in two views is two entries: the view name is part of it.
fmt.Println("entries:", c.Len(), "bytes:", c.Bytes())
// Output:
// hello 7
// entries: 2 bytes: 271
}
func ExampleView_GetOrLoad() {
// A cache of any needs no Cost when each view brings its own.
c := sanecache.New(sanecache.Options[string, any]{MaxBytes: 1024})
defer c.Close()
articles := sanecache.NewView(c, sanecache.ViewOptions[*article]{
Name: "article",
Cost: func(a *article) int64 { return int64(len(a.Body)) * 3 },
NegativeTTL: time.Minute,
Loader: func(_ context.Context, id string) (*article, error) {
if id == "gone" {
return nil, sanecache.ErrNotFound
}
return &article{ID: id, Body: "hello"}, nil
},
})
a, err := articles.GetOrLoad(context.Background(), "1")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(a.ID, a.Body)
_, err = articles.GetOrLoad(context.Background(), "gone")
fmt.Println("missing:", errors.Is(err, sanecache.ErrNotFound))
fmt.Println("loads:", articles.Stats().Loads, "total:", c.Stats().Loads)
// Output:
// 1 hello
// missing: true
// loads: 2 total: 2
}
// A view's batch loader is asked for the caller's ids, without the view's prefix.
func ExampleView_GetManyOrLoad() {
c := sanecache.New(sanecache.Options[string, any]{TTL: time.Minute})
defer c.Close()
seasons := sanecache.NewView(c, sanecache.ViewOptions[*season]{
Name: "season",
BatchLoader: func(_ context.Context, ids []string) (map[string]*season, error) {
fmt.Println("asked for", len(ids))
found := make(map[string]*season, len(ids))
for i, id := range ids {
found[id] = &season{Num: i + 1}
}
return found, nil
},
})
_ = seasons.Set("1", &season{Num: 1})
got, err := seasons.GetManyOrLoad(context.Background(), []string{"1", "2", "3"})
if err != nil {
fmt.Println(err)
return
}
fmt.Println("found:", len(got), "batches:", seasons.Stats().Batches)
// Output:
// asked for 2
// found: 3 batches: 1
}