The Itinera bookRust edition

Part 4: Running it for real, Chapter 24

Running many journeys

What you want to do

Orders come in all day, and the shop shouldn't make a new executor for each one. You want one executor that runs every order of the day, one journey after another, each starting from its own order and nothing else.

The code

The shop is chapter 23's, except that its output no longer holds the refund, which main no longer prints:

Rust, from the program ch24_one_executor_for_every_order
/// What the shop reads back once the journey has ended.
#[derive(Output)]
struct Order {
    receipt: Option<String>,
    tracking: Option<String>,
    audit: Option<String>,
}

main now makes one executor before the loop, and hands it each order's instance in turn:

Rust, from the program ch24_one_executor_for_every_order
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut executor = local_executor_for!(Orders);
    // The order, the customer, the price, the discount, the parcel's weight in kg and its size in
    // cm, the card, and whether the order is a gift card.
    let orders = [
        (
            "A-1",
            "Ann",
            40,
            Some(10),
            2,
            [30, 20, 10],
            "ann-2028",
            false,
        ),
        ("A-7", "Gil", 45, None, 60, [120, 80, 60], "gil-2027", false),
        ("A-8", "Jo", 30, None, 1, [20, 15, 5], "jo-2024", false),
        ("A-9", "Kim", 25, None, 0, [0, 0, 0], "kim-2027", true),
        ("A-10", "Lee", 35, None, 3, [40, 30, 20], "lee-2019", false),
    ];
    for (order, customer, price, discount, weight, size, card, gift_card) in orders {
        println!("{customer}'s order:");
        let instance = Orders::builder()
            .channel("email")
            .initial_data(NewOrder {
                order: order.to_owned(),
                customer: customer.to_owned(),
                price,
                discount,
                parcel: Parcel { weight, size },
                card: card.to_owned(),
                gift_card,
            })
            .create()?;
        let result = executor.run(instance)?;
        let data: Order = result.output()?;
        let receipt = data.receipt.as_deref().unwrap_or("none");
        let tracking = data.tracking.as_deref().unwrap_or("none");
        println!(
            "the journey {}, with receipt {receipt} and tracking {tracking}",
            result.status.kind(),
        );
        println!("audit: {}", data.audit.as_deref().unwrap_or("none"));
    }
    Ok(())
}

NewOrder and Parcel are as chapter 21 shows them, and main runs that chapter's five orders, none of which the order book has completed, and prints:

