Skip to content

Commit ee8ba78

Browse files
committed
[cache] fix issues and add typed version
1 parent df4ea5b commit ee8ba78

File tree

6 files changed

+1292
-38
lines changed

6 files changed

+1292
-38
lines changed

pkg/cache/cache.go

Lines changed: 258 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,283 @@
1+
// Package cache provides a flexible caching abstraction with multiple backend implementations.
2+
//
3+
// The cache package offers a unified interface for caching operations with support for:
4+
// - In-memory caching (memoryCache)
5+
// - Redis-based caching (redisCache)
6+
// - Type-safe caching through generics (Typed[T])
7+
//
8+
// Features:
9+
// - Time-to-live (TTL) support for cache entries
10+
// - Atomic operations for consistency
11+
// - Concurrent-safe implementations
12+
// - Configurable expiration policies
13+
// - Support for custom serialization through the Item interface
14+
//
15+
// Basic Usage:
16+
//
17+
// // Create an in-memory cache with 1 hour default TTL
18+
// cache := cache.NewMemory(time.Hour)
19+
//
20+
// // Set a value
21+
// err := cache.Set(ctx, "key", []byte("value"))
22+
// if err != nil {
23+
// log.Fatal(err)
24+
// }
25+
//
26+
// // Get a value
27+
// value, err := cache.Get(ctx, "key")
28+
// if err != nil {
29+
// if errors.Is(err, cache.ErrKeyNotFound) {
30+
// // Handle missing key
31+
// } else if errors.Is(err, cache.ErrKeyExpired) {
32+
// // Handle expired key
33+
// } else {
34+
// log.Fatal(err)
35+
// }
36+
// }
37+
//
38+
// // Set with custom TTL
39+
// err = cache.Set(ctx, "key", []byte("value"), cache.WithTTL(30*time.Minute))
40+
//
41+
// // Set only if key doesn't exist
42+
// err = cache.SetOrFail(ctx, "key", []byte("value"))
43+
// if errors.Is(err, cache.ErrKeyExists) {
44+
// // Key already exists
45+
// }
46+
//
47+
// // Get and delete in one operation
48+
// value, err = cache.GetAndDelete(ctx, "key")
49+
//
50+
// // Remove expired entries
51+
// err = cache.Cleanup(ctx)
52+
//
53+
// // Get all items and clear the cache
54+
// items, err := cache.Drain(ctx)
55+
//
56+
// // Close the cache when done
57+
// err = cache.Close()
58+
//
59+
// Using Typed Cache:
60+
//
61+
// // Define a type that implements the Item interface
62+
// type MyData struct {
63+
// Field1 string
64+
// Field2 int
65+
// }
66+
//
67+
// func (d *MyData) Marshal() ([]byte, error) {
68+
// return json.Marshal(d)
69+
// }
70+
//
71+
// func (d *MyData) Unmarshal(data []byte) error {
72+
// return json.Unmarshal(data, d)
73+
// }
74+
//
75+
// // Create a typed cache
76+
// storage := cache.NewMemory(time.Hour)
77+
// typedCache := cache.NewTyped[*MyData](storage)
78+
//
79+
// // Set typed value
80+
// data := &MyData{Field1: "test", Field2: 42}
81+
// err := typedCache.Set(ctx, "key", data)
82+
//
83+
// // Get typed value
84+
// retrieved, err := typedCache.Get(ctx, "key")
85+
//
86+
// Using Redis Cache:
87+
//
88+
// // Create a Redis cache
89+
// config := cache.RedisConfig{
90+
// URL: "redis://localhost:6379",
91+
// Prefix: "myapp:",
92+
// TTL: time.Hour,
93+
// }
94+
//
95+
// redisCache, err := cache.NewRedis(config)
96+
// if err != nil {
97+
// log.Fatal(err)
98+
// }
99+
// defer redisCache.Close()
100+
//
101+
// // Use the same interface as memory cache
102+
// err = redisCache.Set(ctx, "key", []byte("value"))
103+
// value, err := redisCache.Get(ctx, "key")
1104
package cache
2105

3106
import "context"
4107

