Skip to content

Commit

Permalink
Add retry to StandardValueSet query to fix network errors (#652)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Aug 3, 2022
1 parent 2dc77de commit bb27b8d
Show file tree
Hide file tree
Showing 5 changed files with 425 additions and 356 deletions.
8 changes: 7 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,20 @@ jobs:
equal: ['linux', <<parameters.os>>]
steps:
- run: yarn add $CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME#$CIRCLE_SHA1
- run:
# STL could also have an SDR inside it. Take it out to prevent conflicts
name: remove sdr from source-tracking
command: |
npm install shx -g
shx rm -rf node_modules/@salesforce/source-deploy-retrieve
working_directory: node_modules/@salesforce/source-tracking
- run:
name: install/build <<parameters.external_project_git_url>> in node_modules
# why doesn't SDR put the metadataRegistry.json in the lib when run from inside a node module? I don't know.
# prevent dependency conflicts between plugin's top-level imports and imported SDR's deps by deleting them
# If there are real conflicts, we'll catch them when bumping a version in the plugin (same nuts)
command: |
yarn install
npm install shx -g
shx rm -rf node_modules/@salesforce/kit
shx rm -rf node_modules/@typescript-eslint
shx rm -rf node_modules/eslint-plugin-header
Expand Down
46 changes: 23 additions & 23 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,61 +25,61 @@
"node": ">=14.0.0"
},
"dependencies": {
"@salesforce/core": "^3.21.1",
"@salesforce/kit": "^1.5.41",
"@salesforce/core": "^3.22.0",
"@salesforce/kit": "^1.5.42",
"@salesforce/ts-types": "^1.5.20",
"archiver": "^5.3.0",
"fast-xml-parser": "^3.17.4",
"archiver": "^5.3.1",
"fast-xml-parser": "^3.21.1",
"got": "^11.8.5",
"graceful-fs": "^4.2.8",
"ignore": "^5.1.8",
"graceful-fs": "^4.2.10",
"ignore": "^5.2.0",
"mime": "2.6.0",
"proxy-agent": "^5.0.0",
"proxy-from-env": "^1.1.0",
"unzipper": "0.10.11",
"xmldom-sfdx-encoding": "^0.1.29"
"xmldom-sfdx-encoding": "^0.1.30"
},
"devDependencies": {
"@salesforce/dev-config": "^3.0.1",
"@salesforce/dev-scripts": "^2.0.1",
"@salesforce/dev-scripts": "^2.0.3",
"@salesforce/prettier-config": "^0.0.2",
"@salesforce/ts-sinon": "^1.1.2",
"@types/archiver": "^5.1.1",
"@salesforce/ts-sinon": "^1.3.21",
"@types/archiver": "^5.3.1",
"@types/deep-equal-in-any-order": "^1.0.1",
"@types/mime": "2.0.3",
"@types/mkdirp": "0.5.2",
"@types/shelljs": "^0.8.11",
"@types/unzipper": "^0.10.5",
"@types/proxy-from-env": "^1.0.1",
"@types/shelljs": "^0.8.9",
"@types/unzipper": "^0.10.3",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"chai": "^4.2.0",
"commitizen": "^3.0.5",
"chai": "^4.3.6",
"commitizen": "^3.1.2",
"cz-conventional-changelog": "^2.1.0",
"deep-equal-in-any-order": "^1.1.19",
"deepmerge": "^4.2.2",
"eslint": "^7.32.0",
"eslint-config-prettier": "^6.11.0",
"eslint-config-prettier": "^6.15.0",
"eslint-config-salesforce": "^0.1.6",
"eslint-config-salesforce-license": "^0.1.6",
"eslint-config-salesforce-typescript": "^0.2.8",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jsdoc": "^35.1.3",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^35.5.1",
"eslint-plugin-prettier": "^4.0.0",
"husky": "^7.0.4",
"jsforce": "2.0.0-beta.10",
"lint-staged": "^10.2.11",
"mocha": "^9.1.3",
"lint-staged": "^10.5.4",
"mocha": "^9.2.2",
"mocha-junit-reporter": "^1.23.3",
"nyc": "^15.1.0",
"prettier": "^2.0.5",
"pretty-quick": "^3.1.0",
"prettier": "^2.7.1",
"pretty-quick": "^3.1.3",
"shelljs": "0.8.5",
"shx": "^0.3.2",
"shx": "^0.3.4",
"sinon": "10.0.0",
"ts-node": "^10.8.1",
"typescript": "^4.1.3"
"typescript": "^4.7.4"
},
"scripts": {
"build": "sf-build",
Expand Down
38 changes: 32 additions & 6 deletions src/resolve/connectionResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { Connection, Logger } from '@salesforce/core';
import { retry, NotRetryableError, RetryError } from 'ts-retry-promise';
import { RegistryAccess, registry as defaultRegistry, MetadataType } from '../registry';
import { standardValueSet } from '../registry/standardvalueset';
import { FileProperties, StdValueSetRecord, ListMetadataQuery } from '../client/types';
Expand Down Expand Up @@ -108,10 +109,28 @@ export class ConnectionResolver {
if (query.type === defaultRegistry.types.standardvalueset.name && members.length === 0) {
const standardValueSetPromises = standardValueSet.fullnames.map(async (standardValueSetFullName) => {
try {
const standardValueSetRecord: StdValueSetRecord = await this.connection.singleRecordQuery(
`SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = '${standardValueSetFullName}'`,
{ tooling: true }
);
// The 'singleRecordQuery' method was having connection errors, using `retry` resolves this
// Note that this type of connection retry logic may someday be added to jsforce v2
// Once that happens this logic could be reverted
const standardValueSetRecord: StdValueSetRecord = await retry(async () => {
try {
return await this.connection.singleRecordQuery(
`SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = '${standardValueSetFullName}'`,
{ tooling: true }
);
} catch (err) {
// We exit the retry loop with `NotRetryableError` if we get an (expected) unsupported metadata type error
const error = err as Error;
if (error.message.includes('either inaccessible or not supported in Metadata API')) {
this.logger.debug('Expected error:', error.message);
throw new NotRetryableError(error.message);
}

// Otherwise throw the err so we can retry again
throw err;
}
});

return (
standardValueSetRecord.Metadata.standardValue.length && {
fullName: standardValueSetRecord.MasterLabel,
Expand All @@ -126,8 +145,15 @@ export class ConnectionResolver {
lastModifiedDate: '',
}
);
} catch (error) {
this.logger.debug((error as Error).message);
} catch (err) {
// error.message here will be overwritten by 'ts-retry-promise'
// Example error.message from the library: "All retries failed" or "Met not retryable error"
// 'ts-retry-promise' exposes the actual error on `error.lastError`
const error = err as RetryError;

if (error.lastError && error.lastError.message) {
this.logger.debug(error.lastError.message);
}
}
});
for await (const standardValueSetResult of standardValueSetPromises) {
Expand Down
48 changes: 47 additions & 1 deletion test/resolve/connectionResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { expect } from 'chai';
import { MockTestOrgData, testSetup } from '@salesforce/core/lib/testSetup';
import { createSandbox, SinonSandbox } from 'sinon';
import { Connection } from '@salesforce/core';
import { Connection, Logger } from '@salesforce/core';
import { mockConnection } from '../mock/client';
import { ConnectionResolver } from '../../src/resolve';
import { MetadataComponent, registry } from '../../src/';
Expand Down Expand Up @@ -258,6 +258,52 @@ describe('ConnectionResolver', () => {
];
expect(result.components).to.deep.equal(expected);
});

it('should retry (ten times) if unexpected error occurs', async () => {
const loggerStub = sandboxStub.stub(Logger.prototype, 'debug');

sandboxStub.stub(connection.metadata, 'list');

const query = "SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = 'AccountOwnership'";

const mockToolingQuery = sandboxStub.stub(connection, 'singleRecordQuery');
mockToolingQuery.withArgs(query).rejects(new Error('Something happened. Oh no.'));

const resolver = new ConnectionResolver(connection);
const result = await resolver.resolve();
const expected: MetadataComponent[] = [];

// filter over queries and find ones called with `query`
const retries = mockToolingQuery.args.filter((call) => call[0] === query);

expect(retries.length).to.equal(11); // first call plus 10 retries
expect(loggerStub.calledOnce).to.be.true;
expect(loggerStub.args[0][0]).to.equal('Something happened. Oh no.');
expect(result.components).to.deep.equal(expected);
});

it('should not retry query if expected unsupported metadata error is encountered', async () => {
const loggerStub = sandboxStub.stub(Logger.prototype, 'debug');

sandboxStub.stub(connection.metadata, 'list');

const errorMessage = 'WorkTypeGroupAddInfo is either inaccessible or not supported in Metadata API';

const mockToolingQuery = sandboxStub.stub(connection, 'singleRecordQuery');
mockToolingQuery
.withArgs("SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = 'WorkTypeGroupAddInfo'")
.rejects(new Error(errorMessage));

const resolver = new ConnectionResolver(connection);
const result = await resolver.resolve();
const expected: MetadataComponent[] = [];

expect(loggerStub.calledOnce).to.be.true;
expect(loggerStub.args[0][0]).to.equal('Expected error:');
expect(loggerStub.args[0][1]).to.equal(errorMessage);
expect(result.components).to.deep.equal(expected);
});

it('should resolve no managed components', async () => {
const metadataQueryStub = sandboxStub.stub(connection.metadata, 'list');

Expand Down
Loading

0 comments on commit bb27b8d

Please sign in to comment.