Skip to content

Commit f30307a

Browse files
gkalpakAndrewKushnir
authored andcommitted
fix(ngcc): support recovering when a worker process crashes (#36626)
Previously, when running in parallel mode and a worker process crashed while processing a task, it was not possible for ngcc to continue without risking ending up with a corrupted entry-point and therefore it exited with an error. This, for example, could happen when a worker process received a `SIGKILL` signal, which was frequently observed in CI environments. This was probably the result of Docker killing processes due to increased memory pressure. One factor that amplifies the problem under Docker (which is often used in CI) is that it is not possible to distinguish between the available CPU cores on the host machine and the ones made available to Docker containers, thus resulting in ngcc spawning too many worker processes. This commit addresses these issues in the following ways: 1. We take advantage of the fact that files are written to disk only after an entry-point has been fully analyzed/compiled. The master process can now determine whether a worker process has not yet started writing files to disk (even if it was in the middle of processing a task) and just put the task back into the tasks queue if the worker process crashes. 2. The master process keeps track of the transformed files that a worker process will attempt to write to disk. If the worker process crashes while writing files, the master process can revert any changes and put the task back into the tasks queue (without risking corruption). 3. When a worker process crashes while processing a task (which can be a result of increased memory pressure or too many worker processes), the master process will not try to re-spawn it. This way the number or worker processes is gradually adjusted to a level that can be accomodated by the system's resources. Examples of ngcc being able to recover after a worker process crashed: - While idling: https://circleci.com/gh/angular/angular/682197 - While compiling: https://circleci.com/gh/angular/angular/682209 - While writing files: https://circleci.com/gh/angular/angular/682267 Jira issue: [FW-2008](https://angular-team.atlassian.net/browse/FW-2008) Fixes #36278 PR Close #36626
1 parent 166a455 commit f30307a

File tree

7 files changed

+69
-48
lines changed

7 files changed

+69
-48
lines changed

packages/compiler-cli/ngcc/src/execution/cluster/executor.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import {FileSystem} from '../../../../src/ngtsc/file_system';
99
import {AsyncLocker} from '../../locking/async_locker';
1010
import {Logger} from '../../logging/logger';
11+
import {FileWriter} from '../../writing/file_writer';
1112
import {PackageJsonUpdater} from '../../writing/package_json_updater';
1213
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from '../api';
1314
import {CreateTaskCompletedCallback} from '../tasks/api';
@@ -21,7 +22,8 @@ import {ClusterMaster} from './master';
2122
export class ClusterExecutor implements Executor {
2223
constructor(
2324
private workerCount: number, private fileSystem: FileSystem, private logger: Logger,
24-
private pkgJsonUpdater: PackageJsonUpdater, private lockFile: AsyncLocker,
25+
private fileWriter: FileWriter, private pkgJsonUpdater: PackageJsonUpdater,
26+
private lockFile: AsyncLocker,
2527
private createTaskCompletedCallback: CreateTaskCompletedCallback) {}
2628

2729
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, _createCompileFn: CreateCompileFn):
@@ -30,8 +32,8 @@ export class ClusterExecutor implements Executor {
3032
this.logger.debug(
3133
`Running ngcc on ${this.constructor.name} (using ${this.workerCount} worker processes).`);
3234
const master = new ClusterMaster(
33-
this.workerCount, this.fileSystem, this.logger, this.pkgJsonUpdater, analyzeEntryPoints,
34-
this.createTaskCompletedCallback);
35+
this.workerCount, this.fileSystem, this.logger, this.fileWriter, this.pkgJsonUpdater,
36+
analyzeEntryPoints, this.createTaskCompletedCallback);
3537
return master.run();
3638
});
3739
}

packages/compiler-cli/ngcc/src/execution/cluster/master.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as cluster from 'cluster';
1212

