🐛 Ensure Task.halt() does not resolve before resource cleanup finishes#1155
🐛 Ensure Task.halt() does not resolve before resource cleanup finishes#1155
Conversation
When a task's operation completes and encapsulate enters its finally block to halt child tasks, a concurrent task.halt() call could issue a .return() that cuts across the in-progress cleanup, causing halt() to resolve while resource finalizers are still running. Add a `settling` flag to Delimiter that encapsulate sets when entering cleanup. While settling, exit() skips the .return() call—the operation is already winding down naturally. close() now also waits for the delimiter future when the outcome is set but not yet finalized. Fixes #1153
commit: |
|
This PR fixes Minimal repro (only uses import { race, resource, withResolvers } from "effection";
yield* race([
withResolvers<void>().operation, // never resolves
(function*() {
yield* resource<number>(function*(provide) {
yield* provide(42);
});
})(),
]);
console.log("race returned"); // never prints with this PR appliedOn alpha.7 without the patch this returns immediately. With the patch applied it hangs indefinitely. Likely cause: the new branch added to else if (!this.finalized) {
yield* this.close();
}When |
Motivation
Task.halt()can resolve while async resource cleanup is still running. Code that usesawait task.halt()as a join point can proceed while resource finalizers are still suspended, producing leaked work and use-after-close races.Fixes #1153
Approach
Add a
settlingflag toDelimiterthatencapsulatesets when entering its finally block (beforegroup.halt()). While settling,exit()skips the.return()call since the operation is already winding down naturally.close()now also waits for the delimiter future when the outcome is set but not yet finalized.The flag is reset after
group.halt()completes so that innerencapsulatecalls (e.g.callcc) don't permanently poison the parent delimiter.Possible Drawbacks or Risks
This is a spike — the
settlingflag adds a new state bit toDelimiterand a coupling betweenencapsulateandDelimiterviaDelimiterContext. A future refactor could internalize this into the Delimiter itself.TODOs and Open Questions
settlingshould live inside the Delimiter's own[Symbol.iterator]rather than being set externally byencapsulate