blob: 00b3abdf1dcacb002ddfcb748259f64dc8b2176d [file] [log] [blame]
Akron16343052025-06-17 16:16:13 +02001package service
2
3import (
4 "context"
5 "crypto/md5"
6 "encoding/hex"
7 "encoding/json"
8 "fmt"
9 "strings"
10 "time"
11
Akrone73bc912025-06-17 16:36:45 +020012 "github.com/korap/korap-mcp/config"
Akron16343052025-06-17 16:16:13 +020013 "github.com/korap/korap-mcp/logger"
14 "github.com/maypok86/otter"
15 "github.com/rs/zerolog"
16)
17
18// CacheEntry represents a cached API response
19type CacheEntry struct {
20 Data []byte `json:"data"`
21 Timestamp time.Time `json:"timestamp"`
22 TTL time.Duration `json:"ttl"`
23}
24
25// IsExpired checks if the cache entry has expired
26func (ce *CacheEntry) IsExpired() bool {
27 return time.Since(ce.Timestamp) > ce.TTL
28}
29
30// Cache represents the response cache system
31type Cache struct {
32 cache *otter.Cache[string, *CacheEntry]
33 logger zerolog.Logger
Akrone73bc912025-06-17 16:36:45 +020034 config *config.CacheConfig
Akron16343052025-06-17 16:16:13 +020035}
36
Akrone73bc912025-06-17 16:36:45 +020037// NewCache creates a new cache instance from config.CacheConfig
38func NewCache(cacheConfig *config.CacheConfig) (*Cache, error) {
Akron16343052025-06-17 16:16:13 +020039 // Create default logging config for cache
Akrone73bc912025-06-17 16:36:45 +020040 logConfig := &config.LoggingConfig{
Akron16343052025-06-17 16:16:13 +020041 Level: "info",
42 Format: "text",
43 }
44
Akrone73bc912025-06-17 16:36:45 +020045 if !cacheConfig.Enabled {
Akron16343052025-06-17 16:16:13 +020046 return &Cache{
47 cache: nil,
48 logger: logger.GetLogger(logConfig),
Akrone73bc912025-06-17 16:36:45 +020049 config: cacheConfig,
Akron16343052025-06-17 16:16:13 +020050 }, nil
51 }
52
53 // Create otter cache with specified capacity
Akrone73bc912025-06-17 16:36:45 +020054 cache, err := otter.MustBuilder[string, *CacheEntry](cacheConfig.MaxSize).
Akron16343052025-06-17 16:16:13 +020055 CollectStats().
Akrone73bc912025-06-17 16:36:45 +020056 WithTTL(cacheConfig.GetDefaultTTL()).
Akron16343052025-06-17 16:16:13 +020057 Build()
58 if err != nil {
59 return nil, fmt.Errorf("failed to create cache: %w", err)
60 }
61
62 return &Cache{
63 cache: &cache,
64 logger: logger.GetLogger(logConfig),
Akrone73bc912025-06-17 16:36:45 +020065 config: cacheConfig,
Akron16343052025-06-17 16:16:13 +020066 }, nil
67}
68
69// generateCacheKey creates a unique cache key for a request
70func (c *Cache) generateCacheKey(method, endpoint string, params map[string]any) string {
71 // Create a deterministic key by combining method, endpoint, and parameters
72 var keyParts []string
73 keyParts = append(keyParts, method, endpoint)
74
75 // Add sorted parameters to ensure deterministic cache keys
76 // Note: json.Marshal automatically sorts map keys lexicographically,
77 // providing deterministic JSON output regardless of map iteration order
78 if params != nil {
79 paramsJSON, _ := json.Marshal(params)
80 keyParts = append(keyParts, string(paramsJSON))
81 }
82
83 key := strings.Join(keyParts, "|")
84
85 // Hash the key to keep it reasonable length and provide privacy
86 hash := md5.Sum([]byte(key))
87 return hex.EncodeToString(hash[:])
88}
89
90// Get retrieves a cached response
91func (c *Cache) Get(ctx context.Context, key string) ([]byte, bool) {
92 if !c.config.Enabled || c.cache == nil {
93 return nil, false
94 }
95
96 entry, found := (*c.cache).Get(key)
97 if !found {
98 c.logger.Debug().Str("key", key).Msg("Cache miss")
99 return nil, false
100 }
101
102 // Check if entry has expired
103 if entry.IsExpired() {
104 c.logger.Debug().Str("key", key).Msg("Cache entry expired")
105 (*c.cache).Delete(key)
106 return nil, false
107 }
108
109 c.logger.Debug().Str("key", key).Msg("Cache hit")
110 return entry.Data, true
111}
112
113// Set stores a response in the cache
114func (c *Cache) Set(ctx context.Context, key string, data []byte, ttl time.Duration) {
115 if !c.config.Enabled || c.cache == nil {
116 return
117 }
118
119 entry := &CacheEntry{
120 Data: data,
121 Timestamp: time.Now(),
122 TTL: ttl,
123 }
124
125 (*c.cache).Set(key, entry)
126 c.logger.Debug().Str("key", key).Dur("ttl", ttl).Msg("Cache entry stored")
127}
128
129// Delete removes an entry from the cache
130func (c *Cache) Delete(ctx context.Context, key string) {
131 if !c.config.Enabled || c.cache == nil {
132 return
133 }
134
135 (*c.cache).Delete(key)
136 c.logger.Debug().Str("key", key).Msg("Cache entry deleted")
137}
138
139// Clear removes all entries from the cache
140func (c *Cache) Clear() {
141 if !c.config.Enabled || c.cache == nil {
142 return
143 }
144
145 (*c.cache).Clear()
146 c.logger.Debug().Msg("Cache cleared")
147}
148
149// Stats returns cache statistics
150func (c *Cache) Stats() map[string]interface{} {
151 if !c.config.Enabled || c.cache == nil {
152 return map[string]interface{}{
153 "enabled": false,
154 }
155 }
156
157 stats := (*c.cache).Stats()
158 return map[string]interface{}{
159 "enabled": true,
160 "size": (*c.cache).Size(),
161 "hits": stats.Hits(),
162 "misses": stats.Misses(),
163 "hit_ratio": stats.Ratio(),
164 "evictions": stats.EvictedCount(),
165 "max_size": c.config.MaxSize,
Akrone73bc912025-06-17 16:36:45 +0200166 "default_ttl": c.config.DefaultTTL,
167 "search_ttl": c.config.SearchTTL,
168 "metadata_ttl": c.config.MetadataTTL,
Akron16343052025-06-17 16:16:13 +0200169 }
170}
171
172// GetTTLForEndpoint returns the appropriate TTL for a given endpoint
173func (c *Cache) GetTTLForEndpoint(endpoint string) time.Duration {
174 endpoint = strings.ToLower(endpoint)
175
176 // Search endpoints get shorter TTL
177 if strings.Contains(endpoint, "search") || strings.Contains(endpoint, "query") {
Akrone73bc912025-06-17 16:36:45 +0200178 return c.config.GetSearchTTL()
Akron16343052025-06-17 16:16:13 +0200179 }
180
181 // Metadata and corpus endpoints get longer TTL
182 if strings.Contains(endpoint, "corpus") || strings.Contains(endpoint, "metadata") ||
183 strings.Contains(endpoint, "statistics") || strings.Contains(endpoint, "info") {
Akrone73bc912025-06-17 16:36:45 +0200184 return c.config.GetMetadataTTL()
Akron16343052025-06-17 16:16:13 +0200185 }
186
187 // Default TTL for other endpoints
Akrone73bc912025-06-17 16:36:45 +0200188 return c.config.GetDefaultTTL()
Akron16343052025-06-17 16:16:13 +0200189}
190
191// Close closes the cache and cleans up resources
192func (c *Cache) Close() error {
193 if c.cache != nil {
194 (*c.cache).Clear()
195 (*c.cache).Close()
196 }
197 return nil
198}