diff --git a/cache/redis.go b/cache/redis.go index 58f7d62..50e9fce 100644 --- a/cache/redis.go +++ b/cache/redis.go @@ -8,7 +8,7 @@ import ( ) type RedisStore struct { - client *redis.Client + Client *redis.Client } func NewRedisStore() *RedisStore { @@ -18,18 +18,18 @@ func NewRedisStore() *RedisStore { DB: 0, }) - return &RedisStore{client: client} + return &RedisStore{Client: client} } func (r *RedisStore) SetKey(key string, value []byte, ttl time.Duration) { - err := r.client.Set(r.client.Context(), key, value, ttl).Err() + err := r.Client.Set(r.Client.Context(), key, value, ttl).Err() if err != nil { log.Logger().Info("Unable to set key in redis" + key + err.Error()) } } func (r *RedisStore) Get(key string) []byte { - get := r.client.Get(r.client.Context(), key) + get := r.Client.Get(r.Client.Context(), key) resp, err := get.Bytes() if err != nil { @@ -40,13 +40,13 @@ func (r *RedisStore) Get(key string) []byte { } func (r *RedisStore) Delete(key string) error { - return r.client.Del(r.client.Context(), key).Err() + return r.Client.Del(r.Client.Context(), key).Err() } func (r *RedisStore) DeleteAll() error { - return r.client.FlushDB(r.client.Context()).Err() + return r.Client.FlushDB(r.Client.Context()).Err() } func (r *RedisStore) Close() { - r.client.Close() + r.Client.Close() } diff --git a/cache/redis_test.go b/cache/redis_test.go new file mode 100644 index 0000000..4d7fbd8 --- /dev/null +++ b/cache/redis_test.go @@ -0,0 +1,54 @@ +package cache + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/alicebob/miniredis/v2" + "github.com/go-redis/redis/v8" +) + +func TestRedisStore(t *testing.T) { + server := miniredis.RunT(t) + client := redis.NewClient(&redis.Options{ + Addr: server.Addr(), + }) + + store := RedisStore{ + Client: client, + } + + key := "test_key" + value := []byte("test_value") + ttl := 10 * time.Second + + store.SetKey(key, value, ttl) + + resp := client.Get(client.Context(), key) + assert.NoError(t, resp.Err()) + assert.Equal(t, string(value), resp.Val()) + + respValue := store.Get(key) + assert.Equal(t, value, respValue) + + err := store.Delete(key) + assert.NoError(t, err) + + resp = client.Get(client.Context(), key) + assert.EqualError(t, redis.Nil, resp.Err().Error()) + + err = store.DeleteAll() + assert.NoError(t, err) + + + keys := client.Keys(client.Context(), "*") + assert.NoError(t, keys.Err()) + assert.Empty(t, keys.Val()) + + store.Close() + + + _, err = client.Ping(client.Context()).Result() + assert.EqualError(t, err, "redis: client is closed") +} diff --git a/cmd/api/main.go b/cmd/api/main.go index d454fc2..031e7b3 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -2,14 +2,20 @@ package main import ( "fmt" + redisStore "github.com/acikkaynak/musahit-harita-backend/cache" _ "github.com/acikkaynak/musahit-harita-backend/docs" "github.com/acikkaynak/musahit-harita-backend/handler" + "github.com/acikkaynak/musahit-harita-backend/middleware/cache" log "github.com/acikkaynak/musahit-harita-backend/pkg/logger" "github.com/acikkaynak/musahit-harita-backend/repository" "github.com/gofiber/adaptor/v2" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cache" + + "os" + "os/signal" + "syscall" + "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/monitor" "github.com/gofiber/fiber/v2/middleware/pprof" @@ -17,9 +23,6 @@ import ( "github.com/gofiber/swagger" jsoniter "github.com/json-iterator/go" "github.com/prometheus/client_golang/prometheus/promhttp" - "os" - "os/signal" - "syscall" ) type Application struct { @@ -28,6 +31,8 @@ type Application struct { } func (a *Application) RegisterApi() { + a.app.Get("/", handler.RedirectSwagger) + // monitor endpoint for pprof a.app.Get("/monitor", monitor.New()) @@ -37,6 +42,9 @@ func (a *Application) RegisterApi() { // metrics endpoint for prometheus a.app.Get("/metrics", adaptor.HTTPHandler(promhttp.Handler())) + // cache invalidate endpoint + a.app.Get("/caches/prune", handler.InvalidateCache()) + // swagger docs endpoint route := a.app.Group("/swagger") route.Get("*", swagger.HandlerDefault) @@ -64,7 +72,7 @@ func main() { pgStore := repository.New() // register redis to fiber app - cache := redisStore.NewRedisStore() + cacheRedis := redisStore.NewRedisStore() application := &Application{ app: app, @@ -93,5 +101,5 @@ func main() { // close database connection pgStore.Close() // close redis connection - cache.Close() + cacheRedis.Close() } diff --git a/go.mod b/go.mod index fc0908b..7675aaf 100644 --- a/go.mod +++ b/go.mod @@ -4,19 +4,23 @@ go 1.20 require ( github.com/Shopify/sarama v1.38.1 + github.com/alicebob/miniredis/v2 v2.30.2 github.com/go-redis/redis/v8 v8.11.5 github.com/gofiber/adaptor/v2 v2.2.1 github.com/gofiber/fiber/v2 v2.46.0 github.com/gofiber/swagger v0.1.11 + github.com/google/uuid v1.3.0 github.com/jackc/pgx/v4 v4.18.1 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_golang v1.15.1 + github.com/stretchr/testify v1.8.1 github.com/swaggo/swag v1.16.1 go.uber.org/zap v1.24.0 ) require ( github.com/KyleBanks/depth v1.2.1 // indirect + github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -31,7 +35,6 @@ require ( github.com/go-openapi/swag v0.22.3 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect @@ -59,6 +62,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect @@ -72,6 +76,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.47.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + github.com/yuin/gopher-lua v1.1.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/crypto v0.7.0 // indirect diff --git a/go.sum b/go.sum index 65baa4e..2f51086 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,10 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A= github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= github.com/Shopify/toxiproxy/v2 v2.5.0 h1:i4LPT+qrSlKNtQf5QliVjdP08GyAH8+BUIc9gT0eahc= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.30.2 h1:lc1UAUT9ZA7h4srlfBmBt2aorm5Yftk9nBjxz7EyY9I= +github.com/alicebob/miniredis/v2 v2.30.2/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= @@ -14,6 +18,9 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -270,6 +277,8 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= +github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -335,6 +344,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/handler/cache_invalidate_handler.go b/handler/cache_invalidate_handler.go new file mode 100644 index 0000000..66cac82 --- /dev/null +++ b/handler/cache_invalidate_handler.go @@ -0,0 +1,21 @@ +package handler + +import ( + "github.com/acikkaynak/musahit-harita-backend/cache" + "github.com/gofiber/fiber/v2" +) + +func InvalidateCache() fiber.Handler { + cacheRepo := cache.NewRedisStore() + + return func(ctx *fiber.Ctx) error { + err := cacheRepo.DeleteAll() + + if err != nil { + ctx.Status(fiber.StatusInternalServerError) + return ctx.SendString(err.Error()) + } + + return ctx.SendStatus(fiber.StatusOK) + } +} diff --git a/handler/swagger.go b/handler/swagger.go new file mode 100644 index 0000000..dc0f904 --- /dev/null +++ b/handler/swagger.go @@ -0,0 +1,11 @@ +package handler + +import ( + "net/http" + + "github.com/gofiber/fiber/v2" +) + +func RedirectSwagger(ctx *fiber.Ctx) error { + return ctx.Redirect("/swagger/index.html", http.StatusPermanentRedirect) +} diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go new file mode 100644 index 0000000..02ee04c --- /dev/null +++ b/middleware/cache/cache.go @@ -0,0 +1,44 @@ +package cache + +import ( + "fmt" + "net/http" + "time" + + "github.com/acikkaynak/musahit-harita-backend/cache" + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" +) + +func New() fiber.Handler { + cacheRepo := cache.NewRedisStore() + return func(c *fiber.Ctx) error { + if c.Path() == "/healthcheck" || c.Path() == "/metrics" || c.Path() == "/monitor" { + return c.Next() + } + reqURI := c.OriginalURL() + hashURL := uuid.NewSHA1(uuid.NameSpaceOID, []byte(reqURI)).String() + if c.Method() != http.MethodGet { + // Don't cache write endpoints. We can maintain of list to exclude certain http methods later. + // Since there will be an update in db, better to remove cache entries for this url + err := cacheRepo.Delete(hashURL) + if err != nil { + fmt.Println(err) + } + return c.Next() + } + cacheData := cacheRepo.Get(hashURL) + if cacheData == nil || len(cacheData) == 0 { + c.Next() + if c.Response().StatusCode() == fiber.StatusOK && len(c.Response().Body()) > 0 { + cacheRepo.SetKey(hashURL, c.Response().Body(), 5*time.Minute) + } + return nil + } + + c.Set("x-cached-response", "true") + c.Response().SetBodyRaw(cacheData) + c.Response().Header.SetContentType(fiber.MIMEApplicationJSON) + return nil + } +}