Step 5: Restart during shipping
The steps so far ran in one process from start to end. This step stops the process in the middle of an order and starts it again, to show what FCQRS recovers and what it runs a second time. The workflow does not change; only the program does.
This page changes the program from step 4.
Stop the process inside a step
In Program.fs (Program.cs), replace the stand-in carrier. A call now takes two seconds, and
with crash the carrier stops the whole process after it ships, before it returns:
Shared setup
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 = {
Status: string
Reservation: string
Shipment: string
Confirmation: Guid
PlacedAt: DateTime
}
// 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>
// Up to five attempts, 200 ms apart.
let reserveRetry = {
Retry.times 5 with
Schedule = FixedInterval(TimeSpan.FromMilliseconds 200.0)
}
// "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
// An order over 1000 waits for a person to approve it, for at
// most three seconds in this program. With no answer by then,
// the wait returns None.
let total = request.UnitPrice * decimal request.Quantity
let approvalWait = TimeSpan.FromSeconds 3.0
let! approved =
workflow {
if total <= 1000m then
return true
else
let! answer =
waitSignalFor<bool> "approval" approvalWait
return defaultArg answer false
}
if not approved then
return {
Status = "not approved"
Reservation = reservation
Shipment = ""
Confirmation = confirmation
PlacedAt = placedAt
}
else
// 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 {
Status = "shipped"
Reservation = reservation
Shipment = shipment
Confirmation = confirmation
PlacedAt = placedAt
}
})
open System
open System.Collections.Generic
open System.Threading
open FCQRS.Workflows
open Order
// Unchanged from step 4.
let api = Unchecked.defaultof<FCQRS.Common.IActor>
type Warehouse() =
interface IWarehouse with
member _.Reserve(_, _, _) = Tasks.Task.FromResult "r-1"
Fcqrs_350-workflow-tutorial_005-restart-during-shipping.md_page.OrderSystemThreadingTasksFCQRSFCQRS.CommonContains common types like Events and Commands Functionality for Write Side.
WorkflowsFcqrs_350-workflow-tutorial_005-restart-during-shipping.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_005-restart-during-shipping.md_page.Order.OrderResultStatus: stringReservation: 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.
Fcqrs_350-workflow-tutorial_005-restart-during-shipping.md_page.Order.IWarehouseReserve: IWarehouse -> string * string * int -> Task<string>keyitemquantitySystem.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
Fcqrs_350-workflow-tutorial_005-restart-during-shipping.md_page.Order.ICarrierShip: ICarrier -> string * string -> Task<string>reservationreserveRetry: RetryPolicyFCQRS.Workflows.RetryCommon retry policies.
times: int -> RetryPolicyAt most `attempts` attempts, with backoff from one second to one minute.
Schedule: RetryScheduleThe wait between attempts.
FixedIntervalRe-send at a fixed interval.
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. is less than TimeSpan.MinValue or greater than TimeSpan.MaxValue. -or- is . -or- is . is equal to . An object that represents .
definition: IWarehouse -> ICarrier -> WorkflowDefinition<OrderRequest,OrderResult>warehouse: IWarehousecarrier: ICarrierFCQRS.Workflows.WorkflowDefines workflows.
define: string -> ('Input -> Workflow<'Result>) -> WorkflowDefinition<'Input,'Result>A workflow named `name` whose instances run `body` on their input.
request: 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.
attempt: 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.
placedAt: 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.
total: decimal(*): ^T1 -> ^T2 -> ^T3Overloaded multiplication operator The first parameter. The second parameter. The result of the operation. 8 * 6 // Evaluates to 48
decimal: ^T -> decimalConverts the argument to System.Decimal using a direct conversion for all primitive numeric types. For strings, the input is converted using UInt64.Parse() with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type. The input value. The converted decimal. decimal "42.23" // evaluates to 42.23M decimal 0xff // evaluates to 255M decimal -10 // evaluates to -10M
approvalWait: TimeSpanFromSeconds: 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 .
approved: bool(<=): 'T -> 'T -> boolStructural less-than-or-equal comparison The first parameter. The second parameter. The result of the comparison. 5 <= 1 // Evaluates to false 5 <= 5 // Evaluates to true [1; 5] <= [1; 6] // Evaluates to true
answer: bool optionwaitSignalFor: string -> TimeSpan -> Workflow<'T option>Waits for a signal named `name` for at most `timeout`, measured from the stored start of the step. Gives `None` when the time passes first; a signal that arrives later stays stored for the next wait.
boolAn abbreviation for the CLI type . Basic Types
defaultArg: 'T option -> 'T -> 'TUsed to specify a default value for an optional argument in the implementation of a function An option representing the argument. The default value of the argument. The argument value. If it is None, the defaultValue is returned. type Vector(x: double, y: double, ?z: double) = let z = defaultArg z 0.0 member this.X = x member this.Y = y member this.Z = z let v1 = Vector(1.0, 2.0) v1.Z // Evaluates to 0. let v2 = Vector(1.0, 2.0, 3.0) v2.Z // Evaluates to 3.0
``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
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.
shipment: stringShip: string * string -> Task<string>CollectionsGenericapi: FCQRS.Common.IActorMicrosoft.FSharp.Core.Operators.UncheckedThis module contains basic operations which do not apply runtime and/or static checks
defaultof: 'TGenerate a default value for any type. This is null for reference types, For structs, this is struct value where all fields have the default value. This function is unsafe in the sense that some F# values do not have proper null values.
FCQRS.Common.IActorDefines the core functionalities and context provided by the FCQRS environment to actors. This interface provides access to essential Akka.NET services and FCQRS initialization methods.
Fcqrs_350-workflow-tutorial_005-restart-during-shipping.md_page.Warehouse_: WarehouseReserve: Warehouse -> string * string * int -> Tasks.Task<string>FromResult: '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.
System.Threading.Tasks.TaskRepresents an asynchronous operation.
// A stand-in for the carrier. A call takes two seconds. With `crash`, the
// process stops after the carrier ships and before FCQRS stores the result.
type Carrier(crash: bool) =
let shipments = Dictionary<string, string>()
interface ICarrier with
member _.Ship(key, reservation) =
task {
Thread.Sleep 2000
if not (shipments.ContainsKey key) then
shipments[key] <- $"s-{shipments.Count + 1}"
printfn $" carrier shipped {reservation} (key {key})"
if crash then
printfn " the process stops before this step is stored"
exit 1
return shipments[key]
}
Fcqrs_350-workflow-tutorial_005-restart-during-shipping.md_page.Carriercrash: boolboolAn abbreviation for the CLI type . Basic Types
shipments: 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_005-restart-during-shipping.md_page.Order.ICarrier_: CarrierShip: Carrier -> string * string -> Tasks.Task<string>key: stringreservation: stringtask: TaskBuilderBuilds a task using computation expression syntax.
System.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 .
``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.
exit: int -> 'TExit the current hardware isolated process, if security settings permit, otherwise raise an exception. Calls . The exit code to use. Never returns. [<EntryPoint>] let main argv = if argv.Length = 0 then eprintfn "You must provide arguments" exit(-1) // Causes program to quit with an error code printfn "Argument count: %i" argv.Length 0
// A stand-in for the carrier. A call takes two seconds. With `crash`, the
// process stops after the carrier ships and before FCQRS stores the result.
public sealed class Carrier(bool crash) : ICarrier
{
private readonly Dictionary<string, string> shipments = new();
public Task<string> Ship(string key, string reservation)
{
Thread.Sleep(2000);
if (!shipments.ContainsKey(key))
{
shipments[key] = $"s-{shipments.Count + 1}";
Console.WriteLine($" carrier shipped {reservation} (key {key})");
}
if (crash)
{
Console.WriteLine(" the process stops before this step is stored");
Environment.Exit(1);
}
return Task.FromResult(shipments[key]);
}
}
Replace the registration. The program reads --crash from its command line and gives it to the
carrier:
// Run the program with --crash to stop it in the middle of shipping.
let crash = Environment.GetCommandLineArgs() |> Array.contains "--crash"
// Registering the workflow also resumes the orders that were running when
// the process last stopped.
let orders =
Workflows.register api (definition (Warehouse()) (Carrier crash))
crash: boolSystem.EnvironmentProvides information about, and means to manipulate, the current environment and platform. This class cannot be inherited.
GetCommandLineArgs: unit -> string arrayReturns a string array containing the command-line arguments for the current process. The system does not support command-line arguments. An array of strings where each element contains a command-line argument. The first element is the executable file name, and the following zero or more elements contain the remaining command-line arguments.
(|>): '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.Collections.ArrayModuleContains operations for working with arrays. See also F# Language Guide - Arrays.
contains: 'T -> 'T array -> boolTests if the array contains the specified element. The value to locate in the input array. The input array. True if the input array contains the specified element; false otherwise. Thrown when the input array is null. [| 1; 2 |] |> Array.contains 2 // evaluates to true [| 1; 2 |] |> Array.contains 5 // evaluates to false
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``: bool -> Carrier// Run the program with --crash to stop it in the middle of shipping.
var crash = args.Contains("--crash");
// 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(crash)));
Replace the code that starts the orders. This step places only o-1:
// 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 request =
match orders.Start id request |> Async.RunSynchronously with
| StartResult.Started -> printfn $"Started {id}"
| StartResult.AlreadyStarted status ->
printfn $"{id} is already started ({status})"
// Start an order, send it an approval if given one, and wait for its end.
let place id quantity approval =
let request = {
Customer = "ada"
Item = "widget"
Quantity = quantity
UnitPrice = 40m
}
start id request
match approval with
| Some approve ->
Workflows.signal orders id "approval" approve |> Async.RunSynchronously
| None -> ()
let outcome =
orders.Result id (TimeSpan.FromSeconds 30.0) |> Async.RunSynchronously
match outcome with
| WorkflowOutcome.Finished result when result.Status = "shipped" ->
printfn $"{id} finished: shipped as {result.Shipment}"
| WorkflowOutcome.Finished result ->
printfn $"{id} finished: {result.Status}"
| other -> printfn $"{id} ended: {other}"
place "o-1" 2 None
start: string -> OrderRequest -> unitid: stringrequest: OrderRequestorders: 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: WorkflowStatusplace: string -> int -> 'a option -> unitquantity: intapproval: 'a optionCustomer: stringItem: stringQuantity: intUnitPrice: decimalSomeThe representation of "Value of type 'T" The input value. An option representing the value.
approve: 'aFCQRS.Workflows.WorkflowsRegisters workflows with an actor system.
signal: WorkflowHandle<'Input,'Result> -> string -> string -> 'Signal -> Async<unit>Stores the signal `name` with a typed payload for the instance `id`.
NoneThe representation of "No value"
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: OrderResultStatus: string(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
other: WorkflowOutcome<OrderResult>// Start an order. A start with an ID that is already in the journal stores
// nothing and returns false.
async Task Start(string id, OrderRequest request)
{
if (await orders.StartAsync(id, request))
Console.WriteLine($"Started {id}");
else
Console.WriteLine(
$"{id} is already started ({await orders.StatusAsync(id)})");
}
// Start an order, send it an approval if given one, and wait for its end.
async Task Place(string id, int quantity, bool? approval)
{
await Start(id, new OrderRequest("ada", "widget", quantity, 40m));
if (approval is bool approve)
await orders.SignalAsync(id, "approval", approve);
var result = await orders.ResultAsync(id, TimeSpan.FromSeconds(30));
Console.WriteLine(result.Status == "shipped"
? $"{id} finished: shipped as {result.Shipment}"
: $"{id} finished: {result.Status}");
}
await Place("o-1", 2, null);
Run it with --crash
Delete bin/Debug/net11.0/orders.db, then run the program with --crash:
dotnet run -- --crash
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)
carrier shipped r-1 (key order/o-1/4)
the process stops before this step is stored
The process exited while o-1 was inside ship. The carrier shipped, but FCQRS never received the
result, so the journal has ship's StepScheduled record and no StepCompleted record for it. The
same happens when a real process is killed, loses power, or is redeployed during a call.
Run it again
Keep orders.db and run the program without the flag:
dotnet run
o-1 is already started (Running ["4 ship"])
carrier shipped r-1 (key order/o-1/4)
o-1 finished: shipped as s-1
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
Registering the workflow found o-1 in the journal and resumed it before the program asked:
the start found it already running, waiting on ship at position 4. To resume an order, FCQRS ran
the body from the beginning and replayed the stored results:
reserve-stockhad a stored result, so the warehouse was not called. Nowarehouseline appears.nowandnewGuidreturned their stored values, so the confirmation number and the order time are the ones from the first run.sleephad ended before the process stopped, and its end was stored, so it did not wait again.shiphad begun and had no result, so FCQRS ran it again, with the same key,order/o-1/4.
What the carrier must do
The carrier was called twice for one shipment. The stand-in printed carrier shipped twice
because it lost its memory of keys when the process stopped. A real carrier must store the key
with the shipment and answer a repeated key with the first shipment. FCQRS cannot do this for it:
no stored record can tell whether a call that was running when the process stopped reached the
other system.
The same holds for every activity. FCQRS gives each step a stable key, stores each result once it arrives, and never runs a step whose result is stored. The external system must make the repeated call harmless.
Next
Step 6: Charge the customer sends a command to an account from the workflow, and releases the stock when the payment is refused.