Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

bitbucket: Initialise using synced rate limiters #33773

Merged
merged 3 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion enterprise/internal/authz/bitbucketserver/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func newAuthzProvider(

var errs error

cli, err := bitbucketserver.NewClient(c.BitbucketServerConnection, nil)
cli, err := bitbucketserver.NewClient(c.URN, c.BitbucketServerConnection, nil)
if err != nil {
errs = errors.Append(errs, err)
return nil, errs
Expand Down
9 changes: 1 addition & 8 deletions enterprise/internal/batches/sources/bitbucketserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,7 @@ func NewBitbucketServerSource(svc *types.ExternalService, cf *httpcli.Factory) (
if err := jsonc.Unmarshal(svc.Config, &c); err != nil {
return nil, errors.Errorf("external service id=%d config error: %s", svc.ID, err)
}
return newBitbucketServerSource(&c, cf, nil)
}

func newBitbucketServerSource(c *schema.BitbucketServerConnection, cf *httpcli.Factory, au auth.Authenticator) (*BitbucketServerSource, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper function wasn't very helpful so I inlined it

if cf == nil {
cf = httpcli.ExternalClientFactory
}
Expand All @@ -46,15 +43,11 @@ func newBitbucketServerSource(c *schema.BitbucketServerConnection, cf *httpcli.F
return nil, err
}

client, err := bitbucketserver.NewClient(c, cli)
client, err := bitbucketserver.NewClient(svc.URN(), &c, cli)
if err != nil {
return nil, err
}

if au != nil {
client = client.WithAuthenticator(au)
}

return &BitbucketServerSource{
au: client.Auth,
client: client,
Expand Down
32 changes: 6 additions & 26 deletions internal/extsvc/bitbucketcloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,6 @@ import (
// The metric generated here will be named as "src_bitbucket_cloud_requests_total".
var requestCounter = metrics.NewRequestMeter("bitbucket_cloud", "Total number of requests sent to the Bitbucket Cloud API.")

// These fields define the self-imposed Bitbucket rate limit (since Bitbucket Cloud does
// not have a concept of rate limiting in HTTP response headers).
//
// See https://godoc.org/golang.org/x/time/rate#Limiter for an explanation of these fields.
//
// The limits chosen here are based on the following logic: Bitbucket Cloud restricts
// "List all repositories" requests (which are a good portion of our requests) to 1,000/hr,
// and they restrict "List a user or team's repositories" requests (which are roughly equal
// to our repository lookup requests) to 1,000/hr.
// See `pkg/extsvc/bitbucketserver/client.go` for the calculations behind these limits`
const (
rateLimitRequestsPerSecond = 2 // 120/min or 7200/hr
rateLimitMaxBurstRequests = 500
)

// Client access a Bitbucket Cloud via the REST API 2.0.
type Client struct {
// HTTP Client used to communicate with the API
Expand All @@ -57,13 +42,13 @@ type Client struct {

// RateLimit is the self-imposed rate limiter (since Bitbucket does not have a concept
// of rate limiting in HTTP response headers).
RateLimit *rate.Limiter
rateLimit *rate.Limiter
}

// NewClient creates a new Bitbucket Cloud API client from the given external
// service configuration. If a nil httpClient is provided, http.DefaultClient
// will be used.
func NewClient(config *schema.BitbucketCloudConnection, httpClient httpcli.Doer) (*Client, error) {
func NewClient(urn string, config *schema.BitbucketCloudConnection, httpClient httpcli.Doer) (*Client, error) {
if httpClient == nil {
httpClient = httpcli.ExternalDoer
}
Expand All @@ -83,20 +68,15 @@ func NewClient(config *schema.BitbucketCloudConnection, httpClient httpcli.Doer)
return nil, err
}

// Normally our registry will return a default infinite limiter when nothing has been
// synced from config. However, we always want to ensure there is at least some form of rate
// limiting for Bitbucket.
defaultLimiter := rate.NewLimiter(rateLimitRequestsPerSecond, rateLimitMaxBurstRequests)
l := ratelimit.DefaultRegistry.GetOrSet(apiURL.String(), defaultLimiter)

return &Client{
httpClient: httpClient,
URL: extsvc.NormalizeBaseURL(apiURL),
Auth: &auth.BasicAuth{
Username: config.Username,
Password: config.AppPassword,
},
RateLimit: l,
// Default limits are defined in extsvc.GetLimitFromConfig
rateLimit: ratelimit.DefaultRegistry.Get(urn),
}, nil
}

Expand All @@ -112,7 +92,7 @@ func (c *Client) WithAuthenticator(a auth.Authenticator) *Client {
httpClient: c.httpClient,
URL: c.URL,
Auth: a,
RateLimit: c.RateLimit,
rateLimit: c.rateLimit,
}
}

Expand Down Expand Up @@ -189,7 +169,7 @@ func (c *Client) do(ctx context.Context, req *http.Request, result interface{})
}

startWait := time.Now()
if err := c.RateLimit.Wait(ctx); err != nil {
if err := c.rateLimit.Wait(ctx); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion internal/extsvc/bitbucketcloud/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func NewTestClient(t testing.TB, name string, update bool) (*Client, func()) {
t.Fatal(err)
}

cli, err := NewClient(&schema.BitbucketCloudConnection{
cli, err := NewClient("urn", &schema.BitbucketCloudConnection{
ApiURL: "https://api.bitbucket.org",
Username: GetenvTestBitbucketCloudUsername(),
AppPassword: os.Getenv("BITBUCKET_CLOUD_APP_PASSWORD"),
Expand Down
55 changes: 14 additions & 41 deletions internal/extsvc/bitbucketserver/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,8 @@ import (
// The metric generated here will be named as "src_bitbucket_requests_total".
var requestCounter = metrics.NewRequestMeter("bitbucket", "Total number of requests sent to the Bitbucket API.")

// These fields define the self-imposed Bitbucket rate limit (since Bitbucket Server does
// not have a concept of rate limiting in HTTP response headers).
//
// See https://godoc.org/golang.org/x/time/rate#Limiter for an explanation of these fields.
//
// We chose the limits here based on the fact that Sourcegraph is a heavy consumer of the Bitbucket
// Server API and that a large customer had reported to us their Bitbucket instance receives
// ~100 req/s so it seems reasonable for us to (at max) consume ~8 req/s.
//
// Note that, for comparison, Bitbucket Cloud restricts "List all repositories" requests (which are
// a good portion of our requests) to 1,000/hr, and they restrict "List a user or team's repositories"
// requests (which are roughly equal to our repository lookup requests) to 1,000/hr. We perform a list
// repositories request for every 1000 repositories on Bitbucket every 1m by default, so for someone
// with 20,000 Bitbucket repositories we need 20,000/1000 requests per minute (1200/hr) + overhead for
// repository lookup requests by users, and requests for identifying which repositories a user has
// access to (if authorization is in use) and requests for changeset synchronization if it is in use.
//
// These are our default values, they can be changed in configuration
const (
defaultRateLimit = rate.Limit(8) // 480/min or 28,800/hr
defaultRateLimitBurst = 500
)

// Client access a Bitbucket Server via the REST API.
type Client struct {
// HTTP Client used to communicate with the API
httpClient httpcli.Doer

// URL is the base URL of Bitbucket Server.
URL *url.URL

Expand All @@ -78,16 +52,20 @@ type Client struct {
// This is generally set using SetOAuth.
Auth auth.Authenticator

// RateLimit is the self-imposed rate limiter (since Bitbucket does not have a concept
// of rate limiting in HTTP response headers).
RateLimit *rate.Limiter
// HTTP Client used to communicate with the API
httpClient httpcli.Doer

// RateLimit is the self-imposed rate limiter (since Bitbucket does not have a
// concept of rate limiting in HTTP response headers). Default limits are defined
// in extsvc.GetLimitFromConfig
rateLimit *rate.Limiter
}

// NewClient returns an authenticated Bitbucket Server API client with
// the provided configuration. If a nil httpClient is provided, http.DefaultClient
// will be used.
func NewClient(config *schema.BitbucketServerConnection, httpClient httpcli.Doer) (*Client, error) {
client, err := newClient(config, httpClient)
func NewClient(urn string, config *schema.BitbucketServerConnection, httpClient httpcli.Doer) (*Client, error) {
client, err := newClient(urn, config, httpClient)
if err != nil {
return nil, err
}
Expand All @@ -114,7 +92,7 @@ func NewClient(config *schema.BitbucketServerConnection, httpClient httpcli.Doer
return client, nil
}

func newClient(config *schema.BitbucketServerConnection, httpClient httpcli.Doer) (*Client, error) {
func newClient(urn string, config *schema.BitbucketServerConnection, httpClient httpcli.Doer) (*Client, error) {
u, err := url.Parse(config.Url)
if err != nil {
return nil, err
Expand All @@ -125,16 +103,11 @@ func newClient(config *schema.BitbucketServerConnection, httpClient httpcli.Doer
}
httpClient = requestCounter.Doer(httpClient, categorize)

// Normally our registry will return a default infinite limiter when nothing has been
// synced from config. However, we always want to ensure there is at least some form of rate
// limiting for Bitbucket.
defaultLimiter := rate.NewLimiter(defaultRateLimit, defaultRateLimitBurst)
l := ratelimit.DefaultRegistry.GetOrSet(u.String(), defaultLimiter)

return &Client{
httpClient: httpClient,
URL: u,
RateLimit: l,
// Default limits are defined in extsvc.GetLimitFromConfig
rateLimit: ratelimit.DefaultRegistry.Get(urn),
}, nil
}

Expand All @@ -145,7 +118,7 @@ func (c *Client) WithAuthenticator(a auth.Authenticator) *Client {
return &Client{
httpClient: c.httpClient,
URL: c.URL,
RateLimit: c.RateLimit,
rateLimit: c.rateLimit,
Auth: a,
}
}
Expand Down Expand Up @@ -967,7 +940,7 @@ func (c *Client) do(ctx context.Context, req *http.Request, result interface{})
}

startWait := time.Now()
if err := c.RateLimit.Wait(ctx); err != nil {
if err := c.rateLimit.Wait(ctx); err != nil {
return nil, err
}

Expand Down
14 changes: 7 additions & 7 deletions internal/extsvc/bitbucketserver/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1090,7 +1090,7 @@ func TestAuth(t *testing.T) {
// Ensure that the different configuration types create the right
// implicit Authenticator.
t.Run("bearer token", func(t *testing.T) {
client, err := NewClient(&schema.BitbucketServerConnection{
client, err := NewClient("urn", &schema.BitbucketServerConnection{
Url: "http://example.com/",
Token: "foo",
}, nil)
Expand All @@ -1107,7 +1107,7 @@ func TestAuth(t *testing.T) {
})

t.Run("basic auth", func(t *testing.T) {
client, err := NewClient(&schema.BitbucketServerConnection{
client, err := NewClient("urn", &schema.BitbucketServerConnection{
Url: "http://example.com/",
Username: "foo",
Password: "bar",
Expand All @@ -1125,7 +1125,7 @@ func TestAuth(t *testing.T) {
})

t.Run("OAuth 1 error", func(t *testing.T) {
if _, err := NewClient(&schema.BitbucketServerConnection{
if _, err := NewClient("urn", &schema.BitbucketServerConnection{
Url: "http://example.com/",
Authorization: &schema.BitbucketServerAuthorization{
Oauth: schema.BitbucketServerOAuth{
Expand All @@ -1150,7 +1150,7 @@ func TestAuth(t *testing.T) {
pemKey := pem.EncodeToMemory(&pem.Block{Bytes: block})
signingKey := base64.StdEncoding.EncodeToString(pemKey)

client, err := NewClient(&schema.BitbucketServerConnection{
client, err := NewClient("urn", &schema.BitbucketServerConnection{
Url: "http://example.com/",
Authorization: &schema.BitbucketServerAuthorization{
Oauth: schema.BitbucketServerOAuth{
Expand Down Expand Up @@ -1229,7 +1229,7 @@ func TestClient_WithAuthenticator(t *testing.T) {

old := &Client{
URL: uri,
RateLimit: rate.NewLimiter(defaultRateLimit, defaultRateLimitBurst),
rateLimit: rate.NewLimiter(10, 10),
Auth: &auth.BasicAuth{Username: "johnsson", Password: "mothersmaidenname"},
}

Expand All @@ -1247,8 +1247,8 @@ func TestClient_WithAuthenticator(t *testing.T) {
t.Fatalf("url: want %q but got %q", old.URL, newClient.URL)
}

if newClient.RateLimit != old.RateLimit {
t.Fatalf("RateLimit: want %#v but got %#v", old.RateLimit, newClient.RateLimit)
if newClient.rateLimit != old.rateLimit {
t.Fatalf("RateLimit: want %#v but got %#v", old.rateLimit, newClient.rateLimit)
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/extsvc/bitbucketserver/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func NewTestClient(t testing.TB, name string, update bool) (*Client, func()) {
Url: instanceURL,
}

cli, err := NewClient(c, hc)
cli, err := NewClient("urn", c, hc)
if err != nil {
t.Fatal(err)
}
Expand Down
6 changes: 2 additions & 4 deletions internal/extsvc/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,6 @@ const (

// TypeOther is the (api.ExternalRepoSpec).ServiceType value for other projects.
TypeOther = "other"

defaultRateLimit = rate.Limit(2)
)

// KindToType returns a Type constants given a Kind
Expand Down Expand Up @@ -417,7 +415,7 @@ func GetLimitFromConfig(kind string, config interface{}) (rate.Limit, error) {
limit = limitOrInf(c.RateLimit.Enabled, c.RateLimit.RequestsPerHour)
}
case *schema.BitbucketCloudConnection:
limit = defaultRateLimit
limit = rate.Limit(2)
if c != nil && c.RateLimit != nil {
limit = limitOrInf(c.RateLimit.Enabled, c.RateLimit.RequestsPerHour)
}
Expand All @@ -427,7 +425,7 @@ func GetLimitFromConfig(kind string, config interface{}) (rate.Limit, error) {
limit = limitOrInf(c.RateLimit.Enabled, c.RateLimit.RequestsPerHour)
}
case *schema.JVMPackagesConnection:
limit = defaultRateLimit
limit = rate.Limit(2)
if c != nil && c.Maven.RateLimit != nil {
limit = limitOrInf(c.Maven.RateLimit.Enabled, c.Maven.RateLimit.RequestsPerHour)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/repos/bitbucketcloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func newBitbucketCloudSource(svc *types.ExternalService, c *schema.BitbucketClou
return nil, err
}

client, err := bitbucketcloud.NewClient(c, cli)
client, err := bitbucketcloud.NewClient(svc.URN(), c, cli)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/repos/bitbucketserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func newBitbucketServerSource(svc *types.ExternalService, c *schema.BitbucketSer
return nil, err
}

client, err := bitbucketserver.NewClient(c, cli)
client, err := bitbucketserver.NewClient(svc.URN(), c, cli)
if err != nil {
return nil, err
}
Expand Down