Skip to content

Commit dd1c9cb

Browse files
authored
🐛 fix: move all task finalization into the scope destructor (#1085)
We need to have 1:1 equivalance between a scope closing and a task settling. - when a task errors or completes, its scope must be destroyed - when a task is halted, its scope must be destroyed. By the same token, when scope is destroyed, its task must halt (if there is such a task). With #1081, we established the scope destroy -> task halt relationship, and now we need to establish the task finalized -> scope destroyed relatioship. This moves all of the finalization logic of the task from the actual coroutine running the task into the scope destructor. Then, the coroutine itself _explicitly_ destroys the scope as its last official act. To make this work, we hid need to make Delimiter.close() re-entrant, so that if it is called from mulitple places it is idempotent and has the same result.
1 parent 153e420 commit dd1c9cb

3 files changed

Lines changed: 50 additions & 34 deletions

File tree

lib/delimiter.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,21 @@ export class Delimiter<T>
3636
*close(): Operation<void> {
3737
let done = this.future.operation;
3838
let interrupted = !this.computed;
39+
3940
this.close = function* close() {
4041
let outcome = yield* done;
4142
if (interrupted && outcome.exists && !outcome.value.ok) {
4243
throw outcome.value.error;
4344
}
4445
};
45-
this.interrupt();
46-
yield* this.close();
46+
if (!this.outcome) {
47+
this.interrupt();
48+
yield* this.close();
49+
} else {
50+
if (interrupted && this.outcome.exists && !this.outcome.value.ok) {
51+
throw this.outcome.value.error;
52+
}
53+
}
4754
}
4855

4956
private exit(outcome: Maybe<Result<T>>): void {

lib/task.ts

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,6 @@ export function createTask<T>(options: TaskOptions<T>): NewTask<T> {
2626
let [scope, destroy] = createScopeInternal(owner);
2727
let future = createFuture<T>();
2828

29-
let top = new Delimiter<T>(() => encapsulate(operation));
30-
scope.set(DelimiterContext, top as Delimiter<unknown>);
31-
32-
scope.ensure(function* () {
33-
try {
34-
yield* top.close();
35-
} finally {
36-
future.reject(new Error("halted"));
37-
}
38-
});
39-
4029
let task = Object.defineProperties(future.future, {
4130
halt: {
4231
enumerable: false,
@@ -77,33 +66,43 @@ export function createTask<T>(options: TaskOptions<T>): NewTask<T> {
7766
},
7867
}) as Task<T>;
7968

80-
let boundary = owner.expect(ErrorContext);
81-
82-
scope.set(ErrorContext, top);
69+
let top = new Delimiter<T>(() => encapsulate(operation));
70+
scope.set(DelimiterContext, top as Delimiter<unknown>);
8371

8472
let group = scope.expect(TaskGroupContext);
8573
group.add(task);
8674

75+
let boundary = owner.expect(ErrorContext);
76+
scope.set(ErrorContext, top);
77+
78+
scope.ensure(function* () {
79+
try {
80+
yield* top.close();
81+
} finally {
82+
group.delete(task);
83+
let { outcome } = top;
84+
if (outcome!.exists) {
85+
let result = outcome!.value;
86+
if (result.ok) {
87+
future.resolve(result.value);
88+
} else {
89+
let { error } = result;
90+
future.reject(error);
91+
boundary.raise(error);
92+
}
93+
} else {
94+
future.reject(new Error("halted"));
95+
}
96+
}
97+
});
98+
8799
let routine = createCoroutine({
88100
scope,
89101
*operation() {
90102
try {
91103
yield* top;
92104
} finally {
93-
group.delete(task);
94-
let { outcome } = top;
95-
if (outcome!.exists) {
96-
let result = outcome!.value;
97-
if (result.ok) {
98-
future.resolve(result.value);
99-
} else {
100-
let { error } = result;
101-
future.reject(error);
102-
boundary.raise(error);
103-
}
104-
} else {
105-
future.reject(new Error("halted"));
106-
}
105+
yield* destroy();
107106
}
108107
},
109108
});

test/run.test.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// deno-lint-ignore-file no-unsafe-finally
2-
3-
import { blowUp, createNumber, describe, expect, it } from "./suite.ts";
2+
import assert from "node:assert";
3+
import { Children } from "../lib/contexts.ts";
44
import {
55
action,
66
createScope,
@@ -13,8 +13,7 @@ import {
1313
until,
1414
useScope,
1515
} from "../mod.ts";
16-
import { Children } from "../lib/contexts.ts";
17-
import assert from "node:assert";
16+
import { blowUp, createNumber, describe, expect, it } from "./suite.ts";
1817

1918
describe("run()", () => {
2019
it("can run an operation", async () => {
@@ -335,9 +334,20 @@ describe("run()", () => {
335334
createScope(scope);
336335
yield* suspend();
337336
});
337+
338338
await task.halt();
339339

340340
assert(scope !== undefined);
341341
expect(scope.expect(Children).size).toEqual(0);
342342
});
343+
344+
it("destroys its scope when completing successfully", async () => {
345+
let scope = await run(function* () {
346+
let parent = yield* useScope();
347+
createScope(parent);
348+
return parent;
349+
});
350+
351+
expect(scope.expect(Children).size).toEqual(0);
352+
});
343353
});

0 commit comments

Comments
 (0)