fix(go.sum): update ResolveSpec dependency to v1.0.87
This commit is contained in:
+340
@@ -0,0 +1,340 @@
|
||||
# Cache Package
|
||||
|
||||
A flexible, provider-based caching library for Go that supports multiple backend storage systems including in-memory, Redis, and Memcache.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple Providers**: Support for in-memory, Redis, and Memcache backends
|
||||
- **Pluggable Architecture**: Easy to add custom cache providers
|
||||
- **Type-Safe API**: Automatic JSON serialization/deserialization
|
||||
- **TTL Support**: Configurable time-to-live for cache entries
|
||||
- **Context-Aware**: All operations support Go contexts
|
||||
- **Statistics**: Built-in cache statistics and monitoring
|
||||
- **Pattern Deletion**: Delete keys by pattern (Redis)
|
||||
- **Lazy Loading**: GetOrSet pattern for easy cache-aside implementation
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/bitechdev/ResolveSpec/pkg/cache
|
||||
```
|
||||
|
||||
For Redis support:
|
||||
```bash
|
||||
go get github.com/redis/go-redis/v9
|
||||
```
|
||||
|
||||
For Memcache support:
|
||||
```bash
|
||||
go get github.com/bradfitz/gomemcache/memcache
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### In-Memory Cache
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/cache"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize with in-memory provider
|
||||
cache.UseMemory(&cache.Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
MaxSize: 10000,
|
||||
})
|
||||
defer cache.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
c := cache.GetDefaultCache()
|
||||
|
||||
// Store a value
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
user := User{ID: 1, Name: "John"}
|
||||
c.Set(ctx, "user:1", user, 10*time.Minute)
|
||||
|
||||
// Retrieve a value
|
||||
var retrieved User
|
||||
c.Get(ctx, "user:1", &retrieved)
|
||||
}
|
||||
```
|
||||
|
||||
### Redis Cache
|
||||
|
||||
```go
|
||||
cache.UseRedis(&cache.RedisConfig{
|
||||
Host: "localhost",
|
||||
Port: 6379,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
Options: &cache.Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
},
|
||||
})
|
||||
defer cache.Close()
|
||||
```
|
||||
|
||||
### Memcache
|
||||
|
||||
```go
|
||||
cache.UseMemcache(&cache.MemcacheConfig{
|
||||
Servers: []string{"localhost:11211"},
|
||||
Options: &cache.Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
},
|
||||
})
|
||||
defer cache.Close()
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Core Methods
|
||||
|
||||
#### Set
|
||||
```go
|
||||
Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error
|
||||
```
|
||||
Stores a value in the cache with automatic JSON serialization.
|
||||
|
||||
#### Get
|
||||
```go
|
||||
Get(ctx context.Context, key string, dest interface{}) error
|
||||
```
|
||||
Retrieves and deserializes a value from the cache.
|
||||
|
||||
#### SetBytes / GetBytes
|
||||
```go
|
||||
SetBytes(ctx context.Context, key string, value []byte, ttl time.Duration) error
|
||||
GetBytes(ctx context.Context, key string) ([]byte, error)
|
||||
```
|
||||
Store and retrieve raw bytes without serialization.
|
||||
|
||||
#### Delete
|
||||
```go
|
||||
Delete(ctx context.Context, key string) error
|
||||
```
|
||||
Removes a key from the cache.
|
||||
|
||||
#### DeleteByPattern
|
||||
```go
|
||||
DeleteByPattern(ctx context.Context, pattern string) error
|
||||
```
|
||||
Removes all keys matching a pattern (Redis only).
|
||||
|
||||
#### Clear
|
||||
```go
|
||||
Clear(ctx context.Context) error
|
||||
```
|
||||
Removes all items from the cache.
|
||||
|
||||
#### Exists
|
||||
```go
|
||||
Exists(ctx context.Context, key string) bool
|
||||
```
|
||||
Checks if a key exists in the cache.
|
||||
|
||||
#### GetOrSet
|
||||
```go
|
||||
GetOrSet(ctx context.Context, key string, dest interface{}, ttl time.Duration,
|
||||
loader func() (interface{}, error)) error
|
||||
```
|
||||
Retrieves a value from cache, or loads and caches it if not found (lazy loading).
|
||||
|
||||
#### Stats
|
||||
```go
|
||||
Stats(ctx context.Context) (*CacheStats, error)
|
||||
```
|
||||
Returns cache statistics including hits, misses, and key counts.
|
||||
|
||||
## Provider Configuration
|
||||
|
||||
### In-Memory Options
|
||||
|
||||
```go
|
||||
&cache.Options{
|
||||
DefaultTTL: 5 * time.Minute, // Default expiration time
|
||||
MaxSize: 10000, // Maximum number of items
|
||||
EvictionPolicy: "LRU", // Eviction strategy (future)
|
||||
}
|
||||
```
|
||||
|
||||
### Redis Configuration
|
||||
|
||||
```go
|
||||
&cache.RedisConfig{
|
||||
Host: "localhost",
|
||||
Port: 6379,
|
||||
Password: "", // Optional authentication
|
||||
DB: 0, // Database number
|
||||
PoolSize: 10, // Connection pool size
|
||||
Options: &cache.Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Memcache Configuration
|
||||
|
||||
```go
|
||||
&cache.MemcacheConfig{
|
||||
Servers: []string{"localhost:11211"},
|
||||
MaxIdleConns: 2,
|
||||
Timeout: 1 * time.Second,
|
||||
Options: &cache.Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Provider
|
||||
|
||||
```go
|
||||
// Create a custom provider instance
|
||||
memProvider := cache.NewMemoryProvider(&cache.Options{
|
||||
DefaultTTL: 10 * time.Minute,
|
||||
MaxSize: 500,
|
||||
})
|
||||
|
||||
// Initialize with custom provider
|
||||
cache.Initialize(memProvider)
|
||||
```
|
||||
|
||||
### Lazy Loading Pattern
|
||||
|
||||
```go
|
||||
var data ExpensiveData
|
||||
err := c.GetOrSet(ctx, "expensive:key", &data, 10*time.Minute, func() (interface{}, error) {
|
||||
// This expensive operation only runs if key is not in cache
|
||||
return computeExpensiveData(), nil
|
||||
})
|
||||
```
|
||||
|
||||
### Query API Cache
|
||||
|
||||
The package includes specialized functions for caching query results:
|
||||
|
||||
```go
|
||||
// Cache a query result
|
||||
api := "GetUsers"
|
||||
query := "SELECT * FROM users WHERE active = true"
|
||||
tablenames := "users"
|
||||
total := int64(150)
|
||||
|
||||
cache.PutQueryAPICache(ctx, api, query, tablenames, total)
|
||||
|
||||
// Retrieve cached query
|
||||
hash := cache.HashQueryAPICache(api, query)
|
||||
cachedQuery, err := cache.FetchQueryAPICache(ctx, hash)
|
||||
```
|
||||
|
||||
## Provider Comparison
|
||||
|
||||
| Feature | In-Memory | Redis | Memcache |
|
||||
|---------|-----------|-------|----------|
|
||||
| Persistence | No | Yes | No |
|
||||
| Distributed | No | Yes | Yes |
|
||||
| Pattern Delete | No | Yes | No |
|
||||
| Statistics | Full | Full | Limited |
|
||||
| Atomic Operations | Yes | Yes | Yes |
|
||||
| Max Item Size | Memory | 512MB | 1MB |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use contexts**: Always pass context for cancellation and timeout control
|
||||
2. **Set appropriate TTLs**: Balance between freshness and performance
|
||||
3. **Handle errors**: Cache misses and errors should be handled gracefully
|
||||
4. **Monitor statistics**: Use Stats() to monitor cache performance
|
||||
5. **Clean up**: Always call Close() when shutting down
|
||||
6. **Pattern consistency**: Use consistent key naming patterns (e.g., "user:id:field")
|
||||
|
||||
## Example: Complete Application
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/cache"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
func NewUserService() *UserService {
|
||||
// Initialize with Redis in production, memory for testing
|
||||
cache.UseRedis(&cache.RedisConfig{
|
||||
Host: "localhost",
|
||||
Port: 6379,
|
||||
Options: &cache.Options{
|
||||
DefaultTTL: 10 * time.Minute,
|
||||
},
|
||||
})
|
||||
|
||||
return &UserService{
|
||||
cache: cache.GetDefaultCache(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserService) GetUser(ctx context.Context, userID int) (*User, error) {
|
||||
var user User
|
||||
cacheKey := fmt.Sprintf("user:%d", userID)
|
||||
|
||||
// Try to get from cache first
|
||||
err := s.cache.GetOrSet(ctx, cacheKey, &user, 15*time.Minute, func() (interface{}, error) {
|
||||
// Load from database if not in cache
|
||||
return s.loadUserFromDB(userID)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) InvalidateUser(ctx context.Context, userID int) error {
|
||||
cacheKey := fmt.Sprintf("user:%d", userID)
|
||||
return s.cache.Delete(ctx, cacheKey)
|
||||
}
|
||||
|
||||
func main() {
|
||||
service := NewUserService()
|
||||
defer cache.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
user, err := service.GetUser(ctx, 123)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("User: %+v", user)
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **In-Memory**: Fastest but limited by RAM and not distributed
|
||||
- **Redis**: Great for distributed systems, persistent, but network overhead
|
||||
- **Memcache**: Good for distributed caching, simpler than Redis but less features
|
||||
|
||||
Choose based on your needs:
|
||||
- Single instance? Use in-memory
|
||||
- Need persistence or advanced features? Use Redis
|
||||
- Simple distributed cache? Use Memcache
|
||||
|
||||
## License
|
||||
|
||||
See repository license.
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultCache *Cache
|
||||
)
|
||||
|
||||
// Initialize initializes the cache with a provider.
|
||||
// If not called, the package will use an in-memory provider by default.
|
||||
func Initialize(provider Provider) {
|
||||
defaultCache = NewCache(provider)
|
||||
}
|
||||
|
||||
// UseMemory configures the cache to use in-memory storage.
|
||||
func UseMemory(opts *Options) error {
|
||||
provider := NewMemoryProvider(opts)
|
||||
defaultCache = NewCache(provider)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UseRedis configures the cache to use Redis storage.
|
||||
func UseRedis(config *RedisConfig) error {
|
||||
provider, err := NewRedisProvider(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize Redis provider: %w", err)
|
||||
}
|
||||
defaultCache = NewCache(provider)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UseMemcache configures the cache to use Memcache storage.
|
||||
func UseMemcache(config *MemcacheConfig) error {
|
||||
provider, err := NewMemcacheProvider(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize Memcache provider: %w", err)
|
||||
}
|
||||
defaultCache = NewCache(provider)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDefaultCache returns the default cache instance.
|
||||
// Initializes with in-memory provider if not already initialized.
|
||||
func GetDefaultCache() *Cache {
|
||||
if defaultCache == nil {
|
||||
_ = UseMemory(&Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
MaxSize: 10000,
|
||||
})
|
||||
}
|
||||
return defaultCache
|
||||
}
|
||||
|
||||
// SetDefaultCache sets a custom cache instance as the default cache.
|
||||
// This is useful for testing or when you want to use a pre-configured cache instance.
|
||||
func SetDefaultCache(cache *Cache) {
|
||||
defaultCache = cache
|
||||
}
|
||||
|
||||
// GetStats returns cache statistics.
|
||||
func GetStats(ctx context.Context) (*CacheStats, error) {
|
||||
cache := GetDefaultCache()
|
||||
return cache.Stats(ctx)
|
||||
}
|
||||
|
||||
// Close closes the cache and releases resources.
|
||||
func Close() error {
|
||||
if defaultCache != nil {
|
||||
return defaultCache.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cache is the main cache manager that wraps a Provider.
|
||||
type Cache struct {
|
||||
provider Provider
|
||||
}
|
||||
|
||||
// NewCache creates a new cache manager with the specified provider.
|
||||
func NewCache(provider Provider) *Cache {
|
||||
return &Cache{
|
||||
provider: provider,
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves and deserializes a value from the cache.
|
||||
func (c *Cache) Get(ctx context.Context, key string, dest interface{}) error {
|
||||
data, exists := c.provider.Get(ctx, key)
|
||||
if !exists {
|
||||
return fmt.Errorf("key not found: %s", key)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, dest); err != nil {
|
||||
return fmt.Errorf("failed to deserialize: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBytes retrieves raw bytes from the cache.
|
||||
func (c *Cache) GetBytes(ctx context.Context, key string) ([]byte, error) {
|
||||
data, exists := c.provider.Get(ctx, key)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("key not found: %s", key)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Set serializes and stores a value in the cache with the specified TTL.
|
||||
func (c *Cache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize: %w", err)
|
||||
}
|
||||
|
||||
return c.provider.Set(ctx, key, data, ttl)
|
||||
}
|
||||
|
||||
// SetBytes stores raw bytes in the cache with the specified TTL.
|
||||
func (c *Cache) SetBytes(ctx context.Context, key string, value []byte, ttl time.Duration) error {
|
||||
return c.provider.Set(ctx, key, value, ttl)
|
||||
}
|
||||
|
||||
// SetWithTags serializes and stores a value in the cache with the specified TTL and tags.
|
||||
func (c *Cache) SetWithTags(ctx context.Context, key string, value interface{}, ttl time.Duration, tags []string) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize: %w", err)
|
||||
}
|
||||
|
||||
return c.provider.SetWithTags(ctx, key, data, ttl, tags)
|
||||
}
|
||||
|
||||
// SetBytesWithTags stores raw bytes in the cache with the specified TTL and tags.
|
||||
func (c *Cache) SetBytesWithTags(ctx context.Context, key string, value []byte, ttl time.Duration, tags []string) error {
|
||||
return c.provider.SetWithTags(ctx, key, value, ttl, tags)
|
||||
}
|
||||
|
||||
// Delete removes a key from the cache.
|
||||
func (c *Cache) Delete(ctx context.Context, key string) error {
|
||||
return c.provider.Delete(ctx, key)
|
||||
}
|
||||
|
||||
// DeleteByTag removes all keys associated with the given tag.
|
||||
func (c *Cache) DeleteByTag(ctx context.Context, tag string) error {
|
||||
return c.provider.DeleteByTag(ctx, tag)
|
||||
}
|
||||
|
||||
// DeleteByPattern removes all keys matching the pattern.
|
||||
func (c *Cache) DeleteByPattern(ctx context.Context, pattern string) error {
|
||||
return c.provider.DeleteByPattern(ctx, pattern)
|
||||
}
|
||||
|
||||
// Clear removes all items from the cache.
|
||||
func (c *Cache) Clear(ctx context.Context) error {
|
||||
return c.provider.Clear(ctx)
|
||||
}
|
||||
|
||||
// Exists checks if a key exists in the cache.
|
||||
func (c *Cache) Exists(ctx context.Context, key string) bool {
|
||||
return c.provider.Exists(ctx, key)
|
||||
}
|
||||
|
||||
// Stats returns statistics about the cache.
|
||||
func (c *Cache) Stats(ctx context.Context) (*CacheStats, error) {
|
||||
return c.provider.Stats(ctx)
|
||||
}
|
||||
|
||||
// Close closes the cache and releases any resources.
|
||||
func (c *Cache) Close() error {
|
||||
return c.provider.Close()
|
||||
}
|
||||
|
||||
// GetOrSet retrieves a value from cache, or sets it if it doesn't exist.
|
||||
// The loader function is called only if the key is not found in cache.
|
||||
func (c *Cache) GetOrSet(ctx context.Context, key string, dest interface{}, ttl time.Duration, loader func() (interface{}, error)) error {
|
||||
// Try to get from cache first
|
||||
err := c.Get(ctx, key, dest)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load the value
|
||||
value, err := loader()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loader failed: %w", err)
|
||||
}
|
||||
|
||||
// Store in cache
|
||||
if err := c.Set(ctx, key, value, ttl); err != nil {
|
||||
return fmt.Errorf("failed to cache value: %w", err)
|
||||
}
|
||||
|
||||
// Populate dest with the loaded value
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize loaded value: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, dest); err != nil {
|
||||
return fmt.Errorf("failed to deserialize loaded value: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remember is a convenience function that caches the result of a function call.
|
||||
// It's similar to GetOrSet but returns the value directly.
|
||||
func (c *Cache) Remember(ctx context.Context, key string, ttl time.Duration, loader func() (interface{}, error)) (interface{}, error) {
|
||||
// Try to get from cache first as bytes
|
||||
data, err := c.GetBytes(ctx, key)
|
||||
if err == nil {
|
||||
var result interface{}
|
||||
if err := json.Unmarshal(data, &result); err == nil {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Load the value
|
||||
value, err := loader()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loader failed: %w", err)
|
||||
}
|
||||
|
||||
// Store in cache
|
||||
if err := c.Set(ctx, key, value, ttl); err != nil {
|
||||
return nil, fmt.Errorf("failed to cache value: %w", err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExampleInMemoryCache demonstrates using the in-memory cache provider.
|
||||
func ExampleInMemoryCache() {
|
||||
// Initialize with in-memory provider
|
||||
err := UseMemory(&Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
MaxSize: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get the cache instance
|
||||
cache := GetDefaultCache()
|
||||
|
||||
// Store a value
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
user := User{ID: 1, Name: "John Doe"}
|
||||
err = cache.Set(ctx, "user:1", user, 10*time.Minute)
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Retrieve a value
|
||||
var retrieved User
|
||||
err = cache.Get(ctx, "user:1", &retrieved)
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Retrieved user: %+v\n", retrieved)
|
||||
|
||||
// Check if key exists
|
||||
exists := cache.Exists(ctx, "user:1")
|
||||
fmt.Printf("Key exists: %v\n", exists)
|
||||
|
||||
// Delete a key
|
||||
err = cache.Delete(ctx, "user:1")
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Get statistics
|
||||
stats, err := cache.Stats(ctx)
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Cache stats: %+v\n", stats)
|
||||
_ = Close()
|
||||
}
|
||||
|
||||
// ExampleRedisCache demonstrates using the Redis cache provider.
|
||||
func ExampleRedisCache() {
|
||||
// Initialize with Redis provider
|
||||
err := UseRedis(&RedisConfig{
|
||||
Host: "localhost",
|
||||
Port: 6379,
|
||||
Password: "", // Set if Redis requires authentication
|
||||
DB: 0,
|
||||
Options: &Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get the cache instance
|
||||
cache := GetDefaultCache()
|
||||
|
||||
// Store raw bytes
|
||||
data := []byte("Hello, Redis!")
|
||||
err = cache.SetBytes(ctx, "greeting", data, 1*time.Hour)
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Retrieve raw bytes
|
||||
retrieved, err := cache.GetBytes(ctx, "greeting")
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Retrieved data: %s\n", string(retrieved))
|
||||
|
||||
// Clear all cache
|
||||
err = cache.Clear(ctx)
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
_ = Close()
|
||||
}
|
||||
|
||||
// ExampleMemcacheCache demonstrates using the Memcache cache provider.
|
||||
func ExampleMemcacheCache() {
|
||||
// Initialize with Memcache provider
|
||||
err := UseMemcache(&MemcacheConfig{
|
||||
Servers: []string{"localhost:11211"},
|
||||
Options: &Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get the cache instance
|
||||
cache := GetDefaultCache()
|
||||
|
||||
// Store a value
|
||||
type Product struct {
|
||||
ID int
|
||||
Name string
|
||||
Price float64
|
||||
}
|
||||
|
||||
product := Product{ID: 100, Name: "Widget", Price: 29.99}
|
||||
err = cache.Set(ctx, "product:100", product, 30*time.Minute)
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Retrieve a value
|
||||
var retrieved Product
|
||||
err = cache.Get(ctx, "product:100", &retrieved)
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Retrieved product: %+v\n", retrieved)
|
||||
_ = Close()
|
||||
}
|
||||
|
||||
// ExampleGetOrSet demonstrates the GetOrSet pattern for lazy loading.
|
||||
func ExampleGetOrSet() {
|
||||
err := UseMemory(&Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
MaxSize: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
cache := GetDefaultCache()
|
||||
|
||||
type ExpensiveData struct {
|
||||
Result string
|
||||
}
|
||||
|
||||
var data ExpensiveData
|
||||
err = cache.GetOrSet(ctx, "expensive:computation", &data, 10*time.Minute, func() (interface{}, error) {
|
||||
// This expensive operation only runs if the key is not in cache
|
||||
fmt.Println("Computing expensive result...")
|
||||
time.Sleep(1 * time.Second)
|
||||
return ExpensiveData{Result: "computed value"}, nil
|
||||
})
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Data: %+v\n", data)
|
||||
|
||||
// Second call will use cached value
|
||||
err = cache.GetOrSet(ctx, "expensive:computation", &data, 10*time.Minute, func() (interface{}, error) {
|
||||
fmt.Println("This won't be called!")
|
||||
return ExpensiveData{Result: "new value"}, nil
|
||||
})
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Cached data: %+v\n", data)
|
||||
_ = Close()
|
||||
}
|
||||
|
||||
// ExampleCustomProvider demonstrates using a custom provider.
|
||||
func ExampleCustomProvider() {
|
||||
// Create a custom provider
|
||||
memProvider := NewMemoryProvider(&Options{
|
||||
DefaultTTL: 10 * time.Minute,
|
||||
MaxSize: 500,
|
||||
})
|
||||
|
||||
// Initialize with custom provider
|
||||
Initialize(memProvider)
|
||||
|
||||
ctx := context.Background()
|
||||
cache := GetDefaultCache()
|
||||
|
||||
// Use the cache
|
||||
err := cache.SetBytes(ctx, "key", []byte("value"), 5*time.Minute)
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Clean expired items (memory provider specific)
|
||||
if mp, ok := cache.provider.(*MemoryProvider); ok {
|
||||
count := mp.CleanExpired(ctx)
|
||||
fmt.Printf("Cleaned %d expired items\n", count)
|
||||
}
|
||||
_ = Close()
|
||||
}
|
||||
|
||||
// ExampleDeleteByPattern demonstrates pattern-based deletion (Redis only).
|
||||
func ExampleDeleteByPattern() {
|
||||
err := UseRedis(&RedisConfig{
|
||||
Host: "localhost",
|
||||
Port: 6379,
|
||||
Options: &Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
cache := GetDefaultCache()
|
||||
|
||||
// Store multiple keys with a pattern
|
||||
_ = cache.SetBytes(ctx, "user:1:profile", []byte("profile1"), 10*time.Minute)
|
||||
_ = cache.SetBytes(ctx, "user:2:profile", []byte("profile2"), 10*time.Minute)
|
||||
_ = cache.SetBytes(ctx, "user:1:settings", []byte("settings1"), 10*time.Minute)
|
||||
|
||||
// Delete all keys matching pattern (Redis glob pattern)
|
||||
err = cache.DeleteByPattern(ctx, "user:*:profile")
|
||||
if err != nil {
|
||||
_ = Close()
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Deleted all user profile keys")
|
||||
_ = Close()
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Provider defines the interface that all cache providers must implement.
|
||||
type Provider interface {
|
||||
// Get retrieves a value from the cache by key.
|
||||
// Returns nil, false if key doesn't exist or is expired.
|
||||
Get(ctx context.Context, key string) ([]byte, bool)
|
||||
|
||||
// Set stores a value in the cache with the specified TTL.
|
||||
// If ttl is 0, the item never expires.
|
||||
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
|
||||
|
||||
// SetWithTags stores a value in the cache with the specified TTL and tags.
|
||||
// Tags can be used to invalidate groups of related keys.
|
||||
// If ttl is 0, the item never expires.
|
||||
SetWithTags(ctx context.Context, key string, value []byte, ttl time.Duration, tags []string) error
|
||||
|
||||
// Delete removes a key from the cache.
|
||||
Delete(ctx context.Context, key string) error
|
||||
|
||||
// DeleteByTag removes all keys associated with the given tag.
|
||||
DeleteByTag(ctx context.Context, tag string) error
|
||||
|
||||
// DeleteByPattern removes all keys matching the pattern.
|
||||
// Pattern syntax depends on the provider implementation.
|
||||
DeleteByPattern(ctx context.Context, pattern string) error
|
||||
|
||||
// Clear removes all items from the cache.
|
||||
Clear(ctx context.Context) error
|
||||
|
||||
// Exists checks if a key exists in the cache.
|
||||
Exists(ctx context.Context, key string) bool
|
||||
|
||||
// Close closes the provider and releases any resources.
|
||||
Close() error
|
||||
|
||||
// Stats returns statistics about the cache provider.
|
||||
Stats(ctx context.Context) (*CacheStats, error)
|
||||
}
|
||||
|
||||
// CacheStats contains cache statistics.
|
||||
type CacheStats struct {
|
||||
Hits int64 `json:"hits"`
|
||||
Misses int64 `json:"misses"`
|
||||
Keys int64 `json:"keys"`
|
||||
ProviderType string `json:"provider_type"`
|
||||
ProviderStats map[string]any `json:"provider_stats,omitempty"`
|
||||
}
|
||||
|
||||
// Options contains configuration options for cache providers.
|
||||
type Options struct {
|
||||
// DefaultTTL is the default time-to-live for cache items.
|
||||
DefaultTTL time.Duration
|
||||
|
||||
// MaxSize is the maximum number of items (for in-memory provider).
|
||||
MaxSize int
|
||||
|
||||
// EvictionPolicy determines how items are evicted (LRU, LFU, etc).
|
||||
EvictionPolicy string
|
||||
}
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/bradfitz/gomemcache/memcache"
|
||||
)
|
||||
|
||||
// MemcacheProvider is a Memcache implementation of the Provider interface.
|
||||
type MemcacheProvider struct {
|
||||
client *memcache.Client
|
||||
options *Options
|
||||
}
|
||||
|
||||
// MemcacheConfig contains Memcache-specific configuration.
|
||||
type MemcacheConfig struct {
|
||||
// Servers is a list of memcache server addresses (e.g., "localhost:11211")
|
||||
Servers []string
|
||||
|
||||
// MaxIdleConns is the maximum number of idle connections (default: 2)
|
||||
MaxIdleConns int
|
||||
|
||||
// Timeout for connection operations (default: 1 second)
|
||||
Timeout time.Duration
|
||||
|
||||
// Options contains general cache options
|
||||
Options *Options
|
||||
}
|
||||
|
||||
// NewMemcacheProvider creates a new Memcache cache provider.
|
||||
func NewMemcacheProvider(config *MemcacheConfig) (*MemcacheProvider, error) {
|
||||
if config == nil {
|
||||
config = &MemcacheConfig{
|
||||
Servers: []string{"localhost:11211"},
|
||||
}
|
||||
}
|
||||
|
||||
if len(config.Servers) == 0 {
|
||||
config.Servers = []string{"localhost:11211"}
|
||||
}
|
||||
|
||||
if config.MaxIdleConns == 0 {
|
||||
config.MaxIdleConns = 2
|
||||
}
|
||||
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = 1 * time.Second
|
||||
}
|
||||
|
||||
if config.Options == nil {
|
||||
config.Options = &Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
client := memcache.New(config.Servers...)
|
||||
client.MaxIdleConns = config.MaxIdleConns
|
||||
client.Timeout = config.Timeout
|
||||
|
||||
// Test connection
|
||||
if err := client.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Memcache: %w", err)
|
||||
}
|
||||
|
||||
return &MemcacheProvider{
|
||||
client: client,
|
||||
options: config.Options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get retrieves a value from the cache by key.
|
||||
func (m *MemcacheProvider) Get(ctx context.Context, key string) ([]byte, bool) {
|
||||
item, err := m.client.Get(key)
|
||||
if err == memcache.ErrCacheMiss {
|
||||
return nil, false
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return item.Value, true
|
||||
}
|
||||
|
||||
// Set stores a value in the cache with the specified TTL.
|
||||
func (m *MemcacheProvider) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
|
||||
if ttl == 0 {
|
||||
ttl = m.options.DefaultTTL
|
||||
}
|
||||
|
||||
item := &memcache.Item{
|
||||
Key: key,
|
||||
Value: value,
|
||||
Expiration: int32(ttl.Seconds()),
|
||||
}
|
||||
|
||||
return m.client.Set(item)
|
||||
}
|
||||
|
||||
// SetWithTags stores a value in the cache with the specified TTL and tags.
|
||||
// Note: Tag support in Memcache is limited and less efficient than Redis.
|
||||
func (m *MemcacheProvider) SetWithTags(ctx context.Context, key string, value []byte, ttl time.Duration, tags []string) error {
|
||||
if ttl == 0 {
|
||||
ttl = m.options.DefaultTTL
|
||||
}
|
||||
|
||||
expiration := int32(ttl.Seconds())
|
||||
|
||||
// Set the main value
|
||||
item := &memcache.Item{
|
||||
Key: key,
|
||||
Value: value,
|
||||
Expiration: expiration,
|
||||
}
|
||||
if err := m.client.Set(item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store tags for this key
|
||||
if len(tags) > 0 {
|
||||
tagsData, err := json.Marshal(tags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal tags: %w", err)
|
||||
}
|
||||
|
||||
tagsItem := &memcache.Item{
|
||||
Key: fmt.Sprintf("cache:tags:%s", key),
|
||||
Value: tagsData,
|
||||
Expiration: expiration,
|
||||
}
|
||||
if err := m.client.Set(tagsItem); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add key to each tag's key list
|
||||
for _, tag := range tags {
|
||||
tagKey := fmt.Sprintf("cache:tag:%s", tag)
|
||||
|
||||
// Get existing keys for this tag
|
||||
var keys []string
|
||||
if item, err := m.client.Get(tagKey); err == nil {
|
||||
_ = json.Unmarshal(item.Value, &keys)
|
||||
}
|
||||
|
||||
// Add current key if not already present
|
||||
found := false
|
||||
for _, k := range keys {
|
||||
if k == key {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
// Store updated key list
|
||||
keysData, err := json.Marshal(keys)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
tagItem := &memcache.Item{
|
||||
Key: tagKey,
|
||||
Value: keysData,
|
||||
Expiration: expiration + 3600, // Give tag lists longer TTL
|
||||
}
|
||||
_ = m.client.Set(tagItem)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a key from the cache.
|
||||
func (m *MemcacheProvider) Delete(ctx context.Context, key string) error {
|
||||
// Get tags for this key
|
||||
tagsKey := fmt.Sprintf("cache:tags:%s", key)
|
||||
if item, err := m.client.Get(tagsKey); err == nil {
|
||||
var tags []string
|
||||
if err := json.Unmarshal(item.Value, &tags); err == nil {
|
||||
// Remove key from each tag's key list
|
||||
for _, tag := range tags {
|
||||
tagKey := fmt.Sprintf("cache:tag:%s", tag)
|
||||
if tagItem, err := m.client.Get(tagKey); err == nil {
|
||||
var keys []string
|
||||
if err := json.Unmarshal(tagItem.Value, &keys); err == nil {
|
||||
// Remove current key from the list
|
||||
newKeys := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
if k != key {
|
||||
newKeys = append(newKeys, k)
|
||||
}
|
||||
}
|
||||
// Update the tag's key list
|
||||
if keysData, err := json.Marshal(newKeys); err == nil {
|
||||
tagItem.Value = keysData
|
||||
_ = m.client.Set(tagItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delete the tags key
|
||||
_ = m.client.Delete(tagsKey)
|
||||
}
|
||||
|
||||
// Delete the actual key
|
||||
err := m.client.Delete(key)
|
||||
if err == memcache.ErrCacheMiss {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteByTag removes all keys associated with the given tag.
|
||||
func (m *MemcacheProvider) DeleteByTag(ctx context.Context, tag string) error {
|
||||
tagKey := fmt.Sprintf("cache:tag:%s", tag)
|
||||
|
||||
// Get all keys associated with this tag
|
||||
item, err := m.client.Get(tagKey)
|
||||
if err == memcache.ErrCacheMiss {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var keys []string
|
||||
if err := json.Unmarshal(item.Value, &keys); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal tag keys: %w", err)
|
||||
}
|
||||
|
||||
// Delete all keys
|
||||
for _, key := range keys {
|
||||
_ = m.client.Delete(key)
|
||||
// Also delete the tags key for this cache key
|
||||
tagsKey := fmt.Sprintf("cache:tags:%s", key)
|
||||
_ = m.client.Delete(tagsKey)
|
||||
}
|
||||
|
||||
// Delete the tag key itself
|
||||
_ = m.client.Delete(tagKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByPattern removes all keys matching the pattern.
|
||||
// Note: Memcache does not support pattern-based deletion natively.
|
||||
// This is a no-op for memcache and returns an error.
|
||||
func (m *MemcacheProvider) DeleteByPattern(ctx context.Context, pattern string) error {
|
||||
return fmt.Errorf("pattern-based deletion is not supported by Memcache")
|
||||
}
|
||||
|
||||
// Clear removes all items from the cache.
|
||||
func (m *MemcacheProvider) Clear(ctx context.Context) error {
|
||||
return m.client.FlushAll()
|
||||
}
|
||||
|
||||
// Exists checks if a key exists in the cache.
|
||||
func (m *MemcacheProvider) Exists(ctx context.Context, key string) bool {
|
||||
_, err := m.client.Get(key)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Close closes the provider and releases any resources.
|
||||
func (m *MemcacheProvider) Close() error {
|
||||
// Memcache client doesn't have a close method
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stats returns statistics about the cache provider.
|
||||
// Note: Memcache provider returns limited statistics.
|
||||
func (m *MemcacheProvider) Stats(ctx context.Context) (*CacheStats, error) {
|
||||
stats := &CacheStats{
|
||||
ProviderType: "memcache",
|
||||
ProviderStats: map[string]any{
|
||||
"note": "Memcache does not provide detailed statistics through the standard client",
|
||||
},
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// memoryItem represents a cached item in memory.
|
||||
type memoryItem struct {
|
||||
Value []byte
|
||||
Expiration time.Time
|
||||
LastAccess time.Time
|
||||
HitCount int64
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// isExpired checks if the item has expired.
|
||||
func (m *memoryItem) isExpired() bool {
|
||||
if m.Expiration.IsZero() {
|
||||
return false
|
||||
}
|
||||
return time.Now().After(m.Expiration)
|
||||
}
|
||||
|
||||
// MemoryProvider is an in-memory implementation of the Provider interface.
|
||||
type MemoryProvider struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]*memoryItem
|
||||
tagToKeys map[string]map[string]struct{} // tag -> set of keys
|
||||
options *Options
|
||||
hits atomic.Int64
|
||||
misses atomic.Int64
|
||||
}
|
||||
|
||||
// NewMemoryProvider creates a new in-memory cache provider.
|
||||
func NewMemoryProvider(opts *Options) *MemoryProvider {
|
||||
if opts == nil {
|
||||
opts = &Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
MaxSize: 10000,
|
||||
}
|
||||
}
|
||||
|
||||
return &MemoryProvider{
|
||||
items: make(map[string]*memoryItem),
|
||||
tagToKeys: make(map[string]map[string]struct{}),
|
||||
options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a value from the cache by key.
|
||||
func (m *MemoryProvider) Get(ctx context.Context, key string) ([]byte, bool) {
|
||||
// First try with read lock for fast path
|
||||
m.mu.RLock()
|
||||
item, exists := m.items[key]
|
||||
if !exists {
|
||||
m.mu.RUnlock()
|
||||
m.misses.Add(1)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if item.isExpired() {
|
||||
m.mu.RUnlock()
|
||||
// Upgrade to write lock to delete expired item
|
||||
m.mu.Lock()
|
||||
delete(m.items, key)
|
||||
m.mu.Unlock()
|
||||
m.misses.Add(1)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Update stats and access time with write lock
|
||||
value := item.Value
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Update access tracking with write lock
|
||||
m.mu.Lock()
|
||||
item.LastAccess = time.Now()
|
||||
item.HitCount++
|
||||
m.mu.Unlock()
|
||||
|
||||
m.hits.Add(1)
|
||||
return value, true
|
||||
}
|
||||
|
||||
// Set stores a value in the cache with the specified TTL.
|
||||
func (m *MemoryProvider) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if ttl == 0 {
|
||||
ttl = m.options.DefaultTTL
|
||||
}
|
||||
|
||||
var expiration time.Time
|
||||
if ttl > 0 {
|
||||
expiration = time.Now().Add(ttl)
|
||||
}
|
||||
|
||||
// Check max size and evict if necessary
|
||||
if m.options.MaxSize > 0 && len(m.items) >= m.options.MaxSize {
|
||||
if _, exists := m.items[key]; !exists {
|
||||
m.evictOne()
|
||||
}
|
||||
}
|
||||
|
||||
m.items[key] = &memoryItem{
|
||||
Value: value,
|
||||
Expiration: expiration,
|
||||
LastAccess: time.Now(),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWithTags stores a value in the cache with the specified TTL and tags.
|
||||
func (m *MemoryProvider) SetWithTags(ctx context.Context, key string, value []byte, ttl time.Duration, tags []string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if ttl == 0 {
|
||||
ttl = m.options.DefaultTTL
|
||||
}
|
||||
|
||||
var expiration time.Time
|
||||
if ttl > 0 {
|
||||
expiration = time.Now().Add(ttl)
|
||||
}
|
||||
|
||||
// Check max size and evict if necessary
|
||||
if m.options.MaxSize > 0 && len(m.items) >= m.options.MaxSize {
|
||||
if _, exists := m.items[key]; !exists {
|
||||
m.evictOne()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove old tag associations if key exists
|
||||
if oldItem, exists := m.items[key]; exists {
|
||||
for _, tag := range oldItem.Tags {
|
||||
if keySet, ok := m.tagToKeys[tag]; ok {
|
||||
delete(keySet, key)
|
||||
if len(keySet) == 0 {
|
||||
delete(m.tagToKeys, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the item
|
||||
m.items[key] = &memoryItem{
|
||||
Value: value,
|
||||
Expiration: expiration,
|
||||
LastAccess: time.Now(),
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
// Add new tag associations
|
||||
for _, tag := range tags {
|
||||
if m.tagToKeys[tag] == nil {
|
||||
m.tagToKeys[tag] = make(map[string]struct{})
|
||||
}
|
||||
m.tagToKeys[tag][key] = struct{}{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a key from the cache.
|
||||
func (m *MemoryProvider) Delete(ctx context.Context, key string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Remove tag associations
|
||||
if item, exists := m.items[key]; exists {
|
||||
for _, tag := range item.Tags {
|
||||
if keySet, ok := m.tagToKeys[tag]; ok {
|
||||
delete(keySet, key)
|
||||
if len(keySet) == 0 {
|
||||
delete(m.tagToKeys, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete(m.items, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByTag removes all keys associated with the given tag.
|
||||
func (m *MemoryProvider) DeleteByTag(ctx context.Context, tag string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Get all keys associated with this tag
|
||||
keySet, exists := m.tagToKeys[tag]
|
||||
if !exists {
|
||||
return nil // No keys with this tag
|
||||
}
|
||||
|
||||
// Delete all items with this tag
|
||||
for key := range keySet {
|
||||
if item, ok := m.items[key]; ok {
|
||||
// Remove this tag from the item's tag list
|
||||
newTags := make([]string, 0, len(item.Tags))
|
||||
for _, t := range item.Tags {
|
||||
if t != tag {
|
||||
newTags = append(newTags, t)
|
||||
}
|
||||
}
|
||||
|
||||
// If item has no more tags, delete it
|
||||
// Otherwise update its tags
|
||||
if len(newTags) == 0 {
|
||||
delete(m.items, key)
|
||||
} else {
|
||||
item.Tags = newTags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the tag mapping
|
||||
delete(m.tagToKeys, tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByPattern removes all keys matching the pattern.
|
||||
func (m *MemoryProvider) DeleteByPattern(ctx context.Context, pattern string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid pattern: %w", err)
|
||||
}
|
||||
|
||||
for key := range m.items {
|
||||
if re.MatchString(key) {
|
||||
delete(m.items, key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear removes all items from the cache.
|
||||
func (m *MemoryProvider) Clear(ctx context.Context) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.items = make(map[string]*memoryItem)
|
||||
m.hits.Store(0)
|
||||
m.misses.Store(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exists checks if a key exists in the cache.
|
||||
func (m *MemoryProvider) Exists(ctx context.Context, key string) bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
item, exists := m.items[key]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
return !item.isExpired()
|
||||
}
|
||||
|
||||
// Close closes the provider and releases any resources.
|
||||
func (m *MemoryProvider) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.items = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stats returns statistics about the cache provider.
|
||||
func (m *MemoryProvider) Stats(ctx context.Context) (*CacheStats, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
// Clean expired items first
|
||||
validKeys := 0
|
||||
for _, item := range m.items {
|
||||
if !item.isExpired() {
|
||||
validKeys++
|
||||
}
|
||||
}
|
||||
|
||||
return &CacheStats{
|
||||
Hits: m.hits.Load(),
|
||||
Misses: m.misses.Load(),
|
||||
Keys: int64(validKeys),
|
||||
ProviderType: "memory",
|
||||
ProviderStats: map[string]any{
|
||||
"capacity": m.options.MaxSize,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// evictOne removes one item from the cache using LRU strategy.
|
||||
func (m *MemoryProvider) evictOne() {
|
||||
var oldestKey string
|
||||
var oldestTime time.Time
|
||||
|
||||
for key, item := range m.items {
|
||||
if item.isExpired() {
|
||||
delete(m.items, key)
|
||||
return
|
||||
}
|
||||
|
||||
if oldestKey == "" || item.LastAccess.Before(oldestTime) {
|
||||
oldestKey = key
|
||||
oldestTime = item.LastAccess
|
||||
}
|
||||
}
|
||||
|
||||
if oldestKey != "" {
|
||||
delete(m.items, oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
// CleanExpired removes all expired items from the cache.
|
||||
func (m *MemoryProvider) CleanExpired(ctx context.Context) int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
count := 0
|
||||
for key, item := range m.items {
|
||||
if item.isExpired() {
|
||||
delete(m.items, key)
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RedisProvider is a Redis implementation of the Provider interface.
|
||||
type RedisProvider struct {
|
||||
client *redis.Client
|
||||
options *Options
|
||||
}
|
||||
|
||||
// RedisConfig contains Redis-specific configuration.
|
||||
type RedisConfig struct {
|
||||
// Host is the Redis server host (default: localhost)
|
||||
Host string
|
||||
|
||||
// Port is the Redis server port (default: 6379)
|
||||
Port int
|
||||
|
||||
// Password for Redis authentication (optional)
|
||||
Password string
|
||||
|
||||
// DB is the Redis database number (default: 0)
|
||||
DB int
|
||||
|
||||
// PoolSize is the maximum number of connections (default: 10)
|
||||
PoolSize int
|
||||
|
||||
// Options contains general cache options
|
||||
Options *Options
|
||||
}
|
||||
|
||||
// NewRedisProvider creates a new Redis cache provider.
|
||||
func NewRedisProvider(config *RedisConfig) (*RedisProvider, error) {
|
||||
if config == nil {
|
||||
config = &RedisConfig{
|
||||
Host: "localhost",
|
||||
Port: 6379,
|
||||
DB: 0,
|
||||
}
|
||||
}
|
||||
|
||||
if config.Host == "" {
|
||||
config.Host = "localhost"
|
||||
}
|
||||
if config.Port == 0 {
|
||||
config.Port = 6379
|
||||
}
|
||||
if config.PoolSize == 0 {
|
||||
config.PoolSize = 10
|
||||
}
|
||||
|
||||
if config.Options == nil {
|
||||
config.Options = &Options{
|
||||
DefaultTTL: 5 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", config.Host, config.Port),
|
||||
Password: config.Password,
|
||||
DB: config.DB,
|
||||
PoolSize: config.PoolSize,
|
||||
})
|
||||
|
||||
// Test connection
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Ping(ctx).Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
|
||||
}
|
||||
|
||||
return &RedisProvider{
|
||||
client: client,
|
||||
options: config.Options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get retrieves a value from the cache by key.
|
||||
func (r *RedisProvider) Get(ctx context.Context, key string) ([]byte, bool) {
|
||||
val, err := r.client.Get(ctx, key).Bytes()
|
||||
if err == redis.Nil {
|
||||
return nil, false
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return val, true
|
||||
}
|
||||
|
||||
// Set stores a value in the cache with the specified TTL.
|
||||
func (r *RedisProvider) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
|
||||
if ttl == 0 {
|
||||
ttl = r.options.DefaultTTL
|
||||
}
|
||||
|
||||
return r.client.Set(ctx, key, value, ttl).Err()
|
||||
}
|
||||
|
||||
// SetWithTags stores a value in the cache with the specified TTL and tags.
|
||||
func (r *RedisProvider) SetWithTags(ctx context.Context, key string, value []byte, ttl time.Duration, tags []string) error {
|
||||
if ttl == 0 {
|
||||
ttl = r.options.DefaultTTL
|
||||
}
|
||||
|
||||
pipe := r.client.Pipeline()
|
||||
|
||||
// Set the value
|
||||
pipe.Set(ctx, key, value, ttl)
|
||||
|
||||
// Add key to each tag's set
|
||||
for _, tag := range tags {
|
||||
tagKey := fmt.Sprintf("cache:tag:%s", tag)
|
||||
pipe.SAdd(ctx, tagKey, key)
|
||||
// Set expiration on tag set (longer than cache items to ensure cleanup)
|
||||
if ttl > 0 {
|
||||
pipe.Expire(ctx, tagKey, ttl+time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
// Store tags for this key for later cleanup
|
||||
if len(tags) > 0 {
|
||||
tagsKey := fmt.Sprintf("cache:tags:%s", key)
|
||||
pipe.SAdd(ctx, tagsKey, tags)
|
||||
if ttl > 0 {
|
||||
pipe.Expire(ctx, tagsKey, ttl)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete removes a key from the cache.
|
||||
func (r *RedisProvider) Delete(ctx context.Context, key string) error {
|
||||
pipe := r.client.Pipeline()
|
||||
|
||||
// Get tags for this key
|
||||
tagsKey := fmt.Sprintf("cache:tags:%s", key)
|
||||
tags, err := r.client.SMembers(ctx, tagsKey).Result()
|
||||
if err == nil && len(tags) > 0 {
|
||||
// Remove key from each tag set
|
||||
for _, tag := range tags {
|
||||
tagKey := fmt.Sprintf("cache:tag:%s", tag)
|
||||
pipe.SRem(ctx, tagKey, key)
|
||||
}
|
||||
// Delete the tags key
|
||||
pipe.Del(ctx, tagsKey)
|
||||
}
|
||||
|
||||
// Delete the actual key
|
||||
pipe.Del(ctx, key)
|
||||
|
||||
_, err = pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteByTag removes all keys associated with the given tag.
|
||||
func (r *RedisProvider) DeleteByTag(ctx context.Context, tag string) error {
|
||||
tagKey := fmt.Sprintf("cache:tag:%s", tag)
|
||||
|
||||
// Get all keys associated with this tag
|
||||
keys, err := r.client.SMembers(ctx, tagKey).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pipe := r.client.Pipeline()
|
||||
|
||||
// Delete all keys and their tag associations
|
||||
for _, key := range keys {
|
||||
pipe.Del(ctx, key)
|
||||
// Also delete the tags key for this cache key
|
||||
tagsKey := fmt.Sprintf("cache:tags:%s", key)
|
||||
pipe.Del(ctx, tagsKey)
|
||||
}
|
||||
|
||||
// Delete the tag set itself
|
||||
pipe.Del(ctx, tagKey)
|
||||
|
||||
_, err = pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteByPattern removes all keys matching the pattern.
|
||||
func (r *RedisProvider) DeleteByPattern(ctx context.Context, pattern string) error {
|
||||
iter := r.client.Scan(ctx, 0, pattern, 0).Iterator()
|
||||
pipe := r.client.Pipeline()
|
||||
|
||||
count := 0
|
||||
for iter.Next(ctx) {
|
||||
pipe.Del(ctx, iter.Val())
|
||||
count++
|
||||
|
||||
// Execute pipeline in batches of 100
|
||||
if count%100 == 0 {
|
||||
if _, err := pipe.Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
pipe = r.client.Pipeline()
|
||||
}
|
||||
}
|
||||
|
||||
if err := iter.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Execute remaining commands
|
||||
if count%100 != 0 {
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear removes all items from the cache.
|
||||
func (r *RedisProvider) Clear(ctx context.Context) error {
|
||||
return r.client.FlushDB(ctx).Err()
|
||||
}
|
||||
|
||||
// Exists checks if a key exists in the cache.
|
||||
func (r *RedisProvider) Exists(ctx context.Context, key string) bool {
|
||||
result, err := r.client.Exists(ctx, key).Result()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return result > 0
|
||||
}
|
||||
|
||||
// Close closes the provider and releases any resources.
|
||||
func (r *RedisProvider) Close() error {
|
||||
return r.client.Close()
|
||||
}
|
||||
|
||||
// Stats returns statistics about the cache provider.
|
||||
func (r *RedisProvider) Stats(ctx context.Context) (*CacheStats, error) {
|
||||
info, err := r.client.Info(ctx, "stats", "keyspace").Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Redis stats: %w", err)
|
||||
}
|
||||
|
||||
dbSize, err := r.client.DBSize(ctx).Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get DB size: %w", err)
|
||||
}
|
||||
|
||||
// Parse stats from INFO command
|
||||
// This is a simplified version - you may want to parse more detailed stats
|
||||
stats := &CacheStats{
|
||||
Keys: dbSize,
|
||||
ProviderType: "redis",
|
||||
ProviderStats: map[string]any{
|
||||
"info": info,
|
||||
},
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
Reference in New Issue
Block a user