Osy#betaRun it on Osyrin: osyrin.com

Reference / Workflow

What happens to work a run stops waiting for

on Withdraw { goto Withdrawn; } // …and the child run the body was awaiting is stopped too

A run that is waiting on child runs — one it awaits, several joined, tasks that wait — and is ended from somewhere else (an event sends it to a terminal, somebody stops it) calls those children off: each is stopped, at its own `terminal cancel`, and so is everything it was waiting for in turn. Nothing is left waiting for an answer nobody will read.

stable1 example compiled by CIworkflowauthoring

Summary#

A parent parked at await Workflow.Run("pick", pick) is waiting for that pick. If the order is withdrawn meanwhile — an event arm sends the parent to a terminal — the body that was waiting is gone, and nobody will ever read the pick's outcome. So the pick is called off: its run is stopped, and if the pick was itself waiting on something, that is stopped too.

You write nothing for this. It is what ending a run means.

Signature#

state Placed {
  subscribe Withdraw();
  enter { await Workflow.Run("pick", pick); goto Shipped; }   // parked here…
  on Withdraw { goto Withdrawn; }                              // …when this ends the run: the pick is stopped
}

Description#

Which runs are called off?#

The ones the run was still waiting for when the code waiting for them ended:

the run was…and then…called off
parked at await Workflow.Run(…)an event arm ended it, or somebody stopped itthe awaited child
parked at a join of legs (Parallel legs (start several, then wait for them))the sameevery leg still running
parked on tasks that wait (Tasks that wait (Task.WhenAll across days))the samethe child each task waits on
past a Task.WhenAnyits body finishedthe child the losing task still waits on

Which are not?#

Anything the run had stopped waiting for of its own accord:

  • a child started without await — fire-and-forget is a run of its own (Workflow.Run (start a workflow));
  • a leg left running on purpose with losers: Losers.Continue;
  • every child of a run whose own body carried it to its end — that body consumed its waits.

Where does a called-off run end?#

At its workflow's own terminal cancel state, when it declares one: the state it is in runs its exit { }, its slots and clocks retire, and its tracked field takes the terminal's value — so the row says what happened to it. A workflow that declares several cancel terminals ends at the one named Cancelled; one that declares none is ended without a transition, and its tracked field stays where it was.

Stopping a child ends its waits by the same rule, so a tree is called off level by level, however deep.

Does compensation run?#

No. A called-off run is stopped where it stands, as the run that was waiting for it was. If withdrawing has to undo something, say so where the withdrawal is written — in the arm that handles it — or keep the work inside a saga whose own catch routes the cancellation.

Examples#

using Osysharp.Workflow;

enum OrderStatus { Placed, Shipped, Withdrawn }
enum PickStatus  { Picking, Picked, CalledOff }

entity Order {
  [Required, MaxLength(40)] string Reference;
  OrderStatus Status = OrderStatus.Placed;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

entity Pick {
  [Required] Order Order;
  PickStatus Status = PickStatus.Picking;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow PickFlow {
  Tracks = Pick.Status; Autostart = false; Initial = Picking;
  event Report();
  state Picking { subscribe Report(); on Report { goto Picked; } }
  terminal success Picked    { }
  terminal cancel  CalledOff { Message = "nobody is waiting for this pick"; }
}

workflow OrderFlow {
  Tracks = Order.Status; Autostart = false; Initial = Placed;
  event Withdraw();
  state Placed {
    subscribe Withdraw();
    enter {
      var pick = Workflow.Once("make-pick", () => new Pick { Order = this.Item });
      await Workflow.Run("pick", pick);
      goto Shipped;
    }
    on Withdraw { goto Withdrawn; }
  }
  terminal success Shipped   { }
  terminal cancel  Withdrawn { Message = "the order was withdrawn"; }
}

[Test]
void withdrawing_the_order_stops_the_pick_it_was_waiting_for() {
  var order = new Order { Reference = "SO-1" };
  Workflow.Run(order);
  Workflow.Settle(order);
  var pick = Pick.Single(p => p.Order == order);
  Assert.Equal(PickStatus.Picking, pick.Status);        // the order is parked, waiting for it

  OrderFlow.RaiseWithdraw(order);
  Workflow.Settle(order);
  Assert.Equal(OrderStatus.Withdrawn, order.Status);
  Workflow.Settle(pick);
  Assert.Equal(PickStatus.CalledOff, pick.Status);      // …and so the pick is called off, at its own terminal
}

See also#

Related

Workflow.Run (start a workflow)

Start the workflow bound to an entity's type, on that entity. Bare — `Workflow.Run(order)` — is fire-and-forget: start…

Parallel legs (start several, then wait for them)

Start several pieces of work at once, each with its own compensation, and wait for them together. Writing…

Tasks that wait (Task.WhenAll across days)

In a workflow body, a call started as a task may wait — on a child workflow run that takes days, a person's answer —…

Transitions — where this item may go next

One row per move this instance can make right now, with each arm's guard evaluated against it. A board offers only the…