refactor(ngcc): move locking code into its own folder (#35861)
PR Close #35861
This commit is contained in:
parent
94fa140888
commit
4acd658635
|
@ -10,10 +10,10 @@
|
||||||
|
|
||||||
import * as cluster from 'cluster';
|
import * as cluster from 'cluster';
|
||||||
|
|
||||||
|
import {AsyncLocker} from '../../locking/async_locker';
|
||||||
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 {AsyncLocker} from '../lock_file';
|
|
||||||
|
|
||||||
import {ClusterMaster} from './master';
|
import {ClusterMaster} from './master';
|
||||||
import {ClusterWorker} from './worker';
|
import {ClusterWorker} from './worker';
|
||||||
|
|
|
@ -1,225 +0,0 @@
|
||||||
/**
|
|
||||||
* @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 {AbsoluteFsPath, CachedFileSystem, FileSystem} from '../../../src/ngtsc/file_system';
|
|
||||||
import {Logger} from '../logging/logger';
|
|
||||||
|
|
||||||
let _lockFilePath: AbsoluteFsPath;
|
|
||||||
export function getLockFilePath(fs: FileSystem) {
|
|
||||||
if (!_lockFilePath) {
|
|
||||||
_lockFilePath =
|
|
||||||
fs.resolve(require.resolve('@angular/compiler-cli/ngcc'), '../__ngcc_lock_file__');
|
|
||||||
}
|
|
||||||
return _lockFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LockFile {
|
|
||||||
path: AbsoluteFsPath;
|
|
||||||
/**
|
|
||||||
* Write a lock file to disk containing the PID of the current process.
|
|
||||||
*/
|
|
||||||
write(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the PID, of the process holding the lock, from the lockFile.
|
|
||||||
*
|
|
||||||
* It is feasible that the lockFile was removed between the call to `write()` that effectively
|
|
||||||
* checks for existence and this attempt to read the file. If so then this method should just
|
|
||||||
* gracefully return `"{unknown}"`.
|
|
||||||
*/
|
|
||||||
read(): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the lock file from disk, whether or not it exists.
|
|
||||||
*/
|
|
||||||
remove(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LockFileWithSignalHandlers implements LockFile {
|
|
||||||
constructor(protected fs: FileSystem) {}
|
|
||||||
|
|
||||||
path = getLockFilePath(this.fs);
|
|
||||||
|
|
||||||
write(): void {
|
|
||||||
try {
|
|
||||||
this.addSignalHandlers();
|
|
||||||
// To avoid race conditions, we check for existence of the lockFile by actually trying to
|
|
||||||
// create it exclusively.
|
|
||||||
return this.fs.writeFile(this.path, process.pid.toString(), /* exclusive */ true);
|
|
||||||
} catch (e) {
|
|
||||||
this.removeSignalHandlers();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read(): string {
|
|
||||||
try {
|
|
||||||
if (this.fs instanceof CachedFileSystem) {
|
|
||||||
// This file is "volatile", it might be changed by an external process,
|
|
||||||
// so we cannot rely upon the cached value when reading it.
|
|
||||||
this.fs.invalidateCaches(this.path);
|
|
||||||
}
|
|
||||||
return this.fs.readFile(this.path);
|
|
||||||
} catch {
|
|
||||||
return '{unknown}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
this.removeSignalHandlers();
|
|
||||||
if (this.fs.exists(this.path)) {
|
|
||||||
this.fs.removeFile(this.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Capture CTRL-C and terminal closing events.
|
|
||||||
* When these occur we remove the lockFile and exit.
|
|
||||||
*/
|
|
||||||
protected addSignalHandlers() {
|
|
||||||
process.addListener('SIGINT', this.signalHandler);
|
|
||||||
process.addListener('SIGHUP', this.signalHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the event handlers to prevent leakage.
|
|
||||||
*/
|
|
||||||
protected removeSignalHandlers() {
|
|
||||||
process.removeListener('SIGINT', this.signalHandler);
|
|
||||||
process.removeListener('SIGHUP', this.signalHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This handler 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SyncLocker is used to prevent more than one instance of ngcc executing at the same time,
|
|
||||||
* when being called in a synchronous context.
|
|
||||||
*
|
|
||||||
* * 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 SyncLocker {
|
|
||||||
constructor(private lockFile: LockFile) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the given function guarded by the lock file.
|
|
||||||
*
|
|
||||||
* @param fn the function to run.
|
|
||||||
* @returns the value returned from the `fn` call.
|
|
||||||
*/
|
|
||||||
lock<T>(fn: () => T): T {
|
|
||||||
this.create();
|
|
||||||
try {
|
|
||||||
return fn();
|
|
||||||
} finally {
|
|
||||||
this.lockFile.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write a lock file to disk, or error if there is already one there.
|
|
||||||
*/
|
|
||||||
protected create(): void {
|
|
||||||
try {
|
|
||||||
this.lockFile.write();
|
|
||||||
} catch (e) {
|
|
||||||
if (e.code !== 'EEXIST') {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
this.handleExistingLockFile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The lockFile already exists so raise a helpful error.
|
|
||||||
*/
|
|
||||||
protected handleExistingLockFile(): void {
|
|
||||||
const pid = this.lockFile.read();
|
|
||||||
throw new Error(
|
|
||||||
`ngcc is already running at process with id ${pid}.\n` +
|
|
||||||
`If you are running multiple builds in parallel then you should pre-process your node_modules via the command line ngcc tool before starting the builds;\n` +
|
|
||||||
`See https://v9.angular.io/guide/ivy#speeding-up-ngcc-compilation.\n` +
|
|
||||||
`(If you are sure no ngcc process is running then you should delete the lockFile at ${this.lockFile.path}.)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AsyncLocker is used to prevent more than one instance of ngcc executing at the same time,
|
|
||||||
* when being called in an asynchronous context.
|
|
||||||
*
|
|
||||||
* * When ngcc starts executing, it creates a file in the `compiler-cli/ngcc` folder.
|
|
||||||
* * If it finds one is already there then it pauses and waits for the file to be removed by the
|
|
||||||
* other process. If the file is not removed within a set timeout period given by
|
|
||||||
* `retryDelay*retryAttempts` an error is thrown with a suitable error message.
|
|
||||||
* * If the process locking the file changes, then we restart the timeout.
|
|
||||||
* * When ngcc completes executing, it removes the file so that future ngcc executions can start.
|
|
||||||
*/
|
|
||||||
export class AsyncLocker {
|
|
||||||
constructor(
|
|
||||||
private lockFile: LockFile, protected logger: Logger, private retryDelay: number,
|
|
||||||
private retryAttempts: number) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a function guarded by the lock file.
|
|
||||||
*
|
|
||||||
* @param fn The function to run.
|
|
||||||
*/
|
|
||||||
async lock<T>(fn: () => Promise<T>): Promise<T> {
|
|
||||||
await this.create();
|
|
||||||
return fn().finally(() => this.lockFile.remove());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async create() {
|
|
||||||
let pid: string = '';
|
|
||||||
for (let attempts = 0; attempts < this.retryAttempts; attempts++) {
|
|
||||||
try {
|
|
||||||
return this.lockFile.write();
|
|
||||||
} catch (e) {
|
|
||||||
if (e.code !== 'EEXIST') {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
const newPid = this.lockFile.read();
|
|
||||||
if (newPid !== pid) {
|
|
||||||
// The process locking the file has changed, so restart the timeout
|
|
||||||
attempts = 0;
|
|
||||||
pid = newPid;
|
|
||||||
}
|
|
||||||
if (attempts === 0) {
|
|
||||||
this.logger.info(
|
|
||||||
`Another process, with id ${pid}, is currently running ngcc.\n` +
|
|
||||||
`Waiting up to ${this.retryDelay*this.retryAttempts/1000}s for it to finish.`);
|
|
||||||
}
|
|
||||||
// The file is still locked by another process so wait for a bit and retry
|
|
||||||
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If we fall out of the loop then we ran out of rety attempts
|
|
||||||
throw new Error(
|
|
||||||
`Timed out waiting ${this.retryAttempts * this.retryDelay/1000}s for another ngcc process, with id ${pid}, to complete.\n` +
|
|
||||||
`(If you are sure no ngcc process is running then you should delete the lockFile at ${this.lockFile.path}.)`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,11 +6,12 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {AsyncLocker} from '../locking/async_locker';
|
||||||
|
import {SyncLocker} from '../locking/sync_locker';
|
||||||
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 {AsyncLocker, SyncLocker} from './lock_file';
|
|
||||||
import {onTaskCompleted} from './utils';
|
import {onTaskCompleted} from './utils';
|
||||||
|
|
||||||
export abstract class SingleProcessorExecutorBase {
|
export abstract class SingleProcessorExecutorBase {
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* @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 {Logger} from '../logging/logger';
|
||||||
|
import {LockFile} from './lock_file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AsyncLocker is used to prevent more than one instance of ngcc executing at the same time,
|
||||||
|
* when being called in an asynchronous context.
|
||||||
|
*
|
||||||
|
* * When ngcc starts executing, it creates a file in the `compiler-cli/ngcc` folder.
|
||||||
|
* * If it finds one is already there then it pauses and waits for the file to be removed by the
|
||||||
|
* other process. If the file is not removed within a set timeout period given by
|
||||||
|
* `retryDelay*retryAttempts` an error is thrown with a suitable error message.
|
||||||
|
* * If the process locking the file changes, then we restart the timeout.
|
||||||
|
* * When ngcc completes executing, it removes the file so that future ngcc executions can start.
|
||||||
|
*/
|
||||||
|
export class AsyncLocker {
|
||||||
|
constructor(
|
||||||
|
private lockFile: LockFile, protected logger: Logger, private retryDelay: number,
|
||||||
|
private retryAttempts: number) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a function guarded by the lock file.
|
||||||
|
*
|
||||||
|
* @param fn The function to run.
|
||||||
|
*/
|
||||||
|
async lock<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
await this.create();
|
||||||
|
return fn().finally(() => this.lockFile.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async create() {
|
||||||
|
let pid: string = '';
|
||||||
|
for (let attempts = 0; attempts < this.retryAttempts; attempts++) {
|
||||||
|
try {
|
||||||
|
return this.lockFile.write();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code !== 'EEXIST') {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
const newPid = this.lockFile.read();
|
||||||
|
if (newPid !== pid) {
|
||||||
|
// The process locking the file has changed, so restart the timeout
|
||||||
|
attempts = 0;
|
||||||
|
pid = newPid;
|
||||||
|
}
|
||||||
|
if (attempts === 0) {
|
||||||
|
this.logger.info(
|
||||||
|
`Another process, with id ${pid}, is currently running ngcc.\n` +
|
||||||
|
`Waiting up to ${this.retryDelay*this.retryAttempts/1000}s for it to finish.`);
|
||||||
|
}
|
||||||
|
// The file is still locked by another process so wait for a bit and retry
|
||||||
|
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we fall out of the loop then we ran out of rety attempts
|
||||||
|
throw new Error(
|
||||||
|
`Timed out waiting ${this.retryAttempts * this.retryDelay/1000}s for another ngcc process, with id ${pid}, to complete.\n` +
|
||||||
|
`(If you are sure no ngcc process is running then you should delete the lock-file at ${this.lockFile.path}.)`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* @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 {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
|
||||||
|
|
||||||
|
export function getLockFilePath(fs: FileSystem) {
|
||||||
|
return fs.resolve(require.resolve('@angular/compiler-cli/ngcc'), '../__ngcc_lock_file__');
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LockFile {
|
||||||
|
path: AbsoluteFsPath;
|
||||||
|
/**
|
||||||
|
* Write a lock file to disk containing the PID of the current process.
|
||||||
|
*/
|
||||||
|
write(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the PID, of the process holding the lock, from the lock-file.
|
||||||
|
*
|
||||||
|
* It is feasible that the lock-file was removed between the call to `write()` that effectively
|
||||||
|
* checks for existence and this attempt to read the file. If so then this method should just
|
||||||
|
* gracefully return `"{unknown}"`.
|
||||||
|
*/
|
||||||
|
read(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the lock file from disk, whether or not it exists.
|
||||||
|
*/
|
||||||
|
remove(): void;
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
/**
|
||||||
|
* @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 {CachedFileSystem, FileSystem} from '../../../src/ngtsc/file_system';
|
||||||
|
import {LockFile, getLockFilePath} from './lock_file';
|
||||||
|
|
||||||
|
export class LockFileWithSignalHandlers implements LockFile {
|
||||||
|
constructor(protected fs: FileSystem) {}
|
||||||
|
|
||||||
|
path = getLockFilePath(this.fs);
|
||||||
|
|
||||||
|
write(): void {
|
||||||
|
try {
|
||||||
|
this.addSignalHandlers();
|
||||||
|
// To avoid race conditions, we check for existence of the lock-file by actually trying to
|
||||||
|
// create it exclusively.
|
||||||
|
return this.fs.writeFile(this.path, process.pid.toString(), /* exclusive */ true);
|
||||||
|
} catch (e) {
|
||||||
|
this.removeSignalHandlers();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read(): string {
|
||||||
|
try {
|
||||||
|
if (this.fs instanceof CachedFileSystem) {
|
||||||
|
// This file is "volatile", it might be changed by an external process,
|
||||||
|
// so we cannot rely upon the cached value when reading it.
|
||||||
|
this.fs.invalidateCaches(this.path);
|
||||||
|
}
|
||||||
|
return this.fs.readFile(this.path);
|
||||||
|
} catch {
|
||||||
|
return '{unknown}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
this.removeSignalHandlers();
|
||||||
|
if (this.fs.exists(this.path)) {
|
||||||
|
this.fs.removeFile(this.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture CTRL-C and terminal closing events.
|
||||||
|
* When these occur we remove the lock-file and exit.
|
||||||
|
*/
|
||||||
|
protected addSignalHandlers() {
|
||||||
|
process.addListener('SIGINT', this.signalHandler);
|
||||||
|
process.addListener('SIGHUP', this.signalHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the event handlers to prevent leakage.
|
||||||
|
*/
|
||||||
|
protected removeSignalHandlers() {
|
||||||
|
process.removeListener('SIGINT', this.signalHandler);
|
||||||
|
process.removeListener('SIGHUP', this.signalHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This handler 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* @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 {Logger} from '../logging/logger';
|
||||||
|
import {LockFile} from './lock_file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SyncLocker is used to prevent more than one instance of ngcc executing at the same time,
|
||||||
|
* when being called in a synchronous context.
|
||||||
|
*
|
||||||
|
* * 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 SyncLocker {
|
||||||
|
constructor(private lockFile: LockFile) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the given function guarded by the lock file.
|
||||||
|
*
|
||||||
|
* @param fn the function to run.
|
||||||
|
* @returns the value returned from the `fn` call.
|
||||||
|
*/
|
||||||
|
lock<T>(fn: () => T): T {
|
||||||
|
this.create();
|
||||||
|
try {
|
||||||
|
return fn();
|
||||||
|
} finally {
|
||||||
|
this.lockFile.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a lock file to disk, or error if there is already one there.
|
||||||
|
*/
|
||||||
|
protected create(): void {
|
||||||
|
try {
|
||||||
|
this.lockFile.write();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code !== 'EEXIST') {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
this.handleExistingLockFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lock-file already exists so raise a helpful error.
|
||||||
|
*/
|
||||||
|
protected handleExistingLockFile(): void {
|
||||||
|
const pid = this.lockFile.read();
|
||||||
|
throw new Error(
|
||||||
|
`ngcc is already running at process with id ${pid}.\n` +
|
||||||
|
`If you are running multiple builds in parallel then you should pre-process your node_modules via the command line ngcc tool before starting the builds;\n` +
|
||||||
|
`See https://v9.angular.io/guide/ivy#speeding-up-ngcc-compilation.\n` +
|
||||||
|
`(If you are sure no ngcc process is running then you should delete the lock-file at ${this.lockFile.path}.)`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,10 +27,12 @@ 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 {AsyncLocker, LockFileWithSignalHandlers, SyncLocker} from './execution/lock_file';
|
|
||||||
import {SingleProcessExecutorAsync, SingleProcessExecutorSync} from './execution/single_process_executor';
|
import {SingleProcessExecutorAsync, SingleProcessExecutorSync} 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';
|
||||||
|
import {AsyncLocker} from './locking/async_locker';
|
||||||
|
import {LockFileWithSignalHandlers} from './locking/lock_file_with_signal_handlers';
|
||||||
|
import {SyncLocker} from './locking/sync_locker';
|
||||||
import {ConsoleLogger} from './logging/console_logger';
|
import {ConsoleLogger} from './logging/console_logger';
|
||||||
import {LogLevel, Logger} from './logging/logger';
|
import {LogLevel, Logger} from './logging/logger';
|
||||||
import {hasBeenProcessed} from './packages/build_marker';
|
import {hasBeenProcessed} from './packages/build_marker';
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {MockFileSystemNative} from '../../../../src/ngtsc/file_system/testing';
|
||||||
import {ClusterExecutor} from '../../../src/execution/cluster/executor';
|
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 {AsyncLocker} from '../../../src/execution/lock_file';
|
import {AsyncLocker} from '../../../src/locking/async_locker';
|
||||||
import {PackageJsonUpdater} from '../../../src/writing/package_json_updater';
|
import {PackageJsonUpdater} from '../../../src/writing/package_json_updater';
|
||||||
import {MockLockFile} from '../../helpers/mock_lock_file';
|
import {MockLockFile} from '../../helpers/mock_lock_file';
|
||||||
import {MockLogger} from '../../helpers/mock_logger';
|
import {MockLogger} from '../../helpers/mock_logger';
|
||||||
|
|
|
@ -1,366 +0,0 @@
|
||||||
/**
|
|
||||||
* @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 {CachedFileSystem, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
|
|
||||||
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
|
||||||
import {AsyncLocker, LockFileWithSignalHandlers, SyncLocker} from '../../src/execution/lock_file';
|
|
||||||
import {MockLockFile} from '../helpers/mock_lock_file';
|
|
||||||
import {MockLogger} from '../helpers/mock_logger';
|
|
||||||
|
|
||||||
runInEachFileSystem(() => {
|
|
||||||
describe('LockFileWithSignalHandlers', () => {
|
|
||||||
/**
|
|
||||||
* This class allows us to test ordering of the calls, and to avoid actually attaching signal
|
|
||||||
* handlers and most importantly not actually exiting the process.
|
|
||||||
*/
|
|
||||||
class LockFileUnderTest extends LockFileWithSignalHandlers {
|
|
||||||
log: string[] = [];
|
|
||||||
constructor(fs: FileSystem, private handleSignals = false) {
|
|
||||||
super(fs);
|
|
||||||
fs.ensureDir(fs.dirname(this.path));
|
|
||||||
}
|
|
||||||
remove() {
|
|
||||||
this.log.push('remove()');
|
|
||||||
super.remove();
|
|
||||||
}
|
|
||||||
addSignalHandlers() {
|
|
||||||
this.log.push('addSignalHandlers()');
|
|
||||||
if (this.handleSignals) {
|
|
||||||
super.addSignalHandlers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
write() {
|
|
||||||
this.log.push('write()');
|
|
||||||
super.write();
|
|
||||||
}
|
|
||||||
read() {
|
|
||||||
const contents = super.read();
|
|
||||||
this.log.push('read() => ' + contents);
|
|
||||||
return contents;
|
|
||||||
}
|
|
||||||
removeSignalHandlers() {
|
|
||||||
this.log.push('removeSignalHandlers()');
|
|
||||||
super.removeSignalHandlers();
|
|
||||||
}
|
|
||||||
exit(code: number) { this.log.push(`exit(${code})`); }
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('write()', () => {
|
|
||||||
it('should call `addSignalHandlers()`', () => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
|
||||||
lockFile.write();
|
|
||||||
expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call `removeSignalHandlers()` if there is an error', () => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
spyOn(fs, 'writeFile').and.throwError('WRITING ERROR');
|
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
|
||||||
expect(() => lockFile.write()).toThrowError('WRITING ERROR');
|
|
||||||
expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()', 'removeSignalHandlers()']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove the lockFile if CTRL-C is triggered', () => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
|
|
||||||
|
|
||||||
lockFile.write();
|
|
||||||
expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()']);
|
|
||||||
|
|
||||||
// Simulate the CTRL-C signal
|
|
||||||
lockFile.log.push('SIGINT');
|
|
||||||
process.emit('SIGINT', 'SIGINT');
|
|
||||||
|
|
||||||
expect(lockFile.log).toEqual([
|
|
||||||
'write()', 'addSignalHandlers()', 'SIGINT', 'remove()', 'removeSignalHandlers()',
|
|
||||||
'exit(1)'
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove the lockFile if terminal is closed', () => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
|
|
||||||
|
|
||||||
lockFile.write();
|
|
||||||
expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()']);
|
|
||||||
|
|
||||||
// Simulate the terminal being closed
|
|
||||||
lockFile.log.push('SIGHUP');
|
|
||||||
process.emit('SIGHUP', 'SIGHUP');
|
|
||||||
|
|
||||||
expect(lockFile.log).toEqual([
|
|
||||||
'write()', 'addSignalHandlers()', 'SIGHUP', 'remove()', 'removeSignalHandlers()',
|
|
||||||
'exit(1)'
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('read()', () => {
|
|
||||||
it('should return the contents of the lockFile', () => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
|
||||||
fs.writeFile(lockFile.path, '188');
|
|
||||||
expect(lockFile.read()).toEqual('188');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return `{unknown}` if the lockFile does not exist', () => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
|
||||||
expect(lockFile.read()).toEqual('{unknown}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not read file from the cache, since the file may have been modified externally',
|
|
||||||
() => {
|
|
||||||
const rawFs = getFileSystem();
|
|
||||||
const fs = new CachedFileSystem(rawFs);
|
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
|
||||||
rawFs.writeFile(lockFile.path, '188');
|
|
||||||
expect(lockFile.read()).toEqual('188');
|
|
||||||
// We need to write to the rawFs to ensure that we don't update the cache at this point
|
|
||||||
rawFs.writeFile(lockFile.path, '444');
|
|
||||||
expect(lockFile.read()).toEqual('444');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('remove()', () => {
|
|
||||||
it('should remove the lock file from the file-system', () => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
|
||||||
fs.writeFile(lockFile.path, '188');
|
|
||||||
lockFile.remove();
|
|
||||||
expect(fs.exists(lockFile.path)).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();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call removeSignalHandlers()', () => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
|
||||||
fs.writeFile(lockFile.path, '188');
|
|
||||||
lockFile.remove();
|
|
||||||
expect(lockFile.log).toEqual(['remove()', 'removeSignalHandlers()']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('SyncLocker', () => {
|
|
||||||
describe('lock()', () => {
|
|
||||||
it('should guard the `fn()` with calls to `write()` and `remove()`', () => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const log: string[] = [];
|
|
||||||
const lockFile = new MockLockFile(fs, log);
|
|
||||||
const locker = new SyncLocker(lockFile);
|
|
||||||
|
|
||||||
locker.lock(() => log.push('fn()'));
|
|
||||||
|
|
||||||
expect(log).toEqual(['write()', 'fn()', 'remove()']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should guard the `fn()` with calls to `write()` and `remove()`, even if it throws',
|
|
||||||
() => {
|
|
||||||
let error: string = '';
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const log: string[] = [];
|
|
||||||
const lockFile = new MockLockFile(fs, log);
|
|
||||||
const locker = new SyncLocker(lockFile);
|
|
||||||
|
|
||||||
try {
|
|
||||||
locker.lock(() => {
|
|
||||||
log.push('fn()');
|
|
||||||
throw new Error('ERROR');
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
error = e.message;
|
|
||||||
}
|
|
||||||
expect(error).toEqual('ERROR');
|
|
||||||
expect(log).toEqual(['write()', 'fn()', 'remove()']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should error if a lock file already exists', () => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const log: string[] = [];
|
|
||||||
const lockFile = new MockLockFile(fs, log);
|
|
||||||
const locker = new SyncLocker(lockFile);
|
|
||||||
|
|
||||||
spyOn(lockFile, 'write').and.callFake(() => { throw {code: 'EEXIST'}; });
|
|
||||||
spyOn(lockFile, 'read').and.returnValue('188');
|
|
||||||
|
|
||||||
expect(() => locker.lock(() => {}))
|
|
||||||
.toThrowError(
|
|
||||||
`ngcc is already running at process with id 188.\n` +
|
|
||||||
`If you are running multiple builds in parallel then you should pre-process your node_modules via the command line ngcc tool before starting the builds;\n` +
|
|
||||||
`See https://v9.angular.io/guide/ivy#speeding-up-ngcc-compilation.\n` +
|
|
||||||
`(If you are sure no ngcc process is running then you should delete the lockFile at ${lockFile.path}.)`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('AsyncLocker', () => {
|
|
||||||
describe('lock()', () => {
|
|
||||||
it('should guard the `fn()` with calls to `write()` and `remove()`', async() => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const log: string[] = [];
|
|
||||||
const lockFile = new MockLockFile(fs, log);
|
|
||||||
const locker = new AsyncLocker(lockFile, new MockLogger(), 100, 10);
|
|
||||||
|
|
||||||
await locker.lock(async() => {
|
|
||||||
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();
|
|
||||||
log.push('fn() - after');
|
|
||||||
return Promise.resolve();
|
|
||||||
});
|
|
||||||
expect(log).toEqual(['write()', 'fn() - before', 'fn() - after', 'remove()']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should guard the `fn()` with calls to `write()` and `remove()`, even if it throws',
|
|
||||||
async() => {
|
|
||||||
let error: string = '';
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const log: string[] = [];
|
|
||||||
const lockFile = new MockLockFile(fs, log);
|
|
||||||
const locker = new AsyncLocker(lockFile, new MockLogger(), 100, 10);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await locker.lock(async() => {
|
|
||||||
log.push('fn()');
|
|
||||||
throw new Error('ERROR');
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
error = e.message;
|
|
||||||
}
|
|
||||||
expect(error).toEqual('ERROR');
|
|
||||||
expect(log).toEqual(['write()', 'fn()', 'remove()']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should retry if another process is locking', async() => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const log: string[] = [];
|
|
||||||
const lockFile = new MockLockFile(fs, log);
|
|
||||||
const logger = new MockLogger();
|
|
||||||
const locker = new AsyncLocker(lockFile, logger, 100, 10);
|
|
||||||
|
|
||||||
let lockFileContents: string|null = '188';
|
|
||||||
spyOn(lockFile, 'write').and.callFake(() => {
|
|
||||||
log.push('write()');
|
|
||||||
if (lockFileContents) {
|
|
||||||
throw {code: 'EEXIST'};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
spyOn(lockFile, 'read').and.callFake(() => {
|
|
||||||
log.push('read() => ' + lockFileContents);
|
|
||||||
return lockFileContents;
|
|
||||||
});
|
|
||||||
|
|
||||||
const promise = locker.lock(async() => log.push('fn()'));
|
|
||||||
// The lock is now waiting on the lockFile becoming free, so no `fn()` in the log.
|
|
||||||
expect(log).toEqual(['write()', 'read() => 188']);
|
|
||||||
expect(logger.logs.info).toEqual([[
|
|
||||||
'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
|
||||||
]]);
|
|
||||||
|
|
||||||
lockFileContents = null;
|
|
||||||
// The lockFile has been removed, so we can create our own lockFile, call `fn()` and then
|
|
||||||
// remove the lockFile.
|
|
||||||
await promise;
|
|
||||||
expect(log).toEqual(['write()', 'read() => 188', 'write()', 'fn()', 'remove()']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should extend the retry timeout if the other process locking the file changes', async() => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const log: string[] = [];
|
|
||||||
const lockFile = new MockLockFile(fs, log);
|
|
||||||
const logger = new MockLogger();
|
|
||||||
const locker = new AsyncLocker(lockFile, logger, 100, 10);
|
|
||||||
|
|
||||||
let lockFileContents: string|null = '188';
|
|
||||||
spyOn(lockFile, 'write').and.callFake(() => {
|
|
||||||
log.push('write()');
|
|
||||||
if (lockFileContents) {
|
|
||||||
throw {code: 'EEXIST'};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
spyOn(lockFile, 'read').and.callFake(() => {
|
|
||||||
log.push('read() => ' + lockFileContents);
|
|
||||||
return lockFileContents;
|
|
||||||
});
|
|
||||||
|
|
||||||
async() => {
|
|
||||||
const promise = locker.lock(async() => log.push('fn()'));
|
|
||||||
// The lock is now waiting on the lockFile becoming free, so no `fn()` in the log.
|
|
||||||
expect(log).toEqual(['write()', 'read() => 188']);
|
|
||||||
expect(logger.logs.info).toEqual([[
|
|
||||||
'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
|
||||||
]]);
|
|
||||||
|
|
||||||
lockFileContents = '444';
|
|
||||||
// The lockFile has been taken over by another process
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 250));
|
|
||||||
expect(log).toEqual(['write()', 'read() => 188', 'write()', 'read() => 444']);
|
|
||||||
expect(logger.logs.info).toEqual([
|
|
||||||
[
|
|
||||||
'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'Another process, with id 444, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
lockFileContents = null;
|
|
||||||
// The lockFile has been removed, so we can create our own lockFile, call `fn()` and then
|
|
||||||
// remove the lockFile.
|
|
||||||
await promise;
|
|
||||||
expect(log).toEqual([
|
|
||||||
'write()', 'read() => 188', 'write()', 'read() => 444', 'write()', 'fn()', 'remove()'
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should error if another process does not release the lockFile before this times out',
|
|
||||||
async() => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const log: string[] = [];
|
|
||||||
const lockFile = new MockLockFile(fs, log);
|
|
||||||
const logger = new MockLogger();
|
|
||||||
const locker = new AsyncLocker(lockFile, logger, 100, 2);
|
|
||||||
|
|
||||||
let lockFileContents: string|null = '188';
|
|
||||||
spyOn(lockFile, 'write').and.callFake(() => {
|
|
||||||
log.push('write()');
|
|
||||||
if (lockFileContents) {
|
|
||||||
throw {code: 'EEXIST'};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
spyOn(lockFile, 'read').and.callFake(() => {
|
|
||||||
log.push('read() => ' + lockFileContents);
|
|
||||||
return lockFileContents;
|
|
||||||
});
|
|
||||||
|
|
||||||
const promise = locker.lock(async() => log.push('fn()'));
|
|
||||||
|
|
||||||
// The lock is now waiting on the lockFile becoming free, so no `fn()` in the log.
|
|
||||||
expect(log).toEqual(['write()', 'read() => 188']);
|
|
||||||
// Do not remove the lockFile and let the call to `lock()` timeout.
|
|
||||||
let error: Error;
|
|
||||||
await promise.catch(e => error = e);
|
|
||||||
expect(log).toEqual(['write()', 'read() => 188', 'write()', 'read() => 188']);
|
|
||||||
expect(error !.message)
|
|
||||||
.toEqual(
|
|
||||||
`Timed out waiting 0.2s for another ngcc process, with id 188, to complete.\n` +
|
|
||||||
`(If you are sure no ngcc process is running then you should delete the lockFile at ${lockFile.path}.)`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -9,9 +9,9 @@
|
||||||
/// <reference types="node" />
|
/// <reference types="node" />
|
||||||
|
|
||||||
import {MockFileSystemNative} from '../../../src/ngtsc/file_system/testing';
|
import {MockFileSystemNative} from '../../../src/ngtsc/file_system/testing';
|
||||||
import {SyncLocker} from '../../src/execution/lock_file';
|
|
||||||
import {SingleProcessExecutorSync} from '../../src/execution/single_process_executor';
|
import {SingleProcessExecutorSync} from '../../src/execution/single_process_executor';
|
||||||
import {SerialTaskQueue} from '../../src/execution/task_selection/serial_task_queue';
|
import {SerialTaskQueue} from '../../src/execution/task_selection/serial_task_queue';
|
||||||
|
import {SyncLocker} from '../../src/locking/sync_locker';
|
||||||
import {PackageJsonUpdater} from '../../src/writing/package_json_updater';
|
import {PackageJsonUpdater} from '../../src/writing/package_json_updater';
|
||||||
import {MockLockFile} from '../helpers/mock_lock_file';
|
import {MockLockFile} from '../helpers/mock_lock_file';
|
||||||
import {MockLogger} from '../helpers/mock_logger';
|
import {MockLogger} from '../helpers/mock_logger';
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {FileSystem} from '../../../src/ngtsc/file_system';
|
import {FileSystem} from '../../../src/ngtsc/file_system';
|
||||||
import {LockFile} from '../../src/execution/lock_file';
|
import {LockFile} from '../../src/locking/lock_file';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A mock implementation of `LockFile` that just logs its calls.
|
* A mock implementation of `LockFile` that just logs its calls.
|
||||||
|
|
|
@ -13,7 +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 {getLockFilePath} from '../../src/execution/lock_file';
|
import {getLockFilePath} from '../../src/locking/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';
|
||||||
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
/**
|
||||||
|
* @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 {getFileSystem} from '../../../src/ngtsc/file_system';
|
||||||
|
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||||
|
import {AsyncLocker} from '../../src/locking/async_locker';
|
||||||
|
import {MockLockFile} from '../helpers/mock_lock_file';
|
||||||
|
import {MockLogger} from '../helpers/mock_logger';
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
|
describe('AsyncLocker', () => {
|
||||||
|
describe('lock()', () => {
|
||||||
|
it('should guard the `fn()` with calls to `write()` and `remove()`', async() => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const log: string[] = [];
|
||||||
|
const lockFile = new MockLockFile(fs, log);
|
||||||
|
const locker = new AsyncLocker(lockFile, new MockLogger(), 100, 10);
|
||||||
|
|
||||||
|
await locker.lock(async() => {
|
||||||
|
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();
|
||||||
|
log.push('fn() - after');
|
||||||
|
});
|
||||||
|
expect(log).toEqual(['write()', 'fn() - before', 'fn() - after', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should guard the `fn()` with calls to `write()` and `remove()`, even if it throws',
|
||||||
|
async() => {
|
||||||
|
let error: string = '';
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const log: string[] = [];
|
||||||
|
const lockFile = new MockLockFile(fs, log);
|
||||||
|
const locker = new AsyncLocker(lockFile, new MockLogger(), 100, 10);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await locker.lock(async() => {
|
||||||
|
log.push('fn()');
|
||||||
|
throw new Error('ERROR');
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
expect(error).toEqual('ERROR');
|
||||||
|
expect(log).toEqual(['write()', 'fn()', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retry if another process is locking', async() => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const log: string[] = [];
|
||||||
|
const lockFile = new MockLockFile(fs, log);
|
||||||
|
const logger = new MockLogger();
|
||||||
|
const locker = new AsyncLocker(lockFile, logger, 100, 10);
|
||||||
|
|
||||||
|
let lockFileContents: string|null = '188';
|
||||||
|
spyOn(lockFile, 'write').and.callFake(() => {
|
||||||
|
log.push('write()');
|
||||||
|
if (lockFileContents) {
|
||||||
|
throw {code: 'EEXIST'};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
spyOn(lockFile, 'read').and.callFake(() => {
|
||||||
|
log.push('read() => ' + lockFileContents);
|
||||||
|
return lockFileContents;
|
||||||
|
});
|
||||||
|
|
||||||
|
const promise = locker.lock(async() => log.push('fn()'));
|
||||||
|
// The lock is now waiting on the lock-file becoming free, so no `fn()` in the log.
|
||||||
|
expect(log).toEqual(['write()', 'read() => 188']);
|
||||||
|
expect(logger.logs.info).toEqual([[
|
||||||
|
'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
||||||
|
]]);
|
||||||
|
|
||||||
|
lockFileContents = null;
|
||||||
|
// The lock-file has been removed, so we can create our own lock-file, call `fn()` and then
|
||||||
|
// remove the lock-file.
|
||||||
|
await promise;
|
||||||
|
expect(log).toEqual(['write()', 'read() => 188', 'write()', 'fn()', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extend the retry timeout if the other process locking the file changes', async() => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const log: string[] = [];
|
||||||
|
const lockFile = new MockLockFile(fs, log);
|
||||||
|
const logger = new MockLogger();
|
||||||
|
const locker = new AsyncLocker(lockFile, logger, 200, 5);
|
||||||
|
|
||||||
|
let lockFileContents: string|null = '188';
|
||||||
|
spyOn(lockFile, 'write').and.callFake(() => {
|
||||||
|
log.push('write()');
|
||||||
|
if (lockFileContents) {
|
||||||
|
throw {code: 'EEXIST'};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
spyOn(lockFile, 'read').and.callFake(() => {
|
||||||
|
log.push('read() => ' + lockFileContents);
|
||||||
|
return lockFileContents;
|
||||||
|
});
|
||||||
|
|
||||||
|
const promise = locker.lock(async() => log.push('fn()'));
|
||||||
|
// The lock is now waiting on the lock-file becoming free, so no `fn()` in the log.
|
||||||
|
expect(log).toEqual(['write()', 'read() => 188']);
|
||||||
|
expect(logger.logs.info).toEqual([[
|
||||||
|
'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
||||||
|
]]);
|
||||||
|
|
||||||
|
lockFileContents = '444';
|
||||||
|
// The lock-file has been taken over by another process - wait for the next attempt
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 250));
|
||||||
|
expect(log).toEqual(['write()', 'read() => 188', 'write()', 'read() => 444']);
|
||||||
|
expect(logger.logs.info).toEqual([
|
||||||
|
[
|
||||||
|
'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Another process, with id 444, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
lockFileContents = null;
|
||||||
|
// The lock-file has been removed, so we can create our own lock-file, call `fn()` and
|
||||||
|
// then remove the lock-file.
|
||||||
|
await promise;
|
||||||
|
expect(log).toEqual([
|
||||||
|
'write()', 'read() => 188', 'write()', 'read() => 444', 'write()', 'fn()', 'remove()'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if another process does not release the lock-file before this times out',
|
||||||
|
async() => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const log: string[] = [];
|
||||||
|
const lockFile = new MockLockFile(fs, log);
|
||||||
|
const logger = new MockLogger();
|
||||||
|
const locker = new AsyncLocker(lockFile, logger, 100, 2);
|
||||||
|
|
||||||
|
let lockFileContents: string|null = '188';
|
||||||
|
spyOn(lockFile, 'write').and.callFake(() => {
|
||||||
|
log.push('write()');
|
||||||
|
if (lockFileContents) {
|
||||||
|
throw {code: 'EEXIST'};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
spyOn(lockFile, 'read').and.callFake(() => {
|
||||||
|
log.push('read() => ' + lockFileContents);
|
||||||
|
return lockFileContents;
|
||||||
|
});
|
||||||
|
|
||||||
|
const promise = locker.lock(async() => log.push('fn()'));
|
||||||
|
|
||||||
|
// The lock is now waiting on the lock-file becoming free, so no `fn()` in the log.
|
||||||
|
expect(log).toEqual(['write()', 'read() => 188']);
|
||||||
|
// Do not remove the lock-file and let the call to `lock()` timeout.
|
||||||
|
let error: Error;
|
||||||
|
await promise.catch(e => error = e);
|
||||||
|
expect(log).toEqual(['write()', 'read() => 188', 'write()', 'read() => 188']);
|
||||||
|
expect(error !.message)
|
||||||
|
.toEqual(
|
||||||
|
`Timed out waiting 0.2s for another ngcc process, with id 188, to complete.\n` +
|
||||||
|
`(If you are sure no ngcc process is running then you should delete the lock-file at ${lockFile.path}.)`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,154 @@
|
||||||
|
/**
|
||||||
|
* @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 {CachedFileSystem, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
|
||||||
|
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||||
|
import {LockFileWithSignalHandlers} from '../../src/locking/lock_file_with_signal_handlers';
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
|
describe('LockFileWithSignalHandlers', () => {
|
||||||
|
/**
|
||||||
|
* This class allows us to test ordering of the calls, and to avoid actually attaching signal
|
||||||
|
* handlers and most importantly not actually exiting the process.
|
||||||
|
*/
|
||||||
|
class LockFileUnderTest extends LockFileWithSignalHandlers {
|
||||||
|
log: string[] = [];
|
||||||
|
constructor(fs: FileSystem, private handleSignals = false) {
|
||||||
|
super(fs);
|
||||||
|
fs.ensureDir(fs.dirname(this.path));
|
||||||
|
}
|
||||||
|
remove() {
|
||||||
|
this.log.push('remove()');
|
||||||
|
super.remove();
|
||||||
|
}
|
||||||
|
addSignalHandlers() {
|
||||||
|
this.log.push('addSignalHandlers()');
|
||||||
|
if (this.handleSignals) {
|
||||||
|
super.addSignalHandlers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write() {
|
||||||
|
this.log.push('write()');
|
||||||
|
super.write();
|
||||||
|
}
|
||||||
|
read() {
|
||||||
|
const contents = super.read();
|
||||||
|
this.log.push('read() => ' + contents);
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
removeSignalHandlers() {
|
||||||
|
this.log.push('removeSignalHandlers()');
|
||||||
|
super.removeSignalHandlers();
|
||||||
|
}
|
||||||
|
exit(code: number) { this.log.push(`exit(${code})`); }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('write()', () => {
|
||||||
|
it('should call `addSignalHandlers()`', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
lockFile.write();
|
||||||
|
expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call `removeSignalHandlers()` if there is an error', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
spyOn(fs, 'writeFile').and.throwError('WRITING ERROR');
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
expect(() => lockFile.write()).toThrowError('WRITING ERROR');
|
||||||
|
expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()', 'removeSignalHandlers()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the lock-file if CTRL-C is triggered', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
|
||||||
|
|
||||||
|
lockFile.write();
|
||||||
|
expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()']);
|
||||||
|
|
||||||
|
// Simulate the CTRL-C signal
|
||||||
|
lockFile.log.push('SIGINT');
|
||||||
|
process.emit('SIGINT', 'SIGINT');
|
||||||
|
|
||||||
|
expect(lockFile.log).toEqual([
|
||||||
|
'write()', 'addSignalHandlers()', 'SIGINT', 'remove()', 'removeSignalHandlers()',
|
||||||
|
'exit(1)'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the lock-file if terminal is closed', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
|
||||||
|
|
||||||
|
lockFile.write();
|
||||||
|
expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()']);
|
||||||
|
|
||||||
|
// Simulate the terminal being closed
|
||||||
|
lockFile.log.push('SIGHUP');
|
||||||
|
process.emit('SIGHUP', 'SIGHUP');
|
||||||
|
|
||||||
|
expect(lockFile.log).toEqual([
|
||||||
|
'write()', 'addSignalHandlers()', 'SIGHUP', 'remove()', 'removeSignalHandlers()',
|
||||||
|
'exit(1)'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('read()', () => {
|
||||||
|
it('should return the contents of the lock-file', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
fs.writeFile(lockFile.path, '188');
|
||||||
|
expect(lockFile.read()).toEqual('188');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return `{unknown}` if the lock-file does not exist', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
expect(lockFile.read()).toEqual('{unknown}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not read file from the cache, since the file may have been modified externally',
|
||||||
|
() => {
|
||||||
|
const rawFs = getFileSystem();
|
||||||
|
const fs = new CachedFileSystem(rawFs);
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
rawFs.writeFile(lockFile.path, '188');
|
||||||
|
expect(lockFile.read()).toEqual('188');
|
||||||
|
// We need to write to the rawFs to ensure that we don't update the cache at this point
|
||||||
|
rawFs.writeFile(lockFile.path, '444');
|
||||||
|
expect(lockFile.read()).toEqual('444');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('remove()', () => {
|
||||||
|
it('should remove the lock file from the file-system', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
fs.writeFile(lockFile.path, '188');
|
||||||
|
lockFile.remove();
|
||||||
|
expect(fs.exists(lockFile.path)).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();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call removeSignalHandlers()', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
fs.writeFile(lockFile.path, '188');
|
||||||
|
lockFile.remove();
|
||||||
|
expect(lockFile.log).toEqual(['remove()', 'removeSignalHandlers()']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* @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 {getFileSystem} from '../../../src/ngtsc/file_system';
|
||||||
|
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||||
|
import {SyncLocker} from '../../src/locking/sync_locker';
|
||||||
|
import {MockLockFile} from '../helpers/mock_lock_file';
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
|
describe('SyncLocker', () => {
|
||||||
|
describe('lock()', () => {
|
||||||
|
it('should guard the `fn()` with calls to `write()` and `remove()`', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const log: string[] = [];
|
||||||
|
const lockFile = new MockLockFile(fs, log);
|
||||||
|
const locker = new SyncLocker(lockFile);
|
||||||
|
|
||||||
|
locker.lock(() => log.push('fn()'));
|
||||||
|
|
||||||
|
expect(log).toEqual(['write()', 'fn()', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should guard the `fn()` with calls to `write()` and `remove()`, even if it throws',
|
||||||
|
() => {
|
||||||
|
let error: string = '';
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const log: string[] = [];
|
||||||
|
const lockFile = new MockLockFile(fs, log);
|
||||||
|
const locker = new SyncLocker(lockFile);
|
||||||
|
|
||||||
|
try {
|
||||||
|
locker.lock(() => {
|
||||||
|
log.push('fn()');
|
||||||
|
throw new Error('ERROR');
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
expect(error).toEqual('ERROR');
|
||||||
|
expect(log).toEqual(['write()', 'fn()', 'remove()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if a lock file already exists', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const log: string[] = [];
|
||||||
|
const lockFile = new MockLockFile(fs, log);
|
||||||
|
const locker = new SyncLocker(lockFile);
|
||||||
|
|
||||||
|
spyOn(lockFile, 'write').and.callFake(() => { throw {code: 'EEXIST'}; });
|
||||||
|
spyOn(lockFile, 'read').and.returnValue('188');
|
||||||
|
|
||||||
|
expect(() => locker.lock(() => {}))
|
||||||
|
.toThrowError(
|
||||||
|
`ngcc is already running at process with id 188.\n` +
|
||||||
|
`If you are running multiple builds in parallel then you should pre-process your node_modules via the command line ngcc tool before starting the builds;\n` +
|
||||||
|
`See https://v9.angular.io/guide/ivy#speeding-up-ngcc-compilation.\n` +
|
||||||
|
`(If you are sure no ngcc process is running then you should delete the lock-file at ${lockFile.path}.)`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue