feat(ngcc): allow async locking timeouts to be configured (#36838)

The commit adds support to the ngcc.config.js file for setting the
`retryAttempts` and `retryDelay` options for the `AsyncLocker`.

An integration test adds a new check for a timeout and actually uses the
ngcc.config.js to reduce the timeout time to prevent the test from taking
too long to complete.

PR Close #36838
This commit is contained in:
Pete Bacon Darwin
2020-04-28 15:32:05 +01:00
committed by Alex Rickabaugh
parent 98931bf9b5
commit 38f805cd06
6 changed files with 272 additions and 264 deletions

View File

@ -109,7 +109,7 @@ export function mainNgcc(options: NgccOptions): void|Promise<void> {
const createTaskCompletedCallback =
getCreateTaskCompletedCallback(pkgJsonUpdater, errorOnFailedEntryPoint, logger, fileSystem);
const executor = getExecutor(
async, inParallel, logger, fileWriter, pkgJsonUpdater, fileSystem,
async, inParallel, logger, fileWriter, pkgJsonUpdater, fileSystem, config,
createTaskCompletedCallback);
return executor.execute(analyzeEntryPoints, createCompileFn);
@ -150,12 +150,13 @@ function getCreateTaskCompletedCallback(
function getExecutor(
async: boolean, inParallel: boolean, logger: Logger, fileWriter: FileWriter,
pkgJsonUpdater: PackageJsonUpdater, fileSystem: FileSystem,
pkgJsonUpdater: PackageJsonUpdater, fileSystem: FileSystem, config: NgccConfiguration,
createTaskCompletedCallback: CreateTaskCompletedCallback): Executor {
const lockFile = new LockFileWithChildProcess(fileSystem, logger);
if (async) {
// Execute asynchronously (either serially or in parallel)
const locker = new AsyncLocker(lockFile, logger, 500, 50);
const {retryAttempts, retryDelay} = config.getLockingConfig();
const locker = new AsyncLocker(lockFile, logger, retryDelay, retryAttempts);
if (inParallel) {
// Execute in parallel. Use up to 8 CPU cores for workers, always reserving one for master.
const workerCount = Math.min(8, os.cpus().length - 1);

View File

@ -20,7 +20,27 @@ export interface NgccProjectConfig<T = NgccPackageConfig> {
/**
* The packages that are configured by this project config.
*/
packages: {[packagePath: string]: T};
packages?: {[packagePath: string]: T};
/**
* Options that control how locking the process is handled.
*/
locking?: ProcessLockingConfiguration;
}
/**
* Options that control how locking the process is handled.
*/
export interface ProcessLockingConfiguration {
/**
* The number of times the AsyncLocker will attempt to lock the process before failing.
* Defaults to 50.
*/
retryAttempts?: number;
/**
* The number of milliseconds between attempts to lock the process.
* Defaults to 500ms.
* */
retryDelay?: number;
}
/**
@ -126,12 +146,18 @@ export const DEFAULT_NGCC_CONFIG: NgccProjectConfig = {
},
},
},
locking: {
retryDelay: 500,
retryAttempts: 50,
}
};
interface VersionedPackageConfig extends NgccPackageConfig {
versionRange: string;
}
type ProcessedConfig = Required<NgccProjectConfig<VersionedPackageConfig[]>>;
const NGCC_CONFIG_FILENAME = 'ngcc.config.js';
/**
@ -159,8 +185,8 @@ const NGCC_CONFIG_FILENAME = 'ngcc.config.js';
* configuration for a package is returned.
*/
export class NgccConfiguration {
private defaultConfig: NgccProjectConfig<VersionedPackageConfig[]>;
private projectConfig: NgccProjectConfig<VersionedPackageConfig[]>;
private defaultConfig: ProcessedConfig;
private projectConfig: ProcessedConfig;
private cache = new Map<string, VersionedPackageConfig>();
readonly hash: string;
@ -170,6 +196,20 @@ export class NgccConfiguration {
this.hash = this.computeHash();
}
/**
* Get the configuration options for locking the ngcc process.
*/
getLockingConfig(): Required<ProcessLockingConfiguration> {
let {retryAttempts, retryDelay} = this.projectConfig.locking;
if (retryAttempts === undefined) {
retryAttempts = this.defaultConfig.locking.retryAttempts!;
}
if (retryDelay === undefined) {
retryDelay = this.defaultConfig.locking.retryDelay!;
}
return {retryAttempts, retryDelay};
}
/**
* Get a configuration for the given `version` of a package at `packagePath`.
*
@ -183,8 +223,9 @@ export class NgccConfiguration {
return this.cache.get(cacheKey)!;
}
const projectLevelConfig =
findSatisfactoryVersion(this.projectConfig.packages[packagePath], version);
const projectLevelConfig = this.projectConfig.packages ?
findSatisfactoryVersion(this.projectConfig.packages[packagePath], version) :
null;
if (projectLevelConfig !== null) {
this.cache.set(cacheKey, projectLevelConfig);
return projectLevelConfig;
@ -196,8 +237,9 @@ export class NgccConfiguration {
return packageLevelConfig;
}
const defaultLevelConfig =
findSatisfactoryVersion(this.defaultConfig.packages[packagePath], version);
const defaultLevelConfig = this.defaultConfig.packages ?
findSatisfactoryVersion(this.defaultConfig.packages[packagePath], version) :
null;
if (defaultLevelConfig !== null) {
this.cache.set(cacheKey, defaultLevelConfig);
return defaultLevelConfig;
@ -207,8 +249,15 @@ export class NgccConfiguration {
}
private processProjectConfig(baseDir: AbsoluteFsPath, projectConfig: NgccProjectConfig):
NgccProjectConfig<VersionedPackageConfig[]> {
const processedConfig: NgccProjectConfig<VersionedPackageConfig[]> = {packages: {}};
ProcessedConfig {
const processedConfig: ProcessedConfig = {packages: {}, locking: {}};
// locking configuration
if (projectConfig.locking !== undefined) {
processedConfig.locking = projectConfig.locking;
}
// packages configuration
for (const packagePathAndVersion in projectConfig.packages) {
const packageConfig = projectConfig.packages[packagePathAndVersion];
if (packageConfig) {
@ -220,6 +269,7 @@ export class NgccConfiguration {
{...packageConfig, versionRange, entryPoints});
}
}
return processedConfig;
}

View File

@ -10,7 +10,7 @@ import {createHash} from 'crypto';
import {absoluteFrom, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {DEFAULT_NGCC_CONFIG, NgccConfiguration} from '../../src/packages/configuration';
import {DEFAULT_NGCC_CONFIG, NgccConfiguration, ProcessLockingConfiguration} from '../../src/packages/configuration';
runInEachFileSystem(() => {
@ -51,7 +51,7 @@ runInEachFileSystem(() => {
}]);
const project1Conf = new NgccConfiguration(fs, project1);
const expectedProject1Config = `{"packages":{"${project1Package1}":[{"entryPoints":{"${
project1Package1EntryPoint1}":{}},"versionRange":"*"}]}}`;
project1Package1EntryPoint1}":{}},"versionRange":"*"}]},"locking":{}}`;
expect(project1Conf.hash)
.toEqual(createHash('md5').update(expectedProject1Config).digest('hex'));
@ -72,7 +72,7 @@ runInEachFileSystem(() => {
}]);
const project2Conf = new NgccConfiguration(fs, project2);
const expectedProject2Config = `{"packages":{"${project2Package1}":[{"entryPoints":{"${
project2Package1EntryPoint1}":{"ignore":true}},"versionRange":"*"}]}}`;
project2Package1EntryPoint1}":{"ignore":true}},"versionRange":"*"}]},"locking":{}}`;
expect(project2Conf.hash)
.toEqual(createHash('md5').update(expectedProject2Config).digest('hex'));
});
@ -80,7 +80,10 @@ runInEachFileSystem(() => {
it('should compute a hash even if there is no project configuration', () => {
loadTestFiles([{name: _Abs('/project-1/empty.js'), contents: ``}]);
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
expect(configuration.hash).toEqual('87c535c3ce0eac2a54c246892e0e21a1');
expect(configuration.hash)
.toEqual(createHash('md5')
.update(JSON.stringify({packages: {}, locking: {}}))
.digest('hex'));
});
});
@ -589,6 +592,61 @@ runInEachFileSystem(() => {
});
});
});
describe('getLockingConfig()', () => {
let originalDefaultConfig: ProcessLockingConfiguration|undefined;
beforeEach(() => {
originalDefaultConfig = DEFAULT_NGCC_CONFIG.locking;
DEFAULT_NGCC_CONFIG.locking = {retryAttempts: 17, retryDelay: 400};
});
afterEach(() => DEFAULT_NGCC_CONFIG.locking = originalDefaultConfig);
it('should return configuration for locking found in a project level file', () => {
loadTestFiles([{
name: _Abs('/project-1/ngcc.config.js'),
contents: `
module.exports = {
locking: {
retryAttempts: 4,
retryDelay: 56,
},
};`
}]);
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
const config = configuration.getLockingConfig();
expect(config).toEqual({
retryAttempts: 4,
retryDelay: 56,
});
});
it('should return configuration for locking partially found in a project level file', () => {
loadTestFiles([{
name: _Abs('/project-1/ngcc.config.js'),
contents: `
module.exports = {
locking: {
retryAttempts: 4,
},
};`
}]);
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
const config = configuration.getLockingConfig();
expect(config).toEqual({
retryAttempts: 4,
retryDelay: 400,
});
});
it('should return default configuration for locking if no project level file', () => {
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
const config = configuration.getLockingConfig();
expect(config).toEqual({
retryAttempts: 17,
retryDelay: 400,
});
});
});
});
function packageWithConfigFiles(