diff --git a/docker-compose.yaml b/docker-compose.yaml index 3decf39..f1ffe82 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -33,5 +33,11 @@ services: interval: 2s timeout: 5s retries: 10 + redis: + image: 'bitnami/redis:latest' + environment: + - ALLOW_EMPTY_PASSWORD=yes + ports: + - 5003:6379 volumes: pgdata: diff --git a/go.mod b/go.mod index 8ed7c29..778c06b 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,11 @@ module url-short go 1.22.3 require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/redis/go-redis/v9 v9.5.3 // indirect golang.org/x/crypto v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index c471d31..4a54df3 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,14 @@ +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU= +github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= diff --git a/handlers.go b/handlers.go index 51e7c45..003fcb8 100644 --- a/handlers.go +++ b/handlers.go @@ -15,6 +15,7 @@ import ( "time" "github.com/golang-jwt/jwt/v5" + "github.com/redis/go-redis/v9" "golang.org/x/crypto/bcrypt" "url-short/internal/database" @@ -22,6 +23,7 @@ import ( type apiConfig struct { DB *database.Queries + RDB *redis.Client JWTSecret string } @@ -64,11 +66,9 @@ type APIUserResponseNoToken struct { } func (apiCfg *apiConfig) healthz(w http.ResponseWriter, r *http.Request) { - payload := HealthResponse{ + respondWithJSON(w, http.StatusOK, HealthResponse{ Status: "ok", - } - - respondWithJSON(w, http.StatusOK, payload) + }) } func (apiCfg *apiConfig) postLongURL(w http.ResponseWriter, r *http.Request, user database.User) { @@ -125,15 +125,60 @@ func (apiCfg *apiConfig) getShortURL(w http.ResponseWriter, r *http.Request) { return } - row, err := apiCfg.DB.SelectURL(r.Context(), query) + cacheVal, err := apiCfg.RDB.Get(r.Context(), query).Result() - if err != nil { + switch { + case err == redis.Nil: + log.Printf("cache miss, key %s does not exists, writing to redis", query) + + row, err := apiCfg.DB.SelectURL(r.Context(), query) + + if err != nil { + log.Println(err) + respondWithError(w, http.StatusInternalServerError, "database error") + return + } + + err = apiCfg.RDB.Set(r.Context(), query, row.LongUrl, (time.Hour * 1)).Err() + + if err != nil { + log.Printf("could not write to redis cache %s", err) + } + + http.Redirect(w, r, row.LongUrl, http.StatusMovedPermanently) + return + + case err != nil: log.Println(err) - respondWithError(w, http.StatusInternalServerError, "database error") + + row, err := apiCfg.DB.SelectURL(r.Context(), query) + + if err != nil { + log.Println(err) + respondWithError(w, http.StatusInternalServerError, "database error") + return + } + + http.Redirect(w, r, row.LongUrl, http.StatusMovedPermanently) + return + + case cacheVal == "": + log.Printf("key %s does not have a value", query) + + row, err := apiCfg.DB.SelectURL(r.Context(), query) + + if err != nil { + log.Println(err) + respondWithError(w, http.StatusInternalServerError, "database error") + return + } + + http.Redirect(w, r, row.LongUrl, http.StatusMovedPermanently) return } - http.Redirect(w, r, row.LongUrl, http.StatusMovedPermanently) + log.Printf("cache hit for key %s", cacheVal) + http.Redirect(w, r, cacheVal, http.StatusMovedPermanently) } func (apiCfg *apiConfig) deleteShortURL(w http.ResponseWriter, r *http.Request, user database.User) { @@ -185,6 +230,12 @@ func (apiCfg *apiConfig) putShortURL(w http.ResponseWriter, r *http.Request, use return } + err = apiCfg.RDB.Set(r.Context(), query, payload.LongURL, (time.Hour * 1)).Err() + + if err != nil { + log.Println(err) + } + respondWithJSON(w, http.StatusOK, ShortURLUpdateResponse{ LongURL: payload.LongURL, ShortURL: query, diff --git a/main.go b/main.go index 609c523..61bab33 100644 --- a/main.go +++ b/main.go @@ -5,14 +5,17 @@ import ( "log" "net/http" "os" + "url-short/internal/database" _ "github.com/lib/pq" + "github.com/redis/go-redis/v9" ) func main() { serverPort := os.Getenv("SERVER_PORT") dbURL := os.Getenv("PG_CONN") + rdbURL := os.Getenv("RDB_CONN") jwtSecret := os.Getenv("JWT_SECRET") db, err := sql.Open("postgres", dbURL) @@ -30,8 +33,17 @@ func main() { Handler: mux, } + opt, err := redis.ParseURL(rdbURL) + + if err != nil { + log.Fatal(err) + } + + redisClient := redis.NewClient(opt) + apiCfg := apiConfig{ DB: dbQueries, + RDB: redisClient, JWTSecret: jwtSecret, }