mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
Let a failed application teardown be retried instead of repeating its rejection
This commit is contained in:
1 parent
0c190c3a15
commit
dd040b4f87
2 files changed
+46
-5
No files matched your search
+18
-5
@@ -38,6 +38,7 @@ export function createApplication({
|
|||||||
let state = Object.freeze({ status: 'created', phase: null });
|
let state = Object.freeze({ status: 'created', phase: null });
|
||||||
let startPromise;
|
let startPromise;
|
||||||
let destroyPromise;
|
let destroyPromise;
|
||||||
|
let destroyRequested = false;
|
||||||
let cleanupPromise;
|
let cleanupPromise;
|
||||||
|
|
||||||
function publish(status, phase = null) {
|
function publish(status, phase = null) {
|
||||||
@@ -51,21 +52,29 @@ export function createApplication({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A release that fails stays registered and the failed attempt is not
|
||||||
|
// remembered, so a later destroy() retries exactly the failed releases.
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
cleanupPromise ||= Promise.resolve().then(async () => {
|
cleanupPromise ||= Promise.resolve().then(async () => {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
for (const phase of STOP_ORDER) {
|
for (const phase of STOP_ORDER) {
|
||||||
|
const failed = [];
|
||||||
while (cleanups[phase].length) {
|
while (cleanups[phase].length) {
|
||||||
|
const dispose = cleanups[phase].pop();
|
||||||
try {
|
try {
|
||||||
await cleanups[phase].pop()();
|
await dispose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push(error);
|
errors.push(error);
|
||||||
|
failed.unshift(dispose);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cleanups[phase].push(...failed);
|
||||||
delete components[phase];
|
delete components[phase];
|
||||||
}
|
}
|
||||||
if (errors.length)
|
if (errors.length) {
|
||||||
|
cleanupPromise = null;
|
||||||
throw new AggregateError(errors, 'Application cleanup failed');
|
throw new AggregateError(errors, 'Application cleanup failed');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return cleanupPromise;
|
return cleanupPromise;
|
||||||
}
|
}
|
||||||
@@ -116,7 +125,7 @@ export function createApplication({
|
|||||||
|
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
start() {
|
start() {
|
||||||
if (destroyPromise)
|
if (destroyRequested)
|
||||||
return Promise.reject(new Error('Application has been destroyed'));
|
return Promise.reject(new Error('Application has been destroyed'));
|
||||||
// Assign before notifying subscribers, including reentrant start/destroy.
|
// Assign before notifying subscribers, including reentrant start/destroy.
|
||||||
startPromise ||= Promise.resolve().then(initialize);
|
startPromise ||= Promise.resolve().then(initialize);
|
||||||
@@ -124,21 +133,25 @@ export function createApplication({
|
|||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
if (destroyPromise) return destroyPromise;
|
if (destroyPromise) return destroyPromise;
|
||||||
destroyPromise = Promise.resolve().then(async () => {
|
destroyRequested = true;
|
||||||
|
const attempt = Promise.resolve().then(async () => {
|
||||||
try {
|
try {
|
||||||
await startPromise?.catch(() => {});
|
await startPromise?.catch(() => {});
|
||||||
await cleanup();
|
await cleanup();
|
||||||
publish('destroyed');
|
publish('destroyed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// A failed teardown can be retried by calling destroy() again.
|
||||||
|
if (destroyPromise === attempt) destroyPromise = null;
|
||||||
publish('failed');
|
publish('failed');
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
listeners.clear();
|
listeners.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
destroyPromise = attempt;
|
||||||
controller.abort();
|
controller.abort();
|
||||||
publish('destroying');
|
publish('destroying');
|
||||||
return destroyPromise;
|
return attempt;
|
||||||
},
|
},
|
||||||
getState: () => state,
|
getState: () => state,
|
||||||
getComponents: () => Object.freeze({ ...components }),
|
getComponents: () => Object.freeze({ ...components }),
|
||||||
|
|||||||
@@ -197,6 +197,34 @@ test('cleanup errors are reported after all remaining resources are attempted',
|
|||||||
assert.equal(app.getState().status, 'failed');
|
assert.equal(app.getState().status, 'failed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a failed teardown can be retried and repeats only the failed release', async () => {
|
||||||
|
let failures = 1;
|
||||||
|
const { app, events } = fixture({
|
||||||
|
createData({ defer }) {
|
||||||
|
defer(() => events.push('data:released'));
|
||||||
|
defer(() => {
|
||||||
|
events.push('data:detach');
|
||||||
|
if (failures-- > 0) throw new Error('detach failed');
|
||||||
|
});
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.start();
|
||||||
|
await assert.rejects(app.destroy(), AggregateError);
|
||||||
|
assert.equal(app.getState().status, 'failed');
|
||||||
|
const firstAttempt = events.splice(0);
|
||||||
|
assert.deepEqual(firstAttempt.slice(-4), [
|
||||||
|
'stop:Controls',
|
||||||
|
'data:detach',
|
||||||
|
'data:released',
|
||||||
|
'stop:Scene',
|
||||||
|
]);
|
||||||
|
await app.destroy();
|
||||||
|
assert.deepEqual(events, ['data:detach']);
|
||||||
|
assert.equal(app.getState().status, 'destroyed');
|
||||||
|
await assert.rejects(app.start(), /destroyed/);
|
||||||
|
});
|
||||||
|
|
||||||
test('subscribers can destroy during startup without allowing the next constructor', async () => {
|
test('subscribers can destroy during startup without allowing the next constructor', async () => {
|
||||||
const { app, events } = fixture();
|
const { app, events } = fixture();
|
||||||
app.subscribe((state) => {
|
app.subscribe((state) => {
|
||||||
|
|||||||
Reference in new issue
Block a user