Skip to content

Commit

Permalink
Add code host rate limiter details to code host connection page (#56638)
Browse files Browse the repository at this point in the history
  • Loading branch information
pjlast committed Sep 28, 2023
1 parent 95dcc46 commit a569bb6
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import classNames from 'classnames'
import type { ExternalServiceKind } from '@sourcegraph/shared/src/graphql-operations'
import { Icon, Link, LoadingSpinner, Tooltip } from '@sourcegraph/wildcard'

import type { RateLimiterState } from './backend'

import styles from '../../site-admin/WebhookInformation.module.scss'

interface ExternalServiceInformationProps {
Expand All @@ -14,6 +16,7 @@ interface ExternalServiceInformationProps {
icon: React.ComponentType<React.PropsWithChildren<{ className?: string }>>
kind: ExternalServiceKind
displayName: string
rateLimiterState?: RateLimiterState | null
codeHostID: string
reposNumber: number
syncInProgress: boolean
Expand All @@ -23,8 +26,38 @@ interface ExternalServiceInformationProps {
} | null
}

export const RateLimiterStateInfo: FC<{ rateLimiterState: RateLimiterState }> = props => {
const { rateLimiterState } = props
const rateLimiterDebug = Object.entries(rateLimiterState).map(([key, value]) => (
<div key={key}>
{key}: {value.toString()}
</div>
))

return (
<tr>
<th className={styles.tableHeader}>Rate limit</th>
{rateLimiterState.infinite ? (
<td>
<Tooltip content={rateLimiterDebug}>
<span>No rate limit</span>
</Tooltip>
</td>
) : (
<td>
<Tooltip content={rateLimiterDebug}>
<span>
{(rateLimiterState.limit / rateLimiterState.interval).toFixed(2)} requests per second
</span>
</Tooltip>
</td>
)}
</tr>
)
}

export const ExternalServiceInformation: FC<ExternalServiceInformationProps> = props => {
const { icon, kind, displayName, codeHostID, reposNumber, syncInProgress, gitHubApp } = props
const { icon, kind, displayName, codeHostID, reposNumber, syncInProgress, gitHubApp, rateLimiterState } = props

return (
<table className={classNames(styles.table, 'table')}>
Expand Down Expand Up @@ -66,6 +99,7 @@ export const ExternalServiceInformation: FC<ExternalServiceInformationProps> = p
)}
</td>
</tr>
{rateLimiterState && <RateLimiterStateInfo rateLimiterState={rateLimiterState} />}
</tbody>
</table>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ export const ExternalServicePage: FC<Props> = props => {
<ExternalServiceInformation
displayName={externalService.displayName}
codeHostID={externalService.id}
rateLimiterState={externalService.rateLimiterState}
reposNumber={numberOfRepos === 0 ? externalService.repoCount : numberOfRepos}
syncInProgress={syncInProgress}
gitHubApp={ghApp}
Expand Down
22 changes: 22 additions & 0 deletions client/web/src/components/externalServices/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,30 @@ import {
type UseShowMorePaginationResult,
} from '../FilteredConnection/hooks/useShowMorePagination'

const RATE_LIMITER_STATE_FRAGMENT = gql`
fragment RateLimiterStateFields on RateLimiterState {
__typename
currentCapacity
burst
limit
interval
lastReplenishment
infinite
}
`

export const externalServiceFragment = gql`
${RATE_LIMITER_STATE_FRAGMENT}
fragment ExternalServiceFields on ExternalService {
id
kind
displayName
config
warning
lastSyncError
rateLimiterState {
...RateLimiterStateFields
}
repoCount
lastSyncAt
nextSyncAt
Expand Down Expand Up @@ -186,10 +202,14 @@ export const EXTERNAL_SERVICE_SYNC_JOBS = gql`

export const LIST_EXTERNAL_SERVICE_FRAGMENT = gql`
${EXTERNAL_SERVICE_SYNC_JOB_CONNECTION_FIELDS_FRAGMENT}
${RATE_LIMITER_STATE_FRAGMENT}
fragment ListExternalServiceFields on ExternalService {
id
kind
displayName
rateLimiterState {
...RateLimiterStateFields
}
config
warning
lastSyncError
Expand Down Expand Up @@ -356,3 +376,5 @@ export interface ExternalServiceFieldsWithConfig extends ExternalServiceFields {
url: string
}
}

export type RateLimiterState = NonNullable<ExternalServiceFields['rateLimiterState']>
9 changes: 9 additions & 0 deletions client/web/src/site-admin/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ export function createExternalService(kind: ExternalServiceKind, url: string): L
nextSyncAt: null,
updatedAt: '2021-03-15T19:39:11Z',
createdAt: '2021-03-15T19:39:11Z',
rateLimiterState: {
__typename: 'RateLimiterState',
currentCapacity: 10,
burst: 10,
limit: 5000,
interval: 1,
lastReplenishment: '2021-03-15T19:39:11Z',
infinite: false,
},
webhookURL: null,
hasConnectionCheck: true,
syncJobs: {
Expand Down
3 changes: 3 additions & 0 deletions cmd/frontend/graphqlbackend/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ go_library(
"product_license_info.go",
"product_subscription_status.go",
"rate_limit.go",
"ratelimiter.go",
"rbac.go",
"recorded_commands.go",
"repositories.go",
Expand Down Expand Up @@ -303,6 +304,7 @@ go_library(
"//internal/observation",
"//internal/oobmigration",
"//internal/perforce",
"//internal/ratelimit",
"//internal/rbac",
"//internal/rcache",
"//internal/redispool",
Expand Down Expand Up @@ -503,6 +505,7 @@ go_test(
"//internal/highlight",
"//internal/inventory",
"//internal/oobmigration",
"//internal/ratelimit",
"//internal/rbac",
"//internal/rbac/types",
"//internal/rcache",
Expand Down
15 changes: 15 additions & 0 deletions cmd/frontend/graphqlbackend/external_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/gqlutil"
"github.com/sourcegraph/sourcegraph/internal/httpcli"
"github.com/sourcegraph/sourcegraph/internal/ratelimit"
"github.com/sourcegraph/sourcegraph/internal/repos"
"github.com/sourcegraph/sourcegraph/internal/repoupdater/protocol"
"github.com/sourcegraph/sourcegraph/internal/types"
Expand Down Expand Up @@ -128,6 +129,20 @@ func (r *externalServiceResolver) DisplayName() string {
return r.externalService.DisplayName
}

func (r *externalServiceResolver) RateLimiterState(ctx context.Context) (*rateLimiterStateResolver, error) {
info, err := ratelimit.GetGlobalLimiterState(ctx)
if err != nil {
return nil, errors.Wrap(err, "getting rate limiter state")
}

state, ok := info[r.externalService.URN()]
if !ok {
return nil, nil
}

return &rateLimiterStateResolver{state: state}, nil
}

func (r *externalServiceResolver) Config(ctx context.Context) (JSONCString, error) {
redacted, err := r.externalService.RedactedConfig(ctx)
if err != nil {
Expand Down
108 changes: 101 additions & 7 deletions cmd/frontend/graphqlbackend/external_services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/database/dbmocks"
"github.com/sourcegraph/sourcegraph/internal/extsvc/github"
"github.com/sourcegraph/sourcegraph/internal/ratelimit"
"github.com/sourcegraph/sourcegraph/internal/repoupdater/protocol"
"github.com/sourcegraph/sourcegraph/lib/errors"

Expand Down Expand Up @@ -622,6 +623,13 @@ func TestExternalServices(t *testing.T) {
users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{SiteAdmin: true}, nil)

externalServices := dbmocks.NewMockExternalServiceStore()
ess := []*types.ExternalService{
{ID: 1, Config: extsvc.NewEmptyConfig()},
{ID: 2, Config: extsvc.NewEmptyConfig(), Kind: extsvc.KindGitHub},
{ID: 3, Config: extsvc.NewEmptyConfig(), Kind: extsvc.KindGitHub},
{ID: 4, Config: extsvc.NewEmptyConfig(), Kind: extsvc.KindAWSCodeCommit},
{ID: 5, Config: extsvc.NewEmptyConfig(), Kind: extsvc.KindGerrit},
}
externalServices.ListFunc.SetDefaultHook(func(_ context.Context, opt database.ExternalServicesListOptions) ([]*types.ExternalService, error) {
if opt.AfterID > 0 || opt.RepoID == 42 {
return []*types.ExternalService{
Expand All @@ -630,18 +638,20 @@ func TestExternalServices(t *testing.T) {
}, nil
}

ess := []*types.ExternalService{
{ID: 1, Config: extsvc.NewEmptyConfig()},
{ID: 2, Config: extsvc.NewEmptyConfig(), Kind: extsvc.KindGitHub},
{ID: 3, Config: extsvc.NewEmptyConfig(), Kind: extsvc.KindGitHub},
{ID: 4, Config: extsvc.NewEmptyConfig(), Kind: extsvc.KindAWSCodeCommit},
{ID: 5, Config: extsvc.NewEmptyConfig(), Kind: extsvc.KindGerrit},
}
if opt.LimitOffset != nil {
return ess[:opt.LimitOffset.Limit], nil
}
return ess, nil
})

// Set up rate limits
ctx := context.Background()
ratelimit.SetupForTest(t)
for _, es := range ess {
rl := ratelimit.NewGlobalRateLimiter(logtest.NoOp(t), es.URN())
rl.SetTokenBucketConfig(ctx, 10, time.Hour)
}

externalServices.CountFunc.SetDefaultHook(func(ctx context.Context, opt database.ExternalServicesListOptions) (int, error) {
if opt.AfterID > 0 {
return 1, nil
Expand Down Expand Up @@ -698,6 +708,90 @@ func TestExternalServices(t *testing.T) {
}
`,
},
{
Schema: mustParseGraphQLSchema(t, db),
Label: "Read with rate limiter state",
Query: `
{
externalServices {
nodes {
id
rateLimiterState {
burst
currentCapacity
infinite
interval
lastReplenishment
limit
}
}
}
}
`,
ExpectedResult: `
{
"externalServices": {
"nodes": [
{
"id":"RXh0ZXJuYWxTZXJ2aWNlOjE=",
"rateLimiterState": {
"burst": 10,
"currentCapacity": 0,
"infinite": false,
"interval": 3600,
"lastReplenishment": "1970-01-01T00:00:00Z",
"limit": 10
}
},
{
"id":"RXh0ZXJuYWxTZXJ2aWNlOjI=",
"rateLimiterState": {
"burst": 10,
"currentCapacity": 0,
"infinite": false,
"interval": 3600,
"lastReplenishment": "1970-01-01T00:00:00Z",
"limit": 10
}
},
{
"id":"RXh0ZXJuYWxTZXJ2aWNlOjM=",
"rateLimiterState": {
"burst": 10,
"currentCapacity": 0,
"infinite": false,
"interval": 3600,
"lastReplenishment": "1970-01-01T00:00:00Z",
"limit": 10
}
},
{
"id":"RXh0ZXJuYWxTZXJ2aWNlOjQ=",
"rateLimiterState": {
"burst": 10,
"currentCapacity": 0,
"infinite": false,
"interval": 3600,
"lastReplenishment": "1970-01-01T00:00:00Z",
"limit": 10
}
},
{
"id":"RXh0ZXJuYWxTZXJ2aWNlOjU=",
"rateLimiterState": {
"burst": 10,
"currentCapacity": 0,
"infinite": false,
"interval": 3600,
"lastReplenishment": "1970-01-01T00:00:00Z",
"limit": 10
}
}
]
}
}
`,
},
{
Schema: mustParseGraphQLSchema(t, db),
Label: "Read all external services for a given repo",
Expand Down
36 changes: 36 additions & 0 deletions cmd/frontend/graphqlbackend/ratelimiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package graphqlbackend

import (
"time"

"github.com/sourcegraph/sourcegraph/internal/gqlutil"
"github.com/sourcegraph/sourcegraph/internal/ratelimit"
)

type rateLimiterStateResolver struct {
state ratelimit.GlobalLimiterInfo
}

func (rl *rateLimiterStateResolver) Burst() int32 {
return int32(rl.state.Burst)
}

func (rl *rateLimiterStateResolver) CurrentCapacity() int32 {
return int32(rl.state.CurrentCapacity)
}

func (rl *rateLimiterStateResolver) Infinite() bool {
return rl.state.Infinite
}

func (rl *rateLimiterStateResolver) Interval() int32 {
return int32(rl.state.Interval / time.Second)
}

func (rl *rateLimiterStateResolver) LastReplenishment() gqlutil.DateTime {
return gqlutil.DateTime{Time: rl.state.LastReplenishment}
}

func (rl *rateLimiterStateResolver) Limit() int32 {
return int32(rl.state.Limit)
}

0 comments on commit a569bb6

Please sign in to comment.