diff --git a/packages/compiler-cli/ngcc/src/execution/cluster/lock_file_with_child_process.ts b/packages/compiler-cli/ngcc/src/execution/cluster/lock_file_with_child_process.ts new file mode 100644 index 0000000000000..b6354976a6cb1 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/execution/cluster/lock_file_with_child_process.ts @@ -0,0 +1,42 @@ +/** + * @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 {ChildProcess} from 'child_process'; +import * as cluster from 'cluster'; + +import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system'; +import {LockFileWithChildProcess} from '../../locking/lock_file_with_child_process'; + + +/** + * A `LockFileWithChildProcess` that is `cluster`-aware and does not spawn unlocker processes from + * worker processes (only from the master process, which does the locking). + */ +export class ClusterLockFileWithChildProcess extends LockFileWithChildProcess { + write(): void { + if (!cluster.isMaster) { + // This is a worker process: + // This method should only be on the master process. + throw new Error('Tried to create a lock-file from a worker process.'); + } + + return super.write(); + } + + protected createUnlocker(path: AbsoluteFsPath): ChildProcess|null { + if (cluster.isMaster) { + // This is the master process: + // Create the unlocker. + return super.createUnlocker(path); + } + + return null; + } +} diff --git a/packages/compiler-cli/ngcc/src/locking/lock_file_with_child_process/index.ts b/packages/compiler-cli/ngcc/src/locking/lock_file_with_child_process/index.ts index cbabff7719db4..a544addf0dd9e 100644 --- a/packages/compiler-cli/ngcc/src/locking/lock_file_with_child_process/index.ts +++ b/packages/compiler-cli/ngcc/src/locking/lock_file_with_child_process/index.ts @@ -5,7 +5,8 @@ * 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 {ChildProcess, fork} from 'child_process'; +import {ChildProcess, ChildProcessByStdio, fork} from 'child_process'; +import {Readable, Writable} from 'stream'; import {AbsoluteFsPath, CachedFileSystem, FileSystem} from '../../../../src/ngtsc/file_system'; import {Logger, LogLevel} from '../../logging/logger'; @@ -77,10 +78,18 @@ export class LockFileWithChildProcess implements LockFile { } } - protected createUnlocker(path: AbsoluteFsPath): ChildProcess { + protected createUnlocker(path: AbsoluteFsPath): ChildProcess|null { this.logger.debug('Forking unlocker child-process'); const logLevel = this.logger.level !== undefined ? this.logger.level.toString() : LogLevel.info.toString(); - return fork(this.fs.resolve(__dirname, './unlocker.js'), [path, logLevel], {detached: true}); + + const unlocker = fork(this.fs.resolve(__dirname, './unlocker.js'), [path, logLevel], { + detached: true, + stdio: 'pipe', + }) as ChildProcessByStdio; + unlocker.stdout.on('data', data => process.stdout.write(data)); + unlocker.stderr.on('data', data => process.stderr.write(data)); + + return unlocker; } } diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 1f63a029bbbf7..0d072866a1ce6 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -27,6 +27,7 @@ import {EntryPointFinder} from './entry_point_finder/interface'; import {TargetedEntryPointFinder} from './entry_point_finder/targeted_entry_point_finder'; import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from './execution/api'; import {ClusterExecutor} from './execution/cluster/executor'; +import {ClusterLockFileWithChildProcess} from './execution/cluster/lock_file_with_child_process'; import {ClusterPackageJsonUpdater} from './execution/cluster/package_json_updater'; import {SingleProcessExecutorAsync, SingleProcessExecutorSync} from './execution/single_process_executor'; import {CreateTaskCompletedCallback, PartiallyOrderedTasks, Task, TaskProcessingOutcome, TaskQueue} from './execution/tasks/api'; @@ -428,7 +429,8 @@ function getCreateTaskCompletedCallback( function getExecutor( async: boolean, inParallel: boolean, logger: Logger, pkgJsonUpdater: PackageJsonUpdater, fileSystem: FileSystem, createTaskCompletedCallback: CreateTaskCompletedCallback): Executor { - const lockFile = new LockFileWithChildProcess(fileSystem, logger); + const lockFile = inParallel ? new ClusterLockFileWithChildProcess(fileSystem, logger) : + new LockFileWithChildProcess(fileSystem, logger); if (async) { // Execute asynchronously (either serially or in parallel) const locker = new AsyncLocker(lockFile, logger, 500, 50); diff --git a/packages/compiler-cli/ngcc/test/execution/cluster/lock_file_with_child_process_spec.ts b/packages/compiler-cli/ngcc/test/execution/cluster/lock_file_with_child_process_spec.ts new file mode 100644 index 0000000000000..17501508d5c05 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/execution/cluster/lock_file_with_child_process_spec.ts @@ -0,0 +1,87 @@ +/** + * @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 {ChildProcess} from 'child_process'; +import * as cluster from 'cluster'; + +import {getFileSystem} from '../../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../../src/ngtsc/file_system/testing'; +import {ClusterLockFileWithChildProcess} from '../../../src/execution/cluster/lock_file_with_child_process'; +import {LockFileWithChildProcess} from '../../../src/locking/lock_file_with_child_process'; +import {MockLogger} from '../../helpers/mock_logger'; +import {mockProperty} from '../../helpers/spy_utils'; + + +runInEachFileSystem(() => { + describe('ClusterLockFileWithChildProcess', () => { + const runAsClusterMaster = mockProperty(cluster, 'isMaster'); + const mockUnlockerProcess = {} as ChildProcess; + let lockFileWithChildProcessSpies: + Record<'createUnlocker'|'read'|'remove'|'write', jasmine.Spy>; + + beforeEach(() => { + lockFileWithChildProcessSpies = { + createUnlocker: spyOn(LockFileWithChildProcess.prototype as any, 'createUnlocker') + .and.returnValue(mockUnlockerProcess), + read: spyOn(LockFileWithChildProcess.prototype, 'read').and.returnValue('{unknown}'), + remove: spyOn(LockFileWithChildProcess.prototype, 'remove'), + write: spyOn(LockFileWithChildProcess.prototype, 'write'), + }; + }); + + it('should be an instance of `LockFileWithChildProcess`', () => { + const lockFile = new ClusterLockFileWithChildProcess(getFileSystem(), new MockLogger()); + + expect(lockFile).toEqual(jasmine.any(ClusterLockFileWithChildProcess)); + expect(lockFile).toEqual(jasmine.any(LockFileWithChildProcess)); + }); + + describe('write()', () => { + it('should create the lock-file when called on the cluster master', () => { + runAsClusterMaster(true); + const lockFile = new ClusterLockFileWithChildProcess(getFileSystem(), new MockLogger()); + + expect(lockFileWithChildProcessSpies.write).not.toHaveBeenCalled(); + + lockFile.write(); + expect(lockFileWithChildProcessSpies.write).toHaveBeenCalledWith(); + }); + + it('should throw an error when called on a cluster worker', () => { + runAsClusterMaster(false); + const lockFile = new ClusterLockFileWithChildProcess(getFileSystem(), new MockLogger()); + + expect(() => lockFile.write()) + .toThrowError('Tried to create a lock-file from a worker process.'); + expect(lockFileWithChildProcessSpies.write).not.toHaveBeenCalled(); + }); + }); + + describe('createUnlocker()', () => { + it('should create the unlocker when called on the cluster master', () => { + runAsClusterMaster(true); + const lockFile = new ClusterLockFileWithChildProcess(getFileSystem(), new MockLogger()); + + lockFileWithChildProcessSpies.createUnlocker.calls.reset(); + + expect((lockFile as any).createUnlocker(lockFile.path)).toBe(mockUnlockerProcess); + expect(lockFileWithChildProcessSpies.createUnlocker).toHaveBeenCalledWith(lockFile.path); + }); + + it('should not create the unlocker when called on a cluster worker', () => { + runAsClusterMaster(false); + const lockFile = new ClusterLockFileWithChildProcess(getFileSystem(), new MockLogger()); + + expect((lockFile as any).createUnlocker(lockFile.path)).toBeNull(); + expect(lockFileWithChildProcessSpies.createUnlocker).not.toHaveBeenCalled(); + }); + }); + }); +});