Skip to content

Commit c182816

Browse files
committed
feat(github-graphql): introduce hierarchical fallback for GraphQL rate limit
Implement a layered fallback mechanism for GraphQL rate limiting: 1. Dynamic rate limit from provider (getRateRemaining) 2. Per-client override (WithFallbackRateLimit) 3. Config override (GRAPHQL_RATE_LIMIT) 4. Default fallback (1000) Also moved GitHub-specific fallback (5000) via WithFallbackRateLimit to the Graphql client.
1 parent 239bf38 commit c182816

3 files changed

Lines changed: 55 additions & 5 deletions

File tree

backend/helpers/pluginhelper/api/graphql_async_client.go

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ import (
2424
"github.com/apache/incubator-devlake/core/log"
2525
"github.com/apache/incubator-devlake/core/plugin"
2626
"github.com/apache/incubator-devlake/core/utils"
27+
"strconv"
2728
"sync"
2829
"time"
2930

3031
"github.com/merico-ai/graphql"
3132
)
3233

34+
// GraphqlClientOption is a function that configures a GraphqlAsyncClient
35+
type GraphqlClientOption func(*GraphqlAsyncClient)
36+
3337
// GraphqlAsyncClient send graphql one by one
3438
type GraphqlAsyncClient struct {
3539
ctx context.Context
@@ -47,34 +51,48 @@ type GraphqlAsyncClient struct {
4751
getRateCost func(q interface{}) int
4852
}
4953

50-
const defaultRateLimit = 5000
54+
// defaultRateLimitConst is the generic fallback rate limit for GraphQL requests.
55+
// It is used as the initial remaining quota when dynamic rate limit
56+
// information is unavailable from the provider.
57+
const defaultRateLimitConst = 1000
5158

5259
// CreateAsyncGraphqlClient creates a new GraphqlAsyncClient
5360
func CreateAsyncGraphqlClient(
5461
taskCtx plugin.TaskContext,
5562
graphqlClient *graphql.Client,
5663
logger log.Logger,
5764
getRateRemaining func(context.Context, *graphql.Client, log.Logger) (rateRemaining int, resetAt *time.Time, err errors.Error),
65+
opts ...GraphqlClientOption,
5866
) (*GraphqlAsyncClient, errors.Error) {
5967
ctxWithCancel, cancel := context.WithCancel(taskCtx.GetContext())
68+
69+
rateLimit := resolveRateLimit(taskCtx, logger)
70+
6071
graphqlAsyncClient := &GraphqlAsyncClient{
6172
ctx: ctxWithCancel,
6273
cancel: cancel,
6374
client: graphqlClient,
6475
logger: logger,
6576
rateExhaustCond: sync.NewCond(&sync.Mutex{}),
66-
rateRemaining: 0,
77+
rateRemaining: rateLimit,
6778
getRateRemaining: getRateRemaining,
6879
}
6980

81+
// apply options
82+
for _, opt := range opts {
83+
opt(graphqlAsyncClient)
84+
}
85+
7086
if getRateRemaining != nil {
7187
rateRemaining, resetAt, err := getRateRemaining(taskCtx.GetContext(), graphqlClient, logger)
7288
if err != nil {
7389
graphqlAsyncClient.logger.Warn(err, "failed to fetch initial graphql rate limit, fallback to default")
74-
graphqlAsyncClient.updateRateRemaining(defaultRateLimit, nil)
90+
graphqlAsyncClient.updateRateRemaining(graphqlAsyncClient.rateRemaining, nil)
7591
} else {
7692
graphqlAsyncClient.updateRateRemaining(rateRemaining, resetAt)
7793
}
94+
} else {
95+
graphqlAsyncClient.updateRateRemaining(graphqlAsyncClient.rateRemaining, nil)
7896
}
7997

8098
// load retry/timeout from configuration
@@ -119,6 +137,10 @@ func (apiClient *GraphqlAsyncClient) updateRateRemaining(rateRemaining int, rese
119137
apiClient.rateExhaustCond.Signal()
120138
}
121139
go func() {
140+
if apiClient.getRateRemaining == nil {
141+
return
142+
}
143+
122144
nextDuring := 3 * time.Minute
123145
if resetAt != nil && resetAt.After(time.Now()) {
124146
nextDuring = time.Until(*resetAt)
@@ -224,3 +246,29 @@ func (apiClient *GraphqlAsyncClient) Wait() {
224246
func (apiClient *GraphqlAsyncClient) Release() {
225247
apiClient.cancel()
226248
}
249+
250+
// WithFallbackRateLimit sets the initial/fallback rate limit used when
251+
// rate limit information cannot be fetched dynamically.
252+
// This value may be overridden later by getRateRemaining.
253+
func WithFallbackRateLimit(limit int) GraphqlClientOption {
254+
return func(c *GraphqlAsyncClient) {
255+
if limit > 0 {
256+
c.rateRemaining = limit
257+
}
258+
}
259+
}
260+
261+
// resolveRateLimit determines the rate limit for GraphQL requests using task configuration -> else default constant.
262+
func resolveRateLimit(taskCtx plugin.TaskContext, logger log.Logger) int {
263+
rateLimit := defaultRateLimitConst
264+
265+
if v := taskCtx.GetConfig("GRAPHQL_RATE_LIMIT"); v != "" {
266+
if parsed, err := strconv.Atoi(v); err == nil {
267+
rateLimit = parsed
268+
} else {
269+
logger.Warn(err, "invalid GRAPHQL_RATE_LIMIT, using default")
270+
}
271+
}
272+
273+
return rateLimit
274+
}

backend/plugins/github_graphql/impl/impl.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ func (p GithubGraphql) PrepareTaskData(taskCtx plugin.TaskContext, options map[s
189189
return 0, nil, errors.Default.Wrap(dataErrors[0], `query rate limit fail`)
190190
}
191191
if query.RateLimit == nil {
192-
logger.Info(`github graphql rate limit are disabled, fallback to 5000req/hour`)
193-
return 5000, nil, nil
192+
logger.Info(`github graphql rate limit unavailable, using fallback rate limit`)
193+
return 0, nil, errors.Default.New("rate limit unavailable")
194194
}
195195
logger.Info(`github graphql init success with remaining %d/%d and will reset at %s`,
196196
query.RateLimit.Remaining, query.RateLimit.Limit, query.RateLimit.ResetAt)

backend/plugins/github_graphql/tasks/graphql_client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,7 @@ func CreateGraphqlClient(
6565
gqlClient,
6666
taskCtx.GetLogger(),
6767
getRateRemaining,
68+
// GitHub GraphQL default fallback aligns with GitHub's standard rate limit (~5000)
69+
helper.WithFallbackRateLimit(5000),
6870
)
6971
}

0 commit comments

Comments
 (0)