feat(ngcc): lock ngcc when processing (#34722)
Previously, it was possible for multiple instance of ngcc to be running at the same time, but this is not supported and can cause confusing and flakey errors at build time. Now, only one instance of ngcc can run at a time. If a second instance tries to execute it fails with an appropriate error message. See https://github.com/angular/angular/issues/32431#issuecomment-571825781 PR Close #34722
This commit is contained in:
parent
3a6cb6a5d2
commit
a107e9edc6
|
@ -13,6 +13,7 @@ import * as cluster from 'cluster';
|
||||||
import {Logger} from '../../logging/logger';
|
import {Logger} from '../../logging/logger';
|
||||||
import {PackageJsonUpdater} from '../../writing/package_json_updater';
|
import {PackageJsonUpdater} from '../../writing/package_json_updater';
|
||||||
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from '../api';
|
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from '../api';
|
||||||
|
import {LockFile} from '../lock_file';
|
||||||
|
|
||||||
import {ClusterMaster} from './master';
|
import {ClusterMaster} from './master';
|
||||||
import {ClusterWorker} from './worker';
|
import {ClusterWorker} from './worker';
|
||||||
|
@ -25,18 +26,19 @@ import {ClusterWorker} from './worker';
|
||||||
export class ClusterExecutor implements Executor {
|
export class ClusterExecutor implements Executor {
|
||||||
constructor(
|
constructor(
|
||||||
private workerCount: number, private logger: Logger,
|
private workerCount: number, private logger: Logger,
|
||||||
private pkgJsonUpdater: PackageJsonUpdater) {}
|
private pkgJsonUpdater: PackageJsonUpdater, private lockFile: LockFile) {}
|
||||||
|
|
||||||
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn):
|
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn):
|
||||||
Promise<void> {
|
Promise<void> {
|
||||||
if (cluster.isMaster) {
|
if (cluster.isMaster) {
|
||||||
|
// This process is the cluster master.
|
||||||
|
return this.lockFile.lock(() => {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Running ngcc on ${this.constructor.name} (using ${this.workerCount} worker processes).`);
|
`Running ngcc on ${this.constructor.name} (using ${this.workerCount} worker processes).`);
|
||||||
|
const master = new ClusterMaster(
|
||||||
// This process is the cluster master.
|
this.workerCount, this.logger, this.pkgJsonUpdater, analyzeEntryPoints);
|
||||||
const master =
|
|
||||||
new ClusterMaster(this.workerCount, this.logger, this.pkgJsonUpdater, analyzeEntryPoints);
|
|
||||||
return master.run();
|
return master.run();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// This process is a cluster worker.
|
// This process is a cluster worker.
|
||||||
const worker = new ClusterWorker(this.logger, createCompileFn);
|
const worker = new ClusterWorker(this.logger, createCompileFn);
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
import * as process from 'process';
|
||||||
|
import {FileSystem} from '../../../src/ngtsc/file_system';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The LockFile is used to prevent more than one instance of ngcc executing at the same time.
|
||||||
|
*
|
||||||
|
* When ngcc starts executing, it creates a file in the `compiler-cli/ngcc` folder. If it finds one
|
||||||
|
* is already there then it fails with a suitable error message.
|
||||||
|
* When ngcc completes executing, it removes the file so that future ngcc executions can start.
|
||||||
|
*/
|
||||||
|
export class LockFile {
|
||||||
|
lockFilePath =
|
||||||
|
this.fs.resolve(require.resolve('@angular/compiler-cli/ngcc'), '../__ngcc_lock_file__');
|
||||||
|
|
||||||
|
constructor(private fs: FileSystem) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a function guarded by the lock file.
|
||||||
|
*
|
||||||
|
* Note that T can be a Promise. If so, we run the `remove()` call in the promise's `finally`
|
||||||
|
* handler. Otherwise we run the `remove()` call in the `try...finally` block.
|
||||||
|
*
|
||||||
|
* @param fn The function to run.
|
||||||
|
*/
|
||||||
|
lock<T>(fn: () => T): T {
|
||||||
|
let isAsync = false;
|
||||||
|
this.create();
|
||||||
|
try {
|
||||||
|
const result = fn();
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
isAsync = true;
|
||||||
|
// The cast is necessary because TS cannot deduce that T is now a promise here.
|
||||||
|
return result.finally(() => this.remove()) as unknown as T;
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!isAsync) {
|
||||||
|
this.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a lock file to disk, or error if there is already one there.
|
||||||
|
*/
|
||||||
|
protected create() {
|
||||||
|
try {
|
||||||
|
this.addSignalHandlers();
|
||||||
|
// To avoid race conditions, we check for existence of the lockfile
|
||||||
|
// by actually trying to create it exclusively
|
||||||
|
this.fs.writeFile(this.lockFilePath, process.pid.toString(), /* exclusive */ true);
|
||||||
|
} catch (e) {
|
||||||
|
this.removeSignalHandlers();
|
||||||
|
if (e.code !== 'EEXIST') {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The lockfile already exists so raise a helpful error.
|
||||||
|
// It is feasible that the lockfile was removed between the previous check for existence
|
||||||
|
// and this file-read. If so then we still error but as gracefully as possible.
|
||||||
|
let pid: string;
|
||||||
|
try {
|
||||||
|
pid = this.fs.readFile(this.lockFilePath);
|
||||||
|
} catch {
|
||||||
|
pid = '{unknown}';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`ngcc is already running at process with id ${pid}.\n` +
|
||||||
|
`(If you are sure no ngcc process is running then you should delete the lockfile at ${this.lockFilePath}.)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the lock file from disk.
|
||||||
|
*/
|
||||||
|
protected remove() {
|
||||||
|
this.removeSignalHandlers();
|
||||||
|
if (this.fs.exists(this.lockFilePath)) {
|
||||||
|
this.fs.removeFile(this.lockFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addSignalHandlers() {
|
||||||
|
process.once('SIGINT', this.signalHandler);
|
||||||
|
process.once('SIGHUP', this.signalHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected removeSignalHandlers() {
|
||||||
|
process.removeListener('SIGINT', this.signalHandler);
|
||||||
|
process.removeListener('SIGHUP', this.signalHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This handle needs to be defined as a property rather than a method
|
||||||
|
* so that it can be passed around as a bound function.
|
||||||
|
*/
|
||||||
|
protected signalHandler =
|
||||||
|
() => {
|
||||||
|
this.remove();
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function wraps `process.exit()` which makes it easier to manage in unit tests,
|
||||||
|
* since it is not possible to mock out `process.exit()` when it is called from signal handlers.
|
||||||
|
*/
|
||||||
|
protected exit(code: number): void {
|
||||||
|
process.exit(code);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import {Logger} from '../logging/logger';
|
||||||
import {PackageJsonUpdater} from '../writing/package_json_updater';
|
import {PackageJsonUpdater} from '../writing/package_json_updater';
|
||||||
|
|
||||||
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from './api';
|
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from './api';
|
||||||
|
import {LockFile} from './lock_file';
|
||||||
import {onTaskCompleted} from './utils';
|
import {onTaskCompleted} from './utils';
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,9 +18,12 @@ import {onTaskCompleted} from './utils';
|
||||||
* An `Executor` that processes all tasks serially and completes synchronously.
|
* An `Executor` that processes all tasks serially and completes synchronously.
|
||||||
*/
|
*/
|
||||||
export class SingleProcessExecutor implements Executor {
|
export class SingleProcessExecutor implements Executor {
|
||||||
constructor(private logger: Logger, private pkgJsonUpdater: PackageJsonUpdater) {}
|
constructor(
|
||||||
|
private logger: Logger, private pkgJsonUpdater: PackageJsonUpdater,
|
||||||
|
private lockFile: LockFile) {}
|
||||||
|
|
||||||
execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn): void {
|
execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn): void {
|
||||||
|
this.lockFile.lock(() => {
|
||||||
this.logger.debug(`Running ngcc on ${this.constructor.name}.`);
|
this.logger.debug(`Running ngcc on ${this.constructor.name}.`);
|
||||||
|
|
||||||
const taskQueue = analyzeEntryPoints();
|
const taskQueue = analyzeEntryPoints();
|
||||||
|
@ -38,6 +42,7 @@ export class SingleProcessExecutor implements Executor {
|
||||||
|
|
||||||
const duration = Math.round((Date.now() - startTime) / 1000);
|
const duration = Math.round((Date.now() - startTime) / 1000);
|
||||||
this.logger.debug(`Processed tasks in ${duration}s.`);
|
this.logger.debug(`Processed tasks in ${duration}s.`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {TargetedEntryPointFinder} from './entry_point_finder/targeted_entry_poin
|
||||||
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor, PartiallyOrderedTasks, Task, TaskProcessingOutcome, TaskQueue} from './execution/api';
|
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor, PartiallyOrderedTasks, Task, TaskProcessingOutcome, TaskQueue} from './execution/api';
|
||||||
import {ClusterExecutor} from './execution/cluster/executor';
|
import {ClusterExecutor} from './execution/cluster/executor';
|
||||||
import {ClusterPackageJsonUpdater} from './execution/cluster/package_json_updater';
|
import {ClusterPackageJsonUpdater} from './execution/cluster/package_json_updater';
|
||||||
|
import {LockFile} from './execution/lock_file';
|
||||||
import {AsyncSingleProcessExecutor, SingleProcessExecutor} from './execution/single_process_executor';
|
import {AsyncSingleProcessExecutor, SingleProcessExecutor} from './execution/single_process_executor';
|
||||||
import {ParallelTaskQueue} from './execution/task_selection/parallel_task_queue';
|
import {ParallelTaskQueue} from './execution/task_selection/parallel_task_queue';
|
||||||
import {SerialTaskQueue} from './execution/task_selection/serial_task_queue';
|
import {SerialTaskQueue} from './execution/task_selection/serial_task_queue';
|
||||||
|
@ -285,7 +286,7 @@ export function mainNgcc(
|
||||||
};
|
};
|
||||||
|
|
||||||
// The executor for actually planning and getting the work done.
|
// The executor for actually planning and getting the work done.
|
||||||
const executor = getExecutor(async, inParallel, logger, pkgJsonUpdater);
|
const executor = getExecutor(async, inParallel, logger, pkgJsonUpdater, new LockFile(fileSystem));
|
||||||
|
|
||||||
return executor.execute(analyzeEntryPoints, createCompileFn);
|
return executor.execute(analyzeEntryPoints, createCompileFn);
|
||||||
}
|
}
|
||||||
|
@ -330,17 +331,17 @@ function getTaskQueue(
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExecutor(
|
function getExecutor(
|
||||||
async: boolean, inParallel: boolean, logger: Logger,
|
async: boolean, inParallel: boolean, logger: Logger, pkgJsonUpdater: PackageJsonUpdater,
|
||||||
pkgJsonUpdater: PackageJsonUpdater): Executor {
|
lockFile: LockFile): Executor {
|
||||||
if (inParallel) {
|
if (inParallel) {
|
||||||
// Execute in parallel (which implies async).
|
// Execute in parallel (which implies async).
|
||||||
// Use up to 8 CPU cores for workers, always reserving one for master.
|
// Use up to 8 CPU cores for workers, always reserving one for master.
|
||||||
const workerCount = Math.min(8, os.cpus().length - 1);
|
const workerCount = Math.min(8, os.cpus().length - 1);
|
||||||
return new ClusterExecutor(workerCount, logger, pkgJsonUpdater);
|
return new ClusterExecutor(workerCount, logger, pkgJsonUpdater, lockFile);
|
||||||
} else {
|
} else {
|
||||||
// Execute serially, on a single thread (either sync or async).
|
// Execute serially, on a single thread (either sync or async).
|
||||||
return async ? new AsyncSingleProcessExecutor(logger, pkgJsonUpdater) :
|
return async ? new AsyncSingleProcessExecutor(logger, pkgJsonUpdater, lockFile) :
|
||||||
new SingleProcessExecutor(logger, pkgJsonUpdater);
|
new SingleProcessExecutor(logger, pkgJsonUpdater, lockFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {ClusterExecutor} from '../../../src/execution/cluster/executor';
|
||||||
import {ClusterMaster} from '../../../src/execution/cluster/master';
|
import {ClusterMaster} from '../../../src/execution/cluster/master';
|
||||||
import {ClusterWorker} from '../../../src/execution/cluster/worker';
|
import {ClusterWorker} from '../../../src/execution/cluster/worker';
|
||||||
import {PackageJsonUpdater} from '../../../src/writing/package_json_updater';
|
import {PackageJsonUpdater} from '../../../src/writing/package_json_updater';
|
||||||
|
import {MockLockFile} from '../../helpers/mock_lock_file';
|
||||||
import {MockLogger} from '../../helpers/mock_logger';
|
import {MockLogger} from '../../helpers/mock_logger';
|
||||||
import {mockProperty} from '../../helpers/spy_utils';
|
import {mockProperty} from '../../helpers/spy_utils';
|
||||||
|
|
||||||
|
@ -23,14 +24,19 @@ describe('ClusterExecutor', () => {
|
||||||
let masterRunSpy: jasmine.Spy;
|
let masterRunSpy: jasmine.Spy;
|
||||||
let workerRunSpy: jasmine.Spy;
|
let workerRunSpy: jasmine.Spy;
|
||||||
let mockLogger: MockLogger;
|
let mockLogger: MockLogger;
|
||||||
|
let mockLockFile: MockLockFile;
|
||||||
let executor: ClusterExecutor;
|
let executor: ClusterExecutor;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
masterRunSpy = spyOn(ClusterMaster.prototype, 'run');
|
masterRunSpy = spyOn(ClusterMaster.prototype, 'run')
|
||||||
workerRunSpy = spyOn(ClusterWorker.prototype, 'run');
|
.and.returnValue(Promise.resolve('CusterMaster#run()'));
|
||||||
|
workerRunSpy = spyOn(ClusterWorker.prototype, 'run')
|
||||||
|
.and.returnValue(Promise.resolve('CusterWorker#run()'));
|
||||||
|
|
||||||
mockLogger = new MockLogger();
|
mockLogger = new MockLogger();
|
||||||
executor = new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater);
|
mockLockFile = new MockLockFile();
|
||||||
|
executor =
|
||||||
|
new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, mockLockFile);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('execute()', () => {
|
describe('execute()', () => {
|
||||||
|
@ -47,7 +53,6 @@ describe('ClusterExecutor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate to `ClusterMaster#run()`', async() => {
|
it('should delegate to `ClusterMaster#run()`', async() => {
|
||||||
masterRunSpy.and.returnValue('CusterMaster#run()');
|
|
||||||
const analyzeEntryPointsSpy = jasmine.createSpy('analyzeEntryPoints');
|
const analyzeEntryPointsSpy = jasmine.createSpy('analyzeEntryPoints');
|
||||||
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
|
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
|
||||||
|
|
||||||
|
@ -60,6 +65,58 @@ describe('ClusterExecutor', () => {
|
||||||
expect(analyzeEntryPointsSpy).toHaveBeenCalledWith();
|
expect(analyzeEntryPointsSpy).toHaveBeenCalledWith();
|
||||||
expect(createCompilerFnSpy).not.toHaveBeenCalled();
|
expect(createCompilerFnSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should call LockFile.create() and LockFile.remove() if master runner completes successfully',
|
||||||
|
async() => {
|
||||||
|
const anyFn: () => any = () => undefined;
|
||||||
|
await executor.execute(anyFn, anyFn);
|
||||||
|
expect(mockLockFile.log).toEqual(['create()', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call LockFile.create() and LockFile.remove() if master runner fails', async() => {
|
||||||
|
const anyFn: () => any = () => undefined;
|
||||||
|
masterRunSpy.and.returnValue(Promise.reject(new Error('master runner error')));
|
||||||
|
let error = '';
|
||||||
|
try {
|
||||||
|
await executor.execute(anyFn, anyFn);
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
expect(error).toEqual('master runner error');
|
||||||
|
expect(mockLockFile.log).toEqual(['create()', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call master runner if Lockfile.create() fails', async() => {
|
||||||
|
const anyFn: () => any = () => undefined;
|
||||||
|
const lockFile = new MockLockFile({throwOnCreate: true});
|
||||||
|
executor =
|
||||||
|
new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, lockFile);
|
||||||
|
let error = '';
|
||||||
|
try {
|
||||||
|
await executor.execute(anyFn, anyFn);
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
expect(error).toEqual('LockFile.create() error');
|
||||||
|
expect(lockFile.log).toEqual(['create()']);
|
||||||
|
expect(masterRunSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if Lockfile.remove() fails', async() => {
|
||||||
|
const anyFn: () => any = () => undefined;
|
||||||
|
const lockFile = new MockLockFile({throwOnRemove: true});
|
||||||
|
executor =
|
||||||
|
new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, lockFile);
|
||||||
|
let error = '';
|
||||||
|
try {
|
||||||
|
await executor.execute(anyFn, anyFn);
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
expect(error).toEqual('LockFile.remove() error');
|
||||||
|
expect(lockFile.log).toEqual(['create()', 'remove()']);
|
||||||
|
expect(masterRunSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('(on cluster worker)', () => {
|
describe('(on cluster worker)', () => {
|
||||||
|
@ -73,7 +130,6 @@ describe('ClusterExecutor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate to `ClusterWorker#run()`', async() => {
|
it('should delegate to `ClusterWorker#run()`', async() => {
|
||||||
workerRunSpy.and.returnValue('CusterWorker#run()');
|
|
||||||
const analyzeEntryPointsSpy = jasmine.createSpy('analyzeEntryPoints');
|
const analyzeEntryPointsSpy = jasmine.createSpy('analyzeEntryPoints');
|
||||||
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
|
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
|
||||||
|
|
||||||
|
@ -86,6 +142,12 @@ describe('ClusterExecutor', () => {
|
||||||
expect(analyzeEntryPointsSpy).not.toHaveBeenCalled();
|
expect(analyzeEntryPointsSpy).not.toHaveBeenCalled();
|
||||||
expect(createCompilerFnSpy).toHaveBeenCalledWith(jasmine.any(Function));
|
expect(createCompilerFnSpy).toHaveBeenCalledWith(jasmine.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not call LockFile.create() or LockFile.remove()', async() => {
|
||||||
|
const anyFn: () => any = () => undefined;
|
||||||
|
await executor.execute(anyFn, anyFn);
|
||||||
|
expect(mockLockFile.log).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,206 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
import * as process from 'process';
|
||||||
|
import {FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
|
||||||
|
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||||
|
import {LockFile} from '../../src/execution/lock_file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class allows us to test the protected methods of LockFile directly,
|
||||||
|
* which are normally hidden as "protected".
|
||||||
|
*
|
||||||
|
* We also add logging in here to track what is being called and in what order.
|
||||||
|
*
|
||||||
|
* Finally this class stubs out the `exit()` method to prevent unit tests from exiting the process.
|
||||||
|
*/
|
||||||
|
class LockFileUnderTest extends LockFile {
|
||||||
|
log: string[] = [];
|
||||||
|
constructor(fs: FileSystem, private handleSignals = false) {
|
||||||
|
super(fs);
|
||||||
|
fs.ensureDir(fs.dirname(this.lockFilePath));
|
||||||
|
}
|
||||||
|
create() {
|
||||||
|
this.log.push('create()');
|
||||||
|
super.create();
|
||||||
|
}
|
||||||
|
remove() {
|
||||||
|
this.log.push('remove()');
|
||||||
|
super.remove();
|
||||||
|
}
|
||||||
|
addSignalHandlers() {
|
||||||
|
if (this.handleSignals) {
|
||||||
|
super.addSignalHandlers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeSignalHandlers() { super.removeSignalHandlers(); }
|
||||||
|
exit(code: number) { this.log.push(`exit(${code})`); }
|
||||||
|
}
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
|
describe('LockFile', () => {
|
||||||
|
describe('lock() - synchronous', () => {
|
||||||
|
it('should guard the `fn()` with calls to `create()` and `remove()`', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
|
||||||
|
lockFile.lock(() => lockFile.log.push('fn()'));
|
||||||
|
expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should guard the `fn()` with calls to `create()` and `remove()`, even if it throws',
|
||||||
|
() => {
|
||||||
|
let error: string = '';
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
|
||||||
|
try {
|
||||||
|
lockFile.lock(() => {
|
||||||
|
lockFile.log.push('fn()');
|
||||||
|
throw new Error('ERROR');
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
expect(error).toEqual('ERROR');
|
||||||
|
expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the lockfile if CTRL-C is triggered', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
|
||||||
|
|
||||||
|
lockFile.lock(() => {
|
||||||
|
lockFile.log.push('SIGINT');
|
||||||
|
process.emit('SIGINT', 'SIGINT');
|
||||||
|
});
|
||||||
|
// Since the test does not actually exit process, the `remove()` is called one more time.
|
||||||
|
expect(lockFile.log).toEqual(['create()', 'SIGINT', 'remove()', 'exit(1)', 'remove()']);
|
||||||
|
// Clean up the signal handlers. In practice this is not needed since the process would have
|
||||||
|
// been terminated already.
|
||||||
|
lockFile.removeSignalHandlers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the lockfile if terminal is closed', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
|
||||||
|
|
||||||
|
lockFile.lock(() => {
|
||||||
|
lockFile.log.push('SIGHUP');
|
||||||
|
process.emit('SIGHUP', 'SIGHUP');
|
||||||
|
});
|
||||||
|
// Since this does not actually exit process, the `remove()` is called one more time.
|
||||||
|
expect(lockFile.log).toEqual(['create()', 'SIGHUP', 'remove()', 'exit(1)', 'remove()']);
|
||||||
|
// Clean up the signal handlers. In practice this is not needed since the process would have
|
||||||
|
// been terminated already.
|
||||||
|
lockFile.removeSignalHandlers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('lock() - asynchronous', () => {
|
||||||
|
it('should guard the `fn()` with calls to `create()` and `remove()`', async() => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
|
||||||
|
await lockFile.lock(async() => {
|
||||||
|
lockFile.log.push('fn() - before');
|
||||||
|
// This promise forces node to do a tick in this function, ensuring that we are truly
|
||||||
|
// testing an async scenario.
|
||||||
|
await Promise.resolve();
|
||||||
|
lockFile.log.push('fn() - after');
|
||||||
|
});
|
||||||
|
expect(lockFile.log).toEqual(['create()', 'fn() - before', 'fn() - after', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should guard the `fn()` with calls to `create()` and `remove()`, even if it throws',
|
||||||
|
async() => {
|
||||||
|
let error: string = '';
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
lockFile.create = () => lockFile.log.push('create()');
|
||||||
|
lockFile.remove = () => lockFile.log.push('remove()');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await lockFile.lock(async() => {
|
||||||
|
lockFile.log.push('fn()');
|
||||||
|
throw new Error('ERROR');
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
expect(error).toEqual('ERROR');
|
||||||
|
expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the lockfile if CTRL-C is triggered', async() => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
|
||||||
|
|
||||||
|
await lockFile.lock(async() => {
|
||||||
|
lockFile.log.push('SIGINT');
|
||||||
|
process.emit('SIGINT', 'SIGINT');
|
||||||
|
});
|
||||||
|
// Since the test does not actually exit process, the `remove()` is called one more time.
|
||||||
|
expect(lockFile.log).toEqual(['create()', 'SIGINT', 'remove()', 'exit(1)', 'remove()']);
|
||||||
|
// Clean up the signal handlers. In practice this is not needed since the process would have
|
||||||
|
// been terminated already.
|
||||||
|
lockFile.removeSignalHandlers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the lockfile if terminal is closed', async() => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
|
||||||
|
|
||||||
|
await lockFile.lock(async() => {
|
||||||
|
lockFile.log.push('SIGHUP');
|
||||||
|
process.emit('SIGHUP', 'SIGHUP');
|
||||||
|
});
|
||||||
|
// Since this does not actually exit process, the `remove()` is called one more time.
|
||||||
|
expect(lockFile.log).toEqual(['create()', 'SIGHUP', 'remove()', 'exit(1)', 'remove()']);
|
||||||
|
// Clean up the signal handlers. In practice this is not needed since the process would have
|
||||||
|
// been terminated already.
|
||||||
|
lockFile.removeSignalHandlers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create()', () => {
|
||||||
|
it('should write a lock file to the file-system', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
expect(fs.exists(lockFile.lockFilePath)).toBe(false);
|
||||||
|
lockFile.create();
|
||||||
|
expect(fs.exists(lockFile.lockFilePath)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if a lock file already exists', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
fs.writeFile(lockFile.lockFilePath, '188');
|
||||||
|
expect(() => lockFile.create())
|
||||||
|
.toThrowError(
|
||||||
|
`ngcc is already running at process with id 188.\n` +
|
||||||
|
`(If you are sure no ngcc process is running then you should delete the lockfile at ${lockFile.lockFilePath}.)`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('remove()', () => {
|
||||||
|
it('should remove the lock file from the file-system', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
fs.writeFile(lockFile.lockFilePath, '188');
|
||||||
|
lockFile.remove();
|
||||||
|
expect(fs.exists(lockFile.lockFilePath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not error if the lock file does not exist', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
expect(() => lockFile.remove()).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,97 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="node" />
|
||||||
|
|
||||||
|
import {SingleProcessExecutor} from '../../src/execution/single_process_executor';
|
||||||
|
import {SerialTaskQueue} from '../../src/execution/task_selection/serial_task_queue';
|
||||||
|
import {PackageJsonUpdater} from '../../src/writing/package_json_updater';
|
||||||
|
import {MockLockFile} from '../helpers/mock_lock_file';
|
||||||
|
import {MockLogger} from '../helpers/mock_logger';
|
||||||
|
|
||||||
|
|
||||||
|
describe('SingleProcessExecutor', () => {
|
||||||
|
let mockLogger: MockLogger;
|
||||||
|
let mockLockFile: MockLockFile;
|
||||||
|
let executor: SingleProcessExecutor;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockLogger = new MockLogger();
|
||||||
|
mockLockFile = new MockLockFile();
|
||||||
|
executor =
|
||||||
|
new SingleProcessExecutor(mockLogger, null as unknown as PackageJsonUpdater, mockLockFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('execute()', () => {
|
||||||
|
it('should call LockFile.create() and LockFile.remove() if processing completes successfully',
|
||||||
|
() => {
|
||||||
|
const noTasks = () => new SerialTaskQueue([] as any);
|
||||||
|
const createCompileFn: () => any = () => undefined;
|
||||||
|
executor.execute(noTasks, createCompileFn);
|
||||||
|
expect(mockLockFile.log).toEqual(['create()', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call LockFile.create() and LockFile.remove() if `analyzeEntryPoints` fails', () => {
|
||||||
|
const errorFn: () => never = () => { throw new Error('analyze error'); };
|
||||||
|
const createCompileFn: () => any = () => undefined;
|
||||||
|
let error: string = '';
|
||||||
|
try {
|
||||||
|
executor.execute(errorFn, createCompileFn);
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
expect(error).toEqual('analyze error');
|
||||||
|
expect(mockLockFile.log).toEqual(['create()', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call LockFile.create() and LockFile.remove() if `createCompileFn` fails', () => {
|
||||||
|
const oneTask = () => new SerialTaskQueue([{}] as any);
|
||||||
|
const createErrorCompileFn: () => any = () => { throw new Error('compile error'); };
|
||||||
|
let error: string = '';
|
||||||
|
try {
|
||||||
|
executor.execute(oneTask, createErrorCompileFn);
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
expect(error).toEqual('compile error');
|
||||||
|
expect(mockLockFile.log).toEqual(['create()', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call `analyzeEntryPoints` if Lockfile.create() fails', () => {
|
||||||
|
const lockFile = new MockLockFile({throwOnCreate: true});
|
||||||
|
const analyzeFn: () => any = () => { lockFile.log.push('analyzeFn'); };
|
||||||
|
const anyFn: () => any = () => undefined;
|
||||||
|
executor =
|
||||||
|
new SingleProcessExecutor(mockLogger, null as unknown as PackageJsonUpdater, lockFile);
|
||||||
|
let error = '';
|
||||||
|
try {
|
||||||
|
executor.execute(analyzeFn, anyFn);
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
expect(error).toEqual('LockFile.create() error');
|
||||||
|
expect(lockFile.log).toEqual(['create()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if Lockfile.remove() fails', () => {
|
||||||
|
const noTasks = () => new SerialTaskQueue([] as any);
|
||||||
|
const anyFn: () => any = () => undefined;
|
||||||
|
const lockFile = new MockLockFile({throwOnRemove: true});
|
||||||
|
executor =
|
||||||
|
new SingleProcessExecutor(mockLogger, null as unknown as PackageJsonUpdater, lockFile);
|
||||||
|
let error = '';
|
||||||
|
try {
|
||||||
|
executor.execute(noTasks, anyFn);
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
expect(error).toEqual('LockFile.remove() error');
|
||||||
|
expect(lockFile.log).toEqual(['create()', 'remove()']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
import {MockFileSystemNative} from '../../../src/ngtsc/file_system/testing';
|
||||||
|
import {LockFile} from '../../src/execution/lock_file';
|
||||||
|
|
||||||
|
export class MockLockFile extends LockFile {
|
||||||
|
log: string[] = [];
|
||||||
|
constructor(private options: {throwOnCreate?: boolean, throwOnRemove?: boolean} = {}) {
|
||||||
|
// This `MockLockFile` is not used in tests that are run via `runInEachFileSystem()`
|
||||||
|
// So we cannot use `getFileSystem()` but instead just instantiate a mock file-system.
|
||||||
|
super(new MockFileSystemNative());
|
||||||
|
}
|
||||||
|
create() {
|
||||||
|
this.log.push('create()');
|
||||||
|
if (this.options.throwOnCreate) throw new Error('LockFile.create() error');
|
||||||
|
}
|
||||||
|
remove() {
|
||||||
|
this.log.push('remove()');
|
||||||
|
if (this.options.throwOnRemove) throw new Error('LockFile.remove() error');
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import * as os from 'os';
|
||||||
import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem, join} from '../../../src/ngtsc/file_system';
|
import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem, join} from '../../../src/ngtsc/file_system';
|
||||||
import {Folder, MockFileSystem, TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
import {Folder, MockFileSystem, TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||||
import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers';
|
import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers';
|
||||||
|
import {LockFile} from '../../src/execution/lock_file';
|
||||||
import {mainNgcc} from '../../src/main';
|
import {mainNgcc} from '../../src/main';
|
||||||
import {markAsProcessed} from '../../src/packages/build_marker';
|
import {markAsProcessed} from '../../src/packages/build_marker';
|
||||||
import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point';
|
import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point';
|
||||||
|
@ -1319,6 +1320,7 @@ runInEachFileSystem(() => {
|
||||||
function initMockFileSystem(fs: FileSystem, testFiles: Folder) {
|
function initMockFileSystem(fs: FileSystem, testFiles: Folder) {
|
||||||
if (fs instanceof MockFileSystem) {
|
if (fs instanceof MockFileSystem) {
|
||||||
fs.init(testFiles);
|
fs.init(testFiles);
|
||||||
|
fs.ensureDir(fs.dirname(new LockFile(fs).lockFilePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
// a random test package that no metadata.json file so not compiled by Angular.
|
// a random test package that no metadata.json file so not compiled by Angular.
|
||||||
|
|
Loading…
Reference in New Issue