108+
// Cache defines the interface for cache implementations.
109+
//
110+
// All cache operations are context-aware and support cancellation and timeouts.
111+
// Implementations must be safe for concurrent use by multiple goroutines.
5112
type Cache interface {
6-
// Set sets the value for the given key in the cache.
113+
// Set stores the value for the given key in the cache, overwriting any existing value.
114+
//
115+
// The value will be stored with the default TTL configured for the cache implementation,
116+
// unless overridden by options. If the key already exists, its value and TTL will be updated.
117+
//
118+
// Parameters:
119+
// - ctx: Context for cancellation and timeouts
120+
// - key: The key to store the value under
121+
// - value: The value to store as a byte slice
122+
// - opts: Optional configuration for this specific item (e.g., custom TTL)
123+
//
124+
// Returns:
125+
// - error: nil on success, otherwise an error describing the failure
126+
//
127+
// Example:
128+
// // Set with default TTL
129+
// err := cache.Set(ctx, "user:123", []byte("user data"))
130+
//
131+
// // Set with custom TTL
132+
// err := cache.Set(ctx, "session:abc", []byte("session data"), cache.WithTTL(30*time.Minute))
133+
//
134+
// // Set with specific expiration time
135+
// expiration := time.Now().Add(2 * time.Hour)
136+
// err := cache.Set(ctx, "temp:xyz", []byte("temp data"), cache.WithValidUntil(expiration))
7137
Set(ctx context.Context, key string, value []byte, opts ...Option) error
8138

9-
// SetOrFail is like Set, but returns ErrKeyExists if the key already exists.
139+
// SetOrFail stores the value for the given key only if the key does not already exist.
140+
//
141+
// This is an atomic operation that prevents race conditions when multiple goroutines
142+
// might try to set the same key simultaneously. If the key exists but has expired,
143+
// it will be overwritten.
144+
//
145+
// Parameters:
146+
// - ctx: Context for cancellation and timeouts
147+
// - key: The key to store the value under
148+
// - value: The value to store as a byte slice
149+
// - opts: Optional configuration for this specific item (e.g., custom TTL)
150+
//
151+
// Returns:
152+
// - error: nil on success, ErrKeyExists if the key already exists and is not expired,
153+
// otherwise an error describing the failure
154+
//
155+
// Example:
156+
// // Try to set a value only if key doesn't exist
157+
// err := cache.SetOrFail(ctx, "lock:resource", []byte("locked"))
158+
// if errors.Is(err, cache.ErrKeyExists) {
159+
// // Key already exists, handle conflict
160+
// }
10161
SetOrFail(ctx context.Context, key string, value []byte, opts ...Option) error
11162

12-
// Get gets the value for the given key from the cache.
163+
// Get retrieves the value for the given key from the cache.
164+
//
165+
// The behavior depends on the key's existence and expiration state:
166+
// - If the key exists and has not expired, returns the value and nil error
167+
// - If the key does not exist, returns nil and ErrKeyNotFound
168+
// - If the key exists but has expired, returns nil and ErrKeyExpired
169+
//
170+
// GetOptions can be used to modify the behavior, such as updating TTL,
171+
// deleting the key after retrieval, or setting a new expiration time.
172+
//
173+
// Parameters:
174+
// - ctx: Context for cancellation and timeouts
175+
// - key: The key to retrieve
176+
// - opts: Optional operations to perform during retrieval (e.g., AndDelete, AndSetTTL)
177+
//
178+
// Returns:
179+
// - []byte: The cached value if found and not expired
180+
// - error: nil on success, ErrKeyNotFound if key doesn't exist,
181+
// ErrKeyExpired if key exists but has expired, otherwise an error
13182
//
14-
// If the key is not found, it returns ErrKeyNotFound.
15-
// If the key has expired, it returns ErrKeyExpired.
16-
// Otherwise, it returns the value and nil.
183+
// Example:
184+
// // Simple get
185+
// value, err := cache.Get(ctx, "user:123")
186+
//
187+
// // Get and extend TTL by 30 minutes
188+
// value, err := cache.Get(ctx, "session:abc", cache.AndUpdateTTL(30*time.Minute))
189+
//
190+
// // Get and delete atomically
191+
// value, err := cache.Get(ctx, "temp:xyz", cache.AndDelete())
17192
Get(ctx context.Context, key string, opts ...GetOption) ([]byte, error)
18193

19-
// GetAndDelete is like Get, but also deletes the key from the cache.
194+
// GetAndDelete retrieves the value for the given key and atomically deletes it from the cache.
195+
//
196+
// This is equivalent to calling Get with the AndDelete option, but provides a more
197+
// convenient API for the common pattern of reading and removing a value in one operation.
198+
//
199+
// Parameters:
200+
// - ctx: Context for cancellation and timeouts
201+
// - key: The key to retrieve and delete
202+
//
203+
// Returns:
204+
// - []byte: The cached value if found and not expired
205+
// - error: nil on success, ErrKeyNotFound if key doesn't exist,
206+
// ErrKeyExpired if key exists but has expired, otherwise an error
207+
//
208+
// Example:
209+
// // Atomically get and remove a value
210+
// value, err := cache.GetAndDelete(ctx, "queue:item:123")
20211
GetAndDelete(ctx context.Context, key string) ([]byte, error)
21212

22213
// Delete removes the item associated with the given key from the cache.
23-
// If the key does not exist, it performs no action and returns nil.
24-
// The operation is safe for concurrent use.
214+
//
215+
// If the key does not exist, this operation performs no action and returns nil.
216+
// The operation is safe for concurrent use by multiple goroutines.
217+
//
218+
// Parameters:
219+
// - ctx: Context for cancellation and timeouts
220+
// - key: The key to remove from the cache
221+
//
222+
// Returns:
223+
// - error: nil on success, otherwise an error describing the failure
224+
//
225+
// Example:
226+
// // Remove a specific key
227+
// err := cache.Delete(ctx, "user:123")
25228
Delete(ctx context.Context, key string) error
26229

27230
// Cleanup removes all expired items from the cache.
28-
// The operation is safe for concurrent use.
231+
//
232+
// This operation scans the entire cache and removes any items that have expired.
233+
// The operation is safe for concurrent use by multiple goroutines.
234+
// Note that some cache implementations (like Redis) handle expiration automatically
235+
// and may not require explicit cleanup.
236+
//
237+
// Parameters:
238+
// - ctx: Context for cancellation and timeouts
239+
//
240+
// Returns:
241+
// - error: nil on success, otherwise an error describing the failure
242+
//
243+
// Example:
244+
// // Periodically clean up expired items
245+
// err := cache.Cleanup(ctx)
29246
Cleanup(ctx context.Context) error
30247

31-
// Drain returns a map of all the non-expired items in the cache.
248+
// Drain returns a map of all non-expired items in the cache and clears the cache.
249+
//
32250
// The returned map is a snapshot of the cache at the time of the call.
33-
// The cache is cleared after the call.
34-
// The operation is safe for concurrent use.
251+
// After this operation, the cache will be empty. This is useful for cache migration,
252+
// backup, or when shutting down an application.
253+
// The operation is safe for concurrent use by multiple goroutines.
254+
//
255+
// Parameters:
256+
// - ctx: Context for cancellation and timeouts
257+
//
258+
// Returns:
259+
// - map[string][]byte: A map containing all non-expired key-value pairs
260+
// - error: nil on success, otherwise an error describing the failure
261+
//
262+
// Example:
263+
// // Get all items and clear the cache
264+
// items, err := cache.Drain(ctx)
265+
// for key, value := range items {
266+
// log.Printf("Drained: %s = %s", key, string(value))
267+
// }
35268
Drain(ctx context.Context) (map[string][]byte, error)
36269

37-
// Close closes the cache.
38-
// The operation is safe for concurrent use.
270+
// Close releases any resources held by the cache.
271+
//
272+
// This should be called when the cache is no longer needed. For some implementations
273+
// (like Redis), this may close network connections. The operation is safe for
274+
// concurrent use by multiple goroutines.
275+
//
276+
// Returns:
277+
// - error: nil on success, otherwise an error describing the failure
278+
//
279+
// Example:
280+
// // Properly close the cache when done
281+
// defer cache.Close()
39282
Close() error
40283
}

