Orchestration in Rust

Stefanos Kouroupis - Nov 7 '20 - - Dev Community

Porting patterns from Object Oriented concepts in Rust do not always work, but there are certain cases they not only work but sometimes they are easier to follow when compared with the original OOP implementation.

Here I am going to demonstrate the concept of orchestration in Rust.

Taking a step back orchestration resembles vaguely of redux, with the key difference that a reducer has not only has to deal with the state but a payload as well.

Orchestration is basically a pipeline for which the previous state is passed in the next operation. The key thing in orchestration is the ability to pause and resume.

I classify this example as an intermediate one, but not that hard to follow.

We will start by talking about and defining our State object;

#[derive(Debug, Clone)]
pub struct State {
    pub proceed: bool,
    pub outcome: f32,
    pub stage: Vec<bool>,
}
Enter fullscreen mode Exit fullscreen mode

Our state has the following

  • proceed: which marks each stage as successful or not, marking proceed as false, that will stop the execution of the remaining stages
  • outcome: result of each stage
  • stage: record the outcome of the stage

Then we will define the Orchestrate trait

trait Orchestrate {
    fn execute(self, state: State) -> State;
}
Enter fullscreen mode Exit fullscreen mode

and we will implement the Orchestrate trait on a Vector of Functions

impl Orchestrate for Vec<fn(State) -> Result<State, Error>> {
     fn execute(self, state: State) -> State {
        self.iter().enumerate().fold(state, |output, (i, func)| {
            let new_state = output.clone();
            if new_state.stage.len() > i {
                if new_state.stage[i] {
                    return new_state;
                } else {
                    let mut next_state = func(new_state).unwrap();
                    next_state.stage[i] = next_state.proceed;
                    return next_state;
                }
            }
            let mut next_state = func(new_state).unwrap();
            next_state.stage.push(next_state.proceed);
            return next_state;
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

It's already easy to see where this is going.

  • we received the array of functions
  • for each execution we check if the operation has been completed in a previous execution (stage)
  • we either execute the stage or ignore it

Next we will define 3 simple operations we want to perform in this orchestration. How are they going to be used, one would wonder.

fn pow2(n: f32) -> f32 {
    n.powf(2.0)
}

fn pow3(n: f32) -> f32 {
    n.powf(3.0)
}

fn sqrt(n: f32) -> f32 {
    n.sqrt()
}
Enter fullscreen mode Exit fullscreen mode

And now my favourite part, instead of just creating a function that gets a State and returns a state and has the pause resume logic in it I am going to create a macro that creates the functions by accepting a function as an argument.

macro_rules! state_function {
    ( $func:expr ) => {{
        pub fn state_fn(c: State) -> Result<State, Error> {
            let stage: Vec<bool> = c.stage.to_vec();
            if c.proceed == false {
                Ok(State {
                    proceed: false,
                    outcome: c.outcome,
                    stage: stage,
                })
            } else {
                let mut rng = rand::thread_rng();
                let y: bool = rng.gen();
                Ok(State {
                    proceed: y,
                    outcome: $func(c.outcome),
                    stage: stage,
                })
            }
        }

        state_fn
    }};
}
Enter fullscreen mode Exit fullscreen mode
  • If proceed is false, don't do anything
  • else return the state with the new outcome.

And to spice things a bit up I used a random boolean, to disrupt the processing and make things a bit more interesting.

So finally on our main function,
Step 1: we create our functions

    let a: fn(State) -> Result<State, Error> = state_function!(pow2);
    let b: fn(State) -> Result<State, Error> = state_function!(pow3);
    let c: fn(State) -> Result<State, Error> = state_function!(sqrt);
Enter fullscreen mode Exit fullscreen mode

Step 2: we define our chain

    let chain: Vec<fn(State) -> Result<State, Error>> =  vec![a, b, c];
Enter fullscreen mode Exit fullscreen mode

Step 3: we execute our chain

    let result = chain
        .execute(State {
            proceed: true,
            outcome: 6.,
            stage: Vec::<bool>::new(),
        });

    println!("{:?}", result);
Enter fullscreen mode Exit fullscreen mode

BONUS
Step 4: we execute our chain with assuming some steps have been completed

    let result = chain
        .execute(State {
            proceed: true,
            outcome: 6.,
            stage: vec![true, false, false],
        });

    println!("{:?}", result);
Enter fullscreen mode Exit fullscreen mode

I hope you enjoyed it... :D

Update: a generic version of this pattern can be found here https://github.com/elasticrash/orchestrator

. . . . . . . . . . . . . . . . . . . . . . . . .