Graph

Record the dependency relationships and operational results of nodes in the system

Graph is dagrs's main body. 'Graph is a network that satisfies FBP logic, provides node dependencies, and runs all of its nodes completely asynchronously.

Graph's structure

pub struct Graph {
    /// Define the Net struct that holds all nodes
    nodes: HashMap<NodeId, Arc<Mutex<dyn Node>>>,
    /// Store a task's running result.Execution results will be read
    /// and written asynchronously by several threads.
    execute_states: HashMap<NodeId, Arc<ExecState>>,
    /// Count all the nodes
    node_count: usize,
    /// Global environment variables for this Net job.
    /// It should be set before the Net job runs.
    env: Arc<EnvVar>,
    /// Mark whether the net task can continue to execute.
    /// When an error occurs during the execution of any task, This flag will still be 			set to true
    is_active: Arc<AtomicBool>,
    /// Node's in_degree, used for check loop
    in_degree: HashMap<NodeId, usize>,
}

The above fields provide information such as the storage of node operation results, node information contained in the graph, and graph operation status.

Create a Graph

A Graph contains multiple nodes, which can be added as long as they implement the [Node] trait.

add node

Firstly, it is necessary to initialize a node that implements the Nodetrait. Let's use DefaultNode as an example.

use dagrs::NodeTable;
let mut node_table = NodeTable::new();
let node_name = "Node X";
let node = DefaultNode::new(NodeName::from(node_name), &mut node_table);
let node1_name = "Node Y";
let node1 = DefaultNode::with_action(
            NodeName::from(node1_name),
            HelloAction::new(),
            &mut node_table,
);

After calling the new function to create a default Graph, it is necessary to add nodes to the Graph, Graphprovides the add_node function to add any node that implements the Node trait.

let mut graph = Graph::new();
graph.add_node(node);

add edge between nodes

Once the nodes are added to the Graph, it is necessary to establish relationships between them by adding edges. The Graph provides the add_edge function to define a directed edge between two nodes, where the source node is connected to one or more target nodes.

For example, after initializing and adding two nodes (node and node1) to the Graph, you can establish an edge from node to node1 as follows:

let node_id = node.id();
let node1_id = node1.id();
graph.add_edge(node_id, vec![node1_id]);

Here, node.id() and node1.id() retrieve the unique identifiers of the nodes node and node1, respectively. The add_edge function is called with the node_id as the source node and a vector containing node1_id as the target node(s). This creates a directed edge from node to node1.

If you need to connect a node to multiple target nodes, you can include all target node IDs in the vector, like this:

let target_nodes = vec![node1_id, node2_id, node3_id];
graph.add_edge(node_id, target_nodes);

This flexibility allows you to construct complex directed graphs by defining edges between various nodes.

Execute a Graph and Retrieve Execution Results

After adding nodes and edges to the Graph, it is ready to be executed. The Graph provides the start function to begin the execution process, ensuring that all nodes are processed in the correct order based on their dependencies. Once execution is complete, you can retrieve the results produced by individual nodes.

For example, to start a Graph and obtain the execution result of a specific node (node1), you can perform the following steps:

graph.start();
let out = graph.execute_states[&node1_id].get_output().unwrap();
let out: &String = out.get().unwrap();
assert_eq!(out, "Hello world");
  1. Start the Graph: The start function begins the execution of the Graph, processing all nodes in a manner that respects their dependencies and actions.
  2. Access Execution State: After execution, the execute_states field of the Graph contains the state of each node. The state for a specific node can be accessed using its unique identifier (node1_id in this case).
  3. Retrieve the Node Output:
    • Call get_output() on the node's execution state to obtain its output, which is wrapped in an Option.
    • Use unwrap() to extract the actual output value if it exists.
  4. Validate the Output: In this example, the output is expected to be of type String, so it is further extracted using get(). The result can then be validated with assertions, such as checking if the output equals "Hello world".

This process ensures that the Graph is executed properly, and the output of individual nodes can be reliably retrieved and verified.