RetryPolicygatedoWhileleases"> RetryPolicygatedoWhileleases">

Retries & failure isolation§

RetryPolicygatedoWhileleases

The problem§

The payment gateway times out sometimes. The fraud service has a deploy window. A warehouse API needs polling until a reservation settles. Failure handling scattered through business code turns every handler into a try/catch labyrinth — and still loses work when the worker itself dies mid-step.

Three tools, three failure classes§

1. Transient failures → a retry policy on the step§

.step("authorise", RetryPolicy.exponential(5, Duration.ofMillis(100)))

The handler just throws. The engine re-dispatches with exponential backoff, up to the cap; the attempt number is visible to the handler (Step.attempt()) when behavior should differ on a retry. The policy lives in the topology — reviewable, and shown on the console trace.

2. Business-level "stop" → a gate§

.step("validate")
.gate("in-stock")          // false ⇒ the instance ENDS CLEANLY — not an error, no alarm
.step("charge")
public boolean inStock(Order o) { return o.quantity() > 0; }

A gate separates "this order shouldn't proceed" (a normal outcome) from "something broke" (a failure). Instances ended by a gate complete without touching your error budget.

3. External dependency not ready → poll with doWhile§

Workflow.define("await-settlement")
    .doWhile("still-pending", b -> b
        .gate("not-cancelled")             // false short-circuits OUT of the loop entirely
        .step("poll")
        .sleep("backoff", Duration.ofSeconds(30)))   // parked server-side, no worker held
    .step("finish")
    .build();
public boolean stillPending(Ctx c)  { return !c.ready(); }      // loop condition, after each pass
public boolean notCancelled(Ctx c)  { return !c.cancelled(); }
public Ctx     poll(Ctx c)          { return c.withStatus(api.check(c.ref())); }

The failure class you don't handle: worker death§

A claimed step carries a lease. If the worker dies mid-step — process kill, node loss, network partition — the lease expires and the step is redelivered to another worker. No handler code participates; at-least-once execution at the step level is the engine's contract. Make handlers idempotent where the side effect demands it (an idempotency key on the payment call), and the whole class disappears.

Why this shape§

  • Failure semantics live in the graph, not in per-handler ceremony — a reviewer sees retry caps, gates, and loops in ten lines of topology.
  • A failed instance stops; it does not roll back. Wiggle deliberately has no automatic compensation today — an explicit, honest gap (see the roadmap). The saga shape — forward steps paired with compensating steps behind a choose — is expressible now, but the ergonomics are yours to build until first-class helpers land.
  • Sleeps park server-side. A 30-second backoff (or a 3-day one) holds no worker; the timer survives restarts and fires once.