[][src]Crate specs_task

Fork-join multitasking for SPECS ECS

Here we expound on the technical details of this module's implementation. For basic usage, see the tests.

In this model, every task is some entity. The entity is allowed to have exactly one component that implements TaskComponent (it may have other components that don't implement TaskComponent). The task will be run to completion by the corresponding TaskRunnerSystem.

Every task entity is also a node in a (hopefully acyclic) directed graph. An edge t2 --> t1 means that t2 cannot start until t1 has completed.

In order for tasks to become unblocked, the TaskManagerSystem must run, whence it will traverse the graph, starting at the "final entities", and check for entities that have completed, potentially unblocking their parents. In order for a task to be run, it must be the descendent of a final entity. Entities become final by calling TaskManager::finalize.

Edges can either come from SingleEdge or MultiEdge components, but you should not use these types directly. You might wonder why we need both. It's a fair question, because adding the SingleEdge concept does not actually make the model capable of representing any semantically new graphs. The reason is efficiency.

If you want to implement a fork join like this (note: time is going left to right but the directed edges are going right to left):

 r#"       ----- t1.1 <---   ----- t2.1 <---
          /               \ /               \
      t0 <------ t1.2 <----<------ t2.2 <---- t3
          \               / \               /
           ----- t1.3 <---   ----- t2.3 <---      "#;

You would actually do this by calling TaskManager::make_fork to create two "fork" entities F1 and F2 that don't have TaskComponents, but they can have both a SingleEdge and a MultiEdge. Note that the children on the MultiEdge are called "prongs" of the fork.

 r#"      single          single          single
      t0 <-------- F1 <-------------- F2 <-------- t3
                   |                  |
          t1.1 <---|          t2.1 <--|
          t1.2 <---| multi    t2.2 <--| multi
          t1.3 <---|          t2.3 <--|            "#;

The semantics would be such that this graph is equivalent to the one above. Before any of the tasks connected to F2 by the MultiEdge could run, the tasks connected by the SingleEdge ({ t0, t1.1, t1.2, t1.3 }) would have to be complete. t3 could only run once all of the descendents of F2 had completed.

The advantages of this scheme are:

  • a traversal of the graph starting from t3 does not visit the same node twice
  • it is a bit easier to create fork-join graphs with larger numbers of concurrent tasks
  • there are fewer edges for the most common use cases

Here's another example with "nested forks" to test your understanding:

r#"   With fork entities:

          t0 <-------------- FA <----- t2
                             |
                      tx <---|
              t1 <--- FB <---|
                       |
              ty <-----|
              tz <-----|

      As time orderings:

          t0   < { t1, tx, ty, tz } < t2
          t1   < { ty, tz }

      Induced graph:

          t0 <------- tx <------- t2
           ^                      |
           |      /------ ty <----|
           |     v                |
           ----- t1 <---- tz <-----          "#;

Every user of this module should use it via the TaskManager. It will enforce certain invariants about the kinds of entities that can be constructed. For example, any entity with a MultiEdge component is considered a "fork entity", and it is not allowed to have a TaskComponent or a TaskProgress. Therefore, if you want a task to have multiple children, it must do so via a fork entity.

These systems must be dispatched for tasks to make progress:

  • TaskManagerSystem
  • TaskRunnerSystem for every T: TaskRunner used

Potential bugs this module won't detect:

  • leaked orphan entities
  • graph cycles
  • finalizing an entity that has children
  • users manually tampering with the TaskProgress, SingleEdge, MultiEdge, or FinalTag components; these should only be used inside this module

Structs

AlreadyJoined

This error means that you tried to join an entity that was already joined (has a SingleEdge).

TaskManager

The main object for users of this module. Used for creating and connecting tasks.

TaskManagerSystem

Traverses all descendents of all finalized entities and unblocks them if possible.

TaskRunnerSystem

The counterpart to an implementation TaskComponent. Runs tasks until completion.

Enums

OnCompletion

What to do to a final task and its descendents when it they complete.

UnexpectedEntity

This error means the entity provided to one of the APIs did not have the expected components.

Traits

TaskComponent

An ephemeral component that needs access to SystemData to run some task. Will be run by the TaskRunnerSystem<T> until run returns true.