Skip to content

Commit e9e4bef

Browse files
committed
[WIP][Bugfix] Mid-build Retries should not cause spurious errors.
1 parent 30f9581 commit e9e4bef

7 files changed

+160
-112
lines changed

lib/builder.ts

+34-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import BuilderError from './errors/builder';
88
import NodeSetupError from './errors/node-setup';
99
import BuildError from './errors/build';
1010
import CancelationRequest from './cancelation-request';
11+
import RetryCancellationRequest from './errors/retry-cancelation';
1112
import filterMap from './utils/filter-map';
1213
import { EventEmitter } from 'events';
1314
import { TransformNode, SourceNode, Node } from 'broccoli-node-api';
@@ -53,7 +54,7 @@ class Builder extends EventEmitter {
5354
watchedPaths: string[];
5455
_nodeWrappers: Map<TransformNode | SourceNode, NodeWrappers>;
5556
outputNodeWrapper: NodeWrappers;
56-
_cancelationRequest: any;
57+
_cancelationRequest?: CancelationRequest;
5758
outputPath: string;
5859
buildId: number;
5960
builderTmpDir!: string;
@@ -143,8 +144,12 @@ class Builder extends EventEmitter {
143144
//
144145
// 1. build up a promise chain, which represents the complete build
145146
pipeline = pipeline.then(async () => {
147+
if (this._cancelationRequest) {
148+
this._cancelationRequest.throwIfRequested();
149+
} else {
150+
throw new BuilderError('Broccoli: The current build is missing a CancelationRequest, this is unexpected please report an issue: https://github.com/broccolijs/broccoli/issues/new ');
151+
}
146152
// 3. begin next build step
147-
this._cancelationRequest.throwIfRequested();
148153
this.emit('beginNode', nw);
149154
try {
150155
await nw.build();
@@ -166,17 +171,35 @@ class Builder extends EventEmitter {
166171
// cleanup, or restarting the build itself.
167172
this._cancelationRequest = new CancelationRequest(pipeline);
168173

174+
let skipFinally = false;
169175
try {
170176
await pipeline;
171177
this.buildHeimdallTree(this.outputNodeWrapper);
178+
} catch (e) {
179+
if (RetryCancellationRequest.isRetry(e)) {
180+
this._cancelationRequest = undefined;
181+
await new Promise((resolve, reject) => {
182+
setTimeout(() => {
183+
try {
184+
resolve(this.build());
185+
} catch(e) {
186+
reject(e);
187+
}
188+
}, e.retryIn);
189+
})
190+
skipFinally = true;
191+
} else {
192+
throw e;
193+
}
172194
} finally {
195+
if (skipFinally) { return; }
173196
let buildsSkipped = filterMap(
174197
this._nodeWrappers.values(),
175198
(nw: NodeWrappers) => nw.buildState.built === false
176199
).length;
177200
logger.debug(`Total nodes skipped: ${buildsSkipped} out of ${this._nodeWrappers.size}`);
178201

179-
this._cancelationRequest = null;
202+
this._cancelationRequest = undefined;
180203
}
181204
}
182205

@@ -186,6 +209,14 @@ class Builder extends EventEmitter {
186209
}
187210
}
188211

212+
async retry(retryIn: number) {
213+
if (this._cancelationRequest) {
214+
return this._cancelationRequest.cancel(new RetryCancellationRequest('Retry', retryIn));
215+
} else {
216+
return this.build();
217+
}
218+
}
219+
189220
// Destructor-like method. Waits on current node to finish building, then cleans up temp directories
190221
async cleanup() {
191222
try {

lib/cancelation-request.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import CancelationError from './errors/cancelation';
22

33
export default class CancelationRequest {
4-
_pendingWork: Promise<void>;
5-
_canceling: Promise<void> | null;
4+
private _pendingWork: Promise<void>;
5+
private _canceling: Promise<void> | null;
6+
private _cancelationError = new CancelationError('Build Canceled');
67

78
constructor(pendingWork: Promise<void>) {
89
this._pendingWork = pendingWork; // all
@@ -15,19 +16,23 @@ export default class CancelationRequest {
1516

1617
throwIfRequested() {
1718
if (this.isCanceled) {
18-
throw new CancelationError('Build Canceled');
19+
throw this._cancelationError;
1920
}
2021
}
2122

2223
then() {
2324
return this._pendingWork.then(...arguments);
2425
}
2526

26-
cancel() {
27+
cancel(cancelationError?: CancelationError) {
2728
if (this._canceling) {
2829
return this._canceling;
2930
}
3031

32+
if (cancelationError) {
33+
this._cancelationError = cancelationError;
34+
}
35+
3136
this._canceling = this._pendingWork.catch(e => {
3237
if (CancelationError.isCancelationError(e)) {
3338
return;

lib/errors/retry-cancelation.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Cancelation from './cancelation';
2+
export default class Retry extends Cancelation {
3+
isRetry: boolean = true;
4+
retryIn: Number;
5+
6+
static isRetry(e: any): boolean {
7+
return typeof e === 'object' && e !== null && e.isRetry === true;
8+
}
9+
10+
constructor(message = 'Retry', retryIn: number) {
11+
super(message);
12+
this.retryIn = retryIn;
13+
}
14+
}

lib/middleware.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ export = function getMiddleware(watcher: Watcher, options: MiddlewareOptions = {
141141
const outputPath = path.resolve(watcher.builder.outputPath);
142142

143143
return async function broccoliMiddleware(request: any, response: any, next: any) {
144-
if (watcher.currentBuild == null) {
145-
throw new Error('Waiting for initial build to start');
144+
if (!watcher.currentBuild) {
145+
throw new Error('Broccoli: watcher must have a currentBuild');
146146
}
147147

148148
try {

lib/server.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,12 @@ class Server extends EventEmitter {
8686
this.instance.listen(this._port, this._host);
8787

8888
this.instance.on('listening', () => {
89-
this.ui.writeLine(`Serving on ${this._url}\n`);
90-
resolve(this._watcher.start());
89+
try {
90+
this.ui.writeLine(`Serving on ${this._url}\n`);
91+
this._watcher.start().then(resolve, reject);
92+
} catch(e) {
93+
reject(e);
94+
}
9195
});
9296

9397
this.instance.on('error', (error: any) => {

0 commit comments

Comments
 (0)