package gocache import ( "bytes" "encoding/gob" "log" "os" "sort" "time" bolt "go.etcd.io/bbolt" ) // SaveToFile stores the content of the cache to a file so that it can be read using // the ReadFromFile function func (cache *Cache) SaveToFile(path string) error { db, err := bolt.Open(path, os.ModePerm, nil) if err != nil { return err } start := time.Now() cache.mutex.RLock() bulkEntries := make([]*Entry, len(cache.entries)) i := 0 for _, v := range cache.entries { bulkEntries[i] = v i++ } cache.mutex.RUnlock() if Debug { log.Printf("unlocked after %s", time.Since(start)) } err = db.Update(func(tx *bolt.Tx) error { _ = tx.DeleteBucket([]byte("entries")) bucket, err := tx.CreateBucket([]byte("entries")) if err != nil { return err } for _, bulkEntry := range bulkEntries { buffer := bytes.Buffer{} err = gob.NewEncoder(&buffer).Encode(bulkEntry) if err != nil { // Failed to encode the value, so we'll skip it. // This is likely due to the fact that the custom struct wasn't registered using gob.Register(...) // See [Persistence - Limitations](https://github.com/TwinProduction/gocache#limitations) continue } bucket.Put([]byte(bulkEntry.Key), buffer.Bytes()) } return nil }) if err != nil { return err } return db.Close() } // ReadFromFile populates the cache using a file created using cache.SaveToFile(path) // // Note that if the number of entries retrieved from the file exceed the configured maxSize, // the extra entries will be automatically evicted according to the EvictionPolicy configured. // This function returns the number of entries evicted, and because this function only reads // from a file and does not modify it, you can safely retry this function after configuring // the cache with the appropriate maxSize, should you desire to. func (cache *Cache) ReadFromFile(path string) (int, error) { db, err := bolt.Open(path, os.ModePerm, nil) if err != nil { return 0, err } defer db.Close() cache.mutex.Lock() defer cache.mutex.Unlock() err = db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte("entries")) // If the bucket doesn't exist, there's nothing to read, so we'll return right now if bucket == nil { return nil } err = bucket.ForEach(func(k, v []byte) error { buffer := new(bytes.Buffer) decoder := gob.NewDecoder(buffer) entry := Entry{} buffer.Write(v) err := decoder.Decode(&entry) if err != nil { // Failed to decode the value, so we'll skip it. // This is likely due to the fact that the custom struct wasn't registered using gob.Register(...) // // Could also be due to a breaking change in a struct's variable. For instance, if the struct has // a variable with a type map[string]string and that variable is modified to map[string]int, // decoding the struct would fail. This can be avoided by using a different variable name every // time you must change the type of a variable within a struct. // // See [Persistence - Limitations](https://github.com/TwinProduction/gocache#limitations) return err } cache.entries[string(k)] = &entry buffer.Reset() return nil }) return err }) if err != nil { return 0, err } // Because pointers don't get stored in the file, we need to relink everything from head to tail var entries []*Entry for _, v := range cache.entries { entries = append(entries, v) } // Sort the slice of entries from oldest to newest sort.Slice(entries, func(i, j int) bool { return entries[i].RelevantTimestamp.Before(entries[j].RelevantTimestamp) }) // Relink the nodes from tail to head var previous *Entry for i := range entries { current := entries[i] if previous == nil { cache.tail = current cache.head = current } else { previous.previous = current current.next = previous cache.head = current } previous = entries[i] if cache.maxMemoryUsage != NoMaxMemoryUsage { cache.memoryUsage += current.SizeInBytes() } } // If the cache doesn't have a maxSize/maxMemoryUsage, then there's no point checking if we need to evict // an entry, so we'll just return now if cache.maxSize == NoMaxSize && cache.maxMemoryUsage == NoMaxMemoryUsage { return 0, nil } // Evict what needs to be evicted numberOfEvictions := 0 // If there's a maxSize and the cache has more entries than the maxSize, evict if cache.maxSize != NoMaxSize && len(cache.entries) > cache.maxSize { for len(cache.entries) > cache.maxSize { numberOfEvictions++ cache.evict() } } // If there's a maxMemoryUsage and the memoryUsage is above the maxMemoryUsage, evict if cache.maxMemoryUsage != NoMaxMemoryUsage && cache.memoryUsage > cache.maxMemoryUsage { for cache.memoryUsage > cache.maxMemoryUsage && len(cache.entries) > 0 { numberOfEvictions++ cache.evict() } } return numberOfEvictions, nil }