From a2aa907cfb42efa23d5b4a44c3d0ad298f78e2de Mon Sep 17 00:00:00 2001 From: Nicolas Sitbon Date: Fri, 29 Nov 2024 14:43:11 +0100 Subject: [PATCH] initial import --- go.mod | 25 +++ go.sum | 153 +++++++++++++++++++ grpc/interceptor/cache/adapter/ristretto.go | 28 ++++ grpc/interceptor/cache/cache.go | 67 ++++++++ grpc/interceptor/cache/cache_test.go | 120 +++++++++++++++ grpc/interceptor/cache/keyExtractor.go | 54 +++++++ grpc/interceptor/cache/keyExtractor_test.go | 23 +++ grpc/interceptor/cache/monitor/prometheus.go | 64 ++++++++ grpc/interceptor/cache/null.go | 23 +++ grpc/interceptor/keyDerivator.go | 23 +++ grpc/interceptor/keyDerivator_test.go | 23 +++ grpc/interceptor/memmove.go | 58 +++++++ grpc/interceptor/memmove_test.go | 64 ++++++++ 13 files changed, 725 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100755 grpc/interceptor/cache/adapter/ristretto.go create mode 100755 grpc/interceptor/cache/cache.go create mode 100755 grpc/interceptor/cache/cache_test.go create mode 100755 grpc/interceptor/cache/keyExtractor.go create mode 100755 grpc/interceptor/cache/keyExtractor_test.go create mode 100755 grpc/interceptor/cache/monitor/prometheus.go create mode 100755 grpc/interceptor/cache/null.go create mode 100755 grpc/interceptor/keyDerivator.go create mode 100755 grpc/interceptor/keyDerivator_test.go create mode 100755 grpc/interceptor/memmove.go create mode 100755 grpc/interceptor/memmove_test.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..159e12a --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/nsitbon/grpc-cache-interceptor + +go 1.23.3 + +require ( + github.com/dgraph-io/ristretto v0.2.0 + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 + github.com/stretchr/testify v1.10.0 + google.golang.org/grpc v1.68.0 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e0a570c --- /dev/null +++ b/go.sum @@ -0,0 +1,153 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= +github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +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-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/grpc/interceptor/cache/adapter/ristretto.go b/grpc/interceptor/cache/adapter/ristretto.go new file mode 100755 index 0000000..0a49dc2 --- /dev/null +++ b/grpc/interceptor/cache/adapter/ristretto.go @@ -0,0 +1,28 @@ +package adapter + +import ( + "time" + + "github.com/dgraph-io/ristretto" + "github.com/nsitbon/grpc-cache-interceptor/grpc/interceptor/cache" +) + +type RistrettoCacheAdapter struct { + cache *ristretto.Cache +} + +func (r *RistrettoCacheAdapter) Get(key interface{}) (interface{}, bool) { + return r.cache.Get(key) +} + +func (r *RistrettoCacheAdapter) Set(key interface{}, value interface{}) { + r.cache.Set(key, value, 0) +} + +func (r *RistrettoCacheAdapter) SetWithTTL(key interface{}, value interface{}, ttl time.Duration) { + r.cache.SetWithTTL(key, value, 0, ttl) +} + +func NewRistrettoCacheAdapter(cache *ristretto.Cache) cache.Cache { + return &RistrettoCacheAdapter{cache: cache} +} diff --git a/grpc/interceptor/cache/cache.go b/grpc/interceptor/cache/cache.go new file mode 100755 index 0000000..887f2a1 --- /dev/null +++ b/grpc/interceptor/cache/cache.go @@ -0,0 +1,67 @@ +package cache + +import ( + "context" + + "github.com/nsitbon/grpc-cache-interceptor/grpc/interceptor" + "google.golang.org/grpc" +) + +type CachingInterceptorKeyDerivator func(string, interface{}) (interface{}, error) + +type CachingInterceptorOption func(interceptor *CachingInterceptor) + +func WithMemMove(memMove MemMoveFn) CachingInterceptorOption { + return func(i *CachingInterceptor) { + i.memMove = memMove + } +} + +type MemMoveFn func(dest, src interface{}) error + +type CachingInterceptor struct { + memMove MemMoveFn + keyDerivator interceptor.KeyDerivator + cache Cache +} + +func NewCachingInterceptor(cache Cache, options ...CachingInterceptorOption) *CachingInterceptor { + i := &CachingInterceptor{cache: cache, keyDerivator: interceptor.NewDefaultKeyDerivatorImpl()} + + for _, opt := range options { + opt(i) + } + + if i.memMove == nil { + i.memMove = interceptor.MemMove + } + + return i +} + +func (i *CachingInterceptor) Intercept(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + if key, err := i.keyDerivator.Derive(method, req); err != nil { + return err + } else if i.getFromCacheAndSetReply(method, key, reply) { + return nil + } else if err = invoker(ctx, method, req, reply, cc, opts...); err == nil { + i.cache.Set(key, reply) + return nil + } else { + return err + } +} + +/* +v and reply are both interface whose dynamic types are equal and of kind pointer to T. +Golang doesn't support generics so we implement genericity by simply copying the content of v (of type T) +to the content of reply (also of type T). To do that we "treat" the content as raw bytes and create a byte slice +backed by the content (of type T). +*/ +func (i *CachingInterceptor) getFromCacheAndSetReply(method string, key interface{}, reply interface{}) bool { + if v, ok := i.cache.Get(key); ok { + return i.memMove(reply, v) == nil + } + + return false +} diff --git a/grpc/interceptor/cache/cache_test.go b/grpc/interceptor/cache/cache_test.go new file mode 100755 index 0000000..5f9e02a --- /dev/null +++ b/grpc/interceptor/cache/cache_test.go @@ -0,0 +1,120 @@ +package cache + +import ( + "encoding/json" + "testing" + "time" + + grpc_testing "github.com/grpc-ecosystem/go-grpc-middleware/testing" + pb_testproto "github.com/grpc-ecosystem/go-grpc-middleware/testing/testproto" + "google.golang.org/grpc" +) + +type GetCall struct { + Key interface{} + Value interface{} + Found bool +} + +type SetCall struct { + Key interface{} + Value interface{} +} + +type MockingCache struct { + values map[interface{}]interface{} + GetCalls []GetCall + SetCalls []SetCall +} + +func (m *MockingCache) SetWithTTL(key interface{}, value interface{}, ttl time.Duration) {} + +func NewMockingCache() *MockingCache { + return &MockingCache{values: make(map[interface{}]interface{})} +} + +func (m *MockingCache) Get(key interface{}) (interface{}, bool) { + v, ok := m.values[string(key.([]byte))] + m.GetCalls = append(m.GetCalls, GetCall{Key: key, Value: v, Found: ok}) + return v, ok +} + +func (m *MockingCache) Set(key interface{}, value interface{}) { + m.values[string(key.([]byte))] = value + m.SetCalls = append(m.SetCalls, SetCall{Key: key, Value: value}) +} + +func deriveKey(key interface{}) string { + if v, err := json.Marshal(key); err != nil { + panic(err) + } else { + return "/mwitkow.testproto.TestService/Ping:" + string(v) + } +} + +func TestItCallsTheInvokerOnCacheMiss(t *testing.T) { + cache := NullCache + its := getSuite(cache, t) + defer its.TearDownSuite() + + request := &pb_testproto.PingRequest{Value: "my_fake_ping_payload"} + resp, err := its.Client.Ping(its.SimpleCtx(), request) + + its.NoError(err) + its.Equal("my_fake_ping_payload", resp.Value) + its.Equal(int32(42), resp.Counter) +} + +func TestItFirstTryToGetValueFromCache(t *testing.T) { + cache := NewMockingCache() + its := getSuite(cache, t) + defer its.TearDownSuite() + + request := &pb_testproto.PingRequest{Value: "my_fake_ping_payload"} + _, _ = its.Client.Ping(its.SimpleCtx(), request) + + its.Len(cache.GetCalls, 1) + its.EqualValues(string(cache.GetCalls[0].Key.([]byte)), deriveKey(request)) + its.False(cache.GetCalls[0].Found) +} + +func TestItSetsTheCacheAfterCallingTheInvoker(t *testing.T) { + cache := NewMockingCache() + its := getSuite(cache, t) + defer its.TearDownSuite() + + request := &pb_testproto.PingRequest{Value: "my_fake_ping_payload"} + resp, _ := its.Client.Ping(its.SimpleCtx(), request) + + its.Len(cache.SetCalls, 1) + its.EqualValues(cache.SetCalls[0].Key, deriveKey(request)) + its.EqualValues(cache.SetCalls[0].Value, resp) +} + +func TestItDoesntCallTheInvokerOnCacheHit(t *testing.T) { + cache := NewMockingCache() + its := getSuite(cache, t) + defer its.TearDownSuite() + + request := &pb_testproto.PingRequest{Value: "my_fake_ping_payload"} + + expectedResponse := &pb_testproto.PingResponse{Value: "expected value", Counter: 1} + cache.values[deriveKey(request)] = expectedResponse + response, err := its.Client.Ping(its.SimpleCtx(), request) + + its.NoError(err) + its.Equal(expectedResponse.Value, response.Value) + its.Equal(expectedResponse.Counter, response.Counter) + + its.Len(cache.GetCalls, 1) + its.Len(cache.SetCalls, 0) +} + +func getSuite(cache Cache, t *testing.T) *grpc_testing.InterceptorTestSuite { + its := &grpc_testing.InterceptorTestSuite{ClientOpts: []grpc.DialOption{ + grpc.WithUnaryInterceptor(NewCachingInterceptor(cache).Intercept)}, + } + its.Suite.SetT(t) + its.SetupSuite() + return its +} diff --git a/grpc/interceptor/cache/keyExtractor.go b/grpc/interceptor/cache/keyExtractor.go new file mode 100755 index 0000000..8535dca --- /dev/null +++ b/grpc/interceptor/cache/keyExtractor.go @@ -0,0 +1,54 @@ +package cache + +import ( + "fmt" + "regexp" +) + +const keyPattern = `^/(([^.]+)\.([^/]+)/([^:]+)):` + +var keyRegex *regexp.Regexp + +type KeyParts struct { + PackageName string + ServiceName string + MethodName string + MethodLongName string +} + +type KeyExtractor interface { + Extract(key interface{}) (*KeyParts, error) +} + +type DefaultKeyExtractorImpl struct{} + +func (d *DefaultKeyExtractorImpl) Extract(key interface{}) (*KeyParts, error) { + if keyAsString, ok := keyIsString(key); !ok { + return nil, fmt.Errorf("expected key type is []byte but got %T", key) + } else if parts := keyRegex.FindStringSubmatch(keyAsString); len(parts) != 5 { + return nil, fmt.Errorf("key expected format is '%s' but got '%s'", keyPattern, keyAsString) + } else { + return &KeyParts{ + MethodLongName:parts[1], + PackageName: parts[2], + ServiceName: parts[3], + MethodName: parts[4], + }, nil + } +} + +func keyIsString(key interface{}) (string, bool) { + if bytes, ok := key.([]byte); ok { + return string(bytes), true + } + + return "", false +} + +func NewDefaultKeyExtractorImpl() *DefaultKeyExtractorImpl { + return &DefaultKeyExtractorImpl{} +} + +func init() { + keyRegex = regexp.MustCompile(keyPattern) +} diff --git a/grpc/interceptor/cache/keyExtractor_test.go b/grpc/interceptor/cache/keyExtractor_test.go new file mode 100755 index 0000000..3b507f5 --- /dev/null +++ b/grpc/interceptor/cache/keyExtractor_test.go @@ -0,0 +1,23 @@ +package cache + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultKeyExtractorImpl_Extract(t *testing.T) { + e := NewDefaultKeyExtractorImpl() + expectedPackage := "catalog" + expectedService := "Catalog" + expectedMethod := "GetCustomVideosByProgramId" + key := []byte(fmt.Sprintf(`/%s.%s/%s:{"some json":1}`, expectedPackage, expectedService, expectedMethod)) + + parts, err := e.Extract(key) + + assert.NoError(t, err) + assert.EqualValues(t, expectedPackage, parts.PackageName) + assert.EqualValues(t, expectedService, parts.ServiceName) + assert.EqualValues(t, expectedMethod, parts.MethodName) +} diff --git a/grpc/interceptor/cache/monitor/prometheus.go b/grpc/interceptor/cache/monitor/prometheus.go new file mode 100755 index 0000000..9d32661 --- /dev/null +++ b/grpc/interceptor/cache/monitor/prometheus.go @@ -0,0 +1,64 @@ +package monitor + +import ( + "time" + + "github.com/nsitbon/grpc-cache-interceptor/grpc/interceptor/cache" +) + +type Option func(*PrometheusMonitor) + +type Recorder interface { + IncCacheKeyMetric(service string, key string, metric string) +} + +type PrometheusMonitor struct { + recorder Recorder + cache cache.Cache + keyExtractor cache.KeyExtractor + onError func(err error) +} + +func (d *PrometheusMonitor) Get(key interface{}) (interface{}, bool) { + ret, found := d.cache.Get(key) + + if parts, err := d.keyExtractor.Extract(key); err == nil { + d.recorder.IncCacheKeyMetric(parts.PackageName, parts.MethodName+"Cache", getMetric(found)) + } else if d.onError != nil { + d.onError(err) + } + + return ret, found +} + +func getMetric(found bool) string { + if found { + return "hit_count" + } else { + return "miss_count" + } +} + +func (d *PrometheusMonitor) Set(key interface{}, value interface{}) { + d.cache.Set(key, value) +} + +func (d *PrometheusMonitor) SetWithTTL(key interface{}, value interface{}, ttl time.Duration) { + d.cache.SetWithTTL(key, value, ttl) +} + +func NewPrometheusMonitor(recorder Recorder, innerCache cache.Cache, keyExtractor cache.KeyExtractor, opts ...Option) cache.Cache { + m := &PrometheusMonitor{recorder: recorder, cache: innerCache, keyExtractor: keyExtractor} + + for _, opt := range opts { + opt(m) + } + + return m +} + +func WithErrorFunc(onError func(error)) Option { + return func(that *PrometheusMonitor) { + that.onError = onError + } +} diff --git a/grpc/interceptor/cache/null.go b/grpc/interceptor/cache/null.go new file mode 100755 index 0000000..e7143b1 --- /dev/null +++ b/grpc/interceptor/cache/null.go @@ -0,0 +1,23 @@ +package cache + +import ( + "time" +) + +var NullCache Cache = &nullCache{} + +type nullCache struct {} + +func (n nullCache) Get(_ interface{}) (interface{}, bool) { + return nil, false +} + +func (n nullCache) Set(_ interface{}, _ interface{}) {} + +func (n nullCache) SetWithTTL(_ interface{}, _ interface{}, _ time.Duration) {} + +type Cache interface { + Get(key interface{}) (interface{}, bool) + Set(key interface{}, value interface{}) + SetWithTTL(key interface{}, value interface{}, ttl time.Duration) +} diff --git a/grpc/interceptor/keyDerivator.go b/grpc/interceptor/keyDerivator.go new file mode 100755 index 0000000..db02f9e --- /dev/null +++ b/grpc/interceptor/keyDerivator.go @@ -0,0 +1,23 @@ +package interceptor + +import ( + "encoding/json" +) + +type KeyDerivator interface { + Derive(method string, req interface{}) (interface{}, error) +} + +type DefaultKeyDerivatorImpl struct{} + +func (d *DefaultKeyDerivatorImpl) Derive(method string, req interface{}) (interface{}, error) { + if raw, err := json.Marshal(req); err != nil { + return nil, err + } else { + return append([]byte(method+":"), raw...), nil + } +} + +func NewDefaultKeyDerivatorImpl() KeyDerivator { + return &DefaultKeyDerivatorImpl{} +} diff --git a/grpc/interceptor/keyDerivator_test.go b/grpc/interceptor/keyDerivator_test.go new file mode 100755 index 0000000..f5c7976 --- /dev/null +++ b/grpc/interceptor/keyDerivator_test.go @@ -0,0 +1,23 @@ +package interceptor + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type MockGrpcRequest struct { + AString string + AnInteger int +} + +func TestItConcatTheMethodAColonAndTheMArshalledRequest(t *testing.T) { + kd := NewDefaultKeyDerivatorImpl() + method := "/catalog/MyMethod" + req := MockGrpcRequest{"a string", 1} + + key, err := kd.Derive(method, &req) + + assert.NoError(t, err) + assert.Equal(t, `/catalog/MyMethod:{"AString":"a string","AnInteger":1}`, string(key.([]byte))) +} diff --git a/grpc/interceptor/memmove.go b/grpc/interceptor/memmove.go new file mode 100755 index 0000000..1ea07c3 --- /dev/null +++ b/grpc/interceptor/memmove.go @@ -0,0 +1,58 @@ +package interceptor + +import ( + "fmt" + "reflect" + "unsafe" +) + +func MemMove(dest, src interface{}) error { + if err := checkParamsType(dest, src); err != nil { + return err + } + + copy(interfaceToSlice(dest), interfaceToSlice(src)) + return nil +} + +func checkParamsType(dest interface{}, src interface{}) error { + if dest == nil { + return fmt.Errorf("MemMove requires arguments to be non nil: dest (nil)") + } else if src == nil { + return fmt.Errorf("MemMove requires arguments to be non nil: src (nil)") + } else if !hasPointerType(dest) { + return fmt.Errorf("MemMove requires arguments to be of pointer type: dest (%T)", dest) + } else if !hasPointerType(src) { + return fmt.Errorf("MemMove requires arguments to be of pointer type: src (%T)", src) + } else if !haveSameType(src, dest) { + return fmt.Errorf("MemMove requires arguments of the same type: src (%T), dest (%T)", src, dest) + } + return nil +} + +func hasPointerType(dest interface{}) bool { + return reflect.ValueOf(dest).Kind() == reflect.Ptr +} + +func haveSameType(src interface{}, dest interface{}) bool { + return reflect.ValueOf(src).Type() == reflect.ValueOf(dest).Type() +} + +func interfaceToSlice(req interface{}) []byte { + return buildByteSlice(getPointerValue(req), getPointedTypeSize(req)) +} + +/* + reflect.SliceHeader is the internal structure used by Go to represent Slice. +*/ +func buildByteSlice(data uintptr, size int) []byte { + return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{Data: data, Len: size, Cap: size})) +} + +func getPointedTypeSize(req interface{}) int { + return int(reflect.Indirect(reflect.ValueOf(req)).Type().Size()) +} + +func getPointerValue(req interface{}) uintptr { + return reflect.ValueOf(req).Pointer() +} diff --git a/grpc/interceptor/memmove_test.go b/grpc/interceptor/memmove_test.go new file mode 100755 index 0000000..dbc6e01 --- /dev/null +++ b/grpc/interceptor/memmove_test.go @@ -0,0 +1,64 @@ +package interceptor + +import ( + "fmt" + "reflect" + "testing" +) + +type FakeType1 struct { + Foo int +} + +type FakeType2 struct { + Foo string +} + +type FakeType3 struct { + FakeType1 + Bar bool +} + +func TestMemMove(t *testing.T) { + t.Parallel() // marks TLog as capable of running in parallel with other tests + tests := []struct { + src interface{} + dest interface{} + hasError bool + }{ + {nil, nil, true}, + {1, nil, true}, + {nil, 2, true}, + {1, 2, true}, + {1, 2.0, true}, + {FakeType1{Foo: 1}, FakeType1{Foo: 2}, true}, + {&FakeType1{Foo: 1}, &FakeType1{Foo: 2}, false}, + {FakeType1{Foo: 1}, FakeType2{Foo: "2"}, true}, + {&FakeType1{Foo: 1}, &FakeType2{Foo: "2"}, true}, + + {FakeType1{Foo: 1}, FakeType3{FakeType1: FakeType1{Foo: 2}, Bar: true}, true}, + {&FakeType1{Foo: 1}, &FakeType3{FakeType1: FakeType1{Foo: 2}, Bar: true}, true}, + } + for _, tt := range tests { + tt := tt + t.Run(fmt.Sprintf("src(%#v)/dest(%#v)", tt.src, tt.dest), func(t *testing.T) { + t.Parallel() + + err := MemMove(tt.dest, tt.src) + + if err != nil != tt.hasError { + expected := "not" + + if tt.hasError { + expected = "" + } + + t.Errorf(fmt.Sprintf("src (%#v) / dest (%#v) error was %s expected: err = %v", tt.src, tt.dest, expected, err)) + } + + if !tt.hasError && !reflect.DeepEqual(tt.dest, tt.src) { + t.Errorf("dest and src were supposed to be equal : dest (%v) / src (%v)", tt.dest, tt.src) + } + }) + } +}