What ch24_one_executor_for_every_order prints, recorded in ch24_one_executor_for_every_order.txt
Ann's order:
log orders A-1 #1 journey_started card, customer, discount, gift_card, order, parcel, price
log orders A-1 #2 attempt_started lookup, attempt 1
log orders A-1 #3 service_provided lookup gets order_book from ledger, a ch24_one_executor_for_every_order::Ledger
the order book has no order A-1 yet
log orders A-1 #4 step_succeeded lookup
log orders A-1 #5 contribution_committed completed
log orders A-1 #6 hook_called already_completed: on step success of lookup
log orders A-1 #7 attempt_started price, attempt 1
log orders A-1 #8 step_succeeded price
log orders A-1 #9 contribution_committed amount
log orders A-1 #10 attempt_started charge, attempt 1
log orders A-1 #11 service_provided charge gets payments from bank, a ch24_one_executor_for_every_order::Bank
the bank charges 30 for order A-1
log orders A-1 #12 step_succeeded charge
log orders A-1 #13 contribution_committed receipt
log orders A-1 #14 hook_called audit: on step success of charge
log orders A-1 #15 contribution_committed audit
log orders A-1 #16 attempt_started ship, attempt 1
log orders A-1 #17 input_adapter_supplied ship gets weight from parcels
log orders A-1 #18 service_provided ship gets carrier from carriers, a ch24_one_executor_for_every_order::MainCarrier
the main carrier books order A-1
log orders A-1 #19 step_info ship: the parcel leaves with receipt R-A-1
log orders A-1 #20 step_succeeded ship
log orders A-1 #21 contribution_committed tracking
log orders A-1 #22 optional_input_absent code
email to Ann: Order A-1 is on its way, tracking T-A-1.
log orders A-1 #23 hook_called notify: on workflow success
log orders A-1 #24 journey_succeeded
the journey succeeded, with receipt R-A-1 and tracking T-A-1
audit: charge took 30, receipt R-A-1
Gil's order:
log orders A-7 #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-7 #2 attempt_started lookup, attempt 1
log orders A-7 #3 service_provided lookup gets order_book from ledger, a ch24_one_executor_for_every_order::Ledger
the order book has no order A-7 yet
log orders A-7 #4 step_succeeded lookup
log orders A-7 #5 contribution_committed completed
log orders A-7 #6 hook_called already_completed: on step success of lookup
log orders A-7 #7 attempt_started price, attempt 1
log orders A-7 #8 optional_input_absent discount
log orders A-7 #9 step_succeeded price
log orders A-7 #10 contribution_committed amount
log orders A-7 #11 attempt_started charge, attempt 1
log orders A-7 #12 service_provided charge gets payments from bank, a ch24_one_executor_for_every_order::Bank
the bank charges 45 for order A-7
log orders A-7 #13 step_succeeded charge
log orders A-7 #14 contribution_committed receipt
log orders A-7 #15 hook_called audit: on step success of charge
log orders A-7 #16 contribution_committed audit
log orders A-7 #17 attempt_started ship, attempt 1
log orders A-7 #18 input_adapter_supplied ship gets weight from parcels
log orders A-7 #19 service_provided ship gets carrier from carriers, a ch24_one_executor_for_every_order::MainCarrier
the main carrier books order A-7
log orders A-7 #20 step_failed ship: too-heavy
log orders A-7 #21 hook_called failed_shipping: on step retry of ship
log orders A-7 #22 step_retrying ship
log orders A-7 #23 attempt_started ship, attempt 2
log orders A-7 #24 input_adapter_supplied ship gets weight from parcels
log orders A-7 #25 service_provided ship gets carrier from carriers, a ch24_one_executor_for_every_order::BackupCarrier
the backup carrier books order A-7
log orders A-7 #26 step_info ship: the parcel leaves with receipt R-A-7
log orders A-7 #27 step_succeeded ship
log orders A-7 #28 contribution_committed tracking
log orders A-7 #29 optional_input_absent code
email to Gil: Order A-7 is on its way, tracking B-A-7.
log orders A-7 #30 hook_called notify: on workflow success
log orders A-7 #31 journey_succeeded
the journey succeeded, with receipt R-A-7 and tracking B-A-7
audit: charge took 45, receipt R-A-7
Jo's order:
log orders A-8 #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-8 #2 attempt_started lookup, attempt 1
log orders A-8 #3 service_provided lookup gets order_book from ledger, a ch24_one_executor_for_every_order::Ledger
the order book has no order A-8 yet
log orders A-8 #4 step_succeeded lookup
log orders A-8 #5 contribution_committed completed
log orders A-8 #6 hook_called already_completed: on step success of lookup
log orders A-8 #7 attempt_started price, attempt 1
log orders A-8 #8 optional_input_absent discount
log orders A-8 #9 step_succeeded price
log orders A-8 #10 contribution_committed amount
log orders A-8 #11 attempt_started charge, attempt 1
log orders A-8 #12 service_provided charge gets payments from bank, a ch24_one_executor_for_every_order::Bank
the bank declines card jo-2024: it has expired
log orders A-8 #13 step_failed charge: card-expired
log orders A-8 #14 step_rescuing charge: failure, rescued by charge_rescue
log orders A-8 #15 attempt_started charge_rescue, attempt 1
the bank says card jo-2024 was replaced by jo-2029
log orders A-8 #16 step_succeeded charge_rescue
log orders A-8 #17 contribution_committed card
log orders A-8 #18 data_overwritten card
log orders A-8 #19 step_rescued charge by charge_rescue
log orders A-8 #20 attempt_started charge, attempt 2
the bank charges 30 for order A-8
log orders A-8 #21 step_succeeded charge
log orders A-8 #22 contribution_committed receipt
log orders A-8 #23 hook_called audit: on step success of charge
log orders A-8 #24 contribution_committed audit
log orders A-8 #25 attempt_started ship, attempt 1
log orders A-8 #26 input_adapter_supplied ship gets weight from parcels
log orders A-8 #27 service_provided ship gets carrier from carriers, a ch24_one_executor_for_every_order::MainCarrier
the main carrier books order A-8
log orders A-8 #28 step_info ship: the parcel leaves with receipt R-A-8
log orders A-8 #29 step_succeeded ship
log orders A-8 #30 contribution_committed tracking
log orders A-8 #31 optional_input_absent code
email to Jo: Order A-8 is on its way, tracking T-A-8.
log orders A-8 #32 hook_called notify: on workflow success
log orders A-8 #33 journey_succeeded
the journey succeeded, with receipt R-A-8 and tracking T-A-8
audit: charge took 30, receipt R-A-8
Kim's order:
log orders A-9 #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-9 #2 attempt_started lookup, attempt 1
log orders A-9 #3 service_provided lookup gets order_book from ledger, a ch24_one_executor_for_every_order::Ledger
the order book has no order A-9 yet
log orders A-9 #4 step_succeeded lookup
log orders A-9 #5 contribution_committed completed
log orders A-9 #6 hook_called already_completed: on step success of lookup
log orders A-9 #7 attempt_started price, attempt 1
log orders A-9 #8 optional_input_absent discount
log orders A-9 #9 step_succeeded price
log orders A-9 #10 contribution_committed amount
log orders A-9 #11 attempt_started charge, attempt 1
log orders A-9 #12 service_provided charge gets payments from bank, a ch24_one_executor_for_every_order::Bank
the bank charges 25 for order A-9
log orders A-9 #13 step_succeeded charge
log orders A-9 #14 contribution_committed receipt
log orders A-9 #15 contribution_committed code
log orders A-9 #16 hook_called audit: on step success of charge
log orders A-9 #17 contribution_committed audit
log orders A-9 #18 attempt_started ship, attempt 1
log orders A-9 #19 input_adapter_supplied ship gets weight from parcels
log orders A-9 #20 service_provided ship gets carrier from carriers, a ch24_one_executor_for_every_order::MainCarrier
log orders A-9 #21 step_skipped ship: nothing-to-ship
log orders A-9 #22 contributions_discarded ship
log orders A-9 #23 optional_input_absent tracking
email to Kim: Order A-9 is paid: your gift card's code is G-A-9.
log orders A-9 #24 hook_called notify: on workflow success
log orders A-9 #25 journey_succeeded
the journey succeeded, with receipt R-A-9 and tracking none
audit: charge took 25, receipt R-A-9
Lee's order:
log orders A-10 #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-10 #2 attempt_started lookup, attempt 1
log orders A-10 #3 service_provided lookup gets order_book from ledger, a ch24_one_executor_for_every_order::Ledger
the order book has no order A-10 yet
log orders A-10 #4 step_succeeded lookup
log orders A-10 #5 contribution_committed completed
log orders A-10 #6 hook_called already_completed: on step success of lookup
log orders A-10 #7 attempt_started price, attempt 1
log orders A-10 #8 optional_input_absent discount
log orders A-10 #9 step_succeeded price
log orders A-10 #10 contribution_committed amount
log orders A-10 #11 attempt_started charge, attempt 1
log orders A-10 #12 service_provided charge gets payments from bank, a ch24_one_executor_for_every_order::Bank
the bank declines card lee-2019: it has expired
log orders A-10 #13 step_failed charge: card-expired
log orders A-10 #14 step_rescuing charge: failure, rescued by charge_rescue
log orders A-10 #15 attempt_started charge_rescue, attempt 1
the bank says nothing replaced card lee-2019
log orders A-10 #16 step_failed charge_rescue: not-replaced
log orders A-10 #17 step_given_up charge_rescue: failure
email to Lee: Sorry, order A-10 could not be completed.
log orders A-10 #18 hook_called notify: on workflow failure
log orders A-10 #19 journey_failed charge_rescue
desk: order A-10 failed at charge_rescue
the journey failed, with receipt none and tracking none
audit: none

