Ownership and cleanup
Draft
Keep one owner for every task and move returned work through successful results.
On this page · 3 sections
One owner, ordinary handles
A task has exactly one runtime owner, independently of how many handles observe it. Handles may be aliased or stored in vectors and products without duplicating ownership.
On successful exit, tasks reachable through the result move to the receiving owner before cleanup begins. Tasks that are not returned are interrupted at their suspension point, drained, and never detached. An error exit has no successful result to promote, so every remaining child stays with the failing owner and is drained there.
type CleanupProbe = {
mut events: Vector(String);
}
let record = fn(probe, event) {
probe.events := Vector.push(probe.events, event);
};
Owner Drains Unreturned Children
let probe: CleanupProbe @ global = CleanupProbe { events = []; };
let (~value: started, ~resolver: start) = OneShot.make();
let (~value: parked, ..) = OneShot.make();
let parent = spawn {
let _child = spawn {
defer record(probe, "child");
OneShot.resolve(start, ());
await parked;
};
await started;
42
};
expect ✓await parent == 42;
expect ✓probe.events == ["child"];Nested cleanup
Descendants drain from the leaves inward. Parent resources remain available until every child has finished cleaning up.
Descendants Clean First
let probe: CleanupProbe @ global = CleanupProbe { events = []; };
let (~value: child_started, ~resolver: start_child) = OneShot.make();
let (~value: descendant_started, ~resolver: start_descendant) = OneShot.make();
let (~value: parked, ..) = OneShot.make();
let parent = spawn {
defer record(probe, "parent");
let _child = spawn {
defer record(probe, "child");
let _descendant = spawn {
defer record(probe, "descendant");
OneShot.resolve(start_descendant, ());
await parked;
};
await descendant_started;
OneShot.resolve(start_child, ());
await parked;
};
await child_started;
7
};
expect ✓await parent == 7;
expect ✓probe.events == ["descendant", "child", "parent"];Provided services
A child sees the services provided where it was spawned, even once its parent provides others.
effect service Journal {
write: fn(entry: String) -> Unit;
}
let journal = (probe: CleanupProbe, name: String) =>
module: Service.Provider(Journal) #{
let service Provider: Journal = {
type t = Unit
init = () => ();
drop = fn(self: t) -> Unit {
record(probe, name ++ " closed");
};
write = fn(self: t, entry: String) -> Unit {
record(probe, name ++ ": " ++ entry);
};
};
};
Children See Where They Were Spawned
let probe: CleanupProbe @ global = CleanupProbe { events = []; };
provide { Journal = journal(probe, "outer"); } {
let early = spawn {
Journal.write("early");
};
provide { Journal = journal(probe, "inner"); } {
let late = spawn {
Journal.write("late");
};
await late;
await early;
};
};
expect ✓probe.events == [
"outer: early",
"inner: late",
"inner closed",
"outer closed",
];Providers are parent resources too. A provide drains the children spawned inside it before its providers drop, so their cleanup can still use them.
Providers Outlive Their Children
let probe: CleanupProbe @ global = CleanupProbe { events = []; };
let (~value: started, ~resolver: start) = OneShot.make();
let (~value: parked, ..) = OneShot.make();
provide { Journal = journal(probe, "journal"); } {
let _child = spawn {
defer Journal.write("child cleaned up");
OneShot.resolve(start, ());
await parked;
};
await started;
};
expect ✓probe.events == ["journal: child cleaned up", "journal closed"];