# Giving up a step early

## What you want to do

Some failures can't be mended by trying again: Ned's parcel is heavier than any carrier of the shop collects, and when the bank's network fails during a charge, the rescue step, which mends only an expired card, can't help either. You want the shop to give such a step up at once, before the retry or the rescue the workflow would otherwise run.

## The code

The shipping first learns a rule of the shop's business: the shop ships no parcel over 80 kg, the most its backup carrier collects. It fails such an order with the reason `over-parcel-limit`, before it books anything:

Rust, from the program [`ch17_giving_up_a_step_early`](https://github.com/itinera-dev/book/blob/main/rust/examples/ch17_giving_up_a_step_early.rs)

```rust
/// Ships an order through the carrier, with its receipt in the parcel, gives back its tracking
/// number, and says when the parcel has left. A parcel the carrier won't collect may be taken on
/// another attempt. A gift card has nothing to ship, and the shop ships no parcel over 80 kg.
#[derive(Step)]
struct Ship {
    #[input]
    order: String,
    #[input]
    weight: i64,
    #[input]
    receipt: String,
    #[input]
    gift_card: bool,
}

#[step]
impl Ship {
    fn run(
        self,
        #[service] carrier: &dyn Carrier,
        #[contributor] mut contributor: Contributor<'_>,
        #[reporter] mut reporter: StepReporter<'_>,
    ) -> Result<Outcome, Error> {
        if self.gift_card {
            return Self::skip_because("nothing-to-ship");
        }
        if self.weight > 80 {
            return Self::fail("over-parcel-limit");
        }
        let tracking = carrier.book(&self.order, self.weight)?;
        contributor.contribute("tracking", tracking);
        if !carrier.collects(self.weight) {
            return Self::fail("too-heavy");
        }
        reporter.info(format!("the parcel leaves with receipt {}", self.receipt))?;
        Self::succeed()
    }
}
```

The trait `Carrier` it asks for is [chapter 12](https://itinera-dev.org/book/rust/12-trying-again.md)'s.

Then the shop gets two new step policies. The first, `heavy_parcels`, defines the hook `on step retry`. It asks for the reason the shipping's attempt failed with, and gives the shipping up when that reason is `over-parcel-limit`, since the limit is the same on every attempt.

Rust, from the program [`ch17_giving_up_a_step_early`](https://github.com/itinera-dev/book/blob/main/rust/examples/ch17_giving_up_a_step_early.rs)

```rust
/// Gives up the shipping of a parcel over the shop's limit, rather than try it again: the limit
/// is the same on every attempt, whichever carrier comes.
#[derive(Default)]
struct HeavyParcels;

#[step_policy(name = "heavy_parcels")]
impl HeavyParcels {
    #[on_step_retry]
    fn before_retrying(
        &self,
        #[reason] reason: Option<Reason>,
    ) -> Result<Option<FailWorkflow>, Error> {
        if reason.as_ref().map(Reason::code) == Some("over-parcel-limit") {
            return Self::fail_workflow("no-carrier-can-take-it");
        }
        Self::carry_on()
    }
}
```

The second, `outage`, defines the hook `on step abnormal termination`. It asks for nothing, and gives up the step it is attached to whenever an error escapes it.

Rust, from the program [`ch17_giving_up_a_step_early`](https://github.com/itinera-dev/book/blob/main/rust/examples/ch17_giving_up_a_step_early.rs)

```rust
/// 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.
#[derive(Default)]
struct Outage;

#[step_policy(name = "outage")]
impl Outage {
    #[on_step_abnormal_termination]
    fn ended_in_error(&self) -> Result<Option<FailWorkflow>, Error> {
        Self::fail_workflow("payment-network-down")
    }
}
```

The workflow attaches `outage` to the charge, beside the audit, and `heavy_parcels` to the shipping:

Rust, from the program [`ch17_giving_up_a_step_early`](https://github.com/itinera-dev/book/blob/main/rust/examples/ch17_giving_up_a_step_early.rs)

```rust
/// The shop's workflow: it charges, repairing a charge that fails and auditing one that succeeds,
/// then ships, and shipping may be tried once more.
#[workflow(name = "orders", reporters = [Log, Desk])]
#[with_step(Charge, name = "charge", rescue = RenewCard, policies = [Audit, Outage])]
#[with_step(Ship, name = "ship", retry_budget = 1, policies = [HeavyParcels])]
struct Orders;
```

The charge and the audit are [chapter 15](https://itinera-dev.org/book/rust/15-reacting-to-what-a-step-did.md)'s, the rescue step `RenewCard` is [chapter 14](https://itinera-dev.org/book/rust/14-repairing-then-trying-once-more.md)'s and the desk is [chapter 11](https://itinera-dev.org/book/rust/11-naming-a-journey.md)'s.

The log is [chapter 16](https://itinera-dev.org/book/rust/16-skipping-a-step.md)'s, and now also says which policy gave a step up, and why:

Rust, from the program [`ch17_giving_up_a_step_early`](https://github.com/itinera-dev/book/blob/main/rust/examples/ch17_giving_up_a_step_early.rs)

```rust
EventBody::StepGivenUp {
    step,
    cause: GiveUpCause::FailWorkflow {
        decided_by, reason, ..
    },
    ..
} => Some(format!(
    "{}: FailWorkflow from {}, {}",
    step.step,
    decided_by.policy,
    reason.code()
)),
```

The rest is [chapter 16](https://itinera-dev.org/book/rust/16-skipping-a-step.md)'s shop. It runs the same five orders, and Cat's again, whose 120 is more than the bank's network can take, as in [chapter 12](https://itinera-dev.org/book/rust/12-trying-again.md). It prints:

What `ch17_giving_up_a_step_early` prints, recorded in [`ch17_giving_up_a_step_early.txt`](https://github.com/itinera-dev/book/blob/main/rust/expected/ch17_giving_up_a_step_early.txt)

```text
Ann's order:
log orders A-1 #1 journey_started card, gift_card, order, price, weight
log orders A-1 #2 attempt_started charge, attempt 1
log orders A-1 #3 service_provided charge gets payments from bank, a ch17_giving_up_a_step_early::Bank
the bank charges 40 for order A-1
log orders A-1 #4 step_succeeded charge
log orders A-1 #5 contribution_committed receipt
log orders A-1 #6 hook_called audit: on step success of charge
log orders A-1 #7 contribution_committed audit
log orders A-1 #8 attempt_started ship, attempt 1
log orders A-1 #9 service_provided ship gets carrier from carriers, a ch17_giving_up_a_step_early::MainCarrier
the main carrier books order A-1
log orders A-1 #10 step_info ship: the parcel leaves with receipt R-A-1
log orders A-1 #11 step_succeeded ship
log orders A-1 #12 contribution_committed tracking
log orders A-1 #13 journey_succeeded
the journey succeeded, with receipt R-A-1 and tracking T-A-1
audit: charge took 40, receipt R-A-1
Gil's order:
log orders A-7 #1 journey_started card, gift_card, order, price, weight
log orders A-7 #2 attempt_started charge, attempt 1
log orders A-7 #3 service_provided charge gets payments from bank, a ch17_giving_up_a_step_early::Bank
the bank charges 45 for order A-7
log orders A-7 #4 step_succeeded charge
log orders A-7 #5 contribution_committed receipt
log orders A-7 #6 hook_called audit: on step success of charge
log orders A-7 #7 contribution_committed audit
log orders A-7 #8 attempt_started ship, attempt 1
log orders A-7 #9 service_provided ship gets carrier from carriers, a ch17_giving_up_a_step_early::MainCarrier
the main carrier books order A-7
log orders A-7 #10 step_failed ship: too-heavy
log orders A-7 #11 hook_called heavy_parcels: on step retry of ship
log orders A-7 #12 step_retrying ship
log orders A-7 #13 attempt_started ship, attempt 2
log orders A-7 #14 service_provided ship gets carrier from carriers, a ch17_giving_up_a_step_early::BackupCarrier
the backup carrier books order A-7
log orders A-7 #15 step_info ship: the parcel leaves with receipt R-A-7
log orders A-7 #16 step_succeeded ship
log orders A-7 #17 contribution_committed tracking
log orders A-7 #18 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, gift_card, order, price, weight
log orders A-8 #2 attempt_started charge, attempt 1
log orders A-8 #3 service_provided charge gets payments from bank, a ch17_giving_up_a_step_early::Bank
the bank declines card jo-2024: it has expired
log orders A-8 #4 step_failed charge: card-expired
log orders A-8 #5 step_rescuing charge: failure, rescued by charge_rescue
log orders A-8 #6 attempt_started charge_rescue, attempt 1
the bank says card jo-2024 was replaced by jo-2029
log orders A-8 #7 step_succeeded charge_rescue
log orders A-8 #8 contribution_committed card
log orders A-8 #9 data_overwritten card
log orders A-8 #10 step_rescued charge by charge_rescue
log orders A-8 #11 attempt_started charge, attempt 2
the bank charges 30 for order A-8
log orders A-8 #12 step_succeeded charge
log orders A-8 #13 contribution_committed receipt
log orders A-8 #14 hook_called audit: on step success of charge
log orders A-8 #15 contribution_committed audit
log orders A-8 #16 attempt_started ship, attempt 1
log orders A-8 #17 service_provided ship gets carrier from carriers, a ch17_giving_up_a_step_early::MainCarrier
the main carrier books order A-8
log orders A-8 #18 step_info ship: the parcel leaves with receipt R-A-8
log orders A-8 #19 step_succeeded ship
log orders A-8 #20 contribution_committed tracking
log orders A-8 #21 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, gift_card, order, price, weight
log orders A-9 #2 attempt_started charge, attempt 1
log orders A-9 #3 service_provided charge gets payments from bank, a ch17_giving_up_a_step_early::Bank
the bank charges 25 for order A-9
log orders A-9 #4 step_succeeded charge
log orders A-9 #5 contribution_committed receipt
log orders A-9 #6 contribution_committed code
log orders A-9 #7 hook_called audit: on step success of charge
log orders A-9 #8 contribution_committed audit
log orders A-9 #9 attempt_started ship, attempt 1
log orders A-9 #10 service_provided ship gets carrier from carriers, a ch17_giving_up_a_step_early::MainCarrier
log orders A-9 #11 step_skipped ship: nothing-to-ship
log orders A-9 #12 contributions_discarded ship
log orders A-9 #13 journey_succeeded
the journey succeeded, with receipt R-A-9 and tracking none
audit: charge took 25, receipt R-A-9
Ned's order:
log orders A-12 #1 journey_started card, gift_card, order, price, weight
log orders A-12 #2 attempt_started charge, attempt 1
log orders A-12 #3 service_provided charge gets payments from bank, a ch17_giving_up_a_step_early::Bank
the bank charges 50 for order A-12
log orders A-12 #4 step_succeeded charge
log orders A-12 #5 contribution_committed receipt
log orders A-12 #6 hook_called audit: on step success of charge
log orders A-12 #7 contribution_committed audit
log orders A-12 #8 attempt_started ship, attempt 1
log orders A-12 #9 service_provided ship gets carrier from carriers, a ch17_giving_up_a_step_early::MainCarrier
log orders A-12 #10 step_failed ship: over-parcel-limit
log orders A-12 #11 hook_called heavy_parcels: on step retry of ship
log orders A-12 #12 step_given_up ship: FailWorkflow from heavy_parcels, no-carrier-can-take-it
log orders A-12 #13 journey_failed ship
desk: order A-12 failed at ship
the journey failed, with receipt R-A-12 and tracking none
audit: charge took 50, receipt R-A-12
Cat's order:
log orders A-3 #1 journey_started card, gift_card, order, price, weight
log orders A-3 #2 attempt_started charge, attempt 1
log orders A-3 #3 service_provided charge gets payments from bank, a ch17_giving_up_a_step_early::Bank
the bank's network fails while charging order A-3
log orders A-3 #4 step_abnormal_termination charge: the payment network is down
log orders A-3 #5 hook_called outage: on step abnormal termination of charge
log orders A-3 #6 step_given_up charge: FailWorkflow from outage, payment-network-down
log orders A-3 #7 journey_failed charge
desk: order A-3 failed at charge
the journey failed, with receipt none and tracking none
audit: none
```

## How it works

1. **A hook can end the journey.** A hook's `Option` holds a **lifecycle** when it returns `Some`: a decision that ends the journey there. `FailWorkflow` fails it, with a reason, a code as a step's failure has. `Self::fail_workflow(...)` returns it, and `Self::carry_on()` still returns `None`, leaving what happens next unchanged.
2. **`on step retry` runs before a retry.** When a step that failed still has a retry to spend, the executor calls it first. Gil's parcel of 60 kg is within the limit, but the main carrier won't collect it, so the shipping fails with `too-heavy`, the hook carries on, and the backup carrier takes the parcel on attempt 2. Ned's parcel of 90 kg is over the limit, so the shipping fails with `over-parcel-limit`, and the hook fails the journey: the shipping is given up with the cause `FailWorkflow`, decided by `heavy_parcels`, and never meets the backup carrier. **The retry budget says how many retries a step may have; a policy can still decide that none of them can help.**
3. **The step says what happened, and the policy decides what to do about it.** The limit is a rule of the business, so it lives in the shipping, which reports it as a reason. The hook only branches on it, asking for it with `#[reason]`, as an `Option<Reason>`, because an attempt that ended in an error reported no reason.
4. **`on step abnormal termination` runs first after an error.** It is called as soon as an error ends an attempt, before the executor decides between a retry, a rescue and giving up. Cat's charge ended in the bank's error, so `outage` failed the journey with the reason `payment-network-down`, and the charge was given up at once. Its rescue step never ran.
5. **A step given up by a hook says who decided.** `step_given_up` carries the cause `FailWorkflow`, the policy that returned it, and its reason, then `journey_failed` follows, naming the step. Neither step decided to give up: the shipping only reported that the parcel was over the limit, and the charge only let the bank's error escape.
6. **Each hook may return only some lifecycles, and its type says which.** `on step retry` and `on step abnormal termination` return `Option<FailWorkflow>`, since failing the journey is the only thing either may decide.

So no order spends a retry or a rescue that can't help it. But none of the shop's journeys tells the customer how it went: the lines that say whether a journey succeeded come from `main`, after each journey has ended, and no customer ever reads them.

## Outcomes

We have shown how a step policy gives a step up early, when no retry or rescue can help it.

| The usual way                                                                                                       | With Itinera                                                                                                               |
| ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Deciding that a retry is pointless usually means more conditions inside the retry loop, beside the call it retries. | A policy's `on step retry` hook decides it from the reason the step reported, attached to the step on the workflow's line. |
| What happens after an unexpected error is often decided wherever the error is caught.                               | `on step abnormal termination` decides it for the step, before any retry or rescue, and the event says which policy did.   |
