Skip to content

Commit a9345da

Browse files
authored
Fix 429 errors on OVSX requests (#14030)
1 parent 817c1a0 commit a9345da

11 files changed

+66
-23
lines changed

dev-packages/cli/src/download-plugins.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ export interface DownloadPluginsOptions {
5555
* Fetch plugins in parallel
5656
*/
5757
parallel?: boolean;
58-
59-
rateLimit?: number;
6058
}
6159

6260
interface PluginDownload {
@@ -65,16 +63,19 @@ interface PluginDownload {
6563
version?: string | undefined
6664
}
6765

68-
export default async function downloadPlugins(ovsxClient: OVSXClient, requestService: RequestService, options: DownloadPluginsOptions = {}): Promise<void> {
66+
export default async function downloadPlugins(
67+
ovsxClient: OVSXClient,
68+
rateLimiter: RateLimiter,
69+
requestService: RequestService,
70+
options: DownloadPluginsOptions = {}
71+
): Promise<void> {
6972
const {
7073
packed = false,
7174
ignoreErrors = false,
7275
apiVersion = DEFAULT_SUPPORTED_API_VERSION,
73-
rateLimit = 15,
7476
parallel = true
7577
} = options;
7678

77-
const rateLimiter = new RateLimiter({ tokensPerInterval: rateLimit, interval: 'second' });
7879
const apiFilter = new OVSXApiFilterImpl(ovsxClient, apiVersion);
7980

8081
// Collect the list of failures to be appended at the end of the script.

dev-packages/cli/src/theia.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ import { ApplicationProps, DEFAULT_SUPPORTED_API_VERSION } from '@theia/applicat
2424
import checkDependencies from './check-dependencies';
2525
import downloadPlugins from './download-plugins';
2626
import runTest from './run-test';
27+
import { RateLimiter } from 'limiter';
2728
import { LocalizationManager, extract } from '@theia/localization-manager';
2829
import { NodeRequestService } from '@theia/request/lib/node-request-service';
29-
import { ExtensionIdMatchesFilterFactory, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory } from '@theia/ovsx-client';
30+
import { ExtensionIdMatchesFilterFactory, OVSX_RATE_LIMIT, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory } from '@theia/ovsx-client';
3031

3132
const { executablePath } = require('puppeteer');
3233

@@ -389,7 +390,7 @@ async function theiaCli(): Promise<void> {
389390
'rate-limit': {
390391
describe: 'Amount of maximum open-vsx requests per second',
391392
number: true,
392-
default: 15
393+
default: OVSX_RATE_LIMIT
393394
},
394395
'proxy-url': {
395396
describe: 'Proxy URL'
@@ -415,22 +416,23 @@ async function theiaCli(): Promise<void> {
415416
strictSSL: strictSsl
416417
});
417418
let client: OVSXClient | undefined;
419+
const rateLimiter = new RateLimiter({ tokensPerInterval: options.rateLimit, interval: 'second' });
418420
if (ovsxRouterConfig) {
419421
const routerConfig = await fs.promises.readFile(ovsxRouterConfig, 'utf8').then(JSON.parse, error => {
420422
console.error(error);
421423
});
422424
if (routerConfig) {
423425
client = await OVSXRouterClient.FromConfig(
424426
routerConfig,
425-
OVSXHttpClient.createClientFactory(requestService),
427+
OVSXHttpClient.createClientFactory(requestService, rateLimiter),
426428
[RequestContainsFilterFactory, ExtensionIdMatchesFilterFactory]
427429
);
428430
}
429431
}
430432
if (!client) {
431-
client = new OVSXHttpClient(apiUrl, requestService);
433+
client = new OVSXHttpClient(apiUrl, requestService, rateLimiter);
432434
}
433-
await downloadPlugins(client, requestService, options);
435+
await downloadPlugins(client, rateLimiter, requestService, options);
434436
},
435437
})
436438
.command<{

dev-packages/ovsx-client/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
},
3131
"dependencies": {
3232
"@theia/request": "1.52.0",
33+
"limiter": "^2.1.0",
3334
"semver": "^7.5.4",
3435
"tslib": "^2.6.2"
3536
}

dev-packages/ovsx-client/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// *****************************************************************************
1616

1717
export { OVSXApiFilter, OVSXApiFilterImpl, OVSXApiFilterProvider } from './ovsx-api-filter';
18-
export { OVSXHttpClient } from './ovsx-http-client';
18+
export { OVSXHttpClient, OVSX_RATE_LIMIT } from './ovsx-http-client';
1919
export { OVSXMockClient } from './ovsx-mock-client';
2020
export { OVSXRouterClient, OVSXRouterConfig, OVSXRouterFilterFactory as FilterFactory } from './ovsx-router-client';
2121
export * from './ovsx-router-filters';

dev-packages/ovsx-client/src/ovsx-api-filter.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,13 @@ export class OVSXApiFilterImpl implements OVSXApiFilter {
6767

6868
protected async queryLatestCompatibleExtension(query: VSXQueryOptions): Promise<VSXExtensionRaw | undefined> {
6969
let offset = 0;
70+
let size = 5;
7071
let loop = true;
7172
while (loop) {
7273
const queryOptions: VSXQueryOptions = {
7374
...query,
7475
offset,
75-
size: 5 // there is a great chance that the newest version will work
76+
size // there is a great chance that the newest version will work
7677
};
7778
const results = await this.client.query(queryOptions);
7879
const compatibleExtension = this.getLatestCompatibleExtension(results.extensions);
@@ -83,6 +84,8 @@ export class OVSXApiFilterImpl implements OVSXApiFilter {
8384
offset += results.extensions.length;
8485
// Continue querying if there are more extensions available
8586
loop = results.totalSize > offset;
87+
// Adjust the size to fetch more extensions next time
88+
size = Math.min(size * 2, 100);
8689
}
8790
return undefined;
8891
}

dev-packages/ovsx-client/src/ovsx-http-client.ts

+26-7
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,26 @@
1616

1717
import { OVSXClient, VSXQueryOptions, VSXQueryResult, VSXSearchOptions, VSXSearchResult } from './ovsx-types';
1818
import { RequestContext, RequestService } from '@theia/request';
19+
import { RateLimiter } from 'limiter';
20+
21+
export const OVSX_RATE_LIMIT = 15;
1922

2023
export class OVSXHttpClient implements OVSXClient {
2124

2225
/**
2326
* @param requestService
2427
* @returns factory that will cache clients based on the requested input URL.
2528
*/
26-
static createClientFactory(requestService: RequestService): (url: string) => OVSXClient {
29+
static createClientFactory(requestService: RequestService, rateLimiter?: RateLimiter): (url: string) => OVSXClient {
2730
// eslint-disable-next-line no-null/no-null
2831
const cachedClients: Record<string, OVSXClient> = Object.create(null);
29-
return url => cachedClients[url] ??= new this(url, requestService);
32+
return url => cachedClients[url] ??= new this(url, requestService, rateLimiter);
3033
}
3134

3235
constructor(
3336
protected vsxRegistryUrl: string,
34-
protected requestService: RequestService
37+
protected requestService: RequestService,
38+
protected rateLimiter = new RateLimiter({ tokensPerInterval: OVSX_RATE_LIMIT, interval: 'second' })
3539
) { }
3640

3741
search(searchOptions?: VSXSearchOptions): Promise<VSXSearchResult> {
@@ -43,10 +47,25 @@ export class OVSXHttpClient implements OVSXClient {
4347
}
4448

4549
protected async requestJson<R>(url: string): Promise<R> {
46-
return RequestContext.asJson<R>(await this.requestService.request({
47-
url,
48-
headers: { 'Accept': 'application/json' }
49-
}));
50+
const attempts = 5;
51+
for (let i = 0; i < attempts; i++) {
52+
// Use 1, 2, 4, 8, 16 tokens for each attempt
53+
const tokenCount = Math.pow(2, i);
54+
await this.rateLimiter.removeTokens(tokenCount);
55+
const context = await this.requestService.request({
56+
url,
57+
headers: { 'Accept': 'application/json' }
58+
});
59+
if (context.res.statusCode === 429) {
60+
console.warn('OVSX rate limit exceeded. Consider reducing the rate limit.');
61+
// If there are still more attempts left, retry the request with a higher token count
62+
if (i < attempts - 1) {
63+
continue;
64+
}
65+
}
66+
return RequestContext.asJson<R>(context);
67+
}
68+
throw new Error('Failed to fetch data from OVSX.');
5069
}
5170

5271
protected buildUrl(url: string, query?: object): string {

packages/vsx-registry/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"@theia/plugin-ext-vscode": "1.52.0",
1212
"@theia/preferences": "1.52.0",
1313
"@theia/workspace": "1.52.0",
14+
"limiter": "^2.1.0",
1415
"luxon": "^2.4.0",
1516
"p-debounce": "^2.1.0",
1617
"semver": "^7.5.4",

packages/vsx-registry/src/common/vsx-environment.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const VSX_ENVIRONMENT_PATH = '/services/vsx-environment';
2020

2121
export const VSXEnvironment = Symbol('VSXEnvironment');
2222
export interface VSXEnvironment {
23+
getRateLimit(): Promise<number>;
2324
getRegistryUri(): Promise<string>;
2425
getRegistryApiUri(): Promise<string>;
2526
getVscodeApiVersion(): Promise<string>;

packages/vsx-registry/src/common/vsx-registry-common-module.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
ExtensionIdMatchesFilterFactory, OVSXApiFilter, OVSXApiFilterImpl, OVSXApiFilterProvider, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory
2222
} from '@theia/ovsx-client';
2323
import { VSXEnvironment } from './vsx-environment';
24+
import { RateLimiter } from 'limiter';
2425

2526
export default new ContainerModule(bind => {
2627
bind(OVSXUrlResolver)
@@ -34,10 +35,15 @@ export default new ContainerModule(bind => {
3435
.all([
3536
vsxEnvironment.getRegistryApiUri(),
3637
vsxEnvironment.getOvsxRouterConfig?.(),
38+
vsxEnvironment.getRateLimit()
3739
])
38-
.then<OVSXClient>(async ([apiUrl, ovsxRouterConfig]) => {
40+
.then<OVSXClient>(async ([apiUrl, ovsxRouterConfig, rateLimit]) => {
41+
const rateLimiter = new RateLimiter({
42+
interval: 'second',
43+
tokensPerInterval: rateLimit
44+
});
3945
if (ovsxRouterConfig) {
40-
const clientFactory = OVSXHttpClient.createClientFactory(requestService);
46+
const clientFactory = OVSXHttpClient.createClientFactory(requestService, rateLimiter);
4147
return OVSXRouterClient.FromConfig(
4248
ovsxRouterConfig,
4349
async url => clientFactory(await urlResolver(url)),
@@ -46,7 +52,8 @@ export default new ContainerModule(bind => {
4652
}
4753
return new OVSXHttpClient(
4854
await urlResolver(apiUrl),
49-
requestService
55+
requestService,
56+
rateLimiter
5057
);
5158
});
5259
// reuse the promise for subsequent calls to this provider

packages/vsx-registry/src/node/vsx-cli.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@
1717
import { CliContribution } from '@theia/core/lib/node';
1818
import { injectable } from '@theia/core/shared/inversify';
1919
import { Argv } from '@theia/core/shared/yargs';
20-
import { OVSXRouterConfig } from '@theia/ovsx-client';
20+
import { OVSX_RATE_LIMIT, OVSXRouterConfig } from '@theia/ovsx-client';
2121
import * as fs from 'fs';
2222

2323
@injectable()
2424
export class VsxCli implements CliContribution {
2525

2626
ovsxRouterConfig: OVSXRouterConfig | undefined;
27+
ovsxRateLimit: number;
2728
pluginsToInstall: string[] = [];
2829

2930
configure(conf: Argv<{}>): void {
3031
conf.option('ovsx-router-config', { description: 'JSON configuration file for the OVSX router client', type: 'string' });
32+
conf.option('ovsx-rate-limit', { description: 'Limits the number of requests to OVSX per second', type: 'number', default: OVSX_RATE_LIMIT });
3133
conf.option('install-plugin', {
3234
alias: 'install-extension',
3335
nargs: 1,
@@ -47,5 +49,7 @@ export class VsxCli implements CliContribution {
4749
if (Array.isArray(pluginsToInstall)) {
4850
this.pluginsToInstall = pluginsToInstall;
4951
}
52+
const ovsxRateLimit = args.ovsxRateLimit;
53+
this.ovsxRateLimit = typeof ovsxRateLimit === 'number' ? ovsxRateLimit : OVSX_RATE_LIMIT;
5054
}
5155
}

packages/vsx-registry/src/node/vsx-environment-impl.ts

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export class VSXEnvironmentImpl implements VSXEnvironment {
3232
@inject(VsxCli)
3333
protected vsxCli: VsxCli;
3434

35+
async getRateLimit(): Promise<number> {
36+
return this.vsxCli.ovsxRateLimit;
37+
}
38+
3539
async getRegistryUri(): Promise<string> {
3640
return this._registryUri.toString(true);
3741
}

0 commit comments

Comments
 (0)