An instance runs one journey and no more. Handing the same instance to the executor a second time is a mistake, and Rust won't compile it:

Rust, the program ch24_instance_run_twice. Does not compile
//! Running one instance twice: an instance runs one journey, so this does not compile.

use itinera::error::Error;
use itinera::executor::local_executor_for;
use itinera::journey::InitialData;
use itinera::step::{Outcome, Step, step};
use itinera::workflow::workflow;

/// Charges an order's price.
#[derive(Step)]
struct Charge {
    #[input]
    price: i64,
}

#[step]
impl Charge {
    fn run(self) -> Result<Outcome, Error> {
        if self.price <= 0 {
            return Self::fail("nothing-to-charge");
        }
        Self::succeed()
    }
}

/// The shop's workflow: for now, it only charges.
#[workflow(name = "orders")]
#[with_step(Charge, name = "charge")]
struct Orders;

/// What the shop knows about an order when its journey starts.
#[derive(InitialData)]
struct NewOrder {
    price: i64,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut executor = local_executor_for!(Orders);
    let instance = Orders::builder()
        .initial_data(NewOrder { price: 40 })
        .create()?;
    let first = executor.run(instance)?;
    let second = executor.run(instance)?;
    println!("{} {}", first.status.kind(), second.status.kind());
    Ok(())
}
What the compiler says about ch24_instance_run_twice, recorded in ch24_instance_run_twice.txt
error[E0382]: use of moved value: `instance`
  --> compile-errors/ch24_instance_run_twice.rs:43:31
   |
