├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── cmd └── sidecache │ └── main.go ├── go.mod ├── go.sum └── pkg ├── cache ├── couchbase.go ├── mock_repository.go ├── redis.go └── repository.go ├── server ├── prometheus_exporter.go ├── server.go └── server_test.go └── tests └── server_bench_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ 26 | 27 | .DS_Store -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.trendyol.com/platform/base/image/golang:1.13.4-alpine3.10 AS builder 2 | 3 | ENV GOPATH /go 4 | ENV CGO_ENABLED=0 5 | ENV GOOS=linux 6 | ENV GOARCH=amd64 7 | ARG VERSION 8 | 9 | RUN mkdir /app 10 | WORKDIR /app 11 | 12 | COPY . . 13 | RUN go mod download 14 | RUN go build -ldflags="-X 'main.version=$VERSION'" -v cmd/sidecache/main.go 15 | 16 | FROM registry.trendyol.com/platform/base/image/alpine:3.10.1 AS alpine 17 | 18 | ENV LANG C.UTF-8 19 | ENV MAIN_CONTAINER_PORT "" 20 | ENV COUCHBASE_HOST "" 21 | ENV COUCHBASE_USERNAME "" 22 | ENV COUCHBASE_PASSWORD "" 23 | ENV BUCKET_NAME "" 24 | 25 | RUN apk --no-cache add tzdata ca-certificates 26 | COPY --from=builder /app/main /app/main 27 | 28 | WORKDIR /app 29 | 30 | RUN chmod +x main 31 | 32 | EXPOSE 9191 33 | 34 | ENTRYPOINT ["./main","app"] 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Trendyol Open Source 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sidecache 2 | Sidecar cache for kubernetes applications. It acts as a proxy sidecar between application and client, routes incoming requests to cache storage or application according to Istio VirtualService routing rules. 3 | 4 | Medium article: https://medium.com/trendyol-tech/trendyol-platform-team-caching-service-to-service-communications-on-kubernetes-istio-82327589b935 5 | 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-ligthgreen.svg)](https://opensource.org/licenses/MIT) 7 | 8 | ## Table Of Contents 9 | 10 | - [Istio Configuration](#istio-configuration-for-routing-http-requests-to-sidecar-container) 11 | - [Environment Variables](#environment-variables) 12 | 13 | ## Istio Configuration for Routing Http Requests to Sidecar Container 14 | 15 | Below VirtualService is responsible for routing all get requests to port 9191 on your pod, other http requests goes to port 8080. 16 | 17 | ``` 18 | apiVersion: networking.istio.io/v1beta1 19 | kind: VirtualService 20 | metadata: 21 | name: foo 22 | spec: 23 | gateways: 24 | - foo-gateway 25 | hosts: 26 | - foo 27 | http: 28 | - match: 29 | - method: 30 | exact: GET 31 | route: 32 | - destination: 33 | host: foo 34 | port: 35 | number: 9191 36 | - route: 37 | - destination: 38 | host: foo 39 | port: 40 | number: 8080 41 | ``` 42 | 43 | 44 | ## Environment Variables 45 | 46 | Environment variables for sidecar container. 47 | 48 | - **MAIN_CONTAINER_PORT**: The port of main application to proxy. 49 | - **COUCHBASE_HOST**: Couchbase host addr. 50 | - **COUCHBASE_USERNAME**: Couchbase username. 51 | - **COUCHBASE_PASSWORD**: Couchbase password. 52 | - **BUCKET_NAME**: Couchbase cache bucket name. 53 | - **CACHE_KEY_PREFIX**: Cache key prefix to prevent url conflicts between different applications. 54 | - **SIDE_CACHE_PORT**: Sidecar container port to listen. 55 | -------------------------------------------------------------------------------- /cmd/sidecache/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/Trendyol/sidecache/pkg/cache" 5 | "github.com/Trendyol/sidecache/pkg/server" 6 | "go.uber.org/zap" 7 | "net/http/httputil" 8 | "net/url" 9 | "os" 10 | ) 11 | 12 | var version string 13 | 14 | func main() { 15 | logger, _ := zap.NewProduction() 16 | logger.Info("Side cache process started...", zap.String("version", version)) 17 | 18 | defer logger.Sync() 19 | couchbaseRepo := cache.NewCouchbaseRepository(logger) 20 | 21 | mainContainerPort := os.Getenv("MAIN_CONTAINER_PORT") 22 | logger.Info("Main container port", zap.String("port", mainContainerPort)) 23 | mainContainerURL, err := url.Parse("http://127.0.0.1:" + mainContainerPort) 24 | if err != nil { 25 | logger.Error("Error occurred on url.Parse", zap.Error(err)) 26 | } 27 | 28 | prom := server.NewPrometheusClient() 29 | 30 | server.BuildInfo(version) 31 | 32 | proxy := httputil.NewSingleHostReverseProxy(mainContainerURL) 33 | 34 | cacheServer := server.NewServer(couchbaseRepo, proxy, prom, logger) 35 | logger.Info("Cache key prefix", zap.String("prefix", cacheServer.CacheKeyPrefix)) 36 | 37 | if couchbaseRepo == nil { 38 | go func() { 39 | for { 40 | logger.Warn("Couchbase repo is nil, retrying connection...") 41 | if newRepo := cache.NewCouchbaseRepository(logger); newRepo != nil { 42 | cacheServer.Repo = newRepo 43 | break 44 | } 45 | } 46 | logger.Info("Couchbase repo recreated successfully.") 47 | }() 48 | } 49 | 50 | stopChan := make(chan int) 51 | cacheServer.Start(stopChan) 52 | } 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Trendyol/sidecache 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-redis/redis v6.15.7+incompatible 7 | github.com/golang/mock v1.4.3 8 | github.com/golang/snappy v0.0.1 // indirect 9 | github.com/google/uuid v1.1.1 // indirect 10 | github.com/onsi/ginkgo v1.12.0 11 | github.com/onsi/gomega v1.9.0 12 | github.com/opentracing/opentracing-go v1.1.0 // indirect 13 | github.com/prometheus/client_golang v1.5.1 14 | go.uber.org/zap v1.14.1 15 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect 16 | gopkg.in/couchbase/gocb.v1 v1.6.6 17 | gopkg.in/couchbase/gocbcore.v7 v7.1.17 // indirect 18 | gopkg.in/couchbaselabs/gocbconnstr.v1 v1.0.4 // indirect 19 | gopkg.in/couchbaselabs/jsonx.v1 v1.0.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 4 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 7 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 8 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 9 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 10 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 11 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 15 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 16 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 17 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 18 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 19 | github.com/go-redis/redis v6.15.7+incompatible h1:3skhDh95XQMpnqeqNftPkQD9jL9e5e36z/1SUm6dy1U= 20 | github.com/go-redis/redis v6.15.7+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 21 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 22 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 23 | github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= 24 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 25 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 28 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 30 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 31 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 32 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 33 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 34 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 35 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 36 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 37 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 38 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 39 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 40 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 41 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 42 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 43 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 44 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 45 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 46 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 47 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 48 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 49 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 50 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 52 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 53 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 54 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 55 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 56 | github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= 57 | github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= 58 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 59 | github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= 60 | github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= 61 | github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= 62 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 63 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 64 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 65 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 66 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 67 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 68 | github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= 69 | github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 70 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 71 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 72 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 73 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 74 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 75 | github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= 76 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 77 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 78 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 79 | github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= 80 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 81 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 82 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 83 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 84 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 85 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 86 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 87 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 88 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 89 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 90 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 91 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 92 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 93 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 94 | go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo= 95 | go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 96 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 97 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 98 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 99 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 100 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 101 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 102 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 103 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 104 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 105 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 106 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 107 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= 108 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 109 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 111 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 112 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 113 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 115 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 116 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 117 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 118 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= 123 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 125 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 126 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 127 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 128 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 129 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 130 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 131 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 132 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 133 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 135 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 136 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 137 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 138 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 139 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 140 | gopkg.in/couchbase/gocb.v1 v1.6.6 h1:rMliGFADaFey8FEUQokDlLHd9YZCPc6lC+a9znCSTpQ= 141 | gopkg.in/couchbase/gocb.v1 v1.6.6/go.mod h1:Ri5Qok4ZKiwmPr75YxZ0uELQy45XJgUSzeUnK806gTY= 142 | gopkg.in/couchbase/gocbcore.v7 v7.1.17 h1:BWSGSO8yPd1n9enURqvXU/iT1F1ObqbmYVNaUkBxbFg= 143 | gopkg.in/couchbase/gocbcore.v7 v7.1.17/go.mod h1:48d2Be0MxRtsyuvn+mWzqmoGUG9uA00ghopzOs148/E= 144 | gopkg.in/couchbaselabs/gocbconnstr.v1 v1.0.4 h1:VVVoIV/nSw1w9ZnTEOjmkeJVcAzaCyxEujKglarxz7U= 145 | gopkg.in/couchbaselabs/gocbconnstr.v1 v1.0.4/go.mod h1:ZjII0iKx4Veo6N6da+pEZu/ptNyKLg9QTVt7fFmR6sw= 146 | gopkg.in/couchbaselabs/jsonx.v1 v1.0.0 h1:SJGarb8dXAsVZWizC26rxBkBYEKhSUxVh5wAnyzBVaI= 147 | gopkg.in/couchbaselabs/jsonx.v1 v1.0.0/go.mod h1:oR201IRovxvLW/eISevH12/+MiKHtNQAKfcX8iWZvJY= 148 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 149 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 150 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 151 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 152 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 153 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 154 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 155 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 156 | gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= 157 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 158 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 159 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 160 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 161 | -------------------------------------------------------------------------------- /pkg/cache/couchbase.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "go.uber.org/zap" 8 | 9 | "gopkg.in/couchbase/gocb.v1" 10 | ) 11 | 12 | type CouchbaseRepository struct { 13 | bucket *gocb.Bucket 14 | logger *zap.Logger 15 | } 16 | 17 | func NewCouchbaseRepository(logger *zap.Logger) *CouchbaseRepository { 18 | couchbaseHost := os.Getenv("COUCHBASE_HOST") 19 | cluster, err := gocb.Connect("couchbase://" + couchbaseHost) 20 | if err != nil { 21 | logger.Error("Couchbase connection error:", zap.Error(err)) 22 | return nil 23 | } 24 | 25 | err = cluster.Authenticate(gocb.PasswordAuthenticator{ 26 | Username: os.Getenv("COUCHBASE_USERNAME"), 27 | Password: os.Getenv("COUCHBASE_PASSWORD"), 28 | }) 29 | 30 | if err != nil { 31 | logger.Error("Couchbase authentication error:", zap.Error(err)) 32 | return nil 33 | } 34 | 35 | cacheBucket, err := cluster.OpenBucket(os.Getenv("BUCKET_NAME"), "") 36 | if err != nil { 37 | logger.Error("Couchbase could not open bucket error:", zap.Error(err)) 38 | return nil 39 | } 40 | cacheBucket.SetOperationTimeout(100 * time.Millisecond) 41 | 42 | return &CouchbaseRepository{bucket: cacheBucket, logger: logger} 43 | } 44 | 45 | func (repository *CouchbaseRepository) SetKey(key string, value []byte, ttl int) { 46 | _, err := repository.bucket.Upsert(key, value, uint32(ttl)) 47 | if err != nil { 48 | repository.logger.Warn("Error occurred when Upsert", zap.String("key", key)) 49 | } 50 | } 51 | 52 | func (repository *CouchbaseRepository) Get(key string) []byte { 53 | var data []byte 54 | _, err := repository.bucket.Get(key, &data) 55 | 56 | if err != nil && err.Error() != "key not found" { 57 | repository.logger.Warn("Error occurred when Get", zap.String("key", key), zap.Error(err)) 58 | } 59 | 60 | return data 61 | } 62 | -------------------------------------------------------------------------------- /pkg/cache/mock_repository.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ../cache/repository.go 3 | 4 | // Package cache is a generated GoMock package. 5 | package cache 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockCacheRepository is a mock of CacheRepository interface 14 | type MockCacheRepository struct { 15 | ctrl *gomock.Controller 16 | recorder *MockCacheRepositoryMockRecorder 17 | } 18 | 19 | // MockCacheRepositoryMockRecorder is the mock recorder for MockCacheRepository 20 | type MockCacheRepositoryMockRecorder struct { 21 | mock *MockCacheRepository 22 | } 23 | 24 | // NewMockCacheRepository creates a new mock instance 25 | func NewMockCacheRepository(ctrl *gomock.Controller) *MockCacheRepository { 26 | mock := &MockCacheRepository{ctrl: ctrl} 27 | mock.recorder = &MockCacheRepositoryMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockCacheRepository) EXPECT() *MockCacheRepositoryMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // SetKey mocks base method 37 | func (m *MockCacheRepository) SetKey(key string, value []byte, ttl int) { 38 | m.ctrl.T.Helper() 39 | m.ctrl.Call(m, "SetKey", key, value, ttl) 40 | } 41 | 42 | // SetKey indicates an expected call of SetKey 43 | func (mr *MockCacheRepositoryMockRecorder) SetKey(key, value, ttl interface{}) *gomock.Call { 44 | mr.mock.ctrl.T.Helper() 45 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetKey", reflect.TypeOf((*MockCacheRepository)(nil).SetKey), key, value, ttl) 46 | } 47 | 48 | // Get mocks base method 49 | func (m *MockCacheRepository) Get(key string) []byte { 50 | m.ctrl.T.Helper() 51 | ret := m.ctrl.Call(m, "Get", key) 52 | ret0, _ := ret[0].([]byte) 53 | return ret0 54 | } 55 | 56 | // Get indicates an expected call of Get 57 | func (mr *MockCacheRepositoryMockRecorder) Get(key interface{}) *gomock.Call { 58 | mr.mock.ctrl.T.Helper() 59 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCacheRepository)(nil).Get), key) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/go-redis/redis" 11 | ) 12 | 13 | type RedisRepository struct { 14 | client *redis.Client 15 | } 16 | 17 | func NewRedisRepository() *RedisRepository { 18 | client := redis.NewClient(&redis.Options{ 19 | Addr: os.Getenv("redisAddr"), 20 | Password: os.Getenv("redisPassword"), 21 | DB: 0, 22 | }) 23 | 24 | return &RedisRepository{client: client} 25 | } 26 | 27 | func (repository *RedisRepository) SetKey(key string, value interface{}, ttl int) { 28 | byteData, err := json.Marshal(value) 29 | 30 | if err != nil { 31 | fmt.Println(err) 32 | return 33 | } 34 | 35 | duration, _ := time.ParseDuration(strconv.FormatInt(int64(ttl), 10)) 36 | status := repository.client.Set(key, string(byteData), duration) 37 | _, err = status.Result() 38 | if err != nil { 39 | fmt.Println(err) 40 | } 41 | } 42 | 43 | func (repository *RedisRepository) Get(key string) []byte { 44 | status := repository.client.Get(key) 45 | stringResult, err := status.Result() 46 | if err != nil { 47 | fmt.Println(err) 48 | } 49 | 50 | return []byte(stringResult) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/cache/repository.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | type CacheRepository interface { 4 | SetKey(key string, value []byte, ttl int) 5 | Get(key string) []byte 6 | } 7 | -------------------------------------------------------------------------------- /pkg/server/prometheus_exporter.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | gauge = prometheus.NewGauge( 10 | prometheus.GaugeOpts{ 11 | Namespace: "sidecache", 12 | Name: "cache_hit", 13 | Help: "This is cache hit metric", 14 | }) 15 | 16 | cacheHitCounter = prometheus.NewCounter( 17 | prometheus.CounterOpts{ 18 | Namespace: "sidecache", 19 | Name: "cache_hit_counter", 20 | Help: "Cache hit count", 21 | }) 22 | 23 | totalRequestCounter = prometheus.NewCounter( 24 | prometheus.CounterOpts{ 25 | Namespace: "sidecache", 26 | Name: "all_request_hit_counter", 27 | Help: "All request hit counter", 28 | }) 29 | 30 | buildInfoGaugeVec = prometheus.NewGaugeVec( 31 | prometheus.GaugeOpts{ 32 | Name: "sidecache_admission_build_info", 33 | Help: "Build info for sidecache admission webhook", 34 | }, []string{"version"}) 35 | ) 36 | 37 | 38 | type Prometheus struct { 39 | CacheHitCounter prometheus.Counter 40 | TotalRequestCounter prometheus.Counter 41 | } 42 | 43 | func NewPrometheusClient() *Prometheus { 44 | prometheus.MustRegister(cacheHitCounter, totalRequestCounter, buildInfoGaugeVec) 45 | 46 | return &Prometheus{TotalRequestCounter: totalRequestCounter, CacheHitCounter: cacheHitCounter} 47 | } 48 | 49 | func BuildInfo(admission string) { 50 | isNotEmptyAdmissionVersion := len(strings.TrimSpace(admission)) > 0 51 | 52 | if isNotEmptyAdmissionVersion { 53 | buildInfoGaugeVec.WithLabelValues(admission) 54 | } 55 | } 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "crypto/md5" 8 | "encoding/hex" 9 | "encoding/json" 10 | "errors" 11 | "io" 12 | "io/ioutil" 13 | "net/http" 14 | "net/http/httputil" 15 | "net/url" 16 | "os" 17 | "strconv" 18 | "strings" 19 | 20 | "github.com/Trendyol/sidecache/pkg/cache" 21 | "github.com/prometheus/client_golang/prometheus/promhttp" 22 | "go.uber.org/zap" 23 | ) 24 | 25 | const CacheHeaderKey = "tysidecarcachable" 26 | const CacheHeaderEnabledKey = "sidecache-headers-enabled" 27 | const applicationDefaultPort = ":9191" 28 | 29 | type CacheServer struct { 30 | Repo cache.CacheRepository 31 | Proxy *httputil.ReverseProxy 32 | Prometheus *Prometheus 33 | Logger *zap.Logger 34 | CacheKeyPrefix string 35 | } 36 | 37 | type CacheData struct { 38 | Body []byte 39 | Headers map[string]string 40 | } 41 | 42 | func NewServer(repo cache.CacheRepository, proxy *httputil.ReverseProxy, prom *Prometheus, logger *zap.Logger) *CacheServer { 43 | return &CacheServer{ 44 | Repo: repo, 45 | Proxy: proxy, 46 | Prometheus: prom, 47 | Logger: logger, 48 | CacheKeyPrefix: os.Getenv("CACHE_KEY_PREFIX"), 49 | } 50 | } 51 | 52 | func (server CacheServer) Start(stopChan chan int) { 53 | server.Proxy.ModifyResponse = func(r *http.Response) error { 54 | cacheHeaderValue := r.Header.Get(CacheHeaderKey) 55 | if cacheHeaderValue != "" { 56 | cacheHeadersEnabled := r.Header.Get(CacheHeaderEnabledKey) 57 | maxAgeInSecond := server.GetHeaderTTL(cacheHeaderValue) 58 | r.Header.Del("Content-Length") // https://github.com/golang/go/issues/14975 59 | var b []byte 60 | var err error 61 | if r.Header.Get("content-encoding") == "gzip" { 62 | reader, _ := gzip.NewReader(r.Body) 63 | b, err = ioutil.ReadAll(reader) 64 | } else { 65 | b, err = ioutil.ReadAll(r.Body) 66 | } 67 | 68 | if err != nil { 69 | server.Logger.Error("Error while reading response body", zap.Error(err)) 70 | return err 71 | } 72 | 73 | buf := server.gzipWriter(b) 74 | go func(reqUrl *url.URL, data []byte, ttl int, cacheHeadersEnabled string) { 75 | hashedURL := server.HashURL(server.ReorderQueryString(reqUrl)) 76 | cacheData := CacheData{Body: data} 77 | 78 | if cacheHeadersEnabled == "true" { 79 | headers := make(map[string]string) 80 | for h,v := range r.Header{ 81 | headers[h] = strings.Join(v, ";") 82 | } 83 | cacheData.Headers = headers 84 | } 85 | 86 | cacheDataBytes, _ := json.Marshal(cacheData) 87 | server.Logger.Info(strconv.FormatBool(server.Repo == nil)) 88 | server.Repo.SetKey(hashedURL, cacheDataBytes, ttl) 89 | }(r.Request.URL, buf.Bytes(), maxAgeInSecond, cacheHeadersEnabled) 90 | 91 | err = r.Body.Close() 92 | if err != nil { 93 | server.Logger.Error("Error while closing response body", zap.Error(err)) 94 | return err 95 | } 96 | 97 | var body io.ReadCloser 98 | if r.Header.Get("content-encoding") == "gzip" { 99 | body = ioutil.NopCloser(buf) 100 | } else { 101 | body = ioutil.NopCloser(bytes.NewReader(b)) 102 | } 103 | 104 | r.Body = body 105 | } 106 | 107 | return nil 108 | } 109 | 110 | http.HandleFunc("/", server.CacheHandler) 111 | http.Handle("/metrics", promhttp.Handler()) 112 | 113 | port := determinatePort() 114 | httpServer := &http.Server{Addr: port} 115 | server.Logger.Info("SideCache process started port: ", zap.String("port", port)) 116 | 117 | go func() { 118 | server.Logger.Warn("Server closed: ", zap.Error(httpServer.ListenAndServe())) 119 | }() 120 | 121 | <-stopChan 122 | 123 | err := httpServer.Shutdown(context.Background()) 124 | if err != nil { 125 | server.Logger.Error("shutdown hook error", zap.Error(err)) 126 | } 127 | } 128 | 129 | func determinatePort() string { 130 | customPort := os.Getenv("SIDE_CACHE_PORT") 131 | if customPort == "" { 132 | return applicationDefaultPort 133 | 134 | } 135 | return ":" + customPort 136 | } 137 | 138 | func (server CacheServer) gzipWriter(b []byte) *bytes.Buffer { 139 | buf := bytes.NewBuffer([]byte{}) 140 | gzipWriter := gzip.NewWriter(buf) 141 | _, err := gzipWriter.Write(b) 142 | if err != nil { 143 | server.Logger.Error("Gzip Writer Encountered With an Error", zap.Error(err)) 144 | } 145 | gzipWriter.Close() 146 | return buf 147 | } 148 | 149 | func (server CacheServer) CacheHandler(w http.ResponseWriter, r *http.Request) { 150 | server.Prometheus.TotalRequestCounter.Inc() 151 | 152 | defer func() { 153 | if rec := recover(); rec != nil { 154 | var err error 155 | switch x := rec.(type) { 156 | case string: 157 | err = errors.New(x) 158 | case error: 159 | err = x 160 | default: 161 | err = errors.New("unknown panic") 162 | } 163 | 164 | server.Logger.Info("Recovered from panic", zap.Error(err)) 165 | http.Error(w, err.Error(), http.StatusInternalServerError) 166 | } 167 | }() 168 | 169 | hashedURL := server.HashURL(server.ReorderQueryString(r.URL)) 170 | cachedDataBytes := server.CheckCache(hashedURL) 171 | 172 | if cachedDataBytes != nil { 173 | w.Header().Add("X-Cache-Response-For", r.URL.String()) 174 | w.Header().Add("Content-Type", "application/json;charset=UTF-8") //todo get from cache? 175 | 176 | var cachedData CacheData 177 | err := json.Unmarshal(cachedDataBytes, &cachedData) 178 | if err != nil { 179 | //backward compatibility 180 | //if we can not marshall cached data to new structure 181 | //we write previously cached byte data 182 | if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { 183 | reader, _ := gzip.NewReader(bytes.NewReader(cachedDataBytes)) 184 | io.Copy(w, reader) 185 | } else { 186 | w.Header().Add("Content-Encoding", "gzip") 187 | io.Copy(w, bytes.NewReader(cachedDataBytes)) 188 | } 189 | } else { 190 | if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { 191 | reader, _ := gzip.NewReader(bytes.NewReader(cachedData.Body)) 192 | delete(cachedData.Headers, "Content-Encoding") 193 | writeHeaders(w, cachedData.Headers) 194 | io.Copy(w, reader) 195 | } else { 196 | writeHeaders(w, cachedData.Headers) 197 | if _, ok := cachedData.Headers["Content-Encoding"]; !ok { 198 | w.Header().Add("Content-Encoding", "gzip") 199 | } 200 | io.Copy(w, bytes.NewReader(cachedData.Body)) 201 | } 202 | } 203 | 204 | server.Prometheus.CacheHitCounter.Inc() 205 | } else { 206 | server.Proxy.ServeHTTP(w, r) 207 | } 208 | } 209 | 210 | func writeHeaders(w http.ResponseWriter, headers map[string]string) { 211 | if headers != nil { 212 | for h, v := range headers { 213 | w.Header().Set(h, v) 214 | } 215 | } 216 | } 217 | 218 | func (server CacheServer) GetHeaderTTL(cacheHeaderValue string) int { 219 | cacheValues := strings.Split(cacheHeaderValue, "=") 220 | var maxAgeInSecond = 0 221 | if len(cacheValues) > 1 { 222 | maxAgeInSecond, _ = strconv.Atoi(cacheValues[1]) 223 | } 224 | return maxAgeInSecond 225 | } 226 | 227 | func (server CacheServer) HashURL(url string) string { 228 | hasher := md5.New() 229 | hasher.Write([]byte(server.CacheKeyPrefix + "/" + url)) 230 | return hex.EncodeToString(hasher.Sum(nil)) 231 | } 232 | 233 | func (server CacheServer) CheckCache(url string) []byte { 234 | if server.Repo == nil { 235 | return nil 236 | } 237 | return server.Repo.Get(url) 238 | } 239 | 240 | func (server CacheServer) ReorderQueryString(url *url.URL) string { 241 | return url.Path + "?" + url.Query().Encode() 242 | } 243 | -------------------------------------------------------------------------------- /pkg/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server_test 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "crypto/md5" 7 | "encoding/hex" 8 | "encoding/json" 9 | "github.com/Trendyol/sidecache/pkg/cache" 10 | "github.com/Trendyol/sidecache/pkg/server" 11 | "github.com/golang/mock/gomock" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "go.uber.org/zap" 14 | "io/ioutil" 15 | "net" 16 | "net/http" 17 | "net/http/httptest" 18 | "net/http/httputil" 19 | "net/url" 20 | "os" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | var apiUrl, _ = url.Parse("http://localhost:8080/") 26 | var proxy = httputil.NewSingleHostReverseProxy(apiUrl) 27 | var logger, _ = zap.NewProduction() 28 | var client = server.NewPrometheusClient() 29 | var cacheServer *server.CacheServer 30 | var repos cache.CacheRepository 31 | 32 | func TestMain(m *testing.M) { 33 | client.CacheHitCounter = prometheus.NewCounter( 34 | prometheus.CounterOpts{ 35 | Namespace: "sidecache", 36 | Name: "cache_hit_counter", 37 | Help: "This is my counter", 38 | }) 39 | 40 | listener, _ := net.Listen("tcp", "127.0.0.1:8080") 41 | fakeApiServer := httptest.NewUnstartedServer( 42 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 | w.Header().Set("Content-Type", "application/json") 44 | w.Header().Set("Cache-TTL", "300") 45 | w.Header().Set("tysidecarcachable", "ttl=300") 46 | w.Header().Set("sidecache-headers-enabled", "true") 47 | 48 | user := map[string]string{ 49 | "Id": "1", 50 | "Name": "Emre Savcı", 51 | "Email": "emre.savci@trendyol.com", 52 | "Phone": "000099999", 53 | } 54 | json.NewEncoder(w).Encode(user) 55 | })) 56 | 57 | cacheServer = server.NewServer(repos, proxy, client, logger) 58 | stopChan := make(chan int) 59 | go cacheServer.Start(stopChan) 60 | 61 | fakeApiServer.Listener.Close() 62 | fakeApiServer.Listener = listener 63 | fakeApiServer.Start() 64 | 65 | code := m.Run() 66 | 67 | fakeApiServer.Close() 68 | stopChan <- 1 69 | os.Exit(code) 70 | } 71 | 72 | func TestReorderQueryString(t *testing.T) { 73 | var firstURL *url.URL 74 | var cacheServer *server.CacheServer 75 | var reorderQueryString string 76 | firstURL, _ = url.Parse("http://localhost:8080/api?year=2020&name=emre") 77 | cacheServer = server.NewServer(nil, nil, nil, nil) 78 | 79 | reorderQueryString = cacheServer.ReorderQueryString(firstURL) 80 | if reorderQueryString != "/api?name=emre&year=2020" { 81 | t.Errorf("Query strings are not equal") 82 | } 83 | } 84 | 85 | func TestHashUrl(t *testing.T) { 86 | var expectedHash string 87 | var actualHash string 88 | var cacheServer *server.CacheServer 89 | 90 | cacheServer = server.NewServer(nil, nil, nil, nil) 91 | cacheServer.CacheKeyPrefix = "test-prefix" 92 | 93 | testUrl := "testurl" 94 | 95 | hasher := md5.New() 96 | hasher.Write([]byte("test-prefix" + "/" + testUrl)) 97 | expectedHash = hex.EncodeToString(hasher.Sum(nil)) 98 | 99 | actualHash = cacheServer.HashURL(testUrl) 100 | 101 | if expectedHash != actualHash { 102 | t.Errorf("Hashes are not equal") 103 | } 104 | } 105 | 106 | func TestGetTTL(t *testing.T) { 107 | var value int 108 | var cacheServer *server.CacheServer 109 | cacheServer = server.NewServer(nil, nil, nil, nil) 110 | value = cacheServer.GetHeaderTTL("max-age=100") 111 | 112 | if value != 100 { 113 | t.Errorf("TTL values are not equal") 114 | } 115 | } 116 | 117 | func TestReturnProxyResponseWhenRepoReturnsNil(t *testing.T) { 118 | ctrl := gomock.NewController(t) 119 | defer ctrl.Finish() 120 | repo := cache.NewMockCacheRepository(ctrl) 121 | repo. 122 | EXPECT(). 123 | Get(gomock.Any()). 124 | Return(nil) 125 | 126 | repo. 127 | EXPECT(). 128 | SetKey(gomock.Any(), gomock.Any(), gomock.Any()). 129 | Times(1) 130 | 131 | cacheServer = server.NewServer(repo, proxy, client, logger) 132 | stopChan := make(chan int) 133 | go cacheServer.Start(stopChan) 134 | time.Sleep(5 * time.Second) 135 | resp, _ := http.Get("http://localhost:9191/api?name=emre&year=2020") 136 | respBody, _ := ioutil.ReadAll(resp.Body) 137 | 138 | actual := make(map[string]string) 139 | 140 | json.Unmarshal(respBody, &actual) 141 | 142 | if actual["Email"] != "emre.savci@trendyol.com" { 143 | t.Errorf("Email is not equal to expected") 144 | } 145 | stopChan <- 1 146 | } 147 | 148 | func TestReturnCacheResponseWhenRepoReturnsData(t *testing.T) { 149 | ctrl := gomock.NewController(t) 150 | repo := cache.NewMockCacheRepository(ctrl) 151 | cacheServer.Repo = repo 152 | 153 | str := "{'name':'emre'}" 154 | buf := bytes.NewBuffer([]byte{}) 155 | gzipWriter := gzip.NewWriter(buf) 156 | gzipWriter.Write([]byte(str)) 157 | gzipWriter.Close() 158 | 159 | repo. 160 | EXPECT(). 161 | Get(gomock.Any()). 162 | Return(buf.Bytes()) 163 | 164 | resp, _ := http.Get("http://localhost:9191/api?name=emre&year=2020") 165 | respBody, _ := ioutil.ReadAll(resp.Body) 166 | 167 | if string(respBody) != str { 168 | t.Errorf("Bodies are not equal") 169 | } 170 | } 171 | 172 | func TestReturnProxyResponseWhenNoCacheHeaderExists(t *testing.T) { 173 | ctrl := gomock.NewController(t) 174 | repo := cache.NewMockCacheRepository(ctrl) 175 | cacheServer.Repo = repo 176 | 177 | repo. 178 | EXPECT(). 179 | Get(gomock.Any()). 180 | Return(nil) 181 | 182 | repo. 183 | EXPECT(). 184 | SetKey(gomock.Any(), gomock.Any(), gomock.Any()). 185 | Times(1) 186 | 187 | httpClient := &http.Client{} 188 | req, _ := http.NewRequest("GET", "http://localhost:9191/api?name=emre&year=2020", nil) 189 | req.Header.Add("X-No-Cache", "true") 190 | resp, _ := httpClient.Do(req) 191 | 192 | respBody, _ := ioutil.ReadAll(resp.Body) 193 | 194 | actual := make(map[string]string) 195 | 196 | json.Unmarshal(respBody, &actual) 197 | 198 | if actual["Email"] != "emre.savci@trendyol.com" { 199 | t.Errorf("Email is not equal to expected") 200 | } 201 | } 202 | 203 | func TestReturnCacheHeadersWhenCacheHeaderEnabled(t *testing.T) { 204 | ctrl := gomock.NewController(t) 205 | repo := cache.NewMockCacheRepository(ctrl) 206 | cacheServer.Repo = repo 207 | 208 | user := map[string]string{ 209 | "Id": "1", 210 | "Name": "Emre Savcı", 211 | "Email": "emre.savci@trendyol.com", 212 | "Phone": "000099999", 213 | } 214 | 215 | userByte, _ := json.Marshal(user) 216 | headers := map[string]string{ 217 | "Content-Type":"application/json", 218 | "Cache-TTL": "300", 219 | "sidecache-headers-enabled": "true", 220 | } 221 | 222 | cacheData := server.CacheData{ 223 | Body: userByte, 224 | Headers: headers, 225 | } 226 | 227 | cacheDataBytes, _ := json.Marshal(cacheData) 228 | 229 | repo. 230 | EXPECT(). 231 | Get(gomock.Any()). 232 | Return(nil) 233 | 234 | repo. 235 | EXPECT(). 236 | SetKey(gomock.Any(), gomock.Eq(cacheDataBytes), gomock.Any()). 237 | Times(1) 238 | 239 | httpClient := &http.Client{} 240 | req, _ := http.NewRequest("GET", "http://localhost:9191/api?name=emre&year=2020", nil) 241 | resp, _ := httpClient.Do(req) 242 | 243 | respBody, _ := ioutil.ReadAll(resp.Body) 244 | 245 | actual := make(map[string]string) 246 | 247 | json.Unmarshal(respBody, &actual) 248 | if actual["Email"] != "emre.savci@trendyol.com" { 249 | t.Errorf("Email is not equal to expected") 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /pkg/tests/server_bench_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "github.com/Trendyol/sidecache/pkg/server" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkServerHash(b *testing.B) { 10 | os.Setenv("CACHE_KEY_PREFIX", "test") 11 | var cacheServer *server.CacheServer = new(server.CacheServer) 12 | for n := 0; n < b.N; n++ { 13 | cacheServer.HashURL("adsfadsdfasdfas") 14 | } 15 | } --------------------------------------------------------------------------------