Osy#betaRun it on Osyrin: osyrin.com

Reference / Workflow

Tasks that wait (Task.WhenAll across days)

var t = F(x); · await t · await Task.WhenAll(F(x), F(y)) · await Task.WhenAny(F(x), G(y))

In a workflow body, a call started as a task may wait — on a child workflow run that takes days, a person's answer — and `await`, `Task.WhenAll` and `Task.WhenAny` over it hold while the run is parked. When the wait ends the run resumes with the task's result, or its exception, exactly as the C# reads. Nothing new to learn: it is `async`, `Task` and `await`.

stable1 example compiled by CIworkflowauthoringasync

Summary#

Some work is several pieces done at once, each of which waits for somebody: two parties sign a contract, three reviewers read a draft. You write that the way C# writes it — start each piece as a task, then await them:

var signed = await Task.WhenAll(Collect(buyer), Collect(seller));

Collect is an ordinary async function that awaits a child workflow run. In a workflow body the tasks may wait for as long as that takes: the run parks, and resumes where the await is written when the answers are in.

Signature#

async Task<T> F(…) { …; await Workflow.Run("label", row); …; return value; }   // a function that waits

var t = F(x);                                  // START it as a task — no `await`
var value = await t;                           // …and wait for it, now or later
var values = await Task.WhenAll(F(x), F(y));   // every result, in order
var first  = await Task.WhenAny(F(x), G(y));   // the first task to finish

Description#

What may a task wait on?#

A child workflow run — await Workflow.Run("label", row), a join of legs, or another task that waits. That covers a person (the child run waits in a state for their event), a timer the child holds, and any depth of children beneath it.

It has to be a function or method call started as a task. A lambda (Task.Run(() => …)) and a continuation run beside their caller from beginning to end and cannot wait; move the work into a function.

What does the body see when it resumes?#

What C# says it sees:

you wrotewhen the wait ends
await tthe task's result — or its exception, thrown here
await Task.WhenAll(a, b)every result, in the order written — once all have finished; a failure is thrown once the others have finished too
await Task.WhenAny(a, b)the first task to finish, succeeded or failed; await it for its result

A child run that ends at a terminal error or terminal cancel arrives as WorkflowError / WorkflowCancelled (Workflow.Run (start a workflow)), which an ordinary try/catch around the await takes.

Does a task keep its place while it waits?#

No — and you do not have to care, with one thing to know. When the wait ends the task's function runs again from its first line, and everything it already did answers from the run's record instead of happening twice:

in the taskwhen it runs again
a call that leaves the platform (Http.*, a client)answers what it answered the first time; nothing is sent (Automatic durability (steps you do not have to write))
await Workflow.Run("label", row)finds the child it started, and takes its outcome
ordinary reads and computationrun again
a runas (Actor) { … } block, or a [CommitsAlone] callruns again, as the same actor — a row it makes before the wait belongs in a step (Workflow.Once), and the compiler says so where it can see one
Guid.NewGuid(), the current timeare worked out afresh

So write a task as you would write any workflow body: what must not repeat is a step. Once the task has finished, its result is recorded and never worked out again.

What may a task write?#

Nothing of the caller's. A task runs beside the body that started it, so it may read, call out and start child runs, and it returns what should be written; the body writes it after the await (async / await and Task). Create the rows a task works on before you start it — the body's writes so far are saved before a task starts, so a row made one line earlier is there for the child run to read.

