Skip to content

Commit f89cf4a

Browse files
committed
feat(core): cache task failures
1 parent 2319dc3 commit f89cf4a

File tree

3 files changed

+87
-20
lines changed

3 files changed

+87
-20
lines changed

e2e/workspace/src/workspace.test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,30 @@ describe('cache', () => {
675675
`${myapp2}-e2e`,
676676
]);
677677

678+
// cache task failures
679+
// --------------------------------------------
680+
updateFile('workspace.json', (c) => {
681+
const workspaceJson = JSON.parse(c);
682+
workspaceJson.projects[myapp1].targets.lint = {
683+
executor: '@nrwl/workspace:run-commands',
684+
options: {
685+
command: 'echo hi && exit 1',
686+
},
687+
};
688+
return JSON.stringify(workspaceJson, null, 2);
689+
});
690+
const failingRun = runCLI(`lint ${myapp1}`, {
691+
silenceError: true,
692+
env: { ...process.env, NX_CACHE_FAILURES: 'true' },
693+
});
694+
expect(failingRun).not.toContain('[retrieved from cache]');
695+
696+
const cachedFailingRun = runCLI(`lint ${myapp1}`, {
697+
silenceError: true,
698+
env: { ...process.env, NX_CACHE_FAILURES: 'true' },
699+
});
700+
expect(cachedFailingRun).toContain('[retrieved from cache]');
701+
678702
// run without caching
679703
// --------------------------------------------
680704

packages/workspace/src/tasks-runner/cache.ts

+30-6
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import { DefaultTasksRunnerOptions } from './default-tasks-runner';
1313
import { spawn } from 'child_process';
1414
import { cacheDirectory } from '../utilities/cache-directory';
1515

16-
export type CachedResult = { terminalOutput: string; outputsPath: string };
16+
export type CachedResult = {
17+
terminalOutput: string;
18+
outputsPath: string;
19+
code: number;
20+
};
1721
export type TaskWithCachedResult = { task: Task; cachedResult: CachedResult };
1822

1923
class CacheConfig {
@@ -83,8 +87,12 @@ export class Cache {
8387
}
8488
}
8589

