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 finishDescription#
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 wrote | when the wait ends |
|---|---|
await t | the 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 task | when 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 computation | run again |
a runas (Actor) { … } block, or a [CommitsAlone] call | runs 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 time | are 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#
- async / await and Task — starting a task, and what a task may do
- Wait for several tasks (Task.WhenAll, Task.WhenAny) — the combinators themselves
- Workflow.Run (start a workflow) — awaiting one child run, and the faults it raises
- Parallel legs (start several, then wait for them) — the same idea with a compensation per leg
- What happens to work a run stops waiting for — what happens to work a run stops waiting for