├── .gitattributes ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .nuget └── NuGet.exe ├── .paket ├── paket.bootstrapper.exe └── paket.targets ├── .travis.yml ├── FSharp.CloudAgent.sln ├── GenerateDocs.cmd ├── LICENSE.txt ├── README.md ├── RELEASE_NOTES.md ├── appveyor.yml ├── build.cmd ├── build.fsx ├── build.sh ├── docs ├── content │ ├── agent-paradigms.fsx │ ├── azure-service-bus.fsx │ ├── handling-errors.fsx │ ├── index.fsx │ ├── tutorial.fsx │ └── windows-service-bus.fsx ├── files │ └── img │ │ ├── create-a-queue.png │ │ ├── logo-template.pdn │ │ ├── logo.png │ │ └── queues-in-vs.png └── tools │ ├── generate.fsx │ ├── paket.references │ └── templates │ └── template.cshtml ├── lib └── README.md ├── nuget ├── FSharp.CloudAgent.nuspec ├── README.md └── publish.cmd ├── paket.dependencies ├── paket.lock ├── src └── FSharp.CloudAgent │ ├── Actors.fs │ ├── AssemblyInfo.fs │ ├── ConnectionFactory.fs │ ├── CustomAssemblyPermissions.fs │ ├── FSharp.CloudAgent.fsproj │ ├── Messaging.fs │ ├── Types.fs │ ├── app.config │ └── paket.references └── tests └── FSharp.CloudAgent.Tests ├── Actors.fs ├── AutomaticRetry.fs ├── FSharp.CloudAgent.Tests.fsproj ├── Helpers.fs ├── ProcessBrokeredMessage.fs ├── Script.fsx ├── app.config └── paket.references /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | # Autodetect text or binary. Do not leave merge conflict markers in the files. 5 | * text=auto merge=union 6 | 7 | # Use LF in the working directory by default. Override with core.autocrlf=true. 8 | *.fs eol=lf 9 | *fsi eol=lf 10 | 11 | # Visual Studio can read LF sln files, but it always writes them as CRLF. 12 | *.sln eol=crlf 13 | 14 | ############################################################################### 15 | # Set default behavior for command prompt diff. 16 | # 17 | # This is need for earlier builds of msysgit that does not have it on by 18 | # default for csharp files. 19 | # Note: This is only used by command line 20 | ############################################################################### 21 | #*.cs diff=csharp 22 | 23 | ############################################################################### 24 | # Set the merge driver for project and solution files 25 | # 26 | # Merging from the command prompt will add diff markers to the files if there 27 | # are conflicts (Merging from VS is not affected by the settings below, in VS 28 | # the diff markers are never inserted). Diff markers may cause the following 29 | # file extensions to fail to load in VS. An alternative would be to treat 30 | # these files as binary and thus will always conflict and require user 31 | # intervention with every merge. To do so, just uncomment the entries below 32 | ############################################################################### 33 | #*.sln merge=binary 34 | #*.csproj merge=binary 35 | #*.vbproj merge=binary 36 | #*.vcxproj merge=binary 37 | #*.vcproj merge=binary 38 | #*.dbproj merge=binary 39 | #*.fsproj merge=binary 40 | #*.lsproj merge=binary 41 | #*.wixproj merge=binary 42 | #*.modelproj merge=binary 43 | #*.sqlproj merge=binary 44 | #*.wwaproj merge=binary 45 | 46 | ############################################################################### 47 | # behavior for image files 48 | # 49 | # image files are treated as binary by default. 50 | ############################################################################### 51 | #*.jpg binary 52 | #*.png binary 53 | #*.gif binary 54 | 55 | ############################################################################### 56 | # diff behavior for common document formats 57 | # 58 | # Convert binary document formats to text before diffing them. This feature 59 | # is only available from the command line. Turn it on by uncommenting the 60 | # entries below. 61 | ############################################################################### 62 | #*.doc diff=astextplain 63 | #*.DOC diff=astextplain 64 | #*.docx diff=astextplain 65 | #*.DOCX diff=astextplain 66 | #*.dot diff=astextplain 67 | #*.DOT diff=astextplain 68 | #*.pdf diff=astextplain 69 | #*.PDF diff=astextplain 70 | #*.rtf diff=astextplain 71 | #*.RTF diff=astextplain 72 | 73 | *.sh text eol=lf 74 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Please provide a succinct description of your issue. 4 | 5 | ### Repro steps 6 | 7 | Please provide the steps required to reproduce the problem 8 | 9 | 1. Step A 10 | 11 | 2. Step B 12 | 13 | ### Expected behavior 14 | 15 | Please provide a description of the behavior you expect. 16 | 17 | ### Actual behavior 18 | 19 | Please provide a description of the actual behavior you observe. 20 | 21 | ### Known workarounds 22 | 23 | Please provide a description of any known workarounds. 24 | 25 | ### Related information 26 | 27 | * Operating system 28 | * Branch 29 | * .NET Runtime, CoreCLR or Mono Version 30 | * Performance information, links to performance testing scripts 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Xamarin Studio / monodevelop user-specific 10 | *.userprefs 11 | 12 | # Build results 13 | 14 | [Dd]ebug/ 15 | [Rr]elease/ 16 | x64/ 17 | build/ 18 | [Bb]in/ 19 | [Oo]bj/ 20 | 21 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 22 | !packages/*/build/ 23 | 24 | # MSTest test Results 25 | [Tt]est[Rr]esult*/ 26 | [Bb]uild[Ll]og.* 27 | 28 | *_i.c 29 | *_p.c 30 | *.ilk 31 | *.meta 32 | *.obj 33 | *.pch 34 | *.pdb 35 | *.pgc 36 | *.pgd 37 | *.rsp 38 | *.sbr 39 | *.tlb 40 | *.tli 41 | *.tlh 42 | *.tmp 43 | *.tmp_proj 44 | *.log 45 | *.vspscc 46 | *.vssscc 47 | .builds 48 | *.pidb 49 | *.log 50 | *.scc 51 | 52 | # Visual C++ cache files 53 | ipch/ 54 | *.aps 55 | *.ncb 56 | *.opensdf 57 | *.sdf 58 | *.cachefile 59 | 60 | # Visual Studio profiler 61 | *.psess 62 | *.vsp 63 | *.vspx 64 | 65 | # Guidance Automation Toolkit 66 | *.gpState 67 | 68 | # ReSharper is a .NET coding add-in 69 | _ReSharper*/ 70 | *.[Rr]e[Ss]harper 71 | 72 | # TeamCity is a build add-in 73 | _TeamCity* 74 | 75 | # DotCover is a Code Coverage Tool 76 | *.dotCover 77 | 78 | # NCrunch 79 | *.ncrunch* 80 | .*crunch*.local.xml 81 | 82 | # Installshield output folder 83 | [Ee]xpress/ 84 | 85 | # DocProject is a documentation generator add-in 86 | DocProject/buildhelp/ 87 | DocProject/Help/*.HxT 88 | DocProject/Help/*.HxC 89 | DocProject/Help/*.hhc 90 | DocProject/Help/*.hhk 91 | DocProject/Help/*.hhp 92 | DocProject/Help/Html2 93 | DocProject/Help/html 94 | 95 | # Click-Once directory 96 | publish/ 97 | 98 | # Publish Web Output 99 | *.Publish.xml 100 | 101 | # Enable nuget.exe in the .nuget folder (though normally executables are not tracked) 102 | !.nuget/NuGet.exe 103 | !.paket/Packet.exe.bootstrapper 104 | .paket/ 105 | 106 | # Windows Azure Build Output 107 | csx 108 | *.build.csdef 109 | 110 | # Windows Store app package directory 111 | AppPackages/ 112 | 113 | # Others 114 | sql/ 115 | *.Cache 116 | ClientBin/ 117 | [Ss]tyle[Cc]op.* 118 | ~$* 119 | *~ 120 | *.dbmdl 121 | *.[Pp]ublish.xml 122 | *.pfx 123 | *.publishsettings 124 | 125 | # RIA/Silverlight projects 126 | Generated_Code/ 127 | 128 | # Backup & report files from converting an old project file to a newer 129 | # Visual Studio version. Backup files are not needed, because we have git ;-) 130 | _UpgradeReport_Files/ 131 | Backup*/ 132 | UpgradeLog*.XML 133 | UpgradeLog*.htm 134 | 135 | # SQL Server files 136 | App_Data/*.mdf 137 | App_Data/*.ldf 138 | 139 | 140 | #LightSwitch generated files 141 | GeneratedArtifacts/ 142 | _Pvt_Extensions/ 143 | ModelManifest.xml 144 | 145 | # ========================= 146 | # Windows detritus 147 | # ========================= 148 | 149 | # Windows image file caches 150 | Thumbs.db 151 | ehthumbs.db 152 | 153 | # Folder config file 154 | Desktop.ini 155 | 156 | # Recycle Bin used on file shares 157 | $RECYCLE.BIN/ 158 | 159 | # Mac desktop service store files 160 | .DS_Store 161 | 162 | # =================================================== 163 | # Exclude F# project specific directories and files 164 | # =================================================== 165 | 166 | # NuGet Packages Directory 167 | packages/ 168 | 169 | # Generated documentation folder 170 | docs/output/ 171 | 172 | # Temp folder used for publishing docs 173 | temp/ 174 | 175 | # Test results produced by build 176 | TestResults.xml 177 | 178 | # Nuget outputs 179 | nuget/*.nupkg 180 | -------------------------------------------------------------------------------- /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FSharp.CloudAgent/4510c97154528efe072bbcd2329301249f66ae1c/.nuget/NuGet.exe -------------------------------------------------------------------------------- /.paket/paket.bootstrapper.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FSharp.CloudAgent/4510c97154528efe072bbcd2329301249f66ae1c/.paket/paket.bootstrapper.exe -------------------------------------------------------------------------------- /.paket/paket.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | true 8 | $(MSBuildThisFileDirectory) 9 | $(MSBuildThisFileDirectory)..\ 10 | 11 | 12 | 13 | $(PaketToolsPath)paket.exe 14 | $(PaketToolsPath)paket.bootstrapper.exe 15 | "$(PaketExePath)" 16 | mono --runtime=v4.0.30319 $(PaketExePath) 17 | "$(PaketBootStrapperExePath)" 18 | mono --runtime=v4.0.30319 $(PaketBootStrapperExePath) 19 | 20 | $(PaketCommand) restore 21 | $(PaketBootStrapperCommand) 22 | 23 | RestorePackages; $(BuildDependsOn); 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | env: 4 | matrix: 5 | - MONO_VERSION="3.4.0" 6 | 7 | install: 8 | - wget "http://download.mono-project.com/archive/${MONO_VERSION}/macos-10-x86/MonoFramework-MDK-${MONO_VERSION}.macos10.xamarin.x86.pkg" 9 | - sudo installer -pkg "MonoFramework-MDK-${MONO_VERSION}.macos10.xamarin.x86.pkg" -target / 10 | 11 | script: 12 | - ./build.sh All 13 | -------------------------------------------------------------------------------- /FSharp.CloudAgent.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 2013 3 | VisualStudioVersion = 12.0.31101.0 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{50DF2A1D-B16F-4226-9E32-91999C64DB5D}" 6 | ProjectSection(SolutionItems) = preProject 7 | paket.dependencies = paket.dependencies 8 | paket.lock = paket.lock 9 | EndProjectSection 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{A6A6AF7D-D6E3-442D-9B1E-58CC91879BE1}" 12 | EndProject 13 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.CloudAgent", "src\FSharp.CloudAgent\FSharp.CloudAgent.fsproj", "{F6147CD2-21E8-41CE-926F-9AB32EAB770D}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "project", "project", "{BF60BC93-E09B-4E5F-9D85-95A519479D54}" 16 | ProjectSection(SolutionItems) = preProject 17 | build.fsx = build.fsx 18 | nuget\FSharp.CloudAgent.nuspec = nuget\FSharp.CloudAgent.nuspec 19 | README.md = README.md 20 | RELEASE_NOTES.md = RELEASE_NOTES.md 21 | EndProjectSection 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{83F16175-43B1-4C90-A1EE-8E351C33435D}" 24 | ProjectSection(SolutionItems) = preProject 25 | docs\tools\generate.fsx = docs\tools\generate.fsx 26 | docs\tools\templates\template.cshtml = docs\tools\templates\template.cshtml 27 | EndProjectSection 28 | EndProject 29 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "content", "content", "{8E6D5255-776D-4B61-85F9-73C37AA1FB9A}" 30 | ProjectSection(SolutionItems) = preProject 31 | docs\content\agent-paradigms.fsx = docs\content\agent-paradigms.fsx 32 | docs\content\azure-service-bus.fsx = docs\content\azure-service-bus.fsx 33 | docs\content\handling-errors.fsx = docs\content\handling-errors.fsx 34 | docs\content\index.fsx = docs\content\index.fsx 35 | docs\content\tutorial.fsx = docs\content\tutorial.fsx 36 | docs\content\windows-service-bus.fsx = docs\content\windows-service-bus.fsx 37 | EndProjectSection 38 | EndProject 39 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{ED8079DD-2B06-4030-9F0F-DC548F98E1C4}" 40 | EndProject 41 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.CloudAgent.Tests", "tests\FSharp.CloudAgent.Tests\FSharp.CloudAgent.Tests.fsproj", "{E789C72A-5CFD-436B-8EF1-61AA2852A89F}" 42 | EndProject 43 | Global 44 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 45 | Debug|Any CPU = Debug|Any CPU 46 | Release|Any CPU = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 49 | {F6147CD2-21E8-41CE-926F-9AB32EAB770D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {F6147CD2-21E8-41CE-926F-9AB32EAB770D}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {F6147CD2-21E8-41CE-926F-9AB32EAB770D}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {F6147CD2-21E8-41CE-926F-9AB32EAB770D}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {E789C72A-5CFD-436B-8EF1-61AA2852A89F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {E789C72A-5CFD-436B-8EF1-61AA2852A89F}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {E789C72A-5CFD-436B-8EF1-61AA2852A89F}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {E789C72A-5CFD-436B-8EF1-61AA2852A89F}.Release|Any CPU.Build.0 = Release|Any CPU 57 | EndGlobalSection 58 | GlobalSection(SolutionProperties) = preSolution 59 | HideSolutionNode = FALSE 60 | EndGlobalSection 61 | GlobalSection(NestedProjects) = preSolution 62 | {83F16175-43B1-4C90-A1EE-8E351C33435D} = {A6A6AF7D-D6E3-442D-9B1E-58CC91879BE1} 63 | {8E6D5255-776D-4B61-85F9-73C37AA1FB9A} = {A6A6AF7D-D6E3-442D-9B1E-58CC91879BE1} 64 | {E789C72A-5CFD-436B-8EF1-61AA2852A89F} = {ED8079DD-2B06-4030-9F0F-DC548F98E1C4} 65 | EndGlobalSection 66 | EndGlobal 67 | -------------------------------------------------------------------------------- /GenerateDocs.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | 4 | packages\FAKE\tools\FAKE.exe build.fsx GenerateDocs 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FSharp.CloudAgent is a simple framework for making distributed F# Agents in the cloud with the minimum of hassle. See http://fsprojects.github.io/FSharp.CloudAgent/ for full documentation. 2 | 3 | 4 | ## Maintainer(s) 5 | 6 | - [@isaacabraham](https://github.com/isaacabraham) 7 | 8 | The default maintainer account for projects under "fsprojects" is [@fsprojectsgit](https://github.com/fsprojectsgit) - F# Community Project Incubation Space (repo management) 9 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ### 0.1 - Initial release 2 | * Basic distributed Agents 3 | * Reliable agents backed by Service Bus queues 4 | * Workers Pools 5 | * Actor Pools 6 | 7 | ### 0.2 - Minor enhancements 8 | * Fixed a couple of bugs in resilient agents. 9 | 10 | ### 0.3 11 | * Explicit session abandonment. 12 | * Batch message support. -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf input 3 | build_script: 4 | - cmd: build.cmd 5 | test: off 6 | os: Visual Studio 2017 7 | version: 0.0.1.{build} 8 | artifacts: 9 | - path: bin 10 | name: bin 11 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | 4 | IF NOT EXIST packages\FAKE\tools\FAKE.exe ( 5 | .paket\paket.bootstrapper.exe 6 | .paket\paket.exe restore 7 | ) 8 | 9 | cd .\src\FSharp.CloudAgent\ 10 | dotnet restore 11 | cd ..\..\tests\FSharp.CloudAgent.Tests\ 12 | dotnet restore 13 | cd ..\..\ 14 | 15 | IF NOT EXIST build.fsx ( 16 | packages\FAKE\tools\FAKE.exe init.fsx 17 | ) 18 | packages\FAKE\tools\FAKE.exe build.fsx %* 19 | -------------------------------------------------------------------------------- /build.fsx: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------- 2 | // FAKE build script 3 | // -------------------------------------------------------------------------------------- 4 | 5 | #r @"packages/FAKE/tools/NuGet.Core.dll" 6 | open Fake.Testing 7 | #r @"packages/FAKE/tools/FakeLib.dll" 8 | open Fake 9 | open Fake.Git 10 | open Fake.AssemblyInfoFile 11 | open Fake.ReleaseNotesHelper 12 | open System 13 | #if MONO 14 | #else 15 | #load "packages/SourceLink.Fake/Tools/Fake.fsx" 16 | open SourceLink 17 | #endif 18 | 19 | let project = "FSharp.CloudAgent" 20 | let summary = "Allows the use of distributed F# Agents in Azure." 21 | let description = "FSharp.CloudAgent provides the capability to run standard F# Agents on top of Azure to allow massively distributed processing of workloads in a resilient manner using Azure Service Bus, either as pools of simple workers or actors." 22 | let authors = [ "Isaac Abraham" ] 23 | let tags = "f# agent actor azure service-bus" 24 | let solutionFile = "FSharp.CloudAgent.sln" 25 | let testAssemblies = "tests/**/bin/Release/net451/*Tests*.dll" 26 | let gitHome = "https://github.com/isaacabraham" 27 | let gitName = "FSharp.CloudAgent" 28 | let gitRaw = environVarOrDefault "gitRaw" "https://raw.github.com/isaacabraham" 29 | Environment.CurrentDirectory <- __SOURCE_DIRECTORY__ 30 | 31 | 32 | 33 | 34 | 35 | // Read additional information from the release notes document 36 | let release = parseReleaseNotes (IO.File.ReadAllLines "RELEASE_NOTES.md") 37 | 38 | let genFSAssemblyInfo (projectPath) = 39 | let projectName = System.IO.Path.GetFileNameWithoutExtension(projectPath) 40 | let basePath = "src/" + projectName 41 | let fileName = basePath + "/AssemblyInfo.fs" 42 | CreateFSharpAssemblyInfo fileName 43 | [ Attribute.Title (projectName) 44 | Attribute.Product project 45 | Attribute.Description summary 46 | Attribute.Version release.AssemblyVersion 47 | Attribute.FileVersion release.AssemblyVersion ] 48 | 49 | let genCSAssemblyInfo (projectPath) = 50 | let projectName = System.IO.Path.GetFileNameWithoutExtension(projectPath) 51 | let basePath = "src/" + projectName + "/Properties" 52 | let fileName = basePath + "/AssemblyInfo.cs" 53 | CreateCSharpAssemblyInfo fileName 54 | [ Attribute.Title (projectName) 55 | Attribute.Product project 56 | Attribute.Description summary 57 | Attribute.Version release.AssemblyVersion 58 | Attribute.FileVersion release.AssemblyVersion ] 59 | 60 | // Generate assembly info files with the right version & up-to-date information 61 | Target "AssemblyInfo" (fun _ -> 62 | let fsProjs = !! "src/**/*.fsproj" 63 | let csProjs = !! "src/**/*.csproj" 64 | fsProjs |> Seq.iter genFSAssemblyInfo 65 | csProjs |> Seq.iter genCSAssemblyInfo 66 | ) 67 | 68 | // -------------------------------------------------------------------------------------- 69 | // Clean build results & restore NuGet packages 70 | 71 | Target "RestorePackages" RestorePackages 72 | 73 | Target "Clean" (fun _ -> 74 | CleanDirs ["bin"; "temp"] 75 | ) 76 | 77 | Target "CleanDocs" (fun _ -> 78 | CleanDirs ["docs/output"] 79 | ) 80 | 81 | // -------------------------------------------------------------------------------------- 82 | // Build library & test project 83 | 84 | Target "Build" (fun _ -> 85 | !! solutionFile 86 | |> MSBuildRelease "" "Rebuild" 87 | |> ignore 88 | ) 89 | 90 | // -------------------------------------------------------------------------------------- 91 | // Run the unit tests using test runner 92 | 93 | Target "RunTests" (fun _ -> 94 | !! testAssemblies 95 | |> NUnit3 (fun p -> 96 | { p with 97 | ShadowCopy = true 98 | TimeOut = TimeSpan.FromMinutes 20. }) 99 | ) 100 | 101 | #if MONO 102 | #else 103 | // -------------------------------------------------------------------------------------- 104 | // SourceLink allows Source Indexing on the PDB generated by the compiler, this allows 105 | // the ability to step through the source code of external libraries https://github.com/ctaggart/SourceLink 106 | 107 | Target "SourceLink" (fun _ -> 108 | let baseUrl = sprintf "%s/%s/{0}/%%var2%%" gitRaw (project.ToLower()) 109 | use repo = new GitRepo(__SOURCE_DIRECTORY__) 110 | !! "src/**/*.fsproj" 111 | |> Seq.iter (fun f -> 112 | let proj = VsProj.LoadRelease f 113 | logfn "source linking %s" proj.OutputFilePdb 114 | let files = proj.Compiles -- "**/AssemblyInfo.fs" 115 | repo.VerifyChecksums files 116 | proj.VerifyPdbChecksums files 117 | proj.CreateSrcSrv baseUrl repo.Revision (repo.Paths files) 118 | Pdbstr.exec proj.OutputFilePdb proj.OutputFilePdbSrcSrv 119 | ) 120 | ) 121 | #endif 122 | 123 | // -------------------------------------------------------------------------------------- 124 | // Build a NuGet package 125 | 126 | let asDependency name = name, GetPackageVersion "packages" name 127 | 128 | Target "NuGet" (fun _ -> 129 | NuGet (fun p -> 130 | { p with 131 | Authors = authors 132 | Project = project 133 | Summary = summary 134 | Description = description 135 | Version = release.NugetVersion 136 | ReleaseNotes = String.Join(Environment.NewLine, release.Notes) 137 | Tags = tags 138 | OutputPath = "bin" 139 | AccessKey = getBuildParamOrDefault "nugetkey" "" 140 | Publish = hasBuildParam "nugetkey" 141 | Dependencies = [ "WindowsAzure.ServiceBus" 142 | "Newtonsoft.Json" ] |> List.map asDependency }) 143 | ("nuget/" + project + ".nuspec") 144 | ) 145 | 146 | // -------------------------------------------------------------------------------------- 147 | // Generate the documentation 148 | 149 | Target "GenerateReferenceDocs" (fun _ -> 150 | if not <| executeFSIWithArgs "docs/tools" "generate.fsx" ["--define:RELEASE"; "--define:REFERENCE"] [] then 151 | failwith "generating reference documentation failed" 152 | ) 153 | 154 | Target "GenerateHelp" (fun _ -> 155 | if not <| executeFSIWithArgs "docs/tools" "generate.fsx" ["--define:RELEASE"; "--define:HELP"] [] then 156 | failwith "generating help documentation failed" 157 | ) 158 | 159 | Target "GenerateDocs" DoNothing 160 | 161 | // -------------------------------------------------------------------------------------- 162 | // Release Scripts 163 | 164 | Target "ReleaseDocs" (fun _ -> 165 | let tempDocsDir = "temp/gh-pages" 166 | CleanDir tempDocsDir 167 | Repository.cloneSingleBranch "" (gitHome + "/" + gitName + ".git") "gh-pages" tempDocsDir 168 | 169 | CopyRecursive "docs/output" tempDocsDir true |> tracefn "%A" 170 | StageAll tempDocsDir 171 | Commit tempDocsDir (sprintf "Update generated documentation for version %s" release.NugetVersion) 172 | Branches.push tempDocsDir 173 | ) 174 | 175 | Target "Release" (fun _ -> 176 | StageAll "" 177 | Commit "" (sprintf "Bump version to %s" release.NugetVersion) 178 | Branches.push "" 179 | 180 | Branches.tag "" release.NugetVersion 181 | Branches.pushTag "" "origin" release.NugetVersion 182 | ) 183 | 184 | Target "BuildPackage" DoNothing 185 | 186 | // -------------------------------------------------------------------------------------- 187 | // Run all targets by default. Invoke 'build ' to override 188 | 189 | Target "All" DoNothing 190 | 191 | "Clean" 192 | ==> "RestorePackages" 193 | ==> "AssemblyInfo" 194 | ==> "Build" 195 | ==> "RunTests" 196 | =?> ("GenerateReferenceDocs",isLocalBuild && not isMono) 197 | =?> ("GenerateDocs",isLocalBuild && not isMono) 198 | ==> "All" 199 | =?> ("ReleaseDocs",isLocalBuild && not isMono) 200 | 201 | "All" 202 | #if MONO 203 | #else 204 | =?> ("SourceLink", Pdbstr.tryFind().IsSome ) 205 | #endif 206 | ==> "NuGet" 207 | ==> "BuildPackage" 208 | 209 | "CleanDocs" 210 | ==> "GenerateHelp" 211 | ==> "GenerateReferenceDocs" 212 | ==> "GenerateDocs" 213 | 214 | "ReleaseDocs" 215 | ==> "Release" 216 | 217 | "BuildPackage" 218 | ==> "Release" 219 | 220 | RunTargetOrDefault "All" 221 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if test "$OS" = "Windows_NT" 3 | then 4 | # use .Net 5 | .nuget/NuGet.exe install FAKE -OutputDirectory packages -ExcludeVersion 6 | .nuget/NuGet.exe install SourceLink.Fake -OutputDirectory packages -ExcludeVersion 7 | [ ! -e build.fsx ] && packages/FAKE/tools/FAKE.exe init.fsx 8 | packages/FAKE/tools/FAKE.exe build.fsx $@ 9 | else 10 | # use mono 11 | mono .nuget/NuGet.exe install FAKE -OutputDirectory packages -ExcludeVersion 12 | mono .nuget/NuGet.exe install SourceLink.Fake -OutputDirectory packages -ExcludeVersion 13 | [ ! -e build.fsx ] && mono packages/FAKE/tools/FAKE.exe init.fsx 14 | mono packages/FAKE/tools/FAKE.exe $@ --fsiargs -d:MONO build.fsx 15 | fi 16 | -------------------------------------------------------------------------------- /docs/content/agent-paradigms.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | Agent Paradigms 3 | =============== 4 | This article briefly explains the two distributed agent models supported by CloudAgent. 5 | 6 | Agent types 7 | ----------- 8 | Different types of agents control the manner in which messages are passed through your 9 | application as well as the sizing behaviour of the pools themselves. 10 | ### Worker Pools 11 | Worker Pools represent agents which are entirely stateless. They can handle messages 12 | in any order and at any time. Worker Pools in CloudAgent are fixed at a size of 512 workers 13 | per node; messages will be randomly distributed through the worker pool. You will probably 14 | use Workers for the majority of your distributed workloads. 15 | ### Actor Pools 16 | Actor Pools represent agents which activate and deactive as work appears for them to 17 | process. Messages are "routed" to a specific agent by means of an ActorKey - a simple 18 | string which is used to determine which agent will receive the message. Each CloudAgent 19 | node maintains a set of agents which will be created and destroyed as required to fulfil 20 | requests for a specific actor. Thus, whilst an actor pool is theoretically unlimited in size, 21 | the actual number of Actors running at any given time is equal to the number of messages 22 | being processed, grouped by ActorKey. 23 | As both Service Bus sessions and F# Agents naturally guarantee sequential flow of messages, 24 | Actor Pools can be used in situations where you do not want to worry about concurrent processing 25 | of messages. This can be used to dramatically simplify your software at the cost of removing 26 | parallelisation of processing for messages keyed to the same actor. 27 | ### Hybrid Models 28 | There is nothing to stop you sharing agent code across both types of agent pools. You can send 29 | some messages over a standard Service Bus Queue for worker-style processing, and send 30 | other messages that you need to run in isolation over a sessionised queue for Actor processing. 31 | 32 | Agent Resiliency 33 | ---------------- 34 | Agents can handle messages in different ways to cope with errors. CloudAgent offers two different 35 | models. 36 | ### Basic Agents 37 | Basic agents are the most simple to create - you simply bind your existing F# Agent code 38 | to a service bus queue. When messages arrive from the service bus, once they have been 39 | successfully deserialised, CloudAgent will dispatch the to an agent for processing and 40 | immediately mark them as complete on the service bus. 41 | This is the simplest model for processing and means that your agents will never receive 42 | the same message more than once. However, it is entirely your responsibility to handle 43 | retry logic of messages which fail, or to log messages which cannot be processed. If 44 | your agent crashes, or Azure decides to restart the machine on which your agent pool is 45 | running on whilst processing a message, that message will be lost. 46 | You might use Basic Agents when creating pools where failing to completely process a single 47 | message is not critical to the overall operation of your application. 48 | ### Resilient Agents 49 | Resilient Agents use the built-in capabilities of Azure Service Bus to guarantee message 50 | processing, with built-in retry logic and dead lettering. Rather than simply receiving a 51 | message and processing them, your F# Agents receive both a message and a reply channel. 52 | Once your agent finishes processing a message, it should reply back with a status 53 | (Completed, Failed or Abandon), which in turn is converted into a Service Bus message 54 | acknowledgement. Only when Service Bus receives this acknowledge will it remove the message 55 | from the queue. Otherwise, if your Agent crashes or fails to reply within the message timeout 56 | period, Service Bus will once again make the message available for other nodes to consume. 57 | Messages that repeatedly fail will automatically be pushed to a dead letter queue on the 58 | service bus where they can be analysed in isolation. 59 | In this model, messages are guaranteed to be delivered at least once to an agent for 60 | processing. Use this model where you need to guarantee delivery of every message in the 61 | system to your agents and need resliency of message processing. 62 | *) -------------------------------------------------------------------------------- /docs/content/azure-service-bus.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | About Azure Service Bus 3 | ======================= 4 | This article is not meant to replace the reams of documentation available on [Azure] [azure-service-bus] 5 | or [MSDN] [msdn-service-bus], but give a quick guide on how to get up an running with 6 | service bus for the purposes of FSharp.CloudAgent, and how to map Azure Service Bus 7 | concepts to F# Agents. 8 | 9 | Note: if you do not want to use Azure, you can self-host using the [Windows Service Bus](windows-service-bus.html). Read through 10 | this article first as the principles are the same, and then read that one for installation guidance. 11 | 12 | Azure Service Bus Overview 13 | -------------------------- 14 | Azure Service Bus provides a simple and cheap distributed messaging framework that runs 15 | in the Azure cloud. It comes in several flavours - the part that CloudAgent harnesses is 16 | that of [Service Bus Queues] [service-bus-queues], which provides reliable messaging 17 | through storage-backed queues in Azure that can have multiple consumers. In the context 18 | of CloudAgent, each consumer is a worker node i.e. .NET process that runs a number of 19 | F# agents to process messages. CloudAgent manages the lifetime of agents as needed, 20 | handing out messages as they arrive to the agent pool for processing, as well as relaying 21 | completion messages back to the Service Bus from agents. 22 | 23 | Service Bus Queues key concepts 24 | ------------------------------- 25 | Within the context of CloudAgent, Service Bus Queues provide the following key features: - 26 | 27 | * **Reliable**. Service Bus Queues guarantee message *delivery to* and *processing by* consumers. 28 | If a message is not completed within a specified timeout, it is made available for processing 29 | again by another consumer. Thus, distributed message processing in CloudAgent takes the form 30 | of *at-least-once* processing; a message will be processed *at least once*, but depending on 31 | whether the consumer successfully "completes" the message, it may be re-processed by other 32 | consumers. Your agents should be written to handle messages in an idempotent manner, or at 33 | least detect repeat messages. 34 | * **High throughput**. Supports up to 2k messages / sec per queue, with average latency of 35 | 20-25ms. 36 | * **Cheap**. $0.01 per 10k messages. 37 | * **Sessionised**. Service Bus Queues provide the ability to ensure sequential delivery of 38 | messages that are partitioned based on an arbitrary "SessionId". This forms the basis of 39 | Actor support within CloudAgent; an ActorKey is mapped to the SessionId of individual 40 | messages, thus ensuring that messages for the same Actor are delivered to the same consumer 41 | in sequence. 42 | 43 | How to create a Service Bus Queue 44 | --------------------------------- 45 | Follow the guidance [here] [service-bus-queues] to create a new service bus namespace. 46 | Once the namespace is created, you can generate a Queue through Visual Studio or in code. 47 | 48 | 49 | 50 | 51 | It is important to understand that for Actor pools, you *must* turn on Session support 52 | when creating the queue; likewise for Worker pools, Session support must be turned *off*. 53 | 54 | [azure-service-bus]: http://azure.microsoft.com/en-us/services/service-bus/ 55 | [msdn-service-bus]: http://msdn.microsoft.com/en-us/library/azure/ee732537.aspx 56 | [service-bus-queues]: http://azure.microsoft.com/en-us/documentation/articles/service-bus-dotnet-how-to-use-queues/ 57 | *) -------------------------------------------------------------------------------- /docs/content/handling-errors.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | Handling Errors 3 | =============== 4 | Your approach towards error handling will most likely differ depending on the type of agent 5 | pool (irrespective of whether worker or actor) being used. 6 | Basic Agents 7 | ------------ 8 | Basic agents work on an "at-most-once" delivery mechanism to F# agents. This means that it is 9 | entirely your responsibility to be able to guarantee processing of these messages. You should 10 | cater for logging out failures as well as persisting failed messages to some persistance store 11 | for later inspection. If an agent crashes due to an unhandled exception, your message will be 12 | lost permanently. 13 | Resilient Agents 14 | ---------------- 15 | Reslient agents work on an "at-least-once" delivery mechanism. Failed messages will automatically 16 | be redelivered until the queue marks them as dead lettered. If an agent crashes due to an unhandled 17 | exception, your message will be automatically re-delivered once it expires. If you set this expiry 18 | too long, you may wish to consider wrapping your agent processing code in a try catch or other 19 | exception handling pattern and immediately returning a Failed to the CloudAgent framework to short- 20 | circuit this wait. 21 | *) -------------------------------------------------------------------------------- /docs/content/index.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | About FSharp.CloudAgent 3 | ======================= 4 | 5 | FSharp.CloudAgent provides a simple framework to easily distribute workloads over the cloud 6 | whilst using standard F# Agents as the processing mechanism. Support exists for both simple 7 | and reliable messaging via Azure Service Bus, and for both workers and actors. 8 | 9 | Samples & documentation 10 | ----------------------- 11 | 12 | The library comes with comprehensible documentation. The API reference is automatically 13 | generated from Markdown comments in the library implementation. 14 | 15 | * [Tutorial](tutorial.html) contains same basic examples of how to use FSharp.CloudAgent. 16 | * [Agent Paradigms](agent-paradigms.html) explains the core differences between the types of supported Cloud Agents. 17 | * [Azure Service Bus](azure-service-bus.html) provides an overview of Azure Service Bus and how it interoperates with F# Agents. 18 | * [Windows Service Bus](windows-service-bus.html) provides an overview of Windows Service Bus and how it can be used in place of Azure Service Bus. 19 | * [Error Handling](handling-errors.html) suggests some high-level principles of how to cater for errors within Cloud Agent. 20 | * [API Reference](reference/index.html) contains automatically generated documentation for all types, modules 21 | and functions in the library. This includes additional brief samples on using most of the 22 | functions. 23 | 24 | Contributing and copyright 25 | -------------------------- 26 | 27 | The project is hosted on [GitHub][gh] where you can [report issues][issues], fork 28 | the project and submit pull requests. If you're adding a new public API, please also 29 | consider adding [samples][content] that can be turned into a documentation. You might 30 | also want to read the [library design notes][readme] to understand how it works. 31 | 32 | The library is available under Public Domain license, which allows modification and 33 | redistribution for both commercial and non-commercial purposes. For more information see the 34 | [License file][license] in the GitHub repository. 35 | 36 | [content]: https://github.com/isaacabraham/FSharp.CloudAgent/tree/master/docs/content 37 | [gh]: https://github.com/isaacabraham/FSharp.CloudAgent 38 | [issues]: https://github.com/isaacabraham/FSharp.CloudAgent/issues 39 | [readme]: https://github.com/isaacabraham/FSharp.CloudAgent/blob/master/README.md 40 | [license]: https://github.com/isaacabraham/FSharp.CloudAgent/blob/master/LICENSE.txt 41 | *) 42 | -------------------------------------------------------------------------------- /docs/content/tutorial.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | #r @"..\..\bin\FSharp.CloudAgent.dll" 3 | 4 | (** 5 | Tutorial 6 | ============ 7 | Getting up and running 8 | ---------------------- 9 | Creating a simple distributable Agent is as simple as creating a regular agent, wrapping it 10 | in a generator function and then binding that function to a service bus queue. 11 | 12 | *) 13 | 14 | open FSharp.CloudAgent 15 | open FSharp.CloudAgent.Messaging 16 | open FSharp.CloudAgent.Connections 17 | 18 | // Standard Azure Service Bus connection string 19 | let serviceBusConnection = ServiceBusConnection "servicebusconnectionstringgoeshere" 20 | 21 | // A DTO 22 | type Person = { Name : string; Age : int } 23 | 24 | // A function which creates an Agent on demand. 25 | let createASimpleAgent agentId = 26 | MailboxProcessor.Start(fun inbox -> 27 | async { 28 | while true do 29 | let! message = inbox.Receive() 30 | printfn "%s is %d years old." message.Name message.Age 31 | }) 32 | 33 | // Create a worker cloud connection to the Service Bus Queue "myMessageQueue" 34 | let cloudConnection = WorkerCloudConnection(serviceBusConnection, Queue "myMessageQueue") 35 | 36 | // Start listening! A local pool of agents will be created that will receive messages. 37 | // Service bus messages will be automatically deserialised into the required message type. 38 | ConnectionFactory.StartListening(cloudConnection, createASimpleAgent >> BasicCloudAgent) 39 | 40 | 41 | (** 42 | 43 | Posting messages to agents 44 | -------------------------- 45 | You can elect to natively send messages to the service bus (as long as the serialisation 46 | format matches i.e. JSON .NET). However, FSharp.CloudAgent includes some handy helper functions 47 | to do this for you. 48 | 49 | *) 50 | 51 | let sendToMyMessageQueue = ConnectionFactory.SendToWorkerPool cloudConnection 52 | 53 | // These messages will be processed in parallel across the worker pool. 54 | sendToMyMessageQueue { Name = "Isaac"; Age = 34 } 55 | sendToMyMessageQueue { Name = "Michael"; Age = 32 } 56 | sendToMyMessageQueue { Name = "Sam"; Age = 27 } 57 | 58 | (** 59 | 60 | Creating Resilient Agents 61 | ------------------------- 62 | We can also create "Resilient" agents. These are standard F# Agents that take advantage of the 63 | Reply Channel functionality inherent in F# agents to allow us to signal back to Azure whether 64 | or not a message was processed correctly or not, which in turn gives us automatic retry and dead 65 | letter functionality that is included with Azure Service Bus. 66 | 67 | *) 68 | 69 | // A function which creates a Resilient Agent on demand. 70 | let createAResilientAgent agentId = 71 | MailboxProcessor.Start(fun inbox -> 72 | async { 73 | while true do 74 | let! message, replyChannel = inbox.Receive() 75 | printfn "%s is %d years old." message.Name message.Age 76 | 77 | match message with 78 | | { Name = "Isaac" } -> replyChannel Completed // all good, message was processed 79 | | { Name = "Richard" } -> replyChannel Failed // error occurred, try again 80 | | _ -> replyChannel Abandoned // give up with this message. 81 | }) 82 | 83 | // Start listening! A local pool of agents will be created that will receive messages. 84 | ConnectionFactory.StartListening(cloudConnection, createAResilientAgent >> ResilientCloudAgent) 85 | 86 | (** 87 | 88 | Creating distributed Actors 89 | --------------------------- 90 | In addition to the massively distributable messages seen above, we can also create agents which are 91 | threadsafe not only within the context of a process but also in the context of multiple processes on 92 | different machines. This is made possible by Azure Service Bus session support. To turn an agent into 93 | an actor, we simply change the connection and the way in which we post messages. 94 | 95 | *) 96 | 97 | // Create an actor cloud connection to the Service Bus Queue "myActorQueue". This queue must have 98 | // Session support turned on. 99 | let actorConnection = ActorCloudConnection(serviceBusConnection, Queue "myActorQueue") 100 | 101 | // Start listening! Agents will be created as required for messages sent to new actors. 102 | ConnectionFactory.StartListening(actorConnection, createASimpleAgent >> BasicCloudAgent) 103 | 104 | // Send some messages to the actor pool 105 | let sendToMyActorQueue = ConnectionFactory.SendToActorPool actorConnection 106 | 107 | let sendToFred = sendToMyActorQueue (ActorKey "Fred") 108 | let sendToMax = sendToMyActorQueue (ActorKey "Max") 109 | 110 | // The first two messages will be sent in sequence to the same agent in the same process. 111 | // The last message will be sent to a different agent, potentially in another process. 112 | sendToFred { Name = "Isaac"; Age = 34 } 113 | sendToFred { Name = "Michael"; Age = 32 } 114 | sendToMax { Name = "Sam"; Age = 27 } -------------------------------------------------------------------------------- /docs/content/windows-service-bus.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | About Windows Service Bus 3 | ======================= 4 | Windows Service Bus (WSB) can be thought of as an on-premises version of Azure Service 5 | Bus. As such, everything mentioned in the [Azure Service Bus](azure-service-bus.html) section still holds true 6 | in terms of operations and application within Cloud Agent. The difference with WSB is 7 | that you manage the service bus yourself on your network, and can host all entire 8 | internally on your network. This article will provide some simple instructions on 9 | installation of Windows Service Bus that will enable you to use Cloud Agent without 10 | using Azure. 11 | 12 | Windows Service Bus versions 13 | ---------------------------- 14 | WSB currently comes in two versions - 1.0 and 1.1. Whilst 1.0 at the current time of 15 | writing is supported natively in Visual Studio 2013, 1.1 is not. However, WSB 1.1 16 | brings a raft of [features] [wsb-11] that bring it up to parity with the 2.x SDK of Azure Service 17 | Bus. As such, WSB 1.1 is recommended for use with Cloud Agent. 18 | 19 | Windows Service Bus Installation 20 | -------------------------------- 21 | Full details can be found [here] [install-wsb]. Essentially, you install the WSB service, which stores data 22 | in a SQL database. This can be full SQL Server or SQL Server Express (indeed, you can even 23 | install it on LocalDB). 24 | 25 | Managing WSB 26 | ------------ 27 | Unlike Azure Service Bus, you cannot use the Azure Portal to manage a locally hosted 28 | Windows Service Bus. Instead, there are several ways to interact with WSB: - 29 | 30 | * You can use the existing Service Bus SDK to programmatically create queues etc. having 31 | connected to the local service bus. 32 | * You can use the [Service Bus Explorer] [sb-exp] tool. Note that only version 2.1 is 33 | compatible with WSB 1.1. The latest version (2.5.3) does NOT support WSB 1.1. 34 | * You can use the [Service Bus Powershell cmdlets] [sb-psh] to perform basic interrogation 35 | of your service bus farm. 36 | 37 | WSB Connection Strings 38 | ---------------------- 39 | Connection strings on WSB are similar to those on Azure Service Bus. 40 | 41 | 1. You can identify the default namespace using the ``get-sbNamespace`` cmdlet. 42 | 2. You can generate a connection string using the ``get-sbClientConfiguration`` cmdlet. 43 | 44 | This connection string can be used directly in place of the standard Azure Service Bus 45 | connection string in CloudAgent. Your application code remains unchanged. 46 | 47 | [wsb-11]: http://msdn.microsoft.com/en-us/library/dn282143.aspx 48 | [install-wsb]: http://msdn.microsoft.com/en-us/library/dn282152.aspx 49 | [sb-exp]: https://code.msdn.microsoft.com/windowsapps/Service-Bus-Explorer-f2abca5a 50 | [sb-psh]: http://msdn.microsoft.com/en-us/library/jj200653%28v=azure.10%29.aspx 51 | *) -------------------------------------------------------------------------------- /docs/files/img/create-a-queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FSharp.CloudAgent/4510c97154528efe072bbcd2329301249f66ae1c/docs/files/img/create-a-queue.png -------------------------------------------------------------------------------- /docs/files/img/logo-template.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FSharp.CloudAgent/4510c97154528efe072bbcd2329301249f66ae1c/docs/files/img/logo-template.pdn -------------------------------------------------------------------------------- /docs/files/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FSharp.CloudAgent/4510c97154528efe072bbcd2329301249f66ae1c/docs/files/img/logo.png -------------------------------------------------------------------------------- /docs/files/img/queues-in-vs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FSharp.CloudAgent/4510c97154528efe072bbcd2329301249f66ae1c/docs/files/img/queues-in-vs.png -------------------------------------------------------------------------------- /docs/tools/generate.fsx: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------- 2 | // Builds the documentation from `.fsx` and `.md` files in the 'docs/content' directory 3 | // (the generated documentation is stored in the 'docs/output' directory) 4 | // -------------------------------------------------------------------------------------- 5 | 6 | // Binaries that have XML documentation (in a corresponding generated XML file) 7 | let referenceBinaries = [ "FSharp.CloudAgent.dll" ] 8 | // Web site location for the generated documentation 9 | let website = "/FSharp.CloudAgent" 10 | 11 | let githubLink = "http://github.com/isaacabraham/FSharp.CloudAgent" 12 | 13 | // Specify more information about your project 14 | let info = 15 | [ "project-name", "FSharp.CloudAgent" 16 | "project-author", "Isaac Abraham" 17 | "project-summary", "Allows the use of F# Agents in Azure." 18 | "project-github", githubLink 19 | "project-nuget", "http://nuget.org/packages/FSharp.CloudAgent" ] 20 | 21 | // -------------------------------------------------------------------------------------- 22 | // For typical project, no changes are needed below 23 | // -------------------------------------------------------------------------------------- 24 | 25 | #I "../../packages/FSharp.Formatting/lib/net40" 26 | #I "../../packages/RazorEngine/lib/net40" 27 | #I "../../packages/FSharp.Compiler.Service/lib/net40" 28 | #r "../../packages/Microsoft.AspNet.Razor/lib/net40/System.Web.Razor.dll" 29 | #r "../../packages/FAKE/tools/NuGet.Core.dll" 30 | #r "../../packages/FAKE/tools/FakeLib.dll" 31 | #r "RazorEngine.dll" 32 | #r "FSharp.Literate.dll" 33 | #r "FSharp.CodeFormat.dll" 34 | #r "FSharp.MetadataFormat.dll" 35 | open Fake 36 | open System.IO 37 | open Fake.FileHelper 38 | open FSharp.Literate 39 | open FSharp.MetadataFormat 40 | 41 | // When called from 'build.fsx', use the public project URL as 42 | // otherwise, use the current 'output' directory. 43 | #if RELEASE 44 | let root = website 45 | #else 46 | let root = "file://" + (__SOURCE_DIRECTORY__ @@ "../output") 47 | #endif 48 | 49 | // Paths with template/source/output locations 50 | let bin = __SOURCE_DIRECTORY__ @@ "../../bin" 51 | let content = __SOURCE_DIRECTORY__ @@ "../content" 52 | let output = __SOURCE_DIRECTORY__ @@ "../output" 53 | let files = __SOURCE_DIRECTORY__ @@ "../files" 54 | let templates = __SOURCE_DIRECTORY__ @@ "templates" 55 | let formatting = __SOURCE_DIRECTORY__ @@ "../../packages/FSharp.Formatting/" 56 | let docTemplate = formatting @@ "templates/docpage.cshtml" 57 | 58 | // Where to look for *.csproj templates (in this order) 59 | let layoutRoots = 60 | [ templates; formatting @@ "templates" 61 | formatting @@ "templates/reference" ] 62 | 63 | // Copy static files and CSS + JS from F# Formatting 64 | let copyFiles () = 65 | CopyRecursive files output true |> Log "Copying file: " 66 | ensureDirectory (output @@ "content") 67 | CopyRecursive (formatting @@ "styles") (output @@ "content") true 68 | |> Log "Copying styles and scripts: " 69 | 70 | // Build API reference from XML comments 71 | let buildReference () = 72 | CleanDir (output @@ "reference") 73 | let binaries = 74 | referenceBinaries 75 | |> List.map (fun lib-> bin @@ lib) 76 | MetadataFormat.Generate 77 | ( binaries, output @@ "reference", layoutRoots, 78 | parameters = ("root", root)::info, 79 | sourceRepo = githubLink @@ "tree/master", 80 | sourceFolder = __SOURCE_DIRECTORY__ @@ ".." @@ "..", 81 | publicOnly = true ) 82 | 83 | // Build documentation from `fsx` and `md` files in `docs/content` 84 | let buildDocumentation () = 85 | let subdirs = Directory.EnumerateDirectories(content, "*", SearchOption.AllDirectories) 86 | for dir in Seq.append [content] subdirs do 87 | let sub = if dir.Length > content.Length then dir.Substring(content.Length + 1) else "." 88 | Literate.ProcessDirectory 89 | ( dir, docTemplate, output @@ sub, replacements = ("root", root)::info, 90 | layoutRoots = layoutRoots ) 91 | 92 | // Generate 93 | copyFiles() 94 | #if HELP 95 | buildDocumentation() 96 | #endif 97 | #if REFERENCE 98 | buildReference() 99 | #endif 100 | -------------------------------------------------------------------------------- /docs/tools/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Formatting 2 | FSharp.Compiler.Service 3 | Microsoft.AspNet.Razor 4 | RazorEngine -------------------------------------------------------------------------------- /docs/tools/templates/template.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @Title 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 |
24 |
25 | 29 |

