Step 2: Retry the warehouse
A real warehouse is sometimes busy. In this step the stand-in warehouse fails the first two calls
for each key, and the reserve-stock step retries until a call succeeds.
This page changes the program from step 1. Each block below names the file it goes in and the block it replaces.
Give the step a retry policy
In Order.fs, the top of the file opens two more namespaces, System for TimeSpan and
FCQRS.Common for FixedInterval. In Order.cs, using static FCQRS.CSharp brings in
RetrySchedules:
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 }
Fcqrs_350-workflow-tutorial_002-retry-the-warehouse.md_page.OrderSystemThreadingTasksFCQRSFCQRS.CommonContains common types like Events and Commands Functionality for Write Side.
WorkflowsFcqrs_350-workflow-tutorial_002-retry-the-warehouse.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_002-retry-the-warehouse.md_page.Order.OrderResultReservation: stringusing 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);
In F#, add the policy before definition, and replace definition. In C#, replace the workflow
class, which now holds the policy:
Shared setup
// Unchanged from step 1.
type IWarehouse =
abstract Reserve:
key: string * item: string * quantity: int -> Task<string>
Fcqrs_350-workflow-tutorial_002-retry-the-warehouse.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 .
// Up to five attempts, 200 ms apart.
let reserveRetry = {
Retry.times 5 with
Schedule = FixedInterval(TimeSpan.FromMilliseconds 200.0)
}
reserveRetry: 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 .
// "order" is stored with every instance. It must not change.
let definition (warehouse: IWarehouse) =
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))
return { Reservation = reservation }
})
definition: IWarehouse -> WorkflowDefinition<OrderRequest,OrderResult>warehouse: IWarehouseFcqrs_350-workflow-tutorial_002-retry-the-warehouse.md_page.Order.IWarehouseFCQRS.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_002-retry-the-warehouse.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: intReservation: string// "order" is stored with every instance. It must not change.
public sealed class OrderWorkflow(IWarehouse warehouse)
: 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));
return new OrderResult(reservation);
}
}
A retry policy belongs to one step. Retry.times 5 allows five attempts. Its default schedule
waits longer after each failure, from one second up to one minute; this program waits a fixed
200 ms so that it finishes quickly. A policy can also set a deadline for the whole step, which is
measured from the step's stored start, so a restart does not extend it.
Make the warehouse busy
In Program.fs (Program.cs), replace the stand-in warehouse:
Shared setup
open System
open System.Collections.Generic
open Order
SystemCollectionsGenericFcqrs_350-workflow-tutorial_002-retry-the-warehouse.md_page.Order// A stand-in for the warehouse. It is busy for the first two calls with a
// key. It remembers keys, so a repeated call returns the first reservation.
type Warehouse() =
let calls = Dictionary<string, int>()
let reservations = Dictionary<string, string>()
interface IWarehouse with
member _.Reserve(key, item, quantity) =
task {
calls[key] <- (match calls.TryGetValue key with
| true, count -> count + 1
| _ -> 1)
if calls[key] <= 2 then
printfn $" warehouse busy (key {key})"
invalidOp "The warehouse is busy"
if not (reservations.ContainsKey key) then
reservations[key] <- $"r-{reservations.Count + 1}"
printfn
$" warehouse reserved {quantity} {item} (key {key})"
return reservations[key]
}
Fcqrs_350-workflow-tutorial_002-retry-the-warehouse.md_page.Warehousecalls: Dictionary<string,int>``.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
intAn abbreviation for the CLI type . Basic Types
reservations: Dictionary<string,string>Fcqrs_350-workflow-tutorial_002-retry-the-warehouse.md_page.Order.IWarehouse_: WarehouseReserve: Warehouse -> string * string * int -> Threading.Tasks.Task<string>key: stringitem: stringquantity: inttask: TaskBuilderBuilds a task using computation expression syntax.
Item: intGets 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.
TryGetValue: string * byref<int> -> boolGets the value associated with the specified key. The key of the value to get. When this method returns, contains the value associated with the specified key, if the key is found; otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. is . if the contains an element with the specified key; otherwise, .
count: int(+): ^T1 -> ^T2 -> ^T3Overloaded addition operator The first parameter. The second parameter. The result of the operation. 2 + 2 // Evaluates to 4 "Hello " + "World" // Evaluates to "Hello World"
(<=): '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
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.
invalidOp: string -> 'TThrow a exception The exception message. The result value. type FileReader(fileName: string) = let mutable isOpen = false member this.Open() = if isOpen then invalidOp "File is already open" // ... Here we may open the file ... isOpen <- true let reader = FileReader("journal.txt") reader.Open() // Executes fine reader.Open() // Throws System.InvalidOperationException: File is already open
``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.
// A stand-in for the warehouse. It is busy for the first two calls with a
// key. It remembers keys, so a repeated call returns the first reservation.
public sealed class Warehouse : IWarehouse
{
private readonly Dictionary<string, int> calls = new();
private readonly Dictionary<string, string> reservations = new();
public Task<string> Reserve(string key, string item, int quantity)
{
calls[key] = calls.GetValueOrDefault(key) + 1;
if (calls[key] <= 2)
{
Console.WriteLine($" warehouse busy (key {key})");
throw new InvalidOperationException("The warehouse is busy");
}
if (!reservations.ContainsKey(key))
{
reservations[key] = $"r-{reservations.Count + 1}";
Console.WriteLine(
$" warehouse reserved {quantity} {item} (key {key})");
}
return Task.FromResult(reservations[key]);
}
}
With step 1's Retry.none, the first failure would end the step. The policy above retries it.
Step 7 shows what a workflow does when a step fails for good.
Run it
Delete bin/Debug/net11.0/orders.db if step 1 left one, 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 finished: reservation r-1
o-1 is already started (Completed)
Journal of order/default-shard/o-1:
1 Started
2 StepScheduled
3 StepCompleted
4 Finished
The step made three attempts, all with the same key, order/o-1/0. The key belongs to the step,
not to the attempt, so a warehouse that remembers keys sees one request, made three times.
The journal is the same as in step 1. FCQRS does not store failed attempts, only the result of the one that succeeded. So the number of attempts is not durable either: if the process stops after the second failure, the step starts again with a fresh count of five.
What the warehouse must do
The step's key makes a repeated call recognisable, but only the warehouse can act on it. A real warehouse must store the key with the reservation and return the stored reservation when the same key arrives again. Without that, an attempt that reserved stock but failed to answer, for example because the connection dropped, would be followed by a second reservation.
Next
Step 3: Read the clock adds a confirmation number, an order time, and a shipping step.