What a task does as a declared actor is the exception: a runas (Actor) { … } block inside a task works in a unit of work of its own and commits what it wrote as the block ends, as that actor ([[security-declared-actor#beside]]) — so two tasks may each act as a different actor at once, before and after their waits.

Two tasks running the same function#

Are two tasks. Task.WhenAll(Collect(buyer), Collect(seller)) starts Collect twice; each has its own record, and each finds its own child run. (Calling a step-making helper twice without starting it as a task is still refused — those two calls would share one record.)

What if the body ends while a task still waits?#

Then nobody can pick that task up again, so the child run it waits on is called off — stopped, at its own terminal cancel (What happens to work a run stops waiting for). That is what happens to the loser of a Task.WhenAny, and to everything a run was waiting for when the run itself is ended.

Examples#

using Osysharp.Workflow;

enum SignatureStatus { Requested, Signed, Refused, Withdrawn }
enum ContractStatus  { Collecting, Signed, FellThrough, Dropped }

entity Contract {
  [MaxLength(200)] string Signers = "";
  ContractStatus Status = ContractStatus.Collecting;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

entity Signature {
  [Required] Contract Contract;
  [Required, MaxLength(80)] string Party;
  SignatureStatus Status = SignatureStatus.Requested;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow SignatureFlow {
  Tracks = Signature.Status; Autostart = false; Initial = Requested;
  event Sign();
  event Refuse();
  state Requested {
    subscribe Sign();
    subscribe Refuse();
    on Sign   { goto Signed; }
    on Refuse { goto Refused; }
  }
  terminal success Signed    { }
  terminal error   Refused   { Message = "the party refused to sign"; }
  terminal cancel  Withdrawn { Message = "the request was withdrawn"; }
}

/// One party's signature: asked for, waited for — however long it takes — and answered by name.
async Task<string> Collect(Signature signature) {
  await Workflow.Run("signature", signature);
  return signature.Party;
}

workflow ContractFlow {
  Tracks = Contract.Status; Autostart = false; Initial = Collecting;
  state Collecting {
    enter {
      var buyer  = Workflow.Once("ask-buyer",  () => new Signature { Contract = this.Item, Party = "buyer" });
      var seller = Workflow.Once("ask-seller", () => new Signature { Contract = this.Item, Party = "seller" });
      try {
        var signed = await Task.WhenAll(Collect(buyer), Collect(seller));   // both asked now; the run waits for both
        this.Item.Signers = signed[0] + " and " + signed[1];
        goto Signed;
      } catch (WorkflowError e) {
        goto FellThrough;
      }
    }
  }
  terminal success Signed      { }
  terminal error   FellThrough { Message = "a party refused"; }
  terminal cancel  Dropped     { Message = "the contract was dropped"; }
}

[Test]
void the_contract_waits_for_both_signatures_and_then_carries_on_with_both() {
  var contract = new Contract { };
  Workflow.Run(contract);
  Workflow.Settle(contract);
  var buyer  = Signature.Single(s => s.Contract == contract && s.Party == "buyer");
  var seller = Signature.Single(s => s.Contract == contract && s.Party == "seller");

  SignatureFlow.RaiseSign(seller);
  Workflow.Settle(contract);
  Assert.Equal(ContractStatus.Collecting, contract.Status);     // one signature is not both

  SignatureFlow.RaiseSign(buyer);
  Workflow.Settle(contract);
  Assert.Equal(ContractStatus.Signed, contract.Status);
  Assert.Equal("buyer and seller", contract.Signers);
}

[Test]
void a_refusal_arrives_at_the_await_once_the_other_party_has_answered() {
  var contract = new Contract { };
  Workflow.Run(contract);
  Workflow.Settle(contract);
  var buyer  = Signature.Single(s => s.Contract == contract && s.Party == "buyer");
  var seller = Signature.Single(s => s.Contract == contract && s.Party == "seller");

  SignatureFlow.RaiseRefuse(buyer);
  Workflow.Settle(contract);
  Assert.Equal(ContractStatus.Collecting, contract.Status);     // `WhenAll` waits for every task

  SignatureFlow.RaiseSign(seller);
  Workflow.Settle(contract);
  Assert.Equal(ContractStatus.FellThrough, contract.Status);
}

See also#

Related

async / await and Task

C#'s async and await, with C#'s meaning. A call to a Task-returning method that you do not await where you make it…

Wait for several tasks (Task.WhenAll, Task.WhenAny)

Task.WhenAll waits for every task and answers their results in order; Task.WhenAny answers the first task to finish…

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…

Automatic durability (steps you do not have to write)

Any call that leaves the platform — an outbound client call, an external service — is made a durable step by the…

What happens to work a run stops waiting for

A run that is waiting on child runs — one it awaits, several joined, tasks that wait — and is ended from somewhere else…