Crate ordr

Crate ordr 

Source
Expand description

Ordr is a library that helps you execute and keep track of a set of interdependent functions.

It can create a graph (specifically a DAG) of functions depending on functions, and execute them as they get ready, in parallel.

Here is a simple example taken from one of the examples in the repo:

╭─────> C ────╮
A             ├─> E
╰──> B ──> D ─╯

E is our target, and it depends C and D and so forth. Ordr will thus start executing A, when that’s done, it will execute B and C in parallel with the output of A, once B is done, it’ll start D (with the output of B) and when ready, E will be executed.

If any of the tasks return an error, the running tasks will be aborted and the execution stops and a partial output will be returned.

A job (such as the above) can also be started with already existing data. Say in the above C fails after B and D have completed successfully, we can then run it again with the A, B, and D data, which will result in only C and then E being run.

The letters in the graph, we call nodes. In Rust code, they can be any struct, and they are the output of a producer; an async function that takes a Context, any number of other nodes, and returns a Result<A>. The context, contains a state that can be anything you want as long as it implements Clone. It’s meant to be used for having database connections or whatever else you need.

It looks like this:

use serde::{Deserialize, Serialize};

#[derive(Clone)]
struct State {
    // Whatever we need
}

// Our node `A`.
#[derive(Clone, Serialize, Deserialize)]
struct A(i32);

#[ordr::producer]
async fn my_a_producer(_ctx: ordr::Context<State>) -> ordr::Result<A> {
    // Do some actual work
    Ok(A(123))
}

// If we then have a node `B` that depends on `A`, we just add it to the arguments:

#[derive(Clone, Serialize, Deserialize)]
struct B(i32);

#[ordr::producer]
async fn make_b(_ctx: ordr::Context<State>, a: A) -> ordr::Result<B> {
    Ok(B(a.0 + 2))
}

// Before we start executing anything, we need to make a job. The `.add::<T>()` adds the node
// and all its dependencies (in this case `A`).
//
// You can add as many targets as you'd like.
let job = ordr::Job::builder().add::<B>().build().unwrap();

// We also need the Context. If your tasks don't need a context, just use `()`.
let state = State {};

// Next we need a worker to execute the job.
let mut worker = ordr::Worker::new(job, state);

// Start the worker.
worker.run().await.unwrap();

// And get the output once it's done. The output is an enum that you can inspect. It will tell
// you if a node failed or if the whole job was cancelled, etc.
let output = worker.get_output().await.unwrap();

assert!(output.is_done());

// Next we can get the collected data/results out. It's a HashMap of the name of the node
// (struct name) to serialized value.
let mut data = worker.data().await;

assert_eq!(data.keys().len(), 2); // Both "A", and "B" is there.

let b = data.remove("B").unwrap();
let b: B = serde_json::from_value(b).unwrap();
assert_eq!(b.0, 125);

A few things to keep in mind:

  • All nodes and the context must implement Clone and Serde’s Serialize and Deserialize.
  • All producers must return a ordr::Result (which is a Result<T, ordr::Error>.
  • All producers must be async and take ordr::Context<State> as the first parameter.
    • State is your state. Whatever you need.

§Mermaid diagram

It might be useful to inspect a Job visually. You can get a graph like this:

let diagram = ordr::mermaid(&job);
println!("{diagram}");

§Adding multiple targets to a job

A job can have multiple targets. If two nodes depend on the same third node, it will only be executed once.

let job = ordr::Job::builder()
    .add::<A>()
    .add::<B>()
    .build()
    .unwrap();

§Partial data

If you aleady have results from earlier, or maybe cached somewhere, then you can add it to the job, and the graph will not run the producers for them (nor its dependencies).

let job = ordr::Job::builder().add::<A>().build().unwrap();

// Worker from a previous job:
let data = worker.data().await;

// Creating a new job with this data.
let job = ordr::Job::builder_with_data(data).add::<A>().build().unwrap();

§Stopping a job

You can stop a job at any time. This can be useful for something like creating timeouts.

let mut worker = ordr::Worker::new(job, state);

// Starts the worker
worker.run();

// Stops it and cancels all running nodes.
worker.stop();

// You can still get the ouput.
let output = worker.get_output().await;

// And whatever data was done before you stopped it.
let data = worker.data().await;

Modules§

serde
Serde
serde_json
Serde JSON

Structs§

Context
First argument of a producer function. It’s just some basic meta data (that I might later expand on) about running the node.
Error
Error type that a producer may return.
Job
Describes what needs to be done, and how to do it. Pass it to a crate::Worker to have it executed.
JobBuilder
Builds a job. Created with Job::builder(). Call .build() on it to create a Job.
Worker
Runs crate::Jobs.

Enums§

JobError
NodeState
The current state of a single node in a job.
Output
Output of running a job. Describes how and if the job was finished. Use crate::Worker::data to get the results out.

Functions§

mermaid
Builds a simple mermaid diagram of the nodes that will be executed when running this job.

Type Aliases§

Result
Return value for producers.

Attribute Macros§

producer
Mark a function return a Result<T, ordr::Error> as a producer of T.