-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy paththroughcache.go
134 lines (118 loc) · 4.52 KB
/
throughcache.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
package cache
import (
"context"
"fmt"
"log/slog"
"time"
)
// Cacheable attempts to read a value from the cache for a given key. On a cache
// miss or read error, it executes the provided function (fn) to retrieve or
// compute the value. If successful, the value is then asynchronously stored in
// the cache with the specified TTL (time-to-live) for future requests.
//
// This function implements a read-through cache pattern, where the cache is
// updated after a cache miss. Cacheable only returns an error if the value cannot
// be retrieved or computed by the provided function.
//
// Errors encountered while storing the value in the cache are logged, but not
// returned to the caller, and the cache set operation occurs in a non-blocking
// goroutine.
//
// The cache read operation is subject to a readTimeout, which defines the
// maximum duration for waiting on a cache response. If the cache read exceeds
// this timeout or fails, the provided function is called to compute the value.
func Cacheable[T any](
ctx context.Context,
c *Cache,
key string,
readTimeout time.Duration,
ttl time.Duration,
fn func(ctx context.Context) (T, error)) (T, error) {
var val T
// Create a context with a timeout for the read operation. The purpose of
// this is to bypass the cache if the read from cache is slow.
readCtx, cancel := context.WithTimeout(ctx, readTimeout)
defer cancel()
// Try to read the value from cache first. If the retrieval is successful
// return the value as there is no need to go to the source system.
if err := c.Get(readCtx, key, &val); err == nil {
return val, nil
}
// Either we've encountered a cache miss or an error. In either case, we
// need to go to the source system to retrieve the value or recompute the
// result.
val, err := fn(ctx)
if err != nil {
// If func to retrieve or compute the value fails nothing further can
// be done. Return the error.
return val, err
}
// If the value was successfully retrieved or computed, store it in the
// cache with the configured TTL in a background goroutine to avoid blocking
// returning the result to the caller.
go func() {
setCtx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
_, err = c.SetIfAbsent(setCtx, key, val, ttl)
if err != nil {
slog.Error(fmt.Sprintf("Failed to update cache for key %s", key),
slog.Any("err", err))
}
}()
return val, nil
}
// Write first invokes the provided function to write the value to the
// source of truth. If the write operation is successful, the value is then
// synchronously stored in the cache with the specified TTL (time-to-live) for
// future requests.
//
// Write implements a write-through cache pattern, where the cache is
// updated after the source of truth is updated. Write is NOT atomic.
// It is possible that the source of truth is updated but the cache is not.
// This is a trade-off for the performance benefits of write-through caching.
// If either write operation fails, an error is returned.
func Write[T any](
ctx context.Context,
c *Cache,
key string,
val T,
ttl time.Duration,
fn func(ctx context.Context, v T) error) error {
// Write the value to the source of truth first. If this operation fails bail
err := fn(ctx, val)
if err != nil {
return err
}
if err := c.SetWithTTL(ctx, key, val, ttl); err != nil {
return fmt.Errorf("redis: SET %s failed: %w", key, err)
}
return nil
}
// Delete removes the value associated with the given key from both the source
// of truth and the cache. It first attempts to delete the value from the source
// of truth using the provided function. If this operation is successful, it then
// removes the corresponding entry from the cache.
//
// Delete is designed to ensure that both the source of truth and the cache remain
// in sync. If the source of truth is updated but the cache is not, the system may
// end up in an inconsistent state. Therefore, it is essential to call this function
// whenever a value needs to be deleted from both the source of truth and the cache.
//
// If either the delete operation from the source of truth or the cache fails, an
// error is returned, providing information on where the failure occurred.
func Delete[T any](
ctx context.Context,
c *Cache,
key string,
val T,
fn func(ctx context.Context, val T) error) error {
// Delete the value from the source of truth first. If this operation fails bail
err := fn(ctx, val)
if err != nil {
return err
}
if err := c.Delete(ctx, key); err != nil {
return fmt.Errorf("redis: DEL %s failed: %w", key, err)
}
return nil
}