feat(ngcc): pause async ngcc processing if another process has the lockfile (#35131)
ngcc uses a lockfile to prevent two ngcc instances from executing at the same time. Previously, if a lockfile was found the current process would error and exit. Now, when in async mode, the current process is able to wait for the previous process to release the lockfile before continuing itself. PR Close #35131
This commit is contained in:

committed by
Alex Rickabaugh

parent
7e8ce24116
commit
eef07539a6
@ -13,7 +13,7 @@ import * as cluster from 'cluster';
|
||||
import {Logger} from '../../logging/logger';
|
||||
import {PackageJsonUpdater} from '../../writing/package_json_updater';
|
||||
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from '../api';
|
||||
import {LockFile} from '../lock_file';
|
||||
import {LockFileAsync} from '../lock_file';
|
||||
|
||||
import {ClusterMaster} from './master';
|
||||
import {ClusterWorker} from './worker';
|
||||
@ -26,7 +26,7 @@ import {ClusterWorker} from './worker';
|
||||
export class ClusterExecutor implements Executor {
|
||||
constructor(
|
||||
private workerCount: number, private logger: Logger,
|
||||
private pkgJsonUpdater: PackageJsonUpdater, private lockFile: LockFile) {}
|
||||
private pkgJsonUpdater: PackageJsonUpdater, private lockFile: LockFileAsync) {}
|
||||
|
||||
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn):
|
||||
Promise<void> {
|
||||
|
@ -6,78 +6,44 @@
|
||||
* 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 {
|
||||
import {CachedFileSystem, FileSystem} from '../../../src/ngtsc/file_system';
|
||||
import {Logger} from '../logging/logger';
|
||||
|
||||
export abstract class LockFileBase {
|
||||
lockFilePath =
|
||||
this.fs.resolve(require.resolve('@angular/compiler-cli/ngcc'), '../__ngcc_lock_file__');
|
||||
|
||||
constructor(private fs: FileSystem) {}
|
||||
constructor(protected 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();
|
||||
protected writeLockFile(): void {
|
||||
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();
|
||||
}
|
||||
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.lockFilePath, process.pid.toString(), /* exclusive */ true);
|
||||
} catch (e) {
|
||||
this.removeSignalHandlers();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a lock file to disk, or error if there is already one there.
|
||||
* Read the pid from the lockfile.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
protected create() {
|
||||
protected readLockFile(): string {
|
||||
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;
|
||||
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.lockFilePath);
|
||||
}
|
||||
|
||||
// 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 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.lockFilePath}.)`);
|
||||
return this.fs.readFile(this.lockFilePath);
|
||||
} catch {
|
||||
return '{unknown}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,18 +57,25 @@ export class LockFile {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture CTRL-C and terminal closing events.
|
||||
* When these occur we remove the lockfile and exit.
|
||||
*/
|
||||
protected addSignalHandlers() {
|
||||
process.once('SIGINT', this.signalHandler);
|
||||
process.once('SIGHUP', this.signalHandler);
|
||||
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 handle needs to be defined as a property rather than a method
|
||||
* 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 =
|
||||
@ -119,3 +92,113 @@ export class LockFile {
|
||||
process.exit(code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LockFileSync 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 LockFileSync extends LockFileBase {
|
||||
/**
|
||||
* 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.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a lock file to disk, or error if there is already one there.
|
||||
*/
|
||||
protected create(): void {
|
||||
try {
|
||||
this.writeLockFile();
|
||||
} 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.readLockFile();
|
||||
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.lockFilePath}.)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LockFileAsync 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 LockFileAsync extends LockFileBase {
|
||||
constructor(
|
||||
fs: FileSystem, protected logger: Logger, private retryDelay: number,
|
||||
private retryAttempts: number) {
|
||||
super(fs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 await fn().finally(() => this.remove());
|
||||
}
|
||||
|
||||
protected async create() {
|
||||
let pid: string = '';
|
||||
for (let attempts = 0; attempts < this.retryAttempts; attempts++) {
|
||||
try {
|
||||
return this.writeLockFile();
|
||||
} catch (e) {
|
||||
if (e.code !== 'EEXIST') {
|
||||
throw e;
|
||||
}
|
||||
const newPid = this.readLockFile();
|
||||
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.lockFilePath}.)`);
|
||||
}
|
||||
}
|
||||
|
@ -10,47 +10,56 @@ import {Logger} from '../logging/logger';
|
||||
import {PackageJsonUpdater} from '../writing/package_json_updater';
|
||||
|
||||
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from './api';
|
||||
import {LockFile} from './lock_file';
|
||||
import {LockFileAsync, LockFileSync} from './lock_file';
|
||||
import {onTaskCompleted} from './utils';
|
||||
|
||||
export abstract class SingleProcessorExecutorBase {
|
||||
constructor(private logger: Logger, private pkgJsonUpdater: PackageJsonUpdater) {}
|
||||
|
||||
doExecute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn):
|
||||
void|Promise<void> {
|
||||
this.logger.debug(`Running ngcc on ${this.constructor.name}.`);
|
||||
|
||||
const taskQueue = analyzeEntryPoints();
|
||||
const compile =
|
||||
createCompileFn((task, outcome) => onTaskCompleted(this.pkgJsonUpdater, task, outcome));
|
||||
|
||||
// Process all tasks.
|
||||
this.logger.debug('Processing tasks...');
|
||||
const startTime = Date.now();
|
||||
|
||||
while (!taskQueue.allTasksCompleted) {
|
||||
const task = taskQueue.getNextTask() !;
|
||||
compile(task);
|
||||
taskQueue.markTaskCompleted(task);
|
||||
}
|
||||
|
||||
const duration = Math.round((Date.now() - startTime) / 1000);
|
||||
this.logger.debug(`Processed tasks in ${duration}s.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An `Executor` that processes all tasks serially and completes synchronously.
|
||||
*/
|
||||
export class SingleProcessExecutor implements Executor {
|
||||
constructor(
|
||||
private logger: Logger, private pkgJsonUpdater: PackageJsonUpdater,
|
||||
private lockFile: LockFile) {}
|
||||
|
||||
export class SingleProcessExecutorSync extends SingleProcessorExecutorBase implements Executor {
|
||||
constructor(logger: Logger, pkgJsonUpdater: PackageJsonUpdater, private lockfile: LockFileSync) {
|
||||
super(logger, pkgJsonUpdater);
|
||||
}
|
||||
execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn): void {
|
||||
this.lockFile.lock(() => {
|
||||
this.logger.debug(`Running ngcc on ${this.constructor.name}.`);
|
||||
|
||||
const taskQueue = analyzeEntryPoints();
|
||||
const compile =
|
||||
createCompileFn((task, outcome) => onTaskCompleted(this.pkgJsonUpdater, task, outcome));
|
||||
|
||||
// Process all tasks.
|
||||
this.logger.debug('Processing tasks...');
|
||||
const startTime = Date.now();
|
||||
|
||||
while (!taskQueue.allTasksCompleted) {
|
||||
const task = taskQueue.getNextTask() !;
|
||||
compile(task);
|
||||
taskQueue.markTaskCompleted(task);
|
||||
}
|
||||
|
||||
const duration = Math.round((Date.now() - startTime) / 1000);
|
||||
this.logger.debug(`Processed tasks in ${duration}s.`);
|
||||
});
|
||||
this.lockfile.lock(() => this.doExecute(analyzeEntryPoints, createCompileFn));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An `Executor` that processes all tasks serially, but still completes asynchronously.
|
||||
*/
|
||||
export class AsyncSingleProcessExecutor extends SingleProcessExecutor {
|
||||
async execute(...args: Parameters<Executor['execute']>): Promise<void> {
|
||||
return super.execute(...args);
|
||||
export class SingleProcessExecutorAsync extends SingleProcessorExecutorBase implements Executor {
|
||||
constructor(logger: Logger, pkgJsonUpdater: PackageJsonUpdater, private lockfile: LockFileAsync) {
|
||||
super(logger, pkgJsonUpdater);
|
||||
}
|
||||
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn):
|
||||
Promise<void> {
|
||||
await this.lockfile.lock(async() => this.doExecute(analyzeEntryPoints, createCompileFn));
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user