From d21881150c09986571a563eaf30bc1687787e63f Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Sat, 25 Apr 2020 11:27:34 +0200 Subject: Improved caching --- internal/feed/cache.go | 230 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 internal/feed/cache.go (limited to 'internal/feed/cache.go') diff --git a/internal/feed/cache.go b/internal/feed/cache.go new file mode 100644 index 0000000..4f27144 --- /dev/null +++ b/internal/feed/cache.go @@ -0,0 +1,230 @@ +package feed + +import ( + "bufio" + "crypto/sha256" + "encoding/gob" + "errors" + "fmt" + "os" + "time" + + "github.com/Necoro/feed2imap-go/internal/log" +) + +const ( + currentVersion byte = 1 + startFeedId uint64 = 1 +) + +type Cache interface { + findItem(*Feed) CachedFeed + Version() byte + transformToCurrent() (Cache, error) +} + +type feedId uint64 + +type feedDescriptor struct { + Name string + Url string +} + +type CachedFeed interface { + Checked(withFailure bool) + Failures() uint +} + +type cachedFeed struct { + LastCheck time.Time + NumFailures uint // can't be named `Failures` b/c it'll collide with the interface + Items []cachedItem +} + +func (cf *cachedFeed) Checked(withFailure bool) { + cf.LastCheck = time.Now() + if withFailure { + cf.NumFailures++ + } else { + cf.NumFailures = 0 + } +} + +func (cf *cachedFeed) Failures() uint { + return cf.NumFailures +} + +type itemHash [sha256.Size]byte + +type cachedItem struct { + Uid string + Title string + Link string + Date time.Time + Updated time.Time + Creator string + Hash itemHash +} + +type v1Cache struct { + version byte + Ids map[feedDescriptor]feedId + NextId uint64 + Feeds map[feedId]*cachedFeed +} + +func (cache *v1Cache) Version() byte { + return cache.version +} + +func New() Cache { + cache := v1Cache{ + Ids: map[feedDescriptor]feedId{}, + Feeds: map[feedId]*cachedFeed{}, + NextId: startFeedId, + } + cache.version = currentVersion + return &cache +} + +func cacheForVersion(version byte) (Cache, error) { + switch version { + case 1: + return New(), nil + default: + return nil, fmt.Errorf("unknown cache version '%d'", version) + } +} + +func (cache *v1Cache) transformToCurrent() (Cache, error) { + return cache, nil +} + +func (cache *v1Cache) getItem(id feedId) CachedFeed { + feed, ok := cache.Feeds[id] + if !ok { + feed = &cachedFeed{} + cache.Feeds[id] = feed + } + return feed +} + +func (cache *v1Cache) findItem(feed *Feed) CachedFeed { + if feed.cached != nil { + return feed.cached.(*cachedFeed) + } + + fId := feedDescriptor{Name: feed.Name, Url: feed.Url} + id, ok := cache.Ids[fId] + if !ok { + var otherId feedDescriptor + changed := false + for otherId, id = range cache.Ids { + if otherId.Name == fId.Name { + log.Warnf("Feed %s seems to have changed URLs: New '%s', old '%s'. Updating.", + fId.Name, fId.Url, otherId.Url) + changed = true + break + } else if otherId.Url == fId.Url { + log.Warnf("Feed with URL '%s' seems to have changed its name: New '%s', old '%s'. Updating", + fId.Url, fId.Name, otherId.Name) + changed = true + break + } + } + if changed { + delete(cache.Ids, otherId) + } else { + id = feedId(cache.NextId) + cache.NextId++ + } + + cache.Ids[fId] = id + } + + item := cache.getItem(id) + feed.cached = item + return item +} + +func (feeds *Feeds) StoreCache(fileName string) error { + cache := feeds.cache + if cache == nil { + return fmt.Errorf("trying to store nil cache") + } + if cache.Version() != currentVersion { + return fmt.Errorf("trying to store cache with unsupported version '%d' (current: '%d')", cache.Version(), currentVersion) + } + + f, err := os.Create(fileName) + if err != nil { + return fmt.Errorf("trying to store cache to '%s': %w", fileName, err) + } + defer f.Close() + + writer := bufio.NewWriter(f) + if err = writer.WriteByte(currentVersion); err != nil { + return fmt.Errorf("writing to '%s': %w", fileName, err) + } + + encoder := gob.NewEncoder(writer) + if err = encoder.Encode(cache); err != nil { + return fmt.Errorf("encoding cache: %w", err) + } + + writer.Flush() + log.Printf("Stored cache to '%s'.", fileName) + + return nil +} + +func (feeds *Feeds) LoadCache(fileName string) error { + cache, err := loadCache(fileName) + if err != nil { + return err + } + feeds.cache = cache + + for _, feed := range feeds.feeds { + feed.cached = cache.findItem(feed) + } + return nil +} + +func loadCache(fileName string) (Cache, error) { + f, err := os.Open(fileName) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // no cache there yet -- make new + return New(), nil + } + return nil, fmt.Errorf("opening cache at '%s': %w", fileName, err) + } + defer f.Close() + + log.Printf("Loading cache from '%s'", fileName) + + reader := bufio.NewReader(f) + version, err := reader.ReadByte() + if err != nil { + return nil, fmt.Errorf("reading from '%s': %w", fileName, err) + } + + cache, err := cacheForVersion(version) + if err != nil { + return nil, err + } + + decoder := gob.NewDecoder(reader) + if err = decoder.Decode(cache); err != nil { + return nil, fmt.Errorf("decoding for version '%d' from '%s': %w", version, fileName, err) + } + + if cache, err = cache.transformToCurrent(); err != nil { + return nil, fmt.Errorf("cannot transform from version %d to %d: %w", version, currentVersion, err) + } + + log.Printf("Loaded cache (version %d), transformed to version %d.", version, currentVersion) + + return cache, nil +} -- cgit v1.2.3-54-g00ecf