lru.go

  1package config
  2
  3import (
  4	"container/list"
  5	"encoding/json"
  6	"fmt"
  7	"log"
  8	"os"
  9	"path/filepath"
 10	"sort"
 11	"sync"
 12	"time"
 13)
 14
 15type Node struct {
 16	Key    string // key = folder:uid:accountID
 17	Folder string
 18	Body   *CachedEmailBody
 19}
 20
 21type LRU struct {
 22	threshold   int
 23	currentSize int
 24	cache       map[string]*list.Element
 25	ll          *list.List
 26	mu          sync.Mutex
 27}
 28
 29var lru *LRU
 30var once sync.Once
 31
 32func GetLRUInstance(threshold int) *LRU {
 33	once.Do(
 34		func() {
 35			lru = &LRU{
 36				threshold: threshold,
 37				cache:     make(map[string]*list.Element),
 38				ll:        list.New(),
 39			}
 40
 41			if err := lru.LoadFromDisk(); err != nil {
 42				log.Printf("Failed to load LRU from disk: %v\n", err)
 43			}
 44
 45		})
 46
 47	lru.mu.Lock()
 48	defer lru.mu.Unlock()
 49
 50	if lru.threshold != threshold {
 51		lru.threshold = threshold
 52		if lru.currentSize > lru.threshold {
 53			lru.evict()
 54		}
 55	}
 56
 57	return lru
 58}
 59
 60func (lru *LRU) makeKey(folder string, uid uint32, accountID string) string {
 61	return fmt.Sprintf("%s:%d:%s", folder, uid, accountID)
 62}
 63
 64func removeBodyFromDisk(folder string, uid uint32, accountID string) error {
 65	cache, err := LoadEmailBodyCache(folder)
 66
 67	if err != nil {
 68		return nil
 69	}
 70
 71	kept := cache.Bodies[:0]
 72	for _, b := range cache.Bodies {
 73		if !(b.UID == uid && b.AccountID == accountID) {
 74			kept = append(kept, b)
 75		}
 76	}
 77
 78	if len(kept) == len(cache.Bodies) {
 79		return nil
 80	}
 81
 82	cache.Bodies = kept
 83	return saveEmailBodyCache(cache)
 84}
 85
 86func (lru *LRU) evict() {
 87	for lru.currentSize > lru.threshold {
 88		back := lru.ll.Back()
 89
 90		if back == nil {
 91			break
 92		}
 93
 94		node := back.Value.(*Node)
 95
 96		lru.ll.Remove(back)
 97		delete(lru.cache, node.Key)
 98		lru.currentSize -= node.Body.SizeBytes
 99
100		_ = removeBodyFromDisk(node.Folder, node.Body.UID, node.Body.AccountID)
101	}
102}
103
104func (lru *LRU) LoadFromDisk() error {
105	dir, err := bodyCacheDir()
106
107	if err != nil {
108		return err
109	}
110
111	entries, err := os.ReadDir(dir)
112	if err != nil {
113		if os.IsNotExist(err) {
114			return nil
115		}
116		return err
117	}
118
119	var caches []EmailBodyCache
120
121	for _, entry := range entries {
122		if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
123			continue
124		}
125
126		path := filepath.Join(dir, entry.Name())
127		data, err := SecureReadFile(path)
128		if err != nil {
129			continue
130		}
131
132		var cache EmailBodyCache
133		if err := json.Unmarshal(data, &cache); err != nil {
134			continue
135		}
136
137		for i := range cache.Bodies {
138			if cache.Bodies[i].SizeBytes <= 0 {
139				cache.Bodies[i].SizeBytes = calculateEmailBodySize(&cache.Bodies[i])
140			}
141		}
142
143		caches = append(caches, cache)
144	}
145
146	type bodyWithFolder struct {
147		folder string
148		body   CachedEmailBody
149	}
150
151	var allBodies []bodyWithFolder
152
153	for _, cache := range caches {
154		for _, body := range cache.Bodies {
155			allBodies = append(allBodies, bodyWithFolder{
156				folder: cache.FolderName,
157				body:   body,
158			})
159		}
160	}
161
162	sort.Slice(allBodies, func(i, j int) bool {
163		ti := allBodies[i].body.LastAccessedAt
164		tj := allBodies[j].body.LastAccessedAt
165		return ti.After(tj)
166	})
167
168	for i := len(allBodies) - 1; i >= 0; i-- {
169		item := allBodies[i]
170
171		if item.body.SizeBytes > lru.threshold {
172			continue
173		}
174
175		key := lru.makeKey(item.folder, item.body.UID, item.body.AccountID)
176
177		bodyCopy := item.body
178		node := &Node{
179			Key:    key,
180			Folder: item.folder,
181			Body:   &bodyCopy,
182		}
183
184		e := lru.ll.PushFront(node)
185		lru.cache[key] = e
186		lru.currentSize += item.body.SizeBytes
187	}
188
189	if lru.currentSize > lru.threshold {
190		lru.evict()
191	}
192	return nil
193}
194
195func saveEmailBodyToDisk(folder string, body *CachedEmailBody) error {
196	cache, err := LoadEmailBodyCache(folder)
197
198	if err != nil {
199		cache = &EmailBodyCache{FolderName: folder}
200	}
201
202	found := false
203	for i, b := range cache.Bodies {
204		if b.UID == body.UID && b.AccountID == body.AccountID {
205			cache.Bodies[i] = *body
206			found = true
207			break
208		}
209	}
210	if !found {
211		cache.Bodies = append(cache.Bodies, *body)
212	}
213
214	return saveEmailBodyCache(cache)
215}
216
217func (lru *LRU) Get(folder string, uid uint32, accountID string) *Node {
218	lru.mu.Lock()
219	defer lru.mu.Unlock()
220
221	key := lru.makeKey(folder, uid, accountID)
222
223	e, ok := lru.cache[key]
224
225	if !ok {
226		return nil
227	}
228
229	lru.ll.MoveToFront(e)
230
231	node := e.Value.(*Node)
232	node.Body.LastAccessedAt = time.Now()
233
234	_ = saveEmailBodyToDisk(folder, node.Body)
235
236	return node
237}
238
239func (lru *LRU) removeKey(key string) {
240	if e, ok := lru.cache[key]; ok {
241		node := e.Value.(*Node)
242
243		lru.currentSize -= node.Body.SizeBytes
244		lru.ll.Remove(e)
245		delete(lru.cache, key)
246	}
247}
248
249func (lru *LRU) Put(folder string, uid uint32, accountID string, body *CachedEmailBody) {
250	lru.mu.Lock()
251	defer lru.mu.Unlock()
252
253	key := lru.makeKey(folder, uid, accountID)
254
255	if body.SizeBytes > lru.threshold {
256		lru.removeKey(key)
257		_ = removeBodyFromDisk(folder, uid, accountID)
258		return
259	}
260
261	body.LastAccessedAt = time.Now()
262
263	if e, ok := lru.cache[key]; ok {
264		node := e.Value.(*Node)
265		lru.currentSize -= node.Body.SizeBytes
266		lru.currentSize += body.SizeBytes
267		node.Body = body
268		lru.ll.MoveToFront(e)
269	} else {
270		node := &Node{
271			Key:    key,
272			Folder: folder,
273			Body:   body,
274		}
275		e := lru.ll.PushFront(node)
276		lru.cache[key] = e
277		lru.currentSize += body.SizeBytes
278	}
279
280	lru.evict()
281
282	_ = saveEmailBodyToDisk(folder, body)
283}
284
285func (lru *LRU) Delete(folder string, uid uint32, accountID string) {
286	lru.mu.Lock()
287	defer lru.mu.Unlock()
288
289	key := lru.makeKey(folder, uid, accountID)
290	lru.removeKey(key)
291	_ = removeBodyFromDisk(folder, uid, accountID)
292}
293
294func resetLRU() {
295	once = sync.Once{}
296	lru = nil
297}