From 114d9ad5d3489537fe940c9161941c14c4436d59 Mon Sep 17 00:00:00 2001 From: Richard Kovacs Date: Tue, 13 Feb 2024 15:43:26 +0100 Subject: [PATCH] Cache Linode API GET calls --- .gitignore | 2 + cloud/scope/common.go | 87 +++++++++++++++++++++++++++++++++++++++---- go.mod | 2 + go.sum | 9 +++++ util/defaults.go | 38 +++++++++++++++++++ 5 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 util/defaults.go diff --git a/.gitignore b/.gitignore index 938e4bfb2..a8d4f996b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .DS_Store .idea +.vscode +__debug_bin* bin/* kind-logs-* cover.out diff --git a/cloud/scope/common.go b/cloud/scope/common.go index 518edc5a8..62934c196 100644 --- a/cloud/scope/common.go +++ b/cloud/scope/common.go @@ -1,21 +1,94 @@ package scope import ( + "fmt" "net/http" + "sync" + "github.com/bxcodec/gotcha" + "github.com/bxcodec/gotcha/cache" + "github.com/bxcodec/httpcache" + "github.com/bxcodec/httpcache/cache/inmem" + "github.com/linode/cluster-api-provider-linode/util" "github.com/linode/linodego" "golang.org/x/oauth2" ) +var ( + initClient sync.Once + linodeClient linodego.Client +) + func createLinodeClient(apiKey string) *linodego.Client { - tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: apiKey}) + initClient.Do(func() { + //nolint:forcetypeassert // Always OK. + baseTrans := http.DefaultTransport.(*http.Transport).Clone() + baseTrans.MaxIdleConns = util.DefaultLinodeAPIMaxIdleConns + // Otherwise we use just a few connections. + baseTrans.MaxConnsPerHost = baseTrans.MaxIdleConns + baseTrans.MaxIdleConnsPerHost = baseTrans.MaxIdleConns - oauth2Client := &http.Client{ - Transport: &oauth2.Transport{ - Source: tokenSource, - }, - } - linodeClient := linodego.NewClient(oauth2Client) + oauthTrans := &oauth2.Transport{ + Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: apiKey}), + Base: baseTrans, + } + + oauth2Client := &http.Client{ + Timeout: util.DefaultLinodeAPITimeout, + Transport: oauthTrans, + } + + cacheStore := gotcha.New(&cache.Option{ + AlgorithmType: cache.LRUAlgorithm, + ExpiryTime: util.DefaultLinodeAPICacheExpiration, + MaxSizeItem: util.DefaultLinodeAPICacheMaxSizeItem, + MaxMemory: util.DefaultLinodeAPICacheMaxMemory, + }) + cachedTrans := httpcache.NewCacheHandlerRoundtrip(&maxAgeFixRoundTripper{ + oauthTrans, + }, false, inmem.NewCache(cacheStore)) + + oauth2Client.Transport = &reqURIFixRoundTripper{ + plain: oauthTrans, + cached: cachedTrans, + } + + linodeClient = linodego.NewClient(oauth2Client) + }) return &linodeClient } + +type maxAgeFixRoundTripper struct { + http.RoundTripper +} + +func (r *maxAgeFixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := r.RoundTripper.RoundTrip(req) + if err == nil && req.Method == http.MethodGet { + // Workaround, because Linode API sends max-age=0 all the time. + resp.Header.Set("Cache-Control", util.DefaultLinodeAPICacheControlOverride) + } + + return resp, err +} + +type reqURIFixRoundTripper struct { + plain http.RoundTripper + cached http.RoundTripper +} + +func (r *reqURIFixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if req.Method != http.MethodGet { + return r.plain.RoundTrip(req) + } + + // Workaround because Linodego doesn't set it. + req.RequestURI = req.URL.Path + + if filter := req.Header.Get("X-Filter"); filter != "" { + req.RequestURI = fmt.Sprintf("%s?filter=%s", req.RequestURI, filter) + } + + return r.cached.RoundTrip(req) +} diff --git a/go.mod b/go.mod index 69bb337c0..e85feded1 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 toolchain go1.21.5 require ( + github.com/bxcodec/httpcache v1.0.0-beta.3 github.com/go-logr/logr v1.4.1 github.com/google/uuid v1.3.1 github.com/linode/linodego v1.28.0 @@ -23,6 +24,7 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bxcodec/gotcha v1.0.0-beta.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect diff --git a/go.sum b/go.sum index 86ceb7f2c..18e40c079 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bxcodec/gotcha v1.0.0-beta.2 h1:0jY/Mx6O5jzM2fkcz84zzyy67hLu/bKGJEFTtcRmw5I= +github.com/bxcodec/gotcha v1.0.0-beta.2/go.mod h1:MEL9PRYL9Squu1zxreMIzJU6xtMouPmQybWEtXrL1nk= +github.com/bxcodec/httpcache v1.0.0-beta.3 h1:4h+Yoda40PthVkiFa5hPtCoH46qbDRq/iCT7M5ACoR4= +github.com/bxcodec/httpcache v1.0.0-beta.3/go.mod h1:NdIkH0u1smImRyp35nIGeqG5IFeKbnyyBIv8cEGSXMY= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -119,6 +123,8 @@ github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -145,8 +151,10 @@ github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ai github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -261,6 +269,7 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/util/defaults.go b/util/defaults.go new file mode 100644 index 000000000..cf94e1ffd --- /dev/null +++ b/util/defaults.go @@ -0,0 +1,38 @@ +/* +Copyright 2023 Akamai Technologies, Inc. + +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 util + +import ( + "time" + + "github.com/bxcodec/gotcha/cache" +) + +const ( + // DefaultLinodeAPITimeout is the default timeout for Linode API calls. + DefaultLinodeAPITimeout = 10 * time.Second + // DefaultLinodeAPIMaxIdleConns is the default max idle connections for Linode API calls. + DefaultLinodeAPIMaxIdleConns = 100 + // DefaultLinodeAPICacheControlOverride is the default cache control override for Linode API calls. + DefaultLinodeAPICacheControlOverride = "max-age=60" + // DefaultLinodeAPICacheExpiration is the default cache expiration for Linode API calls. + DefaultLinodeAPICacheExpiration = 5 * time.Second + // DefaultLinodeAPICacheMaxSizeItem is the default cache max size item for Linode API calls. + DefaultLinodeAPICacheMaxSizeItem = 500 + // DefaultLinodeAPICacheMaxMemory is the default cache max memory for Linode API calls. + DefaultLinodeAPICacheMaxMemory = 1 * cache.MB +)