The Itinera bookRust edition

Part 4: Running it for real, Chapter 25

Reading the result

What you want to do

When an order doesn't go through, you want the code that runs the shop to read why from the journey's result. And you want it to tell a journey that failed, which the customer can be told about, from one that was stopped because the shop itself got something wrong.

The code

The shop is chapter 24's, with one more customer: Max, whose order arrives without a card, because the page that took it forgot to ask for one. The order's card is now an Option, and Max's is None.

Rust, from the program ch25_reading_the_result
/// What the shop knows about an order when its journey starts. An order with no card leaves it
/// out.
#[derive(InitialData)]
struct NewOrder {
    order: String,
    customer: String,
    price: i64,
    discount: Option<i64>,
    parcel: Parcel,
    card: Option<String>,
    gift_card: bool,
}

Parcel is chapter 21's, and Order, the output that follow_up reads, chapter 24's.

main hands each journey's result to follow_up, which looks at how the journey ended before it reads anything else, with a match on its status:

Rust, from the program ch25_reading_the_result
/// Says how an order's journey ended, and what explains it.
fn follow_up(result: &JourneyResult) -> Result<(), Box<dyn std::error::Error>> {
    match &result.status {
        JourneyStatus::Succeeded { .. } => {
            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 succeeded, with receipt {receipt} and tracking {tracking}");
            println!("audit: {}", data.audit.as_deref().unwrap_or("none"));
        }
        JourneyStatus::Failed { failure, .. } => {
            let data: Order = result.output()?;
            let receipt = data.receipt.as_deref().unwrap_or("none");
            println!(
                "the journey failed ({}), with receipt {receipt}",
                why(failure)
            );
        }
        JourneyStatus::Aborted(abort) => {
            println!("the journey was aborted ({})", what_went_wrong(abort));
        }
        _ => println!("the journey ended as {}", result.status.kind()),
    }
    Ok(())
}

Two small functions put into words what explains a failure, and an abort:

Rust, from the program ch25_reading_the_result
/// The cause of a failure, with the reason a step gave for it when it gave one.
fn why(failure: &Failure) -> String {
    match failure {
        Failure::Failure(reason) => format!("{}: {}", failure.cause(), reason.code()),
        _ => failure.cause().to_string(),
    }
}

/// The reason of an abort, with the key that was missing when that is what happened.
fn what_went_wrong(abort: &Abort) -> String {
    match abort {
        Abort::RequiredDataMissing(MissingData::Key { key, .. }) => {
            format!("{}: {key}", abort.reason())
        }
        _ => abort.reason().to_string(),
    }
}

It runs the six orders, and prints:

What ch25_reading_the_result prints, recorded in ch25_reading_the_result.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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 ch25_reading_the_result::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 (failure: not-replaced), with receipt none
Max's order:
log orders A-11 #1 journey_started customer, gift_card, order, parcel, price
log orders A-11 #2 attempt_started lookup, attempt 1
log orders A-11 #3 service_provided lookup gets order_book from ledger, a ch25_reading_the_result::Ledger
the order book has no order A-11 yet
log orders A-11 #4 step_succeeded lookup
log orders A-11 #5 contribution_committed completed
log orders A-11 #6 hook_called already_completed: on step success of lookup
log orders A-11 #7 attempt_started price, attempt 1
log orders A-11 #8 optional_input_absent discount
log orders A-11 #9 step_succeeded price
log orders A-11 #10 contribution_committed amount
log orders A-11 #11 attempt_started charge, attempt 1
log orders A-11 #12 journey_aborted
the journey was aborted (required data missing: card)

How it works

  1. The status is an enum. The result's status is a JourneyStatus, with one variant for each way a journey ends: Succeeded, Failed, which holds a Failure, and Aborted, which holds an Abort. The match is how follow_up tells them apart, and each arm works with what its variant holds. What the journey gave back, the shop reads typed, with result.output(), as chapter 6 showed. These enums are #[non_exhaustive], so a match on them always needs a _ arm; in why and what_went_wrong it names the cause or reason alone.
  2. A failure explains itself. Lee's failure carries its cause, failure, and the reason the rescue step gave, not-replaced: a rescue step that fails is given up, as chapter 18 showed. Failure has one variant for each cause, holding the reason given, or the error that ended the last attempt, and its cause() names the cause. The causes are the ones step_given_up names, as chapter 12 showed.
  3. The result says how a journey ended, not where. It names no step. The desk said that Lee's order failed at charge_rescue, because it read the journey_failed event. Which step failed, and how many attempts each step made, are in the events.
  4. An abort means something that must not happen did. Max's charge asks for a card, and the data bag has none, so the charge can't be built. The executor doesn't fail the journey: it aborts it, with the reason required data missing, naming the key. A failure is an answer of the business, which the shop can give the customer. An abort is a fault in the shop's own code or data, to be fixed. Abort has one variant for each reason, and RequiredDataMissing holds what was missing, which is how what_went_wrong finds the key.
  5. An aborted journey has no output. Max's pricing succeeded, and its amount was committed, but the data bag of a stopped journey may stand halfway through a change, so the result doesn't offer it. result.output() returns an Err for an aborted journey, which is why only the other two arms read it.
  6. An aborted journey runs no workflow hook. Lee was told the order could not be completed, and Max was told nothing, since the hooks of a workflow policy run only for a journey that succeeded or failed. No journey_failed was recorded either, so the desk is silent: Max's log ends with journey_aborted. An aborted journey is never resumed: fix the cause, then run the order again with a new instance.

Every order now ends with an answer the shop can act on. But the shop's real bank and carriers answer over the network, and while a step waits for them, its journey holds the program's thread.

Outcomes

We have shown how the shop reads from the result how each journey ended and what explains it.

The usual way With Itinera
Why a request failed is often a message in a log, or an error type the caller has to guess at. A failed journey's result carries its cause and the reason the step gave.
A bug in the shop's own code and a refusal of the business often end the same way, as an error. A journey that failed and a journey that was aborted are different statuses, and an aborted journey has no output.