39 |     let instance = Orders::builder()
   |         -------- move occurs because `instance` has type `Instance<Orders>`, which does not implement the `Copy` trait
...
42 |     let first = executor.run(instance)?;
   |                              -------- value moved here
43 |     let second = executor.run(instance)?;
   |                               ^^^^^^^^ value used here after move

How it works

  1. An executor keeps nothing between journeys. The local executor runs a journey inside the call to its run, and saves nothing: the journey lives only as long as that call. When run returns, nothing of that journey remains in the executor, so the next journey starts from its own order alone. Gil's shipping needed the backup carrier and Jo's card was replaced, yet Kim's journey, run next by the same executor, starts like every other: journey_started with Kim's data, the lookup's first attempt, and the bank made anew for Kim's charge, as service_provided shows. Its log starts again at #1, since an event's sequence number counts the events of its own journey, never those of the journeys before it. What one journey did never reaches another through the executor.
  2. The loop still creates an instance for every order. An instance is made for one journey, and runs once, as chapter 5 showed. Rust checks it when the program is built: run takes the instance by value, so using it again is the "use of moved value" error above, never something found while the shop is open. To run an order again, for example after the customer has corrected it, create a new instance with the corrected data.
  3. An executor runs one journey at a time. run borrows the executor mutably, which is why it is declared with let mut: it can't be handed a second journey until the first has ended, and each call returns only once its journey has. That is why the loop waits for each order before it starts the next.

The executor now runs every order. But for Lee's order, main learns only that the journey failed. The log shows why, line by line, but the code that runs the shop has only the result, and reads from it only how the journey ended and its output.

Outcomes

We have shown how one executor runs every order of the day, each journey starting from its own data.

The usual way With Itinera
Code that handles many requests often keeps state between them, in a cache or a field, and a request can be affected by the ones before it. The executor keeps nothing between journeys: each starts from its own order, with services made for it.
Running the same request twice by mistake is usually found at run time, if at all. An instance moves into run, so running it twice doesn't compile.