diff --git a/go.mod b/go.mod index fa07aafe241..547a624d226 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,6 @@ require ( github.com/hashicorp/go-msgpack v0.5.5 github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect - github.com/hashicorp/golang-lru v0.5.3 // indirect github.com/hashicorp/serf v0.9.2 // indirect github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 @@ -54,7 +53,6 @@ require ( github.com/klauspost/compress v1.4.1 // indirect github.com/klauspost/cpuid v1.2.0 // indirect github.com/klauspost/pgzip v1.2.4 - github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/krishicks/yaml-patch v0.0.10 github.com/magiconair/properties v1.8.1 github.com/martini-contrib/auth v0.0.0-20150219114609-fa62c19b7ae8 diff --git a/go.sum b/go.sum index 41cc2b77507..deccc4d563e 100644 --- a/go.sum +++ b/go.sum @@ -147,9 +147,11 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbp github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/corpix/uarand v0.1.1 h1:RMr1TWc9F4n5jiPDzFHtmaUXLKLNUFK0SgCLo4BhX/U= github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU= +github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK06d8= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY= @@ -160,14 +162,18 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2 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.0.3 h1:jh22xisGBjrEVnRZ1DVTpBVQm0Xndu8sMl0CWDzSIBI= +github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -188,6 +194,7 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-delve/delve v1.5.0/go.mod h1:c6b3a1Gry6x8a4LGCe/CWzrocrfaHvkUxCj3k4bvSUQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -274,6 +281,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ= github.com/google/go-github/v27 v27.0.4/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -363,9 +371,12 @@ github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2I github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -424,6 +435,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGi github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -451,6 +463,7 @@ github.com/martini-contrib/gzip v0.0.0-20151124214156-6c035326b43f h1:wVDxEVZP1e github.com/martini-contrib/gzip v0.0.0-20151124214156-6c035326b43f/go.mod h1:jhUB0rZB2TPWqy0yGugKRRictO591eSO7If7O4MfCaA= github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 h1:YFh+sjyJTMQSYjKwM4dFKhJPJC/wfo98tPUc17HdoYw= github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI= +github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -499,6 +512,7 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.2.3 h1:f/MjBEBDLttYCGfRaKBbKSRVF5aV2O6fnBpzknuE3jU= github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mmcloughlin/avo v0.0.0-20201105074841-5d2f697d268f/go.mod h1:6aKT4zZIrpGqB3RpFU14ByCSSyKY6LfJz4J/JJChHfI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -545,6 +559,7 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pires/go-proxyproto v0.0.0-20191211124218-517ecdf5bb2b h1:JPLdtNmpXbWytipbGwYz7zXZzlQNASEiFw5aGAM75us= @@ -611,6 +626,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sjmudd/stopwatch v0.0.0-20170613150411-f380bf8a9be1 h1:acClJNSOjUrAUKW+ZneCZymCFDWtSaJG5YQl8FoOlyI= github.com/sjmudd/stopwatch v0.0.0-20170613150411-f380bf8a9be1/go.mod h1:Pgf1sZ2KrHK8vdRTV5UHGp80LT7HMUKuNAiKC402abY= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -629,6 +646,7 @@ github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= @@ -637,6 +655,7 @@ github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJ github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -668,6 +687,7 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/twitchyliquid64/golang-asm v0.15.0/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/uber/jaeger-client-go v2.16.0+incompatible h1:Q2Pp6v3QYiocMxomCaJuwQGFt7E53bPYqEgug/AoBtY= @@ -697,6 +717,7 @@ go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qL go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -704,6 +725,8 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= +golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -816,6 +839,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -868,6 +892,8 @@ golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/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-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201105001634-bc3cf281b174/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201202200335-bef1c476418a h1:TYqOq/v+Ri5aADpldxXOj6PmvcPMOJbLjdALzZDQT2M= golang.org/x/tools v0.0.0-20201202200335-bef1c476418a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -982,6 +1008,7 @@ modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03 modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= diff --git a/go/cache/cache.go b/go/cache/cache.go new file mode 100644 index 00000000000..3f81cec50e7 --- /dev/null +++ b/go/cache/cache.go @@ -0,0 +1,80 @@ +/* +Copyright 2021 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cache + +// Cache is a generic interface type for a data structure that keeps recently used +// objects in memory and evicts them when it becomes full. +type Cache interface { + Get(key string) (interface{}, bool) + Set(key string, val interface{}) bool + ForEach(callback func(interface{}) bool) + + Delete(key string) + Clear() + + // Wait waits for all pending operations on the cache to settle. Since cache writes + // are asynchronous, a write may not be immediately accessible unless the user + // manually calls Wait. + Wait() + + Len() int + Evictions() int64 + UsedCapacity() int64 + MaxCapacity() int64 + SetCapacity(int64) +} + +type cachedObject interface { + CachedSize(alloc bool) int64 +} + +// NewDefaultCacheImpl returns the default cache implementation for Vitess. The options in the +// Config struct control the memory and entry limits for the cache, and the underlying cache +// implementation. +func NewDefaultCacheImpl(cfg *Config) Cache { + switch { + case cfg == nil || (cfg.MaxEntries == 0 && cfg.MaxMemoryUsage == 0): + return &nullCache{} + + case cfg.LFU: + return NewRistrettoCache(cfg.MaxEntries, cfg.MaxMemoryUsage, func(val interface{}) int64 { + return val.(cachedObject).CachedSize(true) + }) + + default: + return NewLRUCache(cfg.MaxEntries, func(_ interface{}) int64 { + return 1 + }) + } +} + +// Config is the configuration options for a cache instance +type Config struct { + // MaxEntries is the estimated amount of entries that the cache will hold at capacity + MaxEntries int64 + // MaxMemoryUsage is the maximum amount of memory the cache can handle + MaxMemoryUsage int64 + // LFU toggles whether to use a new cache implementation with a TinyLFU admission policy + LFU bool +} + +// DefaultConfig is the default configuration for a cache instance in Vitess +var DefaultConfig = &Config{ + MaxEntries: 5000, + MaxMemoryUsage: 32 * 1024 * 1024, + LFU: false, +} diff --git a/go/cache/lru_cache.go b/go/cache/lru_cache.go index cf33235670a..9175f942e94 100644 --- a/go/cache/lru_cache.go +++ b/go/cache/lru_cache.go @@ -25,59 +25,55 @@ package cache import ( "container/list" - "fmt" "sync" "time" ) +var _ Cache = &LRUCache{} + // LRUCache is a typical LRU cache implementation. If the cache // reaches the capacity, the least recently used item is deleted from // the cache. Note the capacity is not the number of items, but the -// total sum of the Size() of each item. +// total sum of the CachedSize() of each item. type LRUCache struct { mu sync.Mutex // list & table contain *entry objects. list *list.List table map[string]*list.Element + cost func(interface{}) int64 size int64 capacity int64 evictions int64 } -// Value is the interface values that go into LRUCache need to satisfy -type Value interface { - // Size returns how big this value is. If you want to just track - // the cache by number of objects, you may return the size as 1. - Size() int -} - // Item is what is stored in the cache type Item struct { Key string - Value Value + Value interface{} } type entry struct { key string - value Value + value interface{} size int64 timeAccessed time.Time } // NewLRUCache creates a new empty cache with the given capacity. -func NewLRUCache(capacity int64) *LRUCache { +func NewLRUCache(capacity int64, cost func(interface{}) int64) *LRUCache { return &LRUCache{ list: list.New(), table: make(map[string]*list.Element), capacity: capacity, + cost: cost, } } // Get returns a value from the cache, and marks the entry as most // recently used. -func (lru *LRUCache) Get(key string) (v Value, ok bool) { +func (lru *LRUCache) Get(key string) (v interface{}, ok bool) { lru.mu.Lock() defer lru.mu.Unlock() @@ -89,20 +85,8 @@ func (lru *LRUCache) Get(key string) (v Value, ok bool) { return element.Value.(*entry).value, true } -// Peek returns a value from the cache without changing the LRU order. -func (lru *LRUCache) Peek(key string) (v Value, ok bool) { - lru.mu.Lock() - defer lru.mu.Unlock() - - element := lru.table[key] - if element == nil { - return nil, false - } - return element.Value.(*entry).value, true -} - // Set sets a value in the cache. -func (lru *LRUCache) Set(key string, value Value) { +func (lru *LRUCache) Set(key string, value interface{}) bool { lru.mu.Lock() defer lru.mu.Unlock() @@ -111,23 +95,12 @@ func (lru *LRUCache) Set(key string, value Value) { } else { lru.addNew(key, value) } -} - -// SetIfAbsent will set the value in the cache if not present. If the -// value exists in the cache, we don't set it. -func (lru *LRUCache) SetIfAbsent(key string, value Value) { - lru.mu.Lock() - defer lru.mu.Unlock() - - if element := lru.table[key]; element != nil { - lru.moveToFront(element) - } else { - lru.addNew(key, value) - } + // the LRU cache cannot fail to insert items; it always returns true + return true } // Delete removes an entry from the cache, and returns if the entry existed. -func (lru *LRUCache) Delete(key string) bool { +func (lru *LRUCache) delete(key string) bool { lru.mu.Lock() defer lru.mu.Unlock() @@ -142,6 +115,11 @@ func (lru *LRUCache) Delete(key string) bool { return true } +// Delete removes an entry from the cache +func (lru *LRUCache) Delete(key string) { + lru.delete(key) +} + // Clear will clear the entire cache. func (lru *LRUCache) Clear() { lru.mu.Lock() @@ -152,6 +130,13 @@ func (lru *LRUCache) Clear() { lru.size = 0 } +// Len returns the size of the cache (in entries) +func (lru *LRUCache) Len() int { + lru.mu.Lock() + defer lru.mu.Unlock() + return lru.list.Len() +} + // SetCapacity will set the capacity of the cache. If the capacity is // smaller, and the current cache size exceed that capacity, the cache // will be shrank. @@ -163,75 +148,40 @@ func (lru *LRUCache) SetCapacity(capacity int64) { lru.checkCapacity() } -// Stats returns a few stats on the cache. -func (lru *LRUCache) Stats() (length, size, capacity, evictions int64, oldest time.Time) { - lru.mu.Lock() - defer lru.mu.Unlock() - if lastElem := lru.list.Back(); lastElem != nil { - oldest = lastElem.Value.(*entry).timeAccessed - } - return int64(lru.list.Len()), lru.size, lru.capacity, lru.evictions, oldest -} - -// StatsJSON returns stats as a JSON object in a string. -func (lru *LRUCache) StatsJSON() string { - if lru == nil { - return "{}" - } - l, s, c, e, o := lru.Stats() - return fmt.Sprintf("{\"Length\": %v, \"Size\": %v, \"Capacity\": %v, \"Evictions\": %v, \"OldestAccess\": \"%v\"}", l, s, c, e, o) -} - -// Length returns how many elements are in the cache -func (lru *LRUCache) Length() int64 { - lru.mu.Lock() - defer lru.mu.Unlock() - return int64(lru.list.Len()) -} +// Wait is a no-op in the LRU cache +func (lru *LRUCache) Wait() {} -// Size returns the sum of the objects' Size() method. -func (lru *LRUCache) Size() int64 { - lru.mu.Lock() - defer lru.mu.Unlock() +// UsedCapacity returns the size of the cache (in bytes) +func (lru *LRUCache) UsedCapacity() int64 { return lru.size } -// Capacity returns the cache maximum capacity. -func (lru *LRUCache) Capacity() int64 { +// MaxCapacity returns the cache maximum capacity. +func (lru *LRUCache) MaxCapacity() int64 { lru.mu.Lock() defer lru.mu.Unlock() return lru.capacity } -// Evictions returns the eviction count. +// Evictions returns the number of evictions func (lru *LRUCache) Evictions() int64 { lru.mu.Lock() defer lru.mu.Unlock() return lru.evictions } -// Oldest returns the insertion time of the oldest element in the cache, -// or a IsZero() time if cache is empty. -func (lru *LRUCache) Oldest() (oldest time.Time) { - lru.mu.Lock() - defer lru.mu.Unlock() - if lastElem := lru.list.Back(); lastElem != nil { - oldest = lastElem.Value.(*entry).timeAccessed - } - return -} - -// Keys returns all the keys for the cache, ordered from most recently +// ForEach yields all the values for the cache, ordered from most recently // used to least recently used. -func (lru *LRUCache) Keys() []string { +func (lru *LRUCache) ForEach(callback func(value interface{}) bool) { lru.mu.Lock() defer lru.mu.Unlock() - keys := make([]string, 0, lru.list.Len()) for e := lru.list.Front(); e != nil; e = e.Next() { - keys = append(keys, e.Value.(*entry).key) + v := e.Value.(*entry) + if !callback(v.value) { + break + } } - return keys } // Items returns all the values for the cache, ordered from most recently @@ -248,8 +198,8 @@ func (lru *LRUCache) Items() []Item { return items } -func (lru *LRUCache) updateInplace(element *list.Element, value Value) { - valueSize := int64(value.Size()) +func (lru *LRUCache) updateInplace(element *list.Element, value interface{}) { + valueSize := lru.cost(value) sizeDiff := valueSize - element.Value.(*entry).size element.Value.(*entry).value = value element.Value.(*entry).size = valueSize @@ -263,8 +213,8 @@ func (lru *LRUCache) moveToFront(element *list.Element) { element.Value.(*entry).timeAccessed = time.Now() } -func (lru *LRUCache) addNew(key string, value Value) { - newEntry := &entry{key, value, int64(value.Size()), time.Now()} +func (lru *LRUCache) addNew(key string, value interface{}) { + newEntry := &entry{key, value, lru.cost(value), time.Now()} element := lru.list.PushFront(newEntry) lru.table[key] = element lru.size += newEntry.size diff --git a/go/cache/lru_cache_test.go b/go/cache/lru_cache_test.go index 9a7f09232e6..152ac17ab6f 100644 --- a/go/cache/lru_cache_test.go +++ b/go/cache/lru_cache_test.go @@ -17,22 +17,20 @@ limitations under the License. package cache import ( - "encoding/json" "testing" - "time" ) type CacheValue struct { - size int + size int64 } -func (cv *CacheValue) Size() int { - return cv.size +func cacheValueSize(val interface{}) int64 { + return val.(*CacheValue).size } func TestInitialState(t *testing.T) { - cache := NewLRUCache(5) - l, sz, c, e, _ := cache.Stats() + cache := NewLRUCache(5, cacheValueSize) + l, sz, c, e := cache.Len(), cache.UsedCapacity(), cache.MaxCapacity(), cache.Evictions() if l != 0 { t.Errorf("length = %v, want 0", l) } @@ -48,7 +46,7 @@ func TestInitialState(t *testing.T) { } func TestSetInsertsValue(t *testing.T) { - cache := NewLRUCache(100) + cache := NewLRUCache(100, cacheValueSize) data := &CacheValue{0} key := "key" cache.Set(key, data) @@ -58,37 +56,14 @@ func TestSetInsertsValue(t *testing.T) { t.Errorf("Cache has incorrect value: %v != %v", data, v) } - k := cache.Keys() - if len(k) != 1 || k[0] != key { - t.Errorf("Cache.Keys() returned incorrect values: %v", k) - } values := cache.Items() if len(values) != 1 || values[0].Key != key { t.Errorf("Cache.Values() returned incorrect values: %v", values) } } -func TestSetIfAbsent(t *testing.T) { - cache := NewLRUCache(100) - data := &CacheValue{0} - key := "key" - cache.SetIfAbsent(key, data) - - v, ok := cache.Get(key) - if !ok || v.(*CacheValue) != data { - t.Errorf("Cache has incorrect value: %v != %v", data, v) - } - - cache.SetIfAbsent(key, &CacheValue{1}) - - v, ok = cache.Get(key) - if !ok || v.(*CacheValue) != data { - t.Errorf("Cache has incorrect value: %v != %v", data, v) - } -} - func TestGetValueWithMultipleTypes(t *testing.T) { - cache := NewLRUCache(100) + cache := NewLRUCache(100, cacheValueSize) data := &CacheValue{0} key := "key" cache.Set(key, data) @@ -105,23 +80,23 @@ func TestGetValueWithMultipleTypes(t *testing.T) { } func TestSetUpdatesSize(t *testing.T) { - cache := NewLRUCache(100) + cache := NewLRUCache(100, cacheValueSize) emptyValue := &CacheValue{0} key := "key1" cache.Set(key, emptyValue) - if _, sz, _, _, _ := cache.Stats(); sz != 0 { - t.Errorf("cache.Size() = %v, expected 0", sz) + if sz := cache.UsedCapacity(); sz != 0 { + t.Errorf("cache.UsedCapacity() = %v, expected 0", sz) } someValue := &CacheValue{20} key = "key2" cache.Set(key, someValue) - if _, sz, _, _, _ := cache.Stats(); sz != 20 { - t.Errorf("cache.Size() = %v, expected 20", sz) + if sz := cache.UsedCapacity(); sz != 20 { + t.Errorf("cache.UsedCapacity() = %v, expected 20", sz) } } func TestSetWithOldKeyUpdatesValue(t *testing.T) { - cache := NewLRUCache(100) + cache := NewLRUCache(100, cacheValueSize) emptyValue := &CacheValue{0} key := "key1" cache.Set(key, emptyValue) @@ -135,67 +110,42 @@ func TestSetWithOldKeyUpdatesValue(t *testing.T) { } func TestSetWithOldKeyUpdatesSize(t *testing.T) { - cache := NewLRUCache(100) + cache := NewLRUCache(100, cacheValueSize) emptyValue := &CacheValue{0} key := "key1" cache.Set(key, emptyValue) - if _, sz, _, _, _ := cache.Stats(); sz != 0 { - t.Errorf("cache.Size() = %v, expected %v", sz, 0) + if sz := cache.UsedCapacity(); sz != 0 { + t.Errorf("cache.UsedCapacity() = %v, expected %v", sz, 0) } someValue := &CacheValue{20} cache.Set(key, someValue) expected := int64(someValue.size) - if _, sz, _, _, _ := cache.Stats(); sz != expected { - t.Errorf("cache.Size() = %v, expected %v", sz, expected) + if sz := cache.UsedCapacity(); sz != expected { + t.Errorf("cache.UsedCapacity() = %v, expected %v", sz, expected) } } func TestGetNonExistent(t *testing.T) { - cache := NewLRUCache(100) + cache := NewLRUCache(100, cacheValueSize) if _, ok := cache.Get("notthere"); ok { t.Error("Cache returned a notthere value after no inserts.") } } -func TestPeek(t *testing.T) { - cache := NewLRUCache(2) - val1 := &CacheValue{1} - cache.Set("key1", val1) - val2 := &CacheValue{1} - cache.Set("key2", val2) - // Make key1 the most recent. - cache.Get("key1") - // Peek key2. - if v, ok := cache.Peek("key2"); ok && v.(*CacheValue) != val2 { - t.Errorf("key2 received: %v, want %v", v, val2) - } - // Push key2 out - cache.Set("key3", &CacheValue{1}) - if v, ok := cache.Peek("key2"); ok { - t.Errorf("key2 received: %v, want absent", v) - } -} - func TestDelete(t *testing.T) { - cache := NewLRUCache(100) + cache := NewLRUCache(100, cacheValueSize) value := &CacheValue{1} key := "key" - if cache.Delete(key) { - t.Error("Item unexpectedly already in cache.") - } - + cache.Delete(key) cache.Set(key, value) + cache.Delete(key) - if !cache.Delete(key) { - t.Error("Expected item to be in cache.") - } - - if _, sz, _, _, _ := cache.Stats(); sz != 0 { - t.Errorf("cache.Size() = %v, expected 0", sz) + if sz := cache.UsedCapacity(); sz != 0 { + t.Errorf("cache.UsedCapacity() = %v, expected 0", sz) } if _, ok := cache.Get(key); ok { @@ -204,21 +154,21 @@ func TestDelete(t *testing.T) { } func TestClear(t *testing.T) { - cache := NewLRUCache(100) + cache := NewLRUCache(100, cacheValueSize) value := &CacheValue{1} key := "key" cache.Set(key, value) cache.Clear() - if _, sz, _, _, _ := cache.Stats(); sz != 0 { - t.Errorf("cache.Size() = %v, expected 0 after Clear()", sz) + if sz := cache.UsedCapacity(); sz != 0 { + t.Errorf("cache.UsedCapacity() = %v, expected 0 after Clear()", sz) } } func TestCapacityIsObeyed(t *testing.T) { size := int64(3) - cache := NewLRUCache(100) + cache := NewLRUCache(100, cacheValueSize) cache.SetCapacity(size) value := &CacheValue{1} @@ -226,50 +176,34 @@ func TestCapacityIsObeyed(t *testing.T) { cache.Set("key1", value) cache.Set("key2", value) cache.Set("key3", value) - if _, sz, _, _, _ := cache.Stats(); sz != size { - t.Errorf("cache.Size() = %v, expected %v", sz, size) + if sz := cache.UsedCapacity(); sz != size { + t.Errorf("cache.UsedCapacity() = %v, expected %v", sz, size) } // Insert one more; something should be evicted to make room. cache.Set("key4", value) - _, sz, _, evictions, _ := cache.Stats() + sz, evictions := cache.UsedCapacity(), cache.Evictions() if sz != size { - t.Errorf("post-evict cache.Size() = %v, expected %v", sz, size) + t.Errorf("post-evict cache.UsedCapacity() = %v, expected %v", sz, size) } if evictions != 1 { - t.Errorf("post-evict cache.evictions = %v, expected 1", evictions) - } - - // Check json stats - data := cache.StatsJSON() - m := make(map[string]interface{}) - if err := json.Unmarshal([]byte(data), &m); err != nil { - t.Errorf("cache.StatsJSON() returned bad json data: %v %v", data, err) - } - if m["Size"].(float64) != float64(size) { - t.Errorf("cache.StatsJSON() returned bad size: %v", m) + t.Errorf("post-evict cache.Evictions() = %v, expected 1", evictions) } // Check various other stats - if l := cache.Length(); l != size { - t.Errorf("cache.StatsJSON() returned bad length: %v", l) + if l := cache.Len(); int64(l) != size { + t.Errorf("cache.Len() returned bad length: %v", l) } - if s := cache.Size(); s != size { - t.Errorf("cache.StatsJSON() returned bad size: %v", s) + if s := cache.UsedCapacity(); s != size { + t.Errorf("cache.UsedCapacity() returned bad size: %v", s) } - if c := cache.Capacity(); c != size { - t.Errorf("cache.StatsJSON() returned bad length: %v", c) - } - - // checks StatsJSON on nil - cache = nil - if s := cache.StatsJSON(); s != "{}" { - t.Errorf("cache.StatsJSON() on nil object returned %v", s) + if c := cache.MaxCapacity(); c != size { + t.Errorf("cache.UsedCapacity() returned bad length: %v", c) } } func TestLRUIsEvicted(t *testing.T) { size := int64(3) - cache := NewLRUCache(size) + cache := NewLRUCache(size, cacheValueSize) cache.Set("key1", &CacheValue{1}) cache.Set("key2", &CacheValue{1}) @@ -278,9 +212,7 @@ func TestLRUIsEvicted(t *testing.T) { // Look up the elements. This will rearrange the LRU ordering. cache.Get("key3") - beforeKey2 := time.Now() cache.Get("key2") - afterKey2 := time.Now() cache.Get("key1") // lru: [key1, key2, key3] @@ -292,11 +224,6 @@ func TestLRUIsEvicted(t *testing.T) { t.Error("Least recently used element was not evicted.") } - // Check oldest - if o := cache.Oldest(); o.Before(beforeKey2) || o.After(afterKey2) { - t.Errorf("cache.Oldest returned an unexpected value: got %v, expected a value between %v and %v", o, beforeKey2, afterKey2) - } - if e, want := cache.Evictions(), int64(1); e != want { t.Errorf("evictions: %d, want: %d", e, want) } diff --git a/go/cache/null.go b/go/cache/null.go new file mode 100644 index 00000000000..5ef0f13a8c7 --- /dev/null +++ b/go/cache/null.go @@ -0,0 +1,63 @@ +/* +Copyright 2021 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cache + +// nullCache is a no-op cache that does not store items +type nullCache struct{} + +// Get never returns anything on the nullCache +func (n *nullCache) Get(_ string) (interface{}, bool) { + return nil, false +} + +// Set is a no-op in the nullCache +func (n *nullCache) Set(_ string, _ interface{}) bool { + return false +} + +// ForEach iterates the nullCache, which is always empty +func (n *nullCache) ForEach(_ func(interface{}) bool) {} + +// Delete is a no-op in the nullCache +func (n *nullCache) Delete(_ string) {} + +// Clear is a no-op in the nullCache +func (n *nullCache) Clear() {} + +// Wait is a no-op in the nullcache +func (n *nullCache) Wait() {} + +func (n *nullCache) Len() int { + return 0 +} + +// Capacity returns the capacity of the nullCache, which is always 0 +func (n *nullCache) UsedCapacity() int64 { + return 0 +} + +// Capacity returns the capacity of the nullCache, which is always 0 +func (n *nullCache) MaxCapacity() int64 { + return 0 +} + +// SetCapacity sets the capacity of the null cache, which is a no-op +func (n *nullCache) SetCapacity(_ int64) {} + +func (n *nullCache) Evictions() int64 { + return 0 +} diff --git a/go/cache/perf_test.go b/go/cache/perf_test.go index b5c9a1a8b38..95546f66c06 100644 --- a/go/cache/perf_test.go +++ b/go/cache/perf_test.go @@ -20,15 +20,11 @@ import ( "testing" ) -type MyValue []byte - -func (mv MyValue) Size() int { - return cap(mv) -} - func BenchmarkGet(b *testing.B) { - cache := NewLRUCache(64 * 1024 * 1024) - value := make(MyValue, 1000) + cache := NewLRUCache(64*1024*1024, func(val interface{}) int64 { + return int64(cap(val.([]byte))) + }) + value := make([]byte, 1000) cache.Set("stuff", value) for i := 0; i < b.N; i++ { val, ok := cache.Get("stuff") diff --git a/go/cache/ristretto.go b/go/cache/ristretto.go new file mode 100644 index 00000000000..29eb52fe692 --- /dev/null +++ b/go/cache/ristretto.go @@ -0,0 +1,28 @@ +package cache + +import ( + "vitess.io/vitess/go/cache/ristretto" +) + +var _ Cache = &ristretto.Cache{} + +// NewRistrettoCache returns a Cache implementation based on Ristretto +func NewRistrettoCache(maxEntries, maxCost int64, cost func(interface{}) int64) *ristretto.Cache { + // The TinyLFU paper recommends to allocate 10x times the max entries amount as counters + // for the admission policy; since our caches are small and we're very interested on admission + // accuracy, we're a bit more greedy than 10x + const CounterRatio = 12 + + config := ristretto.Config{ + NumCounters: maxEntries * CounterRatio, + MaxCost: maxCost, + BufferItems: 64, + Metrics: true, + Cost: cost, + } + cache, err := ristretto.NewCache(&config) + if err != nil { + panic(err) + } + return cache +} diff --git a/go/cache/ristretto/bloom/bbloom.go b/go/cache/ristretto/bloom/bbloom.go new file mode 100644 index 00000000000..586adec9cb6 --- /dev/null +++ b/go/cache/ristretto/bloom/bbloom.go @@ -0,0 +1,210 @@ +// The MIT License (MIT) +// Copyright (c) 2014 Andreas Briese, eduToolbox@Bri-C GmbH, Sarstedt + +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package bloom + +import ( + "bytes" + "encoding/json" + "log" + "math" + "unsafe" +) + +// helper +var mask = []uint8{1, 2, 4, 8, 16, 32, 64, 128} + +func getSize(ui64 uint64) (size uint64, exponent uint64) { + if ui64 < uint64(512) { + ui64 = uint64(512) + } + size = uint64(1) + for size < ui64 { + size <<= 1 + exponent++ + } + return size, exponent +} + +func calcSizeByWrongPositives(numEntries, wrongs float64) (uint64, uint64) { + size := -1 * numEntries * math.Log(wrongs) / math.Pow(float64(0.69314718056), 2) + locs := math.Ceil(float64(0.69314718056) * size / numEntries) + return uint64(size), uint64(locs) +} + +// NewBloomFilter returns a new bloomfilter. +func NewBloomFilter(params ...float64) (bloomfilter *Bloom) { + var entries, locs uint64 + if len(params) == 2 { + if params[1] < 1 { + entries, locs = calcSizeByWrongPositives(params[0], params[1]) + } else { + entries, locs = uint64(params[0]), uint64(params[1]) + } + } else { + log.Fatal("usage: New(float64(number_of_entries), float64(number_of_hashlocations))" + + " i.e. New(float64(1000), float64(3)) or New(float64(number_of_entries)," + + " float64(number_of_hashlocations)) i.e. New(float64(1000), float64(0.03))") + } + size, exponent := getSize(entries) + bloomfilter = &Bloom{ + sizeExp: exponent, + size: size - 1, + setLocs: locs, + shift: 64 - exponent, + } + bloomfilter.Size(size) + return bloomfilter +} + +// Bloom filter +type Bloom struct { + bitset []uint64 + ElemNum uint64 + sizeExp uint64 + size uint64 + setLocs uint64 + shift uint64 +} + +// <--- http://www.cse.yorku.ca/~oz/hash.html +// modified Berkeley DB Hash (32bit) +// hash is casted to l, h = 16bit fragments +// func (bl Bloom) absdbm(b *[]byte) (l, h uint64) { +// hash := uint64(len(*b)) +// for _, c := range *b { +// hash = uint64(c) + (hash << 6) + (hash << bl.sizeExp) - hash +// } +// h = hash >> bl.shift +// l = hash << bl.shift >> bl.shift +// return l, h +// } + +// Add adds hash of a key to the bloomfilter. +func (bl *Bloom) Add(hash uint64) { + h := hash >> bl.shift + l := hash << bl.shift >> bl.shift + for i := uint64(0); i < bl.setLocs; i++ { + bl.Set((h + i*l) & bl.size) + bl.ElemNum++ + } +} + +// Has checks if bit(s) for entry hash is/are set, +// returns true if the hash was added to the Bloom Filter. +func (bl Bloom) Has(hash uint64) bool { + h := hash >> bl.shift + l := hash << bl.shift >> bl.shift + for i := uint64(0); i < bl.setLocs; i++ { + if !bl.IsSet((h + i*l) & bl.size) { + return false + } + } + return true +} + +// AddIfNotHas only Adds hash, if it's not present in the bloomfilter. +// Returns true if hash was added. +// Returns false if hash was already registered in the bloomfilter. +func (bl *Bloom) AddIfNotHas(hash uint64) bool { + if bl.Has(hash) { + return false + } + bl.Add(hash) + return true +} + +// TotalSize returns the total size of the bloom filter. +func (bl *Bloom) TotalSize() int { + // The bl struct has 5 members and each one is 8 byte. The bitset is a + // uint64 byte slice. + return len(bl.bitset)*8 + 5*8 +} + +// Size makes Bloom filter with as bitset of size sz. +func (bl *Bloom) Size(sz uint64) { + bl.bitset = make([]uint64, sz>>6) +} + +// Clear resets the Bloom filter. +func (bl *Bloom) Clear() { + for i := range bl.bitset { + bl.bitset[i] = 0 + } +} + +// Set sets the bit[idx] of bitset. +func (bl *Bloom) Set(idx uint64) { + ptr := unsafe.Pointer(uintptr(unsafe.Pointer(&bl.bitset[idx>>6])) + uintptr((idx%64)>>3)) + *(*uint8)(ptr) |= mask[idx%8] +} + +// IsSet checks if bit[idx] of bitset is set, returns true/false. +func (bl *Bloom) IsSet(idx uint64) bool { + ptr := unsafe.Pointer(uintptr(unsafe.Pointer(&bl.bitset[idx>>6])) + uintptr((idx%64)>>3)) + r := ((*(*uint8)(ptr)) >> (idx % 8)) & 1 + return r == 1 +} + +// bloomJSONImExport +// Im/Export structure used by JSONMarshal / JSONUnmarshal +type bloomJSONImExport struct { + FilterSet []byte + SetLocs uint64 +} + +// NewWithBoolset takes a []byte slice and number of locs per entry, +// returns the bloomfilter with a bitset populated according to the input []byte. +func newWithBoolset(bs *[]byte, locs uint64) *Bloom { + bloomfilter := NewBloomFilter(float64(len(*bs)<<3), float64(locs)) + for i, b := range *bs { + *(*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(&bloomfilter.bitset[0])) + uintptr(i))) = b + } + return bloomfilter +} + +// JSONUnmarshal takes JSON-Object (type bloomJSONImExport) as []bytes +// returns bloom32 / bloom64 object. +func JSONUnmarshal(dbData []byte) (*Bloom, error) { + bloomImEx := bloomJSONImExport{} + if err := json.Unmarshal(dbData, &bloomImEx); err != nil { + return nil, err + } + buf := bytes.NewBuffer(bloomImEx.FilterSet) + bs := buf.Bytes() + bf := newWithBoolset(&bs, bloomImEx.SetLocs) + return bf, nil +} + +// JSONMarshal returns JSON-object (type bloomJSONImExport) as []byte. +func (bl Bloom) JSONMarshal() []byte { + bloomImEx := bloomJSONImExport{} + bloomImEx.SetLocs = bl.setLocs + bloomImEx.FilterSet = make([]byte, len(bl.bitset)<<3) + for i := range bloomImEx.FilterSet { + bloomImEx.FilterSet[i] = *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&bl.bitset[0])) + + uintptr(i))) + } + data, err := json.Marshal(bloomImEx) + if err != nil { + log.Fatal("json.Marshal failed: ", err) + } + return data +} diff --git a/go/cache/ristretto/bloom/bbloom_test.go b/go/cache/ristretto/bloom/bbloom_test.go new file mode 100644 index 00000000000..ac0cb9c9104 --- /dev/null +++ b/go/cache/ristretto/bloom/bbloom_test.go @@ -0,0 +1,114 @@ +package bloom + +import ( + "crypto/rand" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "vitess.io/vitess/go/hack" +) + +var ( + wordlist1 [][]byte + n = 1 << 16 + bf *Bloom +) + +func TestMain(m *testing.M) { + wordlist1 = make([][]byte, n) + for i := range wordlist1 { + b := make([]byte, 32) + rand.Read(b) + wordlist1[i] = b + } + fmt.Println("\n###############\nbbloom_test.go") + fmt.Print("Benchmarks relate to 2**16 OP. --> output/65536 op/ns\n###############\n\n") + + os.Exit(m.Run()) +} + +func TestM_NumberOfWrongs(t *testing.T) { + bf = NewBloomFilter(float64(n*10), float64(7)) + + cnt := 0 + for i := range wordlist1 { + hash := hack.RuntimeMemhash(wordlist1[i], 0) + if !bf.AddIfNotHas(hash) { + cnt++ + } + } + fmt.Printf("Bloomfilter New(7* 2**16, 7) (-> size=%v bit): \n Check for 'false positives': %v wrong positive 'Has' results on 2**16 entries => %v %%\n", len(bf.bitset)<<6, cnt, float64(cnt)/float64(n)) + +} + +func TestM_JSON(t *testing.T) { + const shallBe = int(1 << 16) + + bf = NewBloomFilter(float64(n*10), float64(7)) + + cnt := 0 + for i := range wordlist1 { + hash := hack.RuntimeMemhash(wordlist1[i], 0) + if !bf.AddIfNotHas(hash) { + cnt++ + } + } + + jsonm := bf.JSONMarshal() + + // create new bloomfilter from bloomfilter's JSON representation + bf2, err := JSONUnmarshal(jsonm) + require.NoError(t, err) + + cnt2 := 0 + for i := range wordlist1 { + hash := hack.RuntimeMemhash(wordlist1[i], 0) + if !bf2.AddIfNotHas(hash) { + cnt2++ + } + } + require.Equal(t, shallBe, cnt2) +} + +func BenchmarkM_New(b *testing.B) { + for r := 0; r < b.N; r++ { + _ = NewBloomFilter(float64(n*10), float64(7)) + } +} + +func BenchmarkM_Clear(b *testing.B) { + bf = NewBloomFilter(float64(n*10), float64(7)) + for i := range wordlist1 { + hash := hack.RuntimeMemhash(wordlist1[i], 0) + bf.Add(hash) + } + b.ResetTimer() + for r := 0; r < b.N; r++ { + bf.Clear() + } +} + +func BenchmarkM_Add(b *testing.B) { + bf = NewBloomFilter(float64(n*10), float64(7)) + b.ResetTimer() + for r := 0; r < b.N; r++ { + for i := range wordlist1 { + hash := hack.RuntimeMemhash(wordlist1[i], 0) + bf.Add(hash) + } + } + +} + +func BenchmarkM_Has(b *testing.B) { + b.ResetTimer() + for r := 0; r < b.N; r++ { + for i := range wordlist1 { + hash := hack.RuntimeMemhash(wordlist1[i], 0) + bf.Has(hash) + } + } +} diff --git a/go/cache/ristretto/cache.go b/go/cache/ristretto/cache.go new file mode 100644 index 00000000000..62f086f69c2 --- /dev/null +++ b/go/cache/ristretto/cache.go @@ -0,0 +1,676 @@ +/* + * Copyright 2019 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package ristretto is a fast, fixed size, in-memory cache with a dual focus on +// throughput and hit ratio performance. You can easily add Ristretto to an +// existing system and keep the most valuable data where you need it. +package ristretto + +import ( + "bytes" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + "unsafe" + + "vitess.io/vitess/go/hack" +) + +var ( + // TODO: find the optimal value for this or make it configurable + setBufSize = 32 * 1024 +) + +func defaultStringHash(key string) (uint64, uint64) { + const Seed1 = uint64(0x1122334455667788) + const Seed2 = uint64(0x8877665544332211) + return hack.RuntimeStrhash(key, Seed1), hack.RuntimeStrhash(key, Seed2) +} + +type itemCallback func(*Item) + +const itemSize = int64(unsafe.Sizeof(storeItem{})) + +// Cache is a thread-safe implementation of a hashmap with a TinyLFU admission +// policy and a Sampled LFU eviction policy. You can use the same Cache instance +// from as many goroutines as you want. +type Cache struct { + // store is the central concurrent hashmap where key-value items are stored. + store store + // policy determines what gets let in to the cache and what gets kicked out. + policy policy + // getBuf is a custom ring buffer implementation that gets pushed to when + // keys are read. + getBuf *ringBuffer + // setBuf is a buffer allowing us to batch/drop Sets during times of high + // contention. + setBuf chan *Item + // onEvict is called for item evictions. + onEvict itemCallback + // onReject is called when an item is rejected via admission policy. + onReject itemCallback + // onExit is called whenever a value goes out of scope from the cache. + onExit func(interface{}) + // KeyToHash function is used to customize the key hashing algorithm. + // Each key will be hashed using the provided function. If keyToHash value + // is not set, the default keyToHash function is used. + keyToHash func(string) (uint64, uint64) + // stop is used to stop the processItems goroutine. + stop chan struct{} + // indicates whether cache is closed. + isClosed bool + // cost calculates cost from a value. + cost func(value interface{}) int64 + // ignoreInternalCost dictates whether to ignore the cost of internally storing + // the item in the cost calculation. + ignoreInternalCost bool + // Metrics contains a running log of important statistics like hits, misses, + // and dropped items. + Metrics *Metrics +} + +// Config is passed to NewCache for creating new Cache instances. +type Config struct { + // NumCounters determines the number of counters (keys) to keep that hold + // access frequency information. It's generally a good idea to have more + // counters than the max cache capacity, as this will improve eviction + // accuracy and subsequent hit ratios. + // + // For example, if you expect your cache to hold 1,000,000 items when full, + // NumCounters should be 10,000,000 (10x). Each counter takes up 4 bits, so + // keeping 10,000,000 counters would require 5MB of memory. + NumCounters int64 + // MaxCost can be considered as the cache capacity, in whatever units you + // choose to use. + // + // For example, if you want the cache to have a max capacity of 100MB, you + // would set MaxCost to 100,000,000 and pass an item's number of bytes as + // the `cost` parameter for calls to Set. If new items are accepted, the + // eviction process will take care of making room for the new item and not + // overflowing the MaxCost value. + MaxCost int64 + // BufferItems determines the size of Get buffers. + // + // Unless you have a rare use case, using `64` as the BufferItems value + // results in good performance. + BufferItems int64 + // Metrics determines whether cache statistics are kept during the cache's + // lifetime. There *is* some overhead to keeping statistics, so you should + // only set this flag to true when testing or throughput performance isn't a + // major factor. + Metrics bool + // OnEvict is called for every eviction and passes the hashed key, value, + // and cost to the function. + OnEvict func(item *Item) + // OnReject is called for every rejection done via the policy. + OnReject func(item *Item) + // OnExit is called whenever a value is removed from cache. This can be + // used to do manual memory deallocation. Would also be called on eviction + // and rejection of the value. + OnExit func(val interface{}) + // KeyToHash function is used to customize the key hashing algorithm. + // Each key will be hashed using the provided function. If keyToHash value + // is not set, the default keyToHash function is used. + KeyToHash func(string) (uint64, uint64) + // Cost evaluates a value and outputs a corresponding cost. This function + // is ran after Set is called for a new item or an item update with a cost + // param of 0. + Cost func(value interface{}) int64 + // IgnoreInternalCost set to true indicates to the cache that the cost of + // internally storing the value should be ignored. This is useful when the + // cost passed to set is not using bytes as units. Keep in mind that setting + // this to true will increase the memory usage. + IgnoreInternalCost bool +} + +type itemFlag byte + +const ( + itemNew itemFlag = iota + itemDelete + itemUpdate +) + +// Item is passed to setBuf so items can eventually be added to the cache. +type Item struct { + flag itemFlag + Key uint64 + Conflict uint64 + Value interface{} + Cost int64 + wg *sync.WaitGroup +} + +// NewCache returns a new Cache instance and any configuration errors, if any. +func NewCache(config *Config) (*Cache, error) { + switch { + case config.NumCounters == 0: + return nil, errors.New("NumCounters can't be zero") + case config.MaxCost == 0: + return nil, errors.New("Capacity can't be zero") + case config.BufferItems == 0: + return nil, errors.New("BufferItems can't be zero") + } + policy := newPolicy(config.NumCounters, config.MaxCost) + cache := &Cache{ + store: newStore(), + policy: policy, + getBuf: newRingBuffer(policy, config.BufferItems), + setBuf: make(chan *Item, setBufSize), + keyToHash: config.KeyToHash, + stop: make(chan struct{}), + cost: config.Cost, + ignoreInternalCost: config.IgnoreInternalCost, + } + cache.onExit = func(val interface{}) { + if config.OnExit != nil && val != nil { + config.OnExit(val) + } + } + cache.onEvict = func(item *Item) { + if config.OnEvict != nil { + config.OnEvict(item) + } + cache.onExit(item.Value) + } + cache.onReject = func(item *Item) { + if config.OnReject != nil { + config.OnReject(item) + } + cache.onExit(item.Value) + } + if cache.keyToHash == nil { + cache.keyToHash = defaultStringHash + } + if config.Metrics { + cache.collectMetrics() + } + // NOTE: benchmarks seem to show that performance decreases the more + // goroutines we have running cache.processItems(), so 1 should + // usually be sufficient + go cache.processItems() + return cache, nil +} + +// Wait blocks until all the current cache operations have been processed in the background +func (c *Cache) Wait() { + if c == nil || c.isClosed { + return + } + wg := &sync.WaitGroup{} + wg.Add(1) + c.setBuf <- &Item{wg: wg} + wg.Wait() +} + +// Get returns the value (if any) and a boolean representing whether the +// value was found or not. The value can be nil and the boolean can be true at +// the same time. +func (c *Cache) Get(key string) (interface{}, bool) { + if c == nil || c.isClosed { + return nil, false + } + keyHash, conflictHash := c.keyToHash(key) + c.getBuf.Push(keyHash) + value, ok := c.store.Get(keyHash, conflictHash) + if ok { + c.Metrics.add(hit, keyHash, 1) + } else { + c.Metrics.add(miss, keyHash, 1) + } + return value, ok +} + +// Set attempts to add the key-value item to the cache. If it returns false, +// then the Set was dropped and the key-value item isn't added to the cache. If +// it returns true, there's still a chance it could be dropped by the policy if +// its determined that the key-value item isn't worth keeping, but otherwise the +// item will be added and other items will be evicted in order to make room. +// +// The cost of the entry will be evaluated lazily by the cache's Cost function. +func (c *Cache) Set(key string, value interface{}) bool { + return c.SetWithCost(key, value, 0) +} + +// SetWithCost works like Set but adds a key-value pair to the cache with a specific +// cost. The built-in Cost function will not be called to evaluate the object's cost +// and instead the given value will be used. +func (c *Cache) SetWithCost(key string, value interface{}, cost int64) bool { + if c == nil || c.isClosed { + return false + } + + keyHash, conflictHash := c.keyToHash(key) + i := &Item{ + flag: itemNew, + Key: keyHash, + Conflict: conflictHash, + Value: value, + Cost: cost, + } + // cost is eventually updated. The expiration must also be immediately updated + // to prevent items from being prematurely removed from the map. + if prev, ok := c.store.Update(i); ok { + c.onExit(prev) + i.flag = itemUpdate + } + // Attempt to send item to policy. + select { + case c.setBuf <- i: + return true + default: + if i.flag == itemUpdate { + // Return true if this was an update operation since we've already + // updated the store. For all the other operations (set/delete), we + // return false which means the item was not inserted. + return true + } + c.Metrics.add(dropSets, keyHash, 1) + return false + } +} + +// Delete deletes the key-value item from the cache if it exists. +func (c *Cache) Delete(key string) { + if c == nil || c.isClosed { + return + } + keyHash, conflictHash := c.keyToHash(key) + // Delete immediately. + _, prev := c.store.Del(keyHash, conflictHash) + c.onExit(prev) + // If we've set an item, it would be applied slightly later. + // So we must push the same item to `setBuf` with the deletion flag. + // This ensures that if a set is followed by a delete, it will be + // applied in the correct order. + c.setBuf <- &Item{ + flag: itemDelete, + Key: keyHash, + Conflict: conflictHash, + } +} + +// Close stops all goroutines and closes all channels. +func (c *Cache) Close() { + if c == nil || c.isClosed { + return + } + c.Clear() + + // Block until processItems goroutine is returned. + c.stop <- struct{}{} + close(c.stop) + close(c.setBuf) + c.policy.Close() + c.isClosed = true +} + +// Clear empties the hashmap and zeroes all policy counters. Note that this is +// not an atomic operation (but that shouldn't be a problem as it's assumed that +// Set/Get calls won't be occurring until after this). +func (c *Cache) Clear() { + if c == nil || c.isClosed { + return + } + // Block until processItems goroutine is returned. + c.stop <- struct{}{} + + // Clear out the setBuf channel. +loop: + for { + select { + case i := <-c.setBuf: + if i.flag != itemUpdate { + // In itemUpdate, the value is already set in the store. So, no need to call + // onEvict here. + c.onEvict(i) + } + default: + break loop + } + } + + // Clear value hashmap and policy data. + c.policy.Clear() + c.store.Clear(c.onEvict) + // Only reset metrics if they're enabled. + if c.Metrics != nil { + c.Metrics.Clear() + } + // Restart processItems goroutine. + go c.processItems() +} + +// Len returns the size of the cache (in entries) +func (c *Cache) Len() int { + if c == nil { + return 0 + } + return c.store.Len() +} + +// UsedCapacity returns the size of the cache (in bytes) +func (c *Cache) UsedCapacity() int64 { + if c == nil { + return 0 + } + return c.policy.Used() +} + +// MaxCapacity returns the max cost of the cache (in bytes) +func (c *Cache) MaxCapacity() int64 { + if c == nil { + return 0 + } + return c.policy.MaxCost() +} + +// SetCapacity updates the maxCost of an existing cache. +func (c *Cache) SetCapacity(maxCost int64) { + if c == nil { + return + } + c.policy.UpdateMaxCost(maxCost) +} + +// Evictions returns the number of evictions +func (c *Cache) Evictions() int64 { + // TODO + if c == nil || c.Metrics == nil { + return 0 + } + return int64(c.Metrics.KeysEvicted()) +} + +// ForEach yields all the values currently stored in the cache to the given callback. +// The callback may return `false` to stop the iteration early. +func (c *Cache) ForEach(forEach func(interface{}) bool) { + if c == nil { + return + } + c.store.ForEach(forEach) +} + +// processItems is ran by goroutines processing the Set buffer. +func (c *Cache) processItems() { + startTs := make(map[uint64]time.Time) + numToKeep := 100000 // TODO: Make this configurable via options. + + trackAdmission := func(key uint64) { + if c.Metrics == nil { + return + } + startTs[key] = time.Now() + if len(startTs) > numToKeep { + for k := range startTs { + if len(startTs) <= numToKeep { + break + } + delete(startTs, k) + } + } + } + onEvict := func(i *Item) { + delete(startTs, i.Key) + if c.onEvict != nil { + c.onEvict(i) + } + } + + for { + select { + case i := <-c.setBuf: + if i.wg != nil { + i.wg.Done() + continue + } + // Calculate item cost value if new or update. + if i.Cost == 0 && c.cost != nil && i.flag != itemDelete { + i.Cost = c.cost(i.Value) + } + if !c.ignoreInternalCost { + // Add the cost of internally storing the object. + i.Cost += itemSize + } + + switch i.flag { + case itemNew: + victims, added := c.policy.Add(i.Key, i.Cost) + if added { + c.store.Set(i) + c.Metrics.add(keyAdd, i.Key, 1) + trackAdmission(i.Key) + } else { + c.onReject(i) + } + for _, victim := range victims { + victim.Conflict, victim.Value = c.store.Del(victim.Key, 0) + onEvict(victim) + } + + case itemUpdate: + c.policy.Update(i.Key, i.Cost) + + case itemDelete: + c.policy.Del(i.Key) // Deals with metrics updates. + _, val := c.store.Del(i.Key, i.Conflict) + c.onExit(val) + } + case <-c.stop: + return + } + } +} + +// collectMetrics just creates a new *Metrics instance and adds the pointers +// to the cache and policy instances. +func (c *Cache) collectMetrics() { + c.Metrics = newMetrics() + c.policy.CollectMetrics(c.Metrics) +} + +type metricType int + +const ( + // The following 2 keep track of hits and misses. + hit = iota + miss + // The following 3 keep track of number of keys added, updated and evicted. + keyAdd + keyUpdate + keyEvict + // The following 2 keep track of cost of keys added and evicted. + costAdd + costEvict + // The following keep track of how many sets were dropped or rejected later. + dropSets + rejectSets + // The following 2 keep track of how many gets were kept and dropped on the + // floor. + dropGets + keepGets + // This should be the final enum. Other enums should be set before this. + doNotUse +) + +func stringFor(t metricType) string { + switch t { + case hit: + return "hit" + case miss: + return "miss" + case keyAdd: + return "keys-added" + case keyUpdate: + return "keys-updated" + case keyEvict: + return "keys-evicted" + case costAdd: + return "cost-added" + case costEvict: + return "cost-evicted" + case dropSets: + return "sets-dropped" + case rejectSets: + return "sets-rejected" // by policy. + case dropGets: + return "gets-dropped" + case keepGets: + return "gets-kept" + default: + return "unidentified" + } +} + +// Metrics is a snapshot of performance statistics for the lifetime of a cache instance. +type Metrics struct { + all [doNotUse][]*uint64 +} + +func newMetrics() *Metrics { + s := &Metrics{} + for i := 0; i < doNotUse; i++ { + s.all[i] = make([]*uint64, 256) + slice := s.all[i] + for j := range slice { + slice[j] = new(uint64) + } + } + return s +} + +func (p *Metrics) add(t metricType, hash, delta uint64) { + if p == nil { + return + } + valp := p.all[t] + // Avoid false sharing by padding at least 64 bytes of space between two + // atomic counters which would be incremented. + idx := (hash % 25) * 10 + atomic.AddUint64(valp[idx], delta) +} + +func (p *Metrics) get(t metricType) uint64 { + if p == nil { + return 0 + } + valp := p.all[t] + var total uint64 + for i := range valp { + total += atomic.LoadUint64(valp[i]) + } + return total +} + +// Hits is the number of Get calls where a value was found for the corresponding key. +func (p *Metrics) Hits() uint64 { + return p.get(hit) +} + +// Misses is the number of Get calls where a value was not found for the corresponding key. +func (p *Metrics) Misses() uint64 { + return p.get(miss) +} + +// KeysAdded is the total number of Set calls where a new key-value item was added. +func (p *Metrics) KeysAdded() uint64 { + return p.get(keyAdd) +} + +// KeysUpdated is the total number of Set calls where the value was updated. +func (p *Metrics) KeysUpdated() uint64 { + return p.get(keyUpdate) +} + +// KeysEvicted is the total number of keys evicted. +func (p *Metrics) KeysEvicted() uint64 { + return p.get(keyEvict) +} + +// CostAdded is the sum of costs that have been added (successful Set calls). +func (p *Metrics) CostAdded() uint64 { + return p.get(costAdd) +} + +// CostEvicted is the sum of all costs that have been evicted. +func (p *Metrics) CostEvicted() uint64 { + return p.get(costEvict) +} + +// SetsDropped is the number of Set calls that don't make it into internal +// buffers (due to contention or some other reason). +func (p *Metrics) SetsDropped() uint64 { + return p.get(dropSets) +} + +// SetsRejected is the number of Set calls rejected by the policy (TinyLFU). +func (p *Metrics) SetsRejected() uint64 { + return p.get(rejectSets) +} + +// GetsDropped is the number of Get counter increments that are dropped +// internally. +func (p *Metrics) GetsDropped() uint64 { + return p.get(dropGets) +} + +// GetsKept is the number of Get counter increments that are kept. +func (p *Metrics) GetsKept() uint64 { + return p.get(keepGets) +} + +// Ratio is the number of Hits over all accesses (Hits + Misses). This is the +// percentage of successful Get calls. +func (p *Metrics) Ratio() float64 { + if p == nil { + return 0.0 + } + hits, misses := p.get(hit), p.get(miss) + if hits == 0 && misses == 0 { + return 0.0 + } + return float64(hits) / float64(hits+misses) +} + +// Clear resets all the metrics. +func (p *Metrics) Clear() { + if p == nil { + return + } + for i := 0; i < doNotUse; i++ { + for j := range p.all[i] { + atomic.StoreUint64(p.all[i][j], 0) + } + } +} + +// String returns a string representation of the metrics. +func (p *Metrics) String() string { + if p == nil { + return "" + } + var buf bytes.Buffer + for i := 0; i < doNotUse; i++ { + t := metricType(i) + fmt.Fprintf(&buf, "%s: %d ", stringFor(t), p.get(t)) + } + fmt.Fprintf(&buf, "gets-total: %d ", p.get(hit)+p.get(miss)) + fmt.Fprintf(&buf, "hit-ratio: %.2f", p.Ratio()) + return buf.String() +} diff --git a/go/cache/ristretto/cache_test.go b/go/cache/ristretto/cache_test.go new file mode 100644 index 00000000000..a070c6f785a --- /dev/null +++ b/go/cache/ristretto/cache_test.go @@ -0,0 +1,688 @@ +/* + * Copyright 2019 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ristretto + +import ( + "fmt" + "math/rand" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +var wait = time.Millisecond * 10 + +func TestCacheKeyToHash(t *testing.T) { + keyToHashCount := 0 + c, err := NewCache(&Config{ + NumCounters: 10, + MaxCost: 1000, + BufferItems: 64, + IgnoreInternalCost: true, + KeyToHash: func(key string) (uint64, uint64) { + keyToHashCount++ + return defaultStringHash(key) + }, + }) + require.NoError(t, err) + if c.SetWithCost("1", 1, 1) { + time.Sleep(wait) + val, ok := c.Get("1") + require.True(t, ok) + require.NotNil(t, val) + c.Delete("1") + } + require.Equal(t, 3, keyToHashCount) +} + +func TestCacheMaxCost(t *testing.T) { + charset := "abcdefghijklmnopqrstuvwxyz0123456789" + key := func() string { + k := make([]byte, 2) + for i := range k { + k[i] = charset[rand.Intn(len(charset))] + } + return string(k) + } + c, err := NewCache(&Config{ + NumCounters: 12960, // 36^2 * 10 + MaxCost: 1e6, // 1mb + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + stop := make(chan struct{}, 8) + for i := 0; i < 8; i++ { + go func() { + for { + select { + case <-stop: + return + default: + time.Sleep(time.Millisecond) + + k := key() + if _, ok := c.Get(k); !ok { + val := "" + if rand.Intn(100) < 10 { + val = "test" + } else { + val = strings.Repeat("a", 1000) + } + c.SetWithCost(key(), val, int64(2+len(val))) + } + } + } + }() + } + for i := 0; i < 20; i++ { + time.Sleep(time.Second) + cacheCost := c.Metrics.CostAdded() - c.Metrics.CostEvicted() + t.Logf("total cache cost: %d\n", cacheCost) + require.True(t, float64(cacheCost) <= float64(1e6*1.05)) + } + for i := 0; i < 8; i++ { + stop <- struct{}{} + } +} + +func TestUpdateMaxCost(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 10, + MaxCost: 10, + BufferItems: 64, + }) + require.NoError(t, err) + require.Equal(t, int64(10), c.MaxCapacity()) + require.True(t, c.SetWithCost("1", 1, 1)) + time.Sleep(wait) + _, ok := c.Get("1") + // Set is rejected because the cost of the entry is too high + // when accounting for the internal cost of storing the entry. + require.False(t, ok) + + // Update the max cost of the cache and retry. + c.SetCapacity(1000) + require.Equal(t, int64(1000), c.MaxCapacity()) + require.True(t, c.SetWithCost("1", 1, 1)) + time.Sleep(wait) + val, ok := c.Get("1") + require.True(t, ok) + require.NotNil(t, val) + c.Delete("1") +} + +func TestNewCache(t *testing.T) { + _, err := NewCache(&Config{ + NumCounters: 0, + }) + require.Error(t, err) + + _, err = NewCache(&Config{ + NumCounters: 100, + MaxCost: 0, + }) + require.Error(t, err) + + _, err = NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 0, + }) + require.Error(t, err) + + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + require.NotNil(t, c) +} + +func TestNilCache(t *testing.T) { + var c *Cache + val, ok := c.Get("1") + require.False(t, ok) + require.Nil(t, val) + + require.False(t, c.SetWithCost("1", 1, 1)) + c.Delete("1") + c.Clear() + c.Close() +} + +func TestMultipleClose(t *testing.T) { + var c *Cache + c.Close() + + var err error + c, err = NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + c.Close() + c.Close() +} + +func TestSetAfterClose(t *testing.T) { + c, err := newTestCache() + require.NoError(t, err) + require.NotNil(t, c) + + c.Close() + require.False(t, c.SetWithCost("1", 1, 1)) +} + +func TestClearAfterClose(t *testing.T) { + c, err := newTestCache() + require.NoError(t, err) + require.NotNil(t, c) + + c.Close() + c.Clear() +} + +func TestGetAfterClose(t *testing.T) { + c, err := newTestCache() + require.NoError(t, err) + require.NotNil(t, c) + + require.True(t, c.SetWithCost("1", 1, 1)) + c.Close() + + _, ok := c.Get("2") + require.False(t, ok) +} + +func TestDelAfterClose(t *testing.T) { + c, err := newTestCache() + require.NoError(t, err) + require.NotNil(t, c) + + require.True(t, c.SetWithCost("1", 1, 1)) + c.Close() + + c.Delete("1") +} + +func TestCacheProcessItems(t *testing.T) { + m := &sync.Mutex{} + evicted := make(map[uint64]struct{}) + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + IgnoreInternalCost: true, + Cost: func(value interface{}) int64 { + return int64(value.(int)) + }, + OnEvict: func(item *Item) { + m.Lock() + defer m.Unlock() + evicted[item.Key] = struct{}{} + }, + }) + require.NoError(t, err) + + var key uint64 + var conflict uint64 + + key, conflict = defaultStringHash("1") + c.setBuf <- &Item{ + flag: itemNew, + Key: key, + Conflict: conflict, + Value: 1, + Cost: 0, + } + time.Sleep(wait) + require.True(t, c.policy.Has(key)) + require.Equal(t, int64(1), c.policy.Cost(key)) + + key, conflict = defaultStringHash("1") + c.setBuf <- &Item{ + flag: itemUpdate, + Key: key, + Conflict: conflict, + Value: 2, + Cost: 0, + } + time.Sleep(wait) + require.Equal(t, int64(2), c.policy.Cost(key)) + + key, conflict = defaultStringHash("1") + c.setBuf <- &Item{ + flag: itemDelete, + Key: key, + Conflict: conflict, + } + time.Sleep(wait) + key, conflict = defaultStringHash("1") + val, ok := c.store.Get(key, conflict) + require.False(t, ok) + require.Nil(t, val) + require.False(t, c.policy.Has(1)) + + key, conflict = defaultStringHash("2") + c.setBuf <- &Item{ + flag: itemNew, + Key: key, + Conflict: conflict, + Value: 2, + Cost: 3, + } + key, conflict = defaultStringHash("3") + c.setBuf <- &Item{ + flag: itemNew, + Key: key, + Conflict: conflict, + Value: 3, + Cost: 3, + } + key, conflict = defaultStringHash("4") + c.setBuf <- &Item{ + flag: itemNew, + Key: key, + Conflict: conflict, + Value: 3, + Cost: 3, + } + key, conflict = defaultStringHash("5") + c.setBuf <- &Item{ + flag: itemNew, + Key: key, + Conflict: conflict, + Value: 3, + Cost: 5, + } + time.Sleep(wait) + m.Lock() + require.NotEqual(t, 0, len(evicted)) + m.Unlock() + + defer func() { + require.NotNil(t, recover()) + }() + c.Close() + c.setBuf <- &Item{flag: itemNew} +} + +func TestCacheGet(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + IgnoreInternalCost: true, + Metrics: true, + }) + require.NoError(t, err) + + key, conflict := defaultStringHash("1") + i := Item{ + Key: key, + Conflict: conflict, + Value: 1, + } + c.store.Set(&i) + val, ok := c.Get("1") + require.True(t, ok) + require.NotNil(t, val) + + val, ok = c.Get("2") + require.False(t, ok) + require.Nil(t, val) + + // 0.5 and not 1.0 because we tried Getting each item twice + require.Equal(t, 0.5, c.Metrics.Ratio()) + + c = nil + val, ok = c.Get("0") + require.False(t, ok) + require.Nil(t, val) +} + +// retrySet calls SetWithCost until the item is accepted by the cache. +func retrySet(t *testing.T, c *Cache, key string, value int, cost int64) { + for { + if set := c.SetWithCost(key, value, cost); !set { + time.Sleep(wait) + continue + } + + time.Sleep(wait) + val, ok := c.Get(key) + require.True(t, ok) + require.NotNil(t, val) + require.Equal(t, value, val.(int)) + return + } +} + +func TestCacheSet(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + IgnoreInternalCost: true, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + + retrySet(t, c, "1", 1, 1) + + c.SetWithCost("1", 2, 2) + val, ok := c.store.Get(defaultStringHash("1")) + require.True(t, ok) + require.Equal(t, 2, val.(int)) + + c.stop <- struct{}{} + for i := 0; i < setBufSize; i++ { + key, conflict := defaultStringHash("1") + c.setBuf <- &Item{ + flag: itemUpdate, + Key: key, + Conflict: conflict, + Value: 1, + Cost: 1, + } + } + require.False(t, c.SetWithCost("2", 2, 1)) + require.Equal(t, uint64(1), c.Metrics.SetsDropped()) + close(c.setBuf) + close(c.stop) + + c = nil + require.False(t, c.SetWithCost("1", 1, 1)) +} + +func TestCacheInternalCost(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + + // Get should return false because the cache's cost is too small to store the item + // when accounting for the internal cost. + c.SetWithCost("1", 1, 1) + time.Sleep(wait) + _, ok := c.Get("1") + require.False(t, ok) +} + +func TestCacheDel(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + }) + require.NoError(t, err) + + c.SetWithCost("1", 1, 1) + c.Delete("1") + // The deletes and sets are pushed through the setbuf. It might be possible + // that the delete is not processed before the following get is called. So + // wait for a millisecond for things to be processed. + time.Sleep(time.Millisecond) + val, ok := c.Get("1") + require.False(t, ok) + require.Nil(t, val) + + c = nil + defer func() { + require.Nil(t, recover()) + }() + c.Delete("1") +} + +func TestCacheClear(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + IgnoreInternalCost: true, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + + for i := 0; i < 10; i++ { + c.SetWithCost(strconv.Itoa(i), i, 1) + } + time.Sleep(wait) + require.Equal(t, uint64(10), c.Metrics.KeysAdded()) + + c.Clear() + require.Equal(t, uint64(0), c.Metrics.KeysAdded()) + + for i := 0; i < 10; i++ { + val, ok := c.Get(strconv.Itoa(i)) + require.False(t, ok) + require.Nil(t, val) + } +} + +func TestCacheMetrics(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + IgnoreInternalCost: true, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + + for i := 0; i < 10; i++ { + c.SetWithCost(strconv.Itoa(i), i, 1) + } + time.Sleep(wait) + m := c.Metrics + require.Equal(t, uint64(10), m.KeysAdded()) +} + +func TestMetrics(t *testing.T) { + newMetrics() +} + +func TestNilMetrics(t *testing.T) { + var m *Metrics + for _, f := range []func() uint64{ + m.Hits, + m.Misses, + m.KeysAdded, + m.KeysEvicted, + m.CostEvicted, + m.SetsDropped, + m.SetsRejected, + m.GetsDropped, + m.GetsKept, + } { + require.Equal(t, uint64(0), f()) + } +} + +func TestMetricsAddGet(t *testing.T) { + m := newMetrics() + m.add(hit, 1, 1) + m.add(hit, 2, 2) + m.add(hit, 3, 3) + require.Equal(t, uint64(6), m.Hits()) + + m = nil + m.add(hit, 1, 1) + require.Equal(t, uint64(0), m.Hits()) +} + +func TestMetricsRatio(t *testing.T) { + m := newMetrics() + require.Equal(t, float64(0), m.Ratio()) + + m.add(hit, 1, 1) + m.add(hit, 2, 2) + m.add(miss, 1, 1) + m.add(miss, 2, 2) + require.Equal(t, 0.5, m.Ratio()) + + m = nil + require.Equal(t, float64(0), m.Ratio()) +} + +func TestMetricsString(t *testing.T) { + m := newMetrics() + m.add(hit, 1, 1) + m.add(miss, 1, 1) + m.add(keyAdd, 1, 1) + m.add(keyUpdate, 1, 1) + m.add(keyEvict, 1, 1) + m.add(costAdd, 1, 1) + m.add(costEvict, 1, 1) + m.add(dropSets, 1, 1) + m.add(rejectSets, 1, 1) + m.add(dropGets, 1, 1) + m.add(keepGets, 1, 1) + require.Equal(t, uint64(1), m.Hits()) + require.Equal(t, uint64(1), m.Misses()) + require.Equal(t, 0.5, m.Ratio()) + require.Equal(t, uint64(1), m.KeysAdded()) + require.Equal(t, uint64(1), m.KeysUpdated()) + require.Equal(t, uint64(1), m.KeysEvicted()) + require.Equal(t, uint64(1), m.CostAdded()) + require.Equal(t, uint64(1), m.CostEvicted()) + require.Equal(t, uint64(1), m.SetsDropped()) + require.Equal(t, uint64(1), m.SetsRejected()) + require.Equal(t, uint64(1), m.GetsDropped()) + require.Equal(t, uint64(1), m.GetsKept()) + + require.NotEqual(t, 0, len(m.String())) + + m = nil + require.Equal(t, 0, len(m.String())) + + require.Equal(t, "unidentified", stringFor(doNotUse)) +} + +func TestCacheMetricsClear(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + + c.SetWithCost("1", 1, 1) + stop := make(chan struct{}) + go func() { + for { + select { + case <-stop: + return + default: + c.Get("1") + } + } + }() + time.Sleep(wait) + c.Clear() + stop <- struct{}{} + c.Metrics = nil + c.Metrics.Clear() +} + +// Regression test for bug https://github.com/dgraph-io/ristretto/issues/167 +func TestDropUpdates(t *testing.T) { + originalSetBugSize := setBufSize + defer func() { setBufSize = originalSetBugSize }() + + test := func() { + // dropppedMap stores the items dropped from the cache. + droppedMap := make(map[int]struct{}) + lastEvictedSet := int64(-1) + + var err error + handler := func(_ interface{}, value interface{}) { + v := value.(string) + lastEvictedSet, err = strconv.ParseInt(string(v), 10, 32) + require.NoError(t, err) + + _, ok := droppedMap[int(lastEvictedSet)] + if ok { + panic(fmt.Sprintf("val = %+v was dropped but it got evicted. Dropped items: %+v\n", + lastEvictedSet, droppedMap)) + } + } + + // This is important. The race condition shows up only when the setBuf + // is full and that's why we reduce the buf size here. The test will + // try to fill up the setbuf to it's capacity and then perform an + // update on a key. + setBufSize = 10 + + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + Metrics: true, + OnEvict: func(item *Item) { + if item.Value != nil { + handler(nil, item.Value) + } + }, + }) + require.NoError(t, err) + + for i := 0; i < 5*setBufSize; i++ { + v := fmt.Sprintf("%0100d", i) + // We're updating the same key. + if !c.SetWithCost("0", v, 1) { + // The race condition doesn't show up without this sleep. + time.Sleep(time.Microsecond) + droppedMap[i] = struct{}{} + } + } + // Wait for all the items to be processed. + c.Wait() + // This will cause eviction from the cache. + require.True(t, c.SetWithCost("1", nil, 10)) + c.Close() + } + + // Run the test 100 times since it's not reliable. + for i := 0; i < 100; i++ { + test() + } +} + +func newTestCache() (*Cache, error) { + return NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + Metrics: true, + }) +} diff --git a/go/cache/ristretto/policy.go b/go/cache/ristretto/policy.go new file mode 100644 index 00000000000..9ebf0b38d72 --- /dev/null +++ b/go/cache/ristretto/policy.go @@ -0,0 +1,422 @@ +/* + * Copyright 2020 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ristretto + +import ( + "math" + "sync" + "sync/atomic" + + "vitess.io/vitess/go/cache/ristretto/bloom" +) + +const ( + // lfuSample is the number of items to sample when looking at eviction + // candidates. 5 seems to be the most optimal number [citation needed]. + lfuSample = 5 +) + +// policy is the interface encapsulating eviction/admission behavior. +// +// TODO: remove this interface and just rename defaultPolicy to policy, as we +// are probably only going to use/implement/maintain one policy. +type policy interface { + ringConsumer + // Add attempts to Add the key-cost pair to the Policy. It returns a slice + // of evicted keys and a bool denoting whether or not the key-cost pair + // was added. If it returns true, the key should be stored in cache. + Add(uint64, int64) ([]*Item, bool) + // Has returns true if the key exists in the Policy. + Has(uint64) bool + // Del deletes the key from the Policy. + Del(uint64) + // Cap returns the amount of used capacity. + Used() int64 + // Close stops all goroutines and closes all channels. + Close() + // Update updates the cost value for the key. + Update(uint64, int64) + // Cost returns the cost value of a key or -1 if missing. + Cost(uint64) int64 + // Optionally, set stats object to track how policy is performing. + CollectMetrics(*Metrics) + // Clear zeroes out all counters and clears hashmaps. + Clear() + // MaxCost returns the current max cost of the cache policy. + MaxCost() int64 + // UpdateMaxCost updates the max cost of the cache policy. + UpdateMaxCost(int64) +} + +func newPolicy(numCounters, maxCost int64) policy { + return newDefaultPolicy(numCounters, maxCost) +} + +type defaultPolicy struct { + sync.Mutex + admit *tinyLFU + evict *sampledLFU + itemsCh chan []uint64 + stop chan struct{} + isClosed bool + metrics *Metrics + numCounters int64 + maxCost int64 +} + +func newDefaultPolicy(numCounters, maxCost int64) *defaultPolicy { + p := &defaultPolicy{ + admit: newTinyLFU(numCounters), + evict: newSampledLFU(maxCost), + itemsCh: make(chan []uint64, 3), + stop: make(chan struct{}), + numCounters: numCounters, + maxCost: maxCost, + } + go p.processItems() + return p +} + +func (p *defaultPolicy) CollectMetrics(metrics *Metrics) { + p.metrics = metrics + p.evict.metrics = metrics +} + +type policyPair struct { + key uint64 + cost int64 +} + +func (p *defaultPolicy) processItems() { + for { + select { + case items := <-p.itemsCh: + p.Lock() + p.admit.Push(items) + p.Unlock() + case <-p.stop: + return + } + } +} + +func (p *defaultPolicy) Push(keys []uint64) bool { + if p.isClosed { + return false + } + + if len(keys) == 0 { + return true + } + + select { + case p.itemsCh <- keys: + p.metrics.add(keepGets, keys[0], uint64(len(keys))) + return true + default: + p.metrics.add(dropGets, keys[0], uint64(len(keys))) + return false + } +} + +// Add decides whether the item with the given key and cost should be accepted by +// the policy. It returns the list of victims that have been evicted and a boolean +// indicating whether the incoming item should be accepted. +func (p *defaultPolicy) Add(key uint64, cost int64) ([]*Item, bool) { + p.Lock() + defer p.Unlock() + + // Cannot add an item bigger than entire cache. + if cost > p.evict.getMaxCost() { + return nil, false + } + + // No need to go any further if the item is already in the cache. + if has := p.evict.updateIfHas(key, cost); has { + // An update does not count as an addition, so return false. + return nil, false + } + + // If the execution reaches this point, the key doesn't exist in the cache. + // Calculate the remaining room in the cache (usually bytes). + room := p.evict.roomLeft(cost) + if room >= 0 { + // There's enough room in the cache to store the new item without + // overflowing. Do that now and stop here. + p.evict.add(key, cost) + p.metrics.add(costAdd, key, uint64(cost)) + return nil, true + } + + // incHits is the hit count for the incoming item. + incHits := p.admit.Estimate(key) + // sample is the eviction candidate pool to be filled via random sampling. + // TODO: perhaps we should use a min heap here. Right now our time + // complexity is N for finding the min. Min heap should bring it down to + // O(lg N). + sample := make([]*policyPair, 0, lfuSample) + // As items are evicted they will be appended to victims. + victims := make([]*Item, 0) + + // Delete victims until there's enough space or a minKey is found that has + // more hits than incoming item. + for ; room < 0; room = p.evict.roomLeft(cost) { + // Fill up empty slots in sample. + sample = p.evict.fillSample(sample) + + // Find minimally used item in sample. + minKey, minHits, minID, minCost := uint64(0), int64(math.MaxInt64), 0, int64(0) + for i, pair := range sample { + // Look up hit count for sample key. + if hits := p.admit.Estimate(pair.key); hits < minHits { + minKey, minHits, minID, minCost = pair.key, hits, i, pair.cost + } + } + + // If the incoming item isn't worth keeping in the policy, reject. + if incHits < minHits { + p.metrics.add(rejectSets, key, 1) + return victims, false + } + + // Delete the victim from metadata. + p.evict.del(minKey) + + // Delete the victim from sample. + sample[minID] = sample[len(sample)-1] + sample = sample[:len(sample)-1] + // Store victim in evicted victims slice. + victims = append(victims, &Item{ + Key: minKey, + Conflict: 0, + Cost: minCost, + }) + } + + p.evict.add(key, cost) + p.metrics.add(costAdd, key, uint64(cost)) + return victims, true +} + +func (p *defaultPolicy) Has(key uint64) bool { + p.Lock() + _, exists := p.evict.keyCosts[key] + p.Unlock() + return exists +} + +func (p *defaultPolicy) Del(key uint64) { + p.Lock() + p.evict.del(key) + p.Unlock() +} + +func (p *defaultPolicy) Used() int64 { + p.Lock() + used := p.evict.used + p.Unlock() + return used +} + +func (p *defaultPolicy) Update(key uint64, cost int64) { + p.Lock() + p.evict.updateIfHas(key, cost) + p.Unlock() +} + +func (p *defaultPolicy) Cost(key uint64) int64 { + p.Lock() + if cost, found := p.evict.keyCosts[key]; found { + p.Unlock() + return cost + } + p.Unlock() + return -1 +} + +func (p *defaultPolicy) Clear() { + p.Lock() + p.admit = newTinyLFU(p.numCounters) + p.evict = newSampledLFU(p.maxCost) + p.Unlock() +} + +func (p *defaultPolicy) Close() { + if p.isClosed { + return + } + + // Block until the p.processItems goroutine returns. + p.stop <- struct{}{} + close(p.stop) + close(p.itemsCh) + p.isClosed = true +} + +func (p *defaultPolicy) MaxCost() int64 { + if p == nil || p.evict == nil { + return 0 + } + return p.evict.getMaxCost() +} + +func (p *defaultPolicy) UpdateMaxCost(maxCost int64) { + if p == nil || p.evict == nil { + return + } + p.evict.updateMaxCost(maxCost) +} + +// sampledLFU is an eviction helper storing key-cost pairs. +type sampledLFU struct { + keyCosts map[uint64]int64 + maxCost int64 + used int64 + metrics *Metrics +} + +func newSampledLFU(maxCost int64) *sampledLFU { + return &sampledLFU{ + keyCosts: make(map[uint64]int64), + maxCost: maxCost, + } +} + +func (p *sampledLFU) getMaxCost() int64 { + return atomic.LoadInt64(&p.maxCost) +} + +func (p *sampledLFU) updateMaxCost(maxCost int64) { + atomic.StoreInt64(&p.maxCost, maxCost) +} + +func (p *sampledLFU) roomLeft(cost int64) int64 { + return p.getMaxCost() - (p.used + cost) +} + +func (p *sampledLFU) fillSample(in []*policyPair) []*policyPair { + if len(in) >= lfuSample { + return in + } + for key, cost := range p.keyCosts { + in = append(in, &policyPair{key, cost}) + if len(in) >= lfuSample { + return in + } + } + return in +} + +func (p *sampledLFU) del(key uint64) { + cost, ok := p.keyCosts[key] + if !ok { + return + } + p.used -= cost + delete(p.keyCosts, key) + p.metrics.add(costEvict, key, uint64(cost)) + p.metrics.add(keyEvict, key, 1) +} + +func (p *sampledLFU) add(key uint64, cost int64) { + p.keyCosts[key] = cost + p.used += cost +} + +func (p *sampledLFU) updateIfHas(key uint64, cost int64) bool { + if prev, found := p.keyCosts[key]; found { + // Update the cost of an existing key, but don't worry about evicting. + // Evictions will be handled the next time a new item is added. + p.metrics.add(keyUpdate, key, 1) + if prev > cost { + diff := prev - cost + p.metrics.add(costAdd, key, ^uint64(uint64(diff)-1)) + } else if cost > prev { + diff := cost - prev + p.metrics.add(costAdd, key, uint64(diff)) + } + p.used += cost - prev + p.keyCosts[key] = cost + return true + } + return false +} + +func (p *sampledLFU) clear() { + p.used = 0 + p.keyCosts = make(map[uint64]int64) +} + +// tinyLFU is an admission helper that keeps track of access frequency using +// tiny (4-bit) counters in the form of a count-min sketch. +// tinyLFU is NOT thread safe. +type tinyLFU struct { + freq *cmSketch + door *bloom.Bloom + incrs int64 + resetAt int64 +} + +func newTinyLFU(numCounters int64) *tinyLFU { + return &tinyLFU{ + freq: newCmSketch(numCounters), + door: bloom.NewBloomFilter(float64(numCounters), 0.01), + resetAt: numCounters, + } +} + +func (p *tinyLFU) Push(keys []uint64) { + for _, key := range keys { + p.Increment(key) + } +} + +func (p *tinyLFU) Estimate(key uint64) int64 { + hits := p.freq.Estimate(key) + if p.door.Has(key) { + hits++ + } + return hits +} + +func (p *tinyLFU) Increment(key uint64) { + // Flip doorkeeper bit if not already done. + if added := p.door.AddIfNotHas(key); !added { + // Increment count-min counter if doorkeeper bit is already set. + p.freq.Increment(key) + } + p.incrs++ + if p.incrs >= p.resetAt { + p.reset() + } +} + +func (p *tinyLFU) reset() { + // Zero out incrs. + p.incrs = 0 + // clears doorkeeper bits + p.door.Clear() + // halves count-min counters + p.freq.Reset() +} + +func (p *tinyLFU) clear() { + p.incrs = 0 + p.freq.Clear() + p.door.Clear() +} diff --git a/go/cache/ristretto/policy_test.go b/go/cache/ristretto/policy_test.go new file mode 100644 index 00000000000..c864b6c74d0 --- /dev/null +++ b/go/cache/ristretto/policy_test.go @@ -0,0 +1,276 @@ +/* + * Copyright 2020 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ristretto + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestPolicy(t *testing.T) { + defer func() { + require.Nil(t, recover()) + }() + newPolicy(100, 10) +} + +func TestPolicyMetrics(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.CollectMetrics(newMetrics()) + require.NotNil(t, p.metrics) + require.NotNil(t, p.evict.metrics) +} + +func TestPolicyProcessItems(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.itemsCh <- []uint64{1, 2, 2} + time.Sleep(wait) + p.Lock() + require.Equal(t, int64(2), p.admit.Estimate(2)) + require.Equal(t, int64(1), p.admit.Estimate(1)) + p.Unlock() + + p.stop <- struct{}{} + p.itemsCh <- []uint64{3, 3, 3} + time.Sleep(wait) + p.Lock() + require.Equal(t, int64(0), p.admit.Estimate(3)) + p.Unlock() +} + +func TestPolicyPush(t *testing.T) { + p := newDefaultPolicy(100, 10) + require.True(t, p.Push([]uint64{})) + + keepCount := 0 + for i := 0; i < 10; i++ { + if p.Push([]uint64{1, 2, 3, 4, 5}) { + keepCount++ + } + } + require.NotEqual(t, 0, keepCount) +} + +func TestPolicyAdd(t *testing.T) { + p := newDefaultPolicy(1000, 100) + if victims, added := p.Add(1, 101); victims != nil || added { + t.Fatal("can't add an item bigger than entire cache") + } + p.Lock() + p.evict.add(1, 1) + p.admit.Increment(1) + p.admit.Increment(2) + p.admit.Increment(3) + p.Unlock() + + victims, added := p.Add(1, 1) + require.Nil(t, victims) + require.False(t, added) + + victims, added = p.Add(2, 20) + require.Nil(t, victims) + require.True(t, added) + + victims, added = p.Add(3, 90) + require.NotNil(t, victims) + require.True(t, added) + + victims, added = p.Add(4, 20) + require.NotNil(t, victims) + require.False(t, added) +} + +func TestPolicyHas(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Add(1, 1) + require.True(t, p.Has(1)) + require.False(t, p.Has(2)) +} + +func TestPolicyDel(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Add(1, 1) + p.Del(1) + p.Del(2) + require.False(t, p.Has(1)) + require.False(t, p.Has(2)) +} + +func TestPolicyCap(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Add(1, 1) + require.Equal(t, int64(9), p.MaxCost()-p.Used()) +} + +func TestPolicyUpdate(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Add(1, 1) + p.Update(1, 2) + p.Lock() + require.Equal(t, int64(2), p.evict.keyCosts[1]) + p.Unlock() +} + +func TestPolicyCost(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Add(1, 2) + require.Equal(t, int64(2), p.Cost(1)) + require.Equal(t, int64(-1), p.Cost(2)) +} + +func TestPolicyClear(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Add(1, 1) + p.Add(2, 2) + p.Add(3, 3) + p.Clear() + require.Equal(t, int64(10), p.MaxCost()-p.Used()) + require.False(t, p.Has(1)) + require.False(t, p.Has(2)) + require.False(t, p.Has(3)) +} + +func TestPolicyClose(t *testing.T) { + defer func() { + require.NotNil(t, recover()) + }() + + p := newDefaultPolicy(100, 10) + p.Add(1, 1) + p.Close() + p.itemsCh <- []uint64{1} +} + +func TestPushAfterClose(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Close() + require.False(t, p.Push([]uint64{1, 2})) +} + +func TestAddAfterClose(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Close() + p.Add(1, 1) +} + +func TestSampledLFUAdd(t *testing.T) { + e := newSampledLFU(4) + e.add(1, 1) + e.add(2, 2) + e.add(3, 1) + require.Equal(t, int64(4), e.used) + require.Equal(t, int64(2), e.keyCosts[2]) +} + +func TestSampledLFUDel(t *testing.T) { + e := newSampledLFU(4) + e.add(1, 1) + e.add(2, 2) + e.del(2) + require.Equal(t, int64(1), e.used) + _, ok := e.keyCosts[2] + require.False(t, ok) + e.del(4) +} + +func TestSampledLFUUpdate(t *testing.T) { + e := newSampledLFU(4) + e.add(1, 1) + require.True(t, e.updateIfHas(1, 2)) + require.Equal(t, int64(2), e.used) + require.False(t, e.updateIfHas(2, 2)) +} + +func TestSampledLFUClear(t *testing.T) { + e := newSampledLFU(4) + e.add(1, 1) + e.add(2, 2) + e.add(3, 1) + e.clear() + require.Equal(t, 0, len(e.keyCosts)) + require.Equal(t, int64(0), e.used) +} + +func TestSampledLFURoom(t *testing.T) { + e := newSampledLFU(16) + e.add(1, 1) + e.add(2, 2) + e.add(3, 3) + require.Equal(t, int64(6), e.roomLeft(4)) +} + +func TestSampledLFUSample(t *testing.T) { + e := newSampledLFU(16) + e.add(4, 4) + e.add(5, 5) + sample := e.fillSample([]*policyPair{ + {1, 1}, + {2, 2}, + {3, 3}, + }) + k := sample[len(sample)-1].key + require.Equal(t, 5, len(sample)) + require.NotEqual(t, 1, k) + require.NotEqual(t, 2, k) + require.NotEqual(t, 3, k) + require.Equal(t, len(sample), len(e.fillSample(sample))) + e.del(5) + sample = e.fillSample(sample[:len(sample)-2]) + require.Equal(t, 4, len(sample)) +} + +func TestTinyLFUIncrement(t *testing.T) { + a := newTinyLFU(4) + a.Increment(1) + a.Increment(1) + a.Increment(1) + require.True(t, a.door.Has(1)) + require.Equal(t, int64(2), a.freq.Estimate(1)) + + a.Increment(1) + require.False(t, a.door.Has(1)) + require.Equal(t, int64(1), a.freq.Estimate(1)) +} + +func TestTinyLFUEstimate(t *testing.T) { + a := newTinyLFU(8) + a.Increment(1) + a.Increment(1) + a.Increment(1) + require.Equal(t, int64(3), a.Estimate(1)) + require.Equal(t, int64(0), a.Estimate(2)) +} + +func TestTinyLFUPush(t *testing.T) { + a := newTinyLFU(16) + a.Push([]uint64{1, 2, 2, 3, 3, 3}) + require.Equal(t, int64(1), a.Estimate(1)) + require.Equal(t, int64(2), a.Estimate(2)) + require.Equal(t, int64(3), a.Estimate(3)) + require.Equal(t, int64(6), a.incrs) +} + +func TestTinyLFUClear(t *testing.T) { + a := newTinyLFU(16) + a.Push([]uint64{1, 3, 3, 3}) + a.clear() + require.Equal(t, int64(0), a.incrs) + require.Equal(t, int64(0), a.Estimate(3)) +} diff --git a/go/cache/ristretto/ring.go b/go/cache/ristretto/ring.go new file mode 100644 index 00000000000..afc2c1559f8 --- /dev/null +++ b/go/cache/ristretto/ring.go @@ -0,0 +1,92 @@ +/* + * Copyright 2019 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ristretto + +import ( + "sync" +) + +// ringConsumer is the user-defined object responsible for receiving and +// processing items in batches when buffers are drained. +type ringConsumer interface { + Push([]uint64) bool +} + +// ringStripe is a singular ring buffer that is not concurrent safe. +type ringStripe struct { + cons ringConsumer + data []uint64 + capa int +} + +func newRingStripe(cons ringConsumer, capa int64) *ringStripe { + return &ringStripe{ + cons: cons, + data: make([]uint64, 0, capa), + capa: int(capa), + } +} + +// Push appends an item in the ring buffer and drains (copies items and +// sends to Consumer) if full. +func (s *ringStripe) Push(item uint64) { + s.data = append(s.data, item) + // Decide if the ring buffer should be drained. + if len(s.data) >= s.capa { + // Send elements to consumer and create a new ring stripe. + if s.cons.Push(s.data) { + s.data = make([]uint64, 0, s.capa) + } else { + s.data = s.data[:0] + } + } +} + +// ringBuffer stores multiple buffers (stripes) and distributes Pushed items +// between them to lower contention. +// +// This implements the "batching" process described in the BP-Wrapper paper +// (section III part A). +type ringBuffer struct { + pool *sync.Pool +} + +// newRingBuffer returns a striped ring buffer. The Consumer in ringConfig will +// be called when individual stripes are full and need to drain their elements. +func newRingBuffer(cons ringConsumer, capa int64) *ringBuffer { + // LOSSY buffers use a very simple sync.Pool for concurrently reusing + // stripes. We do lose some stripes due to GC (unheld items in sync.Pool + // are cleared), but the performance gains generally outweigh the small + // percentage of elements lost. The performance primarily comes from + // low-level runtime functions used in the standard library that aren't + // available to us (such as runtime_procPin()). + return &ringBuffer{ + pool: &sync.Pool{ + New: func() interface{} { return newRingStripe(cons, capa) }, + }, + } +} + +// Push adds an element to one of the internal stripes and possibly drains if +// the stripe becomes full. +func (b *ringBuffer) Push(item uint64) { + // Reuse or create a new stripe. + stripe := b.pool.Get().(*ringStripe) + stripe.Push(item) + b.pool.Put(stripe) +} diff --git a/go/cache/ristretto/ring_test.go b/go/cache/ristretto/ring_test.go new file mode 100644 index 00000000000..0dbe962ccc6 --- /dev/null +++ b/go/cache/ristretto/ring_test.go @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ristretto + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +type testConsumer struct { + push func([]uint64) + save bool +} + +func (c *testConsumer) Push(items []uint64) bool { + if c.save { + c.push(items) + return true + } + return false +} + +func TestRingDrain(t *testing.T) { + drains := 0 + r := newRingBuffer(&testConsumer{ + push: func(items []uint64) { + drains++ + }, + save: true, + }, 1) + for i := 0; i < 100; i++ { + r.Push(uint64(i)) + } + require.Equal(t, 100, drains, "buffers shouldn't be dropped with BufferItems == 1") +} + +func TestRingReset(t *testing.T) { + drains := 0 + r := newRingBuffer(&testConsumer{ + push: func(items []uint64) { + drains++ + }, + save: false, + }, 4) + for i := 0; i < 100; i++ { + r.Push(uint64(i)) + } + require.Equal(t, 0, drains, "testConsumer shouldn't be draining") +} + +func TestRingConsumer(t *testing.T) { + mu := &sync.Mutex{} + drainItems := make(map[uint64]struct{}) + r := newRingBuffer(&testConsumer{ + push: func(items []uint64) { + mu.Lock() + defer mu.Unlock() + for i := range items { + drainItems[items[i]] = struct{}{} + } + }, + save: true, + }, 4) + for i := 0; i < 100; i++ { + r.Push(uint64(i)) + } + l := len(drainItems) + require.NotEqual(t, 0, l) + require.True(t, l <= 100) +} diff --git a/go/cache/ristretto/sketch.go b/go/cache/ristretto/sketch.go new file mode 100644 index 00000000000..ce0504a2a83 --- /dev/null +++ b/go/cache/ristretto/sketch.go @@ -0,0 +1,156 @@ +/* + * Copyright 2019 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package ristretto includes multiple probabalistic data structures needed for +// admission/eviction metadata. Most are Counting Bloom Filter variations, but +// a caching-specific feature that is also required is a "freshness" mechanism, +// which basically serves as a "lifetime" process. This freshness mechanism +// was described in the original TinyLFU paper [1], but other mechanisms may +// be better suited for certain data distributions. +// +// [1]: https://arxiv.org/abs/1512.00727 +package ristretto + +import ( + "fmt" + "math/rand" + "time" +) + +// cmSketch is a Count-Min sketch implementation with 4-bit counters, heavily +// based on Damian Gryski's CM4 [1]. +// +// [1]: https://github.com/dgryski/go-tinylfu/blob/master/cm4.go +type cmSketch struct { + rows [cmDepth]cmRow + seed [cmDepth]uint64 + mask uint64 +} + +const ( + // cmDepth is the number of counter copies to store (think of it as rows). + cmDepth = 4 +) + +func newCmSketch(numCounters int64) *cmSketch { + if numCounters == 0 { + panic("cmSketch: bad numCounters") + } + // Get the next power of 2 for better cache performance. + numCounters = next2Power(numCounters) + sketch := &cmSketch{mask: uint64(numCounters - 1)} + // Initialize rows of counters and seeds. + source := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := 0; i < cmDepth; i++ { + sketch.seed[i] = source.Uint64() + sketch.rows[i] = newCmRow(numCounters) + } + return sketch +} + +// Increment increments the count(ers) for the specified key. +func (s *cmSketch) Increment(hashed uint64) { + for i := range s.rows { + s.rows[i].increment((hashed ^ s.seed[i]) & s.mask) + } +} + +// Estimate returns the value of the specified key. +func (s *cmSketch) Estimate(hashed uint64) int64 { + min := byte(255) + for i := range s.rows { + val := s.rows[i].get((hashed ^ s.seed[i]) & s.mask) + if val < min { + min = val + } + } + return int64(min) +} + +// Reset halves all counter values. +func (s *cmSketch) Reset() { + for _, r := range s.rows { + r.reset() + } +} + +// Clear zeroes all counters. +func (s *cmSketch) Clear() { + for _, r := range s.rows { + r.clear() + } +} + +// cmRow is a row of bytes, with each byte holding two counters. +type cmRow []byte + +func newCmRow(numCounters int64) cmRow { + return make(cmRow, numCounters/2) +} + +func (r cmRow) get(n uint64) byte { + return byte(r[n/2]>>((n&1)*4)) & 0x0f +} + +func (r cmRow) increment(n uint64) { + // Index of the counter. + i := n / 2 + // Shift distance (even 0, odd 4). + s := (n & 1) * 4 + // Counter value. + v := (r[i] >> s) & 0x0f + // Only increment if not max value (overflow wrap is bad for LFU). + if v < 15 { + r[i] += 1 << s + } +} + +func (r cmRow) reset() { + // Halve each counter. + for i := range r { + r[i] = (r[i] >> 1) & 0x77 + } +} + +func (r cmRow) clear() { + // Zero each counter. + for i := range r { + r[i] = 0 + } +} + +func (r cmRow) string() string { + s := "" + for i := uint64(0); i < uint64(len(r)*2); i++ { + s += fmt.Sprintf("%02d ", (r[(i/2)]>>((i&1)*4))&0x0f) + } + s = s[:len(s)-1] + return s +} + +// next2Power rounds x up to the next power of 2, if it's not already one. +func next2Power(x int64) int64 { + x-- + x |= x >> 1 + x |= x >> 2 + x |= x >> 4 + x |= x >> 8 + x |= x >> 16 + x |= x >> 32 + x++ + return x +} diff --git a/go/cache/ristretto/sketch_test.go b/go/cache/ristretto/sketch_test.go new file mode 100644 index 00000000000..f0d523df559 --- /dev/null +++ b/go/cache/ristretto/sketch_test.go @@ -0,0 +1,102 @@ +/* + * Copyright 2020 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ristretto + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSketch(t *testing.T) { + defer func() { + require.NotNil(t, recover()) + }() + + s := newCmSketch(5) + require.Equal(t, uint64(7), s.mask) + newCmSketch(0) +} + +func TestSketchIncrement(t *testing.T) { + s := newCmSketch(16) + s.Increment(1) + s.Increment(5) + s.Increment(9) + for i := 0; i < cmDepth; i++ { + if s.rows[i].string() != s.rows[0].string() { + break + } + require.False(t, i == cmDepth-1, "identical rows, bad seeding") + } +} + +func TestSketchEstimate(t *testing.T) { + s := newCmSketch(16) + s.Increment(1) + s.Increment(1) + require.Equal(t, int64(2), s.Estimate(1)) + require.Equal(t, int64(0), s.Estimate(0)) +} + +func TestSketchReset(t *testing.T) { + s := newCmSketch(16) + s.Increment(1) + s.Increment(1) + s.Increment(1) + s.Increment(1) + s.Reset() + require.Equal(t, int64(2), s.Estimate(1)) +} + +func TestSketchClear(t *testing.T) { + s := newCmSketch(16) + for i := 0; i < 16; i++ { + s.Increment(uint64(i)) + } + s.Clear() + for i := 0; i < 16; i++ { + require.Equal(t, int64(0), s.Estimate(uint64(i))) + } +} + +func TestNext2Power(t *testing.T) { + sz := 12 << 30 + szf := float64(sz) * 0.01 + val := int64(szf) + t.Logf("szf = %.2f val = %d\n", szf, val) + pow := next2Power(val) + t.Logf("pow = %d. mult 4 = %d\n", pow, pow*4) +} + +func BenchmarkSketchIncrement(b *testing.B) { + s := newCmSketch(16) + b.SetBytes(1) + for n := 0; n < b.N; n++ { + s.Increment(1) + } +} + +func BenchmarkSketchEstimate(b *testing.B) { + s := newCmSketch(16) + s.Increment(1) + b.SetBytes(1) + for n := 0; n < b.N; n++ { + s.Estimate(1) + } +} diff --git a/go/cache/ristretto/store.go b/go/cache/ristretto/store.go new file mode 100644 index 00000000000..44e5ad8b147 --- /dev/null +++ b/go/cache/ristretto/store.go @@ -0,0 +1,240 @@ +/* + * Copyright 2019 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ristretto + +import ( + "sync" +) + +// TODO: Do we need this to be a separate struct from Item? +type storeItem struct { + key uint64 + conflict uint64 + value interface{} +} + +// store is the interface fulfilled by all hash map implementations in this +// file. Some hash map implementations are better suited for certain data +// distributions than others, so this allows us to abstract that out for use +// in Ristretto. +// +// Every store is safe for concurrent usage. +type store interface { + // Get returns the value associated with the key parameter. + Get(uint64, uint64) (interface{}, bool) + // Set adds the key-value pair to the Map or updates the value if it's + // already present. The key-value pair is passed as a pointer to an + // item object. + Set(*Item) + // Del deletes the key-value pair from the Map. + Del(uint64, uint64) (uint64, interface{}) + // Update attempts to update the key with a new value and returns true if + // successful. + Update(*Item) (interface{}, bool) + // Clear clears all contents of the store. + Clear(onEvict itemCallback) + // ForEach yields all the values in the store + ForEach(forEach func(interface{}) bool) + // Len returns the number of entries in the store + Len() int +} + +// newStore returns the default store implementation. +func newStore() store { + return newShardedMap() +} + +const numShards uint64 = 256 + +type shardedMap struct { + shards []*lockedMap +} + +func newShardedMap() *shardedMap { + sm := &shardedMap{ + shards: make([]*lockedMap, int(numShards)), + } + for i := range sm.shards { + sm.shards[i] = newLockedMap() + } + return sm +} + +func (sm *shardedMap) Get(key, conflict uint64) (interface{}, bool) { + return sm.shards[key%numShards].get(key, conflict) +} + +func (sm *shardedMap) Set(i *Item) { + if i == nil { + // If item is nil make this Set a no-op. + return + } + + sm.shards[i.Key%numShards].Set(i) +} + +func (sm *shardedMap) Del(key, conflict uint64) (uint64, interface{}) { + return sm.shards[key%numShards].Del(key, conflict) +} + +func (sm *shardedMap) Update(newItem *Item) (interface{}, bool) { + return sm.shards[newItem.Key%numShards].Update(newItem) +} + +func (sm *shardedMap) ForEach(forEach func(interface{}) bool) { + for _, shard := range sm.shards { + if !shard.foreach(forEach) { + break + } + } +} + +func (sm *shardedMap) Len() int { + l := 0 + for _, shard := range sm.shards { + l += shard.Len() + } + return l +} + +func (sm *shardedMap) Clear(onEvict itemCallback) { + for i := uint64(0); i < numShards; i++ { + sm.shards[i].Clear(onEvict) + } +} + +type lockedMap struct { + sync.RWMutex + data map[uint64]storeItem +} + +func newLockedMap() *lockedMap { + return &lockedMap{ + data: make(map[uint64]storeItem), + } +} + +func (m *lockedMap) get(key, conflict uint64) (interface{}, bool) { + m.RLock() + item, ok := m.data[key] + m.RUnlock() + if !ok { + return nil, false + } + if conflict != 0 && (conflict != item.conflict) { + return nil, false + } + return item.value, true +} + +func (m *lockedMap) Set(i *Item) { + if i == nil { + // If the item is nil make this Set a no-op. + return + } + + m.Lock() + defer m.Unlock() + item, ok := m.data[i.Key] + + if ok { + // The item existed already. We need to check the conflict key and reject the + // update if they do not match. Only after that the expiration map is updated. + if i.Conflict != 0 && (i.Conflict != item.conflict) { + return + } + } + + m.data[i.Key] = storeItem{ + key: i.Key, + conflict: i.Conflict, + value: i.Value, + } +} + +func (m *lockedMap) Del(key, conflict uint64) (uint64, interface{}) { + m.Lock() + item, ok := m.data[key] + if !ok { + m.Unlock() + return 0, nil + } + if conflict != 0 && (conflict != item.conflict) { + m.Unlock() + return 0, nil + } + + delete(m.data, key) + m.Unlock() + return item.conflict, item.value +} + +func (m *lockedMap) Update(newItem *Item) (interface{}, bool) { + m.Lock() + item, ok := m.data[newItem.Key] + if !ok { + m.Unlock() + return nil, false + } + if newItem.Conflict != 0 && (newItem.Conflict != item.conflict) { + m.Unlock() + return nil, false + } + + m.data[newItem.Key] = storeItem{ + key: newItem.Key, + conflict: newItem.Conflict, + value: newItem.Value, + } + + m.Unlock() + return item.value, true +} + +func (m *lockedMap) Len() int { + m.RLock() + l := len(m.data) + m.RUnlock() + return l +} + +func (m *lockedMap) Clear(onEvict itemCallback) { + m.Lock() + i := &Item{} + if onEvict != nil { + for _, si := range m.data { + i.Key = si.key + i.Conflict = si.conflict + i.Value = si.value + onEvict(i) + } + } + m.data = make(map[uint64]storeItem) + m.Unlock() +} + +func (m *lockedMap) foreach(forEach func(interface{}) bool) bool { + m.RLock() + defer m.RUnlock() + for _, si := range m.data { + if !forEach(si.value) { + return false + } + } + return true +} diff --git a/go/cache/ristretto/store_test.go b/go/cache/ristretto/store_test.go new file mode 100644 index 00000000000..54634736a72 --- /dev/null +++ b/go/cache/ristretto/store_test.go @@ -0,0 +1,224 @@ +/* + * Copyright 2020 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ristretto + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStoreSetGet(t *testing.T) { + s := newStore() + key, conflict := defaultStringHash("1") + i := Item{ + Key: key, + Conflict: conflict, + Value: 2, + } + s.Set(&i) + val, ok := s.Get(key, conflict) + require.True(t, ok) + require.Equal(t, 2, val.(int)) + + i.Value = 3 + s.Set(&i) + val, ok = s.Get(key, conflict) + require.True(t, ok) + require.Equal(t, 3, val.(int)) + + key, conflict = defaultStringHash("2") + i = Item{ + Key: key, + Conflict: conflict, + Value: 2, + } + s.Set(&i) + val, ok = s.Get(key, conflict) + require.True(t, ok) + require.Equal(t, 2, val.(int)) +} + +func TestStoreDel(t *testing.T) { + s := newStore() + key, conflict := defaultStringHash("1") + i := Item{ + Key: key, + Conflict: conflict, + Value: 1, + } + s.Set(&i) + s.Del(key, conflict) + val, ok := s.Get(key, conflict) + require.False(t, ok) + require.Nil(t, val) + + s.Del(2, 0) +} + +func TestStoreClear(t *testing.T) { + s := newStore() + for i := 0; i < 1000; i++ { + key, conflict := defaultStringHash(strconv.Itoa(i)) + it := Item{ + Key: key, + Conflict: conflict, + Value: i, + } + s.Set(&it) + } + s.Clear(nil) + for i := 0; i < 1000; i++ { + key, conflict := defaultStringHash(strconv.Itoa(i)) + val, ok := s.Get(key, conflict) + require.False(t, ok) + require.Nil(t, val) + } +} + +func TestStoreUpdate(t *testing.T) { + s := newStore() + key, conflict := defaultStringHash("1") + i := Item{ + Key: key, + Conflict: conflict, + Value: 1, + } + s.Set(&i) + i.Value = 2 + _, ok := s.Update(&i) + require.True(t, ok) + + val, ok := s.Get(key, conflict) + require.True(t, ok) + require.NotNil(t, val) + + val, ok = s.Get(key, conflict) + require.True(t, ok) + require.Equal(t, 2, val.(int)) + + i.Value = 3 + _, ok = s.Update(&i) + require.True(t, ok) + + val, ok = s.Get(key, conflict) + require.True(t, ok) + require.Equal(t, 3, val.(int)) + + key, conflict = defaultStringHash("2") + i = Item{ + Key: key, + Conflict: conflict, + Value: 2, + } + _, ok = s.Update(&i) + require.False(t, ok) + val, ok = s.Get(key, conflict) + require.False(t, ok) + require.Nil(t, val) +} + +func TestStoreCollision(t *testing.T) { + s := newShardedMap() + s.shards[1].Lock() + s.shards[1].data[1] = storeItem{ + key: 1, + conflict: 0, + value: 1, + } + s.shards[1].Unlock() + val, ok := s.Get(1, 1) + require.False(t, ok) + require.Nil(t, val) + + i := Item{ + Key: 1, + Conflict: 1, + Value: 2, + } + s.Set(&i) + val, ok = s.Get(1, 0) + require.True(t, ok) + require.NotEqual(t, 2, val.(int)) + + _, ok = s.Update(&i) + require.False(t, ok) + val, ok = s.Get(1, 0) + require.True(t, ok) + require.NotEqual(t, 2, val.(int)) + + s.Del(1, 1) + val, ok = s.Get(1, 0) + require.True(t, ok) + require.NotNil(t, val) +} + +func BenchmarkStoreGet(b *testing.B) { + s := newStore() + key, conflict := defaultStringHash("1") + i := Item{ + Key: key, + Conflict: conflict, + Value: 1, + } + s.Set(&i) + b.SetBytes(1) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + s.Get(key, conflict) + } + }) +} + +func BenchmarkStoreSet(b *testing.B) { + s := newStore() + key, conflict := defaultStringHash("1") + b.SetBytes(1) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + i := Item{ + Key: key, + Conflict: conflict, + Value: 1, + } + s.Set(&i) + } + }) +} + +func BenchmarkStoreUpdate(b *testing.B) { + s := newStore() + key, conflict := defaultStringHash("1") + i := Item{ + Key: key, + Conflict: conflict, + Value: 1, + } + s.Set(&i) + b.SetBytes(1) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + s.Update(&Item{ + Key: key, + Conflict: conflict, + Value: 2, + }) + } + }) +} diff --git a/go/hack/runtime.go b/go/hack/runtime.go new file mode 100644 index 00000000000..c7355769307 --- /dev/null +++ b/go/hack/runtime.go @@ -0,0 +1,45 @@ +/* +Copyright 2021 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package hack + +import ( + "reflect" + "unsafe" +) + +//go:noescape +//go:linkname memhash runtime.memhash +func memhash(p unsafe.Pointer, h, s uintptr) uintptr + +//go:noescape +//go:linkname strhash runtime.strhash +func strhash(p unsafe.Pointer, h uintptr) uintptr + +// RuntimeMemhash provides access to the Go runtime's default hash function for arbitrary bytes. +// This is an optimal hash function which takes an input seed and is potentially implemented in hardware +// for most architectures. This is the same hash function that the language's `map` uses. +func RuntimeMemhash(b []byte, seed uint64) uint64 { + pstring := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + return uint64(memhash(unsafe.Pointer(pstring.Data), uintptr(seed), uintptr(pstring.Len))) +} + +// RuntimeStrhash provides access to the Go runtime's default hash function for strings. +// This is an optimal hash function which takes an input seed and is potentially implemented in hardware +// for most architectures. This is the same hash function that the language's `map` uses. +func RuntimeStrhash(str string, seed uint64) uint64 { + return uint64(strhash(unsafe.Pointer(&str), uintptr(seed))) +} diff --git a/go/hack/runtime.s b/go/hack/runtime.s new file mode 100644 index 00000000000..ac00d502ab5 --- /dev/null +++ b/go/hack/runtime.s @@ -0,0 +1,3 @@ +// DO NOT REMOVE: this empty goassembly file forces the Go compiler to perform +// external linking on the sibling `runtime.go`, so that the symbols declared in that +// file become properly resolved diff --git a/go/pools/numbered.go b/go/pools/numbered.go index 1f88ae3d7da..04cc5807d55 100644 --- a/go/pools/numbered.go +++ b/go/pools/numbered.go @@ -47,15 +47,13 @@ type unregistered struct { timeUnregistered time.Time } -func (u *unregistered) Size() int { - return 1 -} - //NewNumbered creates a new numbered func NewNumbered() *Numbered { n := &Numbered{ - resources: make(map[int64]*numberedWrapper), - recentlyUnregistered: cache.NewLRUCache(1000), + resources: make(map[int64]*numberedWrapper), + recentlyUnregistered: cache.NewLRUCache(1000, func(_ interface{}) int64 { + return 1 + }), } n.empty = sync.NewCond(&n.mu) return n diff --git a/go/sync2/consolidator.go b/go/sync2/consolidator.go index 5e7698996c9..d0515615763 100644 --- a/go/sync2/consolidator.go +++ b/go/sync2/consolidator.go @@ -94,7 +94,9 @@ type ConsolidatorCache struct { // NewConsolidatorCache creates a new cache with the given capacity. func NewConsolidatorCache(capacity int64) *ConsolidatorCache { - return &ConsolidatorCache{cache.NewLRUCache(capacity)} + return &ConsolidatorCache{cache.NewLRUCache(capacity, func(_ interface{}) int64 { + return 1 + })} } // Record increments the count for "query" by 1. @@ -128,13 +130,6 @@ func (cc *ConsolidatorCache) Items() []ConsolidatorCacheItem { // request for the same query is already in progress. type ccount int64 -// Size always returns 1 because we use the cache only to track queries, -// independent of the number of requests waiting for them. -// This implements the cache.Value interface. -func (cc *ccount) Size() int { - return 1 -} - func (cc *ccount) add(n int64) int64 { return atomic.AddInt64((*int64)(cc), n) } diff --git a/go/vt/vtexplain/vtexplain_vtgate.go b/go/vt/vtexplain/vtexplain_vtgate.go index ccdce29a55c..5696ead4d20 100644 --- a/go/vt/vtexplain/vtexplain_vtgate.go +++ b/go/vt/vtexplain/vtexplain_vtgate.go @@ -20,13 +20,13 @@ limitations under the License. package vtexplain import ( + "context" "fmt" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/vt/topo" "vitess.io/vitess/go/vt/topo/memorytopo" - "context" - "vitess.io/vitess/go/vt/vterrors" "vitess.io/vitess/go/json2" @@ -69,8 +69,7 @@ func initVtgateExecutor(vSchemaStr, ksShardMapStr string, opts *Options) error { vtgateSession.TargetString = opts.Target streamSize := 10 - queryPlanCacheSize := int64(10) - vtgateExecutor = vtgate.NewExecutor(context.Background(), explainTopo, vtexplainCell, resolver, opts.Normalize, streamSize, queryPlanCacheSize) + vtgateExecutor = vtgate.NewExecutor(context.Background(), explainTopo, vtexplainCell, resolver, opts.Normalize, streamSize, cache.DefaultConfig) return nil } @@ -201,11 +200,12 @@ func vtgateExecute(sql string) ([]*engine.Plan, map[string]*TabletActions, error } var plans []*engine.Plan - for _, item := range planCache.Items() { - plan := item.Value.(*engine.Plan) + planCache.ForEach(func(value interface{}) bool { + plan := value.(*engine.Plan) plan.ExecTime = 0 plans = append(plans, plan) - } + return true + }) planCache.Clear() tabletActions := make(map[string]*TabletActions) diff --git a/go/vt/vtgate/engine/primitive.go b/go/vt/vtgate/engine/primitive.go index e8d16a174cf..467255943f9 100644 --- a/go/vt/vtgate/engine/primitive.go +++ b/go/vt/vtgate/engine/primitive.go @@ -241,13 +241,6 @@ func Exists(m Match, p Primitive) bool { return Find(m, p) != nil } -// Size is defined so that Plan can be given to a cache.LRUCache. -// VTGate needs to maintain a cache of plans. It uses LRUCache, which -// in turn requires its objects to define a Size function. -func (p *Plan) Size() int { - return 1 -} - //MarshalJSON serializes the plan into a JSON representation. func (p *Plan) MarshalJSON() ([]byte, error) { var instructions *PrimitiveDescription diff --git a/go/vt/vtgate/executor.go b/go/vt/vtgate/executor.go index 2755c8a4cee..cac28af3eff 100644 --- a/go/vt/vtgate/executor.go +++ b/go/vt/vtgate/executor.go @@ -101,7 +101,7 @@ type Executor struct { vschema *vindexes.VSchema normalize bool streamSize int - plans *cache.LRUCache + plans cache.Cache vschemaStats *VSchemaStats vm *VSchemaManager @@ -114,14 +114,14 @@ const pathScatterStats = "/debug/scatter_stats" const pathVSchema = "/debug/vschema" // NewExecutor creates a new Executor. -func NewExecutor(ctx context.Context, serv srvtopo.Server, cell string, resolver *Resolver, normalize bool, streamSize int, queryPlanCacheSize int64) *Executor { +func NewExecutor(ctx context.Context, serv srvtopo.Server, cell string, resolver *Resolver, normalize bool, streamSize int, cacheCfg *cache.Config) *Executor { e := &Executor{ serv: serv, cell: cell, resolver: resolver, scatterConn: resolver.scatterConn, txConn: resolver.scatterConn.txConn, - plans: cache.NewLRUCache(queryPlanCacheSize), + plans: cache.NewDefaultCacheImpl(cacheCfg), normalize: normalize, streamSize: streamSize, } @@ -131,13 +131,12 @@ func NewExecutor(ctx context.Context, serv srvtopo.Server, cell string, resolver e.vm.watchSrvVSchema(ctx, cell) executorOnce.Do(func() { - stats.NewGaugeFunc("QueryPlanCacheLength", "Query plan cache length", e.plans.Length) - stats.NewGaugeFunc("QueryPlanCacheSize", "Query plan cache size", e.plans.Size) - stats.NewGaugeFunc("QueryPlanCacheCapacity", "Query plan cache capacity", e.plans.Capacity) + stats.NewGaugeFunc("QueryPlanCacheLength", "Query plan cache length", func() int64 { + return int64(e.plans.Len()) + }) + stats.NewGaugeFunc("QueryPlanCacheSize", "Query plan cache size", e.plans.UsedCapacity) + stats.NewGaugeFunc("QueryPlanCacheCapacity", "Query plan cache capacity", e.plans.MaxCapacity) stats.NewCounterFunc("QueryPlanCacheEvictions", "Query plan cache evictions", e.plans.Evictions) - stats.Publish("QueryPlanCacheOldest", stats.StringFunc(func() string { - return fmt.Sprintf("%v", e.plans.Oldest()) - })) http.Handle(pathQueryPlans, e) http.Handle(pathScatterStats, e) http.Handle(pathVSchema, e) @@ -1290,11 +1289,6 @@ func (e *Executor) getPlan(vcursor *vcursorImpl, sql string, comments sqlparser. ignoreMaxMemoryRows := sqlparser.IgnoreMaxMaxMemoryRowsDirective(stmt) vcursor.SetIgnoreMaxMemoryRows(ignoreMaxMemoryRows) - planKey := vcursor.planPrefixKey() + ":" + sql - if plan, ok := e.plans.Get(planKey); ok { - return plan.(*engine.Plan), nil - } - // Normalize if possible and retry. if (e.normalize && sqlparser.CanNormalize(stmt)) || sqlparser.IsSetStatement(stmt) { parameterize := e.normalize // the public flag is called normalize @@ -1312,10 +1306,11 @@ func (e *Executor) getPlan(vcursor *vcursorImpl, sql string, comments sqlparser. logStats.BindVariables = bindVars } - planKey = vcursor.planPrefixKey() + ":" + query + planKey := vcursor.planPrefixKey() + ":" + query if plan, ok := e.plans.Get(planKey); ok { return plan.(*engine.Plan), nil } + plan, err := planbuilder.BuildFromStmt(query, statement, vcursor, bindVarNeeds) if err != nil { return nil, err @@ -1334,6 +1329,23 @@ func skipQueryPlanCache(safeSession *SafeSession) bool { return safeSession.Options.SkipQueryPlanCache } +type cacheItem struct { + Key string + Value *engine.Plan +} + +func (e *Executor) debugCacheEntries() (items []cacheItem) { + e.plans.ForEach(func(value interface{}) bool { + plan := value.(*engine.Plan) + items = append(items, cacheItem{ + Key: plan.Original, + Value: plan, + }) + return true + }) + return +} + // ServeHTTP shows the current plans in the query cache. func (e *Executor) ServeHTTP(response http.ResponseWriter, request *http.Request) { if err := acl.CheckAccessHTTP(request, acl.DEBUGGING); err != nil { @@ -1343,7 +1355,7 @@ func (e *Executor) ServeHTTP(response http.ResponseWriter, request *http.Request switch request.URL.Path { case pathQueryPlans: - returnAsJSON(response, e.plans.Items()) + returnAsJSON(response, e.debugCacheEntries()) case pathVSchema: returnAsJSON(response, e.VSchema()) case pathScatterStats: @@ -1366,7 +1378,7 @@ func returnAsJSON(response http.ResponseWriter, stuff interface{}) { } // Plans returns the LRU plan cache -func (e *Executor) Plans() *cache.LRUCache { +func (e *Executor) Plans() cache.Cache { return e.plans } diff --git a/go/vt/vtgate/executor_framework_test.go b/go/vt/vtgate/executor_framework_test.go index fcc24f67b08..49ab3aa6caa 100644 --- a/go/vt/vtgate/executor_framework_test.go +++ b/go/vt/vtgate/executor_framework_test.go @@ -30,6 +30,7 @@ import ( "context" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/sqltypes" "vitess.io/vitess/go/streamlog" "vitess.io/vitess/go/vt/discovery" @@ -304,7 +305,6 @@ var unshardedVSchema = ` const ( testBufferSize = 10 - testCacheSize = int64(10) ) type DestinationAnyShardPickerFirstShard struct{} @@ -398,7 +398,7 @@ func createLegacyExecutorEnv() (executor *Executor, sbc1, sbc2, sbclookup *sandb bad.VSchema = badVSchema getSandbox(KsTestUnsharded).VSchema = unshardedVSchema - executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) key.AnyShardPicker = DestinationAnyShardPickerFirstShard{} return executor, sbc1, sbc2, sbclookup @@ -433,7 +433,7 @@ func createExecutorEnv() (executor *Executor, sbc1, sbc2, sbclookup *sandboxconn bad.VSchema = badVSchema getSandbox(KsTestUnsharded).VSchema = unshardedVSchema - executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) key.AnyShardPicker = DestinationAnyShardPickerFirstShard{} return executor, sbc1, sbc2, sbclookup @@ -453,7 +453,7 @@ func createCustomExecutor(vschema string) (executor *Executor, sbc1, sbc2, sbclo sbclookup = hc.AddTestTablet(cell, "0", 1, KsTestUnsharded, "0", topodatapb.TabletType_MASTER, true, 1, nil) getSandbox(KsTestUnsharded).VSchema = unshardedVSchema - executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) return executor, sbc1, sbc2, sbclookup } diff --git a/go/vt/vtgate/executor_scatter_stats.go b/go/vt/vtgate/executor_scatter_stats.go index 9ba7ae3ea3a..760c18e8cc4 100644 --- a/go/vt/vtgate/executor_scatter_stats.go +++ b/go/vt/vtgate/executor_scatter_stats.go @@ -56,12 +56,12 @@ func (e *Executor) gatherScatterStats() (statsResults, error) { totalExecTime := time.Duration(0) totalCount := uint64(0) + var err error plans := make([]*engine.Plan, 0) routes := make([]*engine.Route, 0) // First we go over all plans and collect statistics and all query plans for scatter queries - for _, item := range e.plans.Items() { - plan := item.Value.(*engine.Plan) - + e.plans.ForEach(func(value interface{}) bool { + plan := value.(*engine.Plan) scatter := engine.Find(findScatter, plan.Instructions) readOnly := !engine.Exists(isUpdating, plan.Instructions) isScatter := scatter != nil @@ -69,7 +69,8 @@ func (e *Executor) gatherScatterStats() (statsResults, error) { if isScatter { route, isRoute := scatter.(*engine.Route) if !isRoute { - return statsResults{}, vterrors.Errorf(vtrpc.Code_INTERNAL, "expected a route, but found a %v", scatter) + err = vterrors.Errorf(vtrpc.Code_INTERNAL, "expected a route, but found a %v", scatter) + return false } plans = append(plans, plan) routes = append(routes, route) @@ -83,6 +84,10 @@ func (e *Executor) gatherScatterStats() (statsResults, error) { totalExecTime += plan.ExecTime totalCount += plan.ExecCount + return true + }) + if err != nil { + return statsResults{}, err } // Now we'll go over all scatter queries we've found and produce result items for each diff --git a/go/vt/vtgate/executor_scatter_stats_test.go b/go/vt/vtgate/executor_scatter_stats_test.go index f1b18a6c080..12a591299cf 100644 --- a/go/vt/vtgate/executor_scatter_stats_test.go +++ b/go/vt/vtgate/executor_scatter_stats_test.go @@ -68,6 +68,8 @@ func TestScatterStatsHttpWriting(t *testing.T) { _, err = executor.Execute(context.Background(), "TestExecutorResultsExceeded", session, query4, nil) require.NoError(t, err) + executor.plans.Wait() + recorder := httptest.NewRecorder() executor.WriteScatterStats(recorder) diff --git a/go/vt/vtgate/executor_select_test.go b/go/vt/vtgate/executor_select_test.go index 39400981825..75a9ef5bcce 100644 --- a/go/vt/vtgate/executor_select_test.go +++ b/go/vt/vtgate/executor_select_test.go @@ -21,6 +21,7 @@ import ( "strings" "testing" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/test/utils" "github.com/stretchr/testify/assert" @@ -1025,7 +1026,7 @@ func TestSelectScatter(t *testing.T) { sbc := hc.AddTestTablet(cell, shard, 1, "TestExecutor", shard, topodatapb.TabletType_MASTER, true, 1, nil) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) logChan := QueryLogger.Subscribe("Test") defer QueryLogger.Unsubscribe(logChan) @@ -1057,7 +1058,7 @@ func TestSelectScatterPartial(t *testing.T) { conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) logChan := QueryLogger.Subscribe("Test") defer QueryLogger.Unsubscribe(logChan) @@ -1114,7 +1115,7 @@ func TestStreamSelectScatter(t *testing.T) { for _, shard := range shards { _ = hc.AddTestTablet(cell, shard, 1, "TestExecutor", shard, topodatapb.TabletType_MASTER, true, 1, nil) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) sql := "select id from user" result, err := executorStream(executor, sql) @@ -1167,7 +1168,7 @@ func TestSelectScatterOrderBy(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select col1, col2 from user order by col2 desc" gotResult, err := executorExec(executor, query, nil) @@ -1232,7 +1233,7 @@ func TestSelectScatterOrderByVarChar(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select col1, textcol from user order by textcol desc" gotResult, err := executorExec(executor, query, nil) @@ -1292,7 +1293,7 @@ func TestStreamSelectScatterOrderBy(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select id, col from user order by col desc" gotResult, err := executorStream(executor, query) @@ -1349,7 +1350,7 @@ func TestStreamSelectScatterOrderByVarChar(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select id, textcol from user order by textcol desc" gotResult, err := executorStream(executor, query) @@ -1406,7 +1407,7 @@ func TestSelectScatterAggregate(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select col, sum(foo) from user group by col" gotResult, err := executorExec(executor, query, nil) @@ -1463,7 +1464,7 @@ func TestStreamSelectScatterAggregate(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select col, sum(foo) from user group by col" gotResult, err := executorStream(executor, query) @@ -1521,7 +1522,7 @@ func TestSelectScatterLimit(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select col1, col2 from user order by col2 desc limit 3" gotResult, err := executorExec(executor, query, nil) @@ -1587,7 +1588,7 @@ func TestStreamSelectScatterLimit(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select col1, col2 from user order by col2 desc limit 3" gotResult, err := executorStream(executor, query) diff --git a/go/vt/vtgate/executor_stream_test.go b/go/vt/vtgate/executor_stream_test.go index c4351b79581..c07ae62a0a7 100644 --- a/go/vt/vtgate/executor_stream_test.go +++ b/go/vt/vtgate/executor_stream_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/require" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/sqltypes" "vitess.io/vitess/go/vt/discovery" querypb "vitess.io/vitess/go/vt/proto/query" @@ -59,7 +60,7 @@ func TestStreamSQLSharded(t *testing.T) { for _, shard := range shards { _ = hc.AddTestTablet(cell, shard, 1, "TestExecutor", shard, topodatapb.TabletType_MASTER, true, 1, nil) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) sql := "stream * from sharded_user_msgs" result, err := executorStreamMessages(executor, sql) diff --git a/go/vt/vtgate/executor_test.go b/go/vt/vtgate/executor_test.go index a92b107b291..0fb4b26c7aa 100644 --- a/go/vt/vtgate/executor_test.go +++ b/go/vt/vtgate/executor_test.go @@ -29,7 +29,9 @@ import ( "testing" "time" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/test/utils" + "vitess.io/vitess/go/vt/vtgate/engine" "vitess.io/vitess/go/vt/topo" @@ -1417,42 +1419,32 @@ func TestGetPlanUnnormalized(t *testing.T) { emptyvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) unshardedvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) - logStats1 := NewLogStats(ctx, "Test", "", nil) query1 := "select * from music_user_map where id = 1" - plan1, err := r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) + plan1, logStats1 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) wantSQL := query1 + " /* comment */" if logStats1.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats1.SQL) } - logStats2 := NewLogStats(ctx, "Test", "", nil) - plan2, err := r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats2) - require.NoError(t, err) + plan2, logStats2 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) if plan1 != plan2 { t.Errorf("getPlan(query1): plans must be equal: %p %p", plan1, plan2) } want := []string{ "@unknown:" + query1, } - if keys := r.plans.Keys(); !reflect.DeepEqual(keys, want) { - t.Errorf("Plan keys: %s, want %s", keys, want) - } + assertCacheContains(t, r.plans, want) if logStats2.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats2.SQL) } - logStats3 := NewLogStats(ctx, "Test", "", nil) - plan3, err := r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats3) - require.NoError(t, err) + plan3, logStats3 := getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) if plan1 == plan3 { t.Errorf("getPlan(query1, ks): plans must not be equal: %p %p", plan1, plan3) } if logStats3.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats3.SQL) } - logStats4 := NewLogStats(ctx, "Test", "", nil) - plan4, err := r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats4) - require.NoError(t, err) + plan4, logStats4 := getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) if plan3 != plan4 { t.Errorf("getPlan(query1, ks): plans must be equal: %p %p", plan3, plan4) } @@ -1460,37 +1452,59 @@ func TestGetPlanUnnormalized(t *testing.T) { KsTestUnsharded + "@unknown:" + query1, "@unknown:" + query1, } - if diff := cmp.Diff(want, r.plans.Keys()); diff != "" { - t.Errorf("\n-want,+got:\n%s", diff) - } - //if keys := r.plans.Keys(); !reflect.DeepEqual(keys, want) { - // t.Errorf("Plan keys: %s, want %s", keys, want) - //} + assertCacheContains(t, r.plans, want) if logStats4.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats4.SQL) } } +func assertCacheSize(t *testing.T, c cache.Cache, expected int) { + t.Helper() + var size int + c.ForEach(func(_ interface{}) bool { + size++ + return true + }) + if size != expected { + t.Errorf("getPlan() expected cache to have size %d, but got: %d", expected, size) + } +} + +func assertCacheContains(t *testing.T, c cache.Cache, want []string) { + t.Helper() + for _, wantKey := range want { + if _, ok := c.Get(wantKey); !ok { + t.Errorf("missing key in plan cache: %v", wantKey) + } + } +} + +func getPlanCached(t *testing.T, e *Executor, vcursor *vcursorImpl, sql string, comments sqlparser.MarginComments, bindVars map[string]*querypb.BindVariable, skipQueryPlanCache bool) (*engine.Plan, *LogStats) { + logStats := NewLogStats(ctx, "Test", "", nil) + plan, err := e.getPlan(vcursor, sql, comments, bindVars, skipQueryPlanCache, logStats) + require.NoError(t, err) + + // Wait for cache to settle + e.plans.Wait() + return plan, logStats +} + func TestGetPlanCacheUnnormalized(t *testing.T) { r, _, _, _ := createLegacyExecutorEnv() emptyvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) query1 := "select * from music_user_map where id = 1" - logStats1 := NewLogStats(ctx, "Test", "", nil) - _, err := r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, true /* skipQueryPlanCache */, logStats1) - require.NoError(t, err) - if r.plans.Size() != 0 { - t.Errorf("getPlan() expected cache to have size 0, but got: %b", r.plans.Size()) - } + + _, logStats1 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, true) + assertCacheSize(t, r.plans, 0) + wantSQL := query1 + " /* comment */" if logStats1.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats1.SQL) } - logStats2 := NewLogStats(ctx, "Test", "", nil) - _, err = r.getPlan(emptyvc, query1, makeComments(" /* comment 2 */"), map[string]*querypb.BindVariable{}, false /* skipQueryPlanCache */, logStats2) - require.NoError(t, err) - if r.plans.Size() != 1 { - t.Errorf("getPlan() expected cache to have size 1, but got: %b", r.plans.Size()) - } + + _, logStats2 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment 2 */"), map[string]*querypb.BindVariable{}, false) + assertCacheSize(t, r.plans, 1) + wantSQL = query1 + " /* comment 2 */" if logStats2.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats2.SQL) @@ -1501,58 +1515,39 @@ func TestGetPlanCacheUnnormalized(t *testing.T) { unshardedvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) query1 = "insert /*vt+ SKIP_QUERY_PLAN_CACHE=1 */ into user(id) values (1), (2)" - logStats1 = NewLogStats(ctx, "Test", "", nil) - _, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) - if len(r.plans.Keys()) != 0 { - t.Errorf("Plan keys should be 0, got: %v", len(r.plans.Keys())) - } + getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) + assertCacheSize(t, r.plans, 0) query1 = "insert into user(id) values (1), (2)" - _, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) - if len(r.plans.Keys()) != 1 { - t.Errorf("Plan keys should be 1, got: %v", len(r.plans.Keys())) - } + getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) + assertCacheSize(t, r.plans, 1) // the target string will be resolved and become part of the plan cache key, which adds a new entry ksIDVc1, _ := newVCursorImpl(context.Background(), NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "[deadbeef]"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) - _, err = r.getPlan(ksIDVc1, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) - if len(r.plans.Keys()) != 2 { - t.Errorf("Plan keys should be 2, got: %v", len(r.plans.Keys())) - } + getPlanCached(t, r, ksIDVc1, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) + assertCacheSize(t, r.plans, 2) // the target string will be resolved and become part of the plan cache key, as it's an unsharded ks, it will be the same entry as above ksIDVc2, _ := newVCursorImpl(context.Background(), NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "[beefdead]"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) - _, err = r.getPlan(ksIDVc2, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) - if len(r.plans.Keys()) != 2 { - t.Errorf("Plan keys should be 2, got: %v", len(r.plans.Keys())) - } + getPlanCached(t, r, ksIDVc2, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) + assertCacheSize(t, r.plans, 2) } func TestGetPlanCacheNormalized(t *testing.T) { r, _, _, _ := createLegacyExecutorEnv() r.normalize = true emptyvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) + query1 := "select * from music_user_map where id = 1" - logStats1 := NewLogStats(ctx, "Test", "", nil) - _, err := r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, true /* skipQueryPlanCache */, logStats1) - require.NoError(t, err) - if r.plans.Size() != 0 { - t.Errorf("getPlan() expected cache to have size 0, but got: %b", r.plans.Size()) - } + _, logStats1 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, true /* skipQueryPlanCache */) + assertCacheSize(t, r.plans, 0) wantSQL := "select * from music_user_map where id = :vtg1 /* comment */" if logStats1.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats1.SQL) } - logStats2 := NewLogStats(ctx, "Test", "", nil) - _, err = r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false /* skipQueryPlanCache */, logStats2) - require.NoError(t, err) - if r.plans.Size() != 1 { - t.Errorf("getPlan() expected cache to have size 1, but got: %b", r.plans.Size()) - } + + _, logStats2 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false /* skipQueryPlanCache */) + assertCacheSize(t, r.plans, 1) if logStats2.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats2.SQL) } @@ -1563,35 +1558,22 @@ func TestGetPlanCacheNormalized(t *testing.T) { unshardedvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) query1 = "insert /*vt+ SKIP_QUERY_PLAN_CACHE=1 */ into user(id) values (1), (2)" - logStats1 = NewLogStats(ctx, "Test", "", nil) - _, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) - if len(r.plans.Keys()) != 0 { - t.Errorf("Plan keys should be 0, got: %v", len(r.plans.Keys())) - } + getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) + assertCacheSize(t, r.plans, 0) query1 = "insert into user(id) values (1), (2)" - _, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) - if len(r.plans.Keys()) != 1 { - t.Errorf("Plan keys should be 1, got: %v", len(r.plans.Keys())) - } + getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) + assertCacheSize(t, r.plans, 1) // the target string will be resolved and become part of the plan cache key, which adds a new entry ksIDVc1, _ := newVCursorImpl(context.Background(), NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "[deadbeef]"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) - _, err = r.getPlan(ksIDVc1, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) - if len(r.plans.Keys()) != 2 { - t.Errorf("Plan keys should be 2, got: %v", len(r.plans.Keys())) - } + getPlanCached(t, r, ksIDVc1, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) + assertCacheSize(t, r.plans, 2) // the target string will be resolved and become part of the plan cache key, as it's an unsharded ks, it will be the same entry as above ksIDVc2, _ := newVCursorImpl(context.Background(), NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "[beefdead]"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) - _, err = r.getPlan(ksIDVc2, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) - if len(r.plans.Keys()) != 2 { - t.Errorf("Plan keys should be 2, got: %v", len(r.plans.Keys())) - } + getPlanCached(t, r, ksIDVc2, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) + assertCacheSize(t, r.plans, 2) } func TestGetPlanNormalized(t *testing.T) { @@ -1603,21 +1585,17 @@ func TestGetPlanNormalized(t *testing.T) { query1 := "select * from music_user_map where id = 1" query2 := "select * from music_user_map where id = 2" normalized := "select * from music_user_map where id = :vtg1" - logStats1 := NewLogStats(ctx, "Test", "", nil) - plan1, err := r.getPlan(emptyvc, query1, makeComments(" /* comment 1 */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) - logStats2 := NewLogStats(ctx, "Test", "", nil) - plan2, err := r.getPlan(emptyvc, query1, makeComments(" /* comment 2 */"), map[string]*querypb.BindVariable{}, false, logStats2) - require.NoError(t, err) + + plan1, logStats1 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment 1 */"), map[string]*querypb.BindVariable{}, false) + plan2, logStats2 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment 2 */"), map[string]*querypb.BindVariable{}, false) + if plan1 != plan2 { t.Errorf("getPlan(query1): plans must be equal: %p %p", plan1, plan2) } want := []string{ "@unknown:" + normalized, } - if keys := r.plans.Keys(); !reflect.DeepEqual(keys, want) { - t.Errorf("Plan keys: %s, want %s", keys, want) - } + assertCacheContains(t, r.plans, want) wantSQL := normalized + " /* comment 1 */" if logStats1.SQL != wantSQL { @@ -1628,9 +1606,7 @@ func TestGetPlanNormalized(t *testing.T) { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats2.SQL) } - logStats3 := NewLogStats(ctx, "Test", "", nil) - plan3, err := r.getPlan(emptyvc, query2, makeComments(" /* comment 3 */"), map[string]*querypb.BindVariable{}, false, logStats3) - require.NoError(t, err) + plan3, logStats3 := getPlanCached(t, r, emptyvc, query2, makeComments(" /* comment 3 */"), map[string]*querypb.BindVariable{}, false) if plan1 != plan3 { t.Errorf("getPlan(query2): plans must be equal: %p %p", plan1, plan3) } @@ -1639,9 +1615,7 @@ func TestGetPlanNormalized(t *testing.T) { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats3.SQL) } - logStats4 := NewLogStats(ctx, "Test", "", nil) - plan4, err := r.getPlan(emptyvc, normalized, makeComments(" /* comment 4 */"), map[string]*querypb.BindVariable{}, false, logStats4) - require.NoError(t, err) + plan4, logStats4 := getPlanCached(t, r, emptyvc, normalized, makeComments(" /* comment 4 */"), map[string]*querypb.BindVariable{}, false) if plan1 != plan4 { t.Errorf("getPlan(normalized): plans must be equal: %p %p", plan1, plan4) } @@ -1650,9 +1624,8 @@ func TestGetPlanNormalized(t *testing.T) { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats4.SQL) } - logStats5 := NewLogStats(ctx, "Test", "", nil) - plan3, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment 5 */"), map[string]*querypb.BindVariable{}, false, logStats5) - require.NoError(t, err) + var logStats5 *LogStats + plan3, logStats5 = getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment 5 */"), map[string]*querypb.BindVariable{}, false) if plan1 == plan3 { t.Errorf("getPlan(query1, ks): plans must not be equal: %p %p", plan1, plan3) } @@ -1661,9 +1634,7 @@ func TestGetPlanNormalized(t *testing.T) { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats5.SQL) } - logStats6 := NewLogStats(ctx, "Test", "", nil) - plan4, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment 6 */"), map[string]*querypb.BindVariable{}, false, logStats6) - require.NoError(t, err) + plan4, _ = getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment 6 */"), map[string]*querypb.BindVariable{}, false) if plan3 != plan4 { t.Errorf("getPlan(query1, ks): plans must be equal: %p %p", plan3, plan4) } @@ -1671,20 +1642,14 @@ func TestGetPlanNormalized(t *testing.T) { KsTestUnsharded + "@unknown:" + normalized, "@unknown:" + normalized, } - if keys := r.plans.Keys(); !reflect.DeepEqual(keys, want) { - t.Errorf("Plan keys: %s, want %s", keys, want) - } + assertCacheContains(t, r.plans, want) - // Errors - logStats7 := NewLogStats(ctx, "Test", "", nil) - _, err = r.getPlan(emptyvc, "syntax", makeComments(""), map[string]*querypb.BindVariable{}, false, logStats7) + _, err := r.getPlan(emptyvc, "syntax", makeComments(""), map[string]*querypb.BindVariable{}, false, nil) wantErr := "syntax error at position 7 near 'syntax'" if err == nil || err.Error() != wantErr { t.Errorf("getPlan(syntax): %v, want %s", err, wantErr) } - if keys := r.plans.Keys(); !reflect.DeepEqual(keys, want) { - t.Errorf("Plan keys: %s, want %s", keys, want) - } + assertCacheContains(t, r.plans, want) } func TestPassthroughDDL(t *testing.T) { diff --git a/go/vt/vtgate/executor_vschema_ddl_test.go b/go/vt/vtgate/executor_vschema_ddl_test.go index b6346761cbe..2e974c47cdb 100644 --- a/go/vt/vtgate/executor_vschema_ddl_test.go +++ b/go/vt/vtgate/executor_vschema_ddl_test.go @@ -58,14 +58,14 @@ func waitForVindex(t *testing.T, ks, name string, watch chan *vschemapb.SrvVSche t.Errorf("vschema was not updated as expected") } - // Wait up to 10ms until the vindex manager gets notified of the update + // Wait up to 100ms until the vindex manager gets notified of the update for i := 0; i < 10; i++ { vschema := executor.vm.GetCurrentSrvVschema() vindex, ok := vschema.Keyspaces[ks].Vindexes[name] if ok { return vschema, vindex } - time.Sleep(time.Millisecond) + time.Sleep(10 * time.Millisecond) } t.Fatalf("updated vschema did not contain %s", name) @@ -75,7 +75,7 @@ func waitForVindex(t *testing.T, ks, name string, watch chan *vschemapb.SrvVSche func waitForVschemaTables(t *testing.T, ks string, tables []string, executor *Executor) *vschemapb.SrvVSchema { t.Helper() - // Wait up to 10ms until the vindex manager gets notified of the update + // Wait up to 100ms until the vindex manager gets notified of the update for i := 0; i < 10; i++ { vschema := executor.vm.GetCurrentSrvVschema() gotTables := []string{} @@ -87,7 +87,7 @@ func waitForVschemaTables(t *testing.T, ks string, tables []string, executor *Ex if reflect.DeepEqual(tables, gotTables) { return vschema } - time.Sleep(time.Millisecond) + time.Sleep(10 * time.Millisecond) } t.Fatalf("updated vschema did not contain tables %v", tables) diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go index 473594f419b..da769cf8231 100644 --- a/go/vt/vtgate/planbuilder/plan_test.go +++ b/go/vt/vtgate/planbuilder/plan_test.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "io/ioutil" + "math/rand" "os" "runtime/debug" "strings" @@ -618,6 +619,43 @@ func BenchmarkPlanner(b *testing.B) { } } +func BenchmarkSelectVsDML(b *testing.B) { + vschema := &vschemaWrapper{ + v: loadSchema(b, "schema_test.json"), + sysVarEnabled: true, + version: V3, + } + + var dmlCases []testCase + var selectCases []testCase + + for tc := range iterateExecFile("dml_cases.txt") { + dmlCases = append(dmlCases, tc) + } + + for tc := range iterateExecFile("select_cases.txt") { + if tc.output2ndPlanner != "" { + selectCases = append(selectCases, tc) + } + } + + rand.Shuffle(len(dmlCases), func(i, j int) { + dmlCases[i], dmlCases[j] = dmlCases[j], dmlCases[i] + }) + + rand.Shuffle(len(selectCases), func(i, j int) { + selectCases[i], selectCases[j] = selectCases[j], selectCases[i] + }) + + b.Run("DML (random sample, N=32)", func(b *testing.B) { + benchmarkPlanner(b, V3, dmlCases[:32], vschema) + }) + + b.Run("Select (random sample, N=32)", func(b *testing.B) { + benchmarkPlanner(b, V3, selectCases[:32], vschema) + }) +} + func benchmarkPlanner(b *testing.B, version PlannerVersion, testCases []testCase, vschema *vschemaWrapper) { b.ReportAllocs() for n := 0; n < b.N; n++ { diff --git a/go/vt/vtgate/queryz.go b/go/vt/vtgate/queryz.go index bb84b5ce461..de4ca65dc58 100644 --- a/go/vt/vtgate/queryz.go +++ b/go/vt/vtgate/queryz.go @@ -135,19 +135,15 @@ func queryzHandler(e *Executor, w http.ResponseWriter, r *http.Request) { defer logz.EndHTMLTable(w) w.Write(queryzHeader) - keys := e.plans.Keys() sorter := queryzSorter{ - rows: make([]*queryzRow, 0, len(keys)), + rows: nil, less: func(row1, row2 *queryzRow) bool { return row1.timePQ() > row2.timePQ() }, } - for _, v := range e.plans.Keys() { - result, ok := e.plans.Get(v) - if !ok { - continue - } - plan := result.(*engine.Plan) + + e.plans.ForEach(func(value interface{}) bool { + plan := value.(*engine.Plan) Value := &queryzRow{ Query: logz.Wrappable(sqlparser.TruncateForUI(plan.Original)), } @@ -164,7 +160,9 @@ func queryzHandler(e *Executor, w http.ResponseWriter, r *http.Request) { Value.Color = "high" } sorter.rows = append(sorter.rows, Value) - } + return true + }) + sort.Sort(&sorter) for _, row := range sorter.rows { if err := queryzTmpl.Execute(w, row); err != nil { diff --git a/go/vt/vtgate/queryz_test.go b/go/vt/vtgate/queryz_test.go index 7a6324dbc86..d158567a11f 100644 --- a/go/vt/vtgate/queryz_test.go +++ b/go/vt/vtgate/queryz_test.go @@ -43,6 +43,7 @@ func TestQueryzHandler(t *testing.T) { sql := "select id from user where id = 1" _, err := executorExec(executor, sql, nil) require.NoError(t, err) + executor.plans.Wait() result, ok := executor.plans.Get("@master:" + sql) if !ok { t.Fatalf("couldn't get plan from cache") @@ -54,6 +55,7 @@ func TestQueryzHandler(t *testing.T) { sql = "select id from user" _, err = executorExec(executor, sql, nil) require.NoError(t, err) + executor.plans.Wait() result, ok = executor.plans.Get("@master:" + sql) if !ok { t.Fatalf("couldn't get plan from cache") @@ -67,6 +69,7 @@ func TestQueryzHandler(t *testing.T) { "name": sqltypes.BytesBindVariable([]byte("myname")), }) require.NoError(t, err) + executor.plans.Wait() result, ok = executor.plans.Get("@master:" + sql) if !ok { t.Fatalf("couldn't get plan from cache") diff --git a/go/vt/vtgate/vtgate.go b/go/vt/vtgate/vtgate.go index 65fd87537c3..32fbb701a24 100644 --- a/go/vt/vtgate/vtgate.go +++ b/go/vt/vtgate/vtgate.go @@ -29,6 +29,7 @@ import ( "context" "vitess.io/vitess/go/acl" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/sqltypes" "vitess.io/vitess/go/stats" "vitess.io/vitess/go/tb" @@ -52,15 +53,17 @@ import ( ) var ( - transactionMode = flag.String("transaction_mode", "MULTI", "SINGLE: disallow multi-db transactions, MULTI: allow multi-db transactions with best effort commit, TWOPC: allow multi-db transactions with 2pc commit") - normalizeQueries = flag.Bool("normalize_queries", true, "Rewrite queries with bind vars. Turn this off if the app itself sends normalized queries with bind vars.") - terseErrors = flag.Bool("vtgate-config-terse-errors", false, "prevent bind vars from escaping in returned errors") - streamBufferSize = flag.Int("stream_buffer_size", 32*1024, "the number of bytes sent from vtgate for each stream call. It's recommended to keep this value in sync with vttablet's query-server-config-stream-buffer-size.") - queryPlanCacheSize = flag.Int64("gate_query_cache_size", 10000, "gate server query cache size, maximum number of queries to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") - _ = flag.Bool("disable_local_gateway", false, "deprecated: if specified, this process will not route any queries to local tablets in the local cell") - maxMemoryRows = flag.Int("max_memory_rows", 300000, "Maximum number of rows that will be held in memory for intermediate results as well as the final result.") - warnMemoryRows = flag.Int("warn_memory_rows", 30000, "Warning threshold for in-memory results. A row count higher than this amount will cause the VtGateWarnings.ResultsExceeded counter to be incremented.") - defaultDDLStrategy = flag.String("ddl_strategy", string(schema.DDLStrategyDirect), "Set default strategy for DDL statements. Override with @@ddl_strategy session variable") + transactionMode = flag.String("transaction_mode", "MULTI", "SINGLE: disallow multi-db transactions, MULTI: allow multi-db transactions with best effort commit, TWOPC: allow multi-db transactions with 2pc commit") + normalizeQueries = flag.Bool("normalize_queries", true, "Rewrite queries with bind vars. Turn this off if the app itself sends normalized queries with bind vars.") + terseErrors = flag.Bool("vtgate-config-terse-errors", false, "prevent bind vars from escaping in returned errors") + streamBufferSize = flag.Int("stream_buffer_size", 32*1024, "the number of bytes sent from vtgate for each stream call. It's recommended to keep this value in sync with vttablet's query-server-config-stream-buffer-size.") + queryPlanCacheSize = flag.Int64("gate_query_cache_size", cache.DefaultConfig.MaxEntries, "gate server query cache size, maximum number of queries to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a cache. This config controls the expected amount of unique entries in the cache.") + queryPlanCacheMemory = flag.Int64("gate_query_cache_memory", cache.DefaultConfig.MaxMemoryUsage, "gate server query cache size in bytes, maximum amount of memory to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + queryPlanCacheLFU = flag.Bool("gate_query_cache_lfu", cache.DefaultConfig.LFU, "gate server cache algorithm. when set to true, a new cache algorithm based on a TinyLFU admission policy will be used to improve cache behavior and prevent pollution from sparse queries") + _ = flag.Bool("disable_local_gateway", false, "deprecated: if specified, this process will not route any queries to local tablets in the local cell") + maxMemoryRows = flag.Int("max_memory_rows", 300000, "Maximum number of rows that will be held in memory for intermediate results as well as the final result.") + warnMemoryRows = flag.Int("warn_memory_rows", 30000, "Warning threshold for in-memory results. A row count higher than this amount will cause the VtGateWarnings.ResultsExceeded counter to be incremented.") + defaultDDLStrategy = flag.String("ddl_strategy", string(schema.DDLStrategyDirect), "Set default strategy for DDL statements. Override with @@ddl_strategy session variable") // TODO(deepthi): change these two vars to unexported and move to healthcheck.go when LegacyHealthcheck is removed @@ -178,9 +181,14 @@ func Init(ctx context.Context, serv srvtopo.Server, cell string, tabletTypesToWa srvResolver := srvtopo.NewResolver(serv, gw, cell) resolver := NewResolver(srvResolver, serv, cell, sc) vsm := newVStreamManager(srvResolver, serv, cell) + cacheCfg := &cache.Config{ + MaxEntries: *queryPlanCacheSize, + MaxMemoryUsage: *queryPlanCacheMemory, + LFU: *queryPlanCacheLFU, + } rpcVTGate = &VTGate{ - executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, *queryPlanCacheSize), + executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, cacheCfg), resolver: resolver, vsm: vsm, txConn: tc, @@ -503,9 +511,14 @@ func LegacyInit(ctx context.Context, hc discovery.LegacyHealthCheck, serv srvtop srvResolver := srvtopo.NewResolver(serv, gw, cell) resolver := NewResolver(srvResolver, serv, cell, sc) vsm := newVStreamManager(srvResolver, serv, cell) + cacheCfg := &cache.Config{ + MaxEntries: *queryPlanCacheSize, + MaxMemoryUsage: *queryPlanCacheMemory, + LFU: *queryPlanCacheLFU, + } rpcVTGate = &VTGate{ - executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, *queryPlanCacheSize), + executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, cacheCfg), resolver: resolver, vsm: vsm, txConn: tc, diff --git a/go/vt/vttablet/endtoend/config_test.go b/go/vt/vttablet/endtoend/config_test.go index 6690c58e41b..9848fd3cb26 100644 --- a/go/vt/vttablet/endtoend/config_test.go +++ b/go/vt/vttablet/endtoend/config_test.go @@ -26,6 +26,7 @@ import ( "github.com/stretchr/testify/require" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/sqltypes" querypb "vitess.io/vitess/go/vt/proto/query" topodatapb "vitess.io/vitess/go/vt/proto/topodata" @@ -176,11 +177,24 @@ func TestConsolidatorReplicasOnly(t *testing.T) { } func TestQueryPlanCache(t *testing.T) { + if cache.DefaultConfig.LFU { + const cacheItemSize = 40 + const cachedPlanSize = 2275 + cacheItemSize + const cachePlanSize2 = 2254 + cacheItemSize + testQueryPlanCache(t, cachedPlanSize, cachePlanSize2) + } else { + testQueryPlanCache(t, 1, 1) + } +} + +func testQueryPlanCache(t *testing.T, cachedPlanSize, cachePlanSize2 int) { + t.Helper() + //sleep to avoid race between SchemaChanged event clearing out the plans cache which breaks this test time.Sleep(1 * time.Second) defer framework.Server.SetQueryPlanCacheCap(framework.Server.QueryPlanCacheCap()) - framework.Server.SetQueryPlanCacheCap(1) + framework.Server.SetQueryPlanCacheCap(cachedPlanSize) bindVars := map[string]*querypb.BindVariable{ "ival1": sqltypes.Int64BindVariable(1), @@ -189,20 +203,26 @@ func TestQueryPlanCache(t *testing.T) { client := framework.NewClient() _, _ = client.Execute("select * from vitess_test where intval=:ival1", bindVars) _, _ = client.Execute("select * from vitess_test where intval=:ival2", bindVars) + time.Sleep(100 * time.Millisecond) + vend := framework.DebugVars() verifyIntValue(t, vend, "QueryCacheLength", 1) - verifyIntValue(t, vend, "QueryCacheSize", 1) - verifyIntValue(t, vend, "QueryCacheCapacity", 1) + verifyIntValue(t, vend, "QueryCacheSize", cachedPlanSize) + verifyIntValue(t, vend, "QueryCacheCapacity", cachedPlanSize) - framework.Server.SetQueryPlanCacheCap(10) + framework.Server.SetQueryPlanCacheCap(64 * 1024) _, _ = client.Execute("select * from vitess_test where intval=:ival1", bindVars) + time.Sleep(100 * time.Millisecond) + vend = framework.DebugVars() verifyIntValue(t, vend, "QueryCacheLength", 2) - verifyIntValue(t, vend, "QueryCacheSize", 2) + verifyIntValue(t, vend, "QueryCacheSize", cachedPlanSize*2) _, _ = client.Execute("select * from vitess_test where intval=1", bindVars) + time.Sleep(100 * time.Millisecond) + vend = framework.DebugVars() verifyIntValue(t, vend, "QueryCacheLength", 3) - verifyIntValue(t, vend, "QueryCacheSize", 3) + verifyIntValue(t, vend, "QueryCacheSize", cachedPlanSize*2+cachePlanSize2) } func TestMaxResultSize(t *testing.T) { diff --git a/go/vt/vttablet/tabletserver/query_engine.go b/go/vt/vttablet/tabletserver/query_engine.go index c4d985a1ee0..67b6d4c68da 100644 --- a/go/vt/vttablet/tabletserver/query_engine.go +++ b/go/vt/vttablet/tabletserver/query_engine.go @@ -55,6 +55,7 @@ import ( // and track stats. type TabletPlan struct { *planbuilder.Plan + Original string Fields []*querypb.Field Rules *rules.Rules Authorized []*tableacl.ACLResult @@ -68,11 +69,6 @@ type TabletPlan struct { ErrorCount int64 } -// Size allows TabletPlan to be in cache.LRUCache. -func (*TabletPlan) Size() int { - return 1 -} - // AddStats updates the stats for the current TabletPlan. func (ep *TabletPlan) AddStats(queryCount int64, duration, mysqlTime time.Duration, rowsAffected, rowsReturned, errorCount int64) { ep.mu.Lock() @@ -123,7 +119,7 @@ type QueryEngine struct { // mu protects the following fields. mu sync.RWMutex tables map[string]*schema.Table - plans *cache.LRUCache + plans cache.Cache queryRuleSources *rules.Map // Pools @@ -168,11 +164,17 @@ type QueryEngine struct { // You must call this only once. func NewQueryEngine(env tabletenv.Env, se *schema.Engine) *QueryEngine { config := env.Config() + cacheCfg := &cache.Config{ + MaxEntries: int64(config.QueryCacheSize), + MaxMemoryUsage: config.QueryCacheMemory, + LFU: config.QueryCacheLFU, + } + qe := &QueryEngine{ env: env, se: se, tables: make(map[string]*schema.Table), - plans: cache.NewLRUCache(int64(config.QueryCacheSize)), + plans: cache.NewDefaultCacheImpl(cacheCfg), queryRuleSources: rules.NewMap(), } @@ -214,13 +216,12 @@ func NewQueryEngine(env tabletenv.Env, se *schema.Engine) *QueryEngine { env.Exporter().NewGaugeFunc("StreamBufferSize", "Query engine stream buffer size", qe.streamBufferSize.Get) env.Exporter().NewCounterFunc("TableACLExemptCount", "Query engine table ACL exempt count", qe.tableaclExemptCount.Get) - env.Exporter().NewGaugeFunc("QueryCacheLength", "Query engine query cache length", qe.plans.Length) - env.Exporter().NewGaugeFunc("QueryCacheSize", "Query engine query cache size", qe.plans.Size) - env.Exporter().NewGaugeFunc("QueryCacheCapacity", "Query engine query cache capacity", qe.plans.Capacity) + env.Exporter().NewGaugeFunc("QueryCacheLength", "Query engine query cache length", func() int64 { + return int64(qe.plans.Len()) + }) + env.Exporter().NewGaugeFunc("QueryCacheSize", "Query engine query cache size", qe.plans.UsedCapacity) + env.Exporter().NewGaugeFunc("QueryCacheCapacity", "Query engine query cache capacity", qe.plans.MaxCapacity) env.Exporter().NewCounterFunc("QueryCacheEvictions", "Query engine query cache evictions", qe.plans.Evictions) - env.Exporter().Publish("QueryCacheOldest", stats.StringFunc(func() string { - return fmt.Sprintf("%v", qe.plans.Oldest()) - })) qe.queryCounts = env.Exporter().NewCountersWithMultiLabels("QueryCounts", "query counts", []string{"Table", "Plan"}) qe.queryTimes = env.Exporter().NewCountersWithMultiLabels("QueryTimesNs", "query times in ns", []string{"Table", "Plan"}) qe.queryRowCounts = env.Exporter().NewCountersWithMultiLabels("QueryRowCounts", "query row counts", []string{"Table", "Plan"}) @@ -289,6 +290,7 @@ func (qe *QueryEngine) GetPlan(ctx context.Context, logStats *tabletenv.LogStats defer span.Finish() if plan := qe.getQuery(sql); plan != nil { + logStats.CachedPlan = true return plan, nil } @@ -308,7 +310,7 @@ func (qe *QueryEngine) GetPlan(ctx context.Context, logStats *tabletenv.LogStats if err != nil { return nil, err } - plan := &TabletPlan{Plan: splan} + plan := &TabletPlan{Plan: splan, Original: sql} plan.Rules = qe.queryRuleSources.FilterByPlan(sql, plan.PlanID, plan.TableName().String()) plan.buildAuthorized() if plan.PlanID.IsSelect() { @@ -346,7 +348,7 @@ func (qe *QueryEngine) GetStreamPlan(sql string, isReservedConn bool) (*TabletPl if err != nil { return nil, err } - plan := &TabletPlan{Plan: splan} + plan := &TabletPlan{Plan: splan, Original: sql} plan.Rules = qe.queryRuleSources.FilterByPlan(sql, plan.PlanID, plan.TableName().String()) plan.buildAuthorized() return plan, nil @@ -402,14 +404,6 @@ func (qe *QueryEngine) getQuery(sql string) *TabletPlan { return nil } -// peekQuery fetches the plan without changing the LRU order. -func (qe *QueryEngine) peekQuery(sql string) *TabletPlan { - if cacheResult, ok := qe.plans.Peek(sql); ok { - return cacheResult.(*TabletPlan) - } - return nil -} - // SetQueryPlanCacheCap sets the query plan cache capacity. func (qe *QueryEngine) SetQueryPlanCacheCap(size int) { if size <= 0 { @@ -420,7 +414,7 @@ func (qe *QueryEngine) SetQueryPlanCacheCap(size int) { // QueryPlanCacheCap returns the capacity of the query cache. func (qe *QueryEngine) QueryPlanCacheCap() int { - return int(qe.plans.Capacity()) + return int(qe.plans.MaxCapacity()) } // AddStats adds the given stats for the planName.tableName @@ -450,20 +444,19 @@ func (qe *QueryEngine) handleHTTPQueryPlans(response http.ResponseWriter, reques acl.SendError(response, err) return } - keys := qe.plans.Keys() + response.Header().Set("Content-Type", "text/plain") - response.Write([]byte(fmt.Sprintf("Length: %d\n", len(keys)))) - for _, v := range keys { - response.Write([]byte(fmt.Sprintf("%#v\n", sqlparser.TruncateForUI(v)))) - if plan := qe.peekQuery(v); plan != nil { - if b, err := json.MarshalIndent(plan.Plan, "", " "); err != nil { - response.Write([]byte(err.Error())) - } else { - response.Write(b) - } - response.Write(([]byte)("\n\n")) + qe.plans.ForEach(func(value interface{}) bool { + plan := value.(*TabletPlan) + response.Write([]byte(fmt.Sprintf("%#v\n", sqlparser.TruncateForUI(plan.Original)))) + if b, err := json.MarshalIndent(plan.Plan, "", " "); err != nil { + response.Write([]byte(err.Error())) + } else { + response.Write(b) } - } + response.Write(([]byte)("\n\n")) + return true + }) } func (qe *QueryEngine) handleHTTPQueryStats(response http.ResponseWriter, request *http.Request) { @@ -471,20 +464,20 @@ func (qe *QueryEngine) handleHTTPQueryStats(response http.ResponseWriter, reques acl.SendError(response, err) return } - keys := qe.plans.Keys() response.Header().Set("Content-Type", "application/json; charset=utf-8") - qstats := make([]perQueryStats, 0, len(keys)) - for _, v := range keys { - if plan := qe.peekQuery(v); plan != nil { - var pqstats perQueryStats - pqstats.Query = unicoded(sqlparser.TruncateForUI(v)) - pqstats.Table = plan.TableName().String() - pqstats.Plan = plan.PlanID - pqstats.QueryCount, pqstats.Time, pqstats.MysqlTime, pqstats.RowsAffected, pqstats.RowsReturned, pqstats.ErrorCount = plan.Stats() - - qstats = append(qstats, pqstats) - } - } + var qstats []perQueryStats + qe.plans.ForEach(func(value interface{}) bool { + plan := value.(*TabletPlan) + + var pqstats perQueryStats + pqstats.Query = unicoded(sqlparser.TruncateForUI(plan.Original)) + pqstats.Table = plan.TableName().String() + pqstats.Plan = plan.PlanID + pqstats.QueryCount, pqstats.Time, pqstats.MysqlTime, pqstats.RowsAffected, pqstats.RowsReturned, pqstats.ErrorCount = plan.Stats() + + qstats = append(qstats, pqstats) + return true + }) if b, err := json.MarshalIndent(qstats, "", " "); err != nil { response.Write([]byte(err.Error())) } else { diff --git a/go/vt/vttablet/tabletserver/query_engine_test.go b/go/vt/vttablet/tabletserver/query_engine_test.go index 9ca7469a275..029fc363e31 100644 --- a/go/vt/vttablet/tabletserver/query_engine_test.go +++ b/go/vt/vttablet/tabletserver/query_engine_test.go @@ -17,16 +17,24 @@ limitations under the License. package tabletserver import ( + "context" "expvar" + "fmt" + "math/rand" "net/http" "net/http/httptest" + "os" + "path" "reflect" "strings" + "sync" + "sync/atomic" "testing" "time" - "context" + "github.com/stretchr/testify/require" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/streamlog" "vitess.io/vitess/go/mysql/fakesqldb" @@ -92,7 +100,7 @@ func TestGetPlanPanicDuetoEmptyQuery(t *testing.T) { for query, result := range schematest.Queries() { db.AddQuery(query, result) } - qe := newTestQueryEngine(10, 10*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(10*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() @@ -112,7 +120,7 @@ func TestGetMessageStreamPlan(t *testing.T) { for query, result := range schematest.Queries() { db.AddQuery(query, result) } - qe := newTestQueryEngine(10, 10*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(10*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() @@ -137,6 +145,17 @@ func TestGetMessageStreamPlan(t *testing.T) { } } +func assertPlanCacheSize(t *testing.T, qe *QueryEngine, expected int) { + var size int + qe.plans.ForEach(func(_ interface{}) bool { + size++ + return true + }) + if size != expected { + t.Fatalf("expected query plan cache to contain %d entries, found %d", expected, size) + } +} + func TestQueryPlanCache(t *testing.T) { db := fakesqldb.New(t) defer db.Close() @@ -149,14 +168,18 @@ func TestQueryPlanCache(t *testing.T) { db.AddQuery("select * from test_table_01 where 1 != 1", &sqltypes.Result{}) db.AddQuery("select * from test_table_02 where 1 != 1", &sqltypes.Result{}) - qe := newTestQueryEngine(10, 10*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(10*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() ctx := context.Background() logStats := tabletenv.NewLogStats(ctx, "GetPlanStats") - qe.SetQueryPlanCacheCap(1) + if cache.DefaultConfig.LFU { + qe.SetQueryPlanCacheCap(1024) + } else { + qe.SetQueryPlanCacheCap(1) + } firstPlan, err := qe.GetPlan(ctx, logStats, firstQuery, false, false /* inReservedConn */) if err != nil { t.Fatal(err) @@ -174,9 +197,7 @@ func TestQueryPlanCache(t *testing.T) { expvar.Do(func(kv expvar.KeyValue) { _ = kv.Value.String() }) - if qe.plans.Size() == 0 { - t.Fatalf("query plan cache should not be 0") - } + assertPlanCacheSize(t, qe, 1) qe.ClearQueryPlanCache() } @@ -191,14 +212,14 @@ func TestNoQueryPlanCache(t *testing.T) { db.AddQuery("select * from test_table_01 where 1 != 1", &sqltypes.Result{}) db.AddQuery("select * from test_table_02 where 1 != 1", &sqltypes.Result{}) - qe := newTestQueryEngine(10, 10*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(10*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() ctx := context.Background() logStats := tabletenv.NewLogStats(ctx, "GetPlanStats") - qe.SetQueryPlanCacheCap(1) + qe.SetQueryPlanCacheCap(1024) firstPlan, err := qe.GetPlan(ctx, logStats, firstQuery, true, false /* inReservedConn */) if err != nil { t.Fatal(err) @@ -206,9 +227,7 @@ func TestNoQueryPlanCache(t *testing.T) { if firstPlan == nil { t.Fatalf("plan should not be nil") } - if qe.plans.Size() != 0 { - t.Fatalf("query plan cache should be 0") - } + assertPlanCacheSize(t, qe, 0) qe.ClearQueryPlanCache() } @@ -223,14 +242,14 @@ func TestNoQueryPlanCacheDirective(t *testing.T) { db.AddQuery("select /*vt+ SKIP_QUERY_PLAN_CACHE=1 */ * from test_table_01 where 1 != 1", &sqltypes.Result{}) db.AddQuery("select /*vt+ SKIP_QUERY_PLAN_CACHE=1 */ * from test_table_02 where 1 != 1", &sqltypes.Result{}) - qe := newTestQueryEngine(10, 10*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(10*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() ctx := context.Background() logStats := tabletenv.NewLogStats(ctx, "GetPlanStats") - qe.SetQueryPlanCacheCap(1) + qe.SetQueryPlanCacheCap(1024) firstPlan, err := qe.GetPlan(ctx, logStats, firstQuery, false, false /* inReservedConn */) if err != nil { t.Fatal(err) @@ -238,9 +257,7 @@ func TestNoQueryPlanCacheDirective(t *testing.T) { if firstPlan == nil { t.Fatalf("plan should not be nil") } - if qe.plans.Size() != 0 { - t.Fatalf("query plan cache should be 0") - } + assertPlanCacheSize(t, qe, 0) qe.ClearQueryPlanCache() } @@ -252,7 +269,7 @@ func TestStatsURL(t *testing.T) { } query := "select * from test_table_01" db.AddQuery("select * from test_table_01 where 1 != 1", &sqltypes.Result{}) - qe := newTestQueryEngine(10, 1*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(1*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() @@ -274,10 +291,9 @@ func TestStatsURL(t *testing.T) { qe.handleHTTPQueryRules(response, request) } -func newTestQueryEngine(queryCacheSize int, idleTimeout time.Duration, strict bool, dbcfgs *dbconfigs.DBConfigs) *QueryEngine { +func newTestQueryEngine(idleTimeout time.Duration, strict bool, dbcfgs *dbconfigs.DBConfigs) *QueryEngine { config := tabletenv.NewDefaultConfig() config.DB = dbcfgs - config.QueryCacheSize = queryCacheSize config.OltpReadPool.IdleTimeoutSeconds.Set(idleTimeout) config.OlapReadPool.IdleTimeoutSeconds.Set(idleTimeout) config.TxPool.IdleTimeoutSeconds.Set(idleTimeout) @@ -292,7 +308,7 @@ func runConsolidatedQuery(t *testing.T, sql string) *QueryEngine { db := fakesqldb.New(t) defer db.Close() - qe := newTestQueryEngine(10, 1*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(1*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() @@ -346,3 +362,137 @@ func TestConsolidationsUIRedaction(t *testing.T) { t.Fatalf("Response missing redacted consolidated query: %v %v", redactedSQL, redactedResponse.Body.String()) } } + +func TestPlanCachePollution(t *testing.T) { + plotPath := os.Getenv("CACHE_PLOT_PATH") + if plotPath == "" { + t.Skipf("CACHE_PLOT_PATH not set") + } + + const NormalQueries = 500000 + const PollutingQueries = NormalQueries / 2 + + db := fakesqldb.New(t) + defer db.Close() + + for query, result := range schematest.Queries() { + db.AddQuery(query, result) + } + + db.AddQueryPattern(".*", &sqltypes.Result{}) + + dbcfgs := newDBConfigs(db) + config := tabletenv.NewDefaultConfig() + config.DB = dbcfgs + // config.LFUQueryCacheSizeBytes = 3 * 1024 * 1024 + + env := tabletenv.NewEnv(config, "TabletServerTest") + se := schema.NewEngine(env) + qe := NewQueryEngine(env, se) + + se.InitDBConfig(dbcfgs.DbaWithDB()) + se.Open() + + qe.Open() + defer qe.Close() + + type Stats struct { + queries uint64 + cached uint64 + interval time.Duration + } + + var stats1, stats2 Stats + var wg sync.WaitGroup + + go func() { + cacheMode := "lru" + if config.QueryCacheLFU { + cacheMode = "lfu" + } + + out, err := os.Create(path.Join(plotPath, + fmt.Sprintf("cache_plot_%d_%d_%s.dat", + config.QueryCacheSize, config.QueryCacheMemory, cacheMode, + )), + ) + require.NoError(t, err) + defer out.Close() + + var last1 uint64 + var last2 uint64 + + for range time.Tick(100 * time.Millisecond) { + var avg1, avg2 time.Duration + + if stats1.queries-last1 > 0 { + avg1 = stats1.interval / time.Duration(stats1.queries-last1) + } + if stats2.queries-last2 > 0 { + avg2 = stats2.interval / time.Duration(stats2.queries-last2) + } + + stats1.interval = 0 + last1 = stats1.queries + stats2.interval = 0 + last2 = stats2.queries + + cacheUsed, cacheCap := qe.plans.UsedCapacity(), qe.plans.MaxCapacity() + + t.Logf("%d queries (%f hit rate), cache %d / %d (%f usage), %v %v", + stats1.queries+stats2.queries, + float64(stats1.cached)/float64(stats1.queries), + cacheUsed, cacheCap, + float64(cacheUsed)/float64(cacheCap), avg1, avg2) + + if out != nil { + fmt.Fprintf(out, "%d %f %f %f %f %d %d\n", + stats1.queries+stats2.queries, + float64(stats1.queries)/float64(NormalQueries), + float64(stats2.queries)/float64(PollutingQueries), + float64(stats1.cached)/float64(stats1.queries), + float64(cacheUsed)/float64(cacheCap), + avg1.Microseconds(), + avg2.Microseconds(), + ) + } + } + }() + + runner := func(totalQueries uint64, stats *Stats, sample func() string) { + for i := uint64(0); i < totalQueries; i++ { + ctx := context.Background() + logStats := tabletenv.NewLogStats(ctx, "GetPlanStats") + query := sample() + + start := time.Now() + _, err := qe.GetPlan(ctx, logStats, query, false, false /* inReservedConn */) + require.NoErrorf(t, err, "bad query: %s", query) + stats.interval += time.Since(start) + + atomic.AddUint64(&stats.queries, 1) + if logStats.CachedPlan { + atomic.AddUint64(&stats.cached, 1) + } + } + } + + wg.Add(2) + + go func() { + defer wg.Done() + runner(NormalQueries, &stats1, func() string { + return fmt.Sprintf("SELECT (a, b, c) FROM test_table_%d", rand.Intn(5000)) + }) + }() + + go func() { + defer wg.Done() + time.Sleep(500 * time.Millisecond) + runner(PollutingQueries, &stats2, func() string { + return fmt.Sprintf("INSERT INTO test_table_00 VALUES (1, 2, 3, %d)", rand.Int()) + }) + }() + + wg.Wait() +} diff --git a/go/vt/vttablet/tabletserver/queryz.go b/go/vt/vttablet/tabletserver/queryz.go index 5c30e6bb6e6..f44816f8f14 100644 --- a/go/vt/vttablet/tabletserver/queryz.go +++ b/go/vt/vttablet/tabletserver/queryz.go @@ -145,20 +145,19 @@ func queryzHandler(qe *QueryEngine, w http.ResponseWriter, r *http.Request) { defer logz.EndHTMLTable(w) w.Write(queryzHeader) - keys := qe.plans.Keys() sorter := queryzSorter{ - rows: make([]*queryzRow, 0, len(keys)), + rows: nil, less: func(row1, row2 *queryzRow) bool { return row1.timePQ() > row2.timePQ() }, } - for _, v := range qe.plans.Keys() { - plan := qe.peekQuery(v) + qe.plans.ForEach(func(value interface{}) bool { + plan := value.(*TabletPlan) if plan == nil { - continue + return true } Value := &queryzRow{ - Query: logz.Wrappable(sqlparser.TruncateForUI(v)), + Query: logz.Wrappable(sqlparser.TruncateForUI(plan.Original)), Table: plan.TableName().String(), Plan: plan.PlanID, } @@ -175,7 +174,8 @@ func queryzHandler(qe *QueryEngine, w http.ResponseWriter, r *http.Request) { Value.Color = "high" } sorter.rows = append(sorter.rows, Value) - } + return true + }) sort.Sort(&sorter) for _, Value := range sorter.rows { if err := queryzTmpl.Execute(w, Value); err != nil { diff --git a/go/vt/vttablet/tabletserver/queryz_test.go b/go/vt/vttablet/tabletserver/queryz_test.go index 7c805eedc3c..90278cbf22b 100644 --- a/go/vt/vttablet/tabletserver/queryz_test.go +++ b/go/vt/vttablet/tabletserver/queryz_test.go @@ -35,50 +35,60 @@ import ( func TestQueryzHandler(t *testing.T) { resp := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/schemaz", nil) - qe := newTestQueryEngine(100, 10*time.Second, true, &dbconfigs.DBConfigs{}) + qe := newTestQueryEngine(10*time.Second, true, &dbconfigs.DBConfigs{}) + const query1 = "select name from test_table" plan1 := &TabletPlan{ + Original: query1, Plan: &planbuilder.Plan{ Table: &schema.Table{Name: sqlparser.NewTableIdent("test_table")}, PlanID: planbuilder.PlanSelect, }, } plan1.AddStats(10, 2*time.Second, 1*time.Second, 0, 2, 0) - qe.plans.Set("select name from test_table", plan1) + qe.plans.Set(query1, plan1) + const query2 = "insert into test_table values 1" plan2 := &TabletPlan{ + Original: query2, Plan: &planbuilder.Plan{ Table: &schema.Table{Name: sqlparser.NewTableIdent("test_table")}, PlanID: planbuilder.PlanDDL, }, } plan2.AddStats(1, 2*time.Millisecond, 1*time.Millisecond, 1, 0, 0) - qe.plans.Set("insert into test_table values 1", plan2) + qe.plans.Set(query2, plan2) + const query3 = "show tables" plan3 := &TabletPlan{ + Original: query3, Plan: &planbuilder.Plan{ Table: &schema.Table{Name: sqlparser.NewTableIdent("")}, PlanID: planbuilder.PlanOtherRead, }, } plan3.AddStats(1, 75*time.Millisecond, 50*time.Millisecond, 0, 1, 0) - qe.plans.Set("show tables", plan3) + qe.plans.Set(query3, plan3) qe.plans.Set("", (*TabletPlan)(nil)) + hugeInsert := "insert into test_table values 0" + for i := 1; i < 1000; i++ { + hugeInsert = hugeInsert + fmt.Sprintf(", %d", i) + } plan4 := &TabletPlan{ + Original: hugeInsert, Plan: &planbuilder.Plan{ Table: &schema.Table{Name: sqlparser.NewTableIdent("")}, PlanID: planbuilder.PlanOtherRead, }, } plan4.AddStats(1, 1*time.Millisecond, 1*time.Millisecond, 1, 0, 0) - hugeInsert := "insert into test_table values 0" - for i := 1; i < 1000; i++ { - hugeInsert = hugeInsert + fmt.Sprintf(", %d", i) - } qe.plans.Set(hugeInsert, plan4) qe.plans.Set("", (*TabletPlan)(nil)) + // Wait for cache to settle + qe.plans.Wait() + queryzHandler(qe, resp, req) body, _ := ioutil.ReadAll(resp.Body) planPattern1 := []string{ @@ -158,6 +168,6 @@ func TestQueryzHandler(t *testing.T) { func checkQueryzHasPlan(t *testing.T, planPattern []string, plan *TabletPlan, page []byte) { matcher := regexp.MustCompile(strings.Join(planPattern, `\s*`)) if !matcher.Match(page) { - t.Fatalf("queryz page does not contain\nplan:\n%v\npattern:\n%v\npage:\n%s", plan, strings.Join(planPattern, `\s*`), string(page)) + t.Fatalf("queryz page does not contain\nplan:\n%#v\npattern:\n%v\npage:\n%s", plan, strings.Join(planPattern, `\s*`), string(page)) } } diff --git a/go/vt/vttablet/tabletserver/tabletenv/config.go b/go/vt/vttablet/tabletserver/tabletenv/config.go index ce415f4e2b0..113126c157d 100644 --- a/go/vt/vttablet/tabletserver/tabletenv/config.go +++ b/go/vt/vttablet/tabletserver/tabletenv/config.go @@ -24,6 +24,7 @@ import ( "github.com/golang/protobuf/proto" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/flagutil" "vitess.io/vitess/go/streamlog" "vitess.io/vitess/go/vt/dbconfigs" @@ -103,6 +104,8 @@ func init() { flag.IntVar(¤tConfig.StreamBufferSize, "queryserver-config-stream-buffer-size", defaultConfig.StreamBufferSize, "query server stream buffer size, the maximum number of bytes sent from vttablet for each stream call. It's recommended to keep this value in sync with vtgate's stream_buffer_size.") flag.IntVar(¤tConfig.QueryCacheSize, "queryserver-config-query-cache-size", defaultConfig.QueryCacheSize, "query server query cache size, maximum number of queries to be cached. vttablet analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + flag.Int64Var(¤tConfig.QueryCacheMemory, "queryserver-config-query-cache-memory", defaultConfig.QueryCacheMemory, "query server query cache size in bytes, maximum amount of memory to be used for caching. vttablet analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + flag.BoolVar(¤tConfig.QueryCacheLFU, "queryserver-config-query-cache-lfu", defaultConfig.QueryCacheLFU, "query server cache algorithm. when set to true, a new cache algorithm based on a TinyLFU admission policy will be used to improve cache behavior and prevent pollution from sparse queries") SecondsVar(¤tConfig.SchemaReloadIntervalSeconds, "queryserver-config-schema-reload-time", defaultConfig.SchemaReloadIntervalSeconds, "query server schema reload time, how often vttablet reloads schemas from underlying MySQL instance in seconds. vttablet keeps table schemas in its own memory and periodically refreshes it from MySQL. This config controls the reload time.") SecondsVar(¤tConfig.Oltp.QueryTimeoutSeconds, "queryserver-config-query-timeout", defaultConfig.Oltp.QueryTimeoutSeconds, "query server query timeout (in seconds), this is the query timeout in vttablet side. If a query takes more than this timeout, it will be killed.") SecondsVar(¤tConfig.OltpReadPool.TimeoutSeconds, "queryserver-config-query-pool-timeout", defaultConfig.OltpReadPool.TimeoutSeconds, "query server query pool timeout (in seconds), it is how long vttablet waits for a connection from the query pool. If set to 0 (default) then the overall query timeout is used instead.") @@ -243,6 +246,8 @@ type TabletConfig struct { PassthroughDML bool `json:"passthroughDML,omitempty"` StreamBufferSize int `json:"streamBufferSize,omitempty"` QueryCacheSize int `json:"queryCacheSize,omitempty"` + QueryCacheMemory int64 `json:"queryCacheMemory,omitempty"` + QueryCacheLFU bool `json:"queryCacheLFU,omitempty"` SchemaReloadIntervalSeconds Seconds `json:"schemaReloadIntervalSeconds,omitempty"` WatchReplication bool `json:"watchReplication,omitempty"` TrackSchemaVersions bool `json:"trackSchemaVersions,omitempty"` @@ -447,7 +452,9 @@ var defaultConfig = TabletConfig{ // great (the overhead makes the final packets on the wire about twice // bigger than this). StreamBufferSize: 32 * 1024, - QueryCacheSize: 5000, + QueryCacheSize: int(cache.DefaultConfig.MaxEntries), + QueryCacheMemory: cache.DefaultConfig.MaxMemoryUsage, + QueryCacheLFU: cache.DefaultConfig.LFU, SchemaReloadIntervalSeconds: 30 * 60, MessagePostponeParallelism: 4, CacheResultFields: true, diff --git a/go/vt/vttablet/tabletserver/tabletenv/config_test.go b/go/vt/vttablet/tabletserver/tabletenv/config_test.go index 3930ce48a5c..76d9bd6ac17 100644 --- a/go/vt/vttablet/tabletserver/tabletenv/config_test.go +++ b/go/vt/vttablet/tabletserver/tabletenv/config_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/vt/dbconfigs" "vitess.io/vitess/go/yaml2" ) @@ -130,6 +131,7 @@ oltpReadPool: idleTimeoutSeconds: 1800 maxWaiters: 5000 size: 16 +queryCacheMemory: 33554432 queryCacheSize: 5000 replicationTracker: heartbeatIntervalSeconds: 0.25 @@ -190,7 +192,9 @@ func TestFlags(t *testing.T) { MaxConcurrency: 5, }, StreamBufferSize: 32768, - QueryCacheSize: 5000, + QueryCacheSize: int(cache.DefaultConfig.MaxEntries), + QueryCacheMemory: cache.DefaultConfig.MaxMemoryUsage, + QueryCacheLFU: cache.DefaultConfig.LFU, SchemaReloadIntervalSeconds: 1800, TrackSchemaVersions: false, MessagePostponeParallelism: 4, diff --git a/go/vt/vttablet/tabletserver/tabletenv/logstats.go b/go/vt/vttablet/tabletserver/tabletenv/logstats.go index 5e63bd89db0..6fc131d74fa 100644 --- a/go/vt/vttablet/tabletserver/tabletenv/logstats.go +++ b/go/vt/vttablet/tabletserver/tabletenv/logstats.go @@ -61,6 +61,7 @@ type LogStats struct { TransactionID int64 ReservedID int64 Error error + CachedPlan bool } // NewLogStats constructs a new LogStats with supplied Method and ctx diff --git a/tools/e2e_test_runner.sh b/tools/e2e_test_runner.sh index c581957a366..dc2edbf0e59 100755 --- a/tools/e2e_test_runner.sh +++ b/tools/e2e_test_runner.sh @@ -45,7 +45,7 @@ all_except_flaky_and_cluster_tests=$(echo "$packages_with_tests" | grep -vE ".+ flaky_tests=$(echo "$packages_with_tests" | grep -E ".+ .+_flaky_test\.go" | grep -vE "go/test/endtoend" | cut -d" " -f1) # Run non-flaky tests. -echo "$all_except_flaky_and_cluster_tests" | xargs go test $VT_GO_PARALLEL +echo "$all_except_flaky_and_cluster_tests" | xargs go test -count=1 $VT_GO_PARALLEL if [ $? -ne 0 ]; then echo "ERROR: Go unit tests failed. See above for errors." echo