Saying which attempt took the money
What you want to do
The accountants match every audit line with the bank's records, but the line says neither which journey nor which attempt took the money, and a charge that ends in the bank's error, which may have taken the money before its network failed, has no line at all. You want every line to say which journey and which attempt it is about, and a line for a charge that ended in an error, with the error.
The code
The shop is chapter 33's. The audit's hook asks for two more things, the journey ID and the attempt number, and writes them on its line:
/// Writes an audit line for every step it is attached to that succeeds: the journey, the attempt
/// that took the money, the amount the order was charged, and the receipt the step gave back.
#[derive(Default)]
struct Audit;
#[step_policy(name = "audit")]
impl Audit {
#[on_step_success]
fn charged(
&self,
#[journey_id] journey: &JourneyId,
#[step_name] step: StepName,
#[attempt] attempt: NonZeroU32,
#[from_workflow] amount: i64,
#[from_step] receipt: String,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<Option<OnSuccess>, Error> {
contributor.contribute(
"audit",
format!("{journey}: {step} took {amount} on attempt {attempt}, receipt {receipt}"),
);
Self::carry_on()
}
}/// Writes an audit line for every step it is attached to that succeeds: the journey, the attempt
/// that took the money, the amount the order was charged, and the receipt the step gave back.
#[derive(Default)]
struct Audit;
#[step_policy(name = "audit")]
impl Audit {
#[on_step_success]
async fn charged(
&self,
#[journey_id] journey: &JourneyId,
#[step_name] step: StepName,
#[attempt] attempt: NonZeroU32,
#[from_workflow] amount: i64,
#[from_step] receipt: String,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<Option<OnSuccess>, Error> {
contributor.contribute(
"audit",
format!("{journey}: {step} took {amount} on attempt {attempt}, receipt {receipt}"),
);
Self::carry_on()
}
}The outage policy, which gives up a charge that ended in an error, asks for the same two, the amount, and the error itself, and writes an audit line of its own before giving the charge up:
/// Gives up a step that ends in an error at once: the bank's network is down, and neither a retry
/// nor a rescue can mend that. Since the bank may have taken the money before its network failed,
/// it writes an audit line with the error, for the accountants to check with the bank.
#[derive(Default)]
struct Outage;
#[step_policy(name = "outage")]
impl Outage {
#[on_step_abnormal_termination]
fn ended_in_error(
&self,
#[journey_id] journey: &JourneyId,
#[step_name] step: StepName,
#[attempt] attempt: NonZeroU32,
#[error] error: &Error,
#[from_workflow] amount: i64,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<Option<FailWorkflow>, Error> {
contributor.contribute(
"audit",
format!(
"{journey}: {step} ended in an error on attempt {attempt} ({error}); \
check whether the bank took {amount}"
),
);
Self::fail_workflow("payment-network-down")
}
}/// Gives up a step that ends in an error at once: the bank's network is down, and neither a retry
/// nor a rescue can mend that. Since the bank may have taken the money before its network failed,
/// it writes an audit line with the error, for the accountants to check with the bank.
#[derive(Default)]
struct Outage;
#[step_policy(name = "outage")]
impl Outage {
#[on_step_abnormal_termination]
async fn ended_in_error(
&self,
#[journey_id] journey: &JourneyId,
#[step_name] step: StepName,
#[attempt] attempt: NonZeroU32,
#[error] error: &Error,
#[from_workflow] amount: i64,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<Option<FailWorkflow>, Error> {
contributor.contribute(
"audit",
format!(
"{journey}: {step} ended in an error on attempt {attempt} ({error}); \
check whether the bank took {amount}"
),
);
Self::fail_workflow("payment-network-down")
}
}In the asynchronous form, both hooks are async fn, as hooks became in chapter 26, and ask for the same things.
follow_up, from chapter 25, now prints the audit line of a failed journey too:
/// 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)
);
println!("audit: {}", data.audit.as_deref().unwrap_or("none"));
}
JourneyStatus::Aborted(abort) => {
println!("the journey was aborted ({})", what_went_wrong(abort));
}
_ => println!("the journey ended as {}", result.status.kind()),
}
Ok(())
}Order is chapter 24's, why chapter 25's and what_went_wrong chapter 31's.
In the asynchronous form, main reads back only the audit line, as Audited, and prints it after each journey's status. order is chapter 33's journey chapter 26's and Parcel chapter 21's:
/// What the shop reads back once the journey has ended: the audit's line.
#[derive(Output)]
struct Audited {
audit: Option<String>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (ann, jo, cat) = tokio::join!(
journey(order(
"A-1",
"Ann",
40,
Some(10),
Parcel {
weight: 2,
size: [30, 20, 10],
},
"ann-2028",
)?),
journey(order(
"A-8",
"Jo",
30,
None,
Parcel {
weight: 1,
size: [20, 15, 5],
},
"jo-2024",
)?),
journey(order(
"A-3",
"Cat",
120,
None,
Parcel {
weight: 3,
size: [40, 30, 20],
},
"cat-2027",
)?),
);
for result in [ann?, jo?, cat?] {
let data: Audited = result.output()?;
println!("order {} {}", result.journey_id, result.status.kind());
println!("audit: {}", data.audit.as_deref().unwrap_or("none"));
}
Ok(())
}main runs Ann's order, Jo's, whose expired card is renewed by the rescue step, and Cat's, of 120, on which the bank's network fails. It prints each journey's audit line, even for a journey that failed:
Ann's order:
log orders A-1 for Ann #1 journey_started card, customer, discount, gift_card, order, parcel, price
log orders A-1 for Ann #2 attempt_started lookup, attempt 1
log orders A-1 for Ann #3 service_provided lookup gets order_book from ledger, a ch34_saying_which_attempt_took_the_money::Ledger
the order book has no order A-1 yet
log orders A-1 for Ann #4 step_succeeded lookup
log orders A-1 for Ann #5 contribution_committed completed
log orders A-1 for Ann #6 hook_called already_completed: on step success of lookup
log orders A-1 for Ann #7 attempt_started price, attempt 1
log orders A-1 for Ann #8 step_succeeded price
log orders A-1 for Ann #9 contribution_committed amount
log orders A-1 for Ann #10 attempt_started charge, attempt 1
log orders A-1 for Ann #11 service_provided charge gets payments from bank, a ch34_saying_which_attempt_took_the_money::Bank
the bank charges 30 for order A-1
log orders A-1 for Ann, receipt R-A-1 #12 global_metadata_set receipt
log orders A-1 for Ann, receipt R-A-1 #13 step_succeeded charge
log orders A-1 for Ann, receipt R-A-1 #14 contribution_committed receipt
log orders A-1 for Ann, receipt R-A-1 #15 hook_called audit: on step success of charge
log orders A-1 for Ann, receipt R-A-1 #16 contribution_committed audit
log orders A-1 for Ann, receipt R-A-1 #17 attempt_started ship, attempt 1
log orders A-1 for Ann, receipt R-A-1 #18 input_adapter_supplied ship gets weight from parcels
log orders A-1 for Ann, receipt R-A-1 #19 service_provided ship gets carrier from carriers, a ch34_saying_which_attempt_took_the_money::MainCarrier
the main carrier books order A-1
log orders A-1 for Ann, receipt R-A-1 #20 step_info ship: the parcel leaves with receipt R-A-1
log orders A-1 for Ann, receipt R-A-1 #21 step_succeeded ship
log orders A-1 for Ann, receipt R-A-1 #22 contribution_committed tracking
log orders A-1 for Ann, receipt R-A-1 #23 optional_input_absent code
email to Ann: Order A-1 is on its way, tracking T-A-1.
log orders A-1 for Ann, receipt R-A-1 #24 hook_called notify: on workflow success
log orders A-1 for Ann, receipt R-A-1 #25 journey_succeeded
the journey succeeded, with receipt R-A-1 and tracking T-A-1
audit: A-1: charge took 30 on attempt 1, receipt R-A-1
Jo's order:
log orders A-8 for Jo #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-8 for Jo #2 attempt_started lookup, attempt 1
log orders A-8 for Jo #3 service_provided lookup gets order_book from ledger, a ch34_saying_which_attempt_took_the_money::Ledger
the order book has no order A-8 yet
log orders A-8 for Jo #4 step_succeeded lookup
log orders A-8 for Jo #5 contribution_committed completed
log orders A-8 for Jo #6 hook_called already_completed: on step success of lookup
log orders A-8 for Jo #7 attempt_started price, attempt 1
log orders A-8 for Jo #8 optional_input_absent discount
log orders A-8 for Jo #9 step_succeeded price
log orders A-8 for Jo #10 contribution_committed amount
log orders A-8 for Jo #11 attempt_started charge, attempt 1
log orders A-8 for Jo #12 service_provided charge gets payments from bank, a ch34_saying_which_attempt_took_the_money::Bank
the bank declines card jo-2024: it has expired
log orders A-8 for Jo #13 step_failed charge: card-expired
log orders A-8 for Jo #14 step_rescuing charge: failure, rescued by charge_rescue
log orders A-8 for Jo #15 attempt_started charge_rescue, attempt 1
the bank says card jo-2024 was replaced by jo-2029
log orders A-8 for Jo #16 step_succeeded charge_rescue
log orders A-8 for Jo #17 contribution_committed card
log orders A-8 for Jo #18 data_overwritten card
log orders A-8 for Jo #19 step_rescued charge by charge_rescue
log orders A-8 for Jo #20 attempt_started charge, attempt 2
the bank charges 30 for order A-8
log orders A-8 for Jo, receipt R-A-8 #21 global_metadata_set receipt
log orders A-8 for Jo, receipt R-A-8 #22 step_succeeded charge
log orders A-8 for Jo, receipt R-A-8 #23 contribution_committed receipt
log orders A-8 for Jo, receipt R-A-8 #24 hook_called audit: on step success of charge
log orders A-8 for Jo, receipt R-A-8 #25 contribution_committed audit
log orders A-8 for Jo, receipt R-A-8 #26 attempt_started ship, attempt 1
log orders A-8 for Jo, receipt R-A-8 #27 input_adapter_supplied ship gets weight from parcels
log orders A-8 for Jo, receipt R-A-8 #28 service_provided ship gets carrier from carriers, a ch34_saying_which_attempt_took_the_money::MainCarrier
the main carrier books order A-8
log orders A-8 for Jo, receipt R-A-8 #29 step_info ship: the parcel leaves with receipt R-A-8
log orders A-8 for Jo, receipt R-A-8 #30 step_succeeded ship
log orders A-8 for Jo, receipt R-A-8 #31 contribution_committed tracking
log orders A-8 for Jo, receipt R-A-8 #32 optional_input_absent code
email to Jo: Order A-8 is on its way, tracking T-A-8.
log orders A-8 for Jo, receipt R-A-8 #33 hook_called notify: on workflow success
log orders A-8 for Jo, receipt R-A-8 #34 journey_succeeded
the journey succeeded, with receipt R-A-8 and tracking T-A-8
audit: A-8: charge took 30 on attempt 2, receipt R-A-8
Cat's order:
log orders A-3 for Cat #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-3 for Cat #2 attempt_started lookup, attempt 1
log orders A-3 for Cat #3 service_provided lookup gets order_book from ledger, a ch34_saying_which_attempt_took_the_money::Ledger
the order book has no order A-3 yet
log orders A-3 for Cat #4 step_succeeded lookup
log orders A-3 for Cat #5 contribution_committed completed
log orders A-3 for Cat #6 hook_called already_completed: on step success of lookup
log orders A-3 for Cat #7 attempt_started price, attempt 1
log orders A-3 for Cat #8 optional_input_absent discount
log orders A-3 for Cat #9 step_succeeded price
log orders A-3 for Cat #10 contribution_committed amount
log orders A-3 for Cat #11 attempt_started charge, attempt 1
log orders A-3 for Cat #12 service_provided charge gets payments from bank, a ch34_saying_which_attempt_took_the_money::Bank
the bank's network fails while charging order A-3
log orders A-3 for Cat #13 step_abnormal_termination charge: the payment network is down
log orders A-3 for Cat #14 hook_called outage: on step abnormal termination of charge
log orders A-3 for Cat #15 contribution_committed audit
log orders A-3 for Cat #16 step_given_up charge: FailWorkflow from outage, payment-network-down
email to Cat: Sorry, order A-3 could not be completed.
log orders A-3 for Cat #17 hook_called notify: on workflow failure
log orders A-3 for Cat #18 journey_failed charge
desk: order A-3 failed at charge
the journey failed (FailWorkflow), with receipt none
audit: A-3: charge ended in an error on attempt 1 (the payment network is down); check whether the bank took 120log orders A-8 for Jo #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-3 for Cat #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-1 for Ann #1 journey_started card, customer, discount, gift_card, order, parcel, price
log orders A-3 for Cat #2 attempt_started lookup, attempt 1
log orders A-1 for Ann #2 attempt_started lookup, attempt 1
log orders A-8 for Jo #2 attempt_started lookup, attempt 1
log orders A-1 for Ann #3 service_provided lookup gets order_book from ledger, a ch34_saying_which_attempt_took_the_money_async::Ledger
the order book has no order A-1 yet
log orders A-8 for Jo #3 service_provided lookup gets order_book from ledger, a ch34_saying_which_attempt_took_the_money_async::Ledger
the order book has no order A-8 yet
log orders A-3 for Cat #3 service_provided lookup gets order_book from ledger, a ch34_saying_which_attempt_took_the_money_async::Ledger
the order book has no order A-3 yet
log orders A-8 for Jo #4 step_succeeded lookup
log orders A-3 for Cat #4 step_succeeded lookup
log orders A-1 for Ann #4 step_succeeded lookup
log orders A-3 for Cat #5 contribution_committed completed
log orders A-1 for Ann #5 contribution_committed completed
log orders A-8 for Jo #5 contribution_committed completed
log orders A-1 for Ann #6 hook_called already_completed: on step success of lookup
log orders A-8 for Jo #6 hook_called already_completed: on step success of lookup
log orders A-3 for Cat #6 hook_called already_completed: on step success of lookup
log orders A-8 for Jo #7 attempt_started price, attempt 1
log orders A-3 for Cat #7 attempt_started price, attempt 1
log orders A-1 for Ann #7 attempt_started price, attempt 1
log orders A-3 for Cat #8 optional_input_absent discount
log orders A-1 for Ann #8 step_succeeded price
log orders A-8 for Jo #8 optional_input_absent discount
log orders A-1 for Ann #9 contribution_committed amount
log orders A-8 for Jo #9 step_succeeded price
log orders A-3 for Cat #9 step_succeeded price
log orders A-8 for Jo #10 contribution_committed amount
log orders A-3 for Cat #10 contribution_committed amount
log orders A-1 for Ann #10 attempt_started charge, attempt 1
log orders A-3 for Cat #11 attempt_started charge, attempt 1
log orders A-1 for Ann #11 service_provided charge gets payments from bank, a ch34_saying_which_attempt_took_the_money_async::Bank
log orders A-8 for Jo #11 attempt_started charge, attempt 1
the bank charges 30 for order A-1
log orders A-8 for Jo #12 service_provided charge gets payments from bank, a ch34_saying_which_attempt_took_the_money_async::Bank
log orders A-3 for Cat #12 service_provided charge gets payments from bank, a ch34_saying_which_attempt_took_the_money_async::Bank
the bank declines card jo-2024: it has expired
the bank's network fails while charging order A-3
log orders A-1 for Ann, receipt R-A-1 #12 global_metadata_set receipt
log orders A-3 for Cat #13 step_abnormal_termination charge: the payment network is down
log orders A-1 for Ann, receipt R-A-1 #13 step_succeeded charge
log orders A-8 for Jo #13 step_failed charge: card-expired
log orders A-1 for Ann, receipt R-A-1 #14 contribution_committed receipt
log orders A-8 for Jo #14 step_rescuing charge: failure, rescued by charge_rescue
log orders A-3 for Cat #14 hook_called outage: on step abnormal termination of charge
log orders A-8 for Jo #15 attempt_started charge_rescue, attempt 1
log orders A-3 for Cat #15 contribution_committed audit
log orders A-1 for Ann, receipt R-A-1 #15 hook_called audit: on step success of charge
log orders A-3 for Cat #16 step_given_up charge: FailWorkflow from outage, payment-network-down
log orders A-1 for Ann, receipt R-A-1 #16 contribution_committed audit
the bank says card jo-2024 was replaced by jo-2029
log orders A-1 for Ann, receipt R-A-1 #17 attempt_started ship, attempt 1
log orders A-8 for Jo #16 step_succeeded charge_rescue
email to Cat: Sorry, order A-3 could not be completed.
log orders A-8 for Jo #17 contribution_committed card
log orders A-3 for Cat #17 hook_called notify: on workflow failure
desk: order A-3 failed at charge
log orders A-1 for Ann, receipt R-A-1 #18 input_adapter_supplied ship gets weight from parcels
log orders A-3 for Cat #18 journey_failed charge
log orders A-1 for Ann, receipt R-A-1 #19 service_provided ship gets carrier from carriers, a ch34_saying_which_attempt_took_the_money_async::MainCarrier
log orders A-8 for Jo #18 data_overwritten card
the main carrier books order A-1
log orders A-8 for Jo #19 step_rescued charge by charge_rescue
log orders A-8 for Jo #20 attempt_started charge, attempt 2
log orders A-1 for Ann, receipt R-A-1 #20 step_info ship: the parcel leaves with receipt R-A-1
the bank charges 30 for order A-8
log orders A-1 for Ann, receipt R-A-1 #21 step_succeeded ship
log orders A-8 for Jo, receipt R-A-8 #21 global_metadata_set receipt
log orders A-8 for Jo, receipt R-A-8 #22 step_succeeded charge
log orders A-1 for Ann, receipt R-A-1 #22 contribution_committed tracking
log orders A-1 for Ann, receipt R-A-1 #23 optional_input_absent code
log orders A-8 for Jo, receipt R-A-8 #23 contribution_committed receipt
email to Ann: Order A-1 is on its way, tracking T-A-1.
log orders A-8 for Jo, receipt R-A-8 #24 hook_called audit: on step success of charge
log orders A-8 for Jo, receipt R-A-8 #25 contribution_committed audit
log orders A-1 for Ann, receipt R-A-1 #24 hook_called notify: on workflow success
log orders A-1 for Ann, receipt R-A-1 #25 journey_succeeded
log orders A-8 for Jo, receipt R-A-8 #26 attempt_started ship, attempt 1
log orders A-8 for Jo, receipt R-A-8 #27 input_adapter_supplied ship gets weight from parcels
log orders A-8 for Jo, receipt R-A-8 #28 service_provided ship gets carrier from carriers, a ch34_saying_which_attempt_took_the_money_async::MainCarrier
the main carrier books order A-8
log orders A-8 for Jo, receipt R-A-8 #29 step_info ship: the parcel leaves with receipt R-A-8
log orders A-8 for Jo, receipt R-A-8 #30 step_succeeded ship
log orders A-8 for Jo, receipt R-A-8 #31 contribution_committed tracking
log orders A-8 for Jo, receipt R-A-8 #32 optional_input_absent code
email to Jo: Order A-8 is on its way, tracking T-A-8.
log orders A-8 for Jo, receipt R-A-8 #33 hook_called notify: on workflow success
log orders A-8 for Jo, receipt R-A-8 #34 journey_succeeded
order A-1 succeeded
audit: A-1: charge took 30 on attempt 1, receipt R-A-1
order A-8 succeeded
audit: A-8: charge took 30 on attempt 2, receipt R-A-8
order A-3 failed
audit: A-3: charge ended in an error on attempt 1 (the payment network is down); check whether the bank took 120How it works
- A hook can ask for the journey ID and the attempt number.
#[journey_id]gives the hook the journey's ID, as a&JourneyId, and#[attempt]gives it the number of the attempt whose outcome it follows, as aNonZeroU32, counted from 1. Every step hook can ask for both, and every workflow hook for the journey ID. - The attempt is the one that took the money. Jo's charge failed on its first attempt, the rescue step renewed the card, and the second attempt succeeded, so Jo's audit line says
attempt 2. Ann's saysattempt 1. - A hook called after an error can ask for the error.
#[error]giveson step abnormal terminationthe error the step'srunreturned, as an&Error: for Cat's charge,the payment network is down, the message thatstep_abnormal_termination, event 13, carries.on step retryandon step failureare called after a failure as well as after an error, and a failure has no error, so they ask for it as anOption<&Error>. - The line is kept though the charge was given up. The outage's line is a contribution, committed as event 15 though the charge was given up, as chapter 23 showed, so Cat's line is in the journey's output, telling the accountants to check whether the bank took 120.
Every charge now has its line, with the journey and the attempt. But look at Jo's journey: the rescue step asks the bank's payment system for the new card, through the same payments service as the charge. The bank now answers that question from a card registry of its own, which the rescue step will have to ask instead.
Outcomes
We have shown how a hook asks for the journey ID, the attempt number and the error, as the executor knows them.
| The usual way | With Itinera |
|---|---|
| An audit trail often records that a payment happened, but not which try of a retried call took the money. | A hook asks for the journey ID and the attempt number, and the executor gives it the ones it recorded. |
| When a call to a payment service ends in an error, code often logs the error and moves on, and nobody checks whether the money was taken. | The hook called after the error asks for it, and writes it where the accountants read. |