Stopping a journey when its log fails
What you want to do
The shop's log now writes each line to a logging service, over the network, and a write can fail. You want to know what happens to a journey whose log can't take a line, so that no order goes on with no record of what it did.
The code
The shop is chapter 25's, with its log changed. The log turns each event into a line, as before, and hands it to the logging service. The faked service can't take the line saying that the charge of order A-15 succeeded, and its error becomes the log's: ? returns it from report, so the report fails:
/// The logging service's error: it can't take a line.
#[derive(Debug)]
struct LoggingDown;
impl fmt::Display for LoggingDown {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("the logging service is down")
}
}
impl std::error::Error for LoggingDown {}
/// The shop's logging service, faked: it prints each line it takes. It is down when the line
/// saying that order A-15's charge succeeded reaches it.
#[derive(Default)]
struct LoggingService;
impl LoggingService {
fn write(&self, line: String) -> Result<(), LoggingDown> {
if line.contains(" A-15 ") && line.ends_with(" step_succeeded charge") {
return Err(LoggingDown);
}
println!("{line}");
Ok(())
}
}
/// The shop's log: it writes a line for every event of a journey to the logging service, starting
/// with the workflow's name and the journey's ID. A line the service refuses fails the report.
#[derive(Default)]
struct Log {
service: LoggingService,
}
impl Reporter for Log {
fn report(&mut self, event: &Event) -> Result<(), Error> {
let line = format!(
"log {} {} #{} {}",
event.workflow,
event.journey_id,
event.sequence,
event.kind()
);
let line = match detail(&event.body) {
Some(detail) => format!("{line} {detail}"),
None => line,
};
self.service.write(line)?;
Ok(())
}
}/// The logging service's error: it can't take a line.
#[derive(Debug)]
struct LoggingDown;
impl fmt::Display for LoggingDown {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("the logging service is down")
}
}
impl std::error::Error for LoggingDown {}
/// The shop's logging service, faked: it prints each line it takes, once the network has
/// answered. It is down when the line saying that order A-15's charge succeeded reaches it.
#[derive(Default)]
struct LoggingService;
impl LoggingService {
async fn write(&self, line: String) -> Result<(), LoggingDown> {
network().await;
if line.contains(" A-15 ") && line.ends_with(" step_succeeded charge") {
return Err(LoggingDown);
}
println!("{line}");
Ok(())
}
}
/// The shop's log: it writes a line for every event of a journey to the logging service, starting
/// with the workflow's name and the journey's ID. A line the service refuses fails the report.
#[derive(Default)]
struct Log {
service: LoggingService,
}
impl AsyncReporter for Log {
async fn report(&mut self, event: &Event) -> Result<(), Error> {
let line = format!(
"log {} {} #{} {}",
event.workflow,
event.journey_id,
event.sequence,
event.kind()
);
let line = match detail(&event.body) {
Some(detail) => format!("{line} {detail}"),
None => line,
};
self.service.write(line).await?;
Ok(())
}
}detail is the function the log has had since chapter 22.
In the asynchronous form, the service's write awaits the network, so the log implements AsyncReporter, whose report is an async fn, and the workflow names it under async_reporters in place of reporters, where the desk stays:
#[workflow(name = "orders", reporters = [Log, Desk], policies = [Notify])]#[workflow(name = "orders", reporters = [Desk], async_reporters = [Log], policies = [Notify])]The desk is chapter 11's, and notify is chapter 19's.
what_went_wrong, from chapter 25, now also names the error of a reporter that failed:
/// The reason of an abort, with the key that was missing or the reporter's error when that is what
/// happened.
fn what_went_wrong(abort: &Abort) -> String {
match abort {
Abort::RequiredDataMissing(MissingData::Key { key, .. }) => {
format!("{}: {key}", abort.reason())
}
Abort::ReporterFailed(error) => format!("{}: {error}", abort.reason()),
_ => abort.reason().to_string(),
}
}main runs two orders: Ann's, and the order of a new customer, Pat, who pays 50 for a parcel of 3 kg:
let orders = [
(
"A-1",
"Ann",
40,
Some(10),
2,
[30, 20, 10],
Some("ann-2028"),
false,
),
(
"A-15",
"Pat",
50,
None,
3,
[40, 30, 20],
Some("pat-2028"),
false,
),
];let (ann, pat) = tokio::join!(
journey(order(
"A-1",
"Ann",
40,
Some(10),
Parcel {
weight: 2,
size: [30, 20, 10],
},
"ann-2028",
)?),
journey(order(
"A-15",
"Pat",
50,
None,
Parcel {
weight: 3,
size: [40, 30, 20],
},
"pat-2028",
)?),
);In the asynchronous form, network, which the logging service awaits, order and journey are chapter 26's. It prints:
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 ch31_stopping_a_journey_when_its_log_fails::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 ch31_stopping_a_journey_when_its_log_fails::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 ch31_stopping_a_journey_when_its_log_fails::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
Pat's order:
log orders A-15 #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-15 #2 attempt_started lookup, attempt 1
log orders A-15 #3 service_provided lookup gets order_book from ledger, a ch31_stopping_a_journey_when_its_log_fails::Ledger
the order book has no order A-15 yet
log orders A-15 #4 step_succeeded lookup
log orders A-15 #5 contribution_committed completed
log orders A-15 #6 hook_called already_completed: on step success of lookup
log orders A-15 #7 attempt_started price, attempt 1
log orders A-15 #8 optional_input_absent discount
log orders A-15 #9 step_succeeded price
log orders A-15 #10 contribution_committed amount
log orders A-15 #11 attempt_started charge, attempt 1
log orders A-15 #12 service_provided charge gets payments from bank, a ch31_stopping_a_journey_when_its_log_fails::Bank
the bank charges 50 for order A-15
the journey was aborted (reporter failed: the logging service is down)log orders A-15 #1 journey_started card, customer, gift_card, order, parcel, price
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-15 #2 attempt_started lookup, attempt 1
log orders A-15 #3 service_provided lookup gets order_book from ledger, a ch31_stopping_a_journey_when_its_log_fails_async::Ledger
the order book has no order A-15 yet
log orders A-1 #3 service_provided lookup gets order_book from ledger, a ch31_stopping_a_journey_when_its_log_fails_async::Ledger
the order book has no order A-1 yet
log orders A-1 #4 step_succeeded lookup
log orders A-15 #4 step_succeeded lookup
log orders A-15 #5 contribution_committed completed
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-15 #6 hook_called already_completed: on step success of lookup
log orders A-15 #7 attempt_started price, attempt 1
log orders A-1 #7 attempt_started price, attempt 1
log orders A-1 #8 step_succeeded price
log orders A-15 #8 optional_input_absent discount
log orders A-15 #9 step_succeeded price
log orders A-1 #9 contribution_committed amount
log orders A-1 #10 attempt_started charge, attempt 1
log orders A-15 #10 contribution_committed amount
log orders A-15 #11 attempt_started charge, attempt 1
log orders A-1 #11 service_provided charge gets payments from bank, a ch31_stopping_a_journey_when_its_log_fails_async::Bank
the bank charges 30 for order A-1
log orders A-15 #12 service_provided charge gets payments from bank, a ch31_stopping_a_journey_when_its_log_fails_async::Bank
the bank charges 50 for order A-15
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 ch31_stopping_a_journey_when_its_log_fails_async::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
order A-1 succeeded
order A-15 abortedHow it works
- A reporter that fails aborts the journey. The log's
reportreturned an error on Pat'sstep_succeeded, so the executor aborted the journey, with the reasonreporter failed. A reporter can't report an error and carry on, and a journey with no record of what it did is not one the shop can trust, so it doesn't go on. - The abort is recorded at once. Nothing after it runs: the shipping never starts, so no carrier books Pat's parcel, and no workflow hook runs, so Pat is not told, as chapter 25 showed of an aborted journey.
- What the journey did stays done. The bank took Pat's 50 before the abort. No hook of
failed_shipping, from chapter 23, is called, since no shipping was given up, so nothing gives the money back. An abort is a fault to fix, as chapter 25 said: here, the shop finds Pat's payment with the bank, and refunds it before running the order again, since a new journey would charge Pat again. - The abort carries the reporter's error.
Abort::ReporterFailedholds the error the log returned, which is howwhat_went_wrongprints the service's own words. - Reporters should be written so that they never fail. A reporter's failure costs the whole journey, at whatever point it happens. A log that can lose its service should keep its lines, and send them once the service is back, rather than return the error.
Pat's journey stopped before it did anything it couldn't record. But no record holds Pat's last two events, the charge's success and the abort: the log was the only reporter that printed events, and it was the one that failed.
Outcomes
We have shown how a journey is aborted when one of its reporters fails, and what it leaves behind.
| The usual way | With Itinera |
|---|---|
| A log call that fails is often ignored, or crashes the request at whatever line made it. | A reporter that fails aborts the journey with reporter failed, at once, and the result carries its error. |
| What a request did before it crashed is found by reading the code that ran. | The events before the abort say what the journey did, up to the charge, and the shipping never started. |