Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sh/connection perf #712

Merged
merged 4 commits into from
Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 56 additions & 41 deletions src/org/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { URL } from 'url';
import { AsyncResult, DeployOptions, DeployResultLocator } from 'jsforce/api/metadata';
import { Duration, env, maxBy } from '@salesforce/kit';
import { asString, ensure, isString, JsonCollection, JsonMap, Nullable, Optional } from '@salesforce/ts-types';
import { asString, ensure, isString, JsonCollection, JsonMap, Optional } from '@salesforce/ts-types';
import {
Connection as JSForceConnection,
ConnectionConfig,
Expand Down Expand Up @@ -82,6 +82,12 @@ export class Connection<S extends Schema = Schema> extends JSForceConnection<S>
// All connections are tied to a username
private username!: string;

// Save the DNS resolution result of this connection's instance URL.
private resolvable!: boolean;

// Save the max API version of this connection's org.
private maxApiVersion!: string;

shetzel marked this conversation as resolved.
Show resolved Hide resolved
/**
* Constructor
* **Do not directly construct instances of this class -- use {@link Connection.create} instead.**
Expand Down Expand Up @@ -135,10 +141,7 @@ export class Connection<S extends Schema = Schema> extends JSForceConnection<S>
try {
// No version passed in or in the config, so load one.
if (!baseOptions.version) {
const cachedVersion = await conn.loadInstanceApiVersion();
if (cachedVersion) {
conn.setApiVersion(cachedVersion);
}
await conn.useLatestApiVersion();
} else {
conn.logger.debug(
`The org-api-version ${baseOptions.version} was found from ${
Expand All @@ -153,7 +156,7 @@ export class Connection<S extends Schema = Schema> extends JSForceConnection<S>
}
conn.logger.debug(`Error trying to load the API version: ${e.name} - ${e.message}`);
}
conn.logger.debug(`Using apiVersion ${conn.getApiVersion()}`);
conn.logger.debug(`Connection created with apiVersion ${conn.getApiVersion()}`);
return conn;
}

Expand Down Expand Up @@ -235,18 +238,38 @@ export class Connection<S extends Schema = Schema> extends JSForceConnection<S>
* Retrieves the highest api version that is supported by the target server instance.
*/
public async retrieveMaxApiVersion(): Promise<string> {
if (this.maxApiVersion) {
return this.maxApiVersion;
}

// check API version cache
const cachedApiVersion = this.getCachedApiVersion();
if (cachedApiVersion) {
return cachedApiVersion;
shetzel marked this conversation as resolved.
Show resolved Hide resolved
}

await this.isResolvable();
type Versioned = { version: string };

this.logger.debug(`Fetching API versions supported for org: ${this.getUsername()}`);
const versions: Versioned[] = await this.request<Versioned[]>(`${this.instanceUrl}/services/data`);
// if the server doesn't return a list of versions, it's possibly a instanceUrl issue where the local file is out of date.
if (!Array.isArray(versions)) {
this.logger.debug(`server response for retrieveMaxApiVersion: ${versions as string}`);
throw messages.createError('noApiVersionsError');
}
this.logger.debug(`response for org versions: ${versions.map((item) => item.version).join(',')}`);
const max = ensure(maxBy(versions, (version: Versioned) => version.version));
this.maxApiVersion = ensure(maxBy(versions, (version: Versioned) => version.version)).version;

// cache the max API version just fetched
await this.options.authInfo.save({
instanceApiVersion: this.maxApiVersion,
// This will get messed up if the user changes their local time on their machine.
// Not a big deal since it will just get updated sooner/later.
instanceApiVersionLastRetrieved: new Date().toLocaleString(),
});

return max.version;
return this.maxApiVersion;
}
/**
* Use the latest API version available on `this.instanceUrl`.
Expand All @@ -268,6 +291,10 @@ export class Connection<S extends Schema = Schema> extends JSForceConnection<S>
* Verify that instance has a reachable DNS entry, otherwise will throw error
*/
public async isResolvable(): Promise<boolean> {
if (this.resolvable !== undefined) {
return this.resolvable;
}

if (!this.options.connectionOptions?.instanceUrl) {
throw messages.createError('noInstanceUrlError');
}
Expand All @@ -276,8 +303,10 @@ export class Connection<S extends Schema = Schema> extends JSForceConnection<S>
});
try {
await resolver.resolve();
this.resolvable = true;
return true;
} catch (e) {
this.resolvable = false;
throw messages.createError('domainNotFoundError', [], [], e as Error);
}
}
Expand Down Expand Up @@ -429,10 +458,17 @@ export class Connection<S extends Schema = Schema> extends JSForceConnection<S>
await this.request(requestInfo);
}