86-
async put(task: Task, terminalOutputPath: string, outputs: string[]) {
87-
const terminalOutput = readFileSync(terminalOutputPath).toString();
90+
async put(
91+
task: Task,
92+
terminalOutput: string | null,
93+
outputs: string[],
94+
code: number
95+
) {
8896
const td = join(this.cachePath, task.hash);
8997
const tdCommit = join(this.cachePath, `${task.hash}.commit`);
9098

@@ -97,7 +105,10 @@ export class Cache {
97105
}
98106

99107
mkdirSync(td);
100-
writeFileSync(join(td, 'terminalOutput'), terminalOutput);
108+
writeFileSync(
109+
join(td, 'terminalOutput'),
110+
terminalOutput ?? 'no terminal output'
111+
);
101112

102113
mkdirSync(join(td, 'outputs'));
103114
outputs.forEach((f) => {
@@ -116,14 +127,19 @@ export class Cache {
116127
// creating this file is atomic, whereas creating a folder is not.
117128
// so if the process gets terminated while we are copying stuff into cache,
118129
// the cache entry won't be used.
130+
writeFileSync(join(td, 'code'), code.toString());
119131
writeFileSync(tdCommit, 'true');
120132

121133
if (this.options.remoteCache) {
122134
await this.options.remoteCache.store(task.hash, this.cachePath);
123135
}
124136
}
125137

126-
copyFilesFromCache(cachedResult: CachedResult, outputs: string[]) {
138+
copyFilesFromCache(
139+
hash: string,
140+
cachedResult: CachedResult,
141+
outputs: string[]
142+
) {
127143
outputs.forEach((f) => {
128144
const cached = join(cachedResult.outputsPath, f);
129145
if (existsSync(cached)) {
@@ -153,9 +169,17 @@ export class Cache {
153169
const td = join(this.cachePath, task.hash);
154170

155171
if (existsSync(tdCommit)) {
172+
const terminalOutput = readFileSync(
173+
join(td, 'terminalOutput')
174+
).toString();
175+
let code = 0;
176+
try {
177+
code = Number(readFileSync(join(td, 'code')).toString());
178+
} catch (e) {}
156179
return {
157-
terminalOutput: readFileSync(join(td, 'terminalOutput')).toString(),
180+
terminalOutput,
158181
outputsPath: join(td, 'outputs'),
182+
code,
159183
};
160184
} else {
161185
return null;

packages/workspace/src/tasks-runner/task-orchestrator.ts

+33-14
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,16 @@ export class TaskOrchestrator {
125125
}
126126

127127
const outputs = getOutputs(this.projectGraph.nodes, t.task);
128-
this.cache.copyFilesFromCache(t.cachedResult, outputs);
128+
this.cache.copyFilesFromCache(t.task.hash, t.cachedResult, outputs);
129129

130-
this.options.lifeCycle.endTask(t.task, 0);
130+
this.options.lifeCycle.endTask(t.task, t.cachedResult.code);
131131
});
132132

133-
return tasks.reduce((m, c) => {
133+
return tasks.reduce((m, t) => {
134134
m.push({
135-
task: c.task,
135+
task: t.task,
136136
type: AffectedEventType.TaskCacheRead,
137-
success: true,
137+
success: t.cachedResult.code === 0,
138138
});
139139
return m;
140140
}, []);
@@ -205,10 +205,11 @@ export class TaskOrchestrator {
205205
process.stdout.write(outWithErr.join(''));
206206
}
207207
if (outputPath) {
208-
fs.writeFileSync(outputPath, outWithErr.join(''));
209-
if (code === 0) {
208+
const terminalOutput = outWithErr.join('');
209+
fs.writeFileSync(outputPath, terminalOutput);
210+
if (this.shouldCacheTask(outputPath, code)) {
210211
this.cache
211-
.put(task, outputPath, taskOutputs)
212+
.put(task, terminalOutput, taskOutputs, code)
212213
.then(() => {
213214
this.options.lifeCycle.endTask(task, code);
214215
res(code);
@@ -259,21 +260,22 @@ export class TaskOrchestrator {
259260
p.on('exit', (code) => {
260261
if (code === null) code = 2;
261262
// we didn't print any output as we were running the command
262-
// print all the collected output|
263+
// print all the collected output
263264
if (!forwardOutput) {
264265
output.logCommand(commandLine);
265-
try {
266-
process.stdout.write(fs.readFileSync(outputPath));
267-
} catch (e) {
266+
const terminalOutput = this.readTerminalOutput(outputPath);
267+
if (terminalOutput) {
268+
process.stdout.write(terminalOutput);
269+
} else {
268270
console.error(
269271
`Nx could not find process's output. Run the command without --parallel.`
270272
);
271273
}
272274
}
273275
// we don't have to worry about this statement. code === 0 guarantees the file is there.
274-
if (outputPath && code === 0) {
276+
if (this.shouldCacheTask(outputPath, code)) {
275277
this.cache
276-
.put(task, outputPath, taskOutputs)
278+
.put(task, this.readTerminalOutput(outputPath), taskOutputs, code)
277279
.then(() => {
278280
this.options.lifeCycle.endTask(task, code);
279281
res(code);
@@ -293,6 +295,23 @@ export class TaskOrchestrator {
293295
});
294296
}
295297

298+
private readTerminalOutput(outputPath: string) {
299+
try {
300+
return fs.readFileSync(outputPath).toString();
301+
} catch (e) {
302+
return null;
303+
}
304+
}
305+
306+
private shouldCacheTask(outputPath: string | null, code: number) {
307+
// TODO: vsavkin make caching failures the default in Nx 12.1
308+
if (process.env.NX_CACHE_FAILURES == 'true') {
309+
return outputPath;
310+
} else {
311+
return outputPath && code === 0;
312+
}
313+
}
314+
296315
private envForForkedProcess(
297316
task: Task,
298317
outputPath: string,

0 commit comments

Comments
 (0)