From 30dced6315fb38014b65b3a15e3b20f5dea16cef Mon Sep 17 00:00:00 2001 From: mikestefanello Date: Tue, 7 Dec 2021 21:36:57 -0500 Subject: [PATCH] Added page cache middleware. --- controllers/controller.go | 27 +++++++++++++++++++++------ controllers/page.go | 8 +++++--- controllers/router.go | 19 +++++++++++-------- go.mod | 2 ++ go.sum | 2 ++ middleware/cache.go | 39 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 80 insertions(+), 17 deletions(-) diff --git a/controllers/controller.go b/controllers/controller.go index ae330c9..4dcd068 100644 --- a/controllers/controller.go +++ b/controllers/controller.go @@ -13,6 +13,9 @@ import ( "goweb/config" "goweb/container" "goweb/funcmap" + "goweb/middleware" + + "github.com/eko/gocache/v2/marshaler" "github.com/eko/gocache/v2/store" @@ -48,7 +51,7 @@ func NewController(c *container.Container) Controller { func (t *Controller) RenderPage(c echo.Context, p Page) error { if p.Name == "" { - c.Logger().Error("Page render failed due to missing name") + c.Logger().Error("page render failed due to missing name") return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error") } @@ -65,12 +68,17 @@ func (t *Controller) RenderPage(c echo.Context, p Page) error { return err } - t.cachePage(c, p) + t.cachePage(c, p, buf) + + // Set any headers + for k, v := range p.Headers { + c.Response().Header().Set(k, v) + } return c.HTMLBlob(p.StatusCode, buf.Bytes()) } -func (t *Controller) cachePage(c echo.Context, p Page) { +func (t *Controller) cachePage(c echo.Context, p Page, html *bytes.Buffer) { if !p.Cache.Enabled { return } @@ -84,12 +92,19 @@ func (t *Controller) cachePage(c echo.Context, p Page) { Expiration: p.Cache.MaxAge, Tags: p.Cache.Tags, } - err := t.Container.Cache.Set(c.Request().Context(), key, "my-value", opts) + cp := middleware.CachedPage{ + HTML: html.Bytes(), + Headers: p.Headers, + StatusCode: p.StatusCode, + } + err := marshaler.New(t.Container.Cache).Set(c.Request().Context(), key, cp, opts) if err != nil { - c.Logger().Errorf("Failed to cache page: %s", key) + c.Logger().Errorf("failed to cache page: %s", key) c.Logger().Error(err) return } + + c.Logger().Infof("cached page for: %s", key) } func (t *Controller) parsePageTemplates(p Page) error { @@ -124,7 +139,7 @@ func (t *Controller) parsePageTemplates(p Page) error { func (t *Controller) executeTemplates(c echo.Context, p Page) (*bytes.Buffer, error) { tmpl, ok := templates.Load(p.Name) if !ok { - c.Logger().Error("Uncached page template requested") + c.Logger().Error("uncached page template requested") return nil, echo.NewHTTPError(http.StatusInternalServerError, "Internal server error") } diff --git a/controllers/page.go b/controllers/page.go index 0c0fea5..d87f2f7 100644 --- a/controllers/page.go +++ b/controllers/page.go @@ -33,9 +33,10 @@ type Page struct { Description string Keywords []string } - Pager pager.Pager - CSRF string - Cache struct { + Pager pager.Pager + CSRF string + Headers map[string]string + Cache struct { Enabled bool MaxAge time.Duration Tags []string @@ -49,6 +50,7 @@ func NewPage(c echo.Context) Page { Path: c.Request().URL.Path, StatusCode: http.StatusOK, Pager: pager.NewPager(c, DefaultItemsPerPage), + Headers: make(map[string]string), } p.IsHome = p.Path == "/" diff --git a/controllers/router.go b/controllers/router.go index e943f81..1f3a052 100644 --- a/controllers/router.go +++ b/controllers/router.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" + "github.com/labstack/echo/v4" echomw "github.com/labstack/echo/v4/middleware" @@ -16,6 +17,12 @@ import ( const StaticDir = "static" func BuildRouter(c *container.Container) { + // Static files with proper cache control + // funcmap.File() should be used in templates to append a cache key to the URL in order to break cache + // after each server restart + c.Web.Group("", middleware.CacheControl(c.Config.Cache.MaxAge.StaticFile)). + Static("/", StaticDir) + // Middleware c.Web.Use(echomw.RemoveTrailingSlashWithConfig(echomw.TrailingSlashConfig{ RedirectCode: http.StatusMovedPermanently, @@ -24,18 +31,14 @@ func BuildRouter(c *container.Container) { c.Web.Use(echomw.Recover()) c.Web.Use(echomw.Gzip()) c.Web.Use(echomw.Logger()) + c.Web.Use(echomw.TimeoutWithConfig(echomw.TimeoutConfig{ + Timeout: c.Config.App.Timeout, + })) + c.Web.Use(middleware.PageCache(c.Cache)) c.Web.Use(session.Middleware(sessions.NewCookieStore([]byte(c.Config.App.EncryptionKey)))) c.Web.Use(echomw.CSRFWithConfig(echomw.CSRFConfig{ TokenLookup: "form:csrf", })) - c.Web.Use(echomw.TimeoutWithConfig(echomw.TimeoutConfig{ - Timeout: c.Config.App.Timeout, - })) - // Static files with proper cache control - // funcmap.File() should be used in templates to append a cache key to the URL in order to break cache - // after each server restart - c.Web.Group("", middleware.CacheControl(c.Config.Cache.MaxAge.StaticFile)). - Static("/", StaticDir) // Base controller ctr := NewController(c) diff --git a/go.mod b/go.mod index 84cbcb4..37fc227 100644 --- a/go.mod +++ b/go.mod @@ -49,11 +49,13 @@ require ( github.com/spf13/cast v1.3.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect + github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 // indirect golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect + google.golang.org/appengine v1.4.0 // indirect google.golang.org/protobuf v1.26.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect diff --git a/go.sum b/go.sum index 9f3f9b7..87b1cdb 100644 --- a/go.sum +++ b/go.sum @@ -434,6 +434,7 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -613,6 +614,7 @@ gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZ google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/middleware/cache.go b/middleware/cache.go index 34044d3..e2a16d3 100644 --- a/middleware/cache.go +++ b/middleware/cache.go @@ -4,9 +4,48 @@ import ( "fmt" "time" + "github.com/eko/gocache/v2/cache" + "github.com/eko/gocache/v2/marshaler" + "github.com/go-redis/redis/v8" "github.com/labstack/echo/v4" ) +type CachedPage struct { + URL string + HTML []byte + StatusCode int + Headers map[string]string +} + +func PageCache(ch *cache.Cache) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + key := c.Request().URL.String() + res, err := marshaler.New(ch).Get(c.Request().Context(), key, new(CachedPage)) + if err != nil { + if err == redis.Nil { + c.Logger().Infof("no cached page for: %s", key) + } else { + c.Logger().Errorf("failed getting cached page: %s", key) + c.Logger().Error(err) + } + return next(c) + } + + page := res.(*CachedPage) + + if page.Headers != nil { + for k, v := range page.Headers { + c.Response().Header().Set(k, v) + } + } + c.Logger().Infof("serving cached page for: %s", key) + + return c.HTMLBlob(page.StatusCode, page.HTML) + } + } +} + func CacheControl(maxAge time.Duration) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error {