private async loadInstanceApiVersion(): Promise<Nullable<string>> {
private getCachedApiVersion(): Optional<string> {
// Exit early if the API version cache is disabled.
if (env.getBoolean('SFDX_IGNORE_API_VERSION_CACHE', false)) {
this.logger.debug('Using latest API version since SFDX_IGNORE_API_VERSION_CACHE = true');
return;
}

// Get API version cache data
const authFileFields = this.options.authInfo.getFields();
const lastCheckedDateString = authFileFields.instanceApiVersionLastRetrieved;
let version = authFileFields.instanceApiVersion;
const version = authFileFields.instanceApiVersion;
let lastChecked: Optional<number>;

try {
Expand All @@ -443,41 +479,20 @@ export class Connection<S extends Schema = Schema> extends JSForceConnection<S>
/* Do nothing, it will just request the version again */
}

// Grab the latest api version from the server and cache it in the auth file
const useLatest = async (): Promise<void> => {
// verifies DNS
await this.useLatestApiVersion();
version = this.getApiVersion();
await this.options.authInfo.save({
instanceApiVersion: version,
// This will get messed up if the user changes their local time on their machine.
// Not a big deal since it will just get updated sooner/later.
instanceApiVersionLastRetrieved: new Date().toLocaleString(),
});
};

const ignoreCache = env.getBoolean('SFDX_IGNORE_API_VERSION_CACHE', false);
if (lastChecked && !ignoreCache) {
// Check if the cache has expired
if (lastChecked) {
const now = new Date();
const has24HoursPastSinceLastCheck = now.getTime() - lastChecked > Duration.hours(24).milliseconds;
this.logger.debug(
`Last checked on ${lastCheckedDateString} (now is ${now.toLocaleString()}) - ${
has24HoursPastSinceLastCheck ? '' : 'not '
}getting latest`
);
if (has24HoursPastSinceLastCheck) {
await useLatest();
this.logger.debug(`API version cache last checked on ${lastCheckedDateString} (now is ${now.toLocaleString()})`);

if (!has24HoursPastSinceLastCheck && version) {
// return cached API version
this.logger.debug(`Using cached API version: ${version}`);
return version;
} else {
this.logger.debug('API version cache expired. Re-fetching latest.');
}
} else {
this.logger.debug(
`Using the latest because lastChecked=${lastChecked} and SFDX_IGNORE_API_VERSION_CACHE=${ignoreCache}`
);
// No version found in the file (we never checked before)
// so get the latest.
await useLatest();
}
this.logger.debug(`Loaded latest org-api-version ${version}`);
return version;
}
}

Expand Down
4 changes: 4 additions & 0 deletions test/unit/org/connectionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ describe('Connection', () => {
it('throws error when no valid API version', async () => {
const conn = await Connection.create({ authInfo: fromStub(testAuthInfoWithDomain) });

// @ts-ignore (resetting private property)
conn.maxApiVersion = undefined;

$$.SANDBOX.restore();
$$.SANDBOX.stub(MyDomainResolver.prototype, 'resolve').resolves(TEST_IP);
$$.SANDBOX.stub(conn, 'isResolvable').resolves(true);
Expand Down Expand Up @@ -110,6 +113,7 @@ describe('Connection', () => {
});

it('create() should create a connection with the cached API version updated with latest', async () => {
$$.SANDBOX.stub(Connection.prototype, 'isResolvable').resolves(true);
testAuthInfo.getFields.returns({
instanceApiVersionLastRetrieved: 123,
instanceApiVersion: '40.0',
Expand Down