Step 3: Read the clock
An order needs a time and a confirmation number, and after packing it ships. This step adds both, and with them the rule that matters most when you write a workflow: the body must be deterministic.
This page changes the program from step 2.
Why the body cannot read the clock
When FCQRS recovers an order, it runs the body again from the start and gives each finished step
its stored result. For that to work, the body must ask for the same steps in the same order every
time it runs. A body that reads DateTime.UtcNow gets a different time on every run, and a body
that branches on that time can ask for different steps on replay than it did the first time.
So the body reads the time, makes GUIDs, and draws random numbers only through steps. now,
newGuid, and random are steps whose value FCQRS stores the first time and replays after that.
The same holds for anything else that can change between runs, such as configuration or a database
read: get it in an activity.
FCQRS cannot detect a body that breaks this rule. The replay then runs with different values than the original run.
Add the shipping step
In Order.fs (Order.cs), the result gets three more fields:
module Order
open System
open System.Threading.Tasks
open FCQRS.Common
open FCQRS.Workflows
// What an order workflow is started with. FCQRS stores it in the journal.
type OrderRequest = {
Customer: string
Item: string
Quantity: int
UnitPrice: decimal
}
// What the workflow returns. FCQRS stores it too.
type OrderResult = {
Reservation: string
Shipment: string
Confirmation: Guid
PlacedAt: DateTime
}
Fcqrs_350-workflow-tutorial_003-read-the-clock.md_page.OrderSystemThreadingTasksFCQRSFCQRS.CommonContains common types like Events and Commands Functionality for Write Side.
WorkflowsFcqrs_350-workflow-tutorial_003-read-the-clock.md_page.Order.OrderRequestCustomer: stringstringAn abbreviation for the CLI type . Basic Types
Item: stringQuantity: intintAn abbreviation for the CLI type . Basic Types
UnitPrice: decimaldecimalAn abbreviation for the CLI type . Basic Types
Fcqrs_350-workflow-tutorial_003-read-the-clock.md_page.Order.OrderResultReservation: stringShipment: stringConfirmation: GuidSystem.GuidRepresents a globally unique identifier (GUID).
PlacedAt: DateTimeSystem.DateTimeRepresents an instant in time, typically expressed as a date and time of day.
using FCQRS.Workflows;
using FCQRS.Workflows.CSharp;
using static FCQRS.CSharp;
// What an order workflow is started with. FCQRS stores it in the journal.
public sealed record OrderRequest(
string Customer, string Item, int Quantity, decimal UnitPrice);
// What the workflow returns. FCQRS stores it too.
public sealed record OrderResult(
string Reservation, string Shipment, Guid Confirmation, DateTime PlacedAt);
Add the carrier after the warehouse:
// The warehouse is an external system. The key is the same for every
// attempt of one step, so the warehouse can recognise a repeated call.
type IWarehouse =
abstract Reserve:
key: string * item: string * quantity: int -> Task<string>
// The carrier is another external system. It returns a shipment ID.
type ICarrier =
abstract Ship: key: string * reservation: string -> Task<string>
Fcqrs_350-workflow-tutorial_003-read-the-clock.md_page.Order.IWarehouseReserve: IWarehouse -> string * string * int -> Task<string>keystringAn abbreviation for the CLI type . Basic Types
itemquantityintAn abbreviation for the CLI type . Basic Types
System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
Fcqrs_350-workflow-tutorial_003-read-the-clock.md_page.Order.ICarrierShip: ICarrier -> string * string -> Task<string>reservation// The warehouse is an external system. The key is the same for every
// attempt of one step, so the warehouse can recognise a repeated call.
public interface IWarehouse
{
Task<string> Reserve(string key, string item, int quantity);
}
// The carrier is another external system. It returns a shipment ID.
public interface ICarrier
{
Task<string> Ship(string key, string reservation);
}
Replace the workflow:
Shared setup
// Unchanged from step 2.
let reserveRetry = Retry.times 5
reserveRetry: RetryPolicyFCQRS.Workflows.RetryCommon retry policies.
times: int -> RetryPolicyAt most `attempts` attempts, with backoff from one second to one minute.
// "order" is stored with every instance. It must not change.
let definition (warehouse: IWarehouse) (carrier: ICarrier) =
Workflow.define "order" (fun (request: OrderRequest) ->
workflow {
// A failed attempt is not stored. FCQRS tries again until an
// attempt succeeds or the policy gives up.
let! reservation =
activity "reserve-stock" reserveRetry (fun attempt ->
warehouse.Reserve(
attempt.IdempotencyKey, request.Item, request.Quantity))
// The body must give the same answers on every replay, so it
// reads the clock and makes GUIDs only through steps. FCQRS
// stores their values and replays them.
let! placedAt = now
let! confirmation = newGuid
// Packing takes two seconds. The timer's due time is stored, so
// a restart does not start the wait again.
do! sleep (TimeSpan.FromSeconds 2.0)
let! shipment =
activity "ship" (Retry.times 3) (fun attempt ->
carrier.Ship(attempt.IdempotencyKey, reservation))
return {
Reservation = reservation
Shipment = shipment
Confirmation = confirmation
PlacedAt = placedAt
}
})
definition: IWarehouse -> ICarrier -> WorkflowDefinition<OrderRequest,OrderResult>warehouse: IWarehouseFcqrs_350-workflow-tutorial_003-read-the-clock.md_page.Order.IWarehousecarrier: ICarrierFcqrs_350-workflow-tutorial_003-read-the-clock.md_page.Order.ICarrierFCQRS.Workflows.WorkflowDefines workflows.
define: string -> ('Input -> Workflow<'Result>) -> WorkflowDefinition<'Input,'Result>A workflow named `name` whose instances run `body` on their input.
request: OrderRequestFcqrs_350-workflow-tutorial_003-read-the-clock.md_page.Order.OrderRequestworkflow: WorkflowBuilderBuilds a workflow: `workflow { let! x = activity ... }`.
reservation: stringactivity: string -> RetryPolicy -> (AttemptContext -> Task<'T>) -> Workflow<'T>Calls an external system. The call runs at least once: a crash after the call and before its result is stored repeats it, so pass `IdempotencyKey` to the external system. `name` is stored and must not change while instances are running.
reserveRetry: RetryPolicyattempt: AttemptContextReserve: string * string * int -> Task<string>IdempotencyKey: stringThe same for every attempt of this step, across restarts: workflow name, workflow ID, and step path. Pass it to the external system so a repeated call is recognised.
Item: stringQuantity: intplacedAt: DateTimenow: Workflow<DateTime>The current UTC time, stored so that replay returns the same value.
confirmation: GuidnewGuid: Workflow<Guid>A new GUID, stored so that replay returns the same value.
sleep: TimeSpan -> Workflow<unit>Waits for `duration`. The due time is stored when the step is first reached, so a restart does not postpone it.
System.TimeSpanRepresents a time interval.
FromSeconds: float -> TimeSpanReturns a that represents a specified number of seconds, where the specification is accurate to the nearest millisecond. A number of seconds, accurate to the nearest millisecond. is less than TimeSpan.MinValue or greater than TimeSpan.MaxValue. -or- is . -or- is . is equal to . An object that represents .
shipment: stringFCQRS.Workflows.RetryCommon retry policies.
times: int -> RetryPolicyAt most `attempts` attempts, with backoff from one second to one minute.
Ship: string * string -> Task<string>Reservation: stringShipment: stringConfirmation: GuidPlacedAt: DateTime// "order" is stored with every instance. It must not change.
public sealed class OrderWorkflow(IWarehouse warehouse, ICarrier carrier)
: Workflow<OrderRequest, OrderResult>("order")
{
// Up to five attempts, 200 ms apart.
private static readonly RetryPolicy ReserveRetry = Retry.times(5)
.WithSchedule(RetrySchedules.Fixed(TimeSpan.FromMilliseconds(200)));
public override async WorkflowTask<OrderResult> Run(
WorkflowContext ctx, OrderRequest request)
{
// A failed attempt is not stored. FCQRS tries again until an
// attempt succeeds or the policy gives up.
var reservation = await ctx.Activity("reserve-stock", ReserveRetry,
attempt => warehouse.Reserve(
attempt.IdempotencyKey, request.Item, request.Quantity));
// The body must give the same answers on every replay, so it
// reads the clock and makes GUIDs only through steps. FCQRS
// stores their values and replays them.
var placedAt = await ctx.Now();
var confirmation = await ctx.NewGuid();
// Packing takes two seconds. The timer's due time is stored, so
// a restart does not start the wait again.
await ctx.Sleep(TimeSpan.FromSeconds(2));
var shipment = await ctx.Activity("ship", Retry.times(3),
attempt => carrier.Ship(attempt.IdempotencyKey, reservation));
return new OrderResult(reservation, shipment, confirmation, placedAt);
}
}
The body now has five steps, at positions 0 to 4: reserve-stock, now, newGuid, sleep, and
ship. sleep is a timer. FCQRS stores its due time when the body reaches it, so a process
that restarts during the wait finishes the remaining time, not a new two seconds. A timer holds no
thread while it waits.
Run the shipping step
In Program.fs (Program.cs), the program's opening lines get System.Threading in F# and
FCQRS.Workflows in C#, for the loop that reads the status:
open System
open System.Collections.Generic
open System.IO
open System.Threading
open Microsoft.Data.Sqlite
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.Logging
open FCQRS.Actor
open FCQRS.FSharp
open FCQRS.Workflows
open Order
let database = Path.Combine(AppContext.BaseDirectory, "orders.db")
// FCQRS logs through Microsoft.Extensions.Logging; this program stays quiet.
let logging = LoggerFactory.Create(fun _ -> ())
// FCQRS reads optional settings from IConfiguration; this program sets none.
let configuration = ConfigurationBuilder().Build()
// The journal is a SQLite file next to the program.
let connection = Fcqrs.connect DBType.Sqlite $"Data Source={database};"
let api = Fcqrs.actor configuration logging (Some connection) "orders"
SystemCollectionsGenericIOThreadingMicrosoftDataSqliteExtensionsConfigurationLoggingFCQRSFCQRS.ActorFCQRS.FSharpIdiomatic-F# functional facade for FCQRS. Gives F# consumers the same one-call ergonomics the C# host-builder (HostExtensions.fs) gives C#, but with F# idioms: records-of-functions for the definitions, typed handles for the results, an explicit wiring pipeline, and plain helpers for saga side effects. It is a *pure addition* that wraps only the existing primitives (IActor.InitializeActor / SagaBuilder.initSimple / Projections.startTracked / InitializeSagaStarter / CreateCommandSubscription / Actor.api) and changes nothing in the C# interop layer or the core. open FCQRS.FSharp let api = Fcqrs.actor config loggerFactory (Some (Fcqrs.connect DBType.Sqlite conn)) "Cluster" let documents = Fcqrs.aggregate api { Name="Document"; Initial=...; Decide=...; Fold=... } let slugs = Fcqrs.aggregate api { Name="Slug"; Initial=...; Decide=...; Fold=... } let publication = Fcqrs.saga api (publicationDef documents.Factory slugs.Factory) Fcqrs.wireSagaStarters api [ publication ] let subs = Fcqrs.projection api (Projection.single 0 updateReadModel) // (Projection.multi when you must control which notifications publish) // send a command and await the matching aggregate reply: let! ev = documents.Send (Fcqrs.newCid()) (Fcqrs.aggregateId id) cmd (fun e -> ...)
WorkflowsFcqrs_350-workflow-tutorial_003-read-the-clock.md_page.Orderdatabase: stringSystem.IO.PathPerforms operations on instances that contain file or directory path information. These operations are performed in a cross-platform manner.
Combine: string * string -> stringCombines two strings into a path. The first path to combine. The second path to combine. .NET Framework and .NET Core versions older than 2.1: or contains one or more of the invalid characters defined in . or is . The combined paths. If one of the specified paths is a zero-length string, this method returns the other path. If contains an absolute path, this method returns .
System.AppContextProvides members for setting and retrieving data about an application's context.
BaseDirectory: stringGets the file path of the base directory that the assembly resolver uses to probe for assemblies. The file path of the base directory that the assembly resolver uses to probe for assemblies.
logging: ILoggerFactoryMicrosoft.Extensions.Logging.LoggerFactoryProduces instances of classes based on the given providers.
Create: Action<ILoggingBuilder> -> ILoggerFactoryCreates new instance of configured using provided delegate. A delegate to configure the . The that was created.
configuration: IConfigurationRoot``.ctor``: unit -> unitBuild: unit -> IConfigurationRootBuilds an with keys and values from the set of providers registered in . An with keys and values from the registered providers.
connection: ConnectionFCQRS.FSharp.Fcqrsconnect: DBType -> string -> ConnectionBuild a SQLite/etc. Connection from a raw connection string of any length.
FCQRS.Actor.DBTypeRepresents the type of database connection
SqliteSQLite using Microsoft.Data.Sqlite provider
api: FCQRS.Common.IActoractor: IConfiguration -> ILoggerFactory -> Connection option -> string -> FCQRS.Common.IActorCreate the actor system from plain values (cluster name as a string).
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
using FCQRS.Workflows;
using FCQRS.Workflows.CSharp;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using static FCQRS.CSharp;
var database = Path.Combine(AppContext.BaseDirectory, "orders.db");
// FCQRS logs through Microsoft.Extensions.Logging; this program stays quiet.
var logging = LoggerFactory.Create(_ => { });
// FCQRS reads optional settings from IConfiguration; this program sets none.
var configuration = new ConfigurationBuilder().Build();
// The journal is a SQLite file next to the program.
var actor = ActorApi.Create(
configuration, logging, $"Data Source={database};", "orders");
Add a stand-in carrier. In F#, put it after the warehouse. In C#, put it at the end of the file:
Shared setup
// Unchanged from step 2.
type Warehouse() =
interface IWarehouse with
member _.Reserve(_, _, _) = System.Threading.Tasks.Task.FromResult "r-1"
Fcqrs_350-workflow-tutorial_003-read-the-clock.md_page.WarehouseFcqrs_350-workflow-tutorial_003-read-the-clock.md_page.Order.IWarehouse_: WarehouseReserve: Warehouse -> string * string * int -> Tasks.Task<string>SystemFromResult: 'TResult -> Tasks.Task<'TResult>Creates a that's completed successfully with the specified result. The result to store into the completed task. The type of the result returned by the task. The successfully completed task.
ThreadingTasksSystem.Threading.Tasks.TaskRepresents an asynchronous operation.
// A stand-in for the carrier.
type Carrier() =
let shipments = Dictionary<string, string>()
interface ICarrier with
member _.Ship(key, reservation) =
task {
if not (shipments.ContainsKey key) then
shipments[key] <- $"s-{shipments.Count + 1}"
printfn $" carrier shipped {reservation} (key {key})"
return shipments[key]
}
Fcqrs_350-workflow-tutorial_003-read-the-clock.md_page.Carriershipments: Dictionary<string,string>``.ctor``: unit -> unitInitializes a new instance of the class that is empty, has the default initial capacity, and uses the default equality comparer for the key type.
stringAn abbreviation for the CLI type . Basic Types
Fcqrs_350-workflow-tutorial_003-read-the-clock.md_page.Order.ICarrier_: CarrierShip: Carrier -> string * string -> Tasks.Task<string>key: stringreservation: stringtask: TaskBuilderBuilds a task using computation expression syntax.
``not``: bool -> boolNegate a logical value. Not True equals False and not False equals True The value to negate. The result of the negation. not (2 + 2 = 5) // Evaluates to true // not is a function that can be compose with other functions let fileDoesNotExist = System.IO.File.Exists >> not
ContainsKey: string -> boolDetermines whether the contains the specified key. The key to locate in the . is . if the contains an element with the specified key; otherwise, .
Item: stringGets or sets the value associated with the specified key. The key of the value to get or set. is . The property is retrieved and does not exist in the collection. The value associated with the specified key. If the specified key is not found, a get operation throws a , and a set operation creates a new element with the specified key.
printfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
// A stand-in for the carrier.
public sealed class Carrier : ICarrier
{
private readonly Dictionary<string, string> shipments = new();
public Task<string> Ship(string key, string reservation)
{
if (!shipments.ContainsKey(key))
{
shipments[key] = $"s-{shipments.Count + 1}";
Console.WriteLine($" carrier shipped {reservation} (key {key})");
}
return Task.FromResult(shipments[key]);
}
}
Give the workflow the carrier when you register it:
// Registering the workflow also resumes the orders that were running when
// the process last stopped.
let orders =
Workflows.register api (definition (Warehouse()) (Carrier()))
orders: WorkflowHandle<OrderRequest,OrderResult>FCQRS.Workflows.WorkflowsRegisters workflows with an actor system.
register: FCQRS.Common.IActor -> WorkflowDefinition<'Input,'Result> -> WorkflowHandle<'Input,'Result>Registers the workflow and returns its handle. Instances that were running when the process last stopped resume as soon as this is called.
api: FCQRS.Common.IActordefinition: IWarehouse -> ICarrier -> WorkflowDefinition<OrderRequest,OrderResult>``.ctor``: unit -> Warehouse``.ctor``: unit -> Carrier// Registering the workflow also resumes the orders that were running when
// the process last stopped.
var orders = WorkflowHost.Register(
actor, new OrderWorkflow(new Warehouse(), new Carrier()));
Replace the code that starts the order. It now reads the order's status while the timer runs, and prints the new fields:
let request = {
Customer = "ada"
Item = "widget"
Quantity = 2
UnitPrice = 40m
}
// Start an order. A start with an ID that is already in the journal stores
// nothing and returns the order's status.
let start id =
match orders.Start id request |> Async.RunSynchronously with
| StartResult.Started -> printfn $"Started {id}"
| StartResult.AlreadyStarted status ->
printfn $"{id} is already started ({status})"
start "o-1"
// Read the order's status while it waits for its timer.
let rec waitForTimer () =
match orders.Status "o-1" |> Async.RunSynchronously with
| WorkflowStatus.Running steps as status
when steps |> List.exists (fun step -> step.EndsWith " sleep") ->
printfn $"o-1 status: {status}"
| _ ->
Thread.Sleep 50
waitForTimer ()
waitForTimer ()
// Wait for the order to end. The workflow does not depend on this wait.
let outcome =
orders.Result "o-1" (TimeSpan.FromSeconds 30.0) |> Async.RunSynchronously
match outcome with
| WorkflowOutcome.Finished result ->
let text = $"reservation {result.Reservation}, shipment {result.Shipment}"
printfn $"o-1 finished: {text}"
let placed = result.PlacedAt.ToString "o"
printfn $" confirmation {result.Confirmation}, placed {placed}"
| other -> printfn $"o-1 ended: {other}"
start "o-1"
request: OrderRequestCustomer: stringItem: stringQuantity: intUnitPrice: decimalstart: string -> unitid: stringorders: WorkflowHandle<OrderRequest,OrderResult>Start: string -> 'Input -> Async<StartResult>Starts the workflow with this ID. Returns once the start is stored. A second start with the same ID stores nothing and reports the existing instance.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Microsoft.FSharp.Control.FSharpAsyncHolds static members for creating and manipulating asynchronous computations. See also F# Language Guide - Async Workflows. Async Programming
RunSynchronously: Async<'T> * int option * CancellationToken option -> 'TRuns the asynchronous computation and await its result. If an exception occurs in the asynchronous computation then an exception is re-raised by this function. If no cancellation token is provided then the default cancellation token is used. The computation is started on the current thread if is null, has of true, and no timeout is specified. Otherwise the computation is started by queueing a new work item in the thread pool, and the current thread is blocked awaiting the completion of the computation. The timeout parameter is given in milliseconds. A value of -1 is equivalent to . The computation to run. The amount of time in milliseconds to wait for the result of the computation before raising a . If no value is provided for timeout then a default of -1 is used to correspond to . The cancellation token to be associated with the computation. If one is not supplied, the default cancellation token is used. The result of the computation. Starting Async Computations printfn "A" let result = async { printfn "B" do! Async.Sleep(1000) printfn "C" 17 } |> Async.RunSynchronously printfn "D" Prints "A", "B" immediately, then "C", "D" in 1 second. result is set to 17.
FCQRS.Workflows.StartResultThe answer to a start request.
StartedThis request started the workflow.
printfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
AlreadyStartedA workflow with this ID already exists. Nothing was stored.
status: WorkflowStatuswaitForTimer: unit -> unitStatus: string -> Async<WorkflowStatus>Where the instance is.
FCQRS.Workflows.WorkflowStatusWhere a workflow instance is.
RunningThe body is waiting on these steps, given as "path name".
steps: string listMicrosoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
exists: ('T -> bool) -> 'T list -> boolTests if any element of the list satisfies the given predicate. The predicate is applied to the elements of the input list. If any application returns true then the overall result is true and no further elements are tested. Otherwise, false is returned. The function to test the input elements. The input list. True if any element satisfies the predicate. let input = [1, "Kirk"; 2, "Spock"; 3, "Kenobi"] input |> List.exists (fun x -> x = (3, "Kenobi")) // evaluates true input |> List.exists (fun (n, name) -> n > 5) // evaluates false
step: stringEndsWith: string -> boolSystem.Threading.ThreadCreates and controls a thread, sets its priority, and gets its status.
Sleep: int -> unitSuspends the current thread for the specified number of milliseconds. The number of milliseconds for which the thread is suspended. If the value of the argument is zero, the thread relinquishes the remainder of its time slice to any thread of equal priority that is ready to run. If there are no other threads of equal priority that are ready to run, execution of the current thread is not suspended. The time-out value is negative and is not equal to .
outcome: WorkflowOutcome<OrderResult>Result: string -> TimeSpan -> Async<WorkflowOutcome<'Result>>Waits up to the given time for the instance to end. The wait is in memory: the workflow continues when the caller stops waiting. Raises `TimeoutException` when the time passes, and `InvalidOperationException` when no workflow with this ID was started.
System.TimeSpanRepresents a time interval.
FromSeconds: float -> TimeSpanReturns a that represents a specified number of seconds, where the specification is accurate to the nearest millisecond. A number of seconds, accurate to the nearest millisecond. is less than TimeSpan.MinValue or greater than TimeSpan.MaxValue. -or- is . -or- is . is equal to . An object that represents .
FCQRS.Workflows.WorkflowOutcome`1How a workflow ended.
FinishedThe body returned this value.
result: OrderResulttext: stringplaced: stringToString: string -> stringConverts the value of the current object to its equivalent string representation using the specified format and the formatting conventions of the current culture. A standard or custom date and time format string. The length of is 1, and it is not one of the format specifier characters defined for . -or- does not contain a valid custom format pattern. The date and time is outside the range of dates supported by the calendar used by the current culture. A string representation of value of the current object as specified by .
PlacedAt: DateTimeother: WorkflowOutcome<OrderResult>var request = new OrderRequest("ada", "widget", 2, 40m);
// Start an order. A start with an ID that is already in the journal stores
// nothing and returns false.
async Task Start(string id)
{
if (await orders.StartAsync(id, request))
Console.WriteLine($"Started {id}");
else
Console.WriteLine(
$"{id} is already started ({await orders.StatusAsync(id)})");
}
await Start("o-1");
// Read the order's status while it waits for its timer.
while (true)
{
var status = await orders.StatusAsync("o-1");
if (status is WorkflowStatus.Running running
&& running.pending.Any(step => step.EndsWith(" sleep")))
{
Console.WriteLine($"o-1 status: {status}");
break;
}
await Task.Delay(50);
}
// Wait for the order to end. The workflow does not depend on this wait.
var result = await orders.ResultAsync("o-1", TimeSpan.FromSeconds(30));
Console.WriteLine($"o-1 finished: reservation {result.Reservation}, "
+ $"shipment {result.Shipment}");
Console.WriteLine(
$" confirmation {result.Confirmation}, placed {result.PlacedAt:o}");
await Start("o-1");
Run it
Delete bin/Debug/net11.0/orders.db, then run dotnet run:
Started o-1
warehouse busy (key order/o-1/0)
warehouse busy (key order/o-1/0)
warehouse reserved 2 widget (key order/o-1/0)
o-1 status: Running ["3 sleep"]
carrier shipped r-1 (key order/o-1/4)
o-1 finished: reservation r-1, shipment s-1
confirmation …, placed …
o-1 is already started (Completed)
Journal of order/default-shard/o-1:
1 Started
2 StepScheduled
3 StepCompleted
4 StepCompleted
5 StepCompleted
6 StepScheduled
7 StepCompleted
8 StepScheduled
9 StepCompleted
10 Finished
The confirmation and the time differ on every run; the page shows them as ….
While the timer ran, the status was Running ["3 sleep"]: the order waited on the step at position
3, named sleep. The carrier's key ends in 4, the position of ship.
The journal shows the kinds of steps:
reserve-stockandshipare activities. Each has aStepScheduledrecord when it begins and aStepCompletedrecord with its result.nowandnewGuid(records 4 and 5) have onlyStepCompleted. Their value is computed and stored at once, so there is nothing to run again after a restart.sleephas aStepScheduledrecord with its due time (record 6) and aStepCompletedrecord when the time has passed (record 7).
Next
Step 4: Wait for approval makes large orders wait for a person.