Skip to content

Design choice question #974

Closed
Closed

Description

@davidbrochart

If I understand correctly, in Effection child tasks cannot outlive their parent but their lifetime doesn't really affect their parent. For instance:

import { run, sleep, spawn } from "@effection/effection";

run(function* op() {
  let task1 = yield* spawn(() => sleep(1000));
  let task2 = yield* spawn(() => sleep(2000));
  // yield* task1;
  // yield* task2;
  console.log('done');
});

Will print done immediatly.
This is different than Trio, where all tasks in a nursery (or task group) must finish before the nursery's context manager exits.
Maybe I am missing something, but otherwise I was wondering about the reasons behind this design choice.

Activity

  1. cowboyd commented on Feb 14, 2025

    @cowboyd
    Member

    in Effection child tasks cannot outlive their parent but their lifetime doesn't really affect their parent.

    That is mostly correct, however child tasks do affect the lifetime of their parent insofar as they must exit completely before the parent can exit. The following example derived from yours above will not sleep for a full2000ms, however it will delay 500ms before exiting:

    import { run, sleep, spawn } from "@effection/effection";
    
    run(function* op() {
      yield* spawn(function*() {
        try {
          yield* sleep(2000);
        } finally {
          yield* sleep(500);
        }
      });
      console.log('done');
    });

    It will print "done" immediately, but it cannot exit until the finally block is fully run which includes a 500ms delay.

    This is different than Trio, where all tasks in a nursery (or task group) must finish before the nursery's context manager exits.
    Maybe I am missing something, but otherwise I was wondering about the reasons behind this design choice.

    No, you're not missing anything. In fact, the first version of Effection was loosely based on Trio and it mimicked the same behavior regarding task lifetime. However, we found ourselves still having to spend a lot of thought cycles on explicitly managing children. In our specific case, we were spinning up several operating system processes to act as servers, bundlers, and general workers. When it came time to return from the parent computation (usually because the user hit CTRL-C), we still found ourselves having to do too much management of task cancellation by hand.

    It was actually @jnicklas that had the insight that you really only begin a concurrent operation for one of two reasons: Either to produce some persistent side-effect, or else to consume its outcome directly; usually in the form of a value. In the case of the former, there is no need to keep a persistent side effect around once you return from an operation. We found that by definition, there was almost never a time that we wanted to keep things like sub resources and sub processes around, and when there was, it was elegantly handled by spawning in a parent scope that was explicitly passed in. In the cases where a task was spawned concurrently in order to consume a value, the caller would be required to explicitly depend on that value anyway with something like let result = yield* task and so we found that the actual content and scope of an operation very succinctly captured the appropriate lifetime of its concurrent children.

    We experimented with appending "a child may not outlive its parent" to the original constraint "a parent is not complete until all its children are complete" and we were very happy with the result: it is very rare that you have to ever do any explicit cancellation or management of resources in Effection. Folks new to Effection are very often surprised by how aggressive it is at tearing down its children at first, but it comes to feel very clean and natural after a very short time. After all, to declare that lifetime is based on nothing more than lexical scope is the essence of structured concurrency, no? Perhaps a more philosophical way to put it is in terms of the proverbial tree in the forest: "If a concurrent operation computes, but there is no one there to listen to its value, does it exist?" our answer is no.

    However if you look closely, I do believe that we have preserved, albeit in a slightly modified form, the essential aspect of Trio's guarantee with respect to the lifetimes of its children. As demonstrated in the example above, while Effection will instruct all child tasks to return when a parent task exits, it will still await the outcome of all those returns before it considers itself complete and able to report its result to any callers it might have.

  2. locked and limited conversation to collaborators on Feb 14, 2025
  3. converted this issue into a discussion #975 on Feb 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions