From a8a948adf1413bca1281780c2002b2f2e8a0ecbe Mon Sep 17 00:00:00 2001 From: Joscha Henningsen <44805696+joschahenningsen@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:47:45 +0100 Subject: [PATCH] implement simple cache for news (#459) * implement simple cache for news * improve cache key method name * use lru for cache * cleanup * remove previous cache --- server/backend/news.go | 78 ++++++++++++++++++++++++++++-------------- server/go.mod | 1 + server/go.sum | 2 ++ 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/server/backend/news.go b/server/backend/news.go index 158a14a3..c83e3434 100644 --- a/server/backend/news.go +++ b/server/backend/news.go @@ -4,28 +4,28 @@ import ( "context" "errors" "fmt" - - "gorm.io/gorm" - - "google.golang.org/protobuf/types/known/timestamppb" + "time" pb "github.com/TUM-Dev/Campus-Backend/server/api/tumdev" "github.com/TUM-Dev/Campus-Backend/server/model" + "github.com/hashicorp/golang-lru/v2/expirable" log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + "gorm.io/gorm" ) +var newsSourceCache = expirable.NewLRU[string, []model.NewsSource](1, nil, time.Hour*6) +var newsCache = expirable.NewLRU[string, []model.News](1024, nil, time.Minute*30) + func (s *CampusServer) ListNewsSources(ctx context.Context, _ *pb.ListNewsSourcesRequest) (*pb.ListNewsSourcesReply, error) { if err := s.checkDevice(ctx); err != nil { return nil, err } - var sources []model.NewsSource - if err := s.db.WithContext(ctx). - Joins("File"). - Find(&sources).Error; err != nil { - log.WithError(err).Error("could not find news_sources") + sources, err := s.getNewsSources(ctx) + if err != nil { return nil, status.Error(codes.Internal, "could not ListNewsSources") } @@ -40,27 +40,27 @@ func (s *CampusServer) ListNewsSources(ctx context.Context, _ *pb.ListNewsSource return &pb.ListNewsSourcesReply{Sources: resp}, nil } +const CacheKeyAllNewsSources = "all_news_sources" + +func (s *CampusServer) getNewsSources(ctx context.Context) ([]model.NewsSource, error) { + if newsSources, ok := newsSourceCache.Get(CacheKeyAllNewsSources); ok { + return newsSources, nil + } + var sources []model.NewsSource + if err := s.db.WithContext(ctx).Joins("File").Find(&sources).Error; err != nil { + return nil, err + } + newsSourceCache.Add(CacheKeyAllNewsSources, sources) + return sources, nil +} + func (s *CampusServer) ListNews(ctx context.Context, req *pb.ListNewsRequest) (*pb.ListNewsReply, error) { if err := s.checkDevice(ctx); err != nil { return nil, err } - var newsEntries []model.News - tx := s.db.WithContext(ctx). - Joins("File"). - Joins("NewsSource"). - Joins("NewsSource.File") - if req.NewsSource != 0 { - tx = tx.Where("src = ?", req.NewsSource) - } - if req.OldestDateAt.GetSeconds() != 0 || req.OldestDateAt.GetNanos() != 0 { - tx = tx.Where("date > ?", req.OldestDateAt.AsTime()) - } - if req.LastNewsId != 0 { - tx = tx.Where("news > ?", req.LastNewsId) - } - if err := tx.Find(&newsEntries).Error; err != nil { - log.WithError(err).Error("could not find news item") + var newsEntries, err = s.getNews(ctx, req.NewsSource, req.LastNewsId, req.OldestDateAt.AsTime()) + if err != nil { return nil, status.Error(codes.Internal, "could not ListNews") } @@ -86,6 +86,34 @@ func (s *CampusServer) ListNews(ctx context.Context, req *pb.ListNewsRequest) (* return &pb.ListNewsReply{News: resp}, nil } +func (s *CampusServer) getNews(ctx context.Context, sourceID int32, lastNewsID int32, oldestDateAt time.Time) ([]model.News, error) { + cacheKey := fmt.Sprintf("%d_%d_%d", sourceID, oldestDateAt.Second(), lastNewsID) + + if news, ok := newsCache.Get(cacheKey); ok { + return news, nil + } + + var news []model.News + tx := s.db.WithContext(ctx). + Joins("File"). + Joins("NewsSource"). + Joins("NewsSource.File") + if sourceID != 0 { + tx = tx.Where("src = ?", sourceID) + } + if oldestDateAt.Second() != 0 || oldestDateAt.Nanosecond() != 0 { + tx = tx.Where("date > ?", oldestDateAt) + } + if lastNewsID != 0 { + tx = tx.Where("news > ?", lastNewsID) + } + if err := tx.Find(&news).Error; err != nil { + return nil, err + } + newsCache.Add(cacheKey, news) + return news, nil +} + func (s *CampusServer) ListNewsAlerts(ctx context.Context, req *pb.ListNewsAlertsRequest) (*pb.ListNewsAlertsReply, error) { if err := s.checkDevice(ctx); err != nil { return nil, err diff --git a/server/go.mod b/server/go.mod index ed963c3f..820152a1 100644 --- a/server/go.mod +++ b/server/go.mod @@ -58,6 +58,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/server/go.sum b/server/go.sum index 7b2b5859..9266c693 100644 --- a/server/go.sum +++ b/server/go.sum @@ -157,6 +157,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjw github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH7dPn4/4Gw= github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=