diff --git a/backend/backend.go b/backend/backend.go index b1dcdcdf85..072da88730 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -51,7 +51,6 @@ import ( "github.com/BitBoxSwiss/bitbox-wallet-app/util/logging" "github.com/BitBoxSwiss/bitbox-wallet-app/util/observable" "github.com/BitBoxSwiss/bitbox-wallet-app/util/observable/action" - "github.com/BitBoxSwiss/bitbox-wallet-app/util/ratelimit" "github.com/BitBoxSwiss/bitbox-wallet-app/util/socksproxy" "github.com/btcsuite/btcd/chaincfg" "github.com/ethereum/go-ethereum/params" @@ -285,7 +284,7 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe backend.notifier = notifier backend.socksProxy = backendProxy backend.httpClient = hclient - backend.etherScanHTTPClient = ratelimit.FromTransport(hclient.Transport, etherscan.CallInterval) + backend.etherScanHTTPClient = hclient ratesCache := filepath.Join(arguments.CacheDirectoryPath(), "exchangerates") if err := os.MkdirAll(ratesCache, 0700); err != nil { diff --git a/backend/coins/eth/etherscan/etherscan.go b/backend/coins/eth/etherscan/etherscan.go index f1d51d0e6b..fe7d29e561 100644 --- a/backend/coins/eth/etherscan/etherscan.go +++ b/backend/coins/eth/etherscan/etherscan.go @@ -35,11 +35,13 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "golang.org/x/time/rate" ) -// CallInterval is the duration between etherscan requests. +// callsPerSec is thenumber of etherscanr equests allowed +// per second. // Etherscan rate limits to one request per 0.2 seconds. -var CallInterval = 260 * time.Millisecond +var callsPerSec = 3.8 const apiKey = "X3AFAGQT2QCAFTFPIH9VJY88H9PIQ2UWP7" @@ -50,6 +52,7 @@ const ERC20GasErr = "insufficient funds for gas * price + value" type EtherScan struct { url string httpClient *http.Client + limiter *rate.Limiter } // NewEtherScan creates a new instance of EtherScan. @@ -57,10 +60,14 @@ func NewEtherScan(url string, httpClient *http.Client) *EtherScan { return &EtherScan{ url: url, httpClient: httpClient, + limiter: rate.NewLimiter(rate.Limit(callsPerSec), 1), } } -func (etherScan *EtherScan) call(params url.Values, result interface{}) error { +func (etherScan *EtherScan) call(ctx context.Context, params url.Values, result interface{}) error { + if err := etherScan.limiter.Wait(ctx); err != nil { + return errp.WithStack(err) + } params.Set("apikey", apiKey) response, err := etherScan.httpClient.Get(etherScan.url + "?" + params.Encode()) if err != nil { @@ -323,7 +330,7 @@ func (etherScan *EtherScan) Transactions( result := struct { Result []*Transaction }{} - if err := etherScan.call(params, &result); err != nil { + if err := etherScan.call(context.TODO(), params, &result); err != nil { return nil, err } isERC20 := erc20Token != nil @@ -338,7 +345,7 @@ func (etherScan *EtherScan) Transactions( resultInternal := struct { Result []*Transaction }{} - if err := etherScan.call(params, &resultInternal); err != nil { + if err := etherScan.call(context.TODO(), params, &resultInternal); err != nil { return nil, err } var err error @@ -353,7 +360,7 @@ func (etherScan *EtherScan) Transactions( // ----- RPC node proxy methods follow -func (etherScan *EtherScan) rpcCall(params url.Values, result interface{}) error { +func (etherScan *EtherScan) rpcCall(ctx context.Context, params url.Values, result interface{}) error { params.Set("module", "proxy") var wrapped struct { @@ -364,7 +371,7 @@ func (etherScan *EtherScan) rpcCall(params url.Values, result interface{}) error } `json:"error"` Result *json.RawMessage `json:"result"` } - if err := etherScan.call(params, &wrapped); err != nil { + if err := etherScan.call(ctx, params, &wrapped); err != nil { return err } if wrapped.Error != nil { @@ -389,7 +396,7 @@ func (etherScan *EtherScan) TransactionReceiptWithBlockNumber( params.Set("action", "eth_getTransactionReceipt") params.Set("txhash", hash.Hex()) var result *rpcclient.RPCTransactionReceipt - if err := etherScan.rpcCall(params, &result); err != nil { + if err := etherScan.rpcCall(ctx, params, &result); err != nil { return nil, err } return result, nil @@ -402,7 +409,7 @@ func (etherScan *EtherScan) TransactionByHash( params.Set("action", "eth_getTransactionByHash") params.Set("txhash", hash.Hex()) var result rpcclient.RPCTransaction - if err := etherScan.rpcCall(params, &result); err != nil { + if err := etherScan.rpcCall(ctx, params, &result); err != nil { return nil, false, err } return &result.Transaction, result.BlockNumber == nil, nil @@ -415,7 +422,7 @@ func (etherScan *EtherScan) BlockNumber(ctx context.Context) (*big.Int, error) { params.Set("tag", "latest") params.Set("boolean", "false") var header *types.Header - if err := etherScan.rpcCall(params, &header); err != nil { + if err := etherScan.rpcCall(ctx, params, &header); err != nil { return nil, err } return header.Number, nil @@ -434,7 +441,7 @@ func (etherScan *EtherScan) Balance(ctx context.Context, account common.Address) params.Set("action", "balance") params.Set("address", account.Hex()) params.Set("tag", "latest") - if err := etherScan.call(params, &result); err != nil { + if err := etherScan.call(ctx, params, &result); err != nil { return nil, err } if result.Status != "1" { @@ -461,7 +468,7 @@ func (etherScan *EtherScan) ERC20Balance(account common.Address, erc20Token *erc params.Set("address", account.Hex()) params.Set("contractaddress", erc20Token.ContractAddress().Hex()) params.Set("tag", "latest") - if err := etherScan.call(params, &result); err != nil { + if err := etherScan.call(context.TODO(), params, &result); err != nil { return nil, err } if result.Status != "1" { @@ -485,7 +492,7 @@ func (etherScan *EtherScan) CallContract(ctx context.Context, msg ethereum.CallM panic("not implemented") } var result hexutil.Bytes - if err := etherScan.rpcCall(params, &result); err != nil { + if err := etherScan.rpcCall(ctx, params, &result); err != nil { return nil, err } return result, nil @@ -515,7 +522,7 @@ func (etherScan *EtherScan) EstimateGas(ctx context.Context, msg ethereum.CallMs callMsgParams(¶ms, msg) var result hexutil.Uint64 - if err := etherScan.rpcCall(params, &result); err != nil { + if err := etherScan.rpcCall(ctx, params, &result); err != nil { return 0, err } return uint64(result), nil @@ -528,7 +535,7 @@ func (etherScan *EtherScan) PendingNonceAt(ctx context.Context, account common.A params.Set("address", account.Hex()) params.Set("tag", "pending") var result hexutil.Uint64 - if err := etherScan.rpcCall(params, &result); err != nil { + if err := etherScan.rpcCall(ctx, params, &result); err != nil { return 0, err } return uint64(result), nil @@ -544,7 +551,7 @@ func (etherScan *EtherScan) SendTransaction(ctx context.Context, tx *types.Trans params := url.Values{} params.Set("action", "eth_sendRawTransaction") params.Set("hex", hexutil.Encode(encodedTx)) - return etherScan.rpcCall(params, nil) + return etherScan.rpcCall(ctx, params, nil) } // SuggestGasPrice implements rpc.Interface. @@ -552,7 +559,7 @@ func (etherScan *EtherScan) SuggestGasPrice(ctx context.Context) (*big.Int, erro params := url.Values{} params.Set("action", "eth_gasPrice") var result hexutil.Big - if err := etherScan.rpcCall(params, &result); err != nil { + if err := etherScan.rpcCall(ctx, params, &result); err != nil { return nil, err } return (*big.Int)(&result), nil @@ -581,7 +588,7 @@ func (etherScan *EtherScan) FeeTargets(ctx context.Context) ([]*ethtypes.FeeTarg params := url.Values{} params.Set("module", "gastracker") params.Set("action", "gasoracle") - if err := etherScan.call(params, &result); err != nil { + if err := etherScan.call(ctx, params, &result); err != nil { return nil, err } // Convert string fields to int64 diff --git a/backend/rates/gecko.go b/backend/rates/gecko.go index dbf789e91b..dcfca47060 100644 --- a/backend/rates/gecko.go +++ b/backend/rates/gecko.go @@ -1,6 +1,10 @@ package rates -import "time" +import ( + "time" + + "golang.org/x/time/rate" +) const ( // See the following for docs and details: https://www.coingecko.com/en/api. @@ -16,21 +20,19 @@ const ( maxGeckoRange = 364 * 24 * time.Hour ) -// apiRateLimit specifies the minimal interval between equally spaced API calls +// apiRateLimit specifies the maximum number of API calls per second // to one of the supported exchange rates providers. -func apiRateLimit(baseURL string) time.Duration { +func apiRateLimit(baseURL string) rate.Limit { switch baseURL { default: - return time.Second // arbitrary; localhost, staging, etc. + return rate.Limit(1) // arbitrary; localhost, staging, etc. case coingeckoAPIV3: // API calls. From https://www.coingecko.com/en/api: // > Generous rate limits with up to 100 requests/minute // We use slightly lower value. - return 2 * time.Second + return rate.Limit(0.5) case shiftGeckoMirrorAPIV3: - // Avoid zero to prevent unexpected panics like in time.NewTicker - // and leave some room to breathe. - return 10 * time.Millisecond + return rate.Limit(100) } } diff --git a/backend/rates/history.go b/backend/rates/history.go index d6a9994483..a30a70d265 100644 --- a/backend/rates/history.go +++ b/backend/rates/history.go @@ -278,38 +278,38 @@ func (updater *RateUpdater) fetchGeckoMarketRange(ctx context.Context, coin, fia } // Make the call, abiding the upstream rate limits. - msg := fmt.Sprintf("fetch coingecko coin=%s fiat=%s start=%s", coin, fiat, timeRange.start) var jsonBody struct{ Prices [][2]float64 } // [timestamp in milliseconds, value] - callErr := updater.geckoLimiter.Call(ctx, msg, func() error { - param := url.Values{ - "from": {strconv.FormatInt(timeRange.start.Unix(), 10)}, - "to": {strconv.FormatInt(timeRange.end().Unix(), 10)}, - "vs_currency": {gfiat}, - } - endpoint := fmt.Sprintf("%s/coins/%s/market_chart/range?%s", updater.coingeckoURL, gcoin, param.Encode()) - req, err := http.NewRequest(http.MethodGet, endpoint, nil) - if err != nil { - return err - } + if err := updater.geckoLimiter.Wait(ctx); err != nil { + return nil, err + } - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - res, err := updater.httpClient.Do(req.WithContext(ctx)) - if err != nil { - return err - } - defer res.Body.Close() //nolint:errcheck - if res.StatusCode != http.StatusOK { - return fmt.Errorf("fetchGeckoMarketRange: bad response code %d", res.StatusCode) - } - // 1Mb is more than enough for a single response, but make sure initial - // download with empty cache fits here. See maxGeckoRange. - return json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(&jsonBody) - }) - if callErr != nil { - return nil, callErr + param := url.Values{ + "from": {strconv.FormatInt(timeRange.start.Unix(), 10)}, + "to": {strconv.FormatInt(timeRange.end().Unix(), 10)}, + "vs_currency": {gfiat}, + } + endpoint := fmt.Sprintf("%s/coins/%s/market_chart/range?%s", updater.coingeckoURL, gcoin, param.Encode()) + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + res, err := updater.httpClient.Do(req.WithContext(ctx)) + if err != nil { + return nil, err + } + defer res.Body.Close() //nolint:errcheck + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("fetchGeckoMarketRange: bad response code %d", res.StatusCode) } + // 1Mb is more than enough for a single response, but make sure initial + // download with empty cache fits here. See maxGeckoRange + if err := json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(&jsonBody); err != nil { + return nil, err + } // Transform the response into a usable result. rates := make([]exchangeRate, len(jsonBody.Prices)) for i, v := range jsonBody.Prices { diff --git a/backend/rates/rates.go b/backend/rates/rates.go index 3f4811a029..58ade173ec 100644 --- a/backend/rates/rates.go +++ b/backend/rates/rates.go @@ -27,11 +27,12 @@ import ( "sync" "time" + "golang.org/x/time/rate" + "github.com/BitBoxSwiss/bitbox-wallet-app/util/errp" "github.com/BitBoxSwiss/bitbox-wallet-app/util/logging" "github.com/BitBoxSwiss/bitbox-wallet-app/util/observable" "github.com/BitBoxSwiss/bitbox-wallet-app/util/observable/action" - "github.com/BitBoxSwiss/bitbox-wallet-app/util/ratelimit" "github.com/sirupsen/logrus" "go.etcd.io/bbolt" ) @@ -119,15 +120,13 @@ type RateUpdater struct { // See https://www.coingecko.com/en/api for details. coingeckoURL string // All requests to coingeckoURL are rate-limited using geckoLimiter. - geckoLimiter *ratelimit.LimitedCall + geckoLimiter *rate.Limiter } // NewRateUpdater returns a new rates updater. // The dbdir argument is the location of a historical rates database cache. // The returned updater can function without a valid database cache but may be // impacted by rate limits. The database cache is transparent to the updater users. -// To stay within acceptable rate limits defined by CoinGeckoRateLimit, callers can -// use util/ratelimit package. // // Both Last and PriceAt of the newly created updater always return zero values // until data is fetched from the external APIs. To make the updater start fetching data @@ -155,7 +154,7 @@ func NewRateUpdater(client *http.Client, dbdir string) *RateUpdater { log: log, httpClient: client, coingeckoURL: apiURL, - geckoLimiter: ratelimit.NewLimitedCall(apiRateLimit(apiURL)), + geckoLimiter: rate.NewLimiter(apiRateLimit(apiURL), 1), } } @@ -275,36 +274,44 @@ func (updater *RateUpdater) updateLast(ctx context.Context) { } var geckoRates map[string]map[string]float64 - callErr := updater.geckoLimiter.Call(ctx, "updateLast", func() error { - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - res, err := updater.httpClient.Do(req.WithContext(ctx)) - if err != nil { - return errp.WithStack(err) - } - defer res.Body.Close() //nolint:errcheck - if res.StatusCode != http.StatusOK { - return errp.Newf("bad response code %d", res.StatusCode) - } - const max = 10240 - responseBody, err := io.ReadAll(io.LimitReader(res.Body, max+1)) - if err != nil { - return errp.WithStack(err) - } - if len(responseBody) > max { - return errp.Newf("rates response too long (> %d bytes)", max) - } - if err := json.Unmarshal(responseBody, &geckoRates); err != nil { - return errp.WithMessage(err, - fmt.Sprintf("could not parse rates response: %s", string(responseBody))) - } - return nil - }) - if callErr != nil { - updater.log.WithError(callErr).Errorf("updateLast") + if err := updater.geckoLimiter.Wait(ctx); err != nil { + updater.log.WithError(err).Error("updatelast: could not wait for rate limiter") + updater.last = nil + return + } + + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + res, err := updater.httpClient.Do(req.WithContext(ctx)) + if err != nil { + updater.log.WithError(err).Error("updatelast: could not make request") + updater.last = nil + return + } + defer res.Body.Close() //nolint:errcheck + if res.StatusCode != http.StatusOK { + updater.log.Errorf("updatelast: bad response code %d", res.StatusCode) updater.last = nil return } + const max = 10240 + responseBody, err := io.ReadAll(io.LimitReader(res.Body, max+1)) + if err != nil { + updater.log.WithError(err).Error("updatelast: could not read response") + updater.last = nil + return + } + if len(responseBody) > max { + updater.last = nil + updater.log.Errorf("updatelast: rates response too long (> %d bytes)", max) + return + } + if err := json.Unmarshal(responseBody, &geckoRates); err != nil { + updater.last = nil + updater.log.Errorf("updatelast: could not parse rates response: %s", string(responseBody)) + return + } + // Convert the map with coingecko coin/fiat codes to a map of coin/fiat units. rates := map[string]map[string]float64{} for coin, val := range geckoRates { diff --git a/go.mod b/go.mod index 29c41da042..144d698e51 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( golang.org/x/crypto v0.32.0 golang.org/x/mobile v0.0.0-20240716161057-1ad2df20a8b6 golang.org/x/net v0.34.0 + golang.org/x/time v0.10.0 ) require ( diff --git a/go.sum b/go.sum index 368cb08cbd..783a68c736 100644 --- a/go.sum +++ b/go.sum @@ -288,8 +288,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= diff --git a/util/ratelimit/ratelimit.go b/util/ratelimit/ratelimit.go deleted file mode 100644 index 8266ecbcb9..0000000000 --- a/util/ratelimit/ratelimit.go +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2020 Shift Crypto AG -// -// 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 ratelimit provides a util http.RoundTripper which rate limits its calls. -package ratelimit - -import ( - "context" - "net/http" - "time" - - "github.com/BitBoxSwiss/bitbox-wallet-app/util/logging" - "github.com/sirupsen/logrus" -) - -// FromTransport creates a new HTTP client wrapping base with RateLimitedHTTPTransport. -// The arguments are the same as in NewRateLimitedHTTPTransport. -func FromTransport(base http.RoundTripper, callInterval time.Duration) *http.Client { - rt := NewRateLimitedHTTPTransport(base, callInterval) - return &http.Client{Transport: rt} -} - -// RateLimitedHTTPTransport is a http.RoundTripper that rate limits the requests, -// waiting at least callInterval between requests. -// -// It is suitable only for the requests without a deadline because the transport -// does not extend the timeout duration of a request's context while being blocked -// by the callInterval limit. In such cases, LimitedCall is a better choice. -type RateLimitedHTTPTransport struct { - base http.RoundTripper - callLimiter *LimitedCall -} - -// NewRateLimitedHTTPTransport make a new rate limited http transport. -// If base is nil, http.DefaultTransport is used. -func NewRateLimitedHTTPTransport( - base http.RoundTripper, callInterval time.Duration) *RateLimitedHTTPTransport { - if base == nil { - base = http.DefaultTransport - } - return &RateLimitedHTTPTransport{ - base: base, - callLimiter: NewLimitedCall(callInterval), - } -} - -// RoundTrip implements http.RoundTripper, rate limiting the requests. -func (transport *RateLimitedHTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { - var callRes *http.Response - callErr := transport.callLimiter.Call(req.Context(), req.URL.String(), func() error { - res, err := transport.base.RoundTrip(req) - callRes = res - return err - }) - return callRes, callErr -} - -// LimitedCall allows to rate-limit recurring function calls. -type LimitedCall struct { - tickInterval time.Duration - tickCh chan struct{} - log *logrus.Entry -} - -// NewLimitedCall creates new LimitedCall which allows a function to be called -// at most once per the specified internval. -func NewLimitedCall(minInterval time.Duration) *LimitedCall { - l := &LimitedCall{ - tickInterval: minInterval, - tickCh: make(chan struct{}), - log: logging.Get().WithGroup("ratelimit"), - } - go l.tick() - return l -} - -func (l *LimitedCall) tick() { - l.tickCh <- struct{}{} - time.AfterFunc(l.tickInterval, l.tick) -} - -// Call blocks fn from being executed until at least minInterval, specified -// in NewLimitedCall, passed after the previous invocation or the context is done. -// It propagates the error returned by fn as is. -// -// The logAnnotate arg is the message logged together with the elapsed time -// of how long fn is blocked for, periodically. -func (l *LimitedCall) Call(ctx context.Context, logAnnotate string, fn func() error) error { - logInterval := 5 * time.Second // avoid excessive logging - if logInterval < l.tickInterval { - logInterval = l.tickInterval - } - var elapsed time.Duration - for { - select { - case <-l.tickCh: - if elapsed > 0 { - l.log.Printf("calling %s after %v", logAnnotate, elapsed) - } - return fn() - case <-ctx.Done(): - return ctx.Err() - case <-time.After(logInterval): - elapsed += logInterval - l.log.Printf("waiting to call %s for %v now", logAnnotate, elapsed) - } - } -} diff --git a/vendor/golang.org/x/time/LICENSE b/vendor/golang.org/x/time/LICENSE new file mode 100644 index 0000000000..2a7cf70da6 --- /dev/null +++ b/vendor/golang.org/x/time/LICENSE @@ -0,0 +1,27 @@ +Copyright 2009 The Go Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/time/PATENTS b/vendor/golang.org/x/time/PATENTS new file mode 100644 index 0000000000..733099041f --- /dev/null +++ b/vendor/golang.org/x/time/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/time/rate/rate.go b/vendor/golang.org/x/time/rate/rate.go new file mode 100644 index 0000000000..ec5f0cdd0c --- /dev/null +++ b/vendor/golang.org/x/time/rate/rate.go @@ -0,0 +1,426 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package rate provides a rate limiter. +package rate + +import ( + "context" + "fmt" + "math" + "sync" + "time" +) + +// Limit defines the maximum frequency of some events. +// Limit is represented as number of events per second. +// A zero Limit allows no events. +type Limit float64 + +// Inf is the infinite rate limit; it allows all events (even if burst is zero). +const Inf = Limit(math.MaxFloat64) + +// Every converts a minimum time interval between events to a Limit. +func Every(interval time.Duration) Limit { + if interval <= 0 { + return Inf + } + return 1 / Limit(interval.Seconds()) +} + +// A Limiter controls how frequently events are allowed to happen. +// It implements a "token bucket" of size b, initially full and refilled +// at rate r tokens per second. +// Informally, in any large enough time interval, the Limiter limits the +// rate to r tokens per second, with a maximum burst size of b events. +// As a special case, if r == Inf (the infinite rate), b is ignored. +// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets. +// +// The zero value is a valid Limiter, but it will reject all events. +// Use NewLimiter to create non-zero Limiters. +// +// Limiter has three main methods, Allow, Reserve, and Wait. +// Most callers should use Wait. +// +// Each of the three methods consumes a single token. +// They differ in their behavior when no token is available. +// If no token is available, Allow returns false. +// If no token is available, Reserve returns a reservation for a future token +// and the amount of time the caller must wait before using it. +// If no token is available, Wait blocks until one can be obtained +// or its associated context.Context is canceled. +// +// The methods AllowN, ReserveN, and WaitN consume n tokens. +// +// Limiter is safe for simultaneous use by multiple goroutines. +type Limiter struct { + mu sync.Mutex + limit Limit + burst int + tokens float64 + // last is the last time the limiter's tokens field was updated + last time.Time + // lastEvent is the latest time of a rate-limited event (past or future) + lastEvent time.Time +} + +// Limit returns the maximum overall event rate. +func (lim *Limiter) Limit() Limit { + lim.mu.Lock() + defer lim.mu.Unlock() + return lim.limit +} + +// Burst returns the maximum burst size. Burst is the maximum number of tokens +// that can be consumed in a single call to Allow, Reserve, or Wait, so higher +// Burst values allow more events to happen at once. +// A zero Burst allows no events, unless limit == Inf. +func (lim *Limiter) Burst() int { + lim.mu.Lock() + defer lim.mu.Unlock() + return lim.burst +} + +// TokensAt returns the number of tokens available at time t. +func (lim *Limiter) TokensAt(t time.Time) float64 { + lim.mu.Lock() + _, tokens := lim.advance(t) // does not mutate lim + lim.mu.Unlock() + return tokens +} + +// Tokens returns the number of tokens available now. +func (lim *Limiter) Tokens() float64 { + return lim.TokensAt(time.Now()) +} + +// NewLimiter returns a new Limiter that allows events up to rate r and permits +// bursts of at most b tokens. +func NewLimiter(r Limit, b int) *Limiter { + return &Limiter{ + limit: r, + burst: b, + tokens: float64(b), + } +} + +// Allow reports whether an event may happen now. +func (lim *Limiter) Allow() bool { + return lim.AllowN(time.Now(), 1) +} + +// AllowN reports whether n events may happen at time t. +// Use this method if you intend to drop / skip events that exceed the rate limit. +// Otherwise use Reserve or Wait. +func (lim *Limiter) AllowN(t time.Time, n int) bool { + return lim.reserveN(t, n, 0).ok +} + +// A Reservation holds information about events that are permitted by a Limiter to happen after a delay. +// A Reservation may be canceled, which may enable the Limiter to permit additional events. +type Reservation struct { + ok bool + lim *Limiter + tokens int + timeToAct time.Time + // This is the Limit at reservation time, it can change later. + limit Limit +} + +// OK returns whether the limiter can provide the requested number of tokens +// within the maximum wait time. If OK is false, Delay returns InfDuration, and +// Cancel does nothing. +func (r *Reservation) OK() bool { + return r.ok +} + +// Delay is shorthand for DelayFrom(time.Now()). +func (r *Reservation) Delay() time.Duration { + return r.DelayFrom(time.Now()) +} + +// InfDuration is the duration returned by Delay when a Reservation is not OK. +const InfDuration = time.Duration(math.MaxInt64) + +// DelayFrom returns the duration for which the reservation holder must wait +// before taking the reserved action. Zero duration means act immediately. +// InfDuration means the limiter cannot grant the tokens requested in this +// Reservation within the maximum wait time. +func (r *Reservation) DelayFrom(t time.Time) time.Duration { + if !r.ok { + return InfDuration + } + delay := r.timeToAct.Sub(t) + if delay < 0 { + return 0 + } + return delay +} + +// Cancel is shorthand for CancelAt(time.Now()). +func (r *Reservation) Cancel() { + r.CancelAt(time.Now()) +} + +// CancelAt indicates that the reservation holder will not perform the reserved action +// and reverses the effects of this Reservation on the rate limit as much as possible, +// considering that other reservations may have already been made. +func (r *Reservation) CancelAt(t time.Time) { + if !r.ok { + return + } + + r.lim.mu.Lock() + defer r.lim.mu.Unlock() + + if r.lim.limit == Inf || r.tokens == 0 || r.timeToAct.Before(t) { + return + } + + // calculate tokens to restore + // The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved + // after r was obtained. These tokens should not be restored. + restoreTokens := float64(r.tokens) - r.limit.tokensFromDuration(r.lim.lastEvent.Sub(r.timeToAct)) + if restoreTokens <= 0 { + return + } + // advance time to now + t, tokens := r.lim.advance(t) + // calculate new number of tokens + tokens += restoreTokens + if burst := float64(r.lim.burst); tokens > burst { + tokens = burst + } + // update state + r.lim.last = t + r.lim.tokens = tokens + if r.timeToAct == r.lim.lastEvent { + prevEvent := r.timeToAct.Add(r.limit.durationFromTokens(float64(-r.tokens))) + if !prevEvent.Before(t) { + r.lim.lastEvent = prevEvent + } + } +} + +// Reserve is shorthand for ReserveN(time.Now(), 1). +func (lim *Limiter) Reserve() *Reservation { + return lim.ReserveN(time.Now(), 1) +} + +// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen. +// The Limiter takes this Reservation into account when allowing future events. +// The returned Reservation’s OK() method returns false if n exceeds the Limiter's burst size. +// Usage example: +// +// r := lim.ReserveN(time.Now(), 1) +// if !r.OK() { +// // Not allowed to act! Did you remember to set lim.burst to be > 0 ? +// return +// } +// time.Sleep(r.Delay()) +// Act() +// +// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events. +// If you need to respect a deadline or cancel the delay, use Wait instead. +// To drop or skip events exceeding rate limit, use Allow instead. +func (lim *Limiter) ReserveN(t time.Time, n int) *Reservation { + r := lim.reserveN(t, n, InfDuration) + return &r +} + +// Wait is shorthand for WaitN(ctx, 1). +func (lim *Limiter) Wait(ctx context.Context) (err error) { + return lim.WaitN(ctx, 1) +} + +// WaitN blocks until lim permits n events to happen. +// It returns an error if n exceeds the Limiter's burst size, the Context is +// canceled, or the expected wait time exceeds the Context's Deadline. +// The burst limit is ignored if the rate limit is Inf. +func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) { + // The test code calls lim.wait with a fake timer generator. + // This is the real timer generator. + newTimer := func(d time.Duration) (<-chan time.Time, func() bool, func()) { + timer := time.NewTimer(d) + return timer.C, timer.Stop, func() {} + } + + return lim.wait(ctx, n, time.Now(), newTimer) +} + +// wait is the internal implementation of WaitN. +func (lim *Limiter) wait(ctx context.Context, n int, t time.Time, newTimer func(d time.Duration) (<-chan time.Time, func() bool, func())) error { + lim.mu.Lock() + burst := lim.burst + limit := lim.limit + lim.mu.Unlock() + + if n > burst && limit != Inf { + return fmt.Errorf("rate: Wait(n=%d) exceeds limiter's burst %d", n, burst) + } + // Check if ctx is already cancelled + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + // Determine wait limit + waitLimit := InfDuration + if deadline, ok := ctx.Deadline(); ok { + waitLimit = deadline.Sub(t) + } + // Reserve + r := lim.reserveN(t, n, waitLimit) + if !r.ok { + return fmt.Errorf("rate: Wait(n=%d) would exceed context deadline", n) + } + // Wait if necessary + delay := r.DelayFrom(t) + if delay == 0 { + return nil + } + ch, stop, advance := newTimer(delay) + defer stop() + advance() // only has an effect when testing + select { + case <-ch: + // We can proceed. + return nil + case <-ctx.Done(): + // Context was canceled before we could proceed. Cancel the + // reservation, which may permit other events to proceed sooner. + r.Cancel() + return ctx.Err() + } +} + +// SetLimit is shorthand for SetLimitAt(time.Now(), newLimit). +func (lim *Limiter) SetLimit(newLimit Limit) { + lim.SetLimitAt(time.Now(), newLimit) +} + +// SetLimitAt sets a new Limit for the limiter. The new Limit, and Burst, may be violated +// or underutilized by those which reserved (using Reserve or Wait) but did not yet act +// before SetLimitAt was called. +func (lim *Limiter) SetLimitAt(t time.Time, newLimit Limit) { + lim.mu.Lock() + defer lim.mu.Unlock() + + t, tokens := lim.advance(t) + + lim.last = t + lim.tokens = tokens + lim.limit = newLimit +} + +// SetBurst is shorthand for SetBurstAt(time.Now(), newBurst). +func (lim *Limiter) SetBurst(newBurst int) { + lim.SetBurstAt(time.Now(), newBurst) +} + +// SetBurstAt sets a new burst size for the limiter. +func (lim *Limiter) SetBurstAt(t time.Time, newBurst int) { + lim.mu.Lock() + defer lim.mu.Unlock() + + t, tokens := lim.advance(t) + + lim.last = t + lim.tokens = tokens + lim.burst = newBurst +} + +// reserveN is a helper method for AllowN, ReserveN, and WaitN. +// maxFutureReserve specifies the maximum reservation wait duration allowed. +// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN. +func (lim *Limiter) reserveN(t time.Time, n int, maxFutureReserve time.Duration) Reservation { + lim.mu.Lock() + defer lim.mu.Unlock() + + if lim.limit == Inf { + return Reservation{ + ok: true, + lim: lim, + tokens: n, + timeToAct: t, + } + } + + t, tokens := lim.advance(t) + + // Calculate the remaining number of tokens resulting from the request. + tokens -= float64(n) + + // Calculate the wait duration + var waitDuration time.Duration + if tokens < 0 { + waitDuration = lim.limit.durationFromTokens(-tokens) + } + + // Decide result + ok := n <= lim.burst && waitDuration <= maxFutureReserve + + // Prepare reservation + r := Reservation{ + ok: ok, + lim: lim, + limit: lim.limit, + } + if ok { + r.tokens = n + r.timeToAct = t.Add(waitDuration) + + // Update state + lim.last = t + lim.tokens = tokens + lim.lastEvent = r.timeToAct + } + + return r +} + +// advance calculates and returns an updated state for lim resulting from the passage of time. +// lim is not changed. +// advance requires that lim.mu is held. +func (lim *Limiter) advance(t time.Time) (newT time.Time, newTokens float64) { + last := lim.last + if t.Before(last) { + last = t + } + + // Calculate the new number of tokens, due to time that passed. + elapsed := t.Sub(last) + delta := lim.limit.tokensFromDuration(elapsed) + tokens := lim.tokens + delta + if burst := float64(lim.burst); tokens > burst { + tokens = burst + } + return t, tokens +} + +// durationFromTokens is a unit conversion function from the number of tokens to the duration +// of time it takes to accumulate them at a rate of limit tokens per second. +func (limit Limit) durationFromTokens(tokens float64) time.Duration { + if limit <= 0 { + return InfDuration + } + + duration := (tokens / float64(limit)) * float64(time.Second) + + // Cap the duration to the maximum representable int64 value, to avoid overflow. + if duration > float64(math.MaxInt64) { + return InfDuration + } + + return time.Duration(duration) +} + +// tokensFromDuration is a unit conversion function from a time duration to the number of tokens +// which could be accumulated during that duration at a rate of limit tokens per second. +func (limit Limit) tokensFromDuration(d time.Duration) float64 { + if limit <= 0 { + return 0 + } + return d.Seconds() * float64(limit) +} diff --git a/vendor/golang.org/x/time/rate/sometimes.go b/vendor/golang.org/x/time/rate/sometimes.go new file mode 100644 index 0000000000..6ba99ddb67 --- /dev/null +++ b/vendor/golang.org/x/time/rate/sometimes.go @@ -0,0 +1,67 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package rate + +import ( + "sync" + "time" +) + +// Sometimes will perform an action occasionally. The First, Every, and +// Interval fields govern the behavior of Do, which performs the action. +// A zero Sometimes value will perform an action exactly once. +// +// # Example: logging with rate limiting +// +// var sometimes = rate.Sometimes{First: 3, Interval: 10*time.Second} +// func Spammy() { +// sometimes.Do(func() { log.Info("here I am!") }) +// } +type Sometimes struct { + First int // if non-zero, the first N calls to Do will run f. + Every int // if non-zero, every Nth call to Do will run f. + Interval time.Duration // if non-zero and Interval has elapsed since f's last run, Do will run f. + + mu sync.Mutex + count int // number of Do calls + last time.Time // last time f was run +} + +// Do runs the function f as allowed by First, Every, and Interval. +// +// The model is a union (not intersection) of filters. The first call to Do +// always runs f. Subsequent calls to Do run f if allowed by First or Every or +// Interval. +// +// A non-zero First:N causes the first N Do(f) calls to run f. +// +// A non-zero Every:M causes every Mth Do(f) call, starting with the first, to +// run f. +// +// A non-zero Interval causes Do(f) to run f if Interval has elapsed since +// Do last ran f. +// +// Specifying multiple filters produces the union of these execution streams. +// For example, specifying both First:N and Every:M causes the first N Do(f) +// calls and every Mth Do(f) call, starting with the first, to run f. See +// Examples for more. +// +// If Do is called multiple times simultaneously, the calls will block and run +// serially. Therefore, Do is intended for lightweight operations. +// +// Because a call to Do may block until f returns, if f causes Do to be called, +// it will deadlock. +func (s *Sometimes) Do(f func()) { + s.mu.Lock() + defer s.mu.Unlock() + if s.count == 0 || + (s.First > 0 && s.count < s.First) || + (s.Every > 0 && s.count%s.Every == 0) || + (s.Interval > 0 && time.Since(s.last) >= s.Interval) { + f() + s.last = time.Now() + } + s.count++ +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 924e4500f3..31e993e091 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -291,6 +291,9 @@ golang.org/x/sync/errgroup golang.org/x/sys/cpu golang.org/x/sys/unix golang.org/x/sys/windows +# golang.org/x/time v0.10.0 +## explicit; go 1.18 +golang.org/x/time/rate # golang.org/x/tools v0.23.0 ## explicit; go 1.19 golang.org/x/tools/go/gcexportdata