-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcache.go
More file actions
148 lines (124 loc) · 4.39 KB
/
Copy pathcache.go
File metadata and controls
148 lines (124 loc) · 4.39 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
// Package cache provides a small, driver-based caching abstraction with
// pluggable in-memory and Redis backends, TTL helpers, and a cache-aside
// "Remember" helper for wrapping expensive or slow operations.
package cache
import (
"context"
"errors"
"fmt"
"time"
)
// ErrCacheSetFailed is returned by Remember when the value was fetched
// successfully but could not be written back to the cache.
var ErrCacheSetFailed = errors.New("cache set operation failed")
// TTL describes how long a cache entry should live.
// Build one with DefaultTTL, Forever, WithTTL, or WithTTLUntil.
type TTL struct {
duration time.Duration
useDefault bool
}
//nolint:gochecknoglobals
var (
// DefaultTTL tells the driver to use whatever default TTL it was configured with.
DefaultTTL = TTL{useDefault: true}
// Forever stores an entry without expiration.
Forever = TTL{duration: 0}
)
// WithTTL builds a TTL with an explicit duration.
func WithTTL(d time.Duration) TTL {
return TTL{duration: d}
}
// WithTTLUntil builds a TTL that expires at the given point in time.
//
// It's handy when you already have an absolute expiry (e.g. an "expires_at"
// column from a database) and just need to convert it into a relative TTL.
//
// Internally this is time.Until(t): if t is in the past the resulting
// duration is negative, and drivers that hand it straight to a backend such
// as Redis may expire the key immediately. Make sure t is in the future
// unless that's what you want.
func WithTTLUntil(t time.Time) TTL {
return TTL{duration: time.Until(t)}
}
type Key string
type Hit[Value any] struct {
Value Value
Expiry time.Time
}
// Driver is the interface that cache drivers must implement
// Drivers handle the low-level storage operations
type Driver[Value any] interface {
Get(ctx context.Context, key Key) (value Value, found bool, err error)
Set(ctx context.Context, key Key, value Value, ttl TTL) error
Has(ctx context.Context, key Key) (bool, error)
Delete(ctx context.Context, key Key) error
}
// Cacher is the main cache interface
type Cacher[Value any] interface {
Get(ctx context.Context, key Key) (value Value, found bool, err error)
Set(ctx context.Context, key Key, value Value, ttl TTL) error
Has(ctx context.Context, key Key) (bool, error)
Delete(ctx context.Context, key Key) error
Remember(ctx context.Context, key Key, ttl TTL, fetch func(ctx context.Context) (Value, error)) (Value, error)
}
// Cache is the main cache implementation that uses a driver
type Cache[Value any] struct {
driver Driver[Value]
}
var _ Cacher[string] = (*Cache[string])(nil)
// NewCache creates a new cache instance with the specified driver
func NewCache[Value any](driver Driver[Value]) *Cache[Value] {
return &Cache[Value]{
driver: driver,
}
}
// Get retrieves a value from the cache
func (c *Cache[Value]) Get(ctx context.Context, key Key) (Value, bool, error) { //nolint:ireturn
return c.driver.Get(ctx, key) //nolint:wrapcheck
}
// Set stores a value in the cache with the specified TTL
func (c *Cache[Value]) Set(ctx context.Context, key Key, value Value, ttl TTL) error {
return c.driver.Set(ctx, key, value, ttl) //nolint:wrapcheck
}
// Has checks if a key exists in the cache
func (c *Cache[Value]) Has(ctx context.Context, key Key) (bool, error) {
return c.driver.Has(ctx, key) //nolint:wrapcheck
}
// Delete removes a key from the cache
func (c *Cache[Value]) Delete(ctx context.Context, key Key) error {
return c.driver.Delete(ctx, key) //nolint:wrapcheck
}
// Remember implements the cache-aside pattern
// This is common logic shared by all cache implementations
//
//nolint:ireturn
func (c *Cache[Value]) Remember(
ctx context.Context,
key Key,
ttl TTL,
fetch func(ctx context.Context) (Value, error),
) (Value, error) {
var zeroValue Value
// Try to get from cache first
value, found, err := c.Get(ctx, key)
if err != nil { //nolint:revive
// If there's an error getting from cache, continue to fetch from source
// This allows the application to continue working even if cache is unavailable
} else if found {
return value, nil
}
// Fetch from source
value, err = fetch(ctx)
if err != nil {
return zeroValue, fmt.Errorf("fetch value: %w", err)
}
// Store in cache
if err := c.Set(ctx, key, value, ttl); err != nil {
return value, fmt.Errorf("%w: %w", ErrCacheSetFailed, err)
}
return value, nil
}
// Driver returns the underlying driver
func (c *Cache[Value]) Driver() Driver[Value] {
return c.driver
}