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

Commit a569bb6

Browse files
authored
Add code host rate limiter details to code host connection page (#56638)
1 parent 95dcc46 commit a569bb6

File tree

9 files changed

+262
-8
lines changed

9 files changed

+262
-8
lines changed

client/web/src/components/externalServices/ExternalServiceInformation.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import classNames from 'classnames'
55
import type { ExternalServiceKind } from '@sourcegraph/shared/src/graphql-operations'
66
import { Icon, Link, LoadingSpinner, Tooltip } from '@sourcegraph/wildcard'
77

8+
import type { RateLimiterState } from './backend'
9+
810
import styles from '../../site-admin/WebhookInformation.module.scss'
911

1012
interface ExternalServiceInformationProps {
@@ -14,6 +16,7 @@ interface ExternalServiceInformationProps {
1416
icon: React.ComponentType<React.PropsWithChildren<{ className?: string }>>
1517
kind: ExternalServiceKind
1618
displayName: string
19+
rateLimiterState?: RateLimiterState | null
1720
codeHostID: string
1821
reposNumber: number
1922
syncInProgress: boolean
@@ -23,8 +26,38 @@ interface ExternalServiceInformationProps {
2326
} | null
2427
}
2528

29+
export const RateLimiterStateInfo: FC<{ rateLimiterState: RateLimiterState }> = props => {
30+
const { rateLimiterState } = props
31+
const rateLimiterDebug = Object.entries(rateLimiterState).map(([key, value]) => (
32+
<div key={key}>
33+
{key}: {value.toString()}
34+
</div>
35+
))
36+
37+
return (
38+
<tr>
39+
<th className={styles.tableHeader}>Rate limit</th>
40+
{rateLimiterState.infinite ? (
41+
<td>
42+
<Tooltip content={rateLimiterDebug}>
43+
<span>No rate limit</span>
44+
</Tooltip>
45+
</td>
46+
) : (
47+
<td>
48+
<Tooltip content={rateLimiterDebug}>
49+
<span>
50+
{(rateLimiterState.limit / rateLimiterState.interval).toFixed(2)} requests per second
51+
</span>
52+
</Tooltip>
53+
</td>
54+
)}
55+
</tr>
56+
)
57+
}
58+
2659
export const ExternalServiceInformation: FC<ExternalServiceInformationProps> = props => {
27-
const { icon, kind, displayName, codeHostID, reposNumber, syncInProgress, gitHubApp } = props
60+
const { icon, kind, displayName, codeHostID, reposNumber, syncInProgress, gitHubApp, rateLimiterState } = props
2861

2962
return (
3063
<table className={classNames(styles.table, 'table')}>
@@ -66,6 +99,7 @@ export const ExternalServiceInformation: FC<ExternalServiceInformationProps> = p
6699
)}
67100
</td>
68101
</tr>
102+
{rateLimiterState && <RateLimiterStateInfo rateLimiterState={rateLimiterState} />}
69103
</tbody>
70104
</table>
71105
)

client/web/src/components/externalServices/ExternalServicePage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ export const ExternalServicePage: FC<Props> = props => {
250250
<ExternalServiceInformation
251251
displayName={externalService.displayName}
252252
codeHostID={externalService.id}
253+
rateLimiterState={externalService.rateLimiterState}
253254
reposNumber={numberOfRepos === 0 ? externalService.repoCount : numberOfRepos}
254255
syncInProgress={syncInProgress}
255256
gitHubApp={ghApp}

client/web/src/components/externalServices/backend.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,30 @@ import {
3838
type UseShowMorePaginationResult,
3939
} from '../FilteredConnection/hooks/useShowMorePagination'
4040

41+
const RATE_LIMITER_STATE_FRAGMENT = gql`
42+
fragment RateLimiterStateFields on RateLimiterState {
43+
__typename
44+
currentCapacity
45+
burst
46+
limit
47+
interval
48+
lastReplenishment
49+
infinite
50+
}
51+
`
52+
4153
export const externalServiceFragment = gql`
54+
${RATE_LIMITER_STATE_FRAGMENT}
4255
fragment ExternalServiceFields on ExternalService {
4356
id
4457
kind
4558
displayName
4659
config
4760
warning
4861
lastSyncError
62+
rateLimiterState {
63+
...RateLimiterStateFields
64+
}
4965
repoCount
5066
lastSyncAt
5167
nextSyncAt
@@ -186,10 +202,14 @@ export const EXTERNAL_SERVICE_SYNC_JOBS = gql`
186202

187203
export const LIST_EXTERNAL_SERVICE_FRAGMENT = gql`
188204
${EXTERNAL_SERVICE_SYNC_JOB_CONNECTION_FIELDS_FRAGMENT}
205+
${RATE_LIMITER_STATE_FRAGMENT}
189206
fragment ListExternalServiceFields on ExternalService {
190207
id
191208
kind
192209
displayName
210+
rateLimiterState {
211+
...RateLimiterStateFields
212+
}
193213
config
194214
warning
195215
lastSyncError
@@ -356,3 +376,5 @@ export interface ExternalServiceFieldsWithConfig extends ExternalServiceFields {
356376
url: string
357377
}
358378
}
379+
380+
export type RateLimiterState = NonNullable<ExternalServiceFields['rateLimiterState']>

client/web/src/site-admin/fixtures.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ export function createExternalService(kind: ExternalServiceKind, url: string): L
2020
nextSyncAt: null,
2121
updatedAt: '2021-03-15T19:39:11Z',
2222
createdAt: '2021-03-15T19:39:11Z',
23+
rateLimiterState: {
24+
__typename: 'RateLimiterState',
25+
currentCapacity: 10,
26+
burst: 10,
27+
limit: 5000,
28+
interval: 1,
29+
lastReplenishment: '2021-03-15T19:39:11Z',
30+
infinite: false,
31+
},
2332
webhookURL: null,
2433
hasConnectionCheck: true,
2534
syncJobs: {

cmd/frontend/graphqlbackend/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ go_library(
126126
"product_license_info.go",
127127
"product_subscription_status.go",
128128
"rate_limit.go",
129+
"ratelimiter.go",
129130
"rbac.go",
130131
"recorded_commands.go",
131132
"repositories.go",
@@ -303,6 +304,7 @@ go_library(
303304
"//internal/observation",
304305
"//internal/oobmigration",
305306
"//internal/perforce",
307+
"//internal/ratelimit",
306308
"//internal/rbac",
307309
"//internal/rcache",
308310
"//internal/redispool",
@@ -503,6 +505,7 @@ go_test(
503505
"//internal/highlight",
504506
"//internal/inventory",
505507
"//internal/oobmigration",
508+
"//internal/ratelimit",
506509
"//internal/rbac",
507510
"//internal/rbac/types",
508511
"//internal/rcache",

cmd/frontend/graphqlbackend/external_service.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/sourcegraph/sourcegraph/internal/extsvc"
2020
"github.com/sourcegraph/sourcegraph/internal/gqlutil"
2121
"github.com/sourcegraph/sourcegraph/internal/httpcli"
22+
"github.com/sourcegraph/sourcegraph/internal/ratelimit"
2223
"github.com/sourcegraph/sourcegraph/internal/repos"
2324
"github.com/sourcegraph/sourcegraph/internal/repoupdater/protocol"
2425
"github.com/sourcegraph/sourcegraph/internal/types"
@@ -128,6 +129,20 @@ func (r *externalServiceResolver) DisplayName() string {
128129
return r.externalService.DisplayName
129130
}
130131

132+
func (r *externalServiceResolver) RateLimiterState(ctx context.Context) (*rateLimiterStateResolver, error) {
133+
info, err := ratelimit.GetGlobalLimiterState(ctx)
134+
if err != nil {
135+
return nil, errors.Wrap(err, "getting rate limiter state")
136+
}
137+
138+
state, ok := info[r.externalService.URN()]
139+
if !ok {
140+
return nil, nil
141+
}
142+
143+
return &rateLimiterStateResolver{state: state}, nil
144+
}
145+
131146
func (r *externalServiceResolver) Config(ctx context.Context) (JSONCString, error) {
132147
redacted, err := r.externalService.RedactedConfig(ctx)
133148
if err != nil {

cmd/frontend/graphqlbackend/external_services_test.go

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/sourcegraph/sourcegraph/internal/api"
2020
"github.com/sourcegraph/sourcegraph/internal/database/dbmocks"
2121
"github.com/sourcegraph/sourcegraph/internal/extsvc/github"
22+
"github.com/sourcegraph/sourcegraph/internal/ratelimit"
2223
"github.com/sourcegraph/sourcegraph/internal/repoupdater/protocol"
2324
"github.com/sourcegraph/sourcegraph/lib/errors"
2425

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

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

633-
ess := []*types.ExternalService{
634-
{ID: 1, Config: extsvc.NewEmptyConfig()},
635-
{ID: 2, Config: extsvc.NewEmptyConfig(), Kind: extsvc.KindGitHub},
636-
{ID: 3, Config: extsvc.NewEmptyConfig(), Kind: extsvc.KindGitHub},
637-
{ID: 4, Config: extsvc.NewEmptyConfig(), Kind: extsvc.KindAWSCodeCommit},
638-
{ID: 5, Config: extsvc.NewEmptyConfig(), Kind: extsvc.KindGerrit},
639-
}
640641
if opt.LimitOffset != nil {
641642
return ess[:opt.LimitOffset.Limit], nil
642643
}
643644
return ess, nil
644645
})
646+
647+
// Set up rate limits
648+
ctx := context.Background()
649+
ratelimit.SetupForTest(t)
650+
for _, es := range ess {
651+
rl := ratelimit.NewGlobalRateLimiter(logtest.NoOp(t), es.URN())
652+
rl.SetTokenBucketConfig(ctx, 10, time.Hour)
653+
}
654+
645655
externalServices.CountFunc.SetDefaultHook(func(ctx context.Context, opt database.ExternalServicesListOptions) (int, error) {
646656
if opt.AfterID > 0 {
647657
return 1, nil
@@ -698,6 +708,90 @@ func TestExternalServices(t *testing.T) {
698708
}
699709
`,
700710
},
711+
{
712+
Schema: mustParseGraphQLSchema(t, db),
713+
Label: "Read with rate limiter state",
714+
Query: `
715+
{
716+
externalServices {
717+
nodes {
718+
id
719+
rateLimiterState {
720+
burst
721+
currentCapacity
722+
infinite
723+
interval
724+
lastReplenishment
725+
limit
726+
}
727+
}
728+
}
729+
}
730+
`,
731+
ExpectedResult: `
732+
{
733+
"externalServices": {
734+
"nodes": [
735+
{
736+
"id":"RXh0ZXJuYWxTZXJ2aWNlOjE=",
737+
"rateLimiterState": {
738+
"burst": 10,
739+
"currentCapacity": 0,
740+
"infinite": false,
741+
"interval": 3600,
742+
"lastReplenishment": "1970-01-01T00:00:00Z",
743+
"limit": 10
744+
}
745+
},
746+
{
747+
"id":"RXh0ZXJuYWxTZXJ2aWNlOjI=",
748+
"rateLimiterState": {
749+
"burst": 10,
750+
"currentCapacity": 0,
751+
"infinite": false,
752+
"interval": 3600,
753+
"lastReplenishment": "1970-01-01T00:00:00Z",
754+
"limit": 10
755+
}
756+
},
757+
{
758+
"id":"RXh0ZXJuYWxTZXJ2aWNlOjM=",
759+
"rateLimiterState": {
760+
"burst": 10,
761+
"currentCapacity": 0,
762+
"infinite": false,
763+
"interval": 3600,
764+
"lastReplenishment": "1970-01-01T00:00:00Z",
765+
"limit": 10
766+
}
767+
},
768+
{
769+
"id":"RXh0ZXJuYWxTZXJ2aWNlOjQ=",
770+
"rateLimiterState": {
771+
"burst": 10,
772+
"currentCapacity": 0,
773+
"infinite": false,
774+
"interval": 3600,
775+
"lastReplenishment": "1970-01-01T00:00:00Z",
776+
"limit": 10
777+
}
778+
},
779+
{
780+
"id":"RXh0ZXJuYWxTZXJ2aWNlOjU=",
781+
"rateLimiterState": {
782+
"burst": 10,
783+
"currentCapacity": 0,
784+
"infinite": false,
785+
"interval": 3600,
786+
"lastReplenishment": "1970-01-01T00:00:00Z",
787+
"limit": 10
788+
}
789+
}
790+
]
791+
}
792+
}
793+
`,
794+
},
701795
{
702796
Schema: mustParseGraphQLSchema(t, db),
703797
Label: "Read all external services for a given repo",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package graphqlbackend
2+
3+
import (
4+
"time"
5+
6+
"github.com/sourcegraph/sourcegraph/internal/gqlutil"
7+
"github.com/sourcegraph/sourcegraph/internal/ratelimit"
8+
)
9+
10+
type rateLimiterStateResolver struct {
11+
state ratelimit.GlobalLimiterInfo
12+
}
13+
14+
func (rl *rateLimiterStateResolver) Burst() int32 {
15+
return int32(rl.state.Burst)
16+
}
17+
18+
func (rl *rateLimiterStateResolver) CurrentCapacity() int32 {
19+
return int32(rl.state.CurrentCapacity)
20+
}
21+
22+
func (rl *rateLimiterStateResolver) Infinite() bool {
23+
return rl.state.Infinite
24+
}
25+
26+
func (rl *rateLimiterStateResolver) Interval() int32 {
27+
return int32(rl.state.Interval / time.Second)
28+
}
29+
30+
func (rl *rateLimiterStateResolver) LastReplenishment() gqlutil.DateTime {
31+
return gqlutil.DateTime{Time: rl.state.LastReplenishment}
32+
}
33+
34+
func (rl *rateLimiterStateResolver) Limit() int32 {
35+
return int32(rl.state.Limit)
36+
}

0 commit comments

Comments
 (0)