1313
import {AbsoluteFsPath, FileSystem} from '../../../../src/ngtsc/file_system';
1414
import {Logger} from '../../logging/logger';
15+
import {FileWriter} from '../../writing/file_writer';
1516
import {PackageJsonUpdater} from '../../writing/package_json_updater';
1617
import {AnalyzeEntryPointsFn} from '../api';
1718
import {CreateTaskCompletedCallback, Task, TaskCompletedCallback, TaskQueue} from '../tasks/api';
@@ -34,7 +35,8 @@ export class ClusterMaster {
3435

3536
constructor(
3637
private maxWorkerCount: number, private fileSystem: FileSystem, private logger: Logger,
37-
private pkgJsonUpdater: PackageJsonUpdater, analyzeEntryPoints: AnalyzeEntryPointsFn,
38+
private fileWriter: FileWriter, private pkgJsonUpdater: PackageJsonUpdater,
39+
analyzeEntryPoints: AnalyzeEntryPointsFn,
3840
createTaskCompletedCallback: CreateTaskCompletedCallback) {
3941
if (!cluster.isMaster) {
4042
throw new Error('Tried to instantiate `ClusterMaster` on a worker process.');
@@ -146,6 +148,7 @@ export class ClusterMaster {
146148

147149
// The worker exited unexpectedly: Determine it's status and take an appropriate action.
148150
const assignment = this.taskAssignments.get(worker.id);
151+
this.taskAssignments.delete(worker.id);
149152

150153
this.logger.warn(
151154
`Worker #${worker.id} exited unexpectedly (code: ${code} | signal: ${signal}).\n` +
@@ -158,16 +161,34 @@ export class ClusterMaster {
158161
// The crashed worker process was not in the middle of a task:
159162
// Just spawn another process.
160163
this.logger.debug(`Spawning another worker process to replace #${worker.id}...`);
161-
this.taskAssignments.delete(worker.id);
162164
cluster.fork();
163165
} else {
166+
const {task, files} = assignment;
167+
168+
if (files != null) {
169+
// The crashed worker process was in the middle of writing transformed files:
170+
// Revert any changes before re-processing the task.
171+
this.logger.debug(`Reverting ${files.length} transformed files...`);
172+
this.fileWriter.revertBundle(
173+
task.entryPoint, files, task.formatPropertiesToMarkAsProcessed);
174+
}
175+
164176
// The crashed worker process was in the middle of a task:
165-
// Impossible to know whether we can recover (without ending up with a corrupted entry-point).
166-
// TODO: Use `assignment.files` to revert any changes and rerun the task worker.
167-
const currentTask = assignment.task;
168-
throw new Error(
169-
'Process unexpectedly crashed, while processing format property ' +
170-
`${currentTask.formatProperty} for entry-point '${currentTask.entryPoint.path}'.`);
177+
// Re-add the task back to the queue.
178+
this.taskQueue.markAsUnprocessed(task);
179+
180+
// The crashing might be a result of increased memory consumption by ngcc.
181+
// Do not spawn another process, unless this was the last worker process.
182+
const spawnedWorkerCount = Object.keys(cluster.workers).length;
183+
if (spawnedWorkerCount > 0) {
184+
this.logger.debug(`Not spawning another worker process to replace #${
185+
worker.id}. Continuing with ${spawnedWorkerCount} workers...`);
186+
this.maybeDistributeWork();
187+
} else {
188+
this.logger.debug(`Spawning another worker process to replace #${worker.id}...`);
189+
this.remainingRespawnAttempts--;
190+
cluster.fork();
191+
}
171192
}
172193
}
173194

packages/compiler-cli/ngcc/src/execution/cluster/worker.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,23 @@ if (require.main === module) {
2828

2929
try {
3030
const {
31-
createNewEntryPointFormats = false,
32-
logger = new ConsoleLogger(LogLevel.info),
31+
logger,
3332
pathMappings,
34-
errorOnFailedEntryPoint = false,
35-
enableI18nLegacyMessageIdFormat = true,
33+
enableI18nLegacyMessageIdFormat,
3634
fileSystem,
37-
tsConfig
35+
tsConfig,
36+
getFileWriter,
3837
} = getSharedSetup(parseCommandLineOptions(process.argv.slice(2)));
3938

4039
// NOTE: To avoid file corruption, `ngcc` invocation only creates _one_ instance of
4140
// `PackageJsonUpdater` that actually writes to disk (across all processes).
4241
// In cluster workers we use a `PackageJsonUpdater` that delegates to the cluster master.
4342
const pkgJsonUpdater = new ClusterWorkerPackageJsonUpdater();
43+
const fileWriter = getFileWriter(pkgJsonUpdater);
4444

4545
// The function for creating the `compile()` function.
4646
const createCompileFn = getCreateCompileFn(
47-
fileSystem, logger, pkgJsonUpdater, createNewEntryPointFormats, errorOnFailedEntryPoint,
48-
enableI18nLegacyMessageIdFormat, tsConfig, pathMappings);
47+
fileSystem, logger, fileWriter, enableI18nLegacyMessageIdFormat, tsConfig, pathMappings);
4948

5049
await startWorker(logger, createCompileFn);
5150
process.exitCode = 0;

packages/compiler-cli/ngcc/src/execution/create_compile_function.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ import {PathMappings} from '../ngcc_options';
1616
import {getEntryPointFormat} from '../packages/entry_point';
1717
import {makeEntryPointBundle} from '../packages/entry_point_bundle';
1818
import {FileWriter} from '../writing/file_writer';
19-
import {InPlaceFileWriter} from '../writing/in_place_file_writer';
20-
import {NewEntryPointFileWriter} from '../writing/new_entry_point_file_writer';
21-
import {PackageJsonUpdater} from '../writing/package_json_updater';
2219

2320
import {CreateCompileFn} from './api';
2421
import {Task, TaskProcessingOutcome} from './tasks/api';
@@ -27,13 +24,10 @@ import {Task, TaskProcessingOutcome} from './tasks/api';
2724
* The function for creating the `compile()` function.
2825
*/
2926
export function getCreateCompileFn(
30-
fileSystem: FileSystem, logger: Logger, pkgJsonUpdater: PackageJsonUpdater,
31-
createNewEntryPointFormats: boolean, errorOnFailedEntryPoint: boolean,
27+
fileSystem: FileSystem, logger: Logger, fileWriter: FileWriter,
3228
enableI18nLegacyMessageIdFormat: boolean, tsConfig: ParsedConfiguration|null,
3329
pathMappings: PathMappings|undefined): CreateCompileFn {
3430
return (beforeWritingFiles, onTaskCompleted) => {
35-
const fileWriter = getFileWriter(
36-
fileSystem, logger, pkgJsonUpdater, createNewEntryPointFormats, errorOnFailedEntryPoint);
3731
const {Transformer} = require('../packages/transformer');
3832
const transformer = new Transformer(fileSystem, logger, tsConfig);
3933

@@ -91,11 +85,3 @@ export function getCreateCompileFn(
9185
};
9286
};
9387
}
94-
95-
function getFileWriter(
96-
fs: FileSystem, logger: Logger, pkgJsonUpdater: PackageJsonUpdater,
97-
createNewEntryPointFormats: boolean, errorOnFailedEntryPoint: boolean): FileWriter {
98-
return createNewEntryPointFormats ?
99-
new NewEntryPointFileWriter(fs, logger, errorOnFailedEntryPoint, pkgJsonUpdater) :
100-
new InPlaceFileWriter(fs, logger, errorOnFailedEntryPoint);
101-
}

packages/compiler-cli/ngcc/src/main.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {AsyncNgccOptions, getSharedSetup, NgccOptions, PathMappings, SyncNgccOpt
3636
import {NgccConfiguration} from './packages/configuration';
3737
import {EntryPointJsonProperty, SUPPORTED_FORMAT_PROPERTIES} from './packages/entry_point';
3838
import {EntryPointManifest, InvalidatingEntryPointManifest} from './packages/entry_point_manifest';
39+
import {FileWriter} from './writing/file_writer';
3940
import {DirectPackageJsonUpdater, PackageJsonUpdater} from './writing/package_json_updater';
4041

4142
/**
@@ -54,7 +55,6 @@ export function mainNgcc(options: NgccOptions): void|Promise<void> {
5455
targetEntryPointPath,
5556
propertiesToConsider,
5657
compileAllFormats,
57-
createNewEntryPointFormats,
5858
logger,
5959
pathMappings,
6060
async,
@@ -64,7 +64,8 @@ export function mainNgcc(options: NgccOptions): void|Promise<void> {
6464
fileSystem,
6565
absBasePath,
6666
projectPath,
67-
tsConfig
67+
tsConfig,
68+
getFileWriter,
6869
} = getSharedSetup(options);
6970

7071
const config = new NgccConfiguration(fileSystem, projectPath);
@@ -95,19 +96,20 @@ export function mainNgcc(options: NgccOptions): void|Promise<void> {
9596
logger, finder, fileSystem, supportedPropertiesToConsider, compileAllFormats,
9697
propertiesToConsider, inParallel);
9798

98-
// Create an updater that will actually write to disk. In
99+
// Create an updater that will actually write to disk.
99100
const pkgJsonUpdater = new DirectPackageJsonUpdater(fileSystem);
101+
const fileWriter = getFileWriter(pkgJsonUpdater);
100102

101103
// The function for creating the `compile()` function.
102104
const createCompileFn = getCreateCompileFn(
103-
fileSystem, logger, pkgJsonUpdater, createNewEntryPointFormats, errorOnFailedEntryPoint,
104-
enableI18nLegacyMessageIdFormat, tsConfig, pathMappings);
105+
fileSystem, logger, fileWriter, enableI18nLegacyMessageIdFormat, tsConfig, pathMappings);
105106

106107
// The executor for actually planning and getting the work done.
107108
const createTaskCompletedCallback =
108109
getCreateTaskCompletedCallback(pkgJsonUpdater, errorOnFailedEntryPoint, logger, fileSystem);
109110
const executor = getExecutor(
110-
async, inParallel, logger, pkgJsonUpdater, fileSystem, createTaskCompletedCallback);
111+
async, inParallel, logger, fileWriter, pkgJsonUpdater, fileSystem,
112+
createTaskCompletedCallback);
111113

112114
return executor.execute(analyzeEntryPoints, createCompileFn);
113115
}
@@ -146,8 +148,9 @@ function getCreateTaskCompletedCallback(
146148
}
147149

148150
function getExecutor(
149-
async: boolean, inParallel: boolean, logger: Logger, pkgJsonUpdater: PackageJsonUpdater,
150-
fileSystem: FileSystem, createTaskCompletedCallback: CreateTaskCompletedCallback): Executor {
151+
async: boolean, inParallel: boolean, logger: Logger, fileWriter: FileWriter,
152+
pkgJsonUpdater: PackageJsonUpdater, fileSystem: FileSystem,
153+
createTaskCompletedCallback: CreateTaskCompletedCallback): Executor {
151154
const lockFile = new LockFileWithChildProcess(fileSystem, logger);
152155
if (async) {
153156
// Execute asynchronously (either serially or in parallel)
@@ -156,7 +159,8 @@ function getExecutor(
156159
// Execute in parallel. Use up to 8 CPU cores for workers, always reserving one for master.
157160
const workerCount = Math.min(8, os.cpus().length - 1);
158161
return new ClusterExecutor(
159-
workerCount, fileSystem, logger, pkgJsonUpdater, locker, createTaskCompletedCallback);
162+
workerCount, fileSystem, logger, fileWriter, pkgJsonUpdater, locker,
163+
createTaskCompletedCallback);
160164
} else {
161165
// Execute serially, on a single thread (async).
162166
return new SingleProcessExecutorAsync(logger, locker, createTaskCompletedCallback);

packages/compiler-cli/ngcc/src/ngcc_options.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import {ParsedConfiguration, readConfiguration} from '../../src/perform_compile'
1111
import {ConsoleLogger} from './logging/console_logger';
1212
import {Logger, LogLevel} from './logging/logger';
1313
import {SUPPORTED_FORMAT_PROPERTIES} from './packages/entry_point';
14+
import {FileWriter} from './writing/file_writer';
15+
import {InPlaceFileWriter} from './writing/in_place_file_writer';
16+
import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer';
17+
import {PackageJsonUpdater} from './writing/package_json_updater';
1418

1519
/**
1620
* The options to configure the ngcc compiler for synchronous execution.
@@ -156,6 +160,7 @@ export type OptionalNgccOptions = Pick<NgccOptions, OptionalNgccOptionKeys>;
156160
export type SharedSetup = {
157161
fileSystem: FileSystem; absBasePath: AbsoluteFsPath; projectPath: AbsoluteFsPath;
158162
tsConfig: ParsedConfiguration | null;
163+
getFileWriter(pkgJsonUpdater: PackageJsonUpdater): FileWriter;
159164
};
160165

161166
/**
@@ -210,5 +215,8 @@ export function getSharedSetup(options: NgccOptions): SharedSetup&RequiredNgccOp
210215
absBasePath,
211216
projectPath,
212217
tsConfig,
218+
getFileWriter: (pkgJsonUpdater: PackageJsonUpdater) => createNewEntryPointFormats ?
219+
new NewEntryPointFileWriter(fileSystem, logger, errorOnFailedEntryPoint, pkgJsonUpdater) :
220+
new InPlaceFileWriter(fileSystem, logger, errorOnFailedEntryPoint),
213221
};
214222
}

packages/compiler-cli/ngcc/test/execution/cluster/executor_spec.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {MockFileSystemNative, runInEachFileSystem} from '../../../../src/ngtsc/f
1515
import {ClusterExecutor} from '../../../src/execution/cluster/executor';
1616
import {ClusterMaster} from '../../../src/execution/cluster/master';
1717
import {AsyncLocker} from '../../../src/locking/async_locker';
18+
import {FileWriter} from '../../../src/writing/file_writer';
1819
import {PackageJsonUpdater} from '../../../src/writing/package_json_updater';
1920
import {MockLockFile} from '../../helpers/mock_lock_file';
2021
import {MockLogger} from '../../helpers/mock_logger';
@@ -41,8 +42,8 @@ runInEachFileSystem(() => {
4142
mockLockFile = new MockLockFile(new MockFileSystemNative(), lockFileLog);
4243
locker = new AsyncLocker(mockLockFile, mockLogger, 200, 2);
4344
executor = new ClusterExecutor(
44-
42, getFileSystem(), mockLogger, null as unknown as PackageJsonUpdater, locker,
45-
createTaskCompletedCallback);
45+
42, getFileSystem(), mockLogger, null as unknown as FileWriter,
46+
null as unknown as PackageJsonUpdater, locker, createTaskCompletedCallback);
4647
});
4748

4849
describe('execute()', () => {
@@ -98,8 +99,8 @@ runInEachFileSystem(() => {
9899
});
99100

100101
executor = new ClusterExecutor(
101-
42, getFileSystem(), mockLogger, null as unknown as PackageJsonUpdater, locker,
102-
createTaskCompletedCallback);
102+
42, getFileSystem(), mockLogger, null as unknown as FileWriter,
103+
null as unknown as PackageJsonUpdater, locker, createTaskCompletedCallback);
103104
let error = '';
104105
try {
105106
await executor.execute(anyFn, anyFn);
@@ -118,8 +119,8 @@ runInEachFileSystem(() => {
118119
});
119120

120121
executor = new ClusterExecutor(
121-
42, getFileSystem(), mockLogger, null as unknown as PackageJsonUpdater, locker,
122-
createTaskCompletedCallback);
122+
42, getFileSystem(), mockLogger, null as unknown as FileWriter,
123+
null as unknown as PackageJsonUpdater, locker, createTaskCompletedCallback);
123124
let error = '';
124125
try {
125126
await executor.execute(anyFn, anyFn);

0 commit comments

Comments
 (0)