@Properties["project-name"]

30 |
31 |
32 |
33 |
34 | @RenderBody() 35 |
36 |
37 | F# Project 38 | 57 |
58 |
59 |
60 | Fork me on GitHub 61 | 62 | 63 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | This file is in the `lib` directory. 2 | 3 | Any **libraries** on which your project depends and which are **NOT managed via NuGet** should be kept **in this directory**. 4 | This typically includes custom builds of third-party software, private (i.e. to a company) codebases, and native libraries. 5 | 6 | --- 7 | NOTE: 8 | 9 | This file is a placeholder, used to preserve directory structure in Git. 10 | 11 | This file does not need to be edited. 12 | -------------------------------------------------------------------------------- /nuget/FSharp.CloudAgent.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @project@ 5 | @build.number@ 6 | @authors@ 7 | @authors@ 8 | http://github.com/isaacabraham/FSharp.CloudAgent/blob/master/LICENSE.txt 9 | http://isaacabraham.github.io/FSharp.CloudAgent 10 | http://isaacabraham.github.io/FSharp.CloudAgent/img/logo.png 11 | false 12 | @summary@ 13 | @description@ 14 | @releaseNotes@ 15 | Copyright 2014 16 | @tags@ 17 | @dependencies@ 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /nuget/README.md: -------------------------------------------------------------------------------- 1 | This file is in the `nuget` directory. 2 | 3 | You should use this directory to store any artifacts required to produce a NuGet package for your project. 4 | This typically includes a `.nuspec` file and some `.ps1` scripts, for instance. 5 | Additionally, this example project includes a `.cmd` file suitable for manual deployment of packages to http://nuget.org. 6 | 7 | --- 8 | NOTE: 9 | 10 | This file is a placeholder, used to preserve directory structure in Git. 11 | 12 | This file does not need to be edited. 13 | -------------------------------------------------------------------------------- /nuget/publish.cmd: -------------------------------------------------------------------------------- 1 | @for %%f in (..\bin\*.nupkg) do @..\.nuget\NuGet.exe push %%f -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source http://nuget.org/api/v2 2 | framework: net451 3 | 4 | nuget FSharp.Core redirects: force 5 | nuget FAKE 6 | nuget SourceLink.Fake 7 | nuget FSharp.Formatting 8 | nuget NUnit 9 | nuget NUnit.Console 10 | nuget Microsoft.WindowsAzure.ConfigurationManager 2.0.3 11 | nuget WindowsAzure.ServiceBus 2.5.2 12 | nuget Newtonsoft.Json 13 | nuget Unquote 2.2.2 -------------------------------------------------------------------------------- /paket.lock: -------------------------------------------------------------------------------- 1 | RESTRICTION: == net451 2 | NUGET 3 | remote: http://www.nuget.org/api/v2 4 | FAKE (4.64.4) 5 | FSharp.Compiler.Service (2.0.0.6) 6 | FSharp.Core (4.3.4) - redirects: force 7 | FSharp.Formatting (2.14.4) 8 | FSharp.Compiler.Service (2.0.0.6) 9 | FSharpVSPowerTools.Core (>= 2.3 < 2.4) 10 | FSharpVSPowerTools.Core (2.3) 11 | FSharp.Compiler.Service (>= 2.0.0.3) 12 | Microsoft.WindowsAzure.ConfigurationManager (2.0.3) 13 | Newtonsoft.Json (10.0.3) 14 | NUnit (3.9) 15 | NUnit.Console (3.8) 16 | NUnit.ConsoleRunner (>= 3.8) 17 | NUnit.Extension.NUnitProjectLoader (>= 3.5) 18 | NUnit.Extension.NUnitV2Driver (>= 3.6) 19 | NUnit.Extension.NUnitV2ResultWriter (>= 3.5) 20 | NUnit.Extension.TeamCityEventListener (>= 1.0.3) 21 | NUnit.Extension.VSProjectLoader (>= 3.7) 22 | NUnit.ConsoleRunner (3.8) 23 | NUnit.Extension.NUnitProjectLoader (3.6) 24 | NUnit.Extension.NUnitV2Driver (3.7) 25 | NUnit.Extension.NUnitV2ResultWriter (3.6) 26 | NUnit.Extension.TeamCityEventListener (1.0.3) 27 | NUnit.Extension.VSProjectLoader (3.7) 28 | SourceLink.Fake (1.1) 29 | Unquote (2.2.2) 30 | WindowsAzure.ServiceBus (2.5.2) 31 | Microsoft.WindowsAzure.ConfigurationManager 32 | -------------------------------------------------------------------------------- /src/FSharp.CloudAgent/Actors.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.CloudAgent.Actors 2 | 3 | open FSharp.CloudAgent 4 | open FSharp.CloudAgent.Messaging 5 | open System 6 | open System.Collections.Generic 7 | 8 | /// Manages lifetime of actors. 9 | type internal IActorStore<'a> = 10 | 11 | /// Requests an actor for a particular key. 12 | abstract GetActor : ActorKey -> CloudAgentKind<'a> 13 | 14 | /// Tells the store that an actor is no longer required and can be safely removed. 15 | abstract RemoveActor : ActorKey -> unit 16 | 17 | // Manages per-session actors. 18 | type private ActorStoreRequest = 19 | | Get of ActorKey 20 | | Remove of ActorKey 21 | 22 | type private ActorStoreAgent<'a> = MailboxProcessor> option> 23 | 24 | module internal Factory = 25 | let private createActorStore<'a> createAgent = 26 | let actorStore = 27 | new ActorStoreAgent<'a>(fun inbox -> 28 | let actors = Dictionary() 29 | async { 30 | while true do 31 | let! message, replyChannel = inbox.Receive() 32 | match message with 33 | | Get(ActorKey agentId) -> 34 | let actor = 35 | if not (actors.ContainsKey agentId) then 36 | let actor = createAgent (ActorKey agentId) 37 | actors.Add(agentId, actor) 38 | match actor with 39 | | ResilientCloudAgent actor -> 40 | try actor.Start() 41 | with _ -> () 42 | | BasicCloudAgent actor -> 43 | try actor.Start() 44 | with _ -> () 45 | actors.[agentId] 46 | replyChannel |> Option.iter (fun replyChannel -> replyChannel.Reply actor) 47 | | Remove(ActorKey actorKey) -> 48 | if actors.ContainsKey(actorKey) then 49 | let actorToRemove = 50 | match actors.[actorKey] with 51 | | ResilientCloudAgent actor -> actor :> IDisposable 52 | | BasicCloudAgent actor -> actor :> IDisposable 53 | actors.Remove actorKey |> ignore 54 | actorToRemove.Dispose() 55 | }) 56 | actorStore.Start() 57 | actorStore 58 | 59 | /// Creates an IActorStore that can add / retrieve / remove agents in a threadsafe manner, using the supplied function to create new agents on demand. 60 | let CreateActorStore<'a> createActor = 61 | let actorStore = createActorStore<'a> createActor 62 | { new IActorStore<'a> with 63 | member __.GetActor(sessionId) = actorStore.PostAndReply(fun ch -> (Get sessionId), Some ch) 64 | member __.RemoveActor(sessionId) = actorStore.Post(Remove sessionId, None) } 65 | 66 | /// Selects an agent to consume a message. 67 | type AgentSelectorFunc<'a> = unit -> Async> 68 | 69 | /// Generates a pool of CloudAgents of a specific size that can be used to select an agent when required. 70 | let CreateAgentSelector<'a>(size, createAgent : ActorKey -> CloudAgentKind<'a>) : AgentSelectorFunc<'a> = 71 | let agents = 72 | [ for i in 1 .. size -> 73 | let agent = createAgent (ActorKey <| i.ToString()) 74 | match agent with 75 | | BasicCloudAgent agent -> 76 | try agent.Start() 77 | with _ -> () 78 | | ResilientCloudAgent agent -> 79 | try agent.Start() 80 | with _ -> () 81 | agent ] 82 | 83 | let r = Random() 84 | fun () -> async { return agents.[r.Next(0, agents.Length)] } 85 | -------------------------------------------------------------------------------- /src/FSharp.CloudAgent/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | namespace System 2 | open System.Reflection 3 | 4 | [] 5 | [] 6 | [] 7 | [] 8 | [] 9 | do () 10 | 11 | module internal AssemblyVersionInformation = 12 | let [] Version = "0.3" 13 | -------------------------------------------------------------------------------- /src/FSharp.CloudAgent/ConnectionFactory.fs: -------------------------------------------------------------------------------- 1 | /// Contains connectors for F# Agents to Azure Service Bus Queues as either Workers or Actors. 2 | module FSharp.CloudAgent.ConnectionFactory 3 | 4 | open FSharp.CloudAgent 5 | open FSharp.CloudAgent.Actors 6 | open FSharp.CloudAgent.Messaging 7 | open System 8 | open System.Runtime.Serialization 9 | 10 | [] 11 | module internal Helpers = 12 | open System.Threading 13 | let createDisposable() = 14 | let source = new CancellationTokenSource() 15 | let disposable = 16 | { new IDisposable with 17 | member __.Dispose() = source.Cancel() } 18 | disposable, source.Token 19 | 20 | module internal Actors = 21 | open System.Threading 22 | 23 | /// Contains configuration details for connecting to an Actor Cloud. 24 | type ActorCloudOptions<'a> = 25 | { PollTimeout : TimeSpan 26 | Serializer : ISerializer<'a> 27 | ActorStore : IActorStore<'a> } 28 | 29 | /// Binds a Cloud Agent to a session-enabled Azure Service Bus Queue with affinity between 30 | /// sessions and agents, using an IActorStore to manage actor lifetime. 31 | let BindToCloud<'a> (getNextSession, options:ActorCloudOptions<'a>) = 32 | let processBrokeredMessage = ProcessBrokeredMessage options.Serializer 33 | 34 | /// Locks into a loop, processing messages from a specific session with a specific agent until the session is empty. 35 | let processSession (agent : CloudAgentKind<'a>, session : IActorMessageStream, cancellationToken:CancellationToken) = 36 | let processBrokeredMessage = processBrokeredMessage agent 37 | let rec continueProcessingStream() = 38 | async { 39 | let! message = session.GetNextMessage(options.PollTimeout) |> Async.Catch 40 | match cancellationToken.IsCancellationRequested, message with 41 | | true, _ 42 | | _, Error _ 43 | | _, Result None -> 44 | options.ActorStore.RemoveActor(session.SessionId) 45 | return! session.AbandonSession() 46 | | _, Result(Some message) -> 47 | let! result = processBrokeredMessage message 48 | do! match result with 49 | | Completed -> session.CompleteMessage(message.LockToken) 50 | | Failed -> session.AbandonMessage(message.LockToken) 51 | | Abandoned -> session.DeadLetterMessage(message.LockToken) 52 | let! renewal = session.RenewSessionLock() |> Async.Catch 53 | match renewal with 54 | | Result() -> return! continueProcessingStream() 55 | | Error _ -> options.ActorStore.RemoveActor(session.SessionId) 56 | } 57 | continueProcessingStream() 58 | 59 | let disposable, cancellationToken = createDisposable() 60 | async { 61 | let getNextSession = getNextSession |> withAutomaticRetry 62 | while not cancellationToken.IsCancellationRequested do 63 | let! (session : IActorMessageStream) = getNextSession (options.PollTimeout.TotalMilliseconds |> int) 64 | match cancellationToken.IsCancellationRequested with 65 | | true -> () 66 | | false -> 67 | let agent = options.ActorStore.GetActor(session.SessionId) 68 | processSession (agent, session, cancellationToken) |> Async.Start 69 | } 70 | |> Async.Start 71 | disposable 72 | 73 | module internal Workers = 74 | open System 75 | open FSharp.CloudAgent.Messaging 76 | open FSharp.CloudAgent.Actors.Factory 77 | 78 | type WorkerCloudOptions<'a> = 79 | { PollTime : TimeSpan 80 | Serializer : ISerializer<'a> 81 | GetNextAgent : AgentSelectorFunc<'a> } 82 | 83 | /// Binds CloudAgents to an Azure service bus queue using custom configuration options. 84 | let BindToCloud<'a>(messageStream:ICloudMessageStream, options : WorkerCloudOptions<'a>) = 85 | let getNextMessage = (fun() -> messageStream.GetNextMessage (options.PollTime)) |> withAutomaticRetry 86 | let processBrokeredMessage = ProcessBrokeredMessage options.Serializer 87 | let disposable, token = createDisposable() 88 | async { 89 | while not token.IsCancellationRequested do 90 | // Only try to get a message once we have an available agent. 91 | let! agent = options.GetNextAgent() 92 | let! message = getNextMessage (options.PollTime.TotalMilliseconds |> int) 93 | match token.IsCancellationRequested with 94 | | true -> 95 | // If a cancellation request has occurred, don't process the last received message 96 | // just throw it back to the message provider. 97 | messageStream.AbandonMessage message.LockToken |> ignore 98 | | false -> 99 | // Place the following code in its own async block so it works in the background and we can 100 | // get another message for another agent in parallel. 101 | async { 102 | let! processingResult = processBrokeredMessage agent message 103 | match processingResult with 104 | | Completed -> do! messageStream.CompleteMessage(message.LockToken) 105 | | Failed -> do! messageStream.AbandonMessage(message.LockToken) 106 | | Abandoned -> do! messageStream.DeadLetterMessage(message.LockToken) 107 | } 108 | |> Async.Start 109 | } 110 | |> Async.Start 111 | disposable 112 | 113 | open Workers 114 | open Actors 115 | open FSharp.CloudAgent.Connections 116 | open FSharp.CloudAgent.Actors.Factory 117 | 118 | /// 119 | /// Starts listening to Azure for messages that are handled by agents. 120 | /// 121 | /// The connection to the Azure service bus that will provide messages. 122 | /// A function that can create a single F# Agent to handle messages. 123 | /// An XmlObjectSerializer used to serialize data stream coming off the service bus. 124 | let StartListeningWithWireSerializer<'a>(cloudConnection, createAgentFunc, wireSerializer) = 125 | match cloudConnection with 126 | | WorkerCloudConnection (ServiceBusConnection connection, Queue queue) -> 127 | let options = { PollTime = TimeSpan.FromSeconds 10.; Serializer = JsonSerializer<'a>(); GetNextAgent = CreateAgentSelector(512, createAgentFunc) } 128 | let messageStream = CreateQueueStream(connection, queue, wireSerializer) 129 | Workers.BindToCloud(messageStream, options) 130 | | ActorCloudConnection (ServiceBusConnection connection, Queue queue) -> 131 | let options = { PollTimeout = TimeSpan.FromSeconds 10.; Serializer = JsonSerializer<'a>(); ActorStore = CreateActorStore(createAgentFunc) } 132 | let getNextSessionStream = CreateActorMessageStream(connection, queue, options.PollTimeout, wireSerializer) 133 | Actors.BindToCloud(getNextSessionStream, options) 134 | 135 | /// 136 | /// Starts listening to Azure for messages that are handled by agents. 137 | /// 138 | /// The connection to the Azure service bus that will provide messages. 139 | /// A function that can create a single F# Agent to handle messages. 140 | let StartListening<'a>(cloudConnection, createAgentFunc) = 141 | let wireSerializer = new DataContractSerializer(typeof) 142 | StartListeningWithWireSerializer(cloudConnection, createAgentFunc, wireSerializer) 143 | 144 | let private getConnectionString = function 145 | | WorkerCloudConnection (ServiceBusConnection connection, Queue queue) -> connection, queue 146 | | ActorCloudConnection (ServiceBusConnection connection, Queue queue) -> connection, queue 147 | 148 | /// Posts a message to a pool of workers. 149 | let SendToWorkerPool<'a> connection (message:'a) = postMessage (createMessageDispatcher <| getConnectionString connection) None message 150 | /// Posts a collection of messages to a pool of workers. 151 | let SendToWorkerPoolBatch<'a> connection (messages:'a seq) = postMessages (createMessageDispatcher <| getConnectionString connection) None messages 152 | /// Posts a message to a single actor. 153 | let SendToActorPool<'a> connection (ActorKey recipient) (message:'a) = postMessage (createMessageDispatcher <| getConnectionString connection) (Some recipient) message 154 | /// Posts a collection of messages to a single actor. 155 | let SendToActorPoolBatch<'a> connection (ActorKey recipient) (messages:'a seq) = postMessages (createMessageDispatcher <| getConnectionString connection) (Some recipient) messages -------------------------------------------------------------------------------- /src/FSharp.CloudAgent/CustomAssemblyPermissions.fs: -------------------------------------------------------------------------------- 1 | namespace System 2 | open System.Runtime.CompilerServices 3 | 4 | [] 5 | do () -------------------------------------------------------------------------------- /src/FSharp.CloudAgent/FSharp.CloudAgent.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | FSharp.AzureActors 6 | FSharp.CloudAgent 7 | net451 8 | FSharp.CloudAgent 9 | pdbonly 10 | true 11 | true 12 | ..\..\bin\ 13 | FSharp.CloudAgent.XML 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/FSharp.CloudAgent/Messaging.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.CloudAgent.Messaging 2 | 3 | open FSharp.CloudAgent 4 | open System 5 | open Newtonsoft.Json 6 | open System.Runtime.Serialization 7 | 8 | [] 9 | module internal Streams = 10 | open Microsoft.ServiceBus.Messaging 11 | open FSharp.CloudAgent.Messaging 12 | 13 | /// Represents a stream of cloud messages. 14 | type ICloudMessageStream = 15 | abstract GetNextMessage : TimeSpan -> Async 16 | abstract CompleteMessage : Guid -> Async 17 | abstract AbandonMessage : Guid -> Async 18 | abstract DeadLetterMessage : Guid -> Async 19 | 20 | /// Represents a stream of messages for a specific actor. 21 | type IActorMessageStream = 22 | inherit ICloudMessageStream 23 | abstract RenewSessionLock : unit -> Async 24 | abstract AbandonSession : unit -> Async 25 | abstract SessionId : ActorKey 26 | 27 | type private QueueStream(receiver : MessageReceiver, wireSerializer : XmlObjectSerializer) = 28 | interface ICloudMessageStream with 29 | member __.DeadLetterMessage(token) = 30 | token 31 | |> receiver.DeadLetterAsync 32 | |> Async.AwaitTaskEmpty 33 | 34 | member __.AbandonMessage(token) = 35 | token 36 | |> receiver.AbandonAsync 37 | |> Async.AwaitTaskEmpty 38 | 39 | member __.CompleteMessage(token) = 40 | token 41 | |> receiver.CompleteAsync 42 | |> Async.AwaitTaskEmpty 43 | 44 | member __.GetNextMessage(timeout) = 45 | async { 46 | let! message = receiver.ReceiveAsync(timeout) |> Async.AwaitTask 47 | match message with 48 | | null -> return None 49 | | message -> 50 | return Some { Body = message.GetBody(wireSerializer) 51 | LockToken = message.LockToken 52 | Expiry = message.ExpiresAtUtc } 53 | } 54 | 55 | type private SessionisedQueueStream(session : MessageSession, wireSerializer : XmlObjectSerializer) = 56 | inherit QueueStream(session, wireSerializer) 57 | interface IActorMessageStream with 58 | member __.AbandonSession() = session.CloseAsync() |> Async.AwaitTaskEmpty 59 | member __.RenewSessionLock() = session.RenewLockAsync() |> Async.AwaitTaskEmpty 60 | member __.SessionId = ActorKey session.SessionId 61 | 62 | let CreateActorMessageStream (connectionString, queueName, timeout:TimeSpan, wireSerializer) = 63 | let queue = QueueClient.CreateFromConnectionString(connectionString, queueName) 64 | fun () -> 65 | async { 66 | let! session = queue.AcceptMessageSessionAsync(timeout) |> Async.AwaitTask |> Async.Catch 67 | return 68 | match session with 69 | | Error _ 70 | | Result null -> None 71 | | Result session -> Some(SessionisedQueueStream (session, wireSerializer) :> IActorMessageStream) 72 | } 73 | 74 | let CreateQueueStream(connectionString, queueName, wireSerializer) = 75 | let queueReceiver = MessagingFactory.CreateFromConnectionString(connectionString).CreateMessageReceiver(queueName) 76 | QueueStream(queueReceiver, wireSerializer) :> ICloudMessageStream 77 | 78 | [] 79 | module internal Serialization = 80 | /// Manages serialization / deserialization for putting messages on the queue. 81 | type ISerializer<'a> = 82 | /// Deserializes a string back into an object. 83 | abstract member Deserialize : string -> 'a 84 | /// Serializes an object into a string. 85 | abstract member Serialize : 'a -> string 86 | 87 | /// A serializer using Newtonsoft's JSON .NET serializer. 88 | let JsonSerializer<'a>() = 89 | { new ISerializer<'a> with 90 | member __.Deserialize(json) = JsonConvert.DeserializeObject<'a>(json) 91 | member __.Serialize(data) = JsonConvert.SerializeObject(data) } 92 | 93 | [] 94 | module internal Helpers = 95 | open FSharp.CloudAgent.Messaging 96 | 97 | /// Knows how to process a single brokered message from the service bus, with error handling 98 | /// and processing by the target queue. 99 | let ProcessBrokeredMessage<'a> (serializer:ISerializer<'a>) (agent:CloudAgentKind<'a>) message = 100 | async { 101 | let! messageBody = async { return serializer.Deserialize(message.Body) } |> Async.Catch 102 | let! processResult = 103 | match messageBody with 104 | | Error _ -> async { return Failed } // could not deserialize 105 | | Result messageBody -> 106 | async { 107 | match agent with 108 | | BasicCloudAgent agent -> 109 | // Do not wait for a response - just return success. 110 | agent.Post messageBody 111 | return Completed 112 | | ResilientCloudAgent agent -> 113 | // Wait for the response and return it. Timeout is set based on message expiry unless it's too large. 114 | let expiryInMs = 115 | match (int ((message.Expiry - DateTime.UtcNow).TotalMilliseconds)) with 116 | | expiryMs when expiryMs < -1 -> -1 117 | | expiryMs -> expiryMs 118 | 119 | let! processingResult = agent.PostAndTryAsyncReply((fun ch -> messageBody, ch.Reply), expiryInMs) |> Async.Catch 120 | return 121 | match processingResult with 122 | | Error _ 123 | | Result None -> Failed 124 | | Result (Some status) -> status 125 | } 126 | return processResult 127 | } 128 | 129 | /// Asynchronously gets the "next" item, repeatedly calling the supply getItem function 130 | /// until it returns something. 131 | let withAutomaticRetry getItem pollTime = 132 | let rec continuePolling() = 133 | async { 134 | let! nextItem = getItem() |> Async.Catch 135 | match nextItem with 136 | | Error ex -> return! continuePolling() 137 | | Result None -> return! continuePolling() 138 | | Result (Some item) -> return item 139 | } 140 | continuePolling() 141 | 142 | /// Manages dispatching of messages to a service bus queue. 143 | [] 144 | module internal Dispatch = 145 | open Microsoft.ServiceBus.Messaging 146 | 147 | /// Contains configuration details for posting messages to a cloud of agents or actors. 148 | type MessageDispatcher<'a> = 149 | { ServiceBusConnectionString : string 150 | QueueName : string 151 | Serializer : ISerializer<'a> } 152 | 153 | /// Creates a dispatcher using default settings. 154 | let createMessageDispatcher<'a> (connectionString, queueName) = 155 | { ServiceBusConnectionString = connectionString 156 | QueueName = queueName 157 | Serializer = JsonSerializer<'a>() } 158 | 159 | let private toBrokeredMessage options sessionId message = 160 | let payload = message |> options.Serializer.Serialize 161 | new BrokeredMessage(payload, SessionId = defaultArg sessionId null) 162 | 163 | let postMessages (options:MessageDispatcher<'a>) sessionId messages = 164 | let toBrokeredMessage = toBrokeredMessage options sessionId 165 | async { 166 | let brokeredMessages = 167 | messages 168 | |> Seq.map toBrokeredMessage 169 | |> Seq.toArray 170 | 171 | let queueClient = QueueClient.CreateFromConnectionString(options.ServiceBusConnectionString, options.QueueName) 172 | do! brokeredMessages |> queueClient.SendBatchAsync |> Async.AwaitTaskEmpty 173 | } 174 | 175 | let postMessage (options:MessageDispatcher<'a>) sessionId message = 176 | async { 177 | use brokeredMessage = message |> toBrokeredMessage options sessionId 178 | let queueClient = QueueClient.CreateFromConnectionString(options.ServiceBusConnectionString, options.QueueName) 179 | do! brokeredMessage |> queueClient.SendAsync |> Async.AwaitTaskEmpty 180 | } 181 | -------------------------------------------------------------------------------- /src/FSharp.CloudAgent/Types.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.CloudAgent 2 | 3 | /// Represents a unique key to identify an agent / actor. 4 | type ActorKey = 5 | /// Represents a unique key to identify an agent / actor. 6 | | ActorKey of string 7 | 8 | [] 9 | module internal Async = 10 | open System 11 | open System.Threading.Tasks 12 | 13 | let AwaitTaskEmpty(task : Task) = 14 | Async.FromContinuations(fun (onSuccess, onException, onCancellation) -> 15 | task.ContinueWith(fun t -> 16 | if t.IsCompleted then onSuccess() 17 | elif t.IsFaulted then onException (t.Exception) 18 | else onCancellation (System.OperationCanceledException())) 19 | |> ignore) 20 | 21 | let (|Result|Error|) = 22 | function 23 | | Choice1Of2 result -> Result result 24 | | Choice2Of2 (ex:Exception) -> Error ex 25 | 26 | namespace FSharp.CloudAgent.Connections 27 | 28 | /// Represents details of a connection to an Azure Service Bus. 29 | type ServiceBusConnection = 30 | /// Represents details of a connection to an Azure Service Bus. 31 | | ServiceBusConnection of string 32 | 33 | /// Represents a service bus queue. 34 | type Queue = 35 | /// Represents a service bus queue. 36 | | Queue of string 37 | 38 | /// Represents a connection to a pool of agents. 39 | type CloudConnection = 40 | /// A generic worker cloud that can run workloads in parallel. 41 | | WorkerCloudConnection of ServiceBusConnection * Queue 42 | /// An actor-based cloud that can run workloads in parallel whilst ensuring sequential workloads per-actor. 43 | | ActorCloudConnection of ServiceBusConnection * Queue 44 | namespace FSharp.CloudAgent.Messaging 45 | 46 | open System 47 | 48 | /// The different completion statuses a CloudMessage can have. 49 | type MessageProcessedStatus = 50 | /// The message successfully completed. 51 | | Completed 52 | /// The message was not processed successfully and should be returned to the queue for processing again. 53 | | Failed 54 | /// The message cannot be processed and should be not be attempted again. 55 | | Abandoned 56 | 57 | /// Represents the kinds of F# Agents that can be bound to an Azure Service Bus Queue for processing distributed messages, optionally with automatic retry. 58 | type CloudAgentKind<'a> = 59 | /// A simple cloud agent that offers simple forward-only processing of messages. 60 | | BasicCloudAgent of MailboxProcessor<'a> 61 | /// A cloud agent that requires explicit completion of processed message, with automatic retry and dead lettering. 62 | | ResilientCloudAgent of MailboxProcessor<'a * (MessageProcessedStatus -> unit)> 63 | 64 | /// Contains the raw data of a cloud message. 65 | type internal SimpleCloudMessage = 66 | { Body : string 67 | LockToken : Guid 68 | Expiry : DateTime } 69 | -------------------------------------------------------------------------------- /src/FSharp.CloudAgent/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | True 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/FSharp.CloudAgent/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | WindowsAzure.ServiceBus 3 | Microsoft.WindowsAzure.ConfigurationManager 4 | Newtonsoft.Json -------------------------------------------------------------------------------- /tests/FSharp.CloudAgent.Tests/Actors.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FSharp.CloudAgent.Tests.``Actor Factory`` 3 | 4 | open FSharp.CloudAgent 5 | open FSharp.CloudAgent.Actors 6 | open FSharp.CloudAgent.Messaging 7 | open FSharp.CloudAgent.Tests.Helpers 8 | open NUnit.Framework 9 | open System 10 | open Swensen.Unquote 11 | 12 | let private getActorStore() = Factory.CreateActorStore(fun _ -> getBasicAgent() |> snd) 13 | 14 | let private assertAgentIsStarted (agent:MailboxProcessor<_>) = 15 | Assertions.raisesWith 16 | <@ agent.Start() @> 17 | (fun ex -> <@ ex.Message = "The MailboxProcessor has already been started." @>) 18 | 19 | [] 20 | let ``ActorStore creates new actors``() = 21 | let actorStore = getActorStore() 22 | actorStore.GetActor (ActorKey "isaac") |> ignore 23 | 24 | [] 25 | let ``ActorStore automatically starts agents``() = 26 | let actorStore = getActorStore() 27 | let (BasicCloudAgent isaac) = actorStore.GetActor (ActorKey "isaac") 28 | isaac |> assertAgentIsStarted 29 | 30 | [] 31 | let ``ActorStore manages multiple new actors``() = 32 | let actorStore = getActorStore() 33 | let isaac = actorStore.GetActor (ActorKey "isaac") 34 | let tony = actorStore.GetActor (ActorKey "tony") 35 | isaac <>? tony 36 | 37 | [] 38 | let ``ActorStore reuses existing actors``() = 39 | let actorStore = getActorStore() 40 | let first = actorStore.GetActor (ActorKey "isaac") 41 | let second = actorStore.GetActor (ActorKey "isaac") 42 | first =? second 43 | 44 | [] 45 | let ``ActorStore removes existing actors``() = 46 | let actorStore = getActorStore() 47 | let first = actorStore.GetActor (ActorKey "isaac") 48 | actorStore.RemoveActor (ActorKey "isaac") 49 | let second = actorStore.GetActor (ActorKey "isaac") 50 | first <>? second 51 | 52 | [] 53 | let ``ActorStore disposes of removed agents``() = 54 | let actorStore = getActorStore() 55 | let (BasicCloudAgent isaac) = actorStore.GetActor (ActorKey "isaac") 56 | actorStore.RemoveActor (ActorKey "isaac") 57 | 58 | [] 59 | let ``AgentSelector creates already-started agents``() = 60 | let selector = Factory.CreateAgentSelector(5, fun _ -> getBasicAgent() |> snd) 61 | for _ in 1 .. 10 do 62 | let (BasicCloudAgent agent) = selector() |> Async.RunSynchronously 63 | agent |> assertAgentIsStarted -------------------------------------------------------------------------------- /tests/FSharp.CloudAgent.Tests/AutomaticRetry.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FSharp.CloudAgent.Tests.``Automatic Retry`` 3 | 4 | open FSharp.CloudAgent.Messaging 5 | open NUnit.Framework 6 | open Swensen.Unquote 7 | 8 | let private toGetNext (sequence:seq<_>) = 9 | let enumerator = sequence.GetEnumerator() 10 | fun () -> 11 | if enumerator.MoveNext() then enumerator.Current 12 | else async { return None } 13 | 14 | [] 15 | let ``Automatically retries until a result is found``() = 16 | let getNext = 17 | seq { yield None; yield None; yield Some 5 } 18 | |> Seq.map(fun x -> async { return x }) 19 | |> toGetNext 20 | let value = withAutomaticRetry getNext 0 |> Async.RunSynchronously 21 | 5 =? value 22 | 23 | [] 24 | let ``Automatically handles exceptions``() = 25 | let getNext = seq { yield async { failwith "foo"; return None }; yield async { return Some 5 } } |> toGetNext 26 | let value = withAutomaticRetry getNext 0 |> Async.RunSynchronously 27 | 5 =? value -------------------------------------------------------------------------------- /tests/FSharp.CloudAgent.Tests/FSharp.CloudAgent.Tests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | FSharp.CloudAgent.Tests 5 | Library 6 | net451 7 | pdbonly 8 | true 9 | true 10 | FSharp.CloudAgent.Tests.xml 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | FSharp.CloudAgent 23 | {f6147cd2-21e8-41ce-926f-9ab32eab770d} 24 | True 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/FSharp.CloudAgent.Tests/Helpers.fs: -------------------------------------------------------------------------------- 1 | module FSharp.CloudAgent.Tests.Helpers 2 | 3 | open FSharp.CloudAgent.Messaging 4 | open System 5 | 6 | type Person = { Name : string } 7 | type Thing = { Name : Person } 8 | let internal personSerializer = JsonSerializer() 9 | let internal createMessage<'a> (serializer:ISerializer<'a>) timeout data = 10 | { Body = serializer.Serialize data 11 | LockToken = Guid.NewGuid() 12 | Expiry = timeout } 13 | let internal createPersonMessage = createMessage personSerializer (DateTime.UtcNow.AddSeconds 10.) 14 | let internal processPersonMessage = Helpers.ProcessBrokeredMessage personSerializer 15 | 16 | let getBasicAgent() = 17 | let p = ref { Person.Name = "" } 18 | p, MailboxProcessor.Start(fun inbox -> 19 | async { 20 | while true do 21 | let! person = inbox.Receive() 22 | p := person 23 | }) |> BasicCloudAgent 24 | 25 | let getResilientAgent handler = 26 | MailboxProcessor.Start(fun inbox -> 27 | async { 28 | while true do 29 | let! message, reply = inbox.Receive() 30 | reply (handler message) 31 | }) |> ResilientCloudAgent -------------------------------------------------------------------------------- /tests/FSharp.CloudAgent.Tests/ProcessBrokeredMessage.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FSharp.CloudAgent.Tests.``Process Brokered Message`` 3 | 4 | open FSharp.CloudAgent.Tests.Helpers 5 | open FSharp.CloudAgent.Messaging 6 | open NUnit.Framework 7 | open Swensen.Unquote 8 | open System 9 | open System.Threading 10 | 11 | [] 12 | let ``Passes deserialized object into agent``() = 13 | let person, agent = getBasicAgent() 14 | let _ = { Person.Name = "Isaac" } 15 | |> createPersonMessage 16 | |> processPersonMessage agent 17 | |> Async.RunSynchronously 18 | Async.Sleep(100) |> Async.RunSynchronously 19 | !person =? { Person.Name = "Isaac" } 20 | 21 | [] 22 | let ``Immediately returns completed for BasicCloudAgent``() = 23 | let _, agent = getBasicAgent() 24 | let result = { Person.Name = "Isaac" } 25 | |> createPersonMessage 26 | |> processPersonMessage agent 27 | |> Async.RunSynchronously 28 | result =? Completed 29 | 30 | [] 31 | let ``Serialization failure immediately returns Failed``() = 32 | let _, agent = getBasicAgent() 33 | let message = 34 | let serializer = JsonSerializer() 35 | createMessage serializer (DateTime.UtcNow.AddSeconds 10.) { Name = { Name = "Isaac" } } 36 | 37 | let result = message |> processPersonMessage agent |> Async.RunSynchronously 38 | result =? Failed 39 | 40 | [] 41 | let ``Returns result from Resilient agent``() = 42 | let agent = getResilientAgent(fun _ -> Abandoned) 43 | let result = { Person.Name = "Isaac" } 44 | |> createPersonMessage 45 | |> processPersonMessage agent 46 | |> Async.RunSynchronously 47 | result =? Abandoned 48 | 49 | [] 50 | let ``No result from Resilient agent returns Failed``() = 51 | let agent = getResilientAgent(fun _ -> Thread.Sleep 2001; Completed) 52 | let result = { Person.Name = "Isaac" } 53 | |> createMessage personSerializer (DateTime.UtcNow.AddSeconds 2.) 54 | |> processPersonMessage agent 55 | |> Async.RunSynchronously 56 | result =? Failed 57 | 58 | [] 59 | let ``Exception from Resilient agent returns Failed``() = 60 | let agent = getResilientAgent(fun _ -> failwith "test") 61 | let result = { Person.Name = "Isaac" } 62 | |> createMessage personSerializer (DateTime.UtcNow.AddSeconds 2.) 63 | |> processPersonMessage agent 64 | |> Async.RunSynchronously 65 | result =? Failed 66 | 67 | [] 68 | let ``Message with maximum expiry time is correctly processed``() = 69 | let agent = getResilientAgent(fun _ -> Completed) 70 | let result = { Person.Name = "Isaac" } 71 | |> createMessage personSerializer DateTime.MaxValue 72 | |> processPersonMessage agent 73 | |> Async.RunSynchronously 74 | result =? Completed 75 | -------------------------------------------------------------------------------- /tests/FSharp.CloudAgent.Tests/Script.fsx: -------------------------------------------------------------------------------- 1 | #I @"..\..\src\FSharp.CloudAgent\" 2 | #I @"..\..\packages\" 3 | 4 | #r @"WindowsAzure.ServiceBus\lib\net40-full\Microsoft.ServiceBus.dll" 5 | #r @"Newtonsoft.Json\lib\net45\Newtonsoft.Json.dll" 6 | #r @"System.Runtime.Serialization.dll" 7 | #load "Types.fs" 8 | #load "Actors.fs" 9 | #load "Messaging.fs" 10 | #load "ConnectionFactory.fs" 11 | 12 | open FSharp.CloudAgent 13 | open FSharp.CloudAgent.Connections 14 | open FSharp.CloudAgent.Messaging 15 | 16 | // Connection strings to different service bus queues 17 | let connectionString = ServiceBusConnection "Endpoint=sb://yourServiceBus.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=yourKey" 18 | let workerConn = WorkerCloudConnection(connectionString, Queue "workerQueue") 19 | let actorConn = ActorCloudConnection(connectionString, Queue "actorQueue") 20 | 21 | type Person = { Name : string; Age : int } 22 | 23 | 24 | 25 | (* ------------- Standard F# Agent --------------- *) 26 | 27 | let createBasicAgent (ActorKey actorKey) = 28 | new MailboxProcessor(fun mailbox -> 29 | async { 30 | while true do 31 | let! message = mailbox.Receive() 32 | printfn "Actor %s has received message '%A', processing..." actorKey message 33 | }) 34 | 35 | // Start listening for messages without any error resilience 36 | let disposable = ConnectionFactory.StartListening(workerConn, createBasicAgent >> BasicCloudAgent) 37 | 38 | // Send a message to the worker pool 39 | { Name = "Isaac"; Age = 34 } 40 | |> ConnectionFactory.SendToWorkerPool workerConn 41 | |> Async.RunSynchronously 42 | 43 | disposable.Dispose() 44 | 45 | 46 | 47 | 48 | 49 | 50 | (* ------------- Resilient F# Agent using Service Bus to ensure processing of messages --------------- *) 51 | 52 | let createResilientAgent (ActorKey actorKey) = 53 | MailboxProcessor.Start(fun mailbox -> 54 | async { 55 | while true do 56 | let! message, reply = mailbox.Receive() 57 | printfn "Actor %s has received message '%A', processing..." actorKey message 58 | let response = 59 | match message with 60 | | { Name = "Isaac" } -> Completed 61 | | { Name = "Mike" } -> Abandoned 62 | | _ -> Failed 63 | printfn "%A" response 64 | reply response 65 | }) 66 | 67 | // Start listening for messages with built-in error resilience 68 | ConnectionFactory.StartListening(workerConn, createResilientAgent >> ResilientCloudAgent) 69 | 70 | // Send a message to the worker pool 71 | { Name = "Isaac"; Age = 34 } 72 | |> ConnectionFactory.SendToWorkerPool workerConn 73 | |> Async.RunSynchronously 74 | 75 | 76 | 77 | 78 | (* ------------- Actor-based F# Agents using Service Bus sessions to ensure synchronisation of messages--------------- *) 79 | 80 | // Start listening for actor messages with built-in error resilient 81 | let actorDisposable = ConnectionFactory.StartListening(actorConn, createResilientAgent >> ResilientCloudAgent) 82 | 83 | // Send a message 84 | { Name = "Isaac"; Age = 34 } 85 | |> ConnectionFactory.SendToActorPool actorConn (ActorKey "Tim") 86 | |> Async.RunSynchronously 87 | 88 | actorDisposable.Dispose() -------------------------------------------------------------------------------- /tests/FSharp.CloudAgent.Tests/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | True 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/FSharp.CloudAgent.Tests/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | NUnit 3 | NUnit.Console 4 | Unquote --------------------------------------------------------------------------------