Skip to content

Commit

Permalink
Add caching with expiry to the cache package
Browse files Browse the repository at this point in the history
Why?
Each request to yr.no comes with an expiry time. We should not ask for
the same forecast again if the first one is not expired.

How?
Extend the cache package to use PutWithTTL to the database. This will
save an object. When we ask to retrieve that object, if it is expired
it will throw a KeyExpired error.

In order to allow this to happen, we need to change the cache
implementation to pass through the errors which are returned from
bitcask.

Currently all of the logic is inside the command function. Ideally, this
could be split out into a package.
  • Loading branch information
CiaraTully committed Apr 1, 2024
1 parent 11e21d9 commit 04f4217
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 28 deletions.
45 changes: 35 additions & 10 deletions cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"hash/fnv"
"log"
"os"
"time"

"git.mills.io/prologic/bitcask"
"github.com/google/uuid"
Expand Down Expand Up @@ -67,25 +68,49 @@ func (cache *Cache) Add(object []byte, key string) {
fmt.Print(err)
}
}
func (cache *Cache) Fold(f func(key []byte) error) {
cache.database.Fold(f)
}
func (cache *Cache) Get(key string) string {
func (cache *Cache) AddWithTTL(object []byte, key string, ttl time.Duration ) {
cache.open()
defer cache.close()
hashKey := getHashKey(key)
if cache.database.Has([]byte(hashKey)) {
val, err := cache.database.Get([]byte(hashKey))
cache.close()
return
}
id := uuid.New().String()
if _, err := os.Stat(cache.cacheFolder + "/" + id); errors.Is(err, os.ErrNotExist) {
_, err := os.Create(cache.cacheFolder + "/" + id)
if err != nil {
log.Fatalln(err)
log.Println(err)
}
object, err := os.ReadFile(cache.cacheFolder + "/" + string(val))
err = os.WriteFile(cache.cacheFolder + "/"+ id, []byte(object), 0644)
fmt.Println()
if err != nil {
log.Fatalln(err)
fmt.Println(err)
}
return string(object)
}
return ""
err := cache.database.PutWithTTL([]byte(hashKey), []byte(id), ttl)
if err != nil {
fmt.Print(err)
}
}


func (cache *Cache) Fold(f func(key []byte) error) {
cache.database.Fold(f)
}
func (cache *Cache) Get(key string) (string, error) {
cache.open()
defer cache.close()
hashKey := getHashKey(key)
val, err := cache.database.Get([]byte(hashKey))
if err == bitcask.ErrKeyNotFound || err == bitcask.ErrKeyExpired {
return "", err
}
object, err := os.ReadFile(cache.cacheFolder + "/" + string(val))
if err != nil {
log.Fatalln(err)
}
return string(object), nil
}

func checkCacheFolder(cacheFolder string) {
Expand Down
22 changes: 20 additions & 2 deletions cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cache
import (
"os"
"testing"
"time"
)

func TestNewCache(t *testing.T) {
Expand Down Expand Up @@ -32,15 +33,32 @@ func TestAdd(t *testing.T) {
}
}

func TestAddWithTTL(t *testing.T) {
tempDir := t.TempDir()
dbPath := tempDir + "/db"
cacheFolder := tempDir + "/.cache"
cache := NewCache(dbPath, cacheFolder)
cache.AddWithTTL([]byte("test"), "key", time.Hour)

if _, err := os.Stat(dbPath); os.IsNotExist(err) {
t.Fatalf("DB folder not in expected location %v", err)
}

files, _ := os.ReadDir(cacheFolder)
if len(files) != 1 {
t.Fatalf("file not added to cache folder")
}
}

func TestGet(t *testing.T) {
tempDir := t.TempDir()
dbPath := tempDir + "/db"
cacheFolder := tempDir + "/.cache"
cache := NewCache(dbPath, cacheFolder)
cacheKey := "key"
cache.Add([]byte("test"), cacheKey)
result := cache.Get(cacheKey)

result, _ := cache.Get(cacheKey)
if result != "test" {
t.Fatalf("cached value not retrieved")
}
Expand Down
52 changes: 36 additions & 16 deletions cmd/location.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ Copyright © 2023 NAME HERE <EMAIL ADDRESS>
package cmd

import (
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
"whats-the-weather/main/cache"
"whats-the-weather/main/geocoder"

Expand All @@ -33,39 +35,57 @@ var locationCmd = &cobra.Command{
cahce_key := args[0]
cache_db := cache.NewCache("tmp/db", ".cache")

coords := cache_db.Get(cahce_key)
var longitude string
var latitude string

if coords != "" {
longLat := strings.Split(coords, ",")
longitude = longLat[1]
latitude = longLat[0]
fmt.Printf("Location: %s\n", args[0])
} else {
coords, err := cache_db.Get(cahce_key);
if err != nil {
coordsAndPlaces, _ := geoClient.FindCoordinates(args[0])
firstMatch := coordsAndPlaces[0]
longitude = firstMatch.Longitude
latitude = firstMatch.Latitude
cache_db.Add([]byte(firstMatch.Latitude+","+firstMatch.Longitude), cahce_key)
coords = firstMatch.Latitude+","+firstMatch.Longitude
fmt.Printf("Location: %s\n", firstMatch.DisplayName)

} else {
longLat := strings.Split(coords, ",")
longitude = longLat[1]
latitude = longLat[0]
fmt.Printf("Location: %s\n", args[0])
}

// the following two conversions shoulg be handled in te geocode
// the following two conversions shoulg be handled in the geocode
// packaged when the json is being parsed. They should already be
// float64 objects but for now, we'll convert them here
floatValueLatitude, err := strconv.ParseFloat(latitude, 64)
floatValueLongitude, err := strconv.ParseFloat(longitude, 64)
if err != nil {
fmt.Println("Error:", err)
floatValueLatitude, err1 := strconv.ParseFloat(latitude, 64)
floatValueLongitude, err2 := strconv.ParseFloat(longitude, 64)
if err1 != nil || err2 != nil {
fmt.Println("Error:", err1)
return
}
forecast, _, err := locationforecast.GetCompact(yrClient, floatValueLatitude, floatValueLongitude)

var forecast *locationforecast.GeoJson

cachedForecast, err := cache_db.Get(coords)
if err != nil {
fmt.Print("An error has occured:")
fmt.Println(err)
return
var resp *http.Response
forecast, resp, err = locationforecast.GetCompact(yrClient, floatValueLatitude, floatValueLongitude)
if err != nil {
fmt.Print("An error has occured:")
fmt.Println(err)
return
}
expiresAt := resp.Header.Get("Expires")
parsedExpiresAt, _ := time.Parse(time.RFC1123, expiresAt)
timeToLive := time.Until(parsedExpiresAt)
jsonForecast, _ := json.Marshal(forecast)
cache_db.AddWithTTL([]byte(jsonForecast), coords, timeToLive)
} else {
json.Unmarshal([]byte(cachedForecast), &forecast)

}

forecastData := forecast.Properties.Timeseries[0].Data
temperatureNow := forecastData.Instant.Details.AirTemperature
t := table.NewWriter()
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.21.3

require (
barista.run v0.0.0-20231013003214-d5b04d179ffa
github.com/golang/protobuf v1.5.3
github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/spf13/cobra v1.7.0
github.com/zapling/yr.no-golang-client v0.0.0-20210309083036-f048e27db764
Expand All @@ -19,6 +20,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
golang.org/x/exp v0.0.0-20200228211341-fcea875c7e85 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
Expand Down Expand Up @@ -709,6 +711,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down

0 comments on commit 04f4217

Please sign in to comment.