Build a cancellable pipeline
This tutorial builds a small request-style pipeline with cancellableTask.
You will:
- define operations that receive cancellation automatically
- compose those operations into a larger workflow
- run independent operations with
and! - start the pipeline by passing a
CancellationTokenat the boundary
Use cancellableTask when cancellation is part of the operation's contract.
The computation expression represents a function:
CancellationToken -> Task<'T>
That means the work does not start until the caller supplies a token.
#r "../../src/IcedTasks/bin/Release/net9.0/IcedTasks.dll"
open System
open System.Threading
open System.Threading.Tasks
open IcedTasks
Model a small request
The examples below use in-memory stand-ins for database or HTTP calls.
The important part is that each operation can receive a CancellationToken.
type Customer = {
Id: int
Name: string
IsActive: bool
}
type OrderSummary = { CustomerId: int; OpenOrders: int }
type Dashboard = {
CustomerName: string
OpenOrders: int
CreditLimit: decimal
}
module Store =
let loadCustomer customerId =
cancellableTask {
let! cancellationToken = CancellableTask.getCancellationToken ()
do! Task.Delay(10, cancellationToken)
return {
Id = customerId
Name = "Ada"
IsActive = true
}
}
let loadOpenOrders customerId =
cancellableTask {
do! fun cancellationToken -> Task.Delay(10, cancellationToken)
return {
CustomerId = customerId
OpenOrders = 3
}
}
let loadCreditLimit customerId =
task {
do! Task.Delay 10
return if customerId > 0 then 2500.00M else 0.00M
}
loadCustomer and loadOpenOrders are cancellable operations. They do not
need a token argument in their public parameter list because cancellableTask
will carry the token once the caller starts the pipeline.
loadCreditLimit returns a normal Task<decimal>. You can still bind it inside
cancellableTask; use the cancellable shape for work that needs the ambient
token and bind ordinary task-shaped APIs when you need to interoperate with
them.
Compose the pipeline
The pipeline first loads a customer. Once that result is available, it can start
the independent order and credit-limit lookups together with and!.
let buildDashboard customerId =
cancellableTask {
let! customer = Store.loadCustomer customerId
if not customer.IsActive then
return Error "The customer is inactive."
else
let! orders = Store.loadOpenOrders customer.Id
and! creditLimit = Store.loadCreditLimit customer.Id
return
Ok {
CustomerName = customer.Name
OpenOrders = orders.OpenOrders
CreditLimit = creditLimit
}
}
Use and! only when the operands are independent. The order lookup and
credit-limit lookup can both start after the customer is loaded, and neither
needs the other's result.
If a later step needs the previous result, use another sequential let!.
Start the work at the boundary
A CancellableTask<'T> is started by calling it with a CancellationToken.
In an ASP.NET app this token is usually HttpContext.RequestAborted. In a
console app or test, create a token source explicitly.
let runTutorial () =
task {
use cancellation = new CancellationTokenSource(TimeSpan.FromSeconds 2.0)
let! result = buildDashboard 42 cancellation.Token
match result with
| Ok dashboard ->
return
$"%s{dashboard.CustomerName}: %d{dashboard.OpenOrders} open orders, credit limit %M{dashboard.CreditLimit}"
| Error message -> return message
}
runTutorial().GetAwaiter().GetResult()
Next steps
For more details, continue with:
- Choosing a builder
- Use the cancellable task family for request cancellation
- Use
and!with independent operations
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
val string: value: 'T -> string
--------------------
type string = String
val decimal: value: 'T -> decimal (requires member op_Explicit)
--------------------
type decimal = Decimal
--------------------
type decimal<'Measure> = decimal
<summary> Builds a cancellableTask using computation expression syntax. </summary>
module CancellableTask from IcedTasks.CancellableTasks.CancellableTasks
<summary> Contains functional helper functions for composing and converting CancellableTask values. </summary>
--------------------
type CancellableTask = CancellationToken -> Task
<summary> CancellationToken -> Task </summary>
--------------------
type CancellableTask<'T> = CancellationToken -> Task<'T>
<summary> CancellationToken -> Task<'T> </summary>
<summary>Gets the default cancellation token for executing computations.</summary>
<returns>The default CancellationToken.</returns>
<category index="3">Cancellation and Exceptions</category>
<example id="default-cancellation-token-1"><code lang="F#"> use tokenSource = new CancellationTokenSource() let primes = [ 2; 3; 5; 7; 11 ] for i in primes do let computation = cancellableTask { let! cancellationToken = CancellableTask.getCancellationToken() do! Task.Delay(i * 1000, cancellationToken) printfn $"{i}" } computation tokenSource.Token |> ignore Thread.Sleep(6000) tokenSource.Cancel() printfn "Tasks Finished" </code> This will print "2" 2 seconds from start, "3" 3 seconds from start, "5" 5 seconds from start, cease computation and then followed by "Tasks Finished". </example>
type Task = interface IAsyncResult interface IDisposable new: action: Action -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable + 1 overload member ContinueWith: continuationAction: Action<Task,obj> * state: obj -> Task + 19 overloads member Dispose: unit -> unit member GetAwaiter: unit -> TaskAwaiter member RunSynchronously: unit -> unit + 1 overload member Start: unit -> unit + 1 overload member Wait: unit -> unit + 5 overloads ...
<summary>Represents an asynchronous operation.</summary>
--------------------
type Task<'TResult> = inherit Task new: ``function`` : Func<obj,'TResult> * state: obj -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable<'TResult> + 1 overload member ContinueWith: continuationAction: Action<Task<'TResult>,obj> * state: obj -> Task + 19 overloads member GetAwaiter: unit -> TaskAwaiter<'TResult> member WaitAsync: cancellationToken: CancellationToken -> Task<'TResult> + 4 overloads member Result: 'TResult static member Factory: TaskFactory<'TResult>
<summary>Represents an asynchronous operation that can return a value.</summary>
<typeparam name="TResult">The type of the result produced by this <see cref="T:System.Threading.Tasks.Task`1" />.</typeparam>
--------------------
Task(action: Action) : Task
Task(action: Action, cancellationToken: CancellationToken) : Task
Task(action: Action, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj) : Task
Task(action: Action, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: CancellationToken) : Task
Task(action: Action<obj>, state: obj, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task
--------------------
Task(``function`` : Func<'TResult>) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: CancellationToken) : Task<'TResult>
Task(``function`` : Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task.Delay(millisecondsDelay: int) : Task
Task.Delay(delay: TimeSpan, timeProvider: TimeProvider) : Task
Task.Delay(delay: TimeSpan, cancellationToken: CancellationToken) : Task
Task.Delay(millisecondsDelay: int, cancellationToken: CancellationToken) : Task
Task.Delay(delay: TimeSpan, timeProvider: TimeProvider, cancellationToken: CancellationToken) : Task
type CancellationTokenSource = interface IDisposable new: unit -> unit + 3 overloads member Cancel: unit -> unit + 1 overload member CancelAfter: millisecondsDelay: int -> unit + 1 overload member CancelAsync: unit -> Task member Dispose: unit -> unit member TryReset: unit -> bool static member CreateLinkedTokenSource: token: CancellationToken -> CancellationTokenSource + 3 overloads member IsCancellationRequested: bool member Token: CancellationToken
<summary>Signals to a <see cref="T:System.Threading.CancellationToken" /> that it should be canceled.</summary>
--------------------
CancellationTokenSource() : CancellationTokenSource
CancellationTokenSource(millisecondsDelay: int) : CancellationTokenSource
CancellationTokenSource(delay: TimeSpan) : CancellationTokenSource
CancellationTokenSource(delay: TimeSpan, timeProvider: TimeProvider) : CancellationTokenSource
[<Struct>] type TimeSpan = new: hours: int * minutes: int * seconds: int -> unit + 4 overloads member Add: ts: TimeSpan -> TimeSpan member CompareTo: value: obj -> int + 1 overload member Divide: divisor: float -> TimeSpan + 1 overload member Duration: unit -> TimeSpan member Equals: value: obj -> bool + 2 overloads member GetHashCode: unit -> int member Multiply: factor: float -> TimeSpan member Negate: unit -> TimeSpan member Subtract: ts: TimeSpan -> TimeSpan ...
<summary>Represents a time interval.</summary>
--------------------
TimeSpan ()
TimeSpan(ticks: int64) : TimeSpan
TimeSpan(hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int, microseconds: int) : TimeSpan
TimeSpan.FromSeconds(value: float) : TimeSpan
TimeSpan.FromSeconds(seconds: int64, ?milliseconds: int64, ?microseconds: int64) : TimeSpan
<summary>Gets the <see cref="T:System.Threading.CancellationToken" /> associated with this <see cref="T:System.Threading.CancellationTokenSource" />.</summary>
<exception cref="T:System.ObjectDisposedException">The token source has been disposed.</exception>
<returns>The <see cref="T:System.Threading.CancellationToken" /> associated with this <see cref="T:System.Threading.CancellationTokenSource" />.</returns>
IcedTasks