diff --git a/.gitignore b/.gitignore
index 16efb5f9b..be686e726 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@
*.user
bin/
obj/
+pkg/
packages/
TestResults/
diff --git a/DurableTask.sln b/DurableTask.sln
index 33ff4e70f..dd8e8f4d8 100644
--- a/DurableTask.sln
+++ b/DurableTask.sln
@@ -45,54 +45,112 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.Redis", "src\Du
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.Redis.Tests", "Test\DurableTask.Redis.Tests\DurableTask.Redis.Tests.csproj", "{B1C3B554-A098-47F8-8FA9-C28B5BBF35B1}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DurableTask.AzureServiceFabric", "src\DurableTask.AzureServiceFabric\DurableTask.AzureServiceFabric.csproj", "{3C10169F-4ACB-4FF6-A12B-B74569613E81}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DurableTask.AzureServiceFabric.Tests", "Test\DurableTask.AzureServiceFabric.Tests\DurableTask.AzureServiceFabric.Tests.csproj", "{AA0BCA9D-03F7-4F33-BBD5-2D99181E1C2A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DurableTask.AzureServiceFabric.Integration.Tests", "Test\DurableTask.AzureServiceFabric.Integration.Tests\DurableTask.AzureServiceFabric.Integration.Tests.csproj", "{8C3D4CA2-4158-4631-BD5E-F5D066962FAF}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestFabricApplication", "TestFabricApplication", "{C53918E6-667A-4930-837C-0018C5D6E374}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApplication.Common", "Test\TestFabricApplication\TestApplication.Common\TestApplication.Common.csproj", "{99EB4BD2-72E4-4471-8E8D-9454BEACFA35}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApplication.StatefulService", "Test\TestFabricApplication\TestApplication.StatefulService\TestApplication.StatefulService.csproj", "{88A231A9-940D-43E4-B7CD-B46BA9375367}"
+EndProject
+Project("{A07B5EB6-E848-4116-A8D0-A826331D98C6}") = "TestApplication", "Test\TestFabricApplication\TestApplication\TestApplication.sfproj", "{1D52F94E-933C-411F-96C4-4960B423586F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6F5D2EAD-726D-4FE5-A575-AEB96D1CCE37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F5D2EAD-726D-4FE5-A575-AEB96D1CCE37}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6F5D2EAD-726D-4FE5-A575-AEB96D1CCE37}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6F5D2EAD-726D-4FE5-A575-AEB96D1CCE37}.Debug|x64.Build.0 = Debug|Any CPU
{6F5D2EAD-726D-4FE5-A575-AEB96D1CCE37}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F5D2EAD-726D-4FE5-A575-AEB96D1CCE37}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6F5D2EAD-726D-4FE5-A575-AEB96D1CCE37}.Release|x64.ActiveCfg = Release|Any CPU
+ {6F5D2EAD-726D-4FE5-A575-AEB96D1CCE37}.Release|x64.Build.0 = Release|Any CPU
{6639DA74-9063-4A7C-9484-0A71C4FB5C18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6639DA74-9063-4A7C-9484-0A71C4FB5C18}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6639DA74-9063-4A7C-9484-0A71C4FB5C18}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6639DA74-9063-4A7C-9484-0A71C4FB5C18}.Debug|x64.Build.0 = Debug|Any CPU
{6639DA74-9063-4A7C-9484-0A71C4FB5C18}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6639DA74-9063-4A7C-9484-0A71C4FB5C18}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6639DA74-9063-4A7C-9484-0A71C4FB5C18}.Release|x64.ActiveCfg = Release|Any CPU
+ {6639DA74-9063-4A7C-9484-0A71C4FB5C18}.Release|x64.Build.0 = Release|Any CPU
{879670A7-919B-4AF9-8AFB-7C4EDAA0CC04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{879670A7-919B-4AF9-8AFB-7C4EDAA0CC04}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {879670A7-919B-4AF9-8AFB-7C4EDAA0CC04}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {879670A7-919B-4AF9-8AFB-7C4EDAA0CC04}.Debug|x64.Build.0 = Debug|Any CPU
{879670A7-919B-4AF9-8AFB-7C4EDAA0CC04}.Release|Any CPU.ActiveCfg = Release|Any CPU
{879670A7-919B-4AF9-8AFB-7C4EDAA0CC04}.Release|Any CPU.Build.0 = Release|Any CPU
+ {879670A7-919B-4AF9-8AFB-7C4EDAA0CC04}.Release|x64.ActiveCfg = Release|Any CPU
+ {879670A7-919B-4AF9-8AFB-7C4EDAA0CC04}.Release|x64.Build.0 = Release|Any CPU
{BC204374-927C-4A48-8473-F5BF8C97EAEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC204374-927C-4A48-8473-F5BF8C97EAEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BC204374-927C-4A48-8473-F5BF8C97EAEA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BC204374-927C-4A48-8473-F5BF8C97EAEA}.Debug|x64.Build.0 = Debug|Any CPU
{BC204374-927C-4A48-8473-F5BF8C97EAEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC204374-927C-4A48-8473-F5BF8C97EAEA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BC204374-927C-4A48-8473-F5BF8C97EAEA}.Release|x64.ActiveCfg = Release|Any CPU
+ {BC204374-927C-4A48-8473-F5BF8C97EAEA}.Release|x64.Build.0 = Release|Any CPU
{96980359-8003-4500-B97B-949A7FE01F31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{96980359-8003-4500-B97B-949A7FE01F31}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {96980359-8003-4500-B97B-949A7FE01F31}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {96980359-8003-4500-B97B-949A7FE01F31}.Debug|x64.Build.0 = Debug|Any CPU
{96980359-8003-4500-B97B-949A7FE01F31}.Release|Any CPU.ActiveCfg = Release|Any CPU
{96980359-8003-4500-B97B-949A7FE01F31}.Release|Any CPU.Build.0 = Release|Any CPU
+ {96980359-8003-4500-B97B-949A7FE01F31}.Release|x64.ActiveCfg = Release|Any CPU
+ {96980359-8003-4500-B97B-949A7FE01F31}.Release|x64.Build.0 = Release|Any CPU
{2BC2A5AD-4200-41B9-A034-332BB1D7187B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2BC2A5AD-4200-41B9-A034-332BB1D7187B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2BC2A5AD-4200-41B9-A034-332BB1D7187B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2BC2A5AD-4200-41B9-A034-332BB1D7187B}.Debug|x64.Build.0 = Debug|Any CPU
{2BC2A5AD-4200-41B9-A034-332BB1D7187B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2BC2A5AD-4200-41B9-A034-332BB1D7187B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2BC2A5AD-4200-41B9-A034-332BB1D7187B}.Release|x64.ActiveCfg = Release|Any CPU
+ {2BC2A5AD-4200-41B9-A034-332BB1D7187B}.Release|x64.Build.0 = Release|Any CPU
{C8634F14-BE3B-4685-B0E1-C5726F8F3758}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8634F14-BE3B-4685-B0E1-C5726F8F3758}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C8634F14-BE3B-4685-B0E1-C5726F8F3758}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C8634F14-BE3B-4685-B0E1-C5726F8F3758}.Debug|x64.Build.0 = Debug|Any CPU
{C8634F14-BE3B-4685-B0E1-C5726F8F3758}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8634F14-BE3B-4685-B0E1-C5726F8F3758}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C8634F14-BE3B-4685-B0E1-C5726F8F3758}.Release|x64.ActiveCfg = Release|Any CPU
+ {C8634F14-BE3B-4685-B0E1-C5726F8F3758}.Release|x64.Build.0 = Release|Any CPU
{ACC997BF-D541-4BAF-A91C-7EC0B65426A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ACC997BF-D541-4BAF-A91C-7EC0B65426A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ACC997BF-D541-4BAF-A91C-7EC0B65426A9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {ACC997BF-D541-4BAF-A91C-7EC0B65426A9}.Debug|x64.Build.0 = Debug|Any CPU
{ACC997BF-D541-4BAF-A91C-7EC0B65426A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ACC997BF-D541-4BAF-A91C-7EC0B65426A9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ACC997BF-D541-4BAF-A91C-7EC0B65426A9}.Release|x64.ActiveCfg = Release|Any CPU
+ {ACC997BF-D541-4BAF-A91C-7EC0B65426A9}.Release|x64.Build.0 = Release|Any CPU
{295A38D6-F8BF-446A-A274-E78BD1C56514}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{295A38D6-F8BF-446A-A274-E78BD1C56514}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {295A38D6-F8BF-446A-A274-E78BD1C56514}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {295A38D6-F8BF-446A-A274-E78BD1C56514}.Debug|x64.Build.0 = Debug|Any CPU
{295A38D6-F8BF-446A-A274-E78BD1C56514}.Release|Any CPU.ActiveCfg = Release|Any CPU
{295A38D6-F8BF-446A-A274-E78BD1C56514}.Release|Any CPU.Build.0 = Release|Any CPU
+ {295A38D6-F8BF-446A-A274-E78BD1C56514}.Release|x64.ActiveCfg = Release|Any CPU
+ {295A38D6-F8BF-446A-A274-E78BD1C56514}.Release|x64.Build.0 = Release|Any CPU
{F1B270EA-9DC2-4753-B3DE-B45D3C128FDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1B270EA-9DC2-4753-B3DE-B45D3C128FDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F1B270EA-9DC2-4753-B3DE-B45D3C128FDB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F1B270EA-9DC2-4753-B3DE-B45D3C128FDB}.Debug|x64.Build.0 = Debug|Any CPU
{F1B270EA-9DC2-4753-B3DE-B45D3C128FDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1B270EA-9DC2-4753-B3DE-B45D3C128FDB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F1B270EA-9DC2-4753-B3DE-B45D3C128FDB}.Release|x64.ActiveCfg = Release|Any CPU
+ {F1B270EA-9DC2-4753-B3DE-B45D3C128FDB}.Release|x64.Build.0 = Release|Any CPU
{7CD147F9-1BB9-41FD-A310-BCA07447A6B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7CD147F9-1BB9-41FD-A310-BCA07447A6B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7CD147F9-1BB9-41FD-A310-BCA07447A6B9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7CD147F9-1BB9-41FD-A310-BCA07447A6B9}.Debug|x64.Build.0 = Debug|Any CPU
{7CD147F9-1BB9-41FD-A310-BCA07447A6B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CD147F9-1BB9-41FD-A310-BCA07447A6B9}.Release|Any CPU.Build.0 = Release|Any CPU
{BDA5B320-5282-49FE-B0A7-1F8A0F06862C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -103,6 +161,56 @@ Global
{B1C3B554-A098-47F8-8FA9-C28B5BBF35B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1C3B554-A098-47F8-8FA9-C28B5BBF35B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1C3B554-A098-47F8-8FA9-C28B5BBF35B1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7CD147F9-1BB9-41FD-A310-BCA07447A6B9}.Release|x64.ActiveCfg = Release|Any CPU
+ {7CD147F9-1BB9-41FD-A310-BCA07447A6B9}.Release|x64.Build.0 = Release|Any CPU
+ {3C10169F-4ACB-4FF6-A12B-B74569613E81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3C10169F-4ACB-4FF6-A12B-B74569613E81}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3C10169F-4ACB-4FF6-A12B-B74569613E81}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3C10169F-4ACB-4FF6-A12B-B74569613E81}.Debug|x64.Build.0 = Debug|Any CPU
+ {3C10169F-4ACB-4FF6-A12B-B74569613E81}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3C10169F-4ACB-4FF6-A12B-B74569613E81}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3C10169F-4ACB-4FF6-A12B-B74569613E81}.Release|x64.ActiveCfg = Release|Any CPU
+ {3C10169F-4ACB-4FF6-A12B-B74569613E81}.Release|x64.Build.0 = Release|Any CPU
+ {AA0BCA9D-03F7-4F33-BBD5-2D99181E1C2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AA0BCA9D-03F7-4F33-BBD5-2D99181E1C2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AA0BCA9D-03F7-4F33-BBD5-2D99181E1C2A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AA0BCA9D-03F7-4F33-BBD5-2D99181E1C2A}.Debug|x64.Build.0 = Debug|Any CPU
+ {AA0BCA9D-03F7-4F33-BBD5-2D99181E1C2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AA0BCA9D-03F7-4F33-BBD5-2D99181E1C2A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AA0BCA9D-03F7-4F33-BBD5-2D99181E1C2A}.Release|x64.ActiveCfg = Release|Any CPU
+ {AA0BCA9D-03F7-4F33-BBD5-2D99181E1C2A}.Release|x64.Build.0 = Release|Any CPU
+ {8C3D4CA2-4158-4631-BD5E-F5D066962FAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C3D4CA2-4158-4631-BD5E-F5D066962FAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C3D4CA2-4158-4631-BD5E-F5D066962FAF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8C3D4CA2-4158-4631-BD5E-F5D066962FAF}.Debug|x64.Build.0 = Debug|Any CPU
+ {8C3D4CA2-4158-4631-BD5E-F5D066962FAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C3D4CA2-4158-4631-BD5E-F5D066962FAF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C3D4CA2-4158-4631-BD5E-F5D066962FAF}.Release|x64.ActiveCfg = Release|Any CPU
+ {8C3D4CA2-4158-4631-BD5E-F5D066962FAF}.Release|x64.Build.0 = Release|Any CPU
+ {99EB4BD2-72E4-4471-8E8D-9454BEACFA35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {99EB4BD2-72E4-4471-8E8D-9454BEACFA35}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {99EB4BD2-72E4-4471-8E8D-9454BEACFA35}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {99EB4BD2-72E4-4471-8E8D-9454BEACFA35}.Debug|x64.Build.0 = Debug|Any CPU
+ {99EB4BD2-72E4-4471-8E8D-9454BEACFA35}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {99EB4BD2-72E4-4471-8E8D-9454BEACFA35}.Release|Any CPU.Build.0 = Release|Any CPU
+ {99EB4BD2-72E4-4471-8E8D-9454BEACFA35}.Release|x64.ActiveCfg = Release|Any CPU
+ {99EB4BD2-72E4-4471-8E8D-9454BEACFA35}.Release|x64.Build.0 = Release|Any CPU
+ {88A231A9-940D-43E4-B7CD-B46BA9375367}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {88A231A9-940D-43E4-B7CD-B46BA9375367}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {88A231A9-940D-43E4-B7CD-B46BA9375367}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {88A231A9-940D-43E4-B7CD-B46BA9375367}.Debug|x64.Build.0 = Debug|Any CPU
+ {88A231A9-940D-43E4-B7CD-B46BA9375367}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {88A231A9-940D-43E4-B7CD-B46BA9375367}.Release|Any CPU.Build.0 = Release|Any CPU
+ {88A231A9-940D-43E4-B7CD-B46BA9375367}.Release|x64.ActiveCfg = Release|Any CPU
+ {88A231A9-940D-43E4-B7CD-B46BA9375367}.Release|x64.Build.0 = Release|Any CPU
+ {1D52F94E-933C-411F-96C4-4960B423586F}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {1D52F94E-933C-411F-96C4-4960B423586F}.Debug|x64.ActiveCfg = Debug|x64
+ {1D52F94E-933C-411F-96C4-4960B423586F}.Debug|x64.Build.0 = Debug|x64
+ {1D52F94E-933C-411F-96C4-4960B423586F}.Debug|x64.Deploy.0 = Debug|x64
+ {1D52F94E-933C-411F-96C4-4960B423586F}.Release|Any CPU.ActiveCfg = Release|x64
+ {1D52F94E-933C-411F-96C4-4960B423586F}.Release|x64.ActiveCfg = Release|x64
+ {1D52F94E-933C-411F-96C4-4960B423586F}.Release|x64.Build.0 = Release|x64
+ {1D52F94E-933C-411F-96C4-4960B423586F}.Release|x64.Deploy.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -121,6 +229,13 @@ Global
{7CD147F9-1BB9-41FD-A310-BCA07447A6B9} = {95C69A06-7F62-4652-A480-207B614C2869}
{BDA5B320-5282-49FE-B0A7-1F8A0F06862C} = {DBCD161C-D409-48E5-924E-9B7FA1C36B84}
{B1C3B554-A098-47F8-8FA9-C28B5BBF35B1} = {95C69A06-7F62-4652-A480-207B614C2869}
+ {3C10169F-4ACB-4FF6-A12B-B74569613E81} = {DBCD161C-D409-48E5-924E-9B7FA1C36B84}
+ {AA0BCA9D-03F7-4F33-BBD5-2D99181E1C2A} = {95C69A06-7F62-4652-A480-207B614C2869}
+ {8C3D4CA2-4158-4631-BD5E-F5D066962FAF} = {95C69A06-7F62-4652-A480-207B614C2869}
+ {C53918E6-667A-4930-837C-0018C5D6E374} = {95C69A06-7F62-4652-A480-207B614C2869}
+ {99EB4BD2-72E4-4471-8E8D-9454BEACFA35} = {C53918E6-667A-4930-837C-0018C5D6E374}
+ {88A231A9-940D-43E4-B7CD-B46BA9375367} = {C53918E6-667A-4930-837C-0018C5D6E374}
+ {1D52F94E-933C-411F-96C4-4960B423586F} = {C53918E6-667A-4930-837C-0018C5D6E374}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2D63A120-9394-48D9-8CA9-1184364FB854}
diff --git a/Test/DurableTask.AzureServiceFabric.Integration.Tests/App.config b/Test/DurableTask.AzureServiceFabric.Integration.Tests/App.config
new file mode 100644
index 000000000..e936cc132
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Integration.Tests/App.config
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Test/DurableTask.AzureServiceFabric.Integration.Tests/AssemblySetup.cs b/Test/DurableTask.AzureServiceFabric.Integration.Tests/AssemblySetup.cs
new file mode 100644
index 000000000..f4cc21b60
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Integration.Tests/AssemblySetup.cs
@@ -0,0 +1,64 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Integration.Tests
+{
+ using System;
+ using System.IO;
+
+ using DurableTask.AzureServiceFabric.Integration.Tests.DeploymentUtil;
+
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class AssemblySetup
+ {
+ [AssemblyInitialize]
+ public static void AssemblyInitialize(TestContext testContext)
+ {
+ //Todo: This will fail if local cluster is not setup, currently the test code does not automatically
+ //start a local cluster and that's a manual pre-req step.
+ //Todo: Also this will assume that latest SF test application and service code is packaged, if that's not true
+ //the issue might be hard to detect :-(
+ DeploymentHelper.CleanAsync().Wait();
+ DeploymentHelper.DeployAsync(TestApplicationRootPath).Wait();
+ }
+
+ [AssemblyCleanup]
+ public static void AssemblyCleanup()
+ {
+ DeploymentHelper.CleanAsync().Wait();
+ }
+
+ static string TestApplicationRootPath
+ {
+ get
+ {
+ var sourceRoot = Environment.GetEnvironmentVariable("SourceRoot") ?? string.Empty;
+ var applicationPath = Path.Combine(sourceRoot.Trim(), "Test", "TestFabricApplication", "TestFabricApplication");
+
+ if (!Directory.Exists(applicationPath))
+ {
+ throw new Exception("Could not find test application path, define SourceRoot environment variable to the source path");
+ }
+
+ if (!Directory.Exists(Path.Combine(applicationPath, "pkg", "Debug")))
+ {
+ throw new Exception("Could not find test application package, make sure the test application is built and package generated before running the tests");
+ }
+
+ return applicationPath;
+ }
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Integration.Tests/Constants.cs b/Test/DurableTask.AzureServiceFabric.Integration.Tests/Constants.cs
new file mode 100644
index 000000000..db8bc9f36
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Integration.Tests/Constants.cs
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Integration.Tests
+{
+ static class Constants
+ {
+ public const string TestFabricApplicationAddress = "fabric:/TestFabricApplicationType/TestStatefulService";
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Integration.Tests/DeploymentUtil/ApplicationInfoReader.cs b/Test/DurableTask.AzureServiceFabric.Integration.Tests/DeploymentUtil/ApplicationInfoReader.cs
new file mode 100644
index 000000000..de135a0ed
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Integration.Tests/DeploymentUtil/ApplicationInfoReader.cs
@@ -0,0 +1,105 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Integration.Tests.DeploymentUtil
+{
+ using System.Collections.Specialized;
+ using System.IO;
+ using System.Linq;
+ using System.Xml;
+
+ class ApplicationInfoReader
+ {
+ string applicationRootPath;
+ string applicationPackagePath;
+
+ readonly XmlElement applicationManifestRoot;
+ readonly XmlNamespaceManager applicationManifestNamespaceManager;
+ readonly XmlElement serviceManifestRoot;
+ readonly XmlNamespaceManager serviceManifestNamespaceManager;
+
+ const string ApplicationManifestNodePath = "/sf:ApplicationManifest";
+ const string ApplicationNameAttribute = "ApplicationTypeName";
+ const string ApplicationVersionAttribute = "ApplicationTypeVersion";
+
+ const string StatefulServiceNodePath = "/sf:ServiceManifest/sf:ServiceTypes/sf:StatefulServiceType";
+ const string StatelessServiceNodePath = "/sf:ServiceManifest/sf:ServiceTypes/sf:StatelessServiceType";
+ const string ServiceTypeNameAttribute = "ServiceTypeName";
+
+ public ApplicationInfoReader(string applicationRootPath)
+ {
+ this.applicationRootPath = applicationRootPath;
+ this.applicationPackagePath = Path.Combine(applicationRootPath, @"pkg\Debug");
+
+ var applicationManifest = new XmlDocument();
+ applicationManifest.Load(Path.Combine(this.applicationPackagePath, "ApplicationManifest.xml"));
+ this.applicationManifestNamespaceManager = GetXmlNamespaceManager(applicationManifest.NameTable);
+ this.applicationManifestRoot = applicationManifest.DocumentElement;
+
+ var serviceManifest = new XmlDocument();
+ var serviceDirectory = Directory.EnumerateDirectories(this.applicationPackagePath).First();
+ serviceManifest.Load(Path.Combine(Path.Combine(this.applicationPackagePath, serviceDirectory), "ServiceManifest.xml"));
+ this.serviceManifestNamespaceManager = GetXmlNamespaceManager(serviceManifest.NameTable);
+ this.serviceManifestRoot = serviceManifest.DocumentElement;
+ }
+
+ public string GetApplicationName()
+ {
+ return GetSingleNodeAttributeValue(applicationManifestRoot, applicationManifestNamespaceManager, ApplicationManifestNodePath, ApplicationNameAttribute);
+ }
+
+ public string GetApplicationVersion()
+ {
+ return GetSingleNodeAttributeValue(applicationManifestRoot, applicationManifestNamespaceManager, ApplicationManifestNodePath, ApplicationVersionAttribute);
+ }
+
+ public string GetServiceName()
+ {
+ return GetSingleNodeAttributeValue(serviceManifestRoot, serviceManifestNamespaceManager, StatefulServiceNodePath, ServiceTypeNameAttribute) ??
+ GetSingleNodeAttributeValue(serviceManifestRoot, serviceManifestNamespaceManager, StatelessServiceNodePath, ServiceTypeNameAttribute);
+ }
+
+ public NameValueCollection GetApplicationParameters()
+ {
+ var applicationParametersPath = Path.Combine(applicationRootPath, @"ApplicationParameters\Local.5Node.xml");
+
+ var applicationParameters = new XmlDocument();
+ applicationParameters.Load(applicationParametersPath);
+
+ var parametersPath = "/sf:Application/sf:Parameters/sf:Parameter";
+ var parameters = new NameValueCollection();
+
+ foreach (var parameterNode in applicationParameters.DocumentElement.SelectNodes(parametersPath, GetXmlNamespaceManager(applicationParameters.NameTable)).OfType())
+ {
+ parameters.Add(parameterNode.Attributes["Name"].Value, parameterNode.Attributes["Value"].Value);
+ }
+
+ return parameters;
+ }
+
+ public string ApplicationPackagePath => this.applicationPackagePath;
+
+ string GetSingleNodeAttributeValue(XmlElement root, XmlNamespaceManager namespaceManager, string queryPath, string attributeName)
+ {
+ var node = root.SelectSingleNode(queryPath, namespaceManager);
+ return node?.Attributes?[attributeName].Value;
+ }
+
+ XmlNamespaceManager GetXmlNamespaceManager(XmlNameTable nameTable)
+ {
+ var result = new XmlNamespaceManager(nameTable);
+ result.AddNamespace("sf", "http://schemas.microsoft.com/2011/01/fabric");
+ return result;
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Integration.Tests/DeploymentUtil/DeploymentHelper.cs b/Test/DurableTask.AzureServiceFabric.Integration.Tests/DeploymentUtil/DeploymentHelper.cs
new file mode 100644
index 000000000..80578da76
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Integration.Tests/DeploymentUtil/DeploymentHelper.cs
@@ -0,0 +1,106 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Integration.Tests.DeploymentUtil
+{
+ using System;
+ using System.Fabric;
+ using System.Fabric.Description;
+ using System.Fabric.Health;
+ using System.Fabric.Query;
+ using System.Linq;
+ using System.Threading.Tasks;
+ using System.Xml;
+
+ static class DeploymentHelper
+ {
+ static readonly FabricClient client = new FabricClient();
+
+ public static async Task DeployAsync(string applicationRootPath)
+ {
+ var appInfoReader = new ApplicationInfoReader(applicationRootPath);
+ var serviceName = new Uri($"fabric:/{appInfoReader.GetApplicationName()}/{appInfoReader.GetServiceName()}");
+ var applicationDescription =
+ new ApplicationDescription(
+ new Uri($"fabric:/{appInfoReader.GetApplicationName()}"),
+ appInfoReader.GetApplicationName(),
+ appInfoReader.GetApplicationVersion(),
+ appInfoReader.GetApplicationParameters());
+
+ await DeployAsync(appInfoReader.ApplicationPackagePath, applicationDescription);
+ await WaitForHealthStatusAsync(applicationDescription, serviceName);
+ }
+
+ public static async Task CleanAsync()
+ {
+ var applications = await client.QueryManager.GetApplicationListAsync();
+ foreach (var application in applications)
+ {
+ await client.ApplicationManager.DeleteApplicationAsync(new DeleteApplicationDescription(application.ApplicationName) { ForceDelete = true });
+
+ foreach (var node in await client.QueryManager.GetNodeListAsync())
+ {
+ var replicas = (await client.QueryManager.GetDeployedReplicaListAsync(node.NodeName, application.ApplicationName)).OfType();
+ foreach (var replica in replicas)
+ {
+ await client.ServiceManager.RemoveReplicaAsync(node.NodeName, replica.Partitionid, replica.ReplicaId);
+ }
+ }
+ }
+
+ var applicationTypeList = await client.QueryManager.GetApplicationTypeListAsync();
+ foreach (var applicationType in applicationTypeList)
+ {
+ await client.ApplicationManager.UnprovisionApplicationAsync(applicationType.ApplicationTypeName, applicationType.ApplicationTypeVersion);
+ }
+ }
+
+ static async Task DeployAsync(string applicationPackagePath, ApplicationDescription applicationDescription)
+ {
+ var clusterManifest = new XmlDocument();
+ var clusterManifestXml = await client.ClusterManager.GetClusterManifestAsync();
+ clusterManifest.LoadXml(clusterManifestXml);
+ var namespaceManager = new XmlNamespaceManager(clusterManifest.NameTable);
+ namespaceManager.AddNamespace("sf", "http://schemas.microsoft.com/2011/01/fabric");
+ var imageStoreConnectionStringPath = "/sf:ClusterManifest/sf:FabricSettings/sf:Section[@Name='Management']/sf:Parameter[@Name='ImageStoreConnectionString']";
+ var imageStoreConnectionStringNode = clusterManifest.DocumentElement.SelectSingleNode(imageStoreConnectionStringPath, namespaceManager);
+ var imageStoreConnectionString = imageStoreConnectionStringNode.Attributes["Value"].Value;
+
+ var path = Guid.NewGuid().ToString();
+ client.ApplicationManager.CopyApplicationPackage(imageStoreConnectionString, applicationPackagePath, path);
+
+ await client.ApplicationManager.ProvisionApplicationAsync(path);
+
+ client.ApplicationManager.RemoveApplicationPackage(imageStoreConnectionString, path);
+
+ await client.ApplicationManager.CreateApplicationAsync(applicationDescription);
+ }
+
+ static async Task WaitForHealthStatusAsync(ApplicationDescription applicationDescription, Uri serviceName)
+ {
+ while (true)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(2));
+ var states = (await client.HealthManager.GetApplicationHealthAsync(applicationDescription.ApplicationName)).ServiceHealthStates;
+ if (states.Any() && states.All(s => s.AggregatedHealthState == HealthState.Ok))
+ {
+ return;
+ }
+ if (states.Any() && states.Any(s => s.AggregatedHealthState == HealthState.Error || s.AggregatedHealthState == HealthState.Invalid))
+ {
+ throw new Exception($"Deployment of {serviceName} failed");
+ }
+ }
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Integration.Tests/DurableTask.AzureServiceFabric.Integration.Tests.csproj b/Test/DurableTask.AzureServiceFabric.Integration.Tests/DurableTask.AzureServiceFabric.Integration.Tests.csproj
new file mode 100644
index 000000000..16ef549e3
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Integration.Tests/DurableTask.AzureServiceFabric.Integration.Tests.csproj
@@ -0,0 +1,34 @@
+
+
+
+
+ net461
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Test/DurableTask.AzureServiceFabric.Integration.Tests/FunctionalTests.cs b/Test/DurableTask.AzureServiceFabric.Integration.Tests/FunctionalTests.cs
new file mode 100644
index 000000000..c14dd5932
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Integration.Tests/FunctionalTests.cs
@@ -0,0 +1,346 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Integration.Tests
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.Test.Orchestrations.Performance;
+
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ using TestApplication.Common.Orchestrations;
+
+ [TestClass]
+ public class FunctionalTests
+ {
+ TaskHubClient taskHubClient;
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ this.taskHubClient = Utilities.CreateTaskHubClient();
+ }
+
+ [TestMethod]
+ public async Task Orchestration_With_ScheduledTasks_Finishes()
+ {
+ var instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(SimpleOrchestrationWithTasks), null);
+ var result = await this.taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(2));
+
+ Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus);
+ Assert.AreEqual("\"Hello Gabbar\"", result.Output);
+ }
+
+ [TestMethod]
+ public async Task Orchestration_With_Timer_Finishes_After_The_Wait_Time()
+ {
+ var waitTime = 37;
+ var instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(SimpleOrchestrationWithTasks), waitTime);
+ var result = await this.taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(2));
+
+ Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus);
+ Assert.AreEqual("\"Hello Gabbar\"", result.Output);
+ var orchestrationTime = result.CompletedTime - result.CreatedTime;
+ Console.WriteLine($"Time for Orchestration : {orchestrationTime}, Timer Wait time : {waitTime}");
+ }
+
+ [TestMethod]
+ public async Task Orchestration_With_SubOrchestration_Finishes()
+ {
+ var instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(SimpleOrchestrationWithSubOrchestration), null);
+ var result = await this.taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(2));
+
+ Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus);
+ Assert.AreEqual($"\"TaskResult = Hello World , SubOrchestration1Result = Hello Gabbar, SubOrchestration2Result = Hello Gabbar\"", result.Output);
+ }
+
+ [TestMethod]
+ public async Task Orchestration_With_TimeoutWrapper_Test()
+ {
+ // Task finishes within timeout
+ var input = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 1,
+ MaxDelay = 0,
+ MinDelay = 0,
+ DelayUnit = TimeSpan.FromMilliseconds(1),
+ UseTimeoutTask = true,
+ ExecutionTimeout = TimeSpan.FromMinutes(1)
+ };
+ var instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), input);
+ var result = await this.taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(2));
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus);
+ Console.WriteLine($"Time for Orchestration with no delay running task wrapped in 1 minute timeout : {result.CompletedTime - result.CreatedTime}");
+
+ // Task does not finish within timeout
+ input = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 1,
+ MaxDelay = 5,
+ MinDelay = 5,
+ DelayUnit = TimeSpan.FromSeconds(1),
+ UseTimeoutTask = true,
+ ExecutionTimeout = TimeSpan.FromSeconds(1)
+ };
+
+ instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), input);
+ result = await this.taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(2));
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(OrchestrationStatus.Failed, result.OrchestrationStatus);
+ Console.WriteLine($"Time for Orchestration with 5 second running task wrapped in 1 second timeout : {result.CompletedTime - result.CreatedTime}");
+ }
+
+ [TestMethod]
+ public async Task Orchestration_With_Same_Id_Cant_Be_Started_While_Running()
+ {
+ var instanceId = nameof(Orchestration_With_Same_Id_Cant_Be_Started_While_Running);
+ var testData = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 1,
+ MaxDelay = 5,
+ MinDelay = 5,
+ DelayUnit = TimeSpan.FromSeconds(1),
+ };
+
+ Func>> startFunc = async () =>
+ {
+ OrchestrationInstance instance = null;
+ Exception exception = null;
+
+ try
+ {
+ instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), instanceId, testData);
+ }
+ catch (Exception e)
+ {
+ while (e is AggregateException)
+ {
+ e = e.InnerException;
+ }
+
+ exception = e;
+ }
+
+ return Tuple.Create(instance, exception);
+ };
+
+ var allResults = await Task.WhenAll(startFunc(), startFunc(), startFunc(), startFunc());
+
+ OrchestrationInstance createdInstance = null;
+
+ for(int i = 0; i < allResults.Length; i++)
+ {
+ var result = allResults[i];
+ if (result.Item1 != null)
+ {
+ if (createdInstance != null)
+ {
+ Assert.Fail($"Multiple orchestrations were started with the instance id {instanceId} at the same time");
+ }
+ else
+ {
+ createdInstance = result.Item1;
+ }
+ }
+ else
+ {
+ Assert.IsInstanceOfType(result.Item2, typeof(Exception),
+ $"Exception Type Check Failed : Task {i} returned an unexpected exception {result.Item2}");
+ }
+ }
+
+ Assert.IsNotNull(createdInstance, $"No task was able to create an orchestration with the given instance id {instanceId}");
+ var orchestrationResult = await this.taskHubClient.WaitForOrchestrationAsync(createdInstance, TimeSpan.FromMinutes(2));
+ Assert.AreEqual(OrchestrationStatus.Completed, orchestrationResult.OrchestrationStatus);
+ }
+
+ [TestMethod]
+ public async Task FabricProviderClient_ReturnsRunningOrchestrations()
+ {
+ var instanceIdPrefix = nameof(FabricProviderClient_ReturnsRunningOrchestrations);
+ int numberOfInstances = 5;
+ var startTasks = new List>();
+
+ for (int i = 0; i < numberOfInstances; i++)
+ {
+ var testData = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 1,
+ MaxDelay = 5,
+ MinDelay = 5,
+ DelayUnit = TimeSpan.FromSeconds(1),
+ };
+
+ var task = this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), instanceIdPrefix + i, testData);
+
+ startTasks.Add(task);
+ }
+
+ var scheduledInstances = await Task.WhenAll(startTasks);
+ await Task.Delay(TimeSpan.FromSeconds(2));
+
+ foreach (var scheduledInstance in scheduledInstances)
+ {
+ var runtimeState = await this.taskHubClient.GetOrchestrationStateAsync(scheduledInstance.InstanceId);
+ Console.WriteLine($"Runtime Orchestration State for {scheduledInstance} : {runtimeState}");
+ }
+
+ var waitTasks = new List>();
+ foreach (var scheduledInstance in scheduledInstances)
+ {
+ var waitTask = this.taskHubClient.WaitForOrchestrationAsync(scheduledInstance, TimeSpan.FromMinutes(2));
+ waitTasks.Add(waitTask);
+ }
+
+ var results = await Task.WhenAll(waitTasks);
+ foreach(var result in results)
+ {
+ Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus);
+ }
+ }
+
+
+ [TestMethod]
+ public async Task QueryState_For_Latest_Execution()
+ {
+ var instanceId = nameof(QueryState_For_Latest_Execution);
+ var input = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 1,
+ MaxDelay = 15,
+ MinDelay = 15,
+ DelayUnit = TimeSpan.FromSeconds(1),
+ };
+
+ var instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), instanceId, input);
+
+ var state = await this.taskHubClient.GetOrchestrationStateAsync(instanceId);
+
+ Assert.IsNotNull(state);
+ Assert.AreEqual(instance.ExecutionId, state.OrchestrationInstance.ExecutionId);
+
+ var result = await this.taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(2));
+ Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus);
+
+ input = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 1,
+ MaxDelay = 0,
+ MinDelay = 0,
+ DelayUnit = TimeSpan.FromSeconds(1),
+ };
+
+ var newInstance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), instanceId, input);
+
+ // We want to make sure that once an orchestration is complete, we can create another instance with the same id.
+ var newState = await this.taskHubClient.GetOrchestrationStateAsync(instanceId);
+
+ Assert.IsNotNull(newState);
+ Assert.AreEqual(newInstance.ExecutionId, newState.OrchestrationInstance.ExecutionId);
+ Assert.AreNotEqual(state.OrchestrationInstance.ExecutionId, newState.OrchestrationInstance.ExecutionId);
+
+ result = await this.taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(2));
+ Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus);
+ }
+
+ [TestMethod]
+ public async Task Retry_OnException_Test()
+ {
+ int retryAttempts = 3;
+ var instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(OrchestrationRunningIntoRetry), retryAttempts);
+ var result = await this.taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(2));
+
+ Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus);
+ Assert.AreEqual(retryAttempts.ToString(), result.Output);
+ var orchestrationTime = result.CompletedTime - result.CreatedTime;
+ Console.WriteLine($"Time for Orchestration : {orchestrationTime}");
+ }
+
+ [TestMethod]
+ public async Task ForceTerminate_Terminates_LatestExecution()
+ {
+ var testData = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 5,
+ MaxDelay = 5,
+ MinDelay = 5,
+ DelayUnit = TimeSpan.FromSeconds(1),
+ };
+
+ var instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), testData);
+ await Task.Delay(TimeSpan.FromMilliseconds(1));
+
+ var reason = "Testing Terminate Functionality";
+ await this.taskHubClient.TerminateInstanceAsync(instance, reason);
+ var result = await this.taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(1));
+
+ Assert.AreEqual(OrchestrationStatus.Terminated, result.OrchestrationStatus);
+ Assert.AreEqual(reason, result.Output);
+ }
+
+ [TestMethod]
+ public async Task ForceTerminate_Twice_Terminates_LatestExecution()
+ {
+ var testData = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 2,
+ MaxDelay = 5,
+ MinDelay = 5,
+ DelayUnit = TimeSpan.FromSeconds(1),
+ };
+
+ var instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), testData);
+ await Task.Delay(TimeSpan.FromSeconds(1));
+
+ var reason = "Testing Terminate (Twice) Functionality";
+ var tasks = new List();
+ tasks.Add(this.taskHubClient.TerminateInstanceAsync(instance, reason));
+ tasks.Add(this.taskHubClient.TerminateInstanceAsync(instance, reason));
+ await Task.WhenAll(tasks);
+ var result = await this.taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(1));
+
+ Assert.AreEqual(OrchestrationStatus.Terminated, result.OrchestrationStatus);
+ Assert.AreEqual(reason, result.Output);
+ }
+
+ [TestMethod]
+ public async Task Purge_Removes_State()
+ {
+ var instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(SimpleOrchestrationWithTasks), null);
+ var result = await this.taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(2));
+
+ Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus);
+
+ instance = result.OrchestrationInstance;
+ await this.taskHubClient.PurgeOrchestrationInstanceHistoryAsync(DateTime.UtcNow, OrchestrationStateTimeRangeFilterType.OrchestrationCompletedTimeFilter);
+ var state = await this.taskHubClient.GetOrchestrationStateAsync(instance);
+
+ Assert.IsNull(state);
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Integration.Tests/StressTests.cs b/Test/DurableTask.AzureServiceFabric.Integration.Tests/StressTests.cs
new file mode 100644
index 000000000..1f7b3f154
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Integration.Tests/StressTests.cs
@@ -0,0 +1,258 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Integration.Tests
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.Test.Orchestrations.Performance;
+
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class StressTests
+ {
+ [TestMethod]
+ public async Task ExecuteStressTest()
+ {
+ var driverConfig = new DriverOrchestrationData()
+ {
+ NumberOfParallelOrchestrations = 40,
+ SubOrchestrationData = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 10,
+ NumberOfSerialTasks = 5,
+ MaxDelay = 5,
+ MinDelay = 0,
+ DelayUnit = TimeSpan.FromSeconds(1)
+ }
+ };
+
+ await RunDriverOrchestrationHelper(driverConfig);
+ Console.WriteLine();
+
+ driverConfig.SubOrchestrationData.UseTimeoutTask = true;
+ driverConfig.SubOrchestrationData.ExecutionTimeout = TimeSpan.FromMinutes(2);
+
+ await RunDriverOrchestrationHelper(driverConfig);
+ }
+
+ // This test uses an implementation detail of client - the polling interval in the client for WaitForOrchestration API.
+ // The sleep time is chosen carefully to be close to polling interval to simulate WaitForOrchestration succeeding while
+ // the session could still be there in the store (with a previous implementation which was fixed when the test was introduced).
+ // The test should be modified if the polling interval changes or should be re thought if the polling logic itself changes.
+ // Todo : The above logic did change since the test was initially developed. Need to re-think this test.
+ [TestMethod]
+ public async Task ExecuteSameOrchestrationBackToBack()
+ {
+ int numberOfAttempts = 10; //Try a few times to make sure there is no failure.
+ var instanceId = nameof(ExecuteSameOrchestrationBackToBack);
+ var taskHubClient = Utilities.CreateTaskHubClient();
+
+ Dictionary> results = new Dictionary>();
+
+ for (int attemptNumber = 0; attemptNumber < numberOfAttempts; attemptNumber++)
+ {
+ var instance = await taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), instanceId, new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 1,
+ MaxDelay = 1980,
+ MinDelay = 1900,
+ DelayUnit = TimeSpan.FromMilliseconds(1),
+ });
+
+ var state = await taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(1));
+ results.Add(attemptNumber, new Tuple(instance, state));
+ }
+
+ for(int attemptNumber = 0; attemptNumber < numberOfAttempts; attemptNumber++)
+ {
+ var instance = results[attemptNumber].Item1;
+ var state = results[attemptNumber].Item2;
+ Assert.IsNotNull(state, $"Attempt number {attemptNumber} with execution id {instance.ExecutionId} Failed with null value for WaitForOrchestration");
+ Assert.AreEqual(OrchestrationStatus.Completed, state.OrchestrationStatus, $"Attempt number {attemptNumber} with execution id {instance.ExecutionId} did not reach 'Completed' state.");
+ Console.WriteLine($"Time for orchestration in attempt number {attemptNumber} : {state.CompletedTime - state.CreatedTime}");
+ }
+ }
+
+ [TestMethod]
+ public async Task Test_Lot_Of_Activities_Per_Orchestration()
+ {
+ var tests = new[] { 256, 1024, 2048, 3500, 4000, 8000, 10000, 15000, 20000 };
+ foreach (var numberOfActivities in tests)
+ {
+ Console.WriteLine($"Begin testing orchestration with {numberOfActivities} parallel activities");
+ var taskHubClient = Utilities.CreateTaskHubClient();
+ var instance = await taskHubClient.CreateOrchestrationInstanceAsync(typeof(ExecutionCountingOrchestration), numberOfActivities);
+ var state = await taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(3));
+ await taskHubClient.PurgeOrchestrationInstanceHistoryAsync(DateTime.Now, OrchestrationStateTimeRangeFilterType.OrchestrationCompletedTimeFilter);
+ Assert.IsNotNull(state);
+ Assert.AreEqual(OrchestrationStatus.Completed, state.OrchestrationStatus);
+ Console.WriteLine($"Time for orchestration {state.OrchestrationInstance.InstanceId} with {numberOfActivities} parallel activities : {state.CompletedTime - state.CreatedTime}");
+ }
+ }
+
+ [TestMethod]
+ public async Task SimplePerformanceExperiment()
+ {
+ var testOrchestratorInput = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 3,
+ MaxDelay = 3,
+ MinDelay = 3,
+ DelayUnit = TimeSpan.FromSeconds(1)
+ };
+
+ await RunTestOrchestrationsHelper(100, testOrchestratorInput);
+ Console.WriteLine();
+
+ testOrchestratorInput.UseTimeoutTask = true;
+ testOrchestratorInput.ExecutionTimeout = TimeSpan.FromMinutes(2);
+
+ await RunTestOrchestrationsHelper(100, testOrchestratorInput);
+ }
+
+ [TestMethod]
+ public async Task RunThousandOrchsWithHundredMilliSecondDelayInBetweenStartingEach()
+ {
+ var testOrchestratorInput = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 16,
+ NumberOfSerialTasks = 4,
+ MaxDelay = 500,
+ MinDelay = 50,
+ DelayUnit = TimeSpan.FromMilliseconds(1)
+ };
+
+ await RunTestOrchestrationsHelper(1000, testOrchestratorInput, delayGeneratorFunction: (totalRequestsSoFar) => TimeSpan.FromMilliseconds(100));
+ Console.WriteLine();
+
+ testOrchestratorInput.UseTimeoutTask = true;
+ testOrchestratorInput.ExecutionTimeout = TimeSpan.FromMinutes(2);
+
+ await RunTestOrchestrationsHelper(1000, testOrchestratorInput, delayGeneratorFunction: (totalRequestsSoFar) => TimeSpan.FromMilliseconds(100));
+ }
+
+ [Ignore] //Long running test and not too interesting anyway. (The test is passing - enable as needed to test and ensure).
+ [TestMethod]
+ public async Task InfrequentLoadScenario()
+ {
+ var testOrchestratorInput = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 2,
+ MaxDelay = 0,
+ MinDelay = 0,
+ };
+
+ await RunTestOrchestrationsHelper(200, testOrchestratorInput, delayGeneratorFunction: (totalRequestsSoFar) => TimeSpan.FromSeconds(1));
+ Console.WriteLine();
+
+ testOrchestratorInput.UseTimeoutTask = true;
+ testOrchestratorInput.ExecutionTimeout = TimeSpan.FromMinutes(2);
+
+ await RunTestOrchestrationsHelper(200, testOrchestratorInput, delayGeneratorFunction: (totalRequestsSoFar) => TimeSpan.FromSeconds(1));
+ }
+
+ async Task RunDriverOrchestrationHelper(DriverOrchestrationData driverConfig)
+ {
+ var taskHubClient = Utilities.CreateTaskHubClient();
+ Console.WriteLine($"Orchestration getting scheduled: {DateTime.Now}");
+
+ Stopwatch stopWatch = Stopwatch.StartNew();
+ var instance = await taskHubClient.CreateOrchestrationInstanceAsync(typeof(DriverOrchestration), driverConfig);
+ var state = await taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(5));
+ stopWatch.Stop();
+
+ Console.WriteLine($"Orchestration Status: {state.OrchestrationStatus}");
+ Console.WriteLine($"Orchestration Result: {state.Output}");
+
+ TimeSpan totalTime = stopWatch.Elapsed;
+ TimeSpan orchestrationTime = state.CompletedTime - state.CreatedTime;
+
+ Func elapsedTimeFormatter = timeSpan => $"{timeSpan.Hours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}.{timeSpan.Milliseconds / 10:00}";
+ Console.WriteLine($"Total Meastured Time: {elapsedTimeFormatter(totalTime)}");
+ Console.WriteLine($"Top level Orchestration Time: {elapsedTimeFormatter(orchestrationTime)}");
+
+ var expectedResult = driverConfig.NumberOfParallelOrchestrations * (driverConfig.SubOrchestrationData.NumberOfParallelTasks + driverConfig.SubOrchestrationData.NumberOfSerialTasks);
+
+ Assert.AreEqual(expectedResult.ToString(), state.Output);
+ }
+
+ async Task RunTestOrchestrationsHelper(int numberOfInstances, TestOrchestrationData orchestrationInput, Func delayGeneratorFunction = null)
+ {
+ var taskHubClient = Utilities.CreateTaskHubClient();
+
+ List> results = new List>();
+ List waitTasks = new List();
+ Stopwatch stopWatch = Stopwatch.StartNew();
+ for (int i = 0; i < numberOfInstances; i++)
+ {
+ var instance = await taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), orchestrationInput);
+ waitTasks.Add(Task.Run(async () =>
+ {
+ var state = await taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(2));
+ results.Add(Tuple.Create(instance, state));
+ }));
+ if (delayGeneratorFunction != null)
+ {
+ await Task.Delay(delayGeneratorFunction(i));
+ }
+ }
+
+ await Task.WhenAll(waitTasks);
+ stopWatch.Stop();
+
+ Func elapsedTimeFormatter = timeSpan => $"{timeSpan.Hours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}.{timeSpan.Milliseconds / 10:00}";
+ Console.WriteLine($"Total Meastured Time For All Orchestrations: {elapsedTimeFormatter(stopWatch.Elapsed)}");
+
+ var expectedResult = (orchestrationInput.NumberOfParallelTasks + orchestrationInput.NumberOfSerialTasks).ToString();
+ TimeSpan minTime = TimeSpan.MaxValue;
+ TimeSpan maxTime = TimeSpan.FromMilliseconds(0);
+
+ int failedOrchestrations = 0;
+ foreach (var kvp in results)
+ {
+ if (kvp.Item2 == null)
+ {
+ failedOrchestrations++;
+ var state = await taskHubClient.GetOrchestrationStateAsync(kvp.Item1);
+ Console.WriteLine($"Unfinished orchestration {kvp.Item1}, state : {kvp.Item2?.OrchestrationStatus}");
+ continue;
+ }
+
+ Assert.AreEqual(expectedResult, kvp.Item2.Output, $"Unexpected output for Orchestration : {kvp.Item1.InstanceId}");
+ TimeSpan orchestrationTime = kvp.Item2.CompletedTime - kvp.Item2.CreatedTime;
+ if (minTime > orchestrationTime)
+ {
+ minTime = orchestrationTime;
+ }
+ if (maxTime < orchestrationTime)
+ {
+ maxTime = orchestrationTime;
+ }
+ }
+
+ Console.WriteLine($"Minimum time taken for any orchestration instance : {elapsedTimeFormatter(minTime)}");
+ Console.WriteLine($"Maximum time taken for any orchestration instance : {elapsedTimeFormatter(maxTime)}");
+
+ Assert.AreEqual(0, failedOrchestrations, $"{failedOrchestrations} orchestrations failed out of {numberOfInstances}.");
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Integration.Tests/Utilities.cs b/Test/DurableTask.AzureServiceFabric.Integration.Tests/Utilities.cs
new file mode 100644
index 000000000..4ab7d09f3
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Integration.Tests/Utilities.cs
@@ -0,0 +1,71 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Integration.Tests
+{
+ using System;
+ using System.Diagnostics;
+ using System.Net.Http;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.AzureServiceFabric.Remote;
+ using DurableTask.AzureServiceFabric.Service;
+
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ public static class Utilities
+ {
+ public static TaskHubClient CreateTaskHubClient()
+ {
+ var partitionProvider = new FabricPartitionEndpointResolver(new Uri(Constants.TestFabricApplicationAddress), new DefaultStringPartitionHashing());
+ var httpClient = new HttpClient();
+ return new TaskHubClient(new RemoteOrchestrationServiceClient(partitionProvider));
+ }
+
+ public static async Task ThrowsException(Func action, string expectedMessage) where TException : Exception
+ {
+ try
+ {
+ await action();
+ Assert.Fail($"Method {action.Method} did not throw the expected exception of type {typeof(TException).Name}");
+ }
+ catch (Exception ex)
+ {
+ AggregateException aggregate = ex as AggregateException;
+ if (aggregate != null)
+ {
+ ex = aggregate.InnerException;
+ }
+
+ TException expected = ex as TException;
+ if (expected == null)
+ {
+ Assert.Fail($"Method {action.Method} is expected to throw exception of type {typeof(TException).Name} but has thrown {ex.GetType().Name} instead.");
+ }
+ else if (!string.Equals(expected.Message, expectedMessage, StringComparison.Ordinal))
+ {
+ Assert.Fail($"Method {action.Method} is expected to throw exception with message '{expectedMessage}' but has thrown the message '{expected.Message}' instead.");
+ }
+ }
+ }
+
+ public static async Task MeasureAsync(Func asyncAction)
+ {
+ var timer = Stopwatch.StartNew();
+ await asyncAction();
+ timer.Stop();
+ return timer.Elapsed;
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Integration.Tests/WaitForOrchestrationTests.cs b/Test/DurableTask.AzureServiceFabric.Integration.Tests/WaitForOrchestrationTests.cs
new file mode 100644
index 000000000..0d0eee392
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Integration.Tests/WaitForOrchestrationTests.cs
@@ -0,0 +1,150 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Integration.Tests
+{
+ using System;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.Test.Orchestrations.Performance;
+
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class WaitForOrchestrationTests
+ {
+ TaskHubClient taskHubClient;
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ this.taskHubClient = Utilities.CreateTaskHubClient();
+ }
+
+ [TestMethod]
+ public async Task WaitForOrchestration_Returns_Null_When_Orchestration_Not_Started()
+ {
+ var instance = new OrchestrationInstance() { InstanceId = "NonExisting", ExecutionId = "NonExisting" };
+ OrchestrationState result = null;
+ var waitTime = TimeSpan.FromMinutes(1);
+ var time = await Utilities.MeasureAsync(async () =>
+ {
+ result = await this.taskHubClient.WaitForOrchestrationAsync(instance, waitTime);
+ });
+
+ Assert.IsNull(result);
+ Assert.IsTrue(time < waitTime + waitTime);
+ Console.WriteLine($"Full WaitTime : {waitTime}, Actual time taken for Wait {time}");
+ }
+
+ [TestMethod]
+ public async Task WaitForOrchestration_Returns_State_When_Orchestration_Already_Finished()
+ {
+ var input = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 1,
+ MaxDelay = 0,
+ MinDelay = 0,
+ DelayUnit = TimeSpan.FromMilliseconds(1),
+ UseTimeoutTask = false,
+ };
+ var instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), input);
+ await this.taskHubClient.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(1)); //Wait till orchestration is finished
+
+ var waitTime = TimeSpan.FromMilliseconds(100); //Let this be very low for the test to be effective.
+ OrchestrationState result = null;
+ var time = await Utilities.MeasureAsync(async () =>
+ {
+ result = await this.taskHubClient.WaitForOrchestrationAsync(instance, waitTime);
+ });
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus);
+ Assert.IsTrue(time < waitTime);
+ Console.WriteLine($"Full WaitTime : {waitTime}, Actual time taken for Wait {time}, Actual Time taken for Orchestration : {result.CompletedTime - result.CreatedTime}");
+ }
+
+ [TestMethod]
+ public async Task WaitForOrchestration_Waits_And_Returns_State_When_Orchestration_IsRunning()
+ {
+ var waitTime = TimeSpan.FromMinutes(1);
+ var orchestrationTimeInSeconds = 5;
+ var input = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 1,
+ MaxDelay = orchestrationTimeInSeconds,
+ MinDelay = orchestrationTimeInSeconds,
+ DelayUnit = TimeSpan.FromSeconds(1),
+ UseTimeoutTask = false,
+ };
+
+ var instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), input);
+
+ OrchestrationState result = null;
+ var time = await Utilities.MeasureAsync(async () =>
+ {
+ result = await this.taskHubClient.WaitForOrchestrationAsync(instance, waitTime);
+ });
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus);
+ Assert.IsTrue(time < waitTime);
+ Assert.IsTrue(time > TimeSpan.FromSeconds(orchestrationTimeInSeconds));
+ Console.WriteLine($"Full WaitTime : {waitTime}, Actual time taken for Wait : {time}, Expected Minimum time for orchestration : {orchestrationTimeInSeconds} seconds, Actual Time taken for Orchestration : {result.CompletedTime - result.CreatedTime}");
+ }
+
+ [TestMethod]
+ public async Task WaitForOrchestration_TimesOut_And_Returns_Null_When_Orchestration_DidNotFinishYet()
+ {
+ var initialwaitTimeInSeconds = 2;
+ var orchestrationTimeInSeconds = 5;
+ var waitTime = TimeSpan.FromSeconds(initialwaitTimeInSeconds);
+ var input = new TestOrchestrationData()
+ {
+ NumberOfParallelTasks = 0,
+ NumberOfSerialTasks = 1,
+ MaxDelay = orchestrationTimeInSeconds,
+ MinDelay = orchestrationTimeInSeconds,
+ DelayUnit = TimeSpan.FromSeconds(1),
+ UseTimeoutTask = false,
+ };
+
+ var instance = await this.taskHubClient.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), input);
+
+ OrchestrationState result = null;
+ var time = await Utilities.MeasureAsync(async () =>
+ {
+ result = await this.taskHubClient.WaitForOrchestrationAsync(instance, waitTime);
+ });
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(result.OrchestrationStatus, OrchestrationStatus.Running);
+ Assert.IsTrue(time > waitTime);
+ Console.WriteLine($"Full WaitTime : {waitTime}, Actual time taken for Wait : {time}, Expected Orchestration Running Time : {orchestrationTimeInSeconds} seconds");
+
+ var newWaitTime = TimeSpan.FromMinutes(1);
+ time = await Utilities.MeasureAsync(async () =>
+ {
+ result = await this.taskHubClient.WaitForOrchestrationAsync(instance, newWaitTime);
+ });
+
+ Assert.IsNotNull(result);
+ Assert.IsTrue(time < newWaitTime);
+ Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus);
+ Console.WriteLine($"Full WaitTime : {newWaitTime}, Actual time taken for Wait : {time}, Time taken for Orchestration : {result.CompletedTime - result.CreatedTime}");
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Tests/CountBasedFixedDelayRetryPolicyTests.cs b/Test/DurableTask.AzureServiceFabric.Tests/CountBasedFixedDelayRetryPolicyTests.cs
new file mode 100644
index 000000000..a03a3bcbc
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Tests/CountBasedFixedDelayRetryPolicyTests.cs
@@ -0,0 +1,67 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Tests
+{
+ using System;
+
+ using DurableTask.AzureServiceFabric.TaskHelpers;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class CountBasedFixedDelayRetryPolicyTests
+ {
+ [TestMethod]
+ public void CountBasedFixedDelayRetry_Attempts_MaxCount_Times()
+ {
+ var testRetryPolicy = new CountBasedFixedDelayRetryPolicy(3, TimeSpan.Zero);
+ int attempts = 0;
+ while (testRetryPolicy.ShouldExecute())
+ {
+ attempts++;
+ }
+ Assert.AreEqual(3, attempts);
+ }
+
+ [TestMethod]
+ public void CountBasedFixedDelayRetry_Last_Retry_Delay_Is_Zero()
+ {
+ var testRetryPolicy = new CountBasedFixedDelayRetryPolicy(1, TimeSpan.FromSeconds(1));
+ TimeSpan lastDelay = TimeSpan.MaxValue;
+ while (testRetryPolicy.ShouldExecute())
+ {
+ lastDelay = testRetryPolicy.GetNextDelay();
+ }
+ Assert.AreEqual(TimeSpan.Zero, lastDelay);
+ }
+
+ [TestMethod]
+ public void CountBasedFixedDelayRetry_Non_Last_Retry_Delay_Is_Fixed()
+ {
+ TimeSpan fixedDelay = TimeSpan.FromSeconds(23);
+ var testRetryPolicy = new CountBasedFixedDelayRetryPolicy(3, fixedDelay);
+ int attemptNumber = 1;
+ TimeSpan nextDelay = TimeSpan.MaxValue;
+ while (testRetryPolicy.ShouldExecute())
+ {
+ nextDelay = testRetryPolicy.GetNextDelay();
+ if (attemptNumber < 3)
+ {
+ Assert.AreEqual(fixedDelay, nextDelay);
+ }
+ attemptNumber++;
+ }
+ Assert.AreEqual(TimeSpan.Zero, nextDelay);
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Tests/DurableTask.AzureServiceFabric.Tests.csproj b/Test/DurableTask.AzureServiceFabric.Tests/DurableTask.AzureServiceFabric.Tests.csproj
new file mode 100644
index 000000000..2f45a1744
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Tests/DurableTask.AzureServiceFabric.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+
+ net461
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Test/DurableTask.AzureServiceFabric.Tests/Measure.cs b/Test/DurableTask.AzureServiceFabric.Tests/Measure.cs
new file mode 100644
index 000000000..3fb445f5d
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Tests/Measure.cs
@@ -0,0 +1,53 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Tests
+{
+ using System;
+ using System.Diagnostics;
+ using System.IO;
+ using System.Runtime.Serialization;
+
+ class Measure
+ {
+ public static T DataContractSerialization(T testObject)
+ {
+ return (T)DataContractSerialization(typeof(T), testObject);
+ }
+
+ public static object DataContractSerialization(Type testType, object testObject)
+ {
+ using (var stream = new MemoryStream())
+ {
+ var serializer = new DataContractSerializer(testType);
+ var time = MeasureTime(() => serializer.WriteObject(stream, testObject));
+ Console.WriteLine($"Time for serialization : {time.TotalMilliseconds} ms");
+ Console.WriteLine($"Size of serialized stream : {stream.Length} bytes.");
+
+ stream.Position = 0;
+ object deserialized = null;
+ time = MeasureTime(() => { deserialized = serializer.ReadObject(stream); });
+ Console.WriteLine($"Time for deserialization : {time.TotalMilliseconds} ms");
+ return deserialized;
+ }
+ }
+
+ public static TimeSpan MeasureTime(Action action)
+ {
+ Stopwatch timer = Stopwatch.StartNew();
+ action();
+ timer.Stop();
+ return timer.Elapsed;
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Tests/OrchestrationInstanceComparerTests.cs b/Test/DurableTask.AzureServiceFabric.Tests/OrchestrationInstanceComparerTests.cs
new file mode 100644
index 000000000..2b384275a
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Tests/OrchestrationInstanceComparerTests.cs
@@ -0,0 +1,86 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Tests
+{
+ using DurableTask.Core;
+
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class OrchestrationInstanceComparerTests
+ {
+ [TestMethod]
+ public void OrchestrationInstanceComparer_NullInstanceTests()
+ {
+ Assert.IsTrue(OrchestrationInstanceComparer.Default.Equals(null, null));
+ Assert.IsFalse(OrchestrationInstanceComparer.Default.Equals(null, new OrchestrationInstance()));
+ Assert.IsFalse(OrchestrationInstanceComparer.Default.Equals(new OrchestrationInstance(), null));
+ }
+
+ [TestMethod]
+ public void OrchestrationInstanceComparer_SameOrchestrationInstances()
+ {
+ var first = new OrchestrationInstance()
+ {
+ InstanceId = "InstanceId",
+ ExecutionId = "ExecutionId"
+ };
+
+ var second = new OrchestrationInstance()
+ {
+ InstanceId = "InstanceId",
+ ExecutionId = "ExecutionId"
+ };
+
+ Assert.IsTrue(OrchestrationInstanceComparer.Default.Equals(first, first));
+ Assert.IsTrue(OrchestrationInstanceComparer.Default.Equals(first, second));
+ }
+
+ [TestMethod]
+ public void OrchestrationInstanceComparer_DifferentExecutionIdInstances()
+ {
+ var first = new OrchestrationInstance()
+ {
+ InstanceId = "InstanceId",
+ ExecutionId = "ExecutionId1"
+ };
+
+ var second = new OrchestrationInstance()
+ {
+ InstanceId = "InstanceId",
+ ExecutionId = "ExecutionId2"
+ };
+
+ Assert.IsFalse(OrchestrationInstanceComparer.Default.Equals(first, second));
+ }
+
+ [TestMethod]
+ public void OrchestrationInstanceComparer_DifferentOrchestrationInstances()
+ {
+ var first = new OrchestrationInstance()
+ {
+ InstanceId = "InstanceId1",
+ ExecutionId = "ExecutionId1"
+ };
+
+ var second = new OrchestrationInstance()
+ {
+ InstanceId = "InstanceId2",
+ ExecutionId = "ExecutionId2"
+ };
+
+ Assert.IsFalse(OrchestrationInstanceComparer.Default.Equals(first, second));
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Tests/PersistentSessionTests.cs b/Test/DurableTask.AzureServiceFabric.Tests/PersistentSessionTests.cs
new file mode 100644
index 000000000..9a9febe35
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Tests/PersistentSessionTests.cs
@@ -0,0 +1,58 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Tests
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Immutable;
+
+ using DurableTask.Core;
+ using DurableTask.Core.History;
+
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class PersistentSessionTests
+ {
+ [TestMethod]
+ public void PersistentSession_SerializationTest()
+ {
+ int numberOfHistoryEvents = 256;
+
+ var events = new List();
+ events.Add(new ExecutionStartedEvent(-1, "TestInput"));
+ for (int i = 0; i < numberOfHistoryEvents; i++)
+ {
+ events.Add(new TaskScheduledEvent(-1));
+ events.Add(new TaskCompletedEvent(-1, -1, $"Task {i} Result"));
+ }
+ events.Add(new ExecutionCompletedEvent(-1, "FinalResult", OrchestrationStatus.Completed));
+
+ var instance = new OrchestrationInstance()
+ {
+ InstanceId = "testSession",
+ ExecutionId = Guid.NewGuid().ToString("N")
+ };
+
+ PersistentSession testSession = PersistentSession.Create(instance, events.ToImmutableList());
+
+ var actual = Measure.DataContractSerialization(testSession);
+
+ Assert.IsNotNull(actual);
+ Assert.AreEqual(instance.InstanceId, actual.SessionId.InstanceId);
+ Assert.AreEqual(instance.ExecutionId, actual.SessionId.ExecutionId);
+ Assert.AreEqual(numberOfHistoryEvents * 2 + 2, actual.SessionState.Count);
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Tests/SerializationPerformanceExperiments.cs b/Test/DurableTask.AzureServiceFabric.Tests/SerializationPerformanceExperiments.cs
new file mode 100644
index 000000000..e06be1417
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Tests/SerializationPerformanceExperiments.cs
@@ -0,0 +1,65 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Tests
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Runtime.Serialization;
+
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class SerializationPerformanceExperiments
+ {
+ [TestMethod]
+ public void DataContractSerializationSizeExperiment_PrimitiveTypes()
+ {
+ var testObjects = new List()
+ {
+ Int32.MinValue,
+ Int32.MaxValue,
+ "01234567890123456789012345678901",
+ Guid.NewGuid(),
+ DateTime.UtcNow,
+ Int64.MaxValue,
+ Int64.MinValue,
+ Int16.MaxValue,
+ Int16.MinValue,
+ new ComplexType() { LongProp = 33, StringProp = "0123456789" }
+ };
+
+ foreach (var testObject in testObjects)
+ {
+ var type = testObject.GetType();
+ Console.WriteLine($"Type = {type.Name}");
+ Console.WriteLine($"TestObject = {testObject}");
+ Measure.DataContractSerialization(type, testObject);
+ Console.WriteLine();
+ }
+ }
+
+ [DataContract]
+ class ComplexType
+ {
+ [DataMember] public long LongProp;
+
+ [DataMember] public string StringProp;
+
+ public override string ToString()
+ {
+ return $"'LongProp = {LongProp}, StringProp = {StringProp}'";
+ }
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Tests/TaskMessageItemTests.cs b/Test/DurableTask.AzureServiceFabric.Tests/TaskMessageItemTests.cs
new file mode 100644
index 000000000..5cd67dcd3
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Tests/TaskMessageItemTests.cs
@@ -0,0 +1,43 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Tests
+{
+ using DurableTask.Core;
+ using DurableTask.Core.History;
+
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class TaskMessageItemTests
+ {
+ [TestMethod]
+ public void TaskMessageItem_SerializationTests()
+ {
+ var expected = new TaskMessageItem(new TaskMessage()
+ {
+ OrchestrationInstance = new OrchestrationInstance() { InstanceId = "InstanceId", ExecutionId = "ExecutionId" },
+ SequenceNumber = 33,
+ Event = new TaskScheduledEvent(-1)
+ });
+
+ var actual = Measure.DataContractSerialization(expected);
+
+ Assert.IsNotNull(actual);
+ Assert.AreEqual(expected.TaskMessage.OrchestrationInstance.InstanceId, actual.TaskMessage.OrchestrationInstance.InstanceId);
+ Assert.AreEqual(expected.TaskMessage.OrchestrationInstance.ExecutionId, actual.TaskMessage.OrchestrationInstance.ExecutionId);
+ Assert.AreEqual(expected.TaskMessage.SequenceNumber, actual.TaskMessage.SequenceNumber);
+ Assert.AreEqual(expected.TaskMessage.Event.EventId, actual.TaskMessage.Event.EventId);
+ }
+ }
+}
diff --git a/Test/DurableTask.AzureServiceFabric.Tests/TimerFiredEventComparerTests.cs b/Test/DurableTask.AzureServiceFabric.Tests/TimerFiredEventComparerTests.cs
new file mode 100644
index 000000000..dd1ff9e77
--- /dev/null
+++ b/Test/DurableTask.AzureServiceFabric.Tests/TimerFiredEventComparerTests.cs
@@ -0,0 +1,147 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Tests
+{
+ using System;
+ using System.Collections.Immutable;
+ using System.Linq;
+
+ using DurableTask.Core;
+ using DurableTask.Core.History;
+
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class TimerFiredEventComparerTests
+ {
+ [TestMethod]
+ public void Messages_With_Equal_FiredAt_Value_With_Different_Key_Are_Treated_Separate_And_Sorted_On_Key()
+ {
+ ImmutableSortedSet> timerMessages = ImmutableSortedSet>.Empty.WithComparer(TimerFiredEventComparer.Instance);
+
+ var instance = new OrchestrationInstance()
+ {
+ InstanceId = "InstanceId",
+ };
+
+ var currentTime = DateTime.UtcNow;
+ var builder = timerMessages.ToBuilder();
+ int numberOfMessages = 5;
+
+ var guids = Enumerable.Range(0, numberOfMessages).Select(x => Guid.NewGuid()).ToList();
+
+ // Add them in reverse order to expected order just to make the test case more interesting.
+ for (int i = numberOfMessages -1 ; i >= 0; i--)
+ {
+ builder.Add(new Message(guids[i], new TaskMessageItem(new TaskMessage()
+ {
+ SequenceNumber = i,
+ OrchestrationInstance = instance,
+ Event = new TimerFiredEvent(i) { FireAt = currentTime }
+ })));
+ }
+
+ Assert.AreEqual(numberOfMessages, builder.Count);
+ timerMessages = builder.ToImmutableSortedSet(TimerFiredEventComparer.Instance);
+ Assert.AreEqual(numberOfMessages, timerMessages.Count);
+
+ guids = guids.OrderBy(x => x).ToList();
+
+ // Now enumerating the items should result in right order.
+ for (int i = 0; i < numberOfMessages; i++)
+ {
+ var min = timerMessages.Min;
+ timerMessages = timerMessages.Remove(min);
+ Assert.AreEqual(guids[i], min.Key, "Ordering seems to be broken");
+ }
+
+ Assert.AreEqual(0, timerMessages.Count);
+ }
+
+ [TestMethod]
+ public void Messages_With_Different_FiredAt_Value_Are_Sorted_Based_On_FiredAt()
+ {
+ ImmutableSortedSet> timerMessages = ImmutableSortedSet>.Empty.WithComparer(TimerFiredEventComparer.Instance);
+
+ var instance = new OrchestrationInstance()
+ {
+ InstanceId = "InstanceId",
+ };
+
+ var currentTime = DateTime.UtcNow;
+ var builder = timerMessages.ToBuilder();
+ int numberOfMessages = 5;
+
+ var guids = Enumerable.Range(0, numberOfMessages).Select(x => Guid.NewGuid()).ToList();
+
+ // Add them in reverse order to expected order just to make the test case more interesting.
+ for (int i = numberOfMessages -1 ; i >= 0; i--)
+ {
+ builder.Add(new Message(guids[i], new TaskMessageItem(new TaskMessage() //Let the key values be in reverse order to fired at values again to make test more interesting.
+ {
+ SequenceNumber = i,
+ OrchestrationInstance = instance,
+ Event = new TimerFiredEvent(i) { FireAt = currentTime + TimeSpan.FromSeconds(i) }
+ })));
+ }
+
+ Assert.AreEqual(numberOfMessages, builder.Count);
+ timerMessages = builder.ToImmutableSortedSet(TimerFiredEventComparer.Instance);
+ Assert.AreEqual(numberOfMessages, timerMessages.Count);
+
+ // Now enumerating the items should result in right order.
+ for (int i = 0; i < numberOfMessages; i++)
+ {
+ var min = timerMessages.Min;
+ timerMessages = timerMessages.Remove(min);
+ var firedAt = (min.Value.TaskMessage.Event as TimerFiredEvent)?.FireAt;
+ Assert.AreEqual(currentTime + TimeSpan.FromSeconds(i), firedAt, "Ordering seems to be broken");
+ Assert.AreEqual(guids[i], min.Key);
+ }
+
+ Assert.AreEqual(0, timerMessages.Count);
+ }
+
+ [TestMethod]
+ public void Messages_With_Equal_FiredAt_Value_With_Same_Key_Are_Treated_Equal()
+ {
+ ImmutableSortedSet> timerMessages = ImmutableSortedSet>.Empty.WithComparer(TimerFiredEventComparer.Instance);
+
+ var instance = new OrchestrationInstance()
+ {
+ InstanceId = "InstanceId",
+ };
+
+ var currentTime = DateTime.UtcNow;
+ var builder = timerMessages.ToBuilder();
+ int numberOfMessages = 3;
+
+ // Add them in reverse order to expected order just to make the test case more interesting.
+ var guid = Guid.NewGuid();
+ for (int i = numberOfMessages; i >= 1; i--)
+ {
+ builder.Add(new Message(guid, new TaskMessageItem(new TaskMessage()
+ {
+ SequenceNumber = i,
+ OrchestrationInstance = instance,
+ Event = new TimerFiredEvent(i) { FireAt = currentTime }
+ })));
+ }
+
+ Assert.AreEqual(1, builder.Count);
+ timerMessages = builder.ToImmutableSortedSet(TimerFiredEventComparer.Instance);
+ Assert.AreEqual(1, timerMessages.Count);
+ }
+ }
+}
diff --git a/Test/DurableTask.Test.Orchestrations/Performance/DriverOrchestration.cs b/Test/DurableTask.Test.Orchestrations/Performance/DriverOrchestration.cs
new file mode 100644
index 000000000..362586529
--- /dev/null
+++ b/Test/DurableTask.Test.Orchestrations/Performance/DriverOrchestration.cs
@@ -0,0 +1,39 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.Test.Orchestrations.Performance
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading.Tasks;
+ using DurableTask.Core;
+
+ public class DriverOrchestration : TaskOrchestration
+ {
+ public override async Task RunTask(OrchestrationContext context, DriverOrchestrationData data)
+ {
+ int result = 0;
+ List> results = new List>();
+ int i = 0;
+ for (; i < data.NumberOfParallelOrchestrations; i++)
+ {
+ results.Add(context.CreateSubOrchestrationInstance(typeof(TestOrchestration), data.SubOrchestrationData));
+ }
+
+ int[] counters = await Task.WhenAll(results.ToArray());
+ result = counters.Sum();
+
+ return result;
+ }
+ }
+}
diff --git a/Test/DurableTask.Test.Orchestrations/Performance/DriverOrchestrationData.cs b/Test/DurableTask.Test.Orchestrations/Performance/DriverOrchestrationData.cs
new file mode 100644
index 000000000..3d1e36cef
--- /dev/null
+++ b/Test/DurableTask.Test.Orchestrations/Performance/DriverOrchestrationData.cs
@@ -0,0 +1,28 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.Test.Orchestrations.Performance
+{
+ using System.Runtime.Serialization;
+
+ [DataContract]
+ [KnownType(typeof(DriverOrchestrationData))]
+ public class DriverOrchestrationData
+ {
+ [DataMember]
+ public int NumberOfParallelOrchestrations { get; set; }
+
+ [DataMember]
+ public TestOrchestrationData SubOrchestrationData { get; set; }
+ }
+}
diff --git a/Test/DurableTask.Test.Orchestrations/Performance/ExecutionCountingActivity.cs b/Test/DurableTask.Test.Orchestrations/Performance/ExecutionCountingActivity.cs
new file mode 100644
index 000000000..8dfc5c5a9
--- /dev/null
+++ b/Test/DurableTask.Test.Orchestrations/Performance/ExecutionCountingActivity.cs
@@ -0,0 +1,35 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.Test.Orchestrations.Performance
+{
+ using System;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using DurableTask.Core;
+
+ public sealed class ExecutionCountingActivity : AsyncTaskActivity
+ {
+ // This has to be reset before starting the orchestration and the orchestration
+ // must be run in sequence for this to determine the correct number of times all
+ // the activities together executed.
+ public static int Counter = 0;
+
+ protected override async Task ExecuteAsync(TaskContext context, int taskId)
+ {
+ await Task.Delay(new Random().Next(50, 100));
+ Interlocked.Increment(ref Counter);
+ return Counter;
+ }
+ }
+}
diff --git a/Test/DurableTask.Test.Orchestrations/Performance/ExecutionCountingOrchestration.cs b/Test/DurableTask.Test.Orchestrations/Performance/ExecutionCountingOrchestration.cs
new file mode 100644
index 000000000..e43f839a8
--- /dev/null
+++ b/Test/DurableTask.Test.Orchestrations/Performance/ExecutionCountingOrchestration.cs
@@ -0,0 +1,34 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.Test.Orchestrations.Performance
+{
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+ using DurableTask.Core;
+
+ public sealed class ExecutionCountingOrchestration : TaskOrchestration
+ {
+ public override async Task RunTask(OrchestrationContext context, int numberOfActivities)
+ {
+ List> results = new List>();
+ for (int i = 0; i < numberOfActivities; i++)
+ {
+ results.Add(context.ScheduleTask(typeof(ExecutionCountingActivity), i));
+ }
+
+ await Task.WhenAll(results);
+ return ExecutionCountingActivity.Counter;
+ }
+ }
+}
diff --git a/Test/DurableTask.Test.Orchestrations/Performance/RandomTimeWaitingTask.cs b/Test/DurableTask.Test.Orchestrations/Performance/RandomTimeWaitingTask.cs
new file mode 100644
index 000000000..e2f0fb264
--- /dev/null
+++ b/Test/DurableTask.Test.Orchestrations/Performance/RandomTimeWaitingTask.cs
@@ -0,0 +1,47 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.Test.Orchestrations.Performance
+{
+ using System;
+ using System.Threading.Tasks;
+ using DurableTask.Core;
+
+ public sealed class RandomTimeWaitingTask : AsyncTaskActivity
+ {
+ protected override async Task ExecuteAsync(TaskContext context, RandomTimeWaitingTaskInput input)
+ {
+ int delayTime;
+
+ if (input.MaxDelay == input.MinDelay)
+ {
+ delayTime = input.MaxDelay;
+ }
+ else
+ {
+ Random random = new Random();
+ delayTime = random.Next(input.MinDelay, input.MaxDelay);
+ }
+
+ // Uncomment this block to force some failures.
+ //if (new Random().NextDouble() < 0.1)
+ //{
+ // throw new Exception();
+ //}
+
+ await Task.Delay(TimeSpan.FromMilliseconds(input.DelayUnit.TotalMilliseconds * delayTime));
+
+ return 1;
+ }
+ }
+}
diff --git a/Test/DurableTask.Test.Orchestrations/Performance/RandomTimeWaitingTaskInput.cs b/Test/DurableTask.Test.Orchestrations/Performance/RandomTimeWaitingTaskInput.cs
new file mode 100644
index 000000000..b5bdef6c6
--- /dev/null
+++ b/Test/DurableTask.Test.Orchestrations/Performance/RandomTimeWaitingTaskInput.cs
@@ -0,0 +1,28 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.Test.Orchestrations.Performance
+{
+ using System;
+
+ public class RandomTimeWaitingTaskInput
+ {
+ public string TaskId { get; set; }
+
+ public int MaxDelay { get; set; }
+
+ public int MinDelay { get; set; }
+
+ public TimeSpan DelayUnit { get; set; }
+ }
+}
diff --git a/Test/DurableTask.Test.Orchestrations/Performance/TestOrchestration.cs b/Test/DurableTask.Test.Orchestrations/Performance/TestOrchestration.cs
new file mode 100644
index 000000000..436f89f4f
--- /dev/null
+++ b/Test/DurableTask.Test.Orchestrations/Performance/TestOrchestration.cs
@@ -0,0 +1,93 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.Test.Orchestrations.Performance
+{
+ using DurableTask.Core;
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ public class TestOrchestration : TaskOrchestration
+ {
+ public override async Task RunTask(OrchestrationContext context, TestOrchestrationData data)
+ {
+ if (data.UseTimeoutTask)
+ {
+ return await RunTaskWithTimeout(context, data);
+ }
+ else
+ {
+ return await RunTaskCore(context, data);
+ }
+ }
+
+ async Task RunTaskCore(OrchestrationContext context, TestOrchestrationData data)
+ {
+ int result = 0;
+ List> results = new List>();
+ int i = 0;
+ int j = 0;
+ for (; i < data.NumberOfParallelTasks; i++)
+ {
+ results.Add(context.ScheduleTask(
+ typeof(RandomTimeWaitingTask),
+ new RandomTimeWaitingTaskInput
+ {
+ TaskId = "ParallelTask: " + i.ToString(),
+ MaxDelay = data.MaxDelay,
+ MinDelay = data.MinDelay,
+ DelayUnit = data.DelayUnit
+ }));
+ }
+
+ int[] counters = await Task.WhenAll(results.ToArray());
+ result = counters.Sum();
+
+ for (; j < data.NumberOfSerialTasks; j++)
+ {
+ int c = await context.ScheduleTask(typeof(RandomTimeWaitingTask),
+ new RandomTimeWaitingTaskInput
+ {
+ TaskId = "SerialTask" + (i + j),
+ MaxDelay = data.MaxDelay,
+ MinDelay = data.MinDelay,
+ DelayUnit = data.DelayUnit
+ });
+ result += c;
+ }
+
+ return result;
+ }
+
+ async Task RunTaskWithTimeout(OrchestrationContext context, TestOrchestrationData data)
+ {
+ var timerCts = new CancellationTokenSource();
+
+ var timer = context.CreateTimer(context.CurrentUtcDateTime.Add(data.ExecutionTimeout), -1, timerCts.Token);
+ var mainTask = RunTaskCore(context, data);
+
+ var resultTask = await Task.WhenAny(mainTask, timer);
+
+ if (resultTask == timer)
+ {
+ throw new TimeoutException("Orchestration timed out");
+ }
+
+ timerCts.Cancel();
+ return mainTask.Result;
+ }
+ }
+}
diff --git a/Test/DurableTask.Test.Orchestrations/Performance/TestOrchestrationData.cs b/Test/DurableTask.Test.Orchestrations/Performance/TestOrchestrationData.cs
new file mode 100644
index 000000000..4dc829528
--- /dev/null
+++ b/Test/DurableTask.Test.Orchestrations/Performance/TestOrchestrationData.cs
@@ -0,0 +1,44 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.Test.Orchestrations.Performance
+{
+ using System;
+ using System.Runtime.Serialization;
+
+ [DataContract]
+ [KnownType(typeof(TestOrchestrationData))]
+ public class TestOrchestrationData
+ {
+ [DataMember]
+ public int NumberOfParallelTasks { get; set; }
+
+ [DataMember]
+ public int NumberOfSerialTasks { get; set; }
+
+ [DataMember]
+ public int MaxDelay { get; set; }
+
+ [DataMember]
+ public int MinDelay { get; set; }
+
+ [DataMember]
+ public TimeSpan DelayUnit { get; set; }
+
+ [DataMember]
+ public bool UseTimeoutTask { get; set; }
+
+ [DataMember]
+ public TimeSpan ExecutionTimeout { get; set; }
+ }
+}
diff --git a/Test/TestFabricApplication/TestApplication.Common/Orchestrations/CounterException.cs b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/CounterException.cs
new file mode 100644
index 000000000..4472448a6
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/CounterException.cs
@@ -0,0 +1,42 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace TestApplication.Common.Orchestrations
+{
+ using System;
+ using System.Runtime.Serialization;
+
+ [Serializable]
+ public class CounterException : Exception
+ {
+ readonly string CounterPropName = "Counter";
+
+ public CounterException(int counter)
+ {
+ this.Counter = counter;
+ }
+
+ protected CounterException(SerializationInfo info, StreamingContext context)
+ {
+ this.Counter = info.GetInt32(CounterPropName);
+ }
+
+ public override void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue(CounterPropName, this.Counter);
+ base.GetObjectData(info, context);
+ }
+
+ public int Counter { get; }
+ }
+}
diff --git a/Test/TestFabricApplication/TestApplication.Common/Orchestrations/ExceptionThrowingTask.cs b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/ExceptionThrowingTask.cs
new file mode 100644
index 000000000..3d6de96c5
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/ExceptionThrowingTask.cs
@@ -0,0 +1,32 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace TestApplication.Common.Orchestrations
+{
+ using DurableTask.Core;
+
+ public class ExceptionThrowingTask : TaskActivity
+ {
+ protected override bool Execute(TaskContext context, int remainingAttempts)
+ {
+ if (remainingAttempts > 0)
+ {
+ // ServiceEventSource.Current.Message($"{nameof(ExceptionThrowingTask)}.{nameof(Execute)} : Throwing Exception, RemainingAttempts = {remainingAttempts}");
+ throw new CounterException(remainingAttempts);
+ }
+
+ // ServiceEventSource.Current.Message($"{nameof(ExceptionThrowingTask)}.{nameof(Execute)} : Returning true, RemainingAttempts = {remainingAttempts}");
+ return true;
+ }
+ }
+}
diff --git a/Test/TestFabricApplication/TestApplication.Common/Orchestrations/GenerationBasicOrchestration.cs b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/GenerationBasicOrchestration.cs
new file mode 100644
index 000000000..9868b3aed
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/GenerationBasicOrchestration.cs
@@ -0,0 +1,50 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace TestApplication.Common.Orchestrations
+{
+ using System.Threading.Tasks;
+ using DurableTask.Core;
+
+ public class GenerationBasicOrchestration : TaskOrchestration
+ {
+ // HACK: This is just a hack to communicate result of orchestration back to test
+ public static int Result;
+
+ public override async Task RunTask(OrchestrationContext context, int numberOfGenerations)
+ {
+ int count = await context.ScheduleTask(typeof(GenerationBasicTask));
+ numberOfGenerations--;
+ if (numberOfGenerations > 0)
+ {
+ context.ContinueAsNew(numberOfGenerations);
+ }
+
+ // This is a HACK to get unit test up and running. Should never be done in actual code.
+ Result = count;
+ return count;
+ }
+ }
+
+ public sealed class GenerationBasicTask : TaskActivity
+ {
+ // HACK: This is just a hack to communicate result of orchestration back to test
+ public static int GenerationCount = 0;
+
+ protected override int Execute(TaskContext context, string input)
+ {
+ GenerationCount++;
+ return GenerationCount;
+ }
+ }
+}
diff --git a/Test/TestFabricApplication/TestApplication.Common/Orchestrations/GetUserTask.cs b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/GetUserTask.cs
new file mode 100644
index 000000000..23bb5a3d5
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/GetUserTask.cs
@@ -0,0 +1,25 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace TestApplication.Common.Orchestrations
+{
+ using DurableTask.Core;
+
+ public class GetUserTask : TaskActivity
+ {
+ protected override string Execute(TaskContext context, string input)
+ {
+ return "Gabbar";
+ }
+ }
+}
diff --git a/Test/TestFabricApplication/TestApplication.Common/Orchestrations/GreetUserTask.cs b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/GreetUserTask.cs
new file mode 100644
index 000000000..eb3fd98ed
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/GreetUserTask.cs
@@ -0,0 +1,25 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace TestApplication.Common.Orchestrations
+{
+ using DurableTask.Core;
+
+ public class GreetUserTask : TaskActivity
+ {
+ protected override string Execute(TaskContext context, string user)
+ {
+ return "Hello " + user;
+ }
+ }
+}
diff --git a/Test/TestFabricApplication/TestApplication.Common/Orchestrations/OrchestrationRunningIntoRetry.cs b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/OrchestrationRunningIntoRetry.cs
new file mode 100644
index 000000000..42d0374b1
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/OrchestrationRunningIntoRetry.cs
@@ -0,0 +1,63 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace TestApplication.Common.Orchestrations
+{
+ using System;
+ using System.Threading.Tasks;
+ using DurableTask.Core;
+ using DurableTask.Core.Exceptions;
+
+ public class OrchestrationRunningIntoRetry : TaskOrchestration
+ {
+ CounterException LatestException;
+
+ public override async Task RunTask(OrchestrationContext context, int numberOfRetriesToEnforce)
+ {
+ var result = await context.ScheduleWithRetry(typeof(ExceptionThrowingTask),
+ new RetryOptions(TimeSpan.FromSeconds(1), 5)
+ {
+ BackoffCoefficient = 2,
+ MaxRetryInterval = TimeSpan.FromMinutes(1),
+ Handle = RetryExceptionHandler
+ },
+ this.LatestException?.Counter-1 ?? numberOfRetriesToEnforce);
+
+ if (result)
+ {
+ return numberOfRetriesToEnforce;
+ }
+
+ throw new Exception($"Unexpected exception thrown from {nameof(OrchestrationRunningIntoRetry)}.");
+ }
+
+ bool RetryExceptionHandler(Exception e)
+ {
+ TaskFailedException tfe = e as TaskFailedException;
+
+ if (tfe != null && tfe.InnerException != null)
+ {
+ e = tfe.InnerException;
+ }
+
+ CounterException ce = e as CounterException;
+ if (ce != null)
+ {
+ LatestException = ce;
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Test/TestFabricApplication/TestApplication.Common/Orchestrations/SimpleOrchestrationWithSubOrchestration.cs b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/SimpleOrchestrationWithSubOrchestration.cs
new file mode 100644
index 000000000..159b5b381
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/SimpleOrchestrationWithSubOrchestration.cs
@@ -0,0 +1,30 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace TestApplication.Common.Orchestrations
+{
+ using System.Threading.Tasks;
+ using DurableTask.Core;
+
+ public class SimpleOrchestrationWithSubOrchestration : TaskOrchestration
+ {
+ public override async Task RunTask(OrchestrationContext context, string input)
+ {
+ var subOrch1 = context.CreateSubOrchestrationInstance(typeof(SimpleOrchestrationWithTasks), input);
+ var subOrch2 = context.CreateSubOrchestrationInstance(typeof(SimpleOrchestrationWithTasks), input);
+ var myTask = context.ScheduleTask(typeof(GreetUserTask), "World");
+ await Task.WhenAll(subOrch1, subOrch2, myTask);
+ return $"TaskResult = {myTask.Result} , SubOrchestration1Result = {subOrch1.Result}, SubOrchestration2Result = {subOrch2.Result}";
+ }
+ }
+}
diff --git a/Test/TestFabricApplication/TestApplication.Common/Orchestrations/SimpleOrchestrationWithTasks.cs b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/SimpleOrchestrationWithTasks.cs
new file mode 100644
index 000000000..957139074
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/SimpleOrchestrationWithTasks.cs
@@ -0,0 +1,30 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace TestApplication.Common.Orchestrations
+{
+ using System.Threading.Tasks;
+ using DurableTask.Core;
+
+ public class SimpleOrchestrationWithTasks : TaskOrchestration
+ {
+ public override async Task RunTask(OrchestrationContext context, string input)
+ {
+ string user = await context.ScheduleTask(typeof(GetUserTask));
+
+ string greeting = await context.ScheduleTask(typeof(GreetUserTask), user);
+
+ return greeting;
+ }
+ }
+}
diff --git a/Test/TestFabricApplication/TestApplication.Common/Orchestrations/SimpleOrchestrationWithTimer.cs b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/SimpleOrchestrationWithTimer.cs
new file mode 100644
index 000000000..5880c23fd
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.Common/Orchestrations/SimpleOrchestrationWithTimer.cs
@@ -0,0 +1,28 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace TestApplication.Common.Orchestrations
+{
+ using System;
+ using System.Threading.Tasks;
+ using DurableTask.Core;
+
+ public class SimpleOrchestrationWithTimer : TaskOrchestration
+ {
+ public override async Task RunTask(OrchestrationContext context, int input)
+ {
+ await context.CreateTimer(context.CurrentUtcDateTime.Add(TimeSpan.FromSeconds(input)), null);
+ return await context.ScheduleTask(typeof(GreetUserTask), "Gabbar");
+ }
+ }
+}
diff --git a/Test/TestFabricApplication/TestApplication.Common/TestApplication.Common.csproj b/Test/TestFabricApplication/TestApplication.Common/TestApplication.Common.csproj
new file mode 100644
index 000000000..907f0b0d9
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.Common/TestApplication.Common.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net461
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Test/TestFabricApplication/TestApplication.StatefulService/PackageRoot/Config/Settings.xml b/Test/TestFabricApplication/TestApplication.StatefulService/PackageRoot/Config/Settings.xml
new file mode 100644
index 000000000..9535402bb
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.StatefulService/PackageRoot/Config/Settings.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/Test/TestFabricApplication/TestApplication.StatefulService/PackageRoot/ServiceManifest.xml b/Test/TestFabricApplication/TestApplication.StatefulService/PackageRoot/ServiceManifest.xml
new file mode 100644
index 000000000..3970b79b8
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.StatefulService/PackageRoot/ServiceManifest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+ TestApplication.StatefulService.exe
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Test/TestFabricApplication/TestApplication.StatefulService/Program.cs b/Test/TestFabricApplication/TestApplication.StatefulService/Program.cs
new file mode 100644
index 000000000..dc2bde054
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.StatefulService/Program.cs
@@ -0,0 +1,54 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace TestApplication.StatefulService
+{
+ using System;
+ using System.Diagnostics;
+ using System.Threading;
+
+ using DurableTask.AzureServiceFabric.Service;
+
+ using Microsoft.ServiceFabric.Services.Runtime;
+
+ internal static class Program
+ {
+ ///
+ /// This is the entry point of the service host process.
+ ///
+ private static void Main()
+ {
+ try
+ {
+ // The ServiceManifest.XML file defines one or more service type names.
+ // Registering a service maps a service type name to a .NET type.
+ // When Service Fabric creates an instance of this service type,
+ // an instance of the class is created in this host process.
+ ServiceRuntime.RegisterServiceAsync("StatefulServiceType", context =>
+ {
+ var testProvider = new TestOrchestrationsProvider();
+ var listener = new TaskHubProxyListener(context, testProvider.GetFabricOrchestrationProviderSettings(), testProvider.RegisterOrchestrations);
+ return new TaskHubStatefulService(context, new[] { listener });
+ }).GetAwaiter().GetResult();
+
+ // Prevents this host process from terminating so services keep running.
+ Thread.Sleep(Timeout.Infinite);
+ }
+ catch (Exception e)
+ {
+ Trace.WriteLine(e);
+ throw;
+ }
+ }
+ }
+}
diff --git a/Test/TestFabricApplication/TestApplication.StatefulService/TestApplication.StatefulService.csproj b/Test/TestFabricApplication/TestApplication.StatefulService/TestApplication.StatefulService.csproj
new file mode 100644
index 000000000..2c7a894ff
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.StatefulService/TestApplication.StatefulService.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net461
+ net461
+ x64
+
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Test/TestFabricApplication/TestApplication.StatefulService/TestOrchestrationsProvider.cs b/Test/TestFabricApplication/TestApplication.StatefulService/TestOrchestrationsProvider.cs
new file mode 100644
index 000000000..3fcd93802
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication.StatefulService/TestOrchestrationsProvider.cs
@@ -0,0 +1,85 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace TestApplication.StatefulService
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.AzureServiceFabric;
+ using DurableTask.AzureServiceFabric.Service;
+ using DurableTask.Test.Orchestrations.Performance;
+
+ using TestApplication.Common.Orchestrations;
+
+
+ ///
+ public class TestOrchestrationsProvider
+ {
+ ///
+ public FabricOrchestrationProviderSettings GetFabricOrchestrationProviderSettings()
+ {
+ var settings = new FabricOrchestrationProviderSettings();
+ settings.TaskOrchestrationDispatcherSettings.DispatcherCount = 5;
+ settings.TaskActivityDispatcherSettings.DispatcherCount = 5;
+ return settings;
+ }
+
+ ///
+ public void RegisterOrchestrations(TaskHubWorker taskHubWorker)
+ {
+ taskHubWorker
+ .AddTaskOrchestrations(this.GetOrchestrationTypes().ToArray())
+ .AddTaskOrchestrations(this.GetTaskOrchestrations().Select(instance => new DefaultObjectCreator(instance.Value)).ToArray())
+ .AddTaskActivities(GetActivityTypes().ToArray());
+ }
+
+ ///
+ private IEnumerable GetActivityTypes()
+ {
+ return new Type[]
+ {
+ typeof(GetUserTask),
+ typeof(GreetUserTask),
+ typeof(GenerationBasicTask),
+ typeof(RandomTimeWaitingTask),
+ typeof(ExceptionThrowingTask),
+ typeof(ExecutionCountingActivity)
+ };
+ }
+
+ ///
+ private IEnumerable GetOrchestrationTypes()
+ {
+ return new Type[]
+ {
+ typeof(SimpleOrchestrationWithTasks),
+ typeof(SimpleOrchestrationWithTimer),
+ typeof(GenerationBasicOrchestration),
+ typeof(SimpleOrchestrationWithSubOrchestration),
+ typeof(DriverOrchestration),
+ typeof(TestOrchestration),
+ typeof(ExecutionCountingOrchestration)
+ };
+ }
+
+ ///
+ private IEnumerable> GetTaskOrchestrations()
+ {
+ yield return new KeyValuePair(typeof(OrchestrationRunningIntoRetry).Name, new OrchestrationRunningIntoRetry());
+ }
+ }
+}
diff --git a/Test/TestFabricApplication/TestApplication/ApplicationPackageRoot/ApplicationManifest.xml b/Test/TestFabricApplication/TestApplication/ApplicationPackageRoot/ApplicationManifest.xml
new file mode 100644
index 000000000..c123c17c9
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication/ApplicationPackageRoot/ApplicationManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Test/TestFabricApplication/TestApplication/ApplicationParameters/Cloud.xml b/Test/TestFabricApplication/TestApplication/ApplicationParameters/Cloud.xml
new file mode 100644
index 000000000..c47814698
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication/ApplicationParameters/Cloud.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Test/TestFabricApplication/TestApplication/ApplicationParameters/Local.1Node.xml b/Test/TestFabricApplication/TestApplication/ApplicationParameters/Local.1Node.xml
new file mode 100644
index 000000000..3411afa5b
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication/ApplicationParameters/Local.1Node.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Test/TestFabricApplication/TestApplication/ApplicationParameters/Local.5Node.xml b/Test/TestFabricApplication/TestApplication/ApplicationParameters/Local.5Node.xml
new file mode 100644
index 000000000..c47814698
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication/ApplicationParameters/Local.5Node.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Test/TestFabricApplication/TestApplication/PublishProfiles/Cloud.xml b/Test/TestFabricApplication/TestApplication/PublishProfiles/Cloud.xml
new file mode 100644
index 000000000..1301dd83f
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication/PublishProfiles/Cloud.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Test/TestFabricApplication/TestApplication/PublishProfiles/Local.1Node.xml b/Test/TestFabricApplication/TestApplication/PublishProfiles/Local.1Node.xml
new file mode 100644
index 000000000..6e1403e96
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication/PublishProfiles/Local.1Node.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Test/TestFabricApplication/TestApplication/PublishProfiles/Local.5Node.xml b/Test/TestFabricApplication/TestApplication/PublishProfiles/Local.5Node.xml
new file mode 100644
index 000000000..f42d759c3
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication/PublishProfiles/Local.5Node.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Test/TestFabricApplication/TestApplication/Scripts/Deploy-FabricApplication.ps1 b/Test/TestFabricApplication/TestApplication/Scripts/Deploy-FabricApplication.ps1
new file mode 100644
index 000000000..2897b10e8
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication/Scripts/Deploy-FabricApplication.ps1
@@ -0,0 +1,258 @@
+<#
+.SYNOPSIS
+Deploys a Service Fabric application type to a cluster.
+
+.DESCRIPTION
+This script deploys a Service Fabric application type to a cluster. It is invoked by Visual Studio when deploying a Service Fabric Application project.
+
+.NOTES
+WARNING: This script file is invoked by Visual Studio. Its parameters must not be altered but its logic can be customized as necessary.
+
+.PARAMETER PublishProfileFile
+Path to the file containing the publish profile.
+
+.PARAMETER ApplicationPackagePath
+Path to the folder of the packaged Service Fabric application.
+
+.PARAMETER DeployOnly
+Indicates that the Service Fabric application should not be created or upgraded after registering the application type.
+
+.PARAMETER ApplicationParameter
+Hashtable of the Service Fabric application parameters to be used for the application.
+
+.PARAMETER UnregisterUnusedApplicationVersionsAfterUpgrade
+Indicates whether to unregister any unused application versions that exist after an upgrade is finished.
+
+.PARAMETER OverrideUpgradeBehavior
+Indicates the behavior used to override the upgrade settings specified by the publish profile.
+'None' indicates that the upgrade settings will not be overridden.
+'ForceUpgrade' indicates that an upgrade will occur with default settings, regardless of what is specified in the publish profile.
+'VetoUpgrade' indicates that an upgrade will not occur, regardless of what is specified in the publish profile.
+
+.PARAMETER UseExistingClusterConnection
+Indicates that the script should make use of an existing cluster connection that has already been established in the PowerShell session. The cluster connection parameters configured in the publish profile are ignored.
+
+.PARAMETER OverwriteBehavior
+Overwrite Behavior if an application exists in the cluster with the same name. Available Options are Never, Always, SameAppTypeAndVersion. This setting is not applicable when upgrading an application.
+'Never' will not remove the existing application. This is the default behavior.
+'Always' will remove the existing application even if its Application type and Version is different from the application being created.
+'SameAppTypeAndVersion' will remove the existing application only if its Application type and Version is same as the application being created.
+
+.PARAMETER SkipPackageValidation
+Switch signaling whether the package should be validated or not before deployment.
+
+.PARAMETER SecurityToken
+A security token for authentication to cluster management endpoints. Used for silent authentication to clusters that are protected by Azure Active Directory.
+
+.PARAMETER CopyPackageTimeoutSec
+Timeout in seconds for copying application package to image store.
+
+.EXAMPLE
+. Scripts\Deploy-FabricApplication.ps1 -ApplicationPackagePath 'pkg\Debug'
+
+Deploy the application using the default package location for a Debug build.
+
+.EXAMPLE
+. Scripts\Deploy-FabricApplication.ps1 -ApplicationPackagePath 'pkg\Debug' -DoNotCreateApplication
+
+Deploy the application but do not create the application instance.
+
+.EXAMPLE
+. Scripts\Deploy-FabricApplication.ps1 -ApplicationPackagePath 'pkg\Debug' -ApplicationParameter @{CustomParameter1='MyValue'; CustomParameter2='MyValue'}
+
+Deploy the application by providing values for parameters that are defined in the application manifest.
+#>
+
+Param
+(
+ [String]
+ $PublishProfileFile,
+
+ [String]
+ $ApplicationPackagePath,
+
+ [Switch]
+ $DeployOnly,
+
+ [Hashtable]
+ $ApplicationParameter,
+
+ [Boolean]
+ $UnregisterUnusedApplicationVersionsAfterUpgrade,
+
+ [String]
+ [ValidateSet('None', 'ForceUpgrade', 'VetoUpgrade')]
+ $OverrideUpgradeBehavior = 'None',
+
+ [Switch]
+ $UseExistingClusterConnection,
+
+ [String]
+ [ValidateSet('Never','Always','SameAppTypeAndVersion')]
+ $OverwriteBehavior = 'Never',
+
+ [Switch]
+ $SkipPackageValidation,
+
+ [String]
+ $SecurityToken,
+
+ [int]
+ $CopyPackageTimeoutSec
+)
+
+function Read-XmlElementAsHashtable
+{
+ Param (
+ [System.Xml.XmlElement]
+ $Element
+ )
+
+ $hashtable = @{}
+ if ($Element.Attributes)
+ {
+ $Element.Attributes |
+ ForEach-Object {
+ $boolVal = $null
+ if ([bool]::TryParse($_.Value, [ref]$boolVal)) {
+ $hashtable[$_.Name] = $boolVal
+ }
+ else {
+ $hashtable[$_.Name] = $_.Value
+ }
+ }
+ }
+
+ return $hashtable
+}
+
+function Read-PublishProfile
+{
+ Param (
+ [ValidateScript({Test-Path $_ -PathType Leaf})]
+ [String]
+ $PublishProfileFile
+ )
+
+ $publishProfileXml = [Xml] (Get-Content $PublishProfileFile)
+ $publishProfile = @{}
+
+ $publishProfile.ClusterConnectionParameters = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("ClusterConnectionParameters")
+ $publishProfile.UpgradeDeployment = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("UpgradeDeployment")
+ $publishProfile.CopyPackageParameters = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("CopyPackageParameters")
+
+ if ($publishProfileXml.PublishProfile.Item("UpgradeDeployment"))
+ {
+ $publishProfile.UpgradeDeployment.Parameters = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("UpgradeDeployment").Item("Parameters")
+ if ($publishProfile.UpgradeDeployment["Mode"])
+ {
+ $publishProfile.UpgradeDeployment.Parameters[$publishProfile.UpgradeDeployment["Mode"]] = $true
+ }
+ }
+
+ $publishProfileFolder = (Split-Path $PublishProfileFile)
+ $publishProfile.ApplicationParameterFile = [System.IO.Path]::Combine($PublishProfileFolder, $publishProfileXml.PublishProfile.ApplicationParameterFile.Path)
+
+ return $publishProfile
+}
+
+$LocalFolder = (Split-Path $MyInvocation.MyCommand.Path)
+
+if (!$PublishProfileFile)
+{
+ $PublishProfileFile = "$LocalFolder\..\PublishProfiles\Local.xml"
+}
+
+if (!$ApplicationPackagePath)
+{
+ $ApplicationPackagePath = "$LocalFolder\..\pkg\Release"
+}
+
+$ApplicationPackagePath = Resolve-Path $ApplicationPackagePath
+
+$publishProfile = Read-PublishProfile $PublishProfileFile
+
+if (-not $UseExistingClusterConnection)
+{
+ $ClusterConnectionParameters = $publishProfile.ClusterConnectionParameters
+ if ($SecurityToken)
+ {
+ $ClusterConnectionParameters["SecurityToken"] = $SecurityToken
+ }
+
+ try
+ {
+ [void](Connect-ServiceFabricCluster @ClusterConnectionParameters)
+ }
+ catch [System.Fabric.FabricObjectClosedException]
+ {
+ Write-Warning "Service Fabric cluster may not be connected."
+ throw
+ }
+}
+
+$RegKey = "HKLM:\SOFTWARE\Microsoft\Service Fabric SDK"
+$ModuleFolderPath = (Get-ItemProperty -Path $RegKey -Name FabricSDKPSModulePath).FabricSDKPSModulePath
+Import-Module "$ModuleFolderPath\ServiceFabricSDK.psm1"
+
+$IsUpgrade = ($publishProfile.UpgradeDeployment -and $publishProfile.UpgradeDeployment.Enabled -and $OverrideUpgradeBehavior -ne 'VetoUpgrade') -or $OverrideUpgradeBehavior -eq 'ForceUpgrade'
+
+$PublishParameters = @{
+ 'ApplicationPackagePath' = $ApplicationPackagePath
+ 'ApplicationParameterFilePath' = $publishProfile.ApplicationParameterFile
+ 'ApplicationParameter' = $ApplicationParameter
+ 'ErrorAction' = 'Stop'
+}
+
+if ($publishProfile.CopyPackageParameters.CopyPackageTimeoutSec)
+{
+ $PublishParameters['CopyPackageTimeoutSec'] = $publishProfile.CopyPackageParameters.CopyPackageTimeoutSec
+}
+
+if ($publishProfile.CopyPackageParameters.CompressPackage)
+{
+ $PublishParameters['CompressPackage'] = $publishProfile.CopyPackageParameters.CompressPackage
+}
+
+# CopyPackageTimeoutSec parameter overrides the value from the publish profile
+if ($CopyPackageTimeoutSec)
+{
+ $PublishParameters['CopyPackageTimeoutSec'] = $CopyPackageTimeoutSec
+}
+
+if ($IsUpgrade)
+{
+ $Action = "RegisterAndUpgrade"
+ if ($DeployOnly)
+ {
+ $Action = "Register"
+ }
+
+ $UpgradeParameters = $publishProfile.UpgradeDeployment.Parameters
+
+ if ($OverrideUpgradeBehavior -eq 'ForceUpgrade')
+ {
+ # Warning: Do not alter these upgrade parameters. It will create an inconsistency with Visual Studio's behavior.
+ $UpgradeParameters = @{ UnmonitoredAuto = $true; Force = $true }
+ }
+
+ $PublishParameters['Action'] = $Action
+ $PublishParameters['UpgradeParameters'] = $UpgradeParameters
+ $PublishParameters['UnregisterUnusedVersions'] = $UnregisterUnusedApplicationVersionsAfterUpgrade
+
+ Publish-UpgradedServiceFabricApplication @PublishParameters
+}
+else
+{
+ $Action = "RegisterAndCreate"
+ if ($DeployOnly)
+ {
+ $Action = "Register"
+ }
+
+ $PublishParameters['Action'] = $Action
+ $PublishParameters['OverwriteBehavior'] = $OverwriteBehavior
+ $PublishParameters['SkipPackageValidation'] = $SkipPackageValidation
+
+ Publish-NewServiceFabricApplication @PublishParameters
+}
\ No newline at end of file
diff --git a/Test/TestFabricApplication/TestApplication/TestApplication.sfproj b/Test/TestFabricApplication/TestApplication/TestApplication.sfproj
new file mode 100644
index 000000000..bdfc8e742
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication/TestApplication.sfproj
@@ -0,0 +1,47 @@
+
+
+
+
+ 1d52f94e-933c-411f-96c4-4960b423586f
+ 2.4
+ 1.5
+ 1.6.7
+ v4.6.1
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Service Fabric Tools\Microsoft.VisualStudio.Azure.Fabric.ApplicationProject.targets
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Test/TestFabricApplication/TestApplication/packages.config b/Test/TestFabricApplication/TestApplication/packages.config
new file mode 100644
index 000000000..45379443b
--- /dev/null
+++ b/Test/TestFabricApplication/TestApplication/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/DurableTask.AzureServiceFabric/AssemblyInfo.cs b/src/DurableTask.AzureServiceFabric/AssemblyInfo.cs
new file mode 100644
index 000000000..097afbd40
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/AssemblyInfo.cs
@@ -0,0 +1,21 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+[assembly: ComVisible(false)]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Scope = "type", Target = "DurableTask.AzureServiceFabric.Exceptions.RemoteServiceException")]
+#if !SIGN_ASSEMBLY
+[assembly: InternalsVisibleTo("DurableTask.AzureServiceFabric.Tests")]
+#endif
diff --git a/src/DurableTask.AzureServiceFabric/AsyncManualResetEvent.cs b/src/DurableTask.AzureServiceFabric/AsyncManualResetEvent.cs
new file mode 100644
index 000000000..17b246fa4
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/AsyncManualResetEvent.cs
@@ -0,0 +1,54 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ sealed class AsyncManualResetEvent
+ {
+ TaskCompletionSource taskCompletionSource = new TaskCompletionSource();
+
+ public async Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken)
+ {
+ if (timeout < TimeSpan.Zero)
+ {
+ timeout = TimeSpan.Zero;
+ }
+
+ var delayTask = Task.Delay(timeout, cancellationToken);
+ var resultTask = await Task.WhenAny(delayTask, this.taskCompletionSource.Task);
+ return delayTask != resultTask;
+ }
+
+ public void Set()
+ {
+ this.taskCompletionSource.TrySetResult(true);
+ }
+
+ public void Reset()
+ {
+ while (true)
+ {
+ var thisTcs = this.taskCompletionSource;
+
+ if (!thisTcs.Task.IsCompleted || Interlocked.CompareExchange(ref this.taskCompletionSource, new TaskCompletionSource(), thisTcs) == thisTcs)
+ {
+ return;
+ }
+ }
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Constants.cs b/src/DurableTask.AzureServiceFabric/Constants.cs
new file mode 100644
index 000000000..18a461730
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Constants.cs
@@ -0,0 +1,28 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ static class Constants
+ {
+ internal const string CollectionNameUniquenessPrefix = "DtfxSfp_";
+ internal const string SessionMessagesDictionaryPrefix = CollectionNameUniquenessPrefix + "SessionMessages_";
+ internal const string OrchestrationDictionaryName = CollectionNameUniquenessPrefix + "Orchestrations";
+ internal const string ActivitiesQueueName = CollectionNameUniquenessPrefix + "Activities";
+ internal const string InstanceStoreDictionaryName = CollectionNameUniquenessPrefix + "InstanceStore";
+ internal const string ExecutionStoreDictionaryName = CollectionNameUniquenessPrefix + "ExecutionIdStore";
+ internal const string ScheduledMessagesDictionaryName = CollectionNameUniquenessPrefix + "ScheduledMessages";
+ internal const string TaskHubProxyServiceName = "DurableTask-TaskHubProxyService";
+ internal const string TaskHubProxyListenerEndpointName = "DtfxServiceEndpoint";
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/DurableTask.AzureServiceFabric.csproj b/src/DurableTask.AzureServiceFabric/DurableTask.AzureServiceFabric.csproj
new file mode 100644
index 000000000..8dde46a47
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/DurableTask.AzureServiceFabric.csproj
@@ -0,0 +1,42 @@
+
+
+
+
+ net461;net472
+ true
+ Microsoft.Azure.DurableTask.AzureServiceFabric
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/DurableTask.AzureServiceFabric/ExceptionUtilities.cs b/src/DurableTask.AzureServiceFabric/ExceptionUtilities.cs
new file mode 100644
index 000000000..01368a7f2
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/ExceptionUtilities.cs
@@ -0,0 +1,40 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System;
+ using System.Fabric;
+
+ using DurableTask.AzureServiceFabric.Tracing;
+
+ static class ExceptionUtilities
+ {
+ public static bool IsRetryableFabricException(Exception e)
+ {
+ return e is TimeoutException || e is FabricTransientException;
+ }
+
+ public static void LogReliableCollectionException(string uniqueIdentifier, int attemptNumber, Exception e, bool isTransient)
+ {
+ if (isTransient)
+ {
+ ServiceFabricProviderEventSource.Tracing.RetryableFabricException(uniqueIdentifier, attemptNumber, e.ToString());
+ }
+ else
+ {
+ ServiceFabricProviderEventSource.Tracing.ExceptionInReliableCollectionOperations(uniqueIdentifier, e.ToString());
+ }
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Exceptions/InvalidInstanceIdException.cs b/src/DurableTask.AzureServiceFabric/Exceptions/InvalidInstanceIdException.cs
new file mode 100644
index 000000000..e45d99798
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Exceptions/InvalidInstanceIdException.cs
@@ -0,0 +1,33 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Exceptions
+{
+ using System;
+
+ ///
+ /// Exception representing that instanceId is not Valid.
+ ///
+ [Serializable]
+ public class InvalidInstanceIdException : Exception
+ {
+ ///
+ /// Creates an instance of
+ ///
+ /// Orchestration instance id
+ public InvalidInstanceIdException(string instanceId)
+ : base("Not a valid instanceId: " + instanceId)
+ {
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Exceptions/RemoteServiceException.cs b/src/DurableTask.AzureServiceFabric/Exceptions/RemoteServiceException.cs
new file mode 100644
index 000000000..4590f6817
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Exceptions/RemoteServiceException.cs
@@ -0,0 +1,44 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Exceptions
+{
+ using System;
+ using System.Net;
+
+ using DurableTask.AzureServiceFabric.Remote;
+
+
+ ///
+ /// The exception that is thrown by when proxy service returns non successful message.
+ ///
+ public class RemoteServiceException : Exception
+ {
+ private HttpStatusCode statusCode;
+
+ ///
+ /// Creates an instance of .
+ ///
+ /// Exception message.
+ /// Http response message
+ public RemoteServiceException(string message, HttpStatusCode statusCode) : base(message)
+ {
+ this.statusCode = statusCode;
+ }
+
+ ///
+ /// Instace of sent by proxy service.
+ ///
+ public HttpStatusCode HttpStatusCode => this.statusCode;
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Extensions.cs b/src/DurableTask.AzureServiceFabric/Extensions.cs
new file mode 100644
index 000000000..0c1ec296d
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Extensions.cs
@@ -0,0 +1,52 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System;
+ using System.Web;
+
+ using DurableTask.AzureServiceFabric.Exceptions;
+
+ ///
+ /// Defines extensions methods.
+ ///
+ public static class Extensions
+ {
+ ///
+ /// Validates given instanceId.
+ ///
+ /// Id of an orchestration instance
+ /// boolean indicating whether instanceId is valid or not
+ public static bool IsValidInstanceId(this string instanceId)
+ {
+ if (instanceId == null)
+ {
+ return false;
+ }
+
+ // InsanceId consists of valid url characters is treated as valid.
+ var encodedInstanceId = HttpUtility.UrlEncode(instanceId);
+
+ return instanceId.Equals(encodedInstanceId, StringComparison.OrdinalIgnoreCase);
+ }
+
+ internal static void EnsureValidInstanceId(this string instanceId)
+ {
+ if (!instanceId.IsValidInstanceId())
+ {
+ throw new InvalidInstanceIdException(instanceId);
+ }
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/FabricOrchestrationInstanceStore.cs b/src/DurableTask.AzureServiceFabric/FabricOrchestrationInstanceStore.cs
new file mode 100644
index 000000000..90768c598
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/FabricOrchestrationInstanceStore.cs
@@ -0,0 +1,383 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System;
+ using System.Collections.Concurrent;
+ using System.Collections.Generic;
+ using System.Collections.Immutable;
+ using System.Globalization;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.Core.Tracking;
+ using DurableTask.AzureServiceFabric.TaskHelpers;
+ using DurableTask.AzureServiceFabric.Tracing;
+ using Microsoft.ServiceFabric.Data;
+ using Microsoft.ServiceFabric.Data.Collections;
+
+ // Potential todo:
+ // - Support for querying state across executions (makes sense only after ContinuedAsNew is supported)
+ // - Support writing multiple state events for a given orchestration instance/execution (?)
+ // - Support writing/querying/purging history events
+ class FabricOrchestrationInstanceStore : IFabricOrchestrationServiceInstanceStore
+ {
+ const string InstanceStoreCollectionNamePrefix = Constants.CollectionNameUniquenessPrefix + "InstSt_";
+ const string TimeFormatString = "yyyy-MM-dd-HH";
+ const string TimeFormatStringPrefix = "yyyy-MM-dd-";
+ readonly IReliableStateManager stateManager;
+ readonly CancellationToken cancellationToken;
+ readonly ConcurrentDictionary orchestrationWaiters = new ConcurrentDictionary(OrchestrationInstanceComparer.Default);
+
+ IReliableDictionary instanceStore;
+ IReliableDictionary> executionIdStore;
+
+ public FabricOrchestrationInstanceStore(IReliableStateManager stateManager, CancellationToken token)
+ {
+ this.stateManager = stateManager;
+ this.cancellationToken = token;
+ }
+
+ public int MaxExecutionIdsLength { get; set; } = 100;
+
+ public Task InitializeStoreAsync(bool recreate)
+ {
+ if (recreate)
+ {
+ return DeleteStoreAsync();
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public async Task StartAsync()
+ {
+ await EnsureStoreInitializedAsync();
+ var nowait = CleanupOldDictionariesAsync();
+ }
+
+ public async Task DeleteStoreAsync()
+ {
+ await this.stateManager.RemoveAsync(Constants.InstanceStoreDictionaryName);
+ await this.stateManager.RemoveAsync(Constants.ExecutionStoreDictionaryName);
+ }
+
+ public async Task WriteEntitiesAsync(ITransaction transaction, IEnumerable entities)
+ {
+ await EnsureStoreInitializedAsync();
+ foreach (var entity in entities)
+ {
+ var state = entity as OrchestrationStateInstanceEntity;
+ if (state != null && state.State != null)
+ {
+ var instance = state.State.OrchestrationInstance;
+ string key = GetKey(instance.InstanceId, instance.ExecutionId);
+
+ if (state.State.OrchestrationStatus.IsRunningOrPending())
+ {
+ if (state.State.OrchestrationStatus == OrchestrationStatus.Pending)
+ {
+ await this.executionIdStore.AddOrUpdateAsync(transaction, instance.InstanceId, new List { instance.ExecutionId },
+ (k, old) =>
+ {
+ old.Add(instance.ExecutionId);
+ if (old.Count > this.MaxExecutionIdsLength)
+ {
+ // Remove first 10% items.
+ int skipItemsLength = (int)(this.MaxExecutionIdsLength * 0.1);
+ old = old.Skip(skipItemsLength).ToList();
+ }
+
+ return old;
+ });
+ }
+
+ await this.instanceStore.AddOrUpdateAsync(transaction, key, state.State, (k, oldValue) => state.State);
+ }
+ else
+ {
+ var backupDictionaryName = GetDictionaryKeyFromTimeFormat(DateTime.UtcNow);
+ // It's intentional to not pass 'transaction' parameter to this call, this API doesn't seem to follow
+ // see-what-you-commit-within-the-transaction rules. If we add it within the transaction and immediately
+ // try to AddOrUpdateAsync an entry in dictionary that doesn't work.
+ var backupDictionary = await this.stateManager.GetOrAddAsync>(backupDictionaryName);
+ await backupDictionary.AddOrUpdateAsync(transaction, key, state.State, (k, oldValue) => state.State);
+ await this.instanceStore.TryRemoveAsync(transaction, key);
+ }
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
+ }
+
+ public async Task> GetOrchestrationStateAsync(string instanceId, bool allInstances)
+ {
+ if (allInstances)
+ {
+ throw new NotImplementedException("Querying for state across all executions for an orchestration is not supported, only the latest execution can be queried");
+ }
+
+ await EnsureStoreInitializedAsync();
+
+ string latestExecutionId = (await GetExecutionIds(instanceId))?.Last();
+
+ if (latestExecutionId != null)
+ {
+ var state = await GetOrchestrationStateAsync(instanceId, latestExecutionId);
+ if (state != null)
+ {
+ return new List() { state };
+ }
+ }
+
+ return ImmutableList.Empty;
+ }
+
+ public async Task> GetExecutionIds(string instanceId)
+ {
+ List executionIds = null;
+
+ await RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ using (var tx = this.stateManager.CreateTransaction())
+ {
+ var executionIdsValue = await this.executionIdStore.TryGetValueAsync(tx, instanceId);
+ if (executionIdsValue.HasValue)
+ {
+ executionIds = executionIdsValue.Value;
+ }
+ }
+ }, uniqueActionIdentifier: $"Orchestration Instance Id = {instanceId}, Action = {nameof(FabricOrchestrationInstanceStore)}.{nameof(GetExecutionIds)}");
+
+ return executionIds;
+ }
+
+ public async Task GetOrchestrationStateAsync(string instanceId, string executionId)
+ {
+ await EnsureStoreInitializedAsync();
+ var queryKey = this.GetKey(instanceId, executionId);
+ var result = await RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ using (var txn = this.stateManager.CreateTransaction())
+ {
+ var state = await this.instanceStore.TryGetValueAsync(txn, queryKey);
+ if (state.HasValue)
+ {
+ return new OrchestrationStateInstanceEntity()
+ {
+ State = state.Value,
+ };
+ }
+ }
+ return null;
+ }, uniqueActionIdentifier: $"Orchestration Instance Id = {instanceId}, ExecutionId = {executionId}, Action = {nameof(FabricOrchestrationInstanceStore)}.{nameof(GetOrchestrationStateAsync)}:QueryInstanceStore");
+
+ if (result != null)
+ {
+ return result;
+ }
+
+ // If querying for orchestration which completed an hour ago, we won't return the results.
+ var now = DateTime.UtcNow;
+ for (int i = 0; i < 2; i++)
+ {
+ var backupDictionaryName = GetDictionaryKeyFromTimeFormat(now - TimeSpan.FromHours(i));
+ var backupDictionary = await this.stateManager.TryGetAsync>(backupDictionaryName);
+
+ if (backupDictionary.HasValue)
+ {
+ result = await RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ using (var txn = this.stateManager.CreateTransaction())
+ {
+ var state = await backupDictionary.Value.TryGetValueAsync(txn, queryKey);
+ if (state.HasValue)
+ {
+ return new OrchestrationStateInstanceEntity()
+ {
+ State = state.Value,
+ };
+ }
+ }
+
+ return null;
+ }, uniqueActionIdentifier: $"Orchestration Instance Id = {instanceId}, ExecutionId = {executionId}, Action = {nameof(FabricOrchestrationInstanceStore)}.{nameof(GetOrchestrationStateAsync)}:QueryBackupInstanceStore {backupDictionaryName}");
+
+ if (result != null)
+ {
+ return result;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public Task> GetOrchestrationHistoryEventsAsync(string instanceId, string executionId)
+ {
+ throw new NotImplementedException();
+ }
+
+ // Todo: This is incomplete and inaccurate implementation done for testing purposes.
+ // The method will cleanup state for every orchestration happening in the hour time window of given time,
+ // for example, if the given time is 9.35, it will delete state for all orchestrations that
+ // are completed between 9.00 to 9.59!!!
+ public async Task PurgeOrchestrationHistoryEventsAsync(DateTime threshholdHourlyDateTimeUtc)
+ {
+ try
+ {
+ await this.stateManager.RemoveAsync(GetDictionaryKeyFromTimeFormat(threshholdHourlyDateTimeUtc));
+ }
+ catch (Exception e)
+ {
+ ServiceFabricProviderEventSource.Tracing.LogProxyServiceError($"PurgeOrchestrationHistoryEventsAsync failed with exception {e.ToString()}");
+ }
+ }
+
+ string GetKey(string instanceId, string executionId)
+ {
+ return string.Concat(instanceId, "_", executionId);
+ }
+
+ string GetDictionaryKeyFromTimeFormat(DateTime time)
+ {
+ return GetInstanceStoreBackupDictionaryKey(time, TimeFormatString);
+ }
+
+ string GetDictionaryKeyFromTimePrefixFormat(DateTime time)
+ {
+ return GetInstanceStoreBackupDictionaryKey(time, TimeFormatStringPrefix);
+ }
+
+ string GetInstanceStoreBackupDictionaryKey(DateTime time, string formatString)
+ {
+ return InstanceStoreCollectionNamePrefix + time.ToString(formatString);
+ }
+
+ Task CleanupDayOldDictionariesAsync()
+ {
+ return Utils.RunBackgroundJob(async () =>
+ {
+ var purgeTime = GetDictionaryKeyFromTimePrefixFormat(DateTime.UtcNow - TimeSpan.FromDays(1));
+
+ for (int i = 0; i < 24; i++)
+ {
+ await this.stateManager.RemoveAsync($"{purgeTime}{i:D2}");
+ }
+ }, initialDelay: TimeSpan.FromMinutes(5), delayOnSuccess: TimeSpan.FromHours(12), delayOnException: TimeSpan.FromHours(1), actionName: $"{nameof(CleanupDayOldDictionariesAsync)}", token: this.cancellationToken);
+ }
+
+ Task CleanupOldDictionariesAsync()
+ {
+ return Utils.RunBackgroundJob(async () =>
+ {
+ List toDelete = new List();
+ List toKeep = new List();
+ var currentTime = DateTime.UtcNow;
+ var ttl = TimeSpan.FromDays(1);
+
+ var enumerationTime = await Utils.MeasureAsync(async () =>
+ {
+ var enumerator = this.stateManager.GetAsyncEnumerator();
+ while (await enumerator.MoveNextAsync(this.cancellationToken))
+ {
+ var storeName = enumerator.Current.Name.AbsolutePath.Trim('/');
+ if (storeName.StartsWith(InstanceStoreCollectionNamePrefix))
+ {
+ DateTime storeTime;
+ var stringDate = storeName.Substring(InstanceStoreCollectionNamePrefix.Length);
+ if (DateTime.TryParseExact(stringDate, TimeFormatString, CultureInfo.InvariantCulture, DateTimeStyles.None, out storeTime)
+ && (currentTime - storeTime > ttl))
+ {
+ toDelete.Add(storeName);
+ continue;
+ }
+ }
+
+ toKeep.Add(storeName);
+ }
+ });
+ ServiceFabricProviderEventSource.Tracing.LogTimeTaken($"Enumerating all reliable states (count: {toDelete.Count + toKeep.Count})", enumerationTime.TotalMilliseconds);
+
+ ServiceFabricProviderEventSource.Tracing.ReliableStateManagement($"Deleting {toDelete.Count} stores", String.Join(",", toDelete));
+ ServiceFabricProviderEventSource.Tracing.ReliableStateManagement($"All remaining {toKeep.Count} stores", String.Join(",", toKeep));
+
+ foreach (var storeName in toDelete)
+ {
+ var deleteTime = await Utils.MeasureAsync(async () =>
+ {
+ await this.stateManager.RemoveAsync(storeName);
+ });
+ ServiceFabricProviderEventSource.Tracing.LogTimeTaken($"Deleting reliable state {storeName}", deleteTime.TotalMilliseconds);
+ }
+ }, initialDelay: TimeSpan.FromMinutes(5), delayOnSuccess: TimeSpan.FromHours(1), delayOnException: TimeSpan.FromMinutes(10), actionName: $"{nameof(CleanupOldDictionariesAsync)}", token: this.cancellationToken);
+ }
+
+ async Task EnsureStoreInitializedAsync()
+ {
+ if (this.instanceStore == null)
+ {
+ this.instanceStore = await this.stateManager.GetOrAddAsync>(Constants.InstanceStoreDictionaryName);
+ }
+ if (this.executionIdStore == null)
+ {
+ this.executionIdStore = await this.stateManager.GetOrAddAsync>>(Constants.ExecutionStoreDictionaryName);
+ }
+ }
+
+ public async Task WaitForOrchestrationAsync(OrchestrationInstance instance, TimeSpan timeout)
+ {
+ var currentState = await this.GetOrchestrationStateAsync(instance.InstanceId, instance.ExecutionId);
+
+ // If querying state for an orchestration that's not started or completed and state cleaned up, we will immediately return null.
+ if (currentState?.State == null)
+ {
+ return null;
+ }
+
+ if (currentState.State.OrchestrationStatus.IsTerminalState())
+ {
+ return currentState;
+ }
+
+ var waiter = this.orchestrationWaiters.GetOrAdd(instance, new AsyncManualResetEvent());
+ bool completed = await waiter.WaitAsync(timeout, this.cancellationToken);
+
+ if (!completed)
+ {
+ this.orchestrationWaiters.TryRemove(instance, out waiter);
+ }
+
+ currentState = await this.GetOrchestrationStateAsync(instance.InstanceId, instance.ExecutionId);
+ if (currentState?.State != null && currentState.State.OrchestrationStatus.IsTerminalState())
+ {
+ return currentState;
+ }
+
+ return null;
+ }
+
+ public void OnOrchestrationCompleted(OrchestrationInstance instance)
+ {
+ if (this.orchestrationWaiters.TryRemove(instance, out AsyncManualResetEvent resetEvent))
+ {
+ resetEvent.Set();
+ }
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/FabricOrchestrationProvider.cs b/src/DurableTask.AzureServiceFabric/FabricOrchestrationProvider.cs
new file mode 100644
index 000000000..ec8fc06b4
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/FabricOrchestrationProvider.cs
@@ -0,0 +1,91 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System;
+ using System.Threading;
+
+ using DurableTask.Core;
+ using DurableTask.AzureServiceFabric.Stores;
+ using Microsoft.ServiceFabric.Data;
+
+ ///
+ /// Manages instances of a service fabric based store provider implementations
+ /// for and
+ /// to be used in constructing and .
+ ///
+ ///
+ /// Use to create an instance of .
+ /// Note that this provider object should not be used once method is called
+ /// on the worker object created using this provider. A new provider object should be created after that point.
+ ///
+ public sealed class FabricOrchestrationProvider : IDisposable
+ {
+ readonly FabricOrchestrationService orchestrationService;
+ readonly FabricOrchestrationServiceClient orchestrationClient;
+ readonly FabricProviderClient fabricProviderClient;
+ readonly CancellationTokenSource cancellationTokenSource;
+
+ internal FabricOrchestrationProvider(IReliableStateManager stateManager, FabricOrchestrationProviderSettings settings)
+ {
+ this.cancellationTokenSource = new CancellationTokenSource();
+ var sessionProvider = new SessionProvider(stateManager, cancellationTokenSource.Token);
+ var instanceStore = new FabricOrchestrationInstanceStore(stateManager, cancellationTokenSource.Token);
+ this.orchestrationService = new FabricOrchestrationService(stateManager, sessionProvider, instanceStore, settings, cancellationTokenSource);
+ this.orchestrationClient = new FabricOrchestrationServiceClient(stateManager, sessionProvider, instanceStore);
+ this.fabricProviderClient = new FabricProviderClient(stateManager, sessionProvider);
+ }
+
+ ///
+ /// instance that can be used for constructing .
+ ///
+ public IOrchestrationService OrchestrationService
+ {
+ get
+ {
+ EnsureValidInstance();
+ return this.orchestrationService;
+ }
+ }
+
+ ///
+ /// instance that can be used for constructing .
+ ///
+ public IOrchestrationServiceClient OrchestrationServiceClient
+ {
+ get
+ {
+ EnsureValidInstance();
+ return this.orchestrationClient;
+ }
+ }
+
+ ///
+ /// Disposes the object. The object should be disposed after
+ /// is invoked on the created with the .
+ ///
+ public void Dispose()
+ {
+ this.cancellationTokenSource.Dispose();
+ }
+
+ void EnsureValidInstance()
+ {
+ if (this.cancellationTokenSource.IsCancellationRequested)
+ {
+ throw new InvalidOperationException($"{nameof(IOrchestrationService)} instance has been stopped. Discard this provider instance and create a new provider object.");
+ }
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/FabricOrchestrationProviderFactory.cs b/src/DurableTask.AzureServiceFabric/FabricOrchestrationProviderFactory.cs
new file mode 100644
index 000000000..301dc448c
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/FabricOrchestrationProviderFactory.cs
@@ -0,0 +1,60 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System;
+ using Microsoft.ServiceFabric.Data;
+
+ ///
+ /// Factory to create instances of .
+ ///
+ public class FabricOrchestrationProviderFactory
+ {
+ readonly IReliableStateManager stateManager;
+ readonly FabricOrchestrationProviderSettings settings;
+
+ ///
+ /// Constructor that uses default .
+ ///
+ /// Reliable state manager instance. Comes from service fabric stateful service implementation.
+ public FabricOrchestrationProviderFactory(IReliableStateManager stateManager)
+ : this(stateManager, new FabricOrchestrationProviderSettings())
+ {
+ }
+
+ ///
+ /// Constructor that takes the custom .
+ ///
+ /// Reliable state manager instance. Comes from service fabric stateful service implementation.
+ /// Settings to be used for the provider. Refer to for documentation about specific settings.
+ public FabricOrchestrationProviderFactory(IReliableStateManager stateManager, FabricOrchestrationProviderSettings settings)
+ {
+ if (stateManager == null)
+ {
+ throw new ArgumentNullException(nameof(stateManager));
+ }
+
+ this.stateManager = stateManager;
+ this.settings = settings ?? new FabricOrchestrationProviderSettings();
+ }
+
+ ///
+ /// Creates a new object using the factory's state manager and settings.
+ ///
+ public FabricOrchestrationProvider CreateProvider()
+ {
+ return new FabricOrchestrationProvider(this.stateManager, this.settings);
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/FabricOrchestrationProviderSettings.cs b/src/DurableTask.AzureServiceFabric/FabricOrchestrationProviderSettings.cs
new file mode 100644
index 000000000..9a8f2d37e
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/FabricOrchestrationProviderSettings.cs
@@ -0,0 +1,54 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using DurableTask.Core;
+ using DurableTask.Core.Settings;
+
+ ///
+ /// Provides settings for service fabric based custom provider implementations
+ /// for and
+ /// to be used in constructing and .
+ ///
+ public sealed class FabricOrchestrationProviderSettings
+ {
+ ///
+ /// Constructor. Initializes all settings to their default values.
+ ///
+ public FabricOrchestrationProviderSettings()
+ {
+ TaskOrchestrationDispatcherSettings = new TaskOrchestrationDispatcherSettings()
+ {
+ MaxConcurrentOrchestrations = 1000,
+ DispatcherCount = 10
+ };
+
+ TaskActivityDispatcherSettings = new TaskActivityDispatcherSettings()
+ {
+ MaxConcurrentActivities = 1000,
+ DispatcherCount = 10
+ };
+ }
+
+ ///
+ /// Settings to configure the Task Orchestration Dispatcher
+ ///
+ public TaskOrchestrationDispatcherSettings TaskOrchestrationDispatcherSettings { get; set; }
+
+ ///
+ /// Settings to configure the Task Activity Dispatcher
+ ///
+ public TaskActivityDispatcherSettings TaskActivityDispatcherSettings { get; set; }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/FabricOrchestrationService.cs b/src/DurableTask.AzureServiceFabric/FabricOrchestrationService.cs
new file mode 100644
index 000000000..fe9ef78ba
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/FabricOrchestrationService.cs
@@ -0,0 +1,532 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System;
+ using System.Collections.Concurrent;
+ using System.Collections.Generic;
+ using System.Fabric;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.Core.History;
+ using DurableTask.Core.Tracking;
+ using DurableTask.AzureServiceFabric.Stores;
+ using DurableTask.AzureServiceFabric.TaskHelpers;
+ using DurableTask.AzureServiceFabric.Tracing;
+ using Microsoft.ServiceFabric.Data;
+
+ class FabricOrchestrationService : IOrchestrationService
+ {
+ readonly IReliableStateManager stateManager;
+ readonly IFabricOrchestrationServiceInstanceStore instanceStore;
+ readonly SessionProvider orchestrationProvider;
+ readonly ActivityProvider activitiesProvider;
+ readonly ScheduledMessageProvider scheduledMessagesProvider;
+ readonly FabricOrchestrationProviderSettings settings;
+ readonly CancellationTokenSource cancellationTokenSource;
+
+ ConcurrentDictionary sessionInfos = new ConcurrentDictionary();
+
+ public FabricOrchestrationService(IReliableStateManager stateManager,
+ SessionProvider orchestrationProvider,
+ IFabricOrchestrationServiceInstanceStore instanceStore,
+ FabricOrchestrationProviderSettings settings,
+ CancellationTokenSource cancellationTokenSource)
+ {
+ this.stateManager = stateManager ?? throw new ArgumentNullException(nameof(stateManager));
+ this.orchestrationProvider = orchestrationProvider;
+ this.instanceStore = instanceStore;
+ this.settings = settings;
+ this.cancellationTokenSource = cancellationTokenSource;
+ this.activitiesProvider = new ActivityProvider(this.stateManager, Constants.ActivitiesQueueName, cancellationTokenSource.Token);
+ this.scheduledMessagesProvider = new ScheduledMessageProvider(this.stateManager, Constants.ScheduledMessagesDictionaryName, orchestrationProvider, cancellationTokenSource.Token);
+ }
+
+ public Task StartAsync()
+ {
+ return Task.WhenAll(this.activitiesProvider.StartAsync(),
+ this.scheduledMessagesProvider.StartAsync(),
+ this.instanceStore.StartAsync(),
+ this.orchestrationProvider.StartAsync());
+ }
+
+ public Task StopAsync()
+ {
+ return StopAsync(false);
+ }
+
+ public Task StopAsync(bool isForced)
+ {
+ if (!this.cancellationTokenSource.IsCancellationRequested)
+ {
+ this.cancellationTokenSource.Cancel();
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public Task CreateAsync()
+ {
+ return CreateAsync(true);
+ }
+
+ public Task CreateAsync(bool recreateInstanceStore)
+ {
+ return DeleteAsync(deleteInstanceStore: recreateInstanceStore);
+ // Actual creation will be done on demand when we call GetOrAddAsync in StartAsync method.
+ }
+
+ public Task CreateIfNotExistsAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task DeleteAsync()
+ {
+ return DeleteAsync(true);
+ }
+
+ public Task DeleteAsync(bool deleteInstanceStore)
+ {
+ List tasks = new List();
+ tasks.Add(this.stateManager.RemoveAsync(Constants.OrchestrationDictionaryName));
+ tasks.Add(this.stateManager.RemoveAsync(Constants.ScheduledMessagesDictionaryName));
+ tasks.Add(this.stateManager.RemoveAsync(Constants.ActivitiesQueueName));
+
+ if (deleteInstanceStore)
+ {
+ tasks.Add(this.stateManager.RemoveAsync(Constants.InstanceStoreDictionaryName));
+ }
+
+ return Task.WhenAll(tasks);
+ }
+
+ public bool IsMaxMessageCountExceeded(int currentMessageCount, OrchestrationRuntimeState runtimeState)
+ {
+ return false;
+ }
+
+ public int GetDelayInSecondsAfterOnProcessException(Exception exception)
+ {
+ return GetDelayForFetchOrProcessException(exception);
+ }
+
+ public int GetDelayInSecondsAfterOnFetchException(Exception exception)
+ {
+ return GetDelayForFetchOrProcessException(exception);
+ }
+
+ public int TaskOrchestrationDispatcherCount => this.settings.TaskOrchestrationDispatcherSettings.DispatcherCount;
+ public int MaxConcurrentTaskOrchestrationWorkItems => this.settings.TaskOrchestrationDispatcherSettings.MaxConcurrentOrchestrations;
+
+ // Note: Do not rely on cancellationToken parameter to this method because the top layer does not yet implement any cancellation.
+ public async Task LockNextTaskOrchestrationWorkItemAsync(TimeSpan receiveTimeout, CancellationToken cancellationToken)
+ {
+ var currentSession = await this.orchestrationProvider.AcceptSessionAsync(receiveTimeout);
+
+ if (currentSession == null)
+ {
+ return null;
+ }
+
+ List> newMessages;
+ try
+ {
+ newMessages = await this.orchestrationProvider.ReceiveSessionMessagesAsync(currentSession);
+
+ var currentRuntimeState = new OrchestrationRuntimeState(currentSession.SessionState);
+ var workItem = new TaskOrchestrationWorkItem()
+ {
+ NewMessages = newMessages.Select(m => m.Value.TaskMessage).ToList(),
+ InstanceId = currentSession.SessionId.InstanceId,
+ OrchestrationRuntimeState = currentRuntimeState
+ };
+
+ if (newMessages.Count == 0)
+ {
+ if (currentRuntimeState.ExecutionStartedEvent == null)
+ {
+ ServiceFabricProviderEventSource.Tracing.UnexpectedCodeCondition($"Orchestration with no execution started event found: {currentSession.SessionId}");
+ return null;
+ }
+
+ bool isComplete = currentRuntimeState.OrchestrationStatus.IsTerminalState();
+ if (isComplete)
+ {
+ await this.HandleCompletedOrchestration(workItem);
+ }
+
+ this.orchestrationProvider.TryUnlockSession(currentSession.SessionId, isComplete: isComplete);
+ return null;
+ }
+
+ var sessionInfo = new SessionInformation()
+ {
+ Instance = currentSession.SessionId,
+ LockTokens = newMessages.Select(m => m.Key).ToList()
+ };
+
+ if (!this.sessionInfos.TryAdd(workItem.InstanceId, sessionInfo))
+ {
+ ServiceFabricProviderEventSource.Tracing.UnexpectedCodeCondition($"{nameof(FabricOrchestrationService)}.{nameof(LockNextTaskOrchestrationWorkItemAsync)} : Multiple receivers processing the same session : {currentSession.SessionId.InstanceId}?");
+ }
+
+ return workItem;
+ }
+ catch (Exception)
+ {
+ this.orchestrationProvider.TryUnlockSession(currentSession.SessionId, abandon: true);
+ throw;
+ }
+ }
+
+ public Task RenewTaskOrchestrationWorkItemLockAsync(TaskOrchestrationWorkItem workItem)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task CompleteTaskOrchestrationWorkItemAsync(
+ TaskOrchestrationWorkItem workItem,
+ OrchestrationRuntimeState newOrchestrationRuntimeState,
+ IList outboundMessages,
+ IList orchestratorMessages,
+ IList timerMessages,
+ TaskMessage continuedAsNewMessage,
+ OrchestrationState orchestrationState)
+ {
+ SessionInformation sessionInfo = GetSessionInfo(workItem.InstanceId);
+
+ if (continuedAsNewMessage != null)
+ {
+ throw new Exception("ContinueAsNew is not supported yet");
+ }
+
+ bool isComplete = workItem.OrchestrationRuntimeState.OrchestrationStatus.IsTerminalState();
+
+ IList sessionsToEnqueue = null;
+ List> scheduledMessages = null;
+ List> activityMessages = null;
+
+ await RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ bool retryOnException;
+ do
+ {
+ try
+ {
+ retryOnException = false;
+ sessionsToEnqueue = null;
+ scheduledMessages = null;
+ activityMessages = null;
+
+ using (var txn = this.stateManager.CreateTransaction())
+ {
+ if (outboundMessages?.Count > 0)
+ {
+ activityMessages = outboundMessages.Select(m => new Message(Guid.NewGuid().ToString(), new TaskMessageItem(m))).ToList();
+ await this.activitiesProvider.SendBatchBeginAsync(txn, activityMessages);
+ }
+
+ if (timerMessages?.Count > 0)
+ {
+ scheduledMessages = timerMessages.Select(m => new Message(Guid.NewGuid(), new TaskMessageItem(m))).ToList();
+ await this.scheduledMessagesProvider.SendBatchBeginAsync(txn, scheduledMessages);
+ }
+
+ if (orchestratorMessages?.Count > 0)
+ {
+ if (workItem.OrchestrationRuntimeState?.ParentInstance != null)
+ {
+ sessionsToEnqueue = await this.orchestrationProvider.TryAppendMessageBatchAsync(txn, orchestratorMessages.Select(tm => new TaskMessageItem(tm)));
+ }
+ else
+ {
+ await this.orchestrationProvider.AppendMessageBatchAsync(txn, orchestratorMessages.Select(tm => new TaskMessageItem(tm)));
+ sessionsToEnqueue = orchestratorMessages.Select(m => m.OrchestrationInstance).ToList();
+ }
+ }
+
+ await this.orchestrationProvider.CompleteMessages(txn, sessionInfo.Instance, sessionInfo.LockTokens);
+
+ // When an orchestration is completed, we need to drop the session which involves 2 steps (1) Removing the row from sessions
+ // (2) Dropping the session messages dictionary. The second step is done in background thread for performance so is not
+ // part of transaction. Since it will happen outside the trasanction, if this transaction fails for some reason and we dropped
+ // the session as part of this transaction, we wouldn't have updated the session state but would have lost the messages
+ // in the session messages dictionary which are needed for state to reach complete state (when the orchestration is picked up again in next fetch).
+ // So we don't want to drop session as part of this transaction.
+ // Instead, we drop the session as part of a subsequent different transaction.
+ // However, framework passes us 'null' value for 'newOrchestrationRuntimeState' when orchestration is completed and
+ // if we updated the session state to null and this transaction succeded, and a node failures occurs and we
+ // never call the subsequent transaction, we will lose the runtime state of orchestration and never will be able to
+ // mark it as complete even if it is. So we use the work item's runtime state when 'newOrchestrationRuntimeState' is null
+ // so that the latest state is what is stored for the session.
+ // As part of next transaction, we are going to remove the row anyway for the session and it doesn't matter to update it to 'null'.
+ await this.orchestrationProvider.UpdateSessionState(txn, sessionInfo.Instance, newOrchestrationRuntimeState ?? workItem.OrchestrationRuntimeState);
+
+ // We skip writing to instanceStore when orchestration reached terminal state to avoid a minor timing issue that
+ // wait for an orchestration completes but another orchestration with the same name cannot be started immediately
+ // because the session is still in store. We update the instance store on orchestration completion and drop the
+ // session as part of the next atomic transaction.
+ if (this.instanceStore != null && orchestrationState != null && !isComplete)
+ {
+ await this.instanceStore.WriteEntitiesAsync(txn, new InstanceEntityBase[]
+ {
+ new OrchestrationStateInstanceEntity()
+ {
+ State = orchestrationState
+ }
+ });
+ }
+ await txn.CommitAsync();
+ }
+ }
+ catch (FabricReplicationOperationTooLargeException ex)
+ {
+ ServiceFabricProviderEventSource.Tracing.ExceptionInReliableCollectionOperations($"OrchestrationInstance = {sessionInfo.Instance}, Action = {nameof(CompleteTaskOrchestrationWorkItemAsync)}", ex.ToString());
+ retryOnException = true;
+ newOrchestrationRuntimeState = null;
+ outboundMessages = null;
+ timerMessages = null;
+ orchestratorMessages = null;
+ if (orchestrationState != null)
+ {
+ orchestrationState.OrchestrationStatus = OrchestrationStatus.Failed;
+ orchestrationState.Output = $"Fabric exception when trying to process orchestration: {ex}. Investigate and consider reducing the serialization size of orchestration inputs/outputs/overall length to avoid the issue.";
+ }
+ }
+ } while (retryOnException);
+ }, uniqueActionIdentifier: $"OrchestrationId = '{workItem.InstanceId}', Action = '{nameof(CompleteTaskOrchestrationWorkItemAsync)}'");
+
+ if (activityMessages != null)
+ {
+ this.activitiesProvider.SendBatchComplete(activityMessages);
+ }
+ if (scheduledMessages != null)
+ {
+ this.scheduledMessagesProvider.SendBatchComplete(scheduledMessages);
+ }
+ if (sessionsToEnqueue != null)
+ {
+ foreach (var instance in sessionsToEnqueue)
+ {
+ this.orchestrationProvider.TryEnqueueSession(instance);
+ }
+ }
+
+ if (isComplete)
+ {
+ await HandleCompletedOrchestration(workItem);
+ }
+ }
+
+ // Caller should ensure the workItem has reached terminal state.
+ private async Task HandleCompletedOrchestration(TaskOrchestrationWorkItem workItem)
+ {
+ await RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ using (var txn = this.stateManager.CreateTransaction())
+ {
+ await this.instanceStore.WriteEntitiesAsync(txn, new InstanceEntityBase[]
+ {
+ new OrchestrationStateInstanceEntity()
+ {
+ State = Utils.BuildOrchestrationState(workItem.OrchestrationRuntimeState)
+ }
+ });
+ // DropSession does 2 things (like mentioned in the comments above) - remove the row from sessions dictionary
+ // and delete the session messages dictionary. The second step is in a background thread and not part of transaction.
+ // However even if this transaction failed but we ended up deleting session messages dictionary, that's ok - at
+ // that time, it should be an empty dictionary and we would have updated the runtime session state to full completed
+ // state in the transaction from Complete method. So the subsequent attempt would be able to complete the session.
+ await this.orchestrationProvider.DropSession(txn, workItem.OrchestrationRuntimeState.OrchestrationInstance);
+ await txn.CommitAsync();
+ }
+ }, uniqueActionIdentifier: $"OrchestrationId = '{workItem.InstanceId}', Action = '{nameof(HandleCompletedOrchestration)}'");
+
+ this.instanceStore.OnOrchestrationCompleted(workItem.OrchestrationRuntimeState.OrchestrationInstance);
+
+ string message = string.Format("Orchestration with instanceId : '{0}' and executionId : '{1}' Finished with the status {2} and result {3} in {4} seconds.",
+ workItem.InstanceId,
+ workItem.OrchestrationRuntimeState.OrchestrationInstance.ExecutionId,
+ workItem.OrchestrationRuntimeState.OrchestrationStatus.ToString(),
+ workItem.OrchestrationRuntimeState.Output,
+ (workItem.OrchestrationRuntimeState.CompletedTime - workItem.OrchestrationRuntimeState.CreatedTime).TotalSeconds);
+ ServiceFabricProviderEventSource.Tracing.LogOrchestrationInformation(workItem.InstanceId,
+ workItem.OrchestrationRuntimeState.OrchestrationInstance.ExecutionId,
+ message);
+ }
+
+ public Task AbandonTaskOrchestrationWorkItemAsync(TaskOrchestrationWorkItem workItem)
+ {
+ SessionInformation sessionInfo = TryRemoveSessionInfo(workItem.InstanceId);
+ if (sessionInfo == null)
+ {
+ ServiceFabricProviderEventSource.Tracing.UnexpectedCodeCondition($"{nameof(AbandonTaskOrchestrationWorkItemAsync)} : Could not get a session info object while trying to abandon session {workItem.InstanceId}");
+ }
+ else
+ {
+ this.orchestrationProvider.TryUnlockSession(sessionInfo.Instance, abandon: true);
+ }
+ return Task.CompletedTask;
+ }
+
+ public Task ReleaseTaskOrchestrationWorkItemAsync(TaskOrchestrationWorkItem workItem)
+ {
+ bool isComplete = workItem.OrchestrationRuntimeState.OrchestrationStatus.IsTerminalState();
+
+ SessionInformation sessionInfo = TryRemoveSessionInfo(workItem.InstanceId);
+ if (sessionInfo != null)
+ {
+ this.orchestrationProvider.TryUnlockSession(sessionInfo.Instance, isComplete: isComplete);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public int TaskActivityDispatcherCount => this.settings.TaskActivityDispatcherSettings.DispatcherCount;
+ public int MaxConcurrentTaskActivityWorkItems => this.settings.TaskActivityDispatcherSettings.MaxConcurrentActivities;
+
+ public BehaviorOnContinueAsNew EventBehaviourForContinueAsNew { get; }
+
+ // Note: Do not rely on cancellationToken parameter to this method because the top layer does not yet implement any cancellation.
+ public async Task LockNextTaskActivityWorkItem(TimeSpan receiveTimeout, CancellationToken cancellationToken)
+ {
+ var message = await this.activitiesProvider.ReceiveAsync(receiveTimeout);
+
+ if (message != null)
+ {
+ return new TaskActivityWorkItem()
+ {
+ Id = message.Key,
+ TaskMessage = message.Value.TaskMessage
+ };
+ }
+
+ return null;
+ }
+
+ public async Task CompleteTaskActivityWorkItemAsync(TaskActivityWorkItem workItem, TaskMessage responseMessage)
+ {
+ bool added = false;
+
+ await RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ bool retryOnException;
+ do
+ {
+ try
+ {
+ added = false;
+ retryOnException = false;
+
+ using (var txn = this.stateManager.CreateTransaction())
+ {
+ await this.activitiesProvider.CompleteAsync(txn, workItem.Id);
+ added = await this.orchestrationProvider.TryAppendMessageAsync(txn, new TaskMessageItem(responseMessage));
+ await txn.CommitAsync();
+ }
+ }
+ catch (FabricReplicationOperationTooLargeException ex)
+ {
+ ServiceFabricProviderEventSource.Tracing.ExceptionInReliableCollectionOperations($"OrchestrationInstance = {responseMessage.OrchestrationInstance}, ActivityId = {workItem.Id}, Action = {nameof(CompleteTaskActivityWorkItemAsync)}", ex.ToString());
+ retryOnException = true;
+ var originalEvent = responseMessage.Event;
+ int taskScheduledId = GetTaskScheduledId(originalEvent);
+ string details = $"Fabric exception when trying to save activity result: {ex}. Consider reducing the serialization size of activity result to avoid the issue.";
+ responseMessage.Event = new TaskFailedEvent(originalEvent.EventId, taskScheduledId, ex.Message, details);
+ }
+ } while (retryOnException);
+ }, uniqueActionIdentifier: $"Orchestration = '{responseMessage.OrchestrationInstance}', ActivityId = '{workItem.Id}', Action = '{nameof(CompleteTaskActivityWorkItemAsync)}'");
+
+ if (added)
+ {
+ this.orchestrationProvider.TryEnqueueSession(responseMessage.OrchestrationInstance);
+ }
+ }
+
+ public Task AbandonTaskActivityWorkItemAsync(TaskActivityWorkItem workItem)
+ {
+ this.activitiesProvider.Abandon(workItem.Id);
+ return Task.CompletedTask;
+ }
+
+ public Task RenewTaskActivityWorkItemLockAsync(TaskActivityWorkItem workItem)
+ {
+ return Task.FromResult(workItem);
+ }
+
+ int GetTaskScheduledId(HistoryEvent historyEvent)
+ {
+ TaskCompletedEvent tce = historyEvent as TaskCompletedEvent;
+ if (tce != null)
+ {
+ return tce.TaskScheduledId;
+ }
+
+ TaskFailedEvent tfe = historyEvent as TaskFailedEvent;
+ if (tfe != null)
+ {
+ return tfe.TaskScheduledId;
+ }
+
+ return -1;
+ }
+
+ int GetDelayForFetchOrProcessException(Exception exception)
+ {
+ //Todo: Need to fine tune
+ if (exception is TimeoutException)
+ {
+ return 1;
+ }
+
+ if (exception is FabricNotReadableException)
+ {
+ return 2;
+ }
+
+ return 0;
+ }
+
+ SessionInformation GetSessionInfo(string sessionId)
+ {
+ ServiceFabricProviderEventSource.Tracing.TraceMessage(sessionId, $"{nameof(GetSessionInfo)} - Getting session info");
+ if (!this.sessionInfos.TryGetValue(sessionId, out SessionInformation sessionInfo))
+ {
+ var message = $"{nameof(GetSessionInfo)}. Trying to get a session that's not in locked sessions {sessionId}";
+ ServiceFabricProviderEventSource.Tracing.UnexpectedCodeCondition(message);
+ throw new Exception(message);
+ }
+
+ return sessionInfo;
+ }
+
+ SessionInformation TryRemoveSessionInfo(string sessionId)
+ {
+ var removed = this.sessionInfos.TryRemove(sessionId, out SessionInformation sessionInfo);
+ ServiceFabricProviderEventSource.Tracing.TraceMessage(sessionId, $"{nameof(TryRemoveSessionInfo)}: Removed = {removed}");
+ return sessionInfo;
+ }
+
+ class SessionInformation
+ {
+ public OrchestrationInstance Instance { get; set; }
+
+ public List LockTokens { get; set; }
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/FabricOrchestrationServiceClient.cs b/src/DurableTask.AzureServiceFabric/FabricOrchestrationServiceClient.cs
new file mode 100644
index 000000000..9cc365b35
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/FabricOrchestrationServiceClient.cs
@@ -0,0 +1,210 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.Core.Exceptions;
+ using DurableTask.Core.History;
+ using DurableTask.Core.Tracking;
+ using DurableTask.AzureServiceFabric.Stores;
+ using DurableTask.AzureServiceFabric.TaskHelpers;
+ using DurableTask.AzureServiceFabric.Tracing;
+
+ using Microsoft.ServiceFabric.Data;
+
+ using Newtonsoft.Json;
+
+ class FabricOrchestrationServiceClient : IOrchestrationServiceClient
+ {
+ readonly IReliableStateManager stateManager;
+ readonly IFabricOrchestrationServiceInstanceStore instanceStore;
+ readonly SessionProvider orchestrationProvider;
+
+ public FabricOrchestrationServiceClient(IReliableStateManager stateManager, SessionProvider orchestrationProvider, IFabricOrchestrationServiceInstanceStore instanceStore)
+ {
+ this.stateManager = stateManager ?? throw new ArgumentNullException(nameof(stateManager));
+ this.orchestrationProvider = orchestrationProvider ?? throw new ArgumentNullException(nameof(orchestrationProvider));
+ this.instanceStore = instanceStore ?? throw new ArgumentNullException(nameof(instanceStore));
+ }
+
+ #region IOrchestrationServiceClient
+ public async Task CreateTaskOrchestrationAsync(TaskMessage creationMessage)
+ {
+ creationMessage.OrchestrationInstance.InstanceId.EnsureValidInstanceId();
+ ExecutionStartedEvent startEvent = creationMessage.Event as ExecutionStartedEvent;
+ if (startEvent == null)
+ {
+ await this.SendTaskOrchestrationMessageAsync(creationMessage);
+ return;
+ }
+
+ var instance = creationMessage.OrchestrationInstance;
+
+ var added = await RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ using (var tx = this.stateManager.CreateTransaction())
+ {
+ if (await this.orchestrationProvider.TryAddSession(tx, new TaskMessageItem(creationMessage)))
+ {
+ await WriteExecutionStartedEventToInstanceStore(tx, startEvent);
+ await tx.CommitAsync();
+ return true;
+ }
+
+ return false;
+ }
+ }, uniqueActionIdentifier: $"Orchestration = '{instance}', Action = '{nameof(CreateTaskOrchestrationAsync)}'");
+
+ if (added)
+ {
+ string message = string.Format("Orchestration with instanceId : '{0}' and executionId : '{1}' is Created.", instance.InstanceId, instance.ExecutionId);
+ ServiceFabricProviderEventSource.Tracing.LogOrchestrationInformation(instance.InstanceId, instance.ExecutionId, message);
+ this.orchestrationProvider.TryEnqueueSession(creationMessage.OrchestrationInstance);
+ }
+ else
+ {
+ throw new OrchestrationAlreadyExistsException($"An orchestration with id '{creationMessage.OrchestrationInstance.InstanceId}' is already running.");
+ }
+ }
+
+ public Task CreateTaskOrchestrationAsync(TaskMessage creationMessage, OrchestrationStatus[] dedupeStatuses)
+ {
+ // Todo: Support for dedupeStatuses?
+ if (dedupeStatuses != null)
+ {
+ throw new NotSupportedException($"DedupeStatuses are not supported yet with service fabric provider");
+ }
+
+ creationMessage.OrchestrationInstance.InstanceId.EnsureValidInstanceId();
+
+ return CreateTaskOrchestrationAsync(creationMessage);
+ }
+
+ public Task SendTaskOrchestrationMessageAsync(TaskMessage message)
+ {
+ message.OrchestrationInstance.InstanceId.EnsureValidInstanceId();
+ return this.orchestrationProvider.AppendMessageAsync(new TaskMessageItem(message));
+ }
+
+ public async Task SendTaskOrchestrationMessageBatchAsync(params TaskMessage[] messages)
+ {
+ foreach(var message in messages)
+ {
+ message.OrchestrationInstance.InstanceId.EnsureValidInstanceId();
+ await this.SendTaskOrchestrationMessageAsync(message);
+ }
+ }
+
+ public async Task ForceTerminateTaskOrchestrationAsync(string instanceId, string reason)
+ {
+ instanceId.EnsureValidInstanceId();
+ var latestExecutionId = (await this.instanceStore.GetExecutionIds(instanceId)).Last();
+
+ if (latestExecutionId == null)
+ {
+ throw new ArgumentException($"No execution id found for given instanceId {instanceId}, can only terminate the latest execution of a given orchestration");
+ }
+
+ var taskMessage = new TaskMessage
+ {
+ OrchestrationInstance = new OrchestrationInstance { InstanceId = instanceId, ExecutionId = latestExecutionId },
+ Event = new ExecutionTerminatedEvent(-1, reason)
+ };
+
+ await SendTaskOrchestrationMessageAsync(taskMessage);
+ }
+
+ public async Task> GetOrchestrationStateAsync(string instanceId, bool allExecutions)
+ {
+ instanceId.EnsureValidInstanceId();
+ var stateInstances = await this.instanceStore.GetOrchestrationStateAsync(instanceId, allExecutions);
+
+ var result = new List();
+ foreach(var stateInstance in stateInstances)
+ {
+ if (stateInstance != null)
+ {
+ result.Add(stateInstance.State);
+ }
+ }
+ return result;
+ }
+
+ public async Task GetOrchestrationStateAsync(string instanceId, string executionId)
+ {
+ instanceId.EnsureValidInstanceId();
+ var stateInstance = await this.instanceStore.GetOrchestrationStateAsync(instanceId, executionId);
+ return stateInstance?.State;
+ }
+
+ public Task GetOrchestrationHistoryAsync(string instanceId, string executionId)
+ {
+ instanceId.EnsureValidInstanceId();
+
+ // Other implementations returns full history for the execution.
+ // This implementation returns just the final history, i.e., state.
+ var result = JsonConvert.SerializeObject(this.instanceStore.GetOrchestrationStateAsync(instanceId, executionId));
+ return Task.FromResult(result);
+ }
+
+ public Task PurgeOrchestrationHistoryAsync(DateTime thresholdDateTimeUtc, OrchestrationStateTimeRangeFilterType timeRangeFilterType)
+ {
+ if (timeRangeFilterType != OrchestrationStateTimeRangeFilterType.OrchestrationCompletedTimeFilter)
+ {
+ throw new NotSupportedException("Purging is supported only for Orchestration completed time filter.");
+ }
+
+ return this.instanceStore.PurgeOrchestrationHistoryEventsAsync(thresholdDateTimeUtc);
+ }
+
+ public async Task WaitForOrchestrationAsync(string instanceId, string executionId, TimeSpan timeout, CancellationToken cancellationToken)
+ {
+ instanceId.EnsureValidInstanceId();
+ var instance = new OrchestrationInstance() { InstanceId = instanceId, ExecutionId = executionId };
+ var state = await this.instanceStore.WaitForOrchestrationAsync(instance, timeout);
+ return state?.State;
+ }
+ #endregion
+
+ Task WriteExecutionStartedEventToInstanceStore(ITransaction tx, ExecutionStartedEvent startEvent)
+ {
+ var createdTime = DateTime.UtcNow;
+ var initialState = new OrchestrationState()
+ {
+ Name = startEvent.Name,
+ Version = startEvent.Version,
+ OrchestrationInstance = startEvent.OrchestrationInstance,
+ OrchestrationStatus = OrchestrationStatus.Pending,
+ Input = startEvent.Input,
+ Tags = startEvent.Tags,
+ CreatedTime = createdTime,
+ LastUpdatedTime = createdTime
+ };
+
+ return this.instanceStore.WriteEntitiesAsync(tx, new InstanceEntityBase[]
+ {
+ new OrchestrationStateInstanceEntity()
+ {
+ State = initialState
+ }
+ });
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/FabricOrchestrationServiceController.cs b/src/DurableTask.AzureServiceFabric/FabricOrchestrationServiceController.cs
new file mode 100644
index 000000000..859dcfd91
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/FabricOrchestrationServiceController.cs
@@ -0,0 +1,184 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+ using System.Web.Http;
+ using System.Web.Http.Results;
+
+ using DurableTask.Core;
+ using DurableTask.Core.Exceptions;
+ using DurableTask.AzureServiceFabric.Models;
+ using DurableTask.AzureServiceFabric.Tracing;
+
+ ///
+ /// A Web Api controller that provides TaskHubClient operations.
+ ///
+ public class FabricOrchestrationServiceController : ApiController
+ {
+ private readonly IOrchestrationServiceClient orchestrationServiceClient;
+
+ ///
+ /// Creates an instance of FabricOrchestrationServiceController for given OrchestrationServiceClient
+ ///
+ /// IOrchestrationServiceClient instance
+ public FabricOrchestrationServiceController(IOrchestrationServiceClient orchestrationServiceClient)
+ {
+ this.orchestrationServiceClient = orchestrationServiceClient;
+ }
+
+ ///
+ /// Creates a task orchestration.
+ ///
+ /// Orchestration Id
+ /// Parameters for creating task orchestration.
+ /// Will throw an OrchestrationAlreadyExistsException exception
+ /// If any orchestration with the same instance Id exists in the instance store and it has a status specified in dedupeStatuses.
+ /// object.
+ [HttpPut]
+ [Route("orchestrations/{orchestrationId}")]
+ public async Task CreateTaskOrchestration([FromUri] string orchestrationId, [FromBody] CreateTaskOrchestrationParameters parameters)
+ {
+ parameters.TaskMessage.OrchestrationInstance.InstanceId.EnsureValidInstanceId();
+ if (!orchestrationId.Equals(parameters.TaskMessage.OrchestrationInstance.InstanceId))
+ {
+ return BadRequest($"OrchestrationId from Uri {orchestrationId} doesn't match with the one from body {parameters.TaskMessage.OrchestrationInstance.InstanceId}");
+ }
+ try
+ {
+ if (parameters.DedupeStatuses == null)
+ {
+ await this.orchestrationServiceClient.CreateTaskOrchestrationAsync(parameters.TaskMessage);
+ }
+ else
+ {
+ await this.orchestrationServiceClient.CreateTaskOrchestrationAsync(parameters.TaskMessage, parameters.DedupeStatuses);
+ }
+
+ return new OkResult(this);
+ }
+ catch (OrchestrationAlreadyExistsException)
+ {
+ return Conflict();
+ }
+ }
+
+ ///
+ /// Sends an orchestration message to TaskHubClient.
+ ///
+ ///
+ /// Message to send
+ /// object.
+ [HttpPost]
+ [Route("messages/{messageId}")]
+ public async Task SendTaskOrchestrationMessage([FromUri]long messageId,[FromBody] TaskMessage message)
+ {
+ if (messageId != message.SequenceNumber)
+ {
+ return Conflict();
+ }
+
+ message.OrchestrationInstance.InstanceId.EnsureValidInstanceId();
+ await this.orchestrationServiceClient.SendTaskOrchestrationMessageAsync(message);
+ return new OkResult(this);
+ }
+
+ ///
+ /// Sends an array of orchestration messages to TaskHubClient.
+ ///
+ /// Message to send
+ /// object.
+ [HttpPost]
+ [Route("messages")]
+ public async Task SendTaskOrchestrationMessageBatch([FromBody] TaskMessage[] messages)
+ {
+ await this.orchestrationServiceClient.SendTaskOrchestrationMessageBatchAsync(messages);
+ return new OkResult(this);
+ }
+
+ ///
+ /// Gets the state of orchestration.
+ ///
+ /// Instance id of the orchestration
+ /// Execution id of the orchestration
+ /// object.
+ [HttpGet]
+ [Route("orchestrations/{orchestrationId}")]
+ public async Task GetOrchestrationState([FromUri]string orchestrationId, string executionId)
+ {
+ orchestrationId.EnsureValidInstanceId();
+ var state = await this.orchestrationServiceClient.GetOrchestrationStateAsync(orchestrationId, executionId);
+ return state;
+ }
+
+ ///
+ /// Gets the state of orchestration.
+ ///
+ /// Instance id of the orchestration
+ /// True if method should fetch all executions of the instance, false if the method should only fetch the most recent execution
+ /// List of .
+ [HttpGet]
+ [Route("orchestrations/{orchestrationId}")]
+ public async Task> GetOrchestrationState([FromUri]string orchestrationId, bool allExecutions)
+ {
+ orchestrationId.EnsureValidInstanceId();
+ var state = await this.orchestrationServiceClient.GetOrchestrationStateAsync(orchestrationId, allExecutions);
+ return state;
+ }
+
+ ///
+ /// Terminates an orchestration.
+ ///
+ /// Instance id of the orchestration
+ /// Execution id of the orchestration
+ /// object.
+ [HttpDelete]
+ [Route("orchestrations/{orchestrationId}")]
+ public async Task ForceTerminateTaskOrchestration([FromUri]string orchestrationId, string reason)
+ {
+ orchestrationId.EnsureValidInstanceId();
+ await this.orchestrationServiceClient.ForceTerminateTaskOrchestrationAsync(orchestrationId, reason);
+ return new OkResult(this);
+ }
+
+ ///
+ /// Gets the history of orchestration.
+ ///
+ /// Instance id of the orchestration
+ /// Execution id of the orchestration
+ /// Orchestration history
+ [HttpGet]
+ [Route("history/{orchestrationId}")]
+ public async Task GetOrchestrationHistory([FromUri]string orchestrationId, string executionId)
+ {
+ orchestrationId.EnsureValidInstanceId();
+ var result = await this.orchestrationServiceClient.GetOrchestrationHistoryAsync(orchestrationId, executionId);
+ return result;
+ }
+
+ ///
+ /// Purges orchestration instance state and history for orchestrations older than the specified threshold time.
+ ///
+ /// Purge history parameters
+ /// object.
+ [HttpPost]
+ [Route("history")]
+ public async Task PurgeOrchestrationHistory([FromBody]PurgeOrchestrationHistoryParameters purgeParameters)
+ {
+ await this.orchestrationServiceClient.PurgeOrchestrationHistoryAsync(purgeParameters.ThresholdDateTimeUtc, purgeParameters.TimeRangeFilterType);
+ return new OkResult(this);
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/FabricProviderClient.cs b/src/DurableTask.AzureServiceFabric/FabricProviderClient.cs
new file mode 100644
index 000000000..b5d4ce5a6
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/FabricProviderClient.cs
@@ -0,0 +1,55 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.Core.Serializing;
+ using DurableTask.AzureServiceFabric.Stores;
+ using Microsoft.ServiceFabric.Data;
+ using Newtonsoft.Json;
+
+ internal class FabricProviderClient : IFabricProviderClient
+ {
+ readonly IReliableStateManager stateManager;
+ readonly SessionProvider orchestrationProvider;
+ readonly JsonDataConverter FormattingConverter = new JsonDataConverter(new JsonSerializerSettings() { Formatting = Formatting.Indented });
+
+ public FabricProviderClient(IReliableStateManager stateManager, SessionProvider orchestrationProvider)
+ {
+ this.stateManager = stateManager ?? throw new ArgumentNullException(nameof(stateManager));
+ this.orchestrationProvider = orchestrationProvider ?? throw new ArgumentNullException(nameof(orchestrationProvider));
+ }
+
+ public async Task> GetRunningOrchestrationsAsync()
+ {
+ var sessions = await this.orchestrationProvider.GetSessions();
+ return sessions.Select(s => s.SessionId);
+ }
+
+ public async Task GetOrchestrationRuntimeStateAsync(string instanceId)
+ {
+ var session = await this.orchestrationProvider.GetSession(instanceId);
+ if (session == null)
+ {
+ throw new ArgumentException($"There is no running or pending Orchestration with the instanceId {instanceId}");
+ }
+ return FormattingConverter.Serialize(session.SessionState);
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/IFabricOrchestrationServiceInstanceStore.cs b/src/DurableTask.AzureServiceFabric/IFabricOrchestrationServiceInstanceStore.cs
new file mode 100644
index 000000000..187e88b2c
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/IFabricOrchestrationServiceInstanceStore.cs
@@ -0,0 +1,89 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.Core.Tracking;
+ using Microsoft.ServiceFabric.Data;
+
+ ///
+ /// A store for supporting orchestration service in service fabric world.
+ ///
+ internal interface IFabricOrchestrationServiceInstanceStore
+ {
+ ///
+ /// Runs initialization to prepare the instance store for use
+ ///
+ /// Flag to indicate whether the store should be recreated.
+ Task InitializeStoreAsync(bool recreate);
+
+ ///
+ /// Starts the instance store object.
+ ///
+ ///
+ Task StartAsync();
+
+ ///
+ /// Deletes instances instance store
+ ///
+ Task DeleteStoreAsync();
+
+ ///
+ /// Writes a list of history events to instance store
+ ///
+ /// Service fabric transaction in which this operation is executed.
+ /// List of history events to write
+ Task WriteEntitiesAsync(ITransaction transaction, IEnumerable entities);
+
+ ///
+ /// Gets a list of orchestration states for a given instance
+ ///
+ /// The instance id to return state for
+ /// Flag indication whether to get all history execution ids or just the most recent
+ /// List of matching orchestration states
+ Task> GetOrchestrationStateAsync(string instanceId, bool allInstances);
+
+ ///
+ /// Gets the orchestration state for a given instance and execution id
+ ///
+ /// The instance id to return state for
+ /// The execution id to return state for
+ /// The matching orchestation state or null if not found
+ Task GetOrchestrationStateAsync(string instanceId, string executionId);
+
+ ///
+ /// Gets the list of history events for a given instance and execution id
+ ///
+ /// The instance id to return history for
+ /// The execution id to return history for
+ /// List of history events
+ Task> GetOrchestrationHistoryEventsAsync(string instanceId, string executionId);
+
+ ///
+ /// Purges history from storage for given time range
+ ///
+ /// The datetime in UTC to use as the threshold for purging history
+ Task PurgeOrchestrationHistoryEventsAsync(DateTime thresholdHourlyDateTimeUtc);
+
+ void OnOrchestrationCompleted(OrchestrationInstance instance);
+
+ Task WaitForOrchestrationAsync(OrchestrationInstance instance, TimeSpan timeout);
+
+ Task> GetExecutionIds(string instanceId);
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/IFabricProviderClient.cs b/src/DurableTask.AzureServiceFabric/IFabricProviderClient.cs
new file mode 100644
index 000000000..717910645
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/IFabricProviderClient.cs
@@ -0,0 +1,45 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+
+ ///
+ /// Provides additional useful functionality which is not available through .
+ ///
+ internal interface IFabricProviderClient
+ {
+ ///
+ /// Gets all the orchestration instances which are currently running or pending.
+ ///
+ ///
+ Task> GetRunningOrchestrationsAsync();
+
+ ///
+ /// Gets runtime state of a running or pending orchestration which includes the history events.
+ ///
+ /// The
+ /// of the orchestration.
+ /// Returns serialized runtime state which includes all the history events if the
+ /// orchestration is running or pending.
+ /// The API is intended for diagnostics purpose, so current implementation returns
+ /// a formatted json serialized string which can be very large.
+ /// When the orchestration already completed or was never started.
+ Task GetOrchestrationRuntimeStateAsync(string instanceId);
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/IPartitionEndpointResolver.cs b/src/DurableTask.AzureServiceFabric/IPartitionEndpointResolver.cs
new file mode 100644
index 000000000..578ba4322
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/IPartitionEndpointResolver.cs
@@ -0,0 +1,40 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System.Collections.Generic;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ ///
+ /// Provides partition related information.
+ ///
+ public interface IPartitionEndpointResolver
+ {
+ ///
+ /// Gets end points for all the partitions.
+ ///
+ /// Token to inform when a task is cancelled.
+ /// All the end points.
+ Task> GetPartitionEndpointsAsync(CancellationToken cancellationToken);
+
+ ///
+ /// Gets partition end point for given instanceId.
+ ///
+ /// InstanceId of orchestration
+ /// Token to inform when a task is cancelled.
+ /// Partition end point
+ Task GetPartitionEndPointAsync(string instanceId, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Message.cs b/src/DurableTask.AzureServiceFabric/Message.cs
new file mode 100644
index 000000000..11269dafd
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Message.cs
@@ -0,0 +1,40 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System;
+
+ sealed class Message where TKey : IComparable, IEquatable
+ {
+ public Message(TKey key, TValue value)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ Key = key;
+ Value = value;
+ }
+
+ public TKey Key { get; private set; }
+
+ public TValue Value { get; private set; }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/OrchestrationInstanceComparer.cs b/src/DurableTask.AzureServiceFabric/OrchestrationInstanceComparer.cs
new file mode 100644
index 000000000..957ff5418
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/OrchestrationInstanceComparer.cs
@@ -0,0 +1,43 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System.Collections.Generic;
+ using DurableTask.Core;
+
+ internal class OrchestrationInstanceComparer : IEqualityComparer
+ {
+ public static readonly OrchestrationInstanceComparer Default = new OrchestrationInstanceComparer();
+
+ public bool Equals(OrchestrationInstance first, OrchestrationInstance second)
+ {
+ if (first == null || second == null)
+ {
+ return first == second;
+ }
+
+ if (string.Equals(first.InstanceId, second.InstanceId) && string.Equals(first.ExecutionId, second.ExecutionId))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ public int GetHashCode(OrchestrationInstance instance)
+ {
+ return instance.GetHashCode();
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/OrchestrationStatusExtensions.cs b/src/DurableTask.AzureServiceFabric/OrchestrationStatusExtensions.cs
new file mode 100644
index 000000000..36745c634
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/OrchestrationStatusExtensions.cs
@@ -0,0 +1,30 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using DurableTask.Core;
+
+ static class OrchestrationStatusExtensions
+ {
+ public static bool IsTerminalState(this OrchestrationStatus status)
+ {
+ return !status.IsRunningOrPending();
+ }
+
+ public static bool IsRunningOrPending(this OrchestrationStatus status)
+ {
+ return status == OrchestrationStatus.Pending || status == OrchestrationStatus.Running;
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/PersistentSession.cs b/src/DurableTask.AzureServiceFabric/PersistentSession.cs
new file mode 100644
index 000000000..c51600b0b
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/PersistentSession.cs
@@ -0,0 +1,70 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric
+{
+ using System.Collections.Generic;
+ using System.Collections.Immutable;
+ using System.Runtime.Serialization;
+ using DurableTask.Core;
+ using DurableTask.Core.History;
+
+ [DataContract]
+ sealed partial class PersistentSession : IExtensibleDataObject
+ {
+ // Note : Ideally all the properties in this class should be readonly because this
+ // class is designed to be immutable class. We use private settable properties
+ // for DataContract serialization to work - the private setters should not be used
+ // in the code within this class as that would violate the immutable design. Any
+ // method that mutates the state should return a new instance instead.
+ [DataMember]
+ public OrchestrationInstance SessionId { get; private set; }
+
+ // Note: The properties below are marked IEnumerable but not
+ // IImmutableList because DataContract serialization cannot deserialize the latter.
+ // Except for the constructor and serialization methods, rest of the code in this class
+ // should use only the public immutable list members.
+
+ ///
+ /// Do not use except in the constructor or serialization methods, use property instead.
+ ///
+ [DataMember]
+ IEnumerable sessionState { get; set; }
+
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context)
+ {
+ this.sessionState = this.sessionState.ToImmutableList();
+ }
+
+ private PersistentSession(OrchestrationInstance sessionId, IImmutableList sessionState)
+ {
+ this.SessionId = sessionId;
+ this.sessionState = sessionState ?? ImmutableList.Empty;
+ }
+
+ public static PersistentSession Create(OrchestrationInstance sessionId)
+ {
+ return Create(sessionId, null);
+ }
+
+ public static PersistentSession Create(OrchestrationInstance sessionId, IImmutableList sessionState)
+ {
+ return new PersistentSession(sessionId, sessionState);
+ }
+
+ public ImmutableList SessionState => this.sessionState.ToImmutableList();
+
+ public ExtensionDataObject ExtensionData { get; set; }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/README.md b/src/DurableTask.AzureServiceFabric/README.md
new file mode 100644
index 000000000..948190b1f
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/README.md
@@ -0,0 +1,50 @@
+# Service Fabric Reliable Collections Provider For Durable Task Framework
+
+This page outlines the current status of the custom provider for Durable Task Framework based on Service Fabric Reliable Collections.
+
+What are Reliable Collections?
+https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-reliable-services-reliable-collections
+
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
+For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
+
+One fundamental way Service Fabric provider differs from other providers from a user perspective is that:
+With all other providers, the provider is an external store relative to Task Hub worker i.e., Task hub worker runs in a cluster and provider is hosted in a different cluster elsewhere and runs on different nodes.
+However, with the Service fabric provider, the provider runs on the same cluster as Task hub worker (and Task hub client as well for now). The scale-out of task hub worker nodes should be planned with a regular service fabric partitioning model.
+
+## What has been done?
+
+* Scheduled Tasks, Timers, Sub orchestrations work!
+* Fabric based instance store has limited functionality for now!
+ * No support for history events.
+ * Only the latest state for a given Orchestration instance is stored (previous state changes are overwritten by latest). [For a period of 24 hour time window]
+ * State can be queried only for Orchestrations still running / pending OR completed within an hour ago.
+ * State of orchestration that's completed a day ago will be cleaned up.
+
+## What is pending?
+
+* ContinueAsNew needs to be implemented.
+* Providing support for Azure Storage instance store with Service Fabric based provider. (So that we can persist the state more permanantly)
+* External events should work but not tested.
+* Support for history event tracking.
+* Support for querying orchestration state across executions (only makes sense after we add support for ContinueWith)
+* No automatic lock expiry for activities / orchestrations that are fetched from store but neither completed nor abandoned. (It's not clear if this is really needed with fabric provider given task hub worker and provider run on the same node.)
+* No equivalent of dead letter support (any bad activity / session will keep forever in the system).
+
+## Development Notes
+
+There are pre-requisites for setting up this project for developing or making contributions.
+
+* Current recommended Visual Studio is 2017.
+* Service fabric SDK should be installed.
+ * If this is not installed, when opening the solution, you get a dialog saying .sfproj is not supported (This is a test application project used for functional tests to run.)
+* In visual studio, the default test architecture should be set to x64 for detecting tests properly.
+* There are 2 manual steps that must be done before running tests:
+ * A 5 node local cluster must be setup using Service Fabric Local Cluster Manager (installed by SDK).
+ * After the solution is built, the "TestFabricApplication" must be packaged using the context menu. (The packaged application will be deployed to local cluster as part of test setup)
+
+## Main Contributors
+
+* Pradeep Kadubandi
+* Shankar Reddy Sama
+* Kameswara Rao Tanneru
diff --git a/src/DurableTask.AzureServiceFabric/Remote/CreateTaskOrchestrationParameters.cs b/src/DurableTask.AzureServiceFabric/Remote/CreateTaskOrchestrationParameters.cs
new file mode 100644
index 000000000..19320300e
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Remote/CreateTaskOrchestrationParameters.cs
@@ -0,0 +1,33 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Models
+{
+ using DurableTask.Core;
+
+ ///
+ /// Task orchestration creation parameters.
+ ///
+ public class CreateTaskOrchestrationParameters
+ {
+ ///
+ /// Task message representing task orchestration.
+ ///
+ public TaskMessage TaskMessage { get; set; }
+
+ ///
+ /// States of previous orchestration executions to be considered while de-duping new orchestrations on the client.
+ ///
+ public OrchestrationStatus[] DedupeStatuses { get; set; }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Remote/PurgeOrchestrationHistoryParameters.cs b/src/DurableTask.AzureServiceFabric/Remote/PurgeOrchestrationHistoryParameters.cs
new file mode 100644
index 000000000..b8366c47e
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Remote/PurgeOrchestrationHistoryParameters.cs
@@ -0,0 +1,34 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Models
+{
+ using System;
+ using DurableTask.Core;
+
+ ///
+ /// Purging orchestration history parameters.
+ ///
+ public class PurgeOrchestrationHistoryParameters
+ {
+ ///
+ /// Starting date time for purging orchestrations.
+ ///
+ public DateTime ThresholdDateTimeUtc { get; set; }
+
+ ///
+ /// Orchestration start time range filter type.
+ ///
+ public OrchestrationStateTimeRangeFilterType TimeRangeFilterType { get; set; }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Remote/RemoteOrchestrationServiceClient.cs b/src/DurableTask.AzureServiceFabric/Remote/RemoteOrchestrationServiceClient.cs
new file mode 100644
index 000000000..7f2a883d5
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Remote/RemoteOrchestrationServiceClient.cs
@@ -0,0 +1,316 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Remote
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Net.Http;
+ using System.Net.Http.Formatting;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Web;
+
+ using DurableTask.Core;
+ using DurableTask.Core.Exceptions;
+ using DurableTask.AzureServiceFabric.Exceptions;
+ using DurableTask.AzureServiceFabric.Models;
+
+ using Newtonsoft.Json;
+ using Newtonsoft.Json.Linq;
+
+ ///
+ /// Allows to interact with a remote IOrchestrationServiceClient
+ ///
+ public class RemoteOrchestrationServiceClient : IOrchestrationServiceClient, IDisposable
+ {
+ private readonly IPartitionEndpointResolver partitionProvider;
+ private readonly TimeSpan pollDelay = TimeSpan.FromSeconds(10);
+ private HttpClient httpClient;
+
+ ///
+ /// Creates a new instance.
+ ///
+ public RemoteOrchestrationServiceClient(IPartitionEndpointResolver partitionProvider)
+ {
+ this.partitionProvider = partitionProvider;
+ this.httpClient = new HttpClient();
+ }
+
+ ///
+ /// HttpClient for making REST calls.
+ ///
+ public HttpClient HttpClient
+ {
+ get
+ {
+ return this.httpClient;
+ }
+ set
+ {
+ this.httpClient = value;
+ }
+ }
+
+ ///
+ /// Creates a new orchestration
+ ///
+ /// Orchestration creation message
+ /// Will throw an OrchestrationAlreadyExistsException exception If any orchestration with the same instance Id exists in the instance store.
+ ///
+ public async Task CreateTaskOrchestrationAsync(TaskMessage creationMessage)
+ {
+ creationMessage.OrchestrationInstance.InstanceId.EnsureValidInstanceId();
+ var uri = await ConstructEndpointUriAsync(creationMessage.OrchestrationInstance.InstanceId, GetOrchestrationFragment(), CancellationToken.None);
+ await this.PutJsonAsync(uri, new CreateTaskOrchestrationParameters() { TaskMessage = creationMessage });
+ }
+
+ ///
+ /// Creates a new orchestration and specifies a subset of states which should be de duplicated on in the client side
+ ///
+ /// Orchestration creation message
+ /// States of previous orchestration executions to be considered while de-duping new orchestrations on the client
+ /// Will throw an OrchestrationAlreadyExistsException exception If any orchestration with the same instance Id exists in the instance store and it has a status specified in dedupeStatuses.
+ ///
+ public async Task CreateTaskOrchestrationAsync(TaskMessage creationMessage, OrchestrationStatus[] dedupeStatuses)
+ {
+ creationMessage.OrchestrationInstance.InstanceId.EnsureValidInstanceId();
+ var uri = await ConstructEndpointUriAsync(creationMessage.OrchestrationInstance.InstanceId, GetOrchestrationFragment(creationMessage.OrchestrationInstance.InstanceId), CancellationToken.None);
+ await this.PutJsonAsync(uri, new CreateTaskOrchestrationParameters() { TaskMessage = creationMessage, DedupeStatuses = dedupeStatuses });
+ }
+
+ ///
+ /// Forcefully terminate the specified orchestration instance
+ ///
+ /// Instance to terminate
+ /// Reason for termination
+ public async Task ForceTerminateTaskOrchestrationAsync(string instanceId, string reason)
+ {
+ instanceId.EnsureValidInstanceId();
+ var uri = await ConstructEndpointUriAsync(instanceId, GetOrchestrationFragment(instanceId), CancellationToken.None);
+ var builder = new UriBuilder($"{uri}?reason={HttpUtility.UrlEncode(reason)}");
+ var response = await this.HttpClient.DeleteAsync(builder.Uri);
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new RemoteServiceException("Unable to terminate task instance", response.StatusCode);
+ }
+ }
+
+ ///
+ /// Get a string dump of the execution history of the specified orchestration instance specified execution (generation) of the specified instance
+ ///
+ /// Instance id
+ /// Execution id
+ /// String with formatted JSON representing the execution history
+ public async Task GetOrchestrationHistoryAsync(string instanceId, string executionId)
+ {
+ instanceId.EnsureValidInstanceId();
+ var uri = await ConstructEndpointUriAsync(instanceId, GetOrchestrationFragment(instanceId), CancellationToken.None);
+ var builder = new UriBuilder($"{uri}?executionId={executionId}");
+ var history = await this.HttpClient.GetStringAsync(builder.Uri);
+ return history;
+ }
+
+ ///
+ /// Get a list of orchestration states from the instance storage for the most current execution (generation) of the specified instance.
+ ///
+ /// Instance id
+ /// True if method should fetch all executions of the instance, false if the method should only fetch the most recent execution
+ /// List of OrchestrationState objects that represents the list of orchestrations in the instance store
+ public async Task> GetOrchestrationStateAsync(string instanceId, bool allExecutions)
+ {
+ instanceId.EnsureValidInstanceId();
+ var uri = await ConstructEndpointUriAsync(instanceId, GetOrchestrationFragment(instanceId), CancellationToken.None);
+ var builder = new UriBuilder($"{uri}?allExecutions={allExecutions}");
+ var stateString = await this.HttpClient.GetStringAsync(builder.Uri);
+ var states = JsonConvert.DeserializeObject>(stateString);
+ return states;
+ }
+
+ ///
+ /// Get a list of orchestration states from the instance storage for the specified execution (generation) of the specified instance.
+ ///
+ /// Instance id
+ /// Execution id
+ /// The OrchestrationState of the specified instanceId or null if not found
+ public async Task GetOrchestrationStateAsync(string instanceId, string executionId)
+ {
+ instanceId.EnsureValidInstanceId();
+ var uri = await ConstructEndpointUriAsync(instanceId, GetOrchestrationFragment(instanceId), CancellationToken.None);
+ var builder = new UriBuilder($"{uri}?executionId={executionId}");
+ var stateString = await this.HttpClient.GetStringAsync(builder.Uri);
+ var state = JsonConvert.DeserializeObject(stateString);
+ return state;
+ }
+
+ ///
+ /// Purges orchestration instance state and history for orchestrations older than the specified threshold time.
+ /// Also purges the blob storage.
+ ///
+ /// Threshold date time in UTC
+ /// What to compare the threshold date time against
+ public async Task PurgeOrchestrationHistoryAsync(DateTime thresholdDateTimeUtc, OrchestrationStateTimeRangeFilterType timeRangeFilterType)
+ {
+ List> allTasks = new List>();
+ foreach (var endpoint in await this.GetAllEndpointsAsync(CancellationToken.None))
+ {
+ var uri = $"{endpoint.ToString()}/{GetHistoryFragment()}";
+ var task = this.HttpClient.PostAsJsonAsync(uri, new PurgeOrchestrationHistoryParameters { ThresholdDateTimeUtc = thresholdDateTimeUtc, TimeRangeFilterType = timeRangeFilterType });
+ allTasks.Add(task);
+ }
+
+ var responses = await Task.WhenAll(allTasks.ToArray());
+ foreach (var response in responses)
+ {
+ response.EnsureSuccessStatusCode();
+ }
+ }
+
+ ///
+ /// Send a new message for an orchestration
+ ///
+ /// Message to send
+ ///
+ public async Task SendTaskOrchestrationMessageAsync(TaskMessage message)
+ {
+ message.OrchestrationInstance.InstanceId.EnsureValidInstanceId();
+ var uri = await ConstructEndpointUriAsync(message.OrchestrationInstance.InstanceId, GetMessageFragment(message.SequenceNumber), CancellationToken.None);
+ await this.PutJsonAsync(uri, message);
+ }
+
+ ///
+ /// Send a new set of messages for an orchestration
+ ///
+ /// Messages to send
+ ///
+ public async Task SendTaskOrchestrationMessageBatchAsync(params TaskMessage[] messages)
+ {
+ foreach (var message in messages)
+ {
+ message.OrchestrationInstance.InstanceId.EnsureValidInstanceId();
+ await SendTaskOrchestrationMessageAsync(message);
+ }
+ }
+
+ ///
+ /// Wait for an orchestration to reach any terminal state within the given timeout
+ ///
+ /// Instance id of the orchestration
+ /// Execution id of the orchestration
+ /// Maximum amount of time to wait
+ /// Task cancellation token
+ public async Task WaitForOrchestrationAsync(string instanceId, string executionId, TimeSpan timeout, CancellationToken cancellationToken)
+ {
+ instanceId.EnsureValidInstanceId();
+ var maxTime = DateTime.Now.Add(timeout);
+ OrchestrationState state = null;
+ while (!cancellationToken.IsCancellationRequested && DateTime.Now < maxTime)
+ {
+ state = await this.GetOrchestrationStateAsync(instanceId, executionId);
+ if (state != null && state.OrchestrationStatus.IsTerminalState())
+ {
+ return state;
+ }
+
+ await Task.Delay(this.pollDelay);
+ }
+
+ // Either cancellation was requested or timedout, return the last known state.
+ return state;
+ }
+
+ #region Prepare url fragments for the requests
+
+ private string GetHistoryFragment() => "history";
+
+ private string GetHistoryFragment(string orchestrationId) => $"history/{orchestrationId}";
+
+ private string GetOrchestrationFragment() => "orchestrations";
+
+ private string GetOrchestrationFragment(string orchestrationId) => $"orchestrations/{orchestrationId}";
+
+ private string GetMessageFragment() => "messages";
+
+ private string GetMessageFragment(long messageId) => $"messages/{messageId}";
+
+ #endregion Prepare url fragments for the requests
+
+ private async Task PutJsonAsync(Uri uri, object @object)
+ {
+ var mediaFormatter = new JsonMediaTypeFormatter()
+ {
+ SerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }
+ };
+
+ HttpResponseMessage result = await this.HttpClient.PutAsync(uri, @object, mediaFormatter);
+ if (result.StatusCode == System.Net.HttpStatusCode.Conflict)
+ {
+ throw new OrchestrationAlreadyExistsException("Orchestration already exists");
+ }
+
+ if (!result.IsSuccessStatusCode)
+ {
+ throw new RemoteServiceException("CreateTaskOrchestrationAsync failed", result.StatusCode);
+ }
+ }
+
+ private async Task ConstructEndpointUriAsync(string instanceId, string fragment, CancellationToken cancellationToken)
+ {
+ instanceId.EnsureValidInstanceId();
+ cancellationToken.ThrowIfCancellationRequested();
+ string endpoint = await this.partitionProvider.GetPartitionEndPointAsync(instanceId, cancellationToken);
+ string defaultEndPoint = GetDefaultEndPoint(endpoint);
+ return new Uri($"{defaultEndPoint}/{fragment}");
+ }
+
+ private async Task> GetAllEndpointsAsync(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ IEnumerable endpoints = await this.partitionProvider.GetPartitionEndpointsAsync(cancellationToken);
+ return endpoints.Select(GetDefaultEndPoint).Select(x => new Uri(x));
+ }
+
+ private string GetDefaultEndPoint(string endpoint)
+ {
+ // sample endpoint - {"Endpoints":{"":"http:\/\/10.91.42.35:30001"}}
+ var jObject = JObject.Parse(endpoint);
+ var defaultEndPoint = jObject["Endpoints"][Constants.TaskHubProxyServiceName].ToString();
+ return defaultEndPoint;
+ }
+
+ #region IDisposable Support
+ private bool disposedValue = false; // To detect redundant calls
+
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ this.httpClient.Dispose();
+ }
+
+ disposedValue = true;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+ #endregion
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Service/ActivityLoggingMessageHandler.cs b/src/DurableTask.AzureServiceFabric/Service/ActivityLoggingMessageHandler.cs
new file mode 100644
index 000000000..42e3ef8ea
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/ActivityLoggingMessageHandler.cs
@@ -0,0 +1,46 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using System;
+ using System.Net;
+ using System.Net.Http;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using DurableTask.AzureServiceFabric.Tracing;
+
+ internal class ActivityLoggingMessageHandler : DelegatingHandler
+ {
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ string requestMethod = request.Method.ToString();
+ string requestUri = request.RequestUri.AbsolutePath;
+ ServiceFabricProviderEventSource.Tracing.LogProxyServiceRequestInformation($"Proxy service incoming request {requestUri} with method {requestMethod}");
+ HttpResponseMessage response = null;
+ try
+ {
+ response = await base.SendAsync(request, cancellationToken);
+ ServiceFabricProviderEventSource.Tracing.LogProxyServiceRequestInformation($"Proxy service responding request {requestUri} with method {requestMethod}");
+ }
+ catch (Exception exception)
+ {
+ ServiceFabricProviderEventSource.Tracing.LogProxyServiceError(requestUri, requestMethod, exception);
+ response = request.CreateErrorResponse(HttpStatusCode.InternalServerError, exception.Message);
+ }
+
+ return response;
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Service/DefaultDependencyResolver.cs b/src/DurableTask.AzureServiceFabric/Service/DefaultDependencyResolver.cs
new file mode 100644
index 000000000..b90c7fbca
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/DefaultDependencyResolver.cs
@@ -0,0 +1,62 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Web.Http.Dependencies;
+
+ using Microsoft.Extensions.DependencyInjection;
+
+ ///
+ public sealed class DefaultDependencyResolver : IDependencyResolver
+ {
+ private IServiceProvider provider;
+
+ ///
+ /// Creates an instance of .
+ ///
+ /// An instance of
+ public DefaultDependencyResolver(IServiceProvider provider)
+ {
+ this.provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ }
+
+ ///
+ public object GetService(Type serviceType)
+ {
+ return provider.GetService(serviceType);
+ }
+
+ ///
+ public IEnumerable GetServices(Type serviceType)
+ {
+ return provider.GetServices(serviceType);
+ }
+
+ ///
+ public IDependencyScope BeginScope()
+ {
+ return this;
+ }
+
+ #region IDisposable Support
+ ///
+ public void Dispose()
+ {
+ // no-op
+ }
+ #endregion
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Service/DefaultStringPartitionHashing.cs b/src/DurableTask.AzureServiceFabric/Service/DefaultStringPartitionHashing.cs
new file mode 100644
index 000000000..b73804482
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/DefaultStringPartitionHashing.cs
@@ -0,0 +1,50 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using System;
+ using System.Security.Cryptography;
+ using System.Text;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ ///
+ /// Implements for string elements.
+ ///
+ public class DefaultStringPartitionHashing : IPartitionHashing
+ {
+ ///
+ public Task GeneratePartitionHashCodeAsync(string value, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ long hashCode = 0;
+ if (!string.IsNullOrEmpty(value))
+ {
+ using (var sha256 = SHA256Managed.Create())
+ {
+ var bytes = Encoding.UTF8.GetBytes(value);
+ var hash = sha256.ComputeHash(bytes);
+ var long1 = BitConverter.ToInt64(hash, 0);
+ var long2 = BitConverter.ToInt64(hash, 8);
+ var long3 = BitConverter.ToInt64(hash, 16);
+ var long4 = BitConverter.ToInt64(hash, 24);
+
+ hashCode = long1 ^ long2 ^ long3 ^ long4;
+ }
+ }
+
+ return Task.FromResult(hashCode);
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Service/FabricPartitionEndpointResolver.cs b/src/DurableTask.AzureServiceFabric/Service/FabricPartitionEndpointResolver.cs
new file mode 100644
index 000000000..787513573
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/FabricPartitionEndpointResolver.cs
@@ -0,0 +1,106 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Fabric;
+ using System.Fabric.Query;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using Microsoft.ServiceFabric.Services.Client;
+
+ ///
+ public class FabricPartitionEndpointResolver : IPartitionEndpointResolver
+ {
+ private readonly Uri serviceUri;
+ private readonly ServicePartitionResolver partitionResolver;
+ private readonly IPartitionHashing instanceIdHasher;
+
+ private TaskCompletionSource servicePartitionsTaskCompletionSource;
+ private object tcsLockObject = new object();
+
+ ///
+ /// Creates instance of .
+ ///
+ /// Fabric service uri
+ /// Instance id hasher
+ public FabricPartitionEndpointResolver(Uri serviceUri, IPartitionHashing instanceIdHasher)
+ {
+ this.serviceUri = serviceUri ?? throw new ArgumentNullException(nameof(serviceUri));
+ this.instanceIdHasher = instanceIdHasher ?? throw new ArgumentNullException(nameof(instanceIdHasher));
+ this.partitionResolver = ServicePartitionResolver.GetDefault();
+ }
+
+ ///
+ public async Task GetPartitionEndPointAsync(string instanceId, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ long hash = await this.instanceIdHasher.GeneratePartitionHashCodeAsync(instanceId, cancellationToken);
+ ResolvedServicePartition partition = await this.partitionResolver.ResolveAsync(this.serviceUri, new ServicePartitionKey(hash), cancellationToken);
+ return partition.GetEndpoint().Address;
+ }
+
+ ///
+ public async Task> GetPartitionEndpointsAsync(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var endpoints = new List();
+ foreach (Int64RangePartitionInformation partitionInfo in await this.GetServicePartitionsListAsync())
+ {
+ var resolvedPartition = await this.partitionResolver.ResolveAsync(this.serviceUri, new ServicePartitionKey(partitionInfo.LowKey), cancellationToken);
+ endpoints.Add(resolvedPartition.GetEndpoint().Address);
+ }
+
+ return endpoints;
+ }
+
+ private async Task GetServicePartitionsListAsync()
+ {
+ if (this.servicePartitionsTaskCompletionSource == null)
+ {
+ bool isNewTaskCompletionSourceCreated = false;
+ lock (tcsLockObject)
+ {
+ if (this.servicePartitionsTaskCompletionSource == null)
+ {
+ this.servicePartitionsTaskCompletionSource = new TaskCompletionSource();
+ isNewTaskCompletionSourceCreated = true;
+ }
+ }
+
+ if (isNewTaskCompletionSourceCreated)
+ {
+ using (var fabricClient = new FabricClient())
+ {
+ ServicePartitionList partitionList = await fabricClient.QueryManager.GetPartitionListAsync(this.serviceUri);
+ Int64RangePartitionInformation[] orderedPartitions = partitionList.Select(x => x.PartitionInformation)
+ .OfType()
+ .OrderBy(x => x.LowKey).ToArray();
+ if (partitionList.Count != orderedPartitions.Length)
+ {
+ throw new NotSupportedException("FabricPartitionEndpointResolver doesn't support non Int64RangePartitionInformation partitions");
+ }
+
+ this.servicePartitionsTaskCompletionSource.SetResult(orderedPartitions);
+ }
+ }
+ }
+
+ return this.servicePartitionsTaskCompletionSource.Task.Result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DurableTask.AzureServiceFabric/Service/IOwinAppBuilder.cs b/src/DurableTask.AzureServiceFabric/Service/IOwinAppBuilder.cs
new file mode 100644
index 000000000..46e36a04d
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/IOwinAppBuilder.cs
@@ -0,0 +1,36 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using Owin;
+
+ ///
+ /// Owin application startup interface.
+ ///
+ public interface IOwinAppBuilder
+ {
+ ///
+ /// Gets the listening Address.
+ ///
+ ///
+ string GetListeningAddress();
+
+
+ ///
+ /// Starts up an Owin App.
+ ///
+ /// an instance of
+ void Startup(IAppBuilder appBuilder);
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Service/IPartitionHashing.cs b/src/DurableTask.AzureServiceFabric/Service/IPartitionHashing.cs
new file mode 100644
index 000000000..560c619be
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/IPartitionHashing.cs
@@ -0,0 +1,33 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ ///
+ /// provides long hash code to identify which partition value belongs to.
+ ///
+ /// Element type
+ public interface IPartitionHashing
+ {
+ ///
+ /// Gets Long hash code.
+ ///
+ /// Input element
+ /// Token to inform when a task is cancelled.
+ /// 64 bit hash code
+ Task GeneratePartitionHashCodeAsync(T value, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Service/IServiceListener.cs b/src/DurableTask.AzureServiceFabric/Service/IServiceListener.cs
new file mode 100644
index 000000000..7ea42d19a
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/IServiceListener.cs
@@ -0,0 +1,71 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using System.Fabric;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using Microsoft.ServiceFabric.Services.Communication.Runtime;
+ using Microsoft.ServiceFabric.Services.Runtime;
+
+ ///
+ /// Represents a single listener for
+ ///
+ public interface IServiceListener
+ {
+ ///
+ /// Initialize required resources for service listener.
+ ///
+ /// An instance of which is creating this listener.
+ void Initialize(StatefulService statefulService);
+
+ ///
+ /// This method is called when the replica is being opened and it is the final step of opening the service.
+ /// Override this method to be notified that Open has completed for this replica's internal components.
+ ///
+ /// for this service replica.
+ /// Cancellation token to monitor for cancellation requests.
+ ///
+ Task OnOpenAsync(ReplicaOpenMode openMode, CancellationToken cancellationToken);
+
+ ///
+ /// Creates an instance of .
+ ///
+ /// instance of
+ ServiceReplicaListener CreateServiceReplicaListener();
+
+ ///
+ /// This method is invoked when the service is about to start.
+ ///
+ /// Cancellation token to monitor for cancellation requests.
+ ///
+ Task OnRunAsync(CancellationToken cancellationToken);
+
+ ///
+ /// This method is called when role of the replica is changing.
+ ///
+ /// New for this service replica.
+ /// Cancellation token to monitor for cancellation requests.
+ ///
+ Task OnChangeRoleAsync(ReplicaRole newRole, CancellationToken cancellationToken);
+
+ ///
+ /// This method is called as the final step of closing the service gracefully.
+ ///
+ /// Cancellation token to monitor for cancellation requests.
+ ///
+ Task OnCloseAsync(CancellationToken cancellationToken);
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Service/OwinCommunicationListener.cs b/src/DurableTask.AzureServiceFabric/Service/OwinCommunicationListener.cs
new file mode 100644
index 000000000..87a49193c
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/OwinCommunicationListener.cs
@@ -0,0 +1,105 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using System;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using DurableTask.AzureServiceFabric.Tracing;
+
+ using Microsoft.Owin.Hosting;
+ using Microsoft.ServiceFabric.Services.Communication.Runtime;
+
+ ///
+ /// Provides with support for Owin.
+ ///
+ public class OwinCommunicationListener : ICommunicationListener
+ {
+ ///
+ /// OWIN server handle.
+ ///
+ private IDisposable serverHandle;
+
+ private IOwinAppBuilder owinAppBuilder;
+
+ ///
+ ///
+ ///
+ /// Owin Application builder
+ public OwinCommunicationListener(IOwinAppBuilder owinAppBuilder)
+ {
+ this.owinAppBuilder = owinAppBuilder;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task OpenAsync(CancellationToken cancellationToken)
+ {
+ var listeningAddress = this.owinAppBuilder.GetListeningAddress();
+ ServiceFabricProviderEventSource.Tracing.TraceMessage(nameof(OwinCommunicationListener), $"Opening on {listeningAddress}");
+
+ try
+ {
+ this.serverHandle = WebApp.Start(listeningAddress, appBuilder => this.owinAppBuilder.Startup(appBuilder));
+ return Task.FromResult(listeningAddress);
+ }
+ catch (Exception ex)
+ {
+ ServiceFabricProviderEventSource.Tracing.UnexpectedCodeCondition($"{ex.Message} {ex.StackTrace} {ex.InnerException}");
+ this.StopWebServer();
+ throw;
+ }
+ }
+
+ ///
+ /// Close Owin service.
+ ///
+ ///
+ ///
+ public Task CloseAsync(CancellationToken cancellationToken)
+ {
+ ServiceFabricProviderEventSource.Tracing.TraceMessage(nameof(OwinCommunicationListener), "Listener is closing");
+ this.StopWebServer();
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Abort service.
+ ///
+ public void Abort()
+ {
+ ServiceFabricProviderEventSource.Tracing.TraceMessage(nameof(OwinCommunicationListener), "Listener is aborting");
+ this.StopWebServer();
+ }
+
+ private void StopWebServer()
+ {
+ if (this.serverHandle != null)
+ {
+ try
+ {
+ this.serverHandle.Dispose();
+ }
+ catch (ObjectDisposedException)
+ {
+ // no-op
+ }
+ }
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Service/ProxyServiceExceptionHandler.cs b/src/DurableTask.AzureServiceFabric/Service/ProxyServiceExceptionHandler.cs
new file mode 100644
index 000000000..a40e4c564
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/ProxyServiceExceptionHandler.cs
@@ -0,0 +1,35 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using System.Net;
+ using System.Net.Http;
+ using System.Web.Http.ExceptionHandling;
+ using System.Web.Http.Results;
+
+ using DurableTask.AzureServiceFabric.Tracing;
+
+ internal class ProxyServiceExceptionHandler : ExceptionHandler
+ {
+ public override void Handle(ExceptionHandlerContext context)
+ {
+ ServiceFabricProviderEventSource.Tracing.LogProxyServiceError(context.Request.Method.ToString(),
+ context.Request.RequestUri.AbsolutePath,
+ context.Exception);
+
+ HttpResponseMessage response = context.Request.CreateResponse(HttpStatusCode.InternalServerError, context.Exception);
+ context.Result = new ResponseMessageResult(response);
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Service/ProxyServiceExceptionLogger.cs b/src/DurableTask.AzureServiceFabric/Service/ProxyServiceExceptionLogger.cs
new file mode 100644
index 000000000..f0c039155
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/ProxyServiceExceptionLogger.cs
@@ -0,0 +1,34 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Web.Http.ExceptionHandling;
+
+ using DurableTask.AzureServiceFabric.Tracing;
+
+ ///
+ /// Traces application exceptions.
+ ///
+ public class ProxyServiceExceptionLogger : ExceptionLogger
+ {
+ ///
+ public async override Task LogAsync(ExceptionLoggerContext context, CancellationToken cancellationToken)
+ {
+ ServiceFabricProviderEventSource.Tracing.LogProxyServiceError(context.Request.Method.ToString(), context.Request.RequestUri.AbsolutePath, context.Exception);
+ await base.LogAsync(context, cancellationToken);
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Service/Startup.cs b/src/DurableTask.AzureServiceFabric/Service/Startup.cs
new file mode 100644
index 000000000..20ade0f5c
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/Startup.cs
@@ -0,0 +1,64 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using System;
+ using System.Net;
+ using System.Web.Http;
+ using System.Web.Http.ExceptionHandling;
+
+ using DurableTask.Core;
+ using DurableTask.AzureServiceFabric;
+ using Microsoft.Extensions.DependencyInjection;
+ using Owin;
+
+ class Startup : IOwinAppBuilder
+ {
+ FabricOrchestrationProvider fabricOrchestrationProvider;
+ string listeningAddress;
+
+ public Startup(string listeningAddress, FabricOrchestrationProvider fabricOrchestrationProvider)
+ {
+ this.listeningAddress = listeningAddress ?? throw new ArgumentNullException(nameof(listeningAddress));
+ this.fabricOrchestrationProvider = fabricOrchestrationProvider ?? throw new ArgumentNullException(nameof(fabricOrchestrationProvider));
+ }
+
+ public string GetListeningAddress()
+ {
+ return this.listeningAddress;
+ }
+
+ private ServiceProvider GenerateServiceProvider()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton(this.fabricOrchestrationProvider.OrchestrationServiceClient);
+ services.AddTransient();
+ services.AddSingleton();
+ services.AddSingleton();
+ var serviceProvider = services.BuildServiceProvider();
+ return serviceProvider;
+ }
+
+ void IOwinAppBuilder.Startup(IAppBuilder appBuilder)
+ {
+ ServicePointManager.DefaultConnectionLimit = 256;
+ HttpConfiguration config = new HttpConfiguration();
+ config.MapHttpAttributeRoutes();
+ config.DependencyResolver = new DefaultDependencyResolver(GenerateServiceProvider());
+ config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
+ config.Formatters.JsonFormatter.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.All;
+ appBuilder.UseWebApi(config);
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Service/TaskHubProxyListener.cs b/src/DurableTask.AzureServiceFabric/Service/TaskHubProxyListener.cs
new file mode 100644
index 000000000..07e0871b8
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/TaskHubProxyListener.cs
@@ -0,0 +1,188 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using System;
+ using System.Fabric;
+ using System.Globalization;
+ using System.Linq;
+ using System.Net;
+ using System.Net.Sockets;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.AzureServiceFabric;
+ using DurableTask.AzureServiceFabric.Tracing;
+
+ using Microsoft.ServiceFabric.Services.Communication.Runtime;
+ using Microsoft.ServiceFabric.Services.Runtime;
+
+ ///
+ /// Delegate invoked before starting the worker to register orchestrations.
+ ///
+ /// Instance of
+ public delegate void RegisterOrchestrations(TaskHubWorker taskHubWorker);
+
+ ///
+ /// An instance of this class is created for each service replica by the Service Fabric runtime.
+ /// Listening on HTTP port will expose security risk, use this listener at your own discretation.
+ ///
+ public sealed class TaskHubProxyListener : IServiceListener
+ {
+ readonly RegisterOrchestrations registerOrchestrations;
+ readonly FabricOrchestrationProviderSettings fabricOrchestrationProviderSettings;
+ FabricOrchestrationProviderFactory fabricProviderFactory;
+ FabricOrchestrationProvider fabricOrchestrationProvider;
+ TaskHubWorker worker;
+ ReplicaRole currentRole;
+ StatefulService statefulService;
+
+ ///
+ /// Creates instance of
+ ///
+ /// stateful service context
+ /// instance of
+ /// Delegate invoked before starting the worker.
+ public TaskHubProxyListener(StatefulServiceContext context,
+ FabricOrchestrationProviderSettings fabricOrchestrationProviderSettings,
+ RegisterOrchestrations registerOrchestrations)
+ {
+ this.fabricOrchestrationProviderSettings = fabricOrchestrationProviderSettings ?? throw new ArgumentNullException(nameof(fabricOrchestrationProviderSettings));
+ this.registerOrchestrations = registerOrchestrations ?? throw new ArgumentNullException(nameof(registerOrchestrations));
+ }
+
+ ///
+ /// Handles node's role change.
+ ///
+ /// New for this service replica.
+ /// Cancellation token to monitor for cancellation requests.
+ ///
+ /// A that represents outstanding operation.
+ ///
+ public async Task OnChangeRoleAsync(ReplicaRole newRole, CancellationToken cancellationToken)
+ {
+ ServiceFabricProviderEventSource.Tracing.LogFabricServiceInformation(this.statefulService, $"Fabric On Change Role Async, current role = {this.currentRole}, new role = {newRole}");
+ if (newRole != ReplicaRole.Primary && this.currentRole == ReplicaRole.Primary)
+ {
+ await StopAsync();
+ }
+ this.currentRole = newRole;
+ ServiceFabricProviderEventSource.Tracing.LogFabricServiceInformation(this.statefulService, $"Fabric On Change Role Async, current role = {this.currentRole}");
+ }
+
+ ///
+ /// Handles OnCloseAsync event, shuts down the service.
+ ///
+ /// Cancellation token to monitor for cancellation requests.
+ /// A Task that represents outstanding operation.
+ public async Task OnCloseAsync(CancellationToken cancellationToken)
+ {
+ ServiceFabricProviderEventSource.Tracing.LogFabricServiceInformation(this.statefulService, "OnCloseAsync - will shutdown primary if not already done");
+ await StopAsync();
+ }
+
+ ///
+ public ServiceReplicaListener CreateServiceReplicaListener()
+ {
+ return new ServiceReplicaListener(context =>
+ {
+ var serviceEndpoint = context.CodePackageActivationContext.GetEndpoint(Constants.TaskHubProxyListenerEndpointName);
+ int port = serviceEndpoint.Port;
+
+ string ipAddress = context.NodeContext.IPAddressOrFQDN;
+#if DEBUG
+ IPHostEntry entry = Dns.GetHostEntry(ipAddress);
+ IPAddress ipv4Address = entry.AddressList.FirstOrDefault(
+ address => (address.AddressFamily == AddressFamily.InterNetwork) && (!IPAddress.IsLoopback(address)));
+ ipAddress = ipv4Address.ToString();
+#endif
+
+ EnsureFabricOrchestrationProviderIsInitialized();
+ string listeningAddress = String.Format(CultureInfo.InvariantCulture, "http://{0}:{1}/{2}/dtfx/", ipAddress, port, context.PartitionId);
+ return new OwinCommunicationListener(new Startup(listeningAddress, this.fabricOrchestrationProvider));
+ }, Constants.TaskHubProxyServiceName);
+ }
+
+ ///
+ public async Task OnRunAsync(CancellationToken cancellationToken)
+ {
+ await StartAsync();
+ }
+
+ ///
+ public void Initialize(StatefulService statefulService)
+ {
+ this.statefulService = statefulService;
+ }
+
+ ///
+ public Task OnOpenAsync(ReplicaOpenMode openMode, CancellationToken cancellationToken)
+ {
+ this.fabricProviderFactory = new FabricOrchestrationProviderFactory(statefulService.StateManager, this.fabricOrchestrationProviderSettings);
+ return Task.CompletedTask;
+ }
+
+ async Task StartAsync()
+ {
+ try
+ {
+ EnsureFabricOrchestrationProviderIsInitialized();
+
+ this.worker = new TaskHubWorker(this.fabricOrchestrationProvider.OrchestrationService);
+
+ this.registerOrchestrations(this.worker);
+
+ await this.worker.StartAsync();
+
+ this.worker.TaskActivityDispatcher.IncludeDetails = true;
+ }
+ catch (Exception exception)
+ {
+ ServiceFabricProviderEventSource.Tracing.ServiceRequestFailed("RunAsync failed", $"Exception Details Type: {exception.GetType()}, Message: {exception.Message}, StackTrace: {exception.StackTrace}");
+ throw;
+ }
+ }
+
+ async Task StopAsync()
+ {
+ try
+ {
+ if (this.worker != null)
+ {
+ ServiceFabricProviderEventSource.Tracing.LogFabricServiceInformation(this.statefulService, "Stopping Taskhub Worker");
+ await this.worker.StopAsync(isForced: true);
+ this.worker.Dispose();
+ this.worker = null;
+ this.fabricOrchestrationProvider.Dispose();
+ this.fabricOrchestrationProvider = null;
+ ServiceFabricProviderEventSource.Tracing.LogFabricServiceInformation(this.statefulService, "Stopped Taskhub Worker");
+ }
+ }
+ catch (Exception e)
+ {
+ ServiceFabricProviderEventSource.Tracing.ServiceRequestFailed("Exception when Stopping Worker On Primary Stop", e.ToString());
+ throw;
+ }
+ }
+
+ private void EnsureFabricOrchestrationProviderIsInitialized()
+ {
+ if (this.fabricOrchestrationProvider == null)
+ {
+ this.fabricOrchestrationProvider = this.fabricProviderFactory.CreateProvider();
+ }
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Service/TaskHubStatefulService.cs b/src/DurableTask.AzureServiceFabric/Service/TaskHubStatefulService.cs
new file mode 100644
index 000000000..fc6a2bc47
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Service/TaskHubStatefulService.cs
@@ -0,0 +1,141 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Service
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Fabric;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using DurableTask.AzureServiceFabric.Tracing;
+
+ using Microsoft.ServiceFabric.Services.Communication.Runtime;
+ using Microsoft.ServiceFabric.Services.Runtime;
+
+ ///
+ /// An instance of this class is created for each service replica by the Service Fabric runtime.
+ ///
+ public sealed class TaskHubStatefulService : StatefulService
+ {
+ readonly IList serviceListeners;
+ ReplicaRole currentRole;
+
+ ///
+ /// Creates instance of
+ ///
+ /// stateful service context
+ /// List of
+ public TaskHubStatefulService(StatefulServiceContext context, IList serviceListeners) : base(context)
+ {
+ this.serviceListeners = serviceListeners ?? throw new ArgumentNullException(nameof(serviceListeners));
+ if (this.serviceListeners.Count < 1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(serviceListeners), "At least one service should be present");
+ }
+
+ foreach (var listener in this.serviceListeners)
+ {
+ listener.Initialize(this);
+ }
+ }
+
+ ///
+ /// This method is called when the replica is being opened and it is the final step of opening the service.
+ /// Override this method to be notified that Open has completed for this replica's internal components.
+ ///
+ /// for this service replica.
+ /// Cancellation token to monitor for cancellation requests.
+ ///
+ protected override async Task OnOpenAsync(ReplicaOpenMode openMode, CancellationToken cancellationToken)
+ {
+ foreach (var listener in this.serviceListeners)
+ {
+ await listener.OnOpenAsync(openMode, cancellationToken);
+ }
+ }
+
+ ///
+ /// Optional override to create listeners (e.g., HTTP, Service Remoting, WCF, etc.) for this service replica to handle client or user requests.
+ ///
+ ///
+ /// For more information on service communication, see http://aka.ms/servicefabricservicecommunication
+ ///
+ /// A collection of listeners.
+ protected override IEnumerable CreateServiceReplicaListeners()
+ {
+ return serviceListeners.Select(listener => listener.CreateServiceReplicaListener()).Where(listener => listener != null);
+ }
+
+ ///
+ /// This is the main entry point for your service replica.
+ /// This method executes when this replica of your service becomes primary and has write status.
+ ///
+ /// Canceled when Service Fabric needs to shut down this service replica.
+ protected override async Task RunAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ foreach(var listener in this.serviceListeners)
+ {
+ await listener.OnRunAsync(cancellationToken);
+ }
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ await Task.Delay(1000);
+ }
+ }
+ catch (Exception exception)
+ {
+ ServiceFabricProviderEventSource.Tracing.ServiceRequestFailed("RunAsync failed", $"Exception Details Type: {exception.GetType()}, Message: {exception.Message}, StackTrace: {exception.StackTrace}");
+ throw new FabricException("Exception in RunAsync", exception);
+ }
+ }
+
+ ///
+ /// Handles node's role change.
+ ///
+ /// New for this service replica.
+ /// Cancellation token to monitor for cancellation requests.
+ ///
+ /// A that represents outstanding operation.
+ ///
+ protected override async Task OnChangeRoleAsync(ReplicaRole newRole, CancellationToken cancellationToken)
+ {
+ ServiceFabricProviderEventSource.Tracing.LogFabricServiceInformation(this, $"Fabric On Change Role Async, current role = {this.currentRole}, new role = {newRole}");
+ foreach (var listener in this.serviceListeners)
+ {
+ await listener.OnChangeRoleAsync(newRole, cancellationToken);
+ }
+ this.currentRole = newRole;
+ ServiceFabricProviderEventSource.Tracing.LogFabricServiceInformation(this, $"Fabric On Change Role Async, current role = {this.currentRole}");
+ }
+
+ ///
+ /// Handles OnCloseAsync event, shuts down the service.
+ ///
+ /// Cancellation token to monitor for cancellation requests.
+ /// A Task that represents outstanding operation.
+ protected override async Task OnCloseAsync(CancellationToken cancellationToken)
+ {
+ ServiceFabricProviderEventSource.Tracing.LogFabricServiceInformation(this, "OnCloseAsync - will shutdown primary if not already done");
+ foreach (var listener in this.serviceListeners)
+ {
+ await listener.OnCloseAsync(cancellationToken);
+ }
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Stores/ActivityProvider.cs b/src/DurableTask.AzureServiceFabric/Stores/ActivityProvider.cs
new file mode 100644
index 000000000..1146af9d1
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Stores/ActivityProvider.cs
@@ -0,0 +1,68 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Stores
+{
+ using System;
+ using System.Collections.Concurrent;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using Microsoft.ServiceFabric.Data;
+
+ class ActivityProvider : MessageProviderBase
+ {
+ readonly ConcurrentQueue inMemoryQueue = new ConcurrentQueue();
+
+ public ActivityProvider(IReliableStateManager stateManager, string storeName, CancellationToken token) : base(stateManager, storeName, token)
+ {
+ }
+
+ protected override void AddItemInMemory(string key, TaskMessageItem value)
+ {
+ this.inMemoryQueue.Enqueue(key);
+ }
+
+ public async Task> ReceiveAsync(TimeSpan receiveTimeout)
+ {
+ if (!IsStopped())
+ {
+ bool newItemsBeforeTimeout = true;
+ while (newItemsBeforeTimeout)
+ {
+ if (this.inMemoryQueue.TryDequeue(out string key))
+ {
+ try
+ {
+ return await GetValueAsync(key);
+ }
+ catch (Exception)
+ {
+ this.inMemoryQueue.Enqueue(key);
+ throw;
+ }
+ }
+
+ newItemsBeforeTimeout = await WaitForItemsAsync(receiveTimeout);
+ }
+ }
+ return null;
+ }
+
+ public void Abandon(string key)
+ {
+ this.inMemoryQueue.Enqueue(key);
+ SetWaiterForNewItems();
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Stores/MessageProviderBase.cs b/src/DurableTask.AzureServiceFabric/Stores/MessageProviderBase.cs
new file mode 100644
index 000000000..dd2df57a3
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Stores/MessageProviderBase.cs
@@ -0,0 +1,198 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Stores
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Fabric;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using DurableTask.AzureServiceFabric.TaskHelpers;
+ using DurableTask.AzureServiceFabric.Tracing;
+
+ using Microsoft.ServiceFabric.Data;
+ using Microsoft.ServiceFabric.Data.Collections;
+
+ [SuppressMessage("Microsoft.Design", "CA1001", Justification = "Disposing is done through StartAsync and StopAsync")]
+ abstract class MessageProviderBase where TKey : IComparable, IEquatable
+ {
+ readonly string storeName;
+ readonly AsyncManualResetEvent waitEvent = new AsyncManualResetEvent();
+ readonly TimeSpan metricsInterval = TimeSpan.FromMinutes(1);
+
+ protected MessageProviderBase(IReliableStateManager stateManager, string storeName, CancellationToken token)
+ {
+ this.StateManager = stateManager;
+ this.storeName = storeName;
+ this.CancellationToken = token;
+ }
+
+ protected IReliableStateManager StateManager { get; }
+
+ protected CancellationToken CancellationToken { get; private set; }
+
+ protected IReliableDictionary Store { get; private set; }
+
+ public virtual async Task StartAsync()
+ {
+ await this.InitializeStore();
+ await this.EnumerateItems(kvp => this.AddItemInMemory(kvp.Key, kvp.Value));
+ Task nowait = this.LogMetrics();
+ }
+
+ protected async Task InitializeStore()
+ {
+ this.Store = await this.StateManager.GetOrAddAsync>(this.storeName);
+ }
+
+ public Task CompleteAsync(ITransaction tx, TKey key)
+ {
+ return this.Store.TryRemoveAsync(tx, key);
+ }
+
+ public async Task CompleteBatchAsync(ITransaction tx, IEnumerable keys)
+ {
+ foreach (TKey key in keys)
+ {
+ await this.Store.TryRemoveAsync(tx, key);
+ }
+ }
+
+ ///
+ /// Caller has to pass in a transaction and must call SendComplete with the same item
+ /// after the transaction commit succeeded.
+ ///
+ public Task SendBeginAsync(ITransaction tx, Message item)
+ {
+ return this.Store.TryAddAsync(tx, item.Key, item.Value);
+ }
+
+ public void SendComplete(Message item)
+ {
+ this.AddItemInMemory(item.Key, item.Value);
+ this.waitEvent.Set();
+ }
+
+ ///
+ /// Caller has to pass in a transaction and must call SendBatchComplete with the same items
+ /// after the transaction commit succeeded.
+ ///
+ public async Task SendBatchBeginAsync(ITransaction tx, IEnumerable> items)
+ {
+ foreach (Message item in items)
+ {
+ await this.Store.TryAddAsync(tx, item.Key, item.Value);
+ }
+ }
+
+ public void SendBatchComplete(IEnumerable> items)
+ {
+ foreach (Message item in items)
+ {
+ this.AddItemInMemory(item.Key, item.Value);
+ }
+ this.waitEvent.Set();
+ }
+
+ protected Task> GetValueAsync(TKey key)
+ {
+ return RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ using (var tx = this.StateManager.CreateTransaction())
+ {
+ var result = await this.Store.TryGetValueAsync(tx, key);
+ if (result.HasValue)
+ {
+ return new Message(key, result.Value);
+ }
+ var errorMessage = $"Internal Server Error: Did not find an item in reliable dictionary while having the item key {key} in memory";
+ ServiceFabricProviderEventSource.Tracing.UnexpectedCodeCondition(errorMessage);
+ throw new Exception(errorMessage);
+ }
+ }, uniqueActionIdentifier: $"Key = {key}, Action = MessageProviderBase.GetValueAsync, StoreName : {this.storeName}");
+ }
+
+ protected Task EnumerateItems(Action> itemAction)
+ {
+ return RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ using (var tx = this.StateManager.CreateTransaction())
+ {
+ var count = await this.Store.GetCountAsync(tx);
+
+ if (count > 0)
+ {
+ var enumerable = await this.Store.CreateEnumerableAsync(tx, EnumerationMode.Unordered);
+ using (var enumerator = enumerable.GetAsyncEnumerator())
+ {
+ while (await enumerator.MoveNextAsync(this.CancellationToken))
+ {
+ itemAction(enumerator.Current);
+ }
+ }
+ }
+ }
+ }, uniqueActionIdentifier: $"Action = MessageProviderBase.EnumerateItems, StoreName : {this.storeName}");
+ }
+
+ public Task EnsureStoreInitialized()
+ {
+ if (this.Store == null)
+ {
+ return this.InitializeStore();
+ }
+
+ return Task.CompletedTask;
+ }
+
+ protected Task WaitForItemsAsync(TimeSpan timeout)
+ {
+ this.waitEvent.Reset();
+ return this.waitEvent.WaitAsync(timeout, this.CancellationToken);
+ }
+
+ protected void SetWaiterForNewItems()
+ {
+ this.waitEvent.Set();
+ }
+
+ protected abstract void AddItemInMemory(TKey key, TValue value);
+
+ protected bool IsStopped()
+ {
+ return this.CancellationToken.IsCancellationRequested;
+ }
+
+ protected Task LogMetrics()
+ {
+ return Utils.RunBackgroundJob(async () =>
+ {
+ try
+ {
+ using (ITransaction tx = this.StateManager.CreateTransaction())
+ {
+ long count = await this.Store.GetCountAsync(tx);
+ ServiceFabricProviderEventSource.Tracing.LogStoreCount(this.storeName, count);
+ }
+ }
+ catch (FabricObjectClosedException)
+ {
+ ServiceFabricProviderEventSource.Tracing.ExceptionWhileRunningBackgroundJob("LogMetrics", "Fabric object is closed while running the loop action");
+ }
+ }, initialDelay: this.metricsInterval, delayOnSuccess: this.metricsInterval, delayOnException: this.metricsInterval, actionName: $"Log Store Count of {this.storeName}", token: this.CancellationToken);
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Stores/ScheduledMessageProvider.cs b/src/DurableTask.AzureServiceFabric/Stores/ScheduledMessageProvider.cs
new file mode 100644
index 000000000..9ebf2d164
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Stores/ScheduledMessageProvider.cs
@@ -0,0 +1,172 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Stores
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Immutable;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.Core.History;
+ using DurableTask.AzureServiceFabric.TaskHelpers;
+ using DurableTask.AzureServiceFabric.Tracing;
+ using Microsoft.ServiceFabric.Data;
+
+ class ScheduledMessageProvider : MessageProviderBase
+ {
+ readonly SessionProvider sessionProvider;
+ readonly object @lock = new object();
+
+ ImmutableSortedSet> inMemorySet = ImmutableSortedSet>.Empty.WithComparer(TimerFiredEventComparer.Instance);
+ DateTime nextActivationCheck;
+
+ public ScheduledMessageProvider(IReliableStateManager stateManager, string storeName, SessionProvider sessionProvider, CancellationToken token) : base(stateManager, storeName, token)
+ {
+ this.sessionProvider = sessionProvider;
+ }
+
+ public override async Task StartAsync()
+ {
+ await InitializeStore();
+
+ var builder = this.inMemorySet.ToBuilder();
+
+ await this.EnumerateItems(kvp =>
+ {
+ var timerEvent = kvp.Value?.TaskMessage?.Event as TimerFiredEvent;
+ if (timerEvent == null)
+ {
+ ServiceFabricProviderEventSource.Tracing.UnexpectedCodeCondition($"{nameof(ScheduledMessageProvider)}.{nameof(StartAsync)} : Seeing a non timer event in scheduled messages while filling the pending items collection in role start");
+ }
+ else
+ {
+ builder.Add(new Message(kvp.Key, kvp.Value));
+ }
+ });
+
+ lock (@lock)
+ {
+ if (this.inMemorySet.Count > 0)
+ {
+ ServiceFabricProviderEventSource.Tracing.UnexpectedCodeCondition($"{nameof(ScheduledMessageProvider)}.{nameof(StartAsync)} : Before we set the In memory set from the builder, there are items in it which should not happen.");
+ }
+ this.inMemorySet = builder.ToImmutableSortedSet(TimerFiredEventComparer.Instance);
+ }
+
+ var metricsTask = LogMetrics();
+ var nowait = ProcessScheduledMessages();
+ }
+
+ protected override void AddItemInMemory(Guid key, TaskMessageItem value)
+ {
+ lock (@lock)
+ {
+ this.inMemorySet = this.inMemorySet.Add(new Message(key, value));
+ }
+
+ var timerEvent = value.TaskMessage.Event as TimerFiredEvent;
+ if (timerEvent != null && timerEvent.FireAt < this.nextActivationCheck)
+ {
+ SetWaiterForNewItems();
+ }
+ }
+
+ // Since this method is started as part of StartAsync, the other stores maynot be immediately initialized
+ // by the time this invokes operations on those stores. But that would be a transient error and the next
+ // iteration of processing should take care of making things right.
+ async Task ProcessScheduledMessages()
+ {
+ while (!IsStopped())
+ {
+ try
+ {
+ var currentTime = DateTime.UtcNow;
+ var nextCheck = currentTime + TimeSpan.FromSeconds(1);
+
+ var builder = this.inMemorySet.ToBuilder();
+ List> activatedMessages = new List>();
+
+ while (builder.Count > 0)
+ {
+ var firstPendingMessage = builder.Min;
+ var timerEvent = firstPendingMessage.Value.TaskMessage.Event as TimerFiredEvent;
+
+ if (timerEvent == null)
+ {
+ throw new Exception("Internal Server Error : Ended up adding non TimerFiredEvent TaskMessage as scheduled message");
+ }
+
+ if (timerEvent.FireAt <= currentTime)
+ {
+ activatedMessages.Add(firstPendingMessage);
+ builder.Remove(firstPendingMessage);
+ }
+ else
+ {
+ nextCheck = timerEvent.FireAt;
+ break;
+ }
+ }
+
+ if (IsStopped())
+ {
+ break;
+ }
+
+ if (activatedMessages.Count > 0)
+ {
+ var keys = activatedMessages.Select(m => m.Key);
+ var values = activatedMessages.Select(m => m.Value).ToList();
+
+ IList modifiedSessions = null;
+
+ await RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ using (var tx = this.StateManager.CreateTransaction())
+ {
+ modifiedSessions = await this.sessionProvider.TryAppendMessageBatchAsync(tx, values);
+ await this.CompleteBatchAsync(tx, keys);
+ await tx.CommitAsync();
+ }
+ }, uniqueActionIdentifier: $"Action = '{nameof(ScheduledMessageProvider)}.{nameof(ProcessScheduledMessages)}'");
+
+ lock (@lock)
+ {
+ this.inMemorySet = this.inMemorySet.Except(activatedMessages);
+ }
+
+ if (modifiedSessions != null)
+ {
+ foreach (var sessionId in modifiedSessions)
+ {
+ this.sessionProvider.TryEnqueueSession(sessionId);
+ }
+ }
+ }
+
+ this.nextActivationCheck = nextCheck;
+ await WaitForItemsAsync(this.nextActivationCheck - DateTime.UtcNow);
+ }
+ catch (Exception e)
+ {
+ ServiceFabricProviderEventSource.Tracing.ExceptionWhileRunningBackgroundJob($"{nameof(ScheduledMessageProvider)}.{nameof(ProcessScheduledMessages)}", e.ToString());
+ await Task.Delay(TimeSpan.FromMilliseconds(100));
+ }
+ }
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Stores/SessionMessageProvider.cs b/src/DurableTask.AzureServiceFabric/Stores/SessionMessageProvider.cs
new file mode 100644
index 000000000..a61632237
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Stores/SessionMessageProvider.cs
@@ -0,0 +1,49 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Stores
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.ServiceFabric.Data;
+
+ class SessionMessageProvider : MessageProviderBase
+ {
+ public SessionMessageProvider(IReliableStateManager stateManager, string storeName, CancellationToken token)
+ : base(stateManager, storeName, token)
+ {
+ }
+
+ protected override void AddItemInMemory(Guid key, TaskMessageItem value)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override Task StartAsync()
+ {
+ return InitializeStore();
+ }
+
+ public async Task>> ReceiveBatchAsync()
+ {
+ List> result = new List>();
+ if (!IsStopped())
+ {
+ await this.EnumerateItems(kvp => result.Add(new Message(kvp.Key, kvp.Value)));
+ }
+ return result;
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/Stores/SessionProvider.cs b/src/DurableTask.AzureServiceFabric/Stores/SessionProvider.cs
new file mode 100644
index 000000000..c8a24c351
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/Stores/SessionProvider.cs
@@ -0,0 +1,355 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.Stores
+{
+ using System;
+ using System.Collections.Concurrent;
+ using System.Collections.Generic;
+ using System.Collections.Immutable;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using DurableTask.Core;
+ using DurableTask.AzureServiceFabric.TaskHelpers;
+ using DurableTask.AzureServiceFabric.Tracing;
+
+ using Microsoft.ServiceFabric.Data;
+
+ ///
+ /// There are three interesting methods in this class that require some explanation.
+ /// 'AcceptSession' : Intent is to fetch the next available session and hand it out to consumer.
+ /// 'TryUnlockSession' : Intent is to indicate that the above consumer is done processing the session.
+ /// 'TryEnqueueSession' : Intent is to make the session available for a consumer if needed.
+ ///
+ /// 'AcceptSession' should give a session to only one consumer at a time (strict requirement).
+ /// We should also give out a session with messages in it but this is somewhat loose requirement because consumer
+ /// can handle the scenario where there are no messages. But we don't want to keep making the session available
+ /// to pick up when there are no incoming messages. (So giving a session with no messages once or even twice is ok
+ /// but that should not happening recurringly)
+ ///
+ /// Incoming messages can come at anytime, before the session picked up,
+ /// after 'AcceptSession' gave out a session but the consumer read messages in it, after consumer read messages in it, or
+ /// after the session finished processing by consumer.
+ ///
+ /// 'AcceptSession; will fetch from 'fetchQueue' and returns it (so 'fetchQueueu' should never have a given session twice in
+ /// the queue otherwise 'one consumer at a time' is violated. Also, we should not keep adding to 'fetchQueue' unless the session
+ /// is interesting i.e., it needs processing).
+ ///
+ /// lockedSessions with 'LockState' is used to achieve the above.
+ ///
+ /// 'AcceptSession' fetches from 'fetchQueue' and transitions the session from 'InFetchQueue' to 'Locked'.
+ /// (The implementation contract is such that if an item is in 'fetchQueue', it should be in 'InFetchQueue' state in lockedSessions)
+ ///
+ /// 'TryEnqueuSession' will try to add to lockedSessions with 'InFetchQueue' and only first such call after the session is unlocked will be
+ /// able to add and that call will add the session back to 'fetchQueue'.
+ /// If there is an entry in lockedSessions, 'TryEnqueuSession' does only one thing -
+ /// if the session is already 'Locked' (that means a consumer has the session),
+ /// it will just transition the state to 'NewMessagesWhileLocked' (so that session could be enqueued back again as it gets unlocked).
+ /// Note that once the session is in this state, subsequent calls to 'TryEnqueueSession' before unlock will keep it in this state.
+ /// Also Note that if session is in 'InFetchQueue' state, TryEnqueuSession does not need to (and should not) change the state.
+ ///
+ /// 'TryUnlockSession' first removes the session from lockedSessions [unlock complete at this point]
+ /// but if the removed state is 'NewMessagesWhileLocked', it attempts to put it back on fetchQueue by calling 'TryEnqueueSession'.
+ /// Note that at this point if another new incoming message calls 'TryEnqueueSession' only one of these should be able to add to 'fetchQueue'.
+ ///
+ /// Note that if new messages were sent after 'AcceptSession' but before consumer reads messages from session, we
+ /// will hand out those messages but still add the session to fetchQueue (as part of unlock) and so the next fetch sees the session but
+ /// with no messages. Though this can be made perfect by introducing one more state to 'LockedState', since consumer
+ /// can handle no messages scenario, it's simpler to ignore the difference between the above and new messages being sent
+ /// after consumer reads messages but before the session is unlocked.
+ ///
+ class SessionProvider : MessageProviderBase
+ {
+ ConcurrentQueue fetchQueue = new ConcurrentQueue();
+ ConcurrentDictionary lockedSessions = new ConcurrentDictionary();
+
+ ConcurrentDictionary sessionMessageProviders
+ = new ConcurrentDictionary(OrchestrationInstanceComparer.Default);
+
+ public SessionProvider(IReliableStateManager stateManager, CancellationToken token) : base(stateManager, Constants.OrchestrationDictionaryName, token)
+ {
+ }
+
+ public async Task AcceptSessionAsync(TimeSpan receiveTimeout)
+ {
+ if (!IsStopped())
+ {
+ bool newItemsBeforeTimeout = true;
+ while (newItemsBeforeTimeout)
+ {
+ if (this.fetchQueue.TryDequeue(out string returnInstanceId))
+ {
+ try
+ {
+ return await RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ using (var txn = this.StateManager.CreateTransaction())
+ {
+ var existingValue = await this.Store.TryGetValueAsync(txn, returnInstanceId);
+
+ if (existingValue.HasValue)
+ {
+ if (this.lockedSessions.TryUpdate(returnInstanceId, newValue: LockState.Locked, comparisonValue: LockState.InFetchQueue))
+ {
+ ServiceFabricProviderEventSource.Tracing.TraceMessage(returnInstanceId, "Session Locked Accepted");
+ return existingValue.Value;
+ }
+ else
+ {
+ var errorMessage = $"Internal Server Error : Unexpected to dequeue the session {returnInstanceId} which was already locked before";
+ ServiceFabricProviderEventSource.Tracing.UnexpectedCodeCondition(errorMessage);
+ throw new Exception(errorMessage);
+ }
+ }
+ else
+ {
+ var errorMessage = $"Internal Server Error: Did not find the session object in reliable dictionary while having the session {returnInstanceId} in memory";
+ ServiceFabricProviderEventSource.Tracing.UnexpectedCodeCondition(errorMessage);
+ throw new Exception(errorMessage);
+ }
+ }
+ }, uniqueActionIdentifier: $"{nameof(SessionProvider)}.{nameof(AcceptSessionAsync)}, SessionId : {returnInstanceId}");
+ }
+ catch (Exception)
+ {
+ this.fetchQueue.Enqueue(returnInstanceId);
+ throw;
+ }
+ }
+
+ newItemsBeforeTimeout = await WaitForItemsAsync(receiveTimeout);
+ }
+ }
+ return null;
+ }
+
+ protected override void AddItemInMemory(string key, PersistentSession value)
+ {
+ this.TryEnqueueSession(key);
+ }
+
+ public async Task>> ReceiveSessionMessagesAsync(PersistentSession session)
+ {
+ var sessionMessageProvider = await GetOrAddSessionMessagesInstance(session.SessionId);
+ var messages = await sessionMessageProvider.ReceiveBatchAsync();
+ ServiceFabricProviderEventSource.Tracing.TraceMessage(session.SessionId.InstanceId, $"Number of received messages {messages.Count}");
+ return messages;
+ }
+
+ public async Task CompleteMessages(ITransaction transaction, OrchestrationInstance instance, List lockTokens)
+ {
+ if (this.sessionMessageProviders.TryGetValue(instance, out SessionMessageProvider sessionMessageProvider))
+ {
+ ServiceFabricProviderEventSource.Tracing.TraceMessage(instance.InstanceId, $"Number of completed messages {lockTokens.Count}");
+ await sessionMessageProvider.CompleteBatchAsync(transaction, lockTokens);
+ }
+ else
+ {
+ ServiceFabricProviderEventSource.Tracing.UnexpectedCodeCondition($"{nameof(SessionProvider)}.{nameof(CompleteMessages)} : Did not find session messages provider instance for session : {instance}.");
+ }
+ }
+
+ public async Task UpdateSessionState(ITransaction transaction, OrchestrationInstance instance, OrchestrationRuntimeState newSessionState)
+ {
+ var sessionStateEvents = newSessionState?.Events.ToImmutableList();
+ var result = PersistentSession.Create(instance, sessionStateEvents);
+ await this.Store.SetAsync(transaction, instance.InstanceId, result);
+ }
+
+ public async Task AppendMessageAsync(TaskMessageItem newMessage)
+ {
+ await RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ using (var txn = this.StateManager.CreateTransaction())
+ {
+ await this.AppendMessageAsync(txn, newMessage);
+ await txn.CommitAsync();
+ }
+ }, uniqueActionIdentifier: $"Orchestration = '{newMessage.TaskMessage.OrchestrationInstance}', Action = '{nameof(SessionProvider)}.{nameof(AppendMessageAsync)}'");
+
+ this.TryEnqueueSession(newMessage.TaskMessage.OrchestrationInstance);
+ }
+
+ public async Task AppendMessageAsync(ITransaction transaction, TaskMessageItem newMessage)
+ {
+ await EnsureStoreInitialized();
+ var sessionMessageProvider = await GetOrAddSessionMessagesInstance(newMessage.TaskMessage.OrchestrationInstance);
+ await sessionMessageProvider.SendBeginAsync(transaction, new Message(Guid.NewGuid(), newMessage));
+ await this.Store.TryAddAsync(transaction, newMessage.TaskMessage.OrchestrationInstance.InstanceId, PersistentSession.Create(newMessage.TaskMessage.OrchestrationInstance));
+ }
+
+ public async Task TryAppendMessageAsync(ITransaction transaction, TaskMessageItem newMessage)
+ {
+ if (await this.Store.ContainsKeyAsync(transaction, newMessage.TaskMessage.OrchestrationInstance.InstanceId))
+ {
+ var sessionMessageProvider = await GetOrAddSessionMessagesInstance(newMessage.TaskMessage.OrchestrationInstance);
+ await sessionMessageProvider.SendBeginAsync(transaction, new Message(Guid.NewGuid(), newMessage));
+ return true;
+ }
+
+ return false;
+ }
+
+ public async Task> TryAppendMessageBatchAsync(ITransaction transaction, IEnumerable newMessages)
+ {
+ List modifiedSessions = new List();
+
+ var groups = newMessages.GroupBy(m => m.TaskMessage.OrchestrationInstance, OrchestrationInstanceComparer.Default);
+
+ foreach (var group in groups)
+ {
+ if (await this.Store.ContainsKeyAsync(transaction, group.Key.InstanceId))
+ {
+ var sessionMessageProvider = await GetOrAddSessionMessagesInstance(group.Key);
+ await sessionMessageProvider.SendBatchBeginAsync(transaction, group.Select(tm => new Message(Guid.NewGuid(), tm)));
+ modifiedSessions.Add(group.Key);
+ }
+ }
+
+ return modifiedSessions;
+ }
+
+ public async Task AppendMessageBatchAsync(ITransaction transaction, IEnumerable newMessages)
+ {
+ var groups = newMessages.GroupBy(m => m.TaskMessage.OrchestrationInstance, OrchestrationInstanceComparer.Default);
+
+ foreach (var group in groups)
+ {
+ var groupMessages = group.AsEnumerable();
+
+ await this.Store.TryAddAsync(transaction, group.Key.InstanceId, PersistentSession.Create(group.Key));
+ var sessionMessageProvider = await GetOrAddSessionMessagesInstance(group.Key);
+ await sessionMessageProvider.SendBatchBeginAsync(transaction, group.Select(tm => new Message(Guid.NewGuid(), tm)));
+ }
+ }
+
+ public async Task> GetSessions()
+ {
+ await EnsureStoreInitialized();
+
+ var result = new List();
+ await this.EnumerateItems(kvp => result.Add(kvp.Value));
+ return result;
+ }
+
+ public void TryUnlockSession(OrchestrationInstance instance, bool abandon = false, bool isComplete = false)
+ {
+ ServiceFabricProviderEventSource.Tracing.TraceMessage(instance.InstanceId, $"Session Unlock Begin, Abandon = {abandon}");
+ if (!this.lockedSessions.TryRemove(instance.InstanceId, out LockState lockState) || lockState == LockState.InFetchQueue)
+ {
+ var errorMessage = $"{nameof(SessionProvider)}.{nameof(TryUnlockSession)} : Trying to unlock the session {instance.InstanceId} which was not locked.";
+ ServiceFabricProviderEventSource.Tracing.UnexpectedCodeCondition(errorMessage);
+ throw new Exception(errorMessage);
+ }
+
+ if (!isComplete && (abandon || lockState == LockState.NewMessagesWhileLocked))
+ {
+ this.TryEnqueueSession(instance);
+ }
+
+ ServiceFabricProviderEventSource.Tracing.TraceMessage(instance.InstanceId, $"Session Unlock End, Abandon = {abandon}, removed lock state = {lockState}");
+ }
+
+ public async Task TryAddSession(ITransaction transaction, TaskMessageItem newMessage)
+ {
+ await EnsureStoreInitialized();
+
+ bool added = await this.Store.TryAddAsync(transaction, newMessage.TaskMessage.OrchestrationInstance.InstanceId, PersistentSession.Create(newMessage.TaskMessage.OrchestrationInstance));
+
+ if (added)
+ {
+ var sessionMessageProvider = await GetOrAddSessionMessagesInstance(newMessage.TaskMessage.OrchestrationInstance);
+ await sessionMessageProvider.SendBeginAsync(transaction, new Message(Guid.NewGuid(), newMessage));
+ }
+
+ return added;
+ }
+
+ public async Task GetSession(string instanceId)
+ {
+ await EnsureStoreInitialized();
+
+ return await RetryHelper.ExecuteWithRetryOnTransient(async () =>
+ {
+ using (var txn = this.StateManager.CreateTransaction())
+ {
+ var value = await this.Store.TryGetValueAsync(txn, instanceId);
+ if (value.HasValue)
+ {
+ return value.Value;
+ }
+ }
+ return null;
+ }, uniqueActionIdentifier: $"Orchestration InstanceId = {instanceId}, Action = {nameof(SessionProvider)}.{nameof(GetSession)}");
+ }
+
+ public void TryEnqueueSession(OrchestrationInstance instance)
+ {
+ this.TryEnqueueSession(instance.InstanceId);
+ }
+
+ void TryEnqueueSession(string instanceId)
+ {
+ if (this.lockedSessions.TryAdd(instanceId, LockState.InFetchQueue))
+ {
+ ServiceFabricProviderEventSource.Tracing.TraceMessage(instanceId, "Session Getting Enqueued");
+ this.fetchQueue.Enqueue(instanceId);
+ SetWaiterForNewItems();
+ }
+ else
+ {
+ this.lockedSessions.TryUpdate(instanceId, LockState.NewMessagesWhileLocked, LockState.Locked);
+ }
+ }
+
+ public async Task DropSession(ITransaction txn, OrchestrationInstance instance)
+ {
+ if (instance == null)
+ {
+ throw new ArgumentNullException(nameof(instance));
+ }
+
+ await this.Store.TryRemoveAsync(txn, instance.InstanceId);
+
+ this.sessionMessageProviders.TryRemove(instance, out SessionMessageProvider _);
+
+ var noWait = RetryHelper.ExecuteWithRetryOnTransient(() => this.StateManager.RemoveAsync(GetSessionMessagesDictionaryName(instance)),
+ uniqueActionIdentifier: $"Orchestration = '{instance}', Action = 'DropSessionMessagesDictionaryBackgroundTask'");
+ }
+
+ async Task GetOrAddSessionMessagesInstance(OrchestrationInstance instance)
+ {
+ var newInstance = new SessionMessageProvider(this.StateManager, GetSessionMessagesDictionaryName(instance), this.CancellationToken);
+ var sessionMessageProvider = this.sessionMessageProviders.GetOrAdd(instance, newInstance);
+ await sessionMessageProvider.EnsureStoreInitialized();
+ return sessionMessageProvider;
+ }
+
+ string GetSessionMessagesDictionaryName(OrchestrationInstance instance)
+ {
+ var sessionKey = $"{instance.InstanceId}_{instance.ExecutionId}";
+ return Constants.SessionMessagesDictionaryPrefix + sessionKey;
+ }
+
+ enum LockState
+ {
+ InFetchQueue = 0,
+
+ Locked,
+
+ NewMessagesWhileLocked
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/TaskHelpers/CountBasedFixedDelayRetryPolicy.cs b/src/DurableTask.AzureServiceFabric/TaskHelpers/CountBasedFixedDelayRetryPolicy.cs
new file mode 100644
index 000000000..f2d5d03da
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/TaskHelpers/CountBasedFixedDelayRetryPolicy.cs
@@ -0,0 +1,50 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.TaskHelpers
+{
+ using System;
+
+ class CountBasedFixedDelayRetryPolicy : RetryPolicy
+ {
+ readonly int maxNumberOfAttempts;
+ readonly TimeSpan delay;
+ int pendingAttempts;
+
+ public CountBasedFixedDelayRetryPolicy(int maxNumberOfAttempts, TimeSpan delay)
+ {
+ this.maxNumberOfAttempts = maxNumberOfAttempts;
+ this.delay = delay;
+ this.pendingAttempts = maxNumberOfAttempts;
+ }
+
+ public override bool ShouldExecute()
+ {
+ return this.pendingAttempts-- > 0;
+ }
+
+ public override TimeSpan GetNextDelay()
+ {
+ if (this.pendingAttempts < 1)
+ {
+ return TimeSpan.Zero;
+ }
+ return this.delay;
+ }
+
+ public static RetryPolicy GetNewDefaultPolicy()
+ {
+ return new CountBasedFixedDelayRetryPolicy(3, TimeSpan.FromMilliseconds(100));
+ }
+ }
+}
diff --git a/src/DurableTask.AzureServiceFabric/TaskHelpers/RetryHelper.cs b/src/DurableTask.AzureServiceFabric/TaskHelpers/RetryHelper.cs
new file mode 100644
index 000000000..94768d0e9
--- /dev/null
+++ b/src/DurableTask.AzureServiceFabric/TaskHelpers/RetryHelper.cs
@@ -0,0 +1,112 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.AzureServiceFabric.TaskHelpers
+{
+ using System;
+ using System.Diagnostics;
+ using System.Threading.Tasks;
+
+ using DurableTask.AzureServiceFabric.Tracing;
+
+ static class RetryHelper
+ {
+ public static Task ExecuteWithRetryOnTransient(Func action, string uniqueActionIdentifier)
+ {
+ return ExecuteWithRetryOnTransient(action, CountBasedFixedDelayRetryPolicy.GetNewDefaultPolicy(), uniqueActionIdentifier);
+ }
+
+ static async Task ExecuteWithRetryOnTransient(Func