Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions samples/Eternal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@ open System
open Microsoft.Azure.WebJobs
open DurableFunctions.FSharp

let printTime =
fun (d: DateTime) -> sprintf "Printing at %s!" (d.ToShortTimeString())
|> Activity.define "PrintTime"
let printTime =
Activity.define "PrintTime" (fun (d: DateTime) -> sprintf "Printing at %s!" (d.ToShortTimeString()))

let workflow = orchestrator {
let! s = Activity.call printTime DateTime.Now
let! (s:string) = Activity.call printTime DateTime.Now
do! Orchestrator.delay (TimeSpan.FromSeconds 5.0)
return if s.Contains "00" then Stop else ContinueAsNew ()
}

let workflowWithParam delay = orchestrator {
let! s = Activity.call printTime DateTime.Now
let! (s:string) = Activity.call printTime DateTime.Now
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really weird indeed. I can't see why type inference is broken... Almost seems like a bug in the compiler. Maybe we should change the example not to use .Contains to avoid that.

do! Orchestrator.delay (TimeSpan.FromSeconds delay)
return if s.Contains "00" then Stop else ContinueAsNew (delay + 1.)
}
Expand Down
12 changes: 7 additions & 5 deletions samples/Hello.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ open DurableFunctions.FSharp
let SayHello([<ActivityTrigger>] name) =
sprintf "Hello %s!" name

[<FunctionName("HelloSequence")>]
let Run ([<OrchestrationTrigger>] context: DurableOrchestrationContext) =
context |>
orchestrator {
let workflow = orchestrator {
let! hello1 = Activity.callByName<string> "SayHello" "Tokyo"
let! hello2 = Activity.callByName<string> "SayHello" "Seattle"
let! hello3 = Activity.callByName<string> "SayHello" "London"

// returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
return [hello1; hello2; hello3]
}
}

[<FunctionName("HelloSequence")>]
let Run ([<OrchestrationTrigger>] context: DurableOrchestrationContext) =
Orchestrator.run (workflow, context)

3 changes: 3 additions & 0 deletions samples/samples.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
<Compile Include="ErrorHandling.fs" />
<Compile Include="Eternal.fs" />
<Compile Include="HttpStart.fs" />
<Compile Include="testing.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.27" />
<PackageReference Include="DurableFunctions.FSharp" Version="0.3.1" />
<PackageReference Include="TaskBuilder.fs" Version="1.0.0" />
<PackageReference Include="Moq" Version="4.12.0" />
<PackageReference Include="Expecto" Version="8.*" />
</ItemGroup>

<ItemGroup>
Expand Down
45 changes: 45 additions & 0 deletions samples/testing.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module samples.unittest
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the file name and module name should be in PascalCase to match other samples


open Microsoft.Azure.WebJobs
open DurableFunctions.FSharp
open System
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: unused System


[<FunctionName("SayHelloTest")>]
let SayHello([<ActivityTrigger>] name) =
sprintf "Hello %s!" name

[<FunctionName("HelloSequenceTest")>]
let RunWorkflow ([<OrchestrationTrigger>] context: DurableOrchestrationContextBase) =
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would you think of the following style of the test:

  • Define an activity (as in Typed example)
  • Define an orchestrator value. Don't use the activity directly but accept it as a parameter.
  • Workflow Function calls the orchestrator with the real activity as the argument
  • The test calls the orchestrator with a dummy activity as the argument, which returns hard-coded values. No mocks needed.
    Could you try this out and let me know your opinion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep - makes sense - I'll try that and see how it goes

