diff --git a/go.mod b/go.mod index d792b94de..8513f6394 100644 --- a/go.mod +++ b/go.mod @@ -62,6 +62,7 @@ require ( golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.6.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect google.golang.org/grpc v1.57.1 // indirect diff --git a/go.sum b/go.sum index bc8fd481f..05986eacf 100644 --- a/go.sum +++ b/go.sum @@ -481,6 +481,8 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/newrelic/config.go b/newrelic/config.go index 01eb9a3a9..87ac6ae99 100644 --- a/newrelic/config.go +++ b/newrelic/config.go @@ -13,6 +13,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/mitchellh/go-homedir" + "golang.org/x/sync/semaphore" + "golang.org/x/time/rate" insights "github.com/newrelic/go-insights/client" nr "github.com/newrelic/newrelic-client-go/v2/newrelic" @@ -20,22 +22,49 @@ import ( // Config contains New Relic provider settings type Config struct { - AdminAPIKey string - PersonalAPIKey string - Region string - APIURL string - CACertFile string - InfrastructureAPIURL string - InsecureSkipVerify bool - InsightsAccountID string - InsightsInsertKey string - InsightsInsertURL string - InsightsQueryKey string - InsightsQueryURL string - NerdGraphAPIURL string - SyntheticsAPIURL string - userAgent string - serviceName string + AdminAPIKey string + PersonalAPIKey string + Region string + APIURL string + CACertFile string + InfrastructureAPIURL string + InsecureSkipVerify bool + InsightsAccountID string + InsightsInsertKey string + InsightsInsertURL string + InsightsQueryKey string + InsightsQueryURL string + NerdGraphAPIURL string + SyntheticsAPIURL string + userAgent string + serviceName string + MaxRequestsPerSecond int + MaxConcurrentRequests int +} + +type ThrottledRoundTripper struct { + original http.RoundTripper + ratelimiter *rate.Limiter + concurrency *semaphore.Weighted +} + +func NewThrottledRoundTripper(rt http.RoundTripper, maxRequestsPerSecond int, maxConcurrentRequests int) *ThrottledRoundTripper { + return &ThrottledRoundTripper{ + original: rt, + ratelimiter: rate.NewLimiter(rate.Limit(maxRequestsPerSecond), maxRequestsPerSecond), + concurrency: semaphore.NewWeighted(int64(maxConcurrentRequests)), + } +} + +func (t *ThrottledRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + ctx := req.Context() + + if err := t.concurrency.Acquire(ctx, 1); err != nil { + return nil, err + } + defer t.concurrency.Release(1) + t.ratelimiter.Wait(ctx) + return t.original.RoundTrip(req) } // Client returns a new client for accessing New Relic @@ -75,7 +104,8 @@ func (c *Config) Client() (*nr.NewRelic, error) { t = logging.NewTransport("newrelic", t) } - options = append(options, nr.ConfigHTTPTransport(t)) + throttledTransport := NewThrottledRoundTripper(t, c.MaxRequestsPerSecond, c.MaxConcurrentRequests) + options = append(options, nr.ConfigHTTPTransport(throttledTransport)) if c.APIURL != "" { options = append(options, nr.ConfigBaseURL(c.APIURL)) diff --git a/newrelic/provider.go b/newrelic/provider.go index dfeecd2f6..8ad9073bb 100755 --- a/newrelic/provider.go +++ b/newrelic/provider.go @@ -117,6 +117,16 @@ func Provider() *schema.Provider { Optional: true, DefaultFunc: schema.EnvDefaultFunc("NEW_RELIC_API_CACERT", ""), }, + "max_requests_per_second": { + Type: schema.TypeInt, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("NEW_RELIC_MAX_REQUESTS_PER_SECOND", 600), + }, + "max_concurrent_requests": { + Type: schema.TypeInt, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("NEW_RELIC_MAX_CONCURRENT_REQUESTS", 25), + }, }, DataSourcesMap: map[string]*schema.Resource{ @@ -234,17 +244,19 @@ func providerConfigure(data *schema.ResourceData, terraformVersion string) (inte log.Printf("[INFO] UserAgent: %s", userAgent) cfg := Config{ - AdminAPIKey: adminAPIKey, - PersonalAPIKey: personalAPIKey, - Region: data.Get("region").(string), - APIURL: data.Get("api_url").(string), - SyntheticsAPIURL: data.Get("synthetics_api_url").(string), - NerdGraphAPIURL: data.Get("nerdgraph_api_url").(string), - InfrastructureAPIURL: getInfraAPIURL(data), - userAgent: userAgent, - InsecureSkipVerify: data.Get("insecure_skip_verify").(bool), - CACertFile: data.Get("cacert_file").(string), - serviceName: userAgentServiceName, + AdminAPIKey: adminAPIKey, + PersonalAPIKey: personalAPIKey, + Region: data.Get("region").(string), + APIURL: data.Get("api_url").(string), + SyntheticsAPIURL: data.Get("synthetics_api_url").(string), + NerdGraphAPIURL: data.Get("nerdgraph_api_url").(string), + InfrastructureAPIURL: getInfraAPIURL(data), + userAgent: userAgent, + InsecureSkipVerify: data.Get("insecure_skip_verify").(bool), + CACertFile: data.Get("cacert_file").(string), + serviceName: userAgentServiceName, + MaxRequestsPerSecond: data.Get("max_requests_per_second").(int), + MaxConcurrentRequests: data.Get("max_concurrent_requests").(int), } log.Println("[INFO] Initializing newrelic-client-go")