From ea6c90236aa3713a036280a24eebbeaf7757db45 Mon Sep 17 00:00:00 2001 From: Piotr Biernat Date: Fri, 23 Jul 2021 16:10:37 +0200 Subject: [PATCH] Renamed src/ to pkg/ Added cache with base redis support Added profiling in main.go --- main.go => pkg/cache/datastore.go | 19 +--- pkg/cache/redis_datastore.go | 57 ++++++++++ pkg/cache/response.go | 65 +++++++++++ pkg/cache/route.go | 63 +++++++++++ pkg/client/http.go | 10 ++ pkg/client/tcp.go | 10 ++ {config => pkg/config}/config.go | 30 ++++- pkg/handler/handler.go | 10 ++ pkg/main.go | 59 ++++++++++ pkg/server/server.go | 176 ++++++++++++++++++++++++++++++ pkg/server/struct.go | 10 ++ server/server.go | 163 --------------------------- 12 files changed, 492 insertions(+), 180 deletions(-) rename main.go => pkg/cache/datastore.go (67%) create mode 100644 pkg/cache/redis_datastore.go create mode 100644 pkg/cache/response.go create mode 100644 pkg/cache/route.go create mode 100644 pkg/client/http.go create mode 100644 pkg/client/tcp.go rename {config => pkg/config}/config.go (71%) create mode 100644 pkg/handler/handler.go create mode 100644 pkg/main.go create mode 100644 pkg/server/server.go create mode 100644 pkg/server/struct.go delete mode 100644 server/server.go diff --git a/main.go b/pkg/cache/datastore.go similarity index 67% rename from main.go rename to pkg/cache/datastore.go index 3033f9d..f61593d 100644 --- a/main.go +++ b/pkg/cache/datastore.go @@ -1,25 +1,16 @@ -package main - // ___ ____ ___ ___ // \ \ / / | _ | __| \ \ / / || | __ || || _ | // \ \/ / |___ | |__ \ \/ / || |___ || ||___| // \ / | _ | _ | \ / || __ | || ||\\ // \/ |___ |___ | \/ || ____| || || \\ - +// // Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License // Repo: https://git.pbiernat.dev/golang/vegvisir -import ( - "flag" - "vegvisir/server" -) +package cache -var ( - cFile = flag.String("c", "vegvisir.json", "Path to config file") -) +type CacheDatastore interface { + SetKey(string, interface{}, int) error -func main() { - flag.Parse() - - server.NewServer(*cFile).Run() + GetKey(string) (interface{}, error) } diff --git a/pkg/cache/redis_datastore.go b/pkg/cache/redis_datastore.go new file mode 100644 index 0000000..4fac007 --- /dev/null +++ b/pkg/cache/redis_datastore.go @@ -0,0 +1,57 @@ +// ___ ____ ___ ___ +// \ \ / / | _ | __| \ \ / / || | __ || || _ | +// \ \/ / |___ | |__ \ \/ / || |___ || ||___| +// \ / | _ | _ | \ / || __ | || ||\\ +// \/ |___ |___ | \/ || ____| || || \\ +// +// Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License +// Repo: https://git.pbiernat.dev/golang/vegvisir + +package cache + +import ( + "strconv" + "time" + + "github.com/go-redis/redis" +) + +func NewRedisDatastore(host string, port int) *RedisDatastore { + return &RedisDatastore{ + client: redis.NewClient(&redis.Options{ + Addr: host + ":" + strconv.Itoa(port), + Password: "", // FIXME: use env or param + DB: 0, // FIXME: use env or param + }), + cache: make(map[string]interface{}), + } +} + +type RedisDatastore struct { + client *redis.Client + cache map[string]interface{} +} + +func (ds *RedisDatastore) SetKey(key string, data interface{}, ttl int) error { + err := ds.client.Set(key, data, time.Duration(ttl)*time.Second).Err() + if err != nil { + return err + } + + // ds.cache[key] = data + + return nil +} + +func (ds *RedisDatastore) GetKey(key string) (interface{}, error) { + // if data, ok := ds.cache[key]; ok { + // return data, nil + // } + + data, err := ds.client.Get(key).Result() + if err != nil { + return nil, err + } + + return data, nil +} diff --git a/pkg/cache/response.go b/pkg/cache/response.go new file mode 100644 index 0000000..c213157 --- /dev/null +++ b/pkg/cache/response.go @@ -0,0 +1,65 @@ +// ___ ____ ___ ___ +// \ \ / / | _ | __| \ \ / / || | __ || || _ | +// \ \/ / |___ | |__ \ \/ / || |___ || ||___| +// \ / | _ | _ | \ / || __ | || ||\\ +// \/ |___ |___ | \/ || ____| || || \\ +// +// Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License +// Repo: https://git.pbiernat.dev/golang/vegvisir + +package cache + +import ( + "encoding/json" + "log" +) + +type ResponseCache struct { + URL string + Body string + // Headers map[string]string +} + +type ResponseCacheManager struct { + datastore CacheDatastore + prefix string + ttl int +} + +func NewResponseCacheManager(datastore CacheDatastore, ttl int) ResponseCacheManager { + return ResponseCacheManager{ + datastore: datastore, + prefix: "response_", + ttl: ttl, + } +} + +func (rm *ResponseCacheManager) Save(name string, r ResponseCache) { + data, err := json.Marshal(r) + if err != nil { + log.Println("JSON:", err) // FIXME + } + + name = rm.prefix + name + err = rm.datastore.SetKey(name, string(data), rm.ttl) + if err != nil { + log.Println("REDIS:", err, name) // FIXME + } +} + +func (rm *ResponseCacheManager) Load(name string) (bool, *ResponseCache) { + name = rm.prefix + name + + data, err := rm.datastore.GetKey(name) + if err != nil { + return false, &ResponseCache{} + } + + rc := &ResponseCache{} + err = json.Unmarshal([]byte(data.(string)), &rc) + if err != nil { + log.Println("JSON:", err) // FIXME + } + + return true, rc +} diff --git a/pkg/cache/route.go b/pkg/cache/route.go new file mode 100644 index 0000000..ff28a1c --- /dev/null +++ b/pkg/cache/route.go @@ -0,0 +1,63 @@ +// ___ ____ ___ ___ +// \ \ / / | _ | __| \ \ / / || | __ || || _ | +// \ \/ / |___ | |__ \ \/ / || |___ || ||___| +// \ / | _ | _ | \ / || __ | || ||\\ +// \/ |___ |___ | \/ || ____| || || \\ +// +// Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License +// Repo: https://git.pbiernat.dev/golang/vegvisir + +package cache + +import ( + "encoding/json" + "log" +) + +type RouteCache struct { + SourceUrl string + TargetUrl string +} + +type RouteCacheManager struct { + datastore CacheDatastore + ttl int +} + +func NewRouteCacheManager(datastore CacheDatastore, ttl int) RouteCacheManager { + return RouteCacheManager{ + datastore: datastore, + ttl: ttl, + } +} + +func (rm *RouteCacheManager) Save(name string, r RouteCache) { + data, err := json.Marshal(r) + if err != nil { + log.Println("JSON:", err) // FIXME + } + + err = rm.datastore.SetKey("route_"+name, data, rm.ttl) + if err != nil { + log.Println("REDIS:", err, name) // FIXME + } +} + +func (rm *RouteCacheManager) Load(name string) (bool, RouteCache) { + name = "route_" + name + + data, err := rm.datastore.GetKey(name) + if err != nil { + log.Println("REDIS:", err, name) // FIXME + + return false, RouteCache{} + } + + rc := RouteCache{} + err = json.Unmarshal([]byte(data.(string)), &rc) + if err != nil { + log.Println("JSON:", err) // FIXME + } + + return true, rc +} diff --git a/pkg/client/http.go b/pkg/client/http.go new file mode 100644 index 0000000..0510e9a --- /dev/null +++ b/pkg/client/http.go @@ -0,0 +1,10 @@ +// ___ ____ ___ ___ +// \ \ / / | _ | __| \ \ / / || | __ || || _ | +// \ \/ / |___ | |__ \ \/ / || |___ || ||___| +// \ / | _ | _ | \ / || __ | || ||\\ +// \/ |___ |___ | \/ || ____| || || \\ +// +// Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License +// Repo: https://git.pbiernat.dev/golang/vegvisir + +package client diff --git a/pkg/client/tcp.go b/pkg/client/tcp.go new file mode 100644 index 0000000..4bd534b --- /dev/null +++ b/pkg/client/tcp.go @@ -0,0 +1,10 @@ +// ___ ____ ___ ___ +// \ \ / / | _ | __| \ \ / / || | __ || || _ | +// \ \/ / |___ | |__ \ \/ / || |___ || ||___| +// \ / | _ | _ | \ / || __ | || ||\\ +// \/ |___ |___ | \/ || ____| || || \\ + +// Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License +// Repo: https://git.pbiernat.dev/golang/vegvisir + +package client diff --git a/config/config.go b/pkg/config/config.go similarity index 71% rename from config/config.go rename to pkg/config/config.go index a82e0c0..6fb1785 100644 --- a/config/config.go +++ b/pkg/config/config.go @@ -1,14 +1,20 @@ -package config - // ___ ____ ___ ___ // \ \ / / | _ | __| \ \ / / || | __ || || _ | // \ \/ / |___ | |__ \ \/ / || |___ || ||___| // \ / | _ | _ | \ / || __ | || ||\\ // \/ |___ |___ | \/ || ____| || || \\ - +// // Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License // Repo: https://git.pbiernat.dev/golang/vegvisir +package config + +import ( + "encoding/json" + "io/ioutil" + "os" +) + type Config struct { Server Server Backends map[string]Backend @@ -35,3 +41,21 @@ var DefaultRoute = Route{ Pattern: "(.*)", Target: "", } + +func (c *Config) Load(cPath string) error { + if _, err := os.Stat(cPath); err != nil { + return err + } + + data, err := ioutil.ReadFile(cPath) + if err != nil { + return err + } + + err = json.Unmarshal(data, &c) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go new file mode 100644 index 0000000..7e14d54 --- /dev/null +++ b/pkg/handler/handler.go @@ -0,0 +1,10 @@ +// ___ ____ ___ ___ +// \ \ / / | _ | __| \ \ / / || | __ || || _ | +// \ \/ / |___ | |__ \ \/ / || |___ || ||___| +// \ / | _ | _ | \ / || __ | || ||\\ +// \/ |___ |___ | \/ || ____| || || \\ + +// Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License +// Repo: https://git.pbiernat.dev/golang/vegvisir + +package handler diff --git a/pkg/main.go b/pkg/main.go new file mode 100644 index 0000000..27e15bf --- /dev/null +++ b/pkg/main.go @@ -0,0 +1,59 @@ +// ___ ____ ___ ___ +// \ \ / / | _ | __| \ \ / / || | __ || || _ | +// \ \/ / |___ | |__ \ \/ / || |___ || ||___| +// \ / | _ | _ | \ / || __ | || ||\\ +// \/ |___ |___ | \/ || ____| || || \\ +// +// Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License +// Repo: https://git.pbiernat.dev/golang/vegvisir + +package main + +import ( + "flag" + "log" + "os" + "runtime" + "runtime/pprof" + "vegvisir/pkg/server" +) + +var ( + cFile = flag.String("c", "vegvisir.json", "Path to config file") + + // for profiling... + cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") + memprofile = flag.String("memprofile", "", "write memory profile to `file`") +) + +func main() { + + flag.Parse() + // cpu profiling + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal("could not create CPU profile: ", err) + } + defer f.Close() // error handling omitted for example + if err := pprof.StartCPUProfile(f); err != nil { + log.Fatal("could not start CPU profile: ", err) + } + defer pprof.StopCPUProfile() + } + + server.NewServer(*cFile).Run() + + // memory profiling + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + log.Fatal("could not create memory profile: ", err) + } + defer f.Close() // error handling omitted for example + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + log.Fatal("could not write memory profile: ", err) + } + } +} diff --git a/pkg/server/server.go b/pkg/server/server.go new file mode 100644 index 0000000..ee9f769 --- /dev/null +++ b/pkg/server/server.go @@ -0,0 +1,176 @@ +// ___ ____ ___ ___ +// \ \ / / | _ | __| \ \ / / || | __ || || _ | +// \ \/ / |___ | |__ \ \/ / || |___ || ||___| +// \ / | _ | _ | \ / || __ | || ||\\ +// \/ |___ |___ | \/ || ____| || || \\ +// +// Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License +// Repo: https://git.pbiernat.dev/golang/vegvisir + +package server + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "regexp" + "strings" + "time" + "vegvisir/pkg/cache" + "vegvisir/pkg/config" + + "github.com/valyala/fasthttp" +) + +const ( + Version = "0.1" + Name = "Vegvisir/" + Version +) + +type Server struct { + Config config.Config + + cFilePath string + rCache map[string]cache.RouteCache // Internal route cache + routeCM cache.RouteCacheManager // Redis route cache + respCM cache.ResponseCacheManager // Redis response cache +} + +func NewServer(cPath string) *Server { + datastore := cache.NewRedisDatastore("127.0.0.7", 6379) // FIXME use config or env... + + return &Server{ + cFilePath: cPath, + rCache: make(map[string]cache.RouteCache), + routeCM: cache.NewRouteCacheManager(datastore, 30), //FIXME use ttl(seconds) from config or env... + respCM: cache.NewResponseCacheManager(datastore, 30), //FIXME use ttl(seconds) from config or env... + } +} + +func (s *Server) Run() { + if err := s.Config.Load(s.cFilePath); err != nil { + log.Fatalln("Unable to find config file: ", s.cFilePath, err) + } + + go func() { + serverAddress := s.Config.Server.Address + ":" + fmt.Sprint(s.Config.Server.Port) + if err := fasthttp.ListenAndServe(serverAddress, s.mainHandler); err != nil { + log.Fatalf("Server panic! Error message: %s", err) + } + }() + + log.Println("Server started") + + // Wait for an interrupt + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt, os.Kill) + <-interrupt + + log.Println("SIGKILL or SIGINT caught, shutting down...") + + // Attempt a graceful shutdown + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + s.Shutdown(ctx) + log.Println("Server shutdown successfully.") +} + +func (s *Server) Shutdown(ctx context.Context) { // TODO: wait for all connections to finish + log.Println("Shuting down finished") +} + +func (s *Server) mainHandler(ctx *fasthttp.RequestCtx) { + ctx.Response.Header.Add(fasthttp.HeaderServer, Name) + + // move all below logic to concrete handler or sth.... + reqUri, sReqUri, sReqMethod := ctx.RequestURI(), string(ctx.RequestURI()), string(ctx.Method()) + log.Println("Incomming request:", sReqMethod, sReqUri) + + found, route := s.findRouteByRequestURI(reqUri) + if !found { + // FIXME: return 404/5xx error in repsonse, maybe define it in Backend config? + ctx.SetStatusCode(fasthttp.StatusNotFound) + return + } + + // handle response caching + if ok, data := s.respCM.Load(sReqUri); ok { + log.Println("Read resp from cache: ", route.TargetUrl) + + ctx.SetBody([]byte(data.Body)) + } else { + log.Println("Send req to backend url: ", route.TargetUrl) + + // prepare to send request to backend - separate + bckReq := fasthttp.AcquireRequest() + bckResp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseRequest(bckReq) + defer fasthttp.ReleaseResponse(bckResp) + + // copy headers from backend response and prepare request for backend - separate + bckReq.SetRequestURI(route.TargetUrl) + bckReq.Header.SetMethod(sReqMethod) + + err := fasthttp.Do(bckReq, bckResp) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + return + } + + ctx.Response.Header.SetBytesV(fasthttp.HeaderContentType, bckResp.Header.ContentType()) + ctx.SetStatusCode(bckResp.StatusCode()) + ctx.SetBody(bckResp.Body()) + + // save response to cache + respCache := cache.ResponseCache{ + URL: sReqUri, + Body: string(bckResp.Body()), + // Headers: [] + } // FIXME: prepare resp cache struct in respCM.Save method or other service... + s.respCM.Save(sReqUri, respCache) + } +} + +func (s *Server) findRouteByRequestURI(uri []byte) (bool, cache.RouteCache) { + var sUri string = string(uri) + + for bId := range s.Config.Backends { + bck := s.Config.Backends[bId] + if !strings.Contains(sUri, bck.PrefixUrl) { + continue + } + + for rId := range bck.Routes { + route := &bck.Routes[rId] + + // if ok, cRoute := s.rCacheManager.Load(sUri); ok { + // return true, cRoute + // } + if cRoute, ok := s.rCache[sUri]; ok { + return true, cRoute + } + + rgxp := regexp.MustCompile(fmt.Sprintf("%s%s", bck.PrefixUrl, route.Pattern)) + if rgxp.Match(uri) { + targetUrl := bck.BackendAddress + rgxp.ReplaceAllString(sUri, route.Target) + + cRoute := cache.RouteCache{ // FIXME: data duplication and use short alias for backend and route! + // Backend: bck, + // Route: *route, + SourceUrl: sUri, + TargetUrl: targetUrl, + } + + // s.rCacheManager.Save(sUri, cRoute) + s.rCache[sUri] = cRoute + + return true, cRoute + } + } + } + + return false, cache.RouteCache{} +} diff --git a/pkg/server/struct.go b/pkg/server/struct.go new file mode 100644 index 0000000..8dbb0e4 --- /dev/null +++ b/pkg/server/struct.go @@ -0,0 +1,10 @@ +// ___ ____ ___ ___ +// \ \ / / | _ | __| \ \ / / || | __ || || _ | +// \ \/ / |___ | |__ \ \/ / || |___ || ||___| +// \ / | _ | _ | \ / || __ | || ||\\ +// \/ |___ |___ | \/ || ____| || || \\ +// +// Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License +// Repo: https://git.pbiernat.dev/golang/vegvisir + +package server diff --git a/server/server.go b/server/server.go deleted file mode 100644 index 33c9267..0000000 --- a/server/server.go +++ /dev/null @@ -1,163 +0,0 @@ -package server - -// ___ ____ ___ ___ -// \ \ / / | _ | __| \ \ / / || | __ || || _ | -// \ \/ / |___ | |__ \ \/ / || |___ || ||___| -// \ / | _ | _ | \ / || __ | || ||\\ -// \/ |___ |___ | \/ || ____| || || \\ - -// Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License -// Repo: https://git.pbiernat.dev/golang/vegvisir - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "log" - "os" - "os/signal" - "regexp" - "strings" - "time" - "vegvisir/config" - - "github.com/valyala/fasthttp" -) - -const ( - Version = "0.1" - Name = "Vegvisir/" + Version -) - -type Server struct { - Config config.Config - - cFilePath string - foundBackend *config.Backend - foundRoute *config.Route -} - -func NewServer(cPath string) *Server { - return &Server{ - cFilePath: cPath, - } -} - -func (s *Server) Run() { - if err := s.loadConfig(); err != nil { - log.Fatalln("Unable to find config file: ", s.cFilePath, err) - } - - go func() { - serverAddress := s.Config.Server.Address + ":" + fmt.Sprint(s.Config.Server.Port) - if err := fasthttp.ListenAndServe(serverAddress, s.mainHandler); err != nil { - log.Fatalf("Server panic! Error message: %s", err) - } - }() - - log.Println("Server started") - - // Wait for an interrupt - interrupt := make(chan os.Signal, 1) - signal.Notify(interrupt, os.Interrupt, os.Kill) - <-interrupt - - log.Println("SIGKILL or SIGINT caught, shutting down...") - - // Attempt a graceful shutdown - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - s.Shutdown(ctx) - log.Println("Server shutdown successfully.") -} - -func (s *Server) Shutdown(ctx context.Context) { // TODO: wait for all connections to finish - log.Println("Shuting down finished") -} - -func (s *Server) loadConfig() error { - if _, err := os.Stat(s.cFilePath); err != nil { - return err - } - - data, err := ioutil.ReadFile(s.cFilePath) - if err != nil { - return err - } - - err = json.Unmarshal(data, &s.Config) - if err != nil { - return err - } - - return nil -} - -func (s *Server) mainHandler(ctx *fasthttp.RequestCtx) { - ctx.Response.Header.Add(fasthttp.HeaderServer, Name) - - // move all below logic to concrete handler or sth....? - reqURI, sReqURI, sReqMethod := ctx.RequestURI(), string(ctx.RequestURI()), string(ctx.Method()) - log.Println("Incomming request:", sReqMethod, sReqURI) - - found, rgxp := s.findBackendAndRouteByRequestURI(reqURI) - if !found { - // FIXME: return 404/5xx error in repsonse, maybe define it in Backend config? - ctx.Response.SetStatusCode(fasthttp.StatusNotFound) - return - } - - bckReqURI := s.buildBackendTargetURI(*rgxp, sReqURI) - - // prepare to send request to backend - separate - bckReq := fasthttp.AcquireRequest() - bckResp := fasthttp.AcquireResponse() - defer fasthttp.ReleaseRequest(bckReq) - defer fasthttp.ReleaseResponse(bckResp) - - // copy headers from backend response and prepare request for backend - separate - bckReq.SetRequestURI(bckReqURI) - bckReq.Header.SetMethod(sReqMethod) - - fasthttp.Do(bckReq, bckResp) - - ctx.Response.Header.SetBytesV(fasthttp.HeaderContentType, bckResp.Header.ContentType()) - ctx.SetBody(bckResp.Body()) -} - -func (s *Server) findBackendAndRouteByRequestURI(uri []byte) (bool, *regexp.Regexp) { - for _, bck := range s.Config.Backends { - if !strings.Contains(string(uri), bck.PrefixUrl) { - continue - } - s.foundBackend = &bck // possibly errorneus... - - for _, r := range bck.Routes { - rgxp := regexp.MustCompile(fmt.Sprintf("%s%s", bck.PrefixUrl, r.Pattern)) - // fmt.Println("pattern: ", bck.PrefixUrl+r.Pattern, rgxp.Match(uri)) - if rgxp.Match(uri) { - s.foundBackend = &bck // 100% safe here - s.foundRoute = &r - - return true, rgxp - } - } - } - - return false, nil -} - -func (s *Server) buildBackendTargetURI(rgxp regexp.Regexp, uri string) string { - baseURI := s.foundBackend.BackendAddress - url := rgxp.ReplaceAllString(uri, s.foundRoute.Target) - - // if !strings.HasSuffix(baseURI, "/") { - // baseURI += "/" - // } - - log.Println("Send req to backend url: ", baseURI+url) - - return baseURI + url -}