context |>
orchestrator {
let! hello1 = Activity.callByName<string> "SayHello" "Tokyo"
let! hello2 = Activity.callByName<string> "SayHello" "Seattle"
let! hello3 = Activity.callByName<string> "SayHello" "London"

// returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
return [hello1; hello2; hello3]
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a strongly typed implementation style. I don't want to give the impression that only callByName style is testable. (I hope others are, too)


module WorkflowTests =

open Moq
open Expecto


[<Tests>]
let tests =
testList "orchestration tests" [
testAsync "Orchestration test no records" {

let mockContext = Mock<DurableOrchestrationContextBase>()
mockContext.Setup(fun c -> c.CallActivityAsync<string>("SayHello","Tokyo")).ReturnsAsync("Hello Tokyo") |> ignore
mockContext.Setup(fun c -> c.CallActivityAsync<string>("SayHello","Seattle")).ReturnsAsync("Hello Seattle") |> ignore
mockContext.Setup(fun c -> c.CallActivityAsync<string>("SayHello","London")).ReturnsAsync("Hello London") |> ignore
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks quite mechanic... Can you think of any helper functions that we could provide to make it read smoother? Aren't there any F#-y ways to mock things?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I wanted to use the Foq library to mock as it has a much nicer way of setting up the expectations using quotations but it doesn't work with the DurableOrchestrationContextBase - one of the properties has a sealed setter that causes Foq to fail (someone else already raised that issue so if it gets fixed I can swap it in)

let! results = RunWorkflow mockContext.Object |> Async.AwaitTask

Expect.hasLength results 3 "should be 3 results"
Expect.equal results.[0] "Hello Tokyo!" "Should be Hello Tokyo!"
Expect.equal results.[1] "Hello Seattle!" "Should be Hello Seattle!"
Expect.equal results.[2] "Hello London!" "Should be Hello London!"
}
]
8 changes: 4 additions & 4 deletions src/DurableFunctions.FSharp/Activity.fs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ module Activity =

/// Call an activity by name, passing an object as its input argument
/// and specifying the type to expect for the activity output.
let callByName<'a> (name: string) arg (c: DurableOrchestrationContext) =
let callByName<'a> (name: string) arg (c: DurableOrchestrationContextBase) =
c.CallActivityAsync<'a> (name, arg)

/// Call the activity with given input parameter and return its result.
let call (activity: Activity<'a, 'b>) (arg: 'a) (c: DurableOrchestrationContext) =
let call (activity: Activity<'a, 'b>) (arg: 'a) (c: DurableOrchestrationContextBase) =
c.CallActivityAsync<'b> (activity.name, arg)

let optionsBuilder = function
Expand All @@ -63,13 +63,13 @@ module Activity =

/// Call the activity with given input parameter and return its result. Apply retry
/// policy in case of call failure(s).
let callWithRetries (policy: RetryPolicy) (activity: Activity<'a, 'b>) (arg: 'a) (c: DurableOrchestrationContext) =
let callWithRetries (policy: RetryPolicy) (activity: Activity<'a, 'b>) (arg: 'a) (c: DurableOrchestrationContextBase) =
c.CallActivityWithRetryAsync<'b> (activity.name, (optionsBuilder policy), arg)

/// Call the activity by name passing an object as its input argument
/// and specifying the type to expect for the activity output. Apply retry
/// policy in case of call failure(s).
let callByNameWithRetries<'a> (policy: RetryPolicy) (name:string) arg (c: DurableOrchestrationContext) =
let callByNameWithRetries<'a> (policy: RetryPolicy) (name:string) arg (c: DurableOrchestrationContextBase) =
c.CallActivityWithRetryAsync<'a> (name, (optionsBuilder policy), arg)

/// Call all specified tasks in parallel and combine the results together. To be used
Expand Down
14 changes: 7 additions & 7 deletions src/DurableFunctions.FSharp/Orchestrator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@ type Orchestrator = class

/// Runs a workflow which expects an input parameter by reading this parameter from
/// the orchestration context.
static member run (workflow : ContextTask<'b>, context : DurableOrchestrationContext) : Task<'b> =
static member run (workflow : ContextTask<'b>, context : DurableOrchestrationContextBase) : Task<'b> =
workflow context

/// Runs a workflow which expects an input parameter by reading this parameter from
/// the orchestration context.
static member run (workflow : 'a -> ContextTask<'b>, context : DurableOrchestrationContext) : Task<'b> =
static member run (workflow : 'a -> ContextTask<'b>, context : DurableOrchestrationContextBase) : Task<'b> =
let input = context.GetInput<'a> ()
workflow input context

/// Runs an "eternal" orchestrator: a series of workflow executions chained with
/// [ContinueAsNew] calls. The orchestrator will keep running until Stop command is
/// returned from one of the workflow iterations.
/// This overload always passes [null] to [ContinueAsNew] calls.
static member runEternal (workflow : ContextTask<EternalOrchestrationCommand<unit>>, context : DurableOrchestrationContext) : Task =
static member runEternal (workflow : ContextTask<EternalOrchestrationCommand<unit>>, context : DurableOrchestrationContextBase) : Task =
let task = workflow context
task.ContinueWith (
fun (t: Task<EternalOrchestrationCommand<unit>>) ->
Expand All @@ -40,7 +40,7 @@ type Orchestrator = class
/// [ContinueAsNew] calls. The orchestrator will keep running until Stop command is
/// returned from one of the workflow iterations.
/// This overload always passes the returned value to [ContinueAsNew] calls.
static member runEternal (workflow : 'a -> ContextTask<EternalOrchestrationCommand<'a>>, context : DurableOrchestrationContext) : Task =
static member runEternal (workflow : 'a -> ContextTask<EternalOrchestrationCommand<'a>>, context : DurableOrchestrationContextBase) : Task =
let input = context.GetInput<'a> ()
let task = workflow input context
task.ContinueWith (
Expand All @@ -51,17 +51,17 @@ type Orchestrator = class
)

/// Returns a fixed value as a orchestrator.
static member ret value (_: DurableOrchestrationContext) =
static member ret value (_: DurableOrchestrationContextBase) =
Task.FromResult value

/// Delays orchestrator execution by the specified timespan.
static member delay (timespan: TimeSpan) (context: DurableOrchestrationContext) =
static member delay (timespan: TimeSpan) (context: DurableOrchestrationContextBase) =
let deadline = context.CurrentUtcDateTime.Add timespan
context.CreateTimer(deadline, CancellationToken.None)

/// Wait for an external event. maxTimeToWait specifies the longest period to wait:
/// the call will return an Error if timeout is reached.
static member waitForEvent<'a> (maxTimeToWait: TimeSpan) (eventName: string) (context: DurableOrchestrationContext) =
static member waitForEvent<'a> (maxTimeToWait: TimeSpan) (eventName: string) (context: DurableOrchestrationContextBase) =
let deadline = context.CurrentUtcDateTime.Add maxTimeToWait
let timer = context.CreateTimer(deadline, CancellationToken.None)
let event = context.WaitForExternalEvent<'a> eventName
Expand Down
10 changes: 5 additions & 5 deletions src/DurableFunctions.FSharp/OrchestratorCE.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ open Microsoft.Azure.WebJobs

module OrchestratorBuilder =

type ContextTask = DurableOrchestrationContext -> Task
type ContextTask<'a> = DurableOrchestrationContext -> Task<'a>
type ContextTask = DurableOrchestrationContextBase -> Task
type ContextTask<'a> = DurableOrchestrationContextBase -> Task<'a>

/// Represents the state of a computation:
/// either awaiting something with a continuation,
Expand All @@ -19,7 +19,7 @@ module OrchestratorBuilder =
/// We model tail calls explicitly, but still can't run them without O(n) memory usage.
| ReturnFrom of ContextTask<'a>
/// Implements the machinery of running a `Step<'m, 'm>` as a task returning a continuation task.
and StepStateMachine<'a>(firstStep, c: DurableOrchestrationContext) as this =
and StepStateMachine<'a>(firstStep, c: DurableOrchestrationContextBase) as this =
let methodBuilder = AsyncTaskMethodBuilder<'a Task>()
/// The continuation we left off awaiting on our last MoveNext().
let mutable continuation = fun () -> firstStep
Expand Down Expand Up @@ -112,7 +112,7 @@ module OrchestratorBuilder =
/// Chains together a step with its following step.
/// Note that this requires that the first step has no result.
/// This prevents constructs like `task { return 1; return 2; }`.
let rec combine (step : Step<unit>) (continuation : unit -> Step<'b>) (c: DurableOrchestrationContext) =
let rec combine (step : Step<unit>) (continuation : unit -> Step<'b>) (c: DurableOrchestrationContextBase) =
match step with
| Return _ -> continuation ()
| ReturnFrom t ->
Expand All @@ -121,7 +121,7 @@ module OrchestratorBuilder =
Await (awaitable, fun () -> combine (next()) continuation c)

/// Runs a step as a task -- with a short-circuit for immediately completed steps.
let run (firstStep : unit -> Step<'a>) (c: DurableOrchestrationContext) =
let run (firstStep : unit -> Step<'a>) (c: DurableOrchestrationContextBase) =
try
match firstStep() with
| Return x -> Task.FromResult(x)
Expand Down