diff --git a/main.go b/main.go index 0a5aa47..1745f59 100644 --- a/main.go +++ b/main.go @@ -65,7 +65,7 @@ func main() { stravaActivities := strava.FetchActivities(*minioClient, stravaTokens) stravaCache := cache.New("strava", stravaActivities) r.Get("/strava/cache", stravaCache.Route()) - r.Post("/strava/event", strava.EventRoute(&stravaCache, *minioClient, stravaTokens)) + r.Post("/strava/event", strava.EventRoute(stravaCache, *minioClient, stravaTokens)) r.Get("/strava/event", strava.ChallengeRoute) lumber.Success("init strava cache") diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 91171b8..cc02d54 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "path/filepath" "reflect" "sync" "time" @@ -22,13 +23,12 @@ type Cache[T any] struct { updated time.Time updateCounter prometheus.Counter requestCounter prometheus.Counter + filePath string } -func New[T any](name string, data T) Cache[T] { - return Cache[T]{ - Name: name, - data: data, - updated: time.Now(), +func New[T any](name string, data T) *Cache[T] { + cache := Cache[T]{ + Name: name, updateCounter: promauto.NewCounter(prometheus.CounterOpts{ Name: fmt.Sprintf("cache_%s_updates", name), Help: fmt.Sprintf(`The total number of times the cache "%s" has been updated`, name), @@ -37,10 +37,15 @@ func New[T any](name string, data T) Cache[T] { Name: fmt.Sprintf("cache_%s_requests", name), Help: fmt.Sprintf(`The total number of times the cache "%s" has been requested`, name), }), + filePath: filepath.Join(cacheFolder, fmt.Sprintf("%s.json", name)), } + cache.seedFromFile() + cache.Update(data) + cache.writeToFile() + return &cache } -type response[T any] struct { +type cacheData[T any] struct { Data T `json:"data"` Updated time.Time `json:"updated"` } @@ -54,7 +59,7 @@ func (c *Cache[T]) Route() http.HandlerFunc { } c.mutex.RLock() w.Header().Set("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(response[T]{Data: c.data, Updated: c.updated}) + err := json.NewEncoder(w).Encode(cacheData[T]{Data: c.data, Updated: c.updated}) c.mutex.RUnlock() c.requestCounter.Inc() if err != nil { @@ -75,6 +80,7 @@ func (c *Cache[T]) Update(data T) { c.mutex.Unlock() c.updateCounter.Inc() metrics.CacheUpdates.Inc() + c.writeToFile() if updated { lumber.Success(c.Name, "updated") } diff --git a/pkg/cache/file.go b/pkg/cache/file.go new file mode 100644 index 0000000..99d421f --- /dev/null +++ b/pkg/cache/file.go @@ -0,0 +1,67 @@ +package cache + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/gleich/lumber/v2" +) + +const cacheFolder = "/caches/" + +func (c *Cache[T]) writeToFile() { + var file *os.File + if _, err := os.Stat(c.filePath); os.IsNotExist(err) { + folder := filepath.Dir(c.filePath) + err := os.MkdirAll(folder, 0700) + if err != nil { + lumber.Error(err, "failed to create folder at path:", folder) + return + } + file, err = os.Create(c.filePath) + if err != nil { + lumber.Error(err, "failed to create file at path:", c.filePath) + return + } + } else { + file, err = os.OpenFile(c.filePath, os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + lumber.Error(err, "failed to read file at path:", c.filePath) + return + } + } + defer file.Close() + + b, err := json.Marshal(c.data) + if err != nil { + lumber.Error(err, "encoding data to json failed") + return + } + _, err = file.Write(b) + if err != nil { + lumber.Error(err, "writing data to json failed") + } +} + +// Seed the cache from the persistent cache file +// returns if the cache was able to be seeded or not +func (c *Cache[T]) seedFromFile() { + if _, err := os.Stat(c.filePath); !os.IsNotExist(err) { + b, err := os.ReadFile(c.filePath) + if err != nil { + lumber.Error(err, "reading from cache file from", c.filePath, "failed") + return + } + + var data cacheData[T] + err = json.Unmarshal(b, &data) + if err != nil { + lumber.Error(err, "unmarshal json data failed from:", string(b)) + return + } + + c.data = data.Data + c.updated = data.Updated + } +}