Skip to content

Commit 7b2207b

Browse files
fix: handle +100K comps when resolving from org W-17876861 (#1511)
* test: add UT * chore: bump sfdx-core * test: increase class qty * chore: simplify listMember, avoid currying * fix: array concat to avoid stack overflow * fix: set default listMetadata batch limit --------- Co-authored-by: Steve Hetzel <[email protected]>
1 parent 2028285 commit 7b2207b

File tree

2 files changed

+82
-51
lines changed

2 files changed

+82
-51
lines changed

src/resolve/connectionResolver.ts

Lines changed: 52 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ export class ConnectionResolver {
7373
shouldQueryStandardValueSets = false;
7474

7575
// To limit the number of concurrent requests, batch them per an env var.
76-
// By default there is no batching.
77-
this.requestBatchSize = env.getNumber('SF_LIST_METADATA_BATCH_SIZE', -1);
76+
// Default is 500. From testing we saw jsforce gets stuck on ~1K reqs.
77+
this.requestBatchSize = env.getNumber('SF_LIST_METADATA_BATCH_SIZE', 500);
7878
}
7979

8080
public async resolve(
@@ -171,12 +171,12 @@ export class ConnectionResolver {
171171

172172
// Send batched listMetadata requests based on the SF_LIST_METADATA_BATCH_SIZE env var.
173173
private async sendBatchedRequests(listMdQueries: string[]): Promise<RelevantFileProperties[]> {
174-
const listMetadataResponses: RelevantFileProperties[] = [];
174+
let listMetadataResponses: RelevantFileProperties[] = [];
175175
let listMetadataRequests: Array<Promise<RelevantFileProperties[]>> = [];
176176

177177
const sendIt = async (): Promise<void> => {
178178
const requestBatch = (await Promise.all(listMetadataRequests)).flat();
179-
listMetadataResponses.push(...requestBatch);
179+
listMetadataResponses = listMetadataResponses.concat(requestBatch);
180180
};
181181

182182
// Make batched listMetadata requests
@@ -186,7 +186,7 @@ export class ConnectionResolver {
186186
if (q[1]) {
187187
listMdQuery.folder = q[1];
188188
}
189-
listMetadataRequests.push(listMembers(this.registry)(this.connection)(listMdQuery));
189+
listMetadataRequests.push(listMembers(this.registry, this.connection, listMdQuery));
190190
i++;
191191
if (this.requestBatchSize > 0 && i % this.requestBatchSize === 0) {
192192
getLogger().debug(`Awaiting listMetadata requests ${i - this.requestBatchSize + 1} - ${i}`);
@@ -212,12 +212,12 @@ export class ConnectionResolver {
212212
// SF_LIST_METADATA_BATCH_SIZE env var.
213213
private async sendBatchedQueries(): Promise<RelevantFileProperties[]> {
214214
const mdType = this.registry.getTypeByName('StandardValueSet');
215-
const queryResponses: RelevantFileProperties[] = [];
215+
let queryResponses: RelevantFileProperties[] = [];
216216
let queryRequests: Array<Promise<RelevantFileProperties | undefined>> = [];
217217

218218
const sendIt = async (): Promise<void> => {
219219
const requestBatch = (await Promise.all(queryRequests)).flat();
220-
queryResponses.push(...requestBatch.filter((rb) => !!rb));
220+
queryResponses = queryResponses.concat(requestBatch.filter((rb) => !!rb));
221221
};
222222

223223
// Make batched query requests
@@ -270,57 +270,58 @@ const querySvs =
270270
}
271271
};
272272

273-
const listMembers =
274-
(registry: RegistryAccess) =>
275-
(connection: Connection) =>
276-
async (query: ListMetadataQuery): Promise<RelevantFileProperties[]> => {
277-
const mdType = registry.getTypeByName(query.type);
278-
279-
// Workaround because metadata.list({ type: 'StandardValueSet' }) returns [].
280-
// Query for a subset of known StandardValueSets after all listMetadata calls.
281-
if (mdType.name === registry.getRegistry().types.standardvalueset.name) {
282-
shouldQueryStandardValueSets = true;
283-
return [];
284-
}
285-
286-
// Workaround because metadata.list({ type: 'BotVersion' }) returns [].
287-
if (mdType.name === 'BotVersion') {
288-
try {
289-
const botDefQuery = 'SELECT Id, DeveloperName FROM BotDefinition';
290-
const botVersionQuery = 'SELECT BotDefinitionId, DeveloperName FROM BotVersion';
291-
const botDefs = (await connection.query<{ Id: string; DeveloperName: string }>(botDefQuery)).records;
292-
const botVersionDefs = (
293-
await connection.query<{ BotDefinitionId: string; DeveloperName: string }>(botVersionQuery)
294-
).records;
295-
return botVersionDefs
296-
.map((bvd) => {
297-
const botName = botDefs.find((bd) => bd.Id === bvd.BotDefinitionId)?.DeveloperName;
298-
if (botName) {
299-
return {
300-
fullName: `${botName}.${bvd.DeveloperName}`,
301-
fileName: `bots/${bvd.DeveloperName}.botVersion`,
302-
type: 'BotVersion',
303-
};
304-
}
305-
})
306-
.filter((b) => !!b);
307-
} catch (error) {
308-
const err = SfError.wrap(error);
309-
getLogger().debug(`[${mdType.name}] ${err.message}`);
310-
return [];
311-
}
312-
}
273+
async function listMembers(
274+
registry: RegistryAccess,
275+
connection: Connection,
276+
query: ListMetadataQuery
277+
): Promise<RelevantFileProperties[]> {
278+
const mdType = registry.getTypeByName(query.type);
279+
280+
// Workaround because metadata.list({ type: 'StandardValueSet' }) returns [].
281+
// Query for a subset of known StandardValueSets after all listMetadata calls.
282+
if (mdType.name === registry.getRegistry().types.standardvalueset.name) {
283+
shouldQueryStandardValueSets = true;
284+
return [];
285+
}
313286

287+
// Workaround because metadata.list({ type: 'BotVersion' }) returns [].
288+
if (mdType.name === 'BotVersion') {
314289
try {
315-
requestCount++;
316-
getLogger().debug(`listMetadata for ${inspect(query)}`);
317-
return (await connection.metadata.list(query)).map(inferFilenamesFromType(mdType));
290+
const botDefQuery = 'SELECT Id, DeveloperName FROM BotDefinition';
291+
const botVersionQuery = 'SELECT BotDefinitionId, DeveloperName FROM BotVersion';
292+
const botDefs = (await connection.query<{ Id: string; DeveloperName: string }>(botDefQuery)).records;
293+
const botVersionDefs = (
294+
await connection.query<{ BotDefinitionId: string; DeveloperName: string }>(botVersionQuery)
295+
).records;
296+
return botVersionDefs
297+
.map((bvd) => {
298+
const botName = botDefs.find((bd) => bd.Id === bvd.BotDefinitionId)?.DeveloperName;
299+
if (botName) {
300+
return {
301+
fullName: `${botName}.${bvd.DeveloperName}`,
302+
fileName: `bots/${bvd.DeveloperName}.botVersion`,
303+
type: 'BotVersion',
304+
};
305+
}
306+
})
307+
.filter((b) => !!b);
318308
} catch (error) {
319309
const err = SfError.wrap(error);
320310
getLogger().debug(`[${mdType.name}] ${err.message}`);
321311
return [];
322312
}
323-
};
313+
}
314+
315+
try {
316+
requestCount++;
317+
getLogger().debug(`listMetadata for ${inspect(query)}`);
318+
return (await connection.metadata.list(query)).map(inferFilenamesFromType(mdType));
319+
} catch (error) {
320+
const err = SfError.wrap(error);
321+
getLogger().debug(`[${mdType.name}] ${err.message}`);
322+
return [];
323+
}
324+
}
324325

325326
/* if the Metadata Type doesn't return a correct fileName then help it out */
326327
const inferFilenamesFromType =

test/resolve/connectionResolver.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,36 @@ describe('ConnectionResolver', () => {
168168
expect(metadataQueryStub.calledOnce).to.be.true;
169169
});
170170

171+
it('should resolve +100K components', async () => {
172+
const promiseAllSpy = $$.SANDBOX.spy(Promise, 'all');
173+
// @ts-expect-error spying on private method
174+
const resolverSpy = $$.SANDBOX.spy(ConnectionResolver.prototype, 'sendBatchedQueries');
175+
176+
const metadataListStub = $$.SANDBOX.stub(connection.metadata, 'list');
177+
178+
const classesQty = 200_000;
179+
const tonsOfApexClasses = Array.from({ length: classesQty }, (_value, index) => ({
180+
...StdFileProperty,
181+
fileName: `classes/MyApexClass${index}.class`,
182+
fullName: `MyApexClass${index}`,
183+
type: 'ApexClass',
184+
}));
185+
186+
metadataListStub.withArgs({ type: 'ApexClass' }).resolves(tonsOfApexClasses);
187+
188+
const mdTypes = ['ApexClass'];
189+
const resolver = new ConnectionResolver(connection, undefined, mdTypes);
190+
const result = await resolver.resolve();
191+
const expected = Array.from({ length: classesQty }, (_value, index) => ({
192+
fullName: `MyApexClass${index}`,
193+
type: registry.types.apexclass,
194+
}));
195+
196+
expect(result.components).to.deep.equal(expected);
197+
expect(promiseAllSpy.callCount, 'Expected Promise.all() to be called 2 times').to.equal(1);
198+
expect(promiseAllSpy.firstCall.args[0]).to.be.an('array').with.lengthOf(1);
199+
expect(resolverSpy.called).to.be.false;
200+
});
171201
it('should batch requests per SF_LIST_METADATA_BATCH_SIZE', async () => {
172202
$$.SANDBOX.stub(env, 'getNumber').returns(2);
173203
const promiseAllSpy = $$.SANDBOX.spy(Promise, 'all');

0 commit comments

Comments
 (0)