pkg/cache/errors.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,49 @@ var (
66
// ErrInvalidConfig indicates an invalid configuration.
77
ErrInvalidConfig = errors.New("invalid config")
88
// ErrKeyNotFound indicates no value exists for the given key.
9+
//
10+
// This error is returned by Get operations when the requested key has never been
11+
// set in the cache or has been explicitly deleted. It is also returned by
12+
// GetAndDelete when the key does not exist.
13+
//
14+
// Example:
15+
// _, err := cache.Get(ctx, "nonexistent-key")
16+
// if errors.Is(err, cache.ErrKeyNotFound) {
17+
// // Handle missing key
18+
// }
919
ErrKeyNotFound = errors.New("key not found")
20+
1021
// ErrKeyExpired indicates a value exists but has expired.
22+
//
23+
// This error is returned by Get operations when the requested key exists in the
24+
// cache but its time-to-live (TTL) has elapsed. Expired items may still exist
25+
// in the cache until they are explicitly removed by a Cleanup operation or
26+
// automatically by the cache implementation.
27+
//
28+
// Example:
29+
// // Set a value with 1 second TTL
30+
// cache.Set(ctx, "temp-key", []byte("data"), cache.WithTTL(time.Second))
31+
// time.Sleep(2 * time.Second)
32+
// _, err := cache.Get(ctx, "temp-key")
33+
// if errors.Is(err, cache.ErrKeyExpired) {
34+
// // Handle expired key
35+
// }
1136
ErrKeyExpired = errors.New("key expired")
37+
1238
// ErrKeyExists indicates a conflicting set when the key already exists.
39+
//
40+
// This error is returned by SetOrFail operations when attempting to set a value
41+
// for a key that already exists in the cache and has not expired. This is useful
42+
// for implementing atomic "create if not exists" operations and preventing
43+
// race conditions in concurrent scenarios.
44+
//
45+
// Example:
46+
// // Try to set a value only if key doesn't exist
47+
// err := cache.SetOrFail(ctx, "lock-key", []byte("locked"))
48+
// if errors.Is(err, cache.ErrKeyExists) {
49+
// // Key already exists, handle conflict
50+
// }
1351
ErrKeyExists = errors.New("key already exists")
52+
53+
ErrFailedToCreateZeroValue = errors.New("failed to create zero item")
1454
)

0 commit comments

Comments
 (0)