Skip to content

Commit bb27b8d

Browse files
iowillhoitmshanemcWillieRuemmele
authored
Add retry to StandardValueSet query to fix network errors (#652)
* fix: retries StandardValueSet query * chore: auto-update metadata coverage in METADATA_SUPPORT.md * chore: auto-update metadata coverage in METADATA_SUPPORT.md * chore: package.json bumps * chore: auto-update metadata coverage in METADATA_SUPPORT.md * chore: yarn.lock * ci: prevent stl/sdr conflicts with this sdr * ci: install shx earlier * chore: adds retry tests * chore: auto-update metadata coverage in METADATA_SUPPORT.md Co-authored-by: mshanemc <[email protected]> Co-authored-by: Willie Ruemmele <[email protected]>
1 parent 2dc77de commit bb27b8d

File tree

5 files changed

+425
-356
lines changed

5 files changed

+425
-356
lines changed

.circleci/config.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,20 @@ jobs:
9393
equal: ['linux', <<parameters.os>>]
9494
steps:
9595
- run: yarn add $CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME#$CIRCLE_SHA1
96+
- run:
97+
# STL could also have an SDR inside it. Take it out to prevent conflicts
98+
name: remove sdr from source-tracking
99+
command: |
100+
npm install shx -g
101+
shx rm -rf node_modules/@salesforce/source-deploy-retrieve
102+
working_directory: node_modules/@salesforce/source-tracking
96103
- run:
97104
name: install/build <<parameters.external_project_git_url>> in node_modules
98105
# why doesn't SDR put the metadataRegistry.json in the lib when run from inside a node module? I don't know.
99106
# prevent dependency conflicts between plugin's top-level imports and imported SDR's deps by deleting them
100107
# If there are real conflicts, we'll catch them when bumping a version in the plugin (same nuts)
101108
command: |
102109
yarn install
103-
npm install shx -g
104110
shx rm -rf node_modules/@salesforce/kit
105111
shx rm -rf node_modules/@typescript-eslint
106112
shx rm -rf node_modules/eslint-plugin-header

package.json

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,61 +25,61 @@
2525
"node": ">=14.0.0"
2626
},
2727
"dependencies": {
28-
"@salesforce/core": "^3.21.1",
29-
"@salesforce/kit": "^1.5.41",
28+
"@salesforce/core": "^3.22.0",
29+
"@salesforce/kit": "^1.5.42",
3030
"@salesforce/ts-types": "^1.5.20",
31-
"archiver": "^5.3.0",
32-
"fast-xml-parser": "^3.17.4",
31+
"archiver": "^5.3.1",
32+
"fast-xml-parser": "^3.21.1",
3333
"got": "^11.8.5",
34-
"graceful-fs": "^4.2.8",
35-
"ignore": "^5.1.8",
34+
"graceful-fs": "^4.2.10",
35+
"ignore": "^5.2.0",
3636
"mime": "2.6.0",
3737
"proxy-agent": "^5.0.0",
3838
"proxy-from-env": "^1.1.0",
3939
"unzipper": "0.10.11",
40-
"xmldom-sfdx-encoding": "^0.1.29"
40+
"xmldom-sfdx-encoding": "^0.1.30"
4141
},
4242
"devDependencies": {
4343
"@salesforce/dev-config": "^3.0.1",
44-
"@salesforce/dev-scripts": "^2.0.1",
44+
"@salesforce/dev-scripts": "^2.0.3",
4545
"@salesforce/prettier-config": "^0.0.2",
46-
"@salesforce/ts-sinon": "^1.1.2",
47-
"@types/archiver": "^5.1.1",
46+
"@salesforce/ts-sinon": "^1.3.21",
47+
"@types/archiver": "^5.3.1",
4848
"@types/deep-equal-in-any-order": "^1.0.1",
4949
"@types/mime": "2.0.3",
5050
"@types/mkdirp": "0.5.2",
51+
"@types/shelljs": "^0.8.11",
52+
"@types/unzipper": "^0.10.5",
5153
"@types/proxy-from-env": "^1.0.1",
52-
"@types/shelljs": "^0.8.9",
53-
"@types/unzipper": "^0.10.3",
5454
"@typescript-eslint/eslint-plugin": "^4.33.0",
5555
"@typescript-eslint/parser": "^4.33.0",
56-
"chai": "^4.2.0",
57-
"commitizen": "^3.0.5",
56+
"chai": "^4.3.6",
57+
"commitizen": "^3.1.2",
5858
"cz-conventional-changelog": "^2.1.0",
5959
"deep-equal-in-any-order": "^1.1.19",
6060
"deepmerge": "^4.2.2",
6161
"eslint": "^7.32.0",
62-
"eslint-config-prettier": "^6.11.0",
62+
"eslint-config-prettier": "^6.15.0",
6363
"eslint-config-salesforce": "^0.1.6",
6464
"eslint-config-salesforce-license": "^0.1.6",
6565
"eslint-config-salesforce-typescript": "^0.2.8",
6666
"eslint-plugin-header": "^3.1.1",
67-
"eslint-plugin-import": "^2.24.2",
68-
"eslint-plugin-jsdoc": "^35.1.3",
67+
"eslint-plugin-import": "^2.26.0",
68+
"eslint-plugin-jsdoc": "^35.5.1",
6969
"eslint-plugin-prettier": "^4.0.0",
7070
"husky": "^7.0.4",
7171
"jsforce": "2.0.0-beta.10",
72-
"lint-staged": "^10.2.11",
73-
"mocha": "^9.1.3",
72+
"lint-staged": "^10.5.4",
73+
"mocha": "^9.2.2",
7474
"mocha-junit-reporter": "^1.23.3",
7575
"nyc": "^15.1.0",
76-
"prettier": "^2.0.5",
77-
"pretty-quick": "^3.1.0",
76+
"prettier": "^2.7.1",
77+
"pretty-quick": "^3.1.3",
7878
"shelljs": "0.8.5",
79-
"shx": "^0.3.2",
79+
"shx": "^0.3.4",
8080
"sinon": "10.0.0",
8181
"ts-node": "^10.8.1",
82-
"typescript": "^4.1.3"
82+
"typescript": "^4.7.4"
8383
},
8484
"scripts": {
8585
"build": "sf-build",

src/resolve/connectionResolver.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { Connection, Logger } from '@salesforce/core';
9+
import { retry, NotRetryableError, RetryError } from 'ts-retry-promise';
910
import { RegistryAccess, registry as defaultRegistry, MetadataType } from '../registry';
1011
import { standardValueSet } from '../registry/standardvalueset';
1112
import { FileProperties, StdValueSetRecord, ListMetadataQuery } from '../client/types';
@@ -108,10 +109,28 @@ export class ConnectionResolver {
108109
if (query.type === defaultRegistry.types.standardvalueset.name && members.length === 0) {
109110
const standardValueSetPromises = standardValueSet.fullnames.map(async (standardValueSetFullName) => {
110111
try {
111-
const standardValueSetRecord: StdValueSetRecord = await this.connection.singleRecordQuery(
112-
`SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = '${standardValueSetFullName}'`,
113-
{ tooling: true }
114-
);
112+
// The 'singleRecordQuery' method was having connection errors, using `retry` resolves this
113+
// Note that this type of connection retry logic may someday be added to jsforce v2
114+
// Once that happens this logic could be reverted
115+
const standardValueSetRecord: StdValueSetRecord = await retry(async () => {
116+
try {
117+
return await this.connection.singleRecordQuery(
118+
`SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = '${standardValueSetFullName}'`,
119+
{ tooling: true }
120+
);
121+
} catch (err) {
122+
// We exit the retry loop with `NotRetryableError` if we get an (expected) unsupported metadata type error
123+
const error = err as Error;
124+
if (error.message.includes('either inaccessible or not supported in Metadata API')) {
125+
this.logger.debug('Expected error:', error.message);
126+
throw new NotRetryableError(error.message);
127+
}
128+
129+
// Otherwise throw the err so we can retry again
130+
throw err;
131+
}
132+
});
133+
115134
return (
116135
standardValueSetRecord.Metadata.standardValue.length && {
117136
fullName: standardValueSetRecord.MasterLabel,
@@ -126,8 +145,15 @@ export class ConnectionResolver {
126145
lastModifiedDate: '',
127146
}
128147
);
129-
} catch (error) {
130-
this.logger.debug((error as Error).message);
148+
} catch (err) {
149+
// error.message here will be overwritten by 'ts-retry-promise'
150+
// Example error.message from the library: "All retries failed" or "Met not retryable error"
151+
// 'ts-retry-promise' exposes the actual error on `error.lastError`
152+
const error = err as RetryError;
153+
154+
if (error.lastError && error.lastError.message) {
155+
this.logger.debug(error.lastError.message);
156+
}
131157
}
132158
});
133159
for await (const standardValueSetResult of standardValueSetPromises) {

test/resolve/connectionResolver.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { expect } from 'chai';
99
import { MockTestOrgData, testSetup } from '@salesforce/core/lib/testSetup';
1010
import { createSandbox, SinonSandbox } from 'sinon';
11-
import { Connection } from '@salesforce/core';
11+
import { Connection, Logger } from '@salesforce/core';
1212
import { mockConnection } from '../mock/client';
1313
import { ConnectionResolver } from '../../src/resolve';
1414
import { MetadataComponent, registry } from '../../src/';
@@ -258,6 +258,52 @@ describe('ConnectionResolver', () => {
258258
];
259259
expect(result.components).to.deep.equal(expected);
260260
});
261+
262+
it('should retry (ten times) if unexpected error occurs', async () => {
263+
const loggerStub = sandboxStub.stub(Logger.prototype, 'debug');
264+
265+
sandboxStub.stub(connection.metadata, 'list');
266+
267+
const query = "SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = 'AccountOwnership'";
268+
269+
const mockToolingQuery = sandboxStub.stub(connection, 'singleRecordQuery');
270+
mockToolingQuery.withArgs(query).rejects(new Error('Something happened. Oh no.'));
271+
272+
const resolver = new ConnectionResolver(connection);
273+
const result = await resolver.resolve();
274+
const expected: MetadataComponent[] = [];
275+
276+
// filter over queries and find ones called with `query`
277+
const retries = mockToolingQuery.args.filter((call) => call[0] === query);
278+
279+
expect(retries.length).to.equal(11); // first call plus 10 retries
280+
expect(loggerStub.calledOnce).to.be.true;
281+
expect(loggerStub.args[0][0]).to.equal('Something happened. Oh no.');
282+
expect(result.components).to.deep.equal(expected);
283+
});
284+
285+
it('should not retry query if expected unsupported metadata error is encountered', async () => {
286+
const loggerStub = sandboxStub.stub(Logger.prototype, 'debug');
287+
288+
sandboxStub.stub(connection.metadata, 'list');
289+
290+
const errorMessage = 'WorkTypeGroupAddInfo is either inaccessible or not supported in Metadata API';
291+
292+
const mockToolingQuery = sandboxStub.stub(connection, 'singleRecordQuery');
293+
mockToolingQuery
294+
.withArgs("SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = 'WorkTypeGroupAddInfo'")
295+
.rejects(new Error(errorMessage));
296+
297+
const resolver = new ConnectionResolver(connection);
298+
const result = await resolver.resolve();
299+
const expected: MetadataComponent[] = [];
300+
301+
expect(loggerStub.calledOnce).to.be.true;
302+
expect(loggerStub.args[0][0]).to.equal('Expected error:');
303+
expect(loggerStub.args[0][1]).to.equal(errorMessage);
304+
expect(result.components).to.deep.equal(expected);
305+
});
306+
261307
it('should resolve no managed components', async () => {
262308
const metadataQueryStub = sandboxStub.stub(connection.metadata, 'list');
263309

0 commit comments

Comments
 (0)