diff --git a/.gitignore b/.gitignore index 16efb5f9b..8a865504c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ bin/ obj/ packages/ +pkg/ TestResults/ # Packages # diff --git a/DurableTask.ServiceFabric.sln b/DurableTask.ServiceFabric.sln new file mode 100644 index 000000000..b4c1c08c0 --- /dev/null +++ b/DurableTask.ServiceFabric.sln @@ -0,0 +1,142 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26403.7 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{501E1168-418C-4832-B88C-617735BD02C9}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.Config = .nuget\NuGet.Config + .nuget\NuGet.exe = .nuget\NuGet.exe + .nuget\NuGet.targets = .nuget\NuGet.targets + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0AE737F5-4AA1-402F-A61C-258CA00E282C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{5177D1DE-AC86-41B9-8D04-41988A7C2208}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestFabricApplication", "TestFabricApplication", "{507C0CC2-082F-4EB7-8984-E2648976EB04}" +EndProject +Project("{A07B5EB6-E848-4116-A8D0-A826331D98C6}") = "TestFabricApplication", "Test\TestFabricApplication\TestFabricApplication\TestFabricApplication.sfproj", "{CA924781-AE52-45FA-A817-C39C822FB782}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestStatefulService", "Test\TestFabricApplication\TestStatefulService\TestStatefulService.csproj", "{95D6C9C7-F7A0-4125-840D-1854BEC24978}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication.Common", "Test\TestFabricApplication\TestApplication.Common\TestApplication.Common.csproj", "{EA90334D-3194-4266-81EB-EECA9C760315}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.ServiceFabric", "src\DurableTask.ServiceFabric\DurableTask.ServiceFabric.csproj", "{5C617B24-4902-4992-8F71-AB14F00B74A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.ServiceFabric.Test", "Test\DurableTask.ServiceFabric.Test\DurableTask.ServiceFabric.Test.csproj", "{EDC63DDB-8397-4AEC-BF54-6395B2527075}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DurableTask.Test.Orchestrations", "Test\DurableTask.Test.Orchestrations\DurableTask.Test.Orchestrations.csproj", "{2BC2A5AD-4200-41B9-A034-332BB1D7187B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.ServiceFabric.Failover.Tests", "Test\DurableTask.ServiceFabric.Stress.Tests\DurableTask.ServiceFabric.Failover.Tests.csproj", "{BE4624F3-E565-428F-9EED-E6847530746A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.ServiceFabric.UnitTests", "Test\DurableTask.ServiceFabric.UnitTests\DurableTask.ServiceFabric.UnitTests.csproj", "{31C9C932-6376-46D4-A68A-A0A7F2726D38}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.ServiceFabric.Tests.Tooling", "Test\DurableTask.ServiceFabric.Tests.Tooling\DurableTask.ServiceFabric.Tests.Tooling.csproj", "{ED6817FD-A301-40CA-B287-1C40E7AAC7D1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DurableTask.Core", "src\DurableTask.Core\DurableTask.Core.csproj", "{6B386E8F-20EB-43DE-BBD7-6408A232B4BA}" +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 + {CA924781-AE52-45FA-A817-C39C822FB782}.Debug|Any CPU.ActiveCfg = Debug|x64 + {CA924781-AE52-45FA-A817-C39C822FB782}.Debug|x64.ActiveCfg = Debug|x64 + {CA924781-AE52-45FA-A817-C39C822FB782}.Debug|x64.Build.0 = Debug|x64 + {CA924781-AE52-45FA-A817-C39C822FB782}.Debug|x64.Deploy.0 = Debug|x64 + {CA924781-AE52-45FA-A817-C39C822FB782}.Release|Any CPU.ActiveCfg = Release|x64 + {CA924781-AE52-45FA-A817-C39C822FB782}.Release|x64.ActiveCfg = Release|x64 + {CA924781-AE52-45FA-A817-C39C822FB782}.Release|x64.Build.0 = Release|x64 + {CA924781-AE52-45FA-A817-C39C822FB782}.Release|x64.Deploy.0 = Release|x64 + {95D6C9C7-F7A0-4125-840D-1854BEC24978}.Debug|Any CPU.ActiveCfg = Debug|x64 + {95D6C9C7-F7A0-4125-840D-1854BEC24978}.Debug|x64.ActiveCfg = Debug|x64 + {95D6C9C7-F7A0-4125-840D-1854BEC24978}.Debug|x64.Build.0 = Debug|x64 + {95D6C9C7-F7A0-4125-840D-1854BEC24978}.Release|Any CPU.ActiveCfg = Release|x64 + {95D6C9C7-F7A0-4125-840D-1854BEC24978}.Release|x64.ActiveCfg = Release|x64 + {95D6C9C7-F7A0-4125-840D-1854BEC24978}.Release|x64.Build.0 = Release|x64 + {EA90334D-3194-4266-81EB-EECA9C760315}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA90334D-3194-4266-81EB-EECA9C760315}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA90334D-3194-4266-81EB-EECA9C760315}.Debug|x64.ActiveCfg = Debug|Any CPU + {EA90334D-3194-4266-81EB-EECA9C760315}.Debug|x64.Build.0 = Debug|Any CPU + {EA90334D-3194-4266-81EB-EECA9C760315}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA90334D-3194-4266-81EB-EECA9C760315}.Release|Any CPU.Build.0 = Release|Any CPU + {EA90334D-3194-4266-81EB-EECA9C760315}.Release|x64.ActiveCfg = Release|Any CPU + {EA90334D-3194-4266-81EB-EECA9C760315}.Release|x64.Build.0 = Release|Any CPU + {5C617B24-4902-4992-8F71-AB14F00B74A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C617B24-4902-4992-8F71-AB14F00B74A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C617B24-4902-4992-8F71-AB14F00B74A8}.Debug|x64.ActiveCfg = Debug|Any CPU + {5C617B24-4902-4992-8F71-AB14F00B74A8}.Debug|x64.Build.0 = Debug|Any CPU + {5C617B24-4902-4992-8F71-AB14F00B74A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C617B24-4902-4992-8F71-AB14F00B74A8}.Release|Any CPU.Build.0 = Release|Any CPU + {5C617B24-4902-4992-8F71-AB14F00B74A8}.Release|x64.ActiveCfg = Release|Any CPU + {5C617B24-4902-4992-8F71-AB14F00B74A8}.Release|x64.Build.0 = Release|Any CPU + {EDC63DDB-8397-4AEC-BF54-6395B2527075}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDC63DDB-8397-4AEC-BF54-6395B2527075}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDC63DDB-8397-4AEC-BF54-6395B2527075}.Debug|x64.ActiveCfg = Debug|Any CPU + {EDC63DDB-8397-4AEC-BF54-6395B2527075}.Debug|x64.Build.0 = Debug|Any CPU + {EDC63DDB-8397-4AEC-BF54-6395B2527075}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDC63DDB-8397-4AEC-BF54-6395B2527075}.Release|Any CPU.Build.0 = Release|Any CPU + {EDC63DDB-8397-4AEC-BF54-6395B2527075}.Release|x64.ActiveCfg = Release|Any CPU + {EDC63DDB-8397-4AEC-BF54-6395B2527075}.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 + {BE4624F3-E565-428F-9EED-E6847530746A}.Debug|Any CPU.ActiveCfg = Debug|x64 + {BE4624F3-E565-428F-9EED-E6847530746A}.Debug|x64.ActiveCfg = Debug|x64 + {BE4624F3-E565-428F-9EED-E6847530746A}.Debug|x64.Build.0 = Debug|x64 + {BE4624F3-E565-428F-9EED-E6847530746A}.Release|Any CPU.ActiveCfg = Release|x64 + {BE4624F3-E565-428F-9EED-E6847530746A}.Release|x64.ActiveCfg = Release|x64 + {BE4624F3-E565-428F-9EED-E6847530746A}.Release|x64.Build.0 = Release|x64 + {31C9C932-6376-46D4-A68A-A0A7F2726D38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31C9C932-6376-46D4-A68A-A0A7F2726D38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31C9C932-6376-46D4-A68A-A0A7F2726D38}.Debug|x64.ActiveCfg = Debug|Any CPU + {31C9C932-6376-46D4-A68A-A0A7F2726D38}.Debug|x64.Build.0 = Debug|Any CPU + {31C9C932-6376-46D4-A68A-A0A7F2726D38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31C9C932-6376-46D4-A68A-A0A7F2726D38}.Release|Any CPU.Build.0 = Release|Any CPU + {31C9C932-6376-46D4-A68A-A0A7F2726D38}.Release|x64.ActiveCfg = Release|Any CPU + {31C9C932-6376-46D4-A68A-A0A7F2726D38}.Release|x64.Build.0 = Release|Any CPU + {ED6817FD-A301-40CA-B287-1C40E7AAC7D1}.Debug|Any CPU.ActiveCfg = Debug|x64 + {ED6817FD-A301-40CA-B287-1C40E7AAC7D1}.Debug|x64.ActiveCfg = Debug|x64 + {ED6817FD-A301-40CA-B287-1C40E7AAC7D1}.Debug|x64.Build.0 = Debug|x64 + {ED6817FD-A301-40CA-B287-1C40E7AAC7D1}.Release|Any CPU.ActiveCfg = Release|x64 + {ED6817FD-A301-40CA-B287-1C40E7AAC7D1}.Release|x64.ActiveCfg = Release|x64 + {ED6817FD-A301-40CA-B287-1C40E7AAC7D1}.Release|x64.Build.0 = Release|x64 + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA}.Debug|x64.ActiveCfg = Debug|Any CPU + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA}.Debug|x64.Build.0 = Debug|Any CPU + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA}.Release|Any CPU.Build.0 = Release|Any CPU + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA}.Release|x64.ActiveCfg = Release|Any CPU + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA}.Release|x64.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {507C0CC2-082F-4EB7-8984-E2648976EB04} = {5177D1DE-AC86-41B9-8D04-41988A7C2208} + {CA924781-AE52-45FA-A817-C39C822FB782} = {507C0CC2-082F-4EB7-8984-E2648976EB04} + {95D6C9C7-F7A0-4125-840D-1854BEC24978} = {507C0CC2-082F-4EB7-8984-E2648976EB04} + {EA90334D-3194-4266-81EB-EECA9C760315} = {507C0CC2-082F-4EB7-8984-E2648976EB04} + {5C617B24-4902-4992-8F71-AB14F00B74A8} = {0AE737F5-4AA1-402F-A61C-258CA00E282C} + {EDC63DDB-8397-4AEC-BF54-6395B2527075} = {5177D1DE-AC86-41B9-8D04-41988A7C2208} + {2BC2A5AD-4200-41B9-A034-332BB1D7187B} = {5177D1DE-AC86-41B9-8D04-41988A7C2208} + {BE4624F3-E565-428F-9EED-E6847530746A} = {5177D1DE-AC86-41B9-8D04-41988A7C2208} + {31C9C932-6376-46D4-A68A-A0A7F2726D38} = {5177D1DE-AC86-41B9-8D04-41988A7C2208} + {ED6817FD-A301-40CA-B287-1C40E7AAC7D1} = {5177D1DE-AC86-41B9-8D04-41988A7C2208} + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA} = {0AE737F5-4AA1-402F-A61C-258CA00E282C} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + EnterpriseLibraryConfigurationToolBinariesPath = packages\TransientFaultHandling.Core.5.1.1209.1\lib\NET4 + SolutionGuid = {AB50E5E3-EAB4-4EBA-AE13-551034A521F8} + EndGlobalSection +EndGlobal diff --git a/Test/DurableTask.ServiceFabric.Stress.Tests/ChaosRunner.cs b/Test/DurableTask.ServiceFabric.Stress.Tests/ChaosRunner.cs new file mode 100644 index 000000000..7e780d6c4 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Stress.Tests/ChaosRunner.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 DurableTask.ServiceFabric.Failover.Tests +{ + using System; + using System.ComponentModel; + using System.Fabric; + using System.Fabric.Testability.Scenario; + using System.Threading; + using System.Threading.Tasks; + + class ChaosRunner + { + public async Task RunFailoverScenario(TimeSpan timeToRun, CancellationToken token) + { + var partitionSelector = PartitionSelector.PartitionKeyOf(new Uri("fabric:/TestFabricApplication/TestStatefulService"), 1); + TimeSpan maxServiceStabilizationTimeout = TimeSpan.FromMinutes(3); + var client = new FabricClient("localhost:19000"); + + var parameters = new FailoverTestScenarioParameters( + partitionSelector, + timeToRun, + maxServiceStabilizationTimeout); + + Console.WriteLine($"FailoverTest Info : TimeToRun = {parameters.TimeToRun}"); + Console.WriteLine($"FailoverTest Info : WaitTimeBetweenFaults = {parameters.WaitTimeBetweenFaults}"); + Console.WriteLine($"FailoverTest Info : MaxServiceStabilizationTimeout = {parameters.MaxServiceStabilizationTimeout}"); + + var testScenario = new FailoverTestScenario(client, parameters); + testScenario.ProgressChanged += OnTestProgressChanged; + + try + { + await testScenario.ExecuteAsync(token); + } + catch (TaskCanceledException) + { + } + catch (AggregateException ae) + { + Console.Error.WriteLine($"{DateTime.UtcNow} : Exception inside the failover test scenario : {ae.InnerException}"); + } + } + + private void OnTestProgressChanged(object sender, ProgressChangedEventArgs e) + { + var utcNow = DateTime.UtcNow; + Console.WriteLine($"{utcNow} : FailoverTest Progress Percentage {e.ProgressPercentage}"); + Console.WriteLine($"{utcNow} : FailoverTest UserState {e.UserState}"); + } + } +} diff --git a/Test/DurableTask.ServiceFabric.Stress.Tests/DurableTask.ServiceFabric.Failover.Tests.csproj b/Test/DurableTask.ServiceFabric.Stress.Tests/DurableTask.ServiceFabric.Failover.Tests.csproj new file mode 100644 index 000000000..af9297a5c --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Stress.Tests/DurableTask.ServiceFabric.Failover.Tests.csproj @@ -0,0 +1,118 @@ + + + + + + Debug + AnyCPU + {BE4624F3-E565-428F-9EED-E6847530746A} + Exe + Properties + DurableTask.ServiceFabric.Failover.Tests + DurableTask.ServiceFabric.Failover.Tests + 512 + true + v4.5.1 + + + + x64 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + x64 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + ..\..\packages\Microsoft.ServiceFabric.Data.2.5.216\lib\net45\Microsoft.ServiceFabric.Data.dll + True + + + ..\..\packages\Microsoft.ServiceFabric.Data.2.5.216\lib\net45\Microsoft.ServiceFabric.Data.Interfaces.dll + True + + + ..\..\packages\Microsoft.ServiceFabric.FabricTransport.Internal.2.5.216\lib\net45\Microsoft.ServiceFabric.FabricTransport.dll + + + ..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\Microsoft.ServiceFabric.Internal.dll + True + + + ..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\Microsoft.ServiceFabric.Internal.Strings.dll + True + + + ..\..\packages\Microsoft.ServiceFabric.Services.2.5.216\lib\net45\Microsoft.ServiceFabric.Services.dll + True + + + ..\..\packages\Microsoft.ServiceFabric.Services.Remoting.2.5.216\lib\net45\Microsoft.ServiceFabric.Services.Remoting.dll + + + ..\..\packages\Microsoft.ServiceFabric.Services.2.5.216\lib\net45\ServiceFabricServiceModel.dll + True + + + + + ..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\System.Fabric.dll + True + + + + + + + + + + + + + + + + + + + + + + + + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA} + DurableTask.Core + + + {2bc2a5ad-4200-41b9-a034-332bb1d7187b} + DurableTask.Test.Orchestrations + + + {ea90334d-3194-4266-81eb-eeca9c760315} + TestApplication.Common + + + + + \ No newline at end of file diff --git a/Test/DurableTask.ServiceFabric.Stress.Tests/OrchestrationRunner.cs b/Test/DurableTask.ServiceFabric.Stress.Tests/OrchestrationRunner.cs new file mode 100644 index 000000000..7d26bff37 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Stress.Tests/OrchestrationRunner.cs @@ -0,0 +1,163 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric.Failover.Tests +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Diagnostics; + using System.Threading; + using System.Threading.Tasks; + using DurableTask.Core; + using DurableTask.Test.Orchestrations.Perf; + using Microsoft.ServiceFabric.Services.Client; + using Microsoft.ServiceFabric.Services.Remoting.Client; + using TestApplication.Common; + + class OrchestrationRunner + { + ConcurrentBag instances = new ConcurrentBag(); + IRemoteClient serviceClient = ServiceProxy.Create(new Uri("fabric:/TestFabricApplication/TestStatefulService"), new ServicePartitionKey(1)); + Dictionary outcomeFrequencies = new Dictionary(); + + public async Task RunTestAsync(CancellationToken token) + { + CancellationTokenSource cts2 = new CancellationTokenSource(); + + Stopwatch watch = Stopwatch.StartNew(); + var programTask = RunOrchestrations(token); + var statusTask = PollState(cts2.Token); + + await programTask; + + cts2.Cancel(); + await statusTask; + watch.Stop(); + + Func elapsedTimeFormatter = timeSpan => $"{timeSpan.Hours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}.{timeSpan.Milliseconds / 10:00}"; + Console.WriteLine($"{nameof(RunTestAsync)}: Total elapsed time for the program : {elapsedTimeFormatter(watch.Elapsed)}"); + } + + async Task PollState(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await PrintStatusAggregation(); + await Task.Delay(TimeSpan.FromSeconds(10)); + } + await PrintStatusAggregation(printFailedInstances: true); + } + + async Task PrintStatusAggregation(bool printFailedInstances = false) + { + Console.WriteLine($"{nameof(PrintStatusAggregation)}: Number of instances so far : {instances.Count}"); + + var failedInstances = new Dictionary(); + + foreach (var instance in instances) + { + var state = await Utils.ExceptionWrapper(() => serviceClient.GetOrchestrationState(instance), $"Get Orchestration State {instance}"); + var key = state?.OrchestrationStatus.ToString() ?? "NullState"; + + if (state?.OrchestrationStatus != OrchestrationStatus.Completed) + { + failedInstances.Add(instance, state); + } + + if (!outcomeFrequencies.ContainsKey(key)) + { + outcomeFrequencies.Add(key, 0); + } + + outcomeFrequencies[key]++; + } + + foreach (var kvp in outcomeFrequencies) + { + Console.WriteLine($"{nameof(PrintStatusAggregation)}: {kvp.Key} : {kvp.Value}"); + } + + if (printFailedInstances && failedInstances.Count > 0) + { + Console.WriteLine("Unsuccessful Orchestrations:"); + foreach (var kvp in failedInstances) + { + Console.WriteLine($"{kvp.Key} : {kvp.Value?.OrchestrationStatus}"); + } + } + + outcomeFrequencies.Clear(); + } + + async Task RunOrchestrations(CancellationToken cancellationToken) + { + int totalRequests = 0; + List tasks = new List(); + Console.WriteLine($"{nameof(RunOrchestrations)}: Starting orchestrations"); + + while (!cancellationToken.IsCancellationRequested) + { + var newOrchData = new TestOrchestrationData() + { + NumberOfParallelTasks = 15, + NumberOfSerialTasks = 5, + MaxDelay = 500, + MinDelay = 50, + DelayUnit = TimeSpan.FromMilliseconds(1), + UseTimeoutTask = false, //Change to true when testing timers + ExecutionTimeout = TimeSpan.FromMinutes(5) + }; + + totalRequests++; + var instance = await Utils.ExceptionWrapper(() => serviceClient.StartTestOrchestrationAsync(newOrchData), $"StartOrchestration {totalRequests}"); + if (instance != null) + { + instances.Add(instance); + var waitTask = Utils.ExceptionWrapper(() => serviceClient.WaitForOrchestration(instance, TimeSpan.FromMinutes(10)), $"WaitForOrchestration {totalRequests}"); + tasks.Add(waitTask); + } + + var delayBetweenRequests = 50; + try + { + await Task.Delay(TimeSpan.FromMilliseconds(delayBetweenRequests), cancellationToken); + } + catch (TaskCanceledException) + { + break; + } + } + + Console.WriteLine($"{nameof(RunOrchestrations)}: Total orchestrations : {totalRequests}"); + Console.WriteLine($"{nameof(RunOrchestrations)}: Waiting for pending orchestrations"); + await Task.WhenAll(tasks); + + Console.WriteLine($"{nameof(RunOrchestrations)}: Done"); + } + + int GetVariableDelayForTotalRequests(int totalRequests) + { + int delay = 0; + if (totalRequests % 10 == 0) + { + delay = 1000; + } + if (totalRequests % 100 == 0) + { + delay = 3000; + } + return delay; + } + } +} diff --git a/Test/DurableTask.ServiceFabric.Stress.Tests/Program.cs b/Test/DurableTask.ServiceFabric.Stress.Tests/Program.cs new file mode 100644 index 000000000..1587587c4 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Stress.Tests/Program.cs @@ -0,0 +1,78 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric.Failover.Tests +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Threading; + using System.Threading.Tasks; + + class Program + { + static void Main(string[] args) + { + MainAsync(args).Wait(); + } + + static async Task MainAsync(string[] args) + { + var timeToRun = ValidateArgs(args); + + var tasks = new List(); + if (timeToRun != TimeSpan.Zero) + { + CancellationTokenSource cts = new CancellationTokenSource(); + if (timeToRun <= TimeSpan.FromMinutes(Int32.MaxValue)) //Cancellation token can't take more time than Int32.MaxValue + { + cts.CancelAfter(timeToRun); + } + + Console.WriteLine("Press CTRL+C to stop running orchestrations and print results"); + Console.CancelKeyPress += (sender, e) => + { + cts.Cancel(); + e.Cancel = true; + }; + + var runner = new OrchestrationRunner(); + tasks.Add(runner.RunTestAsync(cts.Token)); + + await Task.Delay(TimeSpan.FromMinutes(2)); + + var chaosRunner = new ChaosRunner(); + tasks.Add(chaosRunner.RunFailoverScenario(timeToRun, cts.Token)); + } + + await Task.WhenAll(tasks); + } + + static TimeSpan ValidateArgs(string[] args) + { + if (args == null || args.Length == 0) + { + return TimeSpan.MaxValue; //Indicates that test should run till user stops it. + } + + var first = args[0]; + int toBeRunTime = 0; + if (!int.TryParse(first, out toBeRunTime)) + { + Console.WriteLine($"{Process.GetCurrentProcess().ProcessName} [Time to run (minutes)]"); + } + + return TimeSpan.FromMinutes(toBeRunTime); + } + } +} diff --git a/Test/DurableTask.ServiceFabric.Stress.Tests/Properties/AssemblyInfo.cs b/Test/DurableTask.ServiceFabric.Stress.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..05adeb4bc --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Stress.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DurableTask.ServiceFabric.Failover.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DurableTask.ServiceFabric.Failover.Tests")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("be4624f3-e565-428f-9eed-e6847530746a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Test/DurableTask.ServiceFabric.Stress.Tests/Utils.cs b/Test/DurableTask.ServiceFabric.Stress.Tests/Utils.cs new file mode 100644 index 000000000..54b7e57e8 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Stress.Tests/Utils.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.ServiceFabric.Failover.Tests +{ + using System; + using System.Threading.Tasks; + + class Utils + { + public static async Task ExceptionWrapper(Func> action, string actionName) + { + try + { + return await action(); + } + catch (Exception e) + { + Console.Error.WriteLine($"Exception caught from method {actionName} : {e}"); + return default(TResult); + } + } + } +} diff --git a/Test/DurableTask.ServiceFabric.Stress.Tests/app.config b/Test/DurableTask.ServiceFabric.Stress.Tests/app.config new file mode 100644 index 000000000..877e0c4b1 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Stress.Tests/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Test/DurableTask.ServiceFabric.Stress.Tests/packages.config b/Test/DurableTask.ServiceFabric.Stress.Tests/packages.config new file mode 100644 index 000000000..792498cb5 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Stress.Tests/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Test/DurableTask.ServiceFabric.Test/AssemblySetup.cs b/Test/DurableTask.ServiceFabric.Test/AssemblySetup.cs new file mode 100644 index 000000000..ce7be91a8 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Test/AssemblySetup.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.ServiceFabric.Test +{ + using System; + using System.IO; + using DurableTask.ServiceFabric.Test.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.ServiceFabric.Test/Constants.cs b/Test/DurableTask.ServiceFabric.Test/Constants.cs new file mode 100644 index 000000000..23d7c95f8 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Test/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.ServiceFabric.Test +{ + static class Constants + { + public const string TestFabricApplicationAddress = "fabric:/TestFabricApplicationType/TestStatefulService"; + } +} diff --git a/Test/DurableTask.ServiceFabric.Test/DeploymentUtil/ApplicationInfoReader.cs b/Test/DurableTask.ServiceFabric.Test/DeploymentUtil/ApplicationInfoReader.cs new file mode 100644 index 000000000..34d66d046 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Test/DeploymentUtil/ApplicationInfoReader.cs @@ -0,0 +1,108 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric.Test.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 + { + get { return 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.ServiceFabric.Test/DeploymentUtil/DeploymentHelper.cs b/Test/DurableTask.ServiceFabric.Test/DeploymentUtil/DeploymentHelper.cs new file mode 100644 index 000000000..1335bea37 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Test/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.ServiceFabric.Test.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.ServiceFabric.Test/DurableTask.ServiceFabric.Test.csproj b/Test/DurableTask.ServiceFabric.Test/DurableTask.ServiceFabric.Test.csproj new file mode 100644 index 000000000..d741c8e25 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Test/DurableTask.ServiceFabric.Test.csproj @@ -0,0 +1,135 @@ + + + + + + Debug + AnyCPU + {EDC63DDB-8397-4AEC-BF54-6395B2527075} + Library + Properties + DurableTask.ServiceFabric.Test + DurableTask.ServiceFabric.Test + 512 + v4.5.1 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + x64 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + ..\..\packages\Microsoft.ServiceFabric.Data.2.5.216\lib\net45\Microsoft.ServiceFabric.Data.dll + True + + + False + ..\..\packages\Microsoft.ServiceFabric.Data.2.5.216\lib\net45\Microsoft.ServiceFabric.Data.Interfaces.dll + True + + + ..\..\packages\Microsoft.ServiceFabric.FabricTransport.Internal.2.5.216\lib\net45\Microsoft.ServiceFabric.FabricTransport.dll + + + ..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\Microsoft.ServiceFabric.Internal.dll + True + + + ..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\Microsoft.ServiceFabric.Internal.Strings.dll + True + + + ..\..\packages\Microsoft.ServiceFabric.Services.2.5.216\lib\net45\Microsoft.ServiceFabric.Services.dll + True + + + ..\..\packages\Microsoft.ServiceFabric.Services.Remoting.2.5.216\lib\net45\Microsoft.ServiceFabric.Services.Remoting.dll + + + + ..\..\packages\Microsoft.ServiceFabric.Services.2.5.216\lib\net45\ServiceFabricServiceModel.dll + True + + + + + ..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\System.Fabric.dll + True + + + + + + + + + + + + + + + + + + + + + + + + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA} + DurableTask.Core + + + {5c617b24-4902-4992-8f71-ab14f00b74a8} + DurableTask.ServiceFabric + + + {2bc2a5ad-4200-41b9-a034-332bb1d7187b} + DurableTask.Test.Orchestrations + + + {ea90334d-3194-4266-81eb-eeca9c760315} + TestApplication.Common + + + {95d6c9c7-f7a0-4125-840d-1854bec24978} + TestStatefulService + + + + + + Designer + + + + + + + + \ No newline at end of file diff --git a/Test/DurableTask.ServiceFabric.Test/FunctionalTests.cs b/Test/DurableTask.ServiceFabric.Test/FunctionalTests.cs new file mode 100644 index 000000000..37701516b --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Test/FunctionalTests.cs @@ -0,0 +1,371 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric.Test +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using DurableTask.Core; + using DurableTask.Test.Orchestrations.Perf; + using Microsoft.ServiceFabric.Services.Client; + using Microsoft.ServiceFabric.Services.Remoting.Client; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using TestApplication.Common; + using TestStatefulService.TestOrchestrations; + + [TestClass] + public class FunctionalTests + { + IRemoteClient serviceClient; + + [TestInitialize] + public void TestInitialize() + { + this.serviceClient = ServiceProxy.Create(new Uri(Constants.TestFabricApplicationAddress), new ServicePartitionKey(1)); + } + + [TestMethod] + public async Task Orchestration_With_ScheduledTasks_Finishes() + { + var result = await this.serviceClient.RunOrchestrationAsync(typeof(SimpleOrchestrationWithTasks).Name, null, 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 result = await this.serviceClient.RunOrchestrationAsync(typeof(SimpleOrchestrationWithTimer).Name, waitTime, TimeSpan.FromMinutes(2)); + + Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus); + Assert.AreEqual("\"Hello Gabbar\"", result.Output); + var orchestrationTime = result.CompletedTime - result.CreatedTime; + Assert.IsTrue(orchestrationTime > TimeSpan.FromSeconds(waitTime)); + Console.WriteLine($"Time for Orchestration : {orchestrationTime}, Timer Wait time : {waitTime}"); + } + + [TestMethod] + public async Task GenerationBasicTest() + { + int generationsToRun = 2; + + var result = await this.serviceClient.RunOrchestrationAsync(typeof(GenerationBasicOrchestration).Name, generationsToRun, TimeSpan.FromMinutes(2)); + + Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus); + Assert.AreEqual(generationsToRun.ToString(), result.Output, "Orchestration Result is wrong!!!"); + } + + [TestMethod] + public async Task Orchestration_With_SubOrchestration_Finishes() + { + var result = await this.serviceClient.RunOrchestrationAsync(typeof(SimpleOrchestrationWithSubOrchestration).Name, null, 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 instance = await this.serviceClient.StartTestOrchestrationAsync(new TestOrchestrationData() + { + NumberOfParallelTasks = 0, + NumberOfSerialTasks = 1, + MaxDelay = 0, + MinDelay = 0, + DelayUnit = TimeSpan.FromMilliseconds(1), + UseTimeoutTask = true, + ExecutionTimeout = TimeSpan.FromMinutes(1) + }); + var result = await this.serviceClient.WaitForOrchestration(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 + instance = await this.serviceClient.StartTestOrchestrationAsync(new TestOrchestrationData() + { + NumberOfParallelTasks = 0, + NumberOfSerialTasks = 1, + MaxDelay = 5, + MinDelay = 5, + DelayUnit = TimeSpan.FromSeconds(1), + UseTimeoutTask = true, + ExecutionTimeout = TimeSpan.FromSeconds(1) + }); + result = await this.serviceClient.WaitForOrchestration(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.serviceClient.StartTestOrchestrationWithInstanceIdAsync(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(InvalidOperationException), + $"Exception Type Check Failed : Task {i} returned an unexpected exception {result.Item2}"); + Assert.AreEqual($"An orchestration with id '{instanceId}' is already running.", result.Item2.Message, + $"Exception Message 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.serviceClient.WaitForOrchestration(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), + }; + + startTasks.Add(this.serviceClient.StartTestOrchestrationWithInstanceIdAsync(instanceIdPrefix + i, testData)); + } + + var scheduledInstances = await Task.WhenAll(startTasks); + await Task.Delay(TimeSpan.FromSeconds(2)); + + var allInstances = (await this.serviceClient.GetRunningOrchestrations()).ToList(); + Assert.AreEqual(numberOfInstances, allInstances.Count); + + foreach (var scheduledInstance in scheduledInstances) + { + var returnedInstance = allInstances.FirstOrDefault(i => string.Equals(i.InstanceId, scheduledInstance.InstanceId)); + Assert.IsNotNull(returnedInstance); + Assert.AreEqual(scheduledInstance.ExecutionId, returnedInstance.ExecutionId); + var runtimeState = await this.serviceClient.GetOrchestrationRuntimeState(scheduledInstance.InstanceId); + Console.WriteLine($"Runtime Orchestration State for {scheduledInstance} : {runtimeState}"); + } + + var waitTasks = new List>(); + foreach (var scheduledInstance in scheduledInstances) + { + waitTasks.Add(this.serviceClient.WaitForOrchestration(scheduledInstance, TimeSpan.FromMinutes(2))); + } + + var results = await Task.WhenAll(waitTasks); + foreach(var result in results) + { + Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus); + } + } + + [TestMethod] + public async Task Orchestration_With_Long_InstanceId_Name_Test() + { + var instanceId = $"MySomewhatLongNamespaceName|MyRelativelyLongRootEntityName|{Guid.NewGuid().ToString("N")}|MyRatherLongRuleName"; + var instance = await this.serviceClient.StartTestOrchestrationWithInstanceIdAsync(instanceId, new TestOrchestrationData() + { + NumberOfParallelTasks = 16, + NumberOfSerialTasks = 5, + MaxDelay = 0, + MinDelay = 0, + DelayUnit = TimeSpan.FromMilliseconds(1) + }); + + var result = await this.serviceClient.WaitForOrchestration(instance, TimeSpan.FromMinutes(1)); + Assert.IsNotNull(result); + Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus); + } + + [TestMethod] + public async Task QueryState_For_Latest_Execution() + { + var instanceId = nameof(QueryState_For_Latest_Execution); + var instance = await this.serviceClient.StartTestOrchestrationWithInstanceIdAsync(instanceId, new TestOrchestrationData() + { + NumberOfParallelTasks = 0, + NumberOfSerialTasks = 1, + MaxDelay = 15, + MinDelay = 15, + DelayUnit = TimeSpan.FromSeconds(1), + }); + + var state = await this.serviceClient.GetOrchestrationStateWithInstanceId(instanceId); + + Assert.IsNotNull(state); + Assert.AreEqual(instance.ExecutionId, state.OrchestrationInstance.ExecutionId); + + var result = await this.serviceClient.WaitForOrchestration(instance, TimeSpan.FromMinutes(2)); + Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus); + + var newInstance = await this.serviceClient.StartTestOrchestrationWithInstanceIdAsync(instanceId, new TestOrchestrationData() + { + NumberOfParallelTasks = 0, + NumberOfSerialTasks = 1, + MaxDelay = 0, + MinDelay = 0, + DelayUnit = TimeSpan.FromSeconds(1), + }); + + // We want to make sure that once an orchestration is complete, we can create another instance with the same id. + var newState = await this.serviceClient.GetOrchestrationStateWithInstanceId(instanceId); + + Assert.IsNotNull(newState); + Assert.AreEqual(newInstance.ExecutionId, newState.OrchestrationInstance.ExecutionId); + Assert.AreNotEqual(state.OrchestrationInstance.ExecutionId, newState.OrchestrationInstance.ExecutionId); + + result = await this.serviceClient.WaitForOrchestration(instance, TimeSpan.FromMinutes(2)); + Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus); + } + + [TestMethod] + public async Task Retry_OnException_Test() + { + int retryAttempts = 3; + var minumumExpectedTime = TimeSpan.FromSeconds(7); //This should be calculated based on RetryOptions in the Orchestration code. + + var result = await this.serviceClient.RunOrchestrationAsync(typeof(OrchestrationRunningIntoRetry).Name, retryAttempts, 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}, Minumum expected time : {minumumExpectedTime}"); + Assert.IsTrue(orchestrationTime > minumumExpectedTime); + } + + [TestMethod] + public async Task ForceTerminate_Terminates_LatestExecution() + { + var testData = new TestOrchestrationData() + { + NumberOfParallelTasks = 0, + NumberOfSerialTasks = 2, + MaxDelay = 5, + MinDelay = 5, + DelayUnit = TimeSpan.FromSeconds(1), + }; + + var instance = await this.serviceClient.StartTestOrchestrationAsync(testData); + await Task.Delay(TimeSpan.FromSeconds(1)); + + var reason = "Testing Terminate Functionality"; + await this.serviceClient.TerminateOrchestration(instance.InstanceId, reason); + var result = await this.serviceClient.WaitForOrchestration(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.serviceClient.StartTestOrchestrationAsync(testData); + await Task.Delay(TimeSpan.FromSeconds(1)); + + var reason = "Testing Terminate (Twice) Functionality"; + var tasks = new List(); + tasks.Add(this.serviceClient.TerminateOrchestration(instance.InstanceId, reason)); + tasks.Add(this.serviceClient.TerminateOrchestration(instance.InstanceId, reason)); + await Task.WhenAll(tasks); + var result = await this.serviceClient.WaitForOrchestration(instance, TimeSpan.FromMinutes(1)); + + Assert.AreEqual(OrchestrationStatus.Terminated, result.OrchestrationStatus); + Assert.AreEqual(reason, result.Output); + } + + [TestMethod] + public async Task Purge_Removes_State() + { + var result = await this.serviceClient.RunOrchestrationAsync(typeof(SimpleOrchestrationWithTasks).Name, null, TimeSpan.FromMinutes(2)); + Assert.AreEqual(OrchestrationStatus.Completed, result.OrchestrationStatus); + + var instance = result.OrchestrationInstance; + + await this.serviceClient.PurgeOrchestrationHistoryEventsAsync(); + + var state = await this.serviceClient.GetOrchestrationState(instance); + Assert.IsNull(state); + } + } +} diff --git a/Test/DurableTask.ServiceFabric.Test/Properties/AssemblyInfo.cs b/Test/DurableTask.ServiceFabric.Test/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..61fc9be86 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DurableTask.ServiceFabric.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DurableTask.ServiceFabric.Test")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("edc63ddb-8397-4aec-bf54-6395b2527075")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Test/DurableTask.ServiceFabric.Test/StressTests.cs b/Test/DurableTask.ServiceFabric.Test/StressTests.cs new file mode 100644 index 000000000..bddd4c53f --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Test/StressTests.cs @@ -0,0 +1,257 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric.Test +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Threading.Tasks; + using DurableTask.Core; + using DurableTask.Test.Orchestrations.Perf; + using Microsoft.ServiceFabric.Services.Client; + using Microsoft.ServiceFabric.Services.Remoting.Client; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using TestApplication.Common; + + [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 serviceClient = ServiceProxy.Create(new Uri(Constants.TestFabricApplicationAddress), new ServicePartitionKey(1)); + + Dictionary> results = new Dictionary>(); + + for (int attemptNumber = 0; attemptNumber < numberOfAttempts; attemptNumber++) + { + var instance = await serviceClient.StartTestOrchestrationWithInstanceIdAsync(instanceId, new TestOrchestrationData() + { + NumberOfParallelTasks = 0, + NumberOfSerialTasks = 1, + MaxDelay = 1980, + MinDelay = 1900, + DelayUnit = TimeSpan.FromMilliseconds(1), + }); + + var state = await serviceClient.WaitForOrchestration(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 serviceClient = ServiceProxy.Create(new Uri(Constants.TestFabricApplicationAddress), new ServicePartitionKey(1)); + var state = await serviceClient.RunOrchestrationAsync(typeof(ExecutionCountingOrchestration).Name, numberOfActivities, TimeSpan.FromMinutes(3)); + 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}"); + Assert.AreEqual(numberOfActivities.ToString(), state.Output); + } + } + + [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 serviceClient = ServiceProxy.Create(new Uri(Constants.TestFabricApplicationAddress), new ServicePartitionKey(1)); + Console.WriteLine($"Orchestration getting scheduled: {DateTime.Now}"); + + Stopwatch stopWatch = Stopwatch.StartNew(); + var state = await serviceClient.RunDriverOrchestrationAsync(driverConfig, 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 serviceClient = ServiceProxy.Create(new Uri(Constants.TestFabricApplicationAddress), new ServicePartitionKey(1)); + + List> results = new List>(); + List waitTasks = new List(); + Stopwatch stopWatch = Stopwatch.StartNew(); + for (int i = 0; i < numberOfInstances; i++) + { + var instance = await serviceClient.StartTestOrchestrationAsync(orchestrationInput); + waitTasks.Add(Task.Run(async () => + { + var state = await serviceClient.WaitForOrchestration(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 serviceClient.GetOrchestrationState(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.ServiceFabric.Test/Utilities.cs b/Test/DurableTask.ServiceFabric.Test/Utilities.cs new file mode 100644 index 000000000..ccfad3fe7 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Test/Utilities.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.ServiceFabric.Test +{ + using System; + using System.Diagnostics; + using System.Threading.Tasks; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Utilities + { + 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.ServiceFabric.Test/WaitForOrchestrationTests.cs b/Test/DurableTask.ServiceFabric.Test/WaitForOrchestrationTests.cs new file mode 100644 index 000000000..29ac3f2cc --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Test/WaitForOrchestrationTests.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.ServiceFabric.Test +{ + using System; + using System.Threading.Tasks; + using DurableTask.Core; + using DurableTask.Test.Orchestrations.Perf; + using Microsoft.ServiceFabric.Services.Client; + using Microsoft.ServiceFabric.Services.Remoting.Client; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using TestApplication.Common; + + [TestClass] + public class WaitForOrchestrationTests + { + IRemoteClient serviceClient; + + [TestInitialize] + public void TestInitialize() + { + this.serviceClient = ServiceProxy.Create(new Uri(Constants.TestFabricApplicationAddress), new ServicePartitionKey(1)); + } + + [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.serviceClient.WaitForOrchestration(instance, waitTime); + }); + + Assert.IsNull(result); + Assert.IsTrue(time < waitTime); + Console.WriteLine($"Full WaitTime : {waitTime}, Actual time taken for Wait {time}"); + } + + [TestMethod] + public async Task WaitForOrchestration_Returns_State_When_Orchestration_Already_Finished() + { + var instance = await this.serviceClient.StartTestOrchestrationAsync(new TestOrchestrationData() + { + NumberOfParallelTasks = 0, + NumberOfSerialTasks = 1, + MaxDelay = 0, + MinDelay = 0, + DelayUnit = TimeSpan.FromMilliseconds(1), + UseTimeoutTask = false, + }); + await this.serviceClient.WaitForOrchestration(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.serviceClient.WaitForOrchestration(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 instance = await this.serviceClient.StartTestOrchestrationAsync(new TestOrchestrationData() + { + NumberOfParallelTasks = 0, + NumberOfSerialTasks = 1, + MaxDelay = orchestrationTimeInSeconds, + MinDelay = orchestrationTimeInSeconds, + DelayUnit = TimeSpan.FromSeconds(1), + UseTimeoutTask = false, + }); + + OrchestrationState result = null; + var time = await Utilities.MeasureAsync(async () => + { + result = await this.serviceClient.WaitForOrchestration(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 instance = await this.serviceClient.StartTestOrchestrationAsync(new TestOrchestrationData() + { + NumberOfParallelTasks = 0, + NumberOfSerialTasks = 1, + MaxDelay = orchestrationTimeInSeconds, + MinDelay = orchestrationTimeInSeconds, + DelayUnit = TimeSpan.FromSeconds(1), + UseTimeoutTask = false, + }); + + OrchestrationState result = null; + var time = await Utilities.MeasureAsync(async () => + { + result = await this.serviceClient.WaitForOrchestration(instance, waitTime); + }); + + Assert.IsNull(result); + Assert.IsTrue(time > waitTime); + Assert.IsTrue(time < TimeSpan.FromSeconds(orchestrationTimeInSeconds)); + 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.serviceClient.WaitForOrchestration(instance, newWaitTime); + }); + + Assert.IsNotNull(result); + Assert.IsTrue(time < newWaitTime); + Assert.IsTrue(time > TimeSpan.FromSeconds(orchestrationTimeInSeconds - initialwaitTimeInSeconds)); //Theoritically, this could be flakey. The right way is to check for the difference being within a small internval, adjust this if needed based on the output below. + 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.ServiceFabric.Test/app.config b/Test/DurableTask.ServiceFabric.Test/app.config new file mode 100644 index 000000000..534e63b33 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Test/app.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Test/DurableTask.ServiceFabric.Test/packages.config b/Test/DurableTask.ServiceFabric.Test/packages.config new file mode 100644 index 000000000..e1a44ae1b --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Test/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Test/DurableTask.ServiceFabric.Tests.Tooling/DurableTask.ServiceFabric.Tests.Tooling.csproj b/Test/DurableTask.ServiceFabric.Tests.Tooling/DurableTask.ServiceFabric.Tests.Tooling.csproj new file mode 100644 index 000000000..9cca2b322 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Tests.Tooling/DurableTask.ServiceFabric.Tests.Tooling.csproj @@ -0,0 +1,103 @@ + + + + + + Debug + AnyCPU + {ED6817FD-A301-40CA-B287-1C40E7AAC7D1} + Exe + DurableTask.ServiceFabric.Tests.Tooling + DurableTask.ServiceFabric.Tests.Tooling + v4.5.1 + 512 + true + + + + x64 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x64 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\Microsoft.ServiceFabric.Data.2.5.216\lib\net45\Microsoft.ServiceFabric.Data.dll + + + ..\..\packages\Microsoft.ServiceFabric.Data.2.5.216\lib\net45\Microsoft.ServiceFabric.Data.Interfaces.dll + True + + + ..\..\packages\Microsoft.ServiceFabric.FabricTransport.Internal.2.5.216\lib\net45\Microsoft.ServiceFabric.FabricTransport.dll + + + ..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\Microsoft.ServiceFabric.Internal.dll + + + ..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\Microsoft.ServiceFabric.Internal.Strings.dll + + + ..\..\packages\Microsoft.ServiceFabric.Services.2.5.216\lib\net45\Microsoft.ServiceFabric.Services.dll + + + ..\..\packages\Microsoft.ServiceFabric.Services.Remoting.2.5.216\lib\net45\Microsoft.ServiceFabric.Services.Remoting.dll + + + + + ..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\System.Fabric.dll + True + + + ..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\System.Fabric.Management.ServiceModel.dll + + + ..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\System.Fabric.Management.ServiceModel.XmlSerializers.dll + + + ..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\System.Fabric.Strings.dll + True + + + + + + + + + + + + + + + + + + + + + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA} + DurableTask.Core + + + {ea90334d-3194-4266-81eb-eeca9c760315} + TestApplication.Common + + + + \ No newline at end of file diff --git a/Test/DurableTask.ServiceFabric.Tests.Tooling/Program.cs b/Test/DurableTask.ServiceFabric.Tests.Tooling/Program.cs new file mode 100644 index 000000000..e6e21f7c2 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Tests.Tooling/Program.cs @@ -0,0 +1,108 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric.Tests.Tooling +{ + using System; + using System.Threading.Tasks; + using DurableTask.Core; + using Microsoft.ServiceFabric.Services.Client; + using Microsoft.ServiceFabric.Services.Remoting.Client; + using TestApplication.Common; + + class Program + { + static IRemoteClient serviceClient = ServiceProxy.Create(new Uri("fabric:/TestFabricApplication/TestStatefulService"), new ServicePartitionKey(1)); + + static void Main(string[] args) + { + MainAsync().Wait(); + } + + static void PrintMenu() + { + Console.WriteLine("-----------------------------------"); + Console.WriteLine(" 1. List Orchestrations"); + Console.WriteLine(" 2. Get runtime state"); + Console.WriteLine(" 3. Terminate instance"); + Console.WriteLine("-----------------------------------"); + } + + static string Prompt(string message) + { + Console.WriteLine(message); + return Console.ReadLine(); + } + + static async Task MainAsync() + { + while (true) + { + try + { + PrintMenu(); + var action = Prompt("Type an option or hit enter to exit"); + + switch (action) + { + case "1": + await ListOrchestrations(); + break; + case "2": + await GetOrchestrationRuntimeState(); + break; + case "3": + await TerminateOrchestration(); + break; + default: + return; + } + } + catch (Exception ex) + { + Console.WriteLine($"Unhandled exception caught from the operation : {ex}"); + } + } + } + + private static async Task TerminateOrchestration() + { + var instanceId = Prompt("InstanceId?"); + var executionId = Prompt("ExecutionId?"); + var instance = new OrchestrationInstance() { InstanceId = instanceId, ExecutionId = executionId }; + await serviceClient.TerminateOrchestration(instanceId, executionId); + var state = await serviceClient.WaitForOrchestration(instance, TimeSpan.FromMinutes(1)); + if (state == null) + { + Console.WriteLine("Orchestration did not complete within 1 minute after termination, trying to get current state"); + state = await serviceClient.GetOrchestrationState(instance); + Console.WriteLine($"Current status : {state?.OrchestrationStatus}"); + } + } + + private static async Task GetOrchestrationRuntimeState() + { + var instanceId = Prompt("InstanceId?"); + var state = await serviceClient.GetOrchestrationRuntimeState(instanceId); + Console.WriteLine(state); + } + + private static async Task ListOrchestrations() + { + foreach (var instance in await serviceClient.GetRunningOrchestrations()) + { + Console.WriteLine(instance); + } + } + } +} diff --git a/Test/DurableTask.ServiceFabric.Tests.Tooling/Properties/AssemblyInfo.cs b/Test/DurableTask.ServiceFabric.Tests.Tooling/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..54cd56a06 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Tests.Tooling/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DurableTask.ServiceFabric.Tests.Tooling")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DurableTask.ServiceFabric.Tests.Tooling")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ed6817fd-a301-40ca-b287-1c40e7aac7d1")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Test/DurableTask.ServiceFabric.Tests.Tooling/app.config b/Test/DurableTask.ServiceFabric.Tests.Tooling/app.config new file mode 100644 index 000000000..884f9844f --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Tests.Tooling/app.config @@ -0,0 +1,3 @@ + + + diff --git a/Test/DurableTask.ServiceFabric.Tests.Tooling/packages.config b/Test/DurableTask.ServiceFabric.Tests.Tooling/packages.config new file mode 100644 index 000000000..4a980a8a5 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.Tests.Tooling/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Test/DurableTask.ServiceFabric.UnitTests/CountBasedFixedDelayRetryPolicyTests.cs b/Test/DurableTask.ServiceFabric.UnitTests/CountBasedFixedDelayRetryPolicyTests.cs new file mode 100644 index 000000000..3856a91c0 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.UnitTests/CountBasedFixedDelayRetryPolicyTests.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.ServiceFabric.UnitTests +{ + using System; + 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.ServiceFabric.UnitTests/DurableTask.ServiceFabric.UnitTests.csproj b/Test/DurableTask.ServiceFabric.UnitTests/DurableTask.ServiceFabric.UnitTests.csproj new file mode 100644 index 000000000..be7bec0a9 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.UnitTests/DurableTask.ServiceFabric.UnitTests.csproj @@ -0,0 +1,90 @@ + + + + + + Debug + AnyCPU + {31C9C932-6376-46D4-A68A-A0A7F2726D38} + Library + Properties + DurableTask.ServiceFabric.UnitTests + DurableTask.ServiceFabric.UnitTests + 512 + v4.5.1 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + False + ..\..\packages\System.Collections.Immutable.1.5.0\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + + + + + + + + + + + + + + + + + + + + + + + + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA} + DurableTask.Core + + + {5c617b24-4902-4992-8f71-ab14f00b74a8} + DurableTask.ServiceFabric + + + + + + Designer + + + + + + + + \ No newline at end of file diff --git a/Test/DurableTask.ServiceFabric.UnitTests/Measure.cs b/Test/DurableTask.ServiceFabric.UnitTests/Measure.cs new file mode 100644 index 000000000..b8558834b --- /dev/null +++ b/Test/DurableTask.ServiceFabric.UnitTests/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.ServiceFabric.UnitTests +{ + 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.ServiceFabric.UnitTests/OrchestrationInstanceComparerTests.cs b/Test/DurableTask.ServiceFabric.UnitTests/OrchestrationInstanceComparerTests.cs new file mode 100644 index 000000000..307b22a76 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.UnitTests/OrchestrationInstanceComparerTests.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 DurableTask.ServiceFabric.UnitTests +{ + 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.ServiceFabric.UnitTests/PersistentSessionTests.cs b/Test/DurableTask.ServiceFabric.UnitTests/PersistentSessionTests.cs new file mode 100644 index 000000000..bdc74243e --- /dev/null +++ b/Test/DurableTask.ServiceFabric.UnitTests/PersistentSessionTests.cs @@ -0,0 +1,56 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric.UnitTests +{ + 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.ServiceFabric.UnitTests/Properties/AssemblyInfo.cs b/Test/DurableTask.ServiceFabric.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..e73ff9a61 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DurableTask.ServiceFabric.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DurableTask.ServiceFabric.UnitTests")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("31c9c932-6376-46d4-a68a-a0a7f2726d38")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Test/DurableTask.ServiceFabric.UnitTests/SerializationPerformanceExperiments.cs b/Test/DurableTask.ServiceFabric.UnitTests/SerializationPerformanceExperiments.cs new file mode 100644 index 000000000..df2bc5e17 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.UnitTests/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.ServiceFabric.UnitTests +{ + using System; + using System.Collections.Generic; + using System.IO; + 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.ServiceFabric.UnitTests/TaskMessageItemTests.cs b/Test/DurableTask.ServiceFabric.UnitTests/TaskMessageItemTests.cs new file mode 100644 index 000000000..2881a08f2 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.UnitTests/TaskMessageItemTests.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 DurableTask.ServiceFabric.UnitTests +{ + 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.ServiceFabric.UnitTests/TimerFiredEventComparerTests.cs b/Test/DurableTask.ServiceFabric.UnitTests/TimerFiredEventComparerTests.cs new file mode 100644 index 000000000..d24fd2aa3 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.UnitTests/TimerFiredEventComparerTests.cs @@ -0,0 +1,137 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric.UnitTests +{ + using System; + using System.Collections.Immutable; + 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; + + // Add them in reverse order to expected order just to make the test case more interesting. + for (int i = numberOfMessages; i >= 1; i--) + { + builder.Add(new Message($"Message{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); + + // Now enumerating the items should result in right order. + for (int i = 1; i <= numberOfMessages; i++) + { + var min = timerMessages.Min; + timerMessages = timerMessages.Remove(min); + Assert.AreEqual($"Message{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; + + // Add them in reverse order to expected order just to make the test case more interesting. + for (int i = numberOfMessages; i >= 1; i--) + { + builder.Add(new Message($"Message{6-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 = 1; 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($"Message{6 - 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. + for (int i = numberOfMessages; i >= 1; i--) + { + builder.Add(new Message("Message", 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.ServiceFabric.UnitTests/app.config b/Test/DurableTask.ServiceFabric.UnitTests/app.config new file mode 100644 index 000000000..e51d1d527 --- /dev/null +++ b/Test/DurableTask.ServiceFabric.UnitTests/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/Test/DurableTask.ServiceFabric.UnitTests/packages.config b/Test/DurableTask.ServiceFabric.UnitTests/packages.config new file mode 100644 index 000000000..f3a2d4d6d --- /dev/null +++ b/Test/DurableTask.ServiceFabric.UnitTests/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Test/DurableTask.Test.Orchestrations/Perf/DriverOrchestration.cs b/Test/DurableTask.Test.Orchestrations/Perf/DriverOrchestration.cs new file mode 100644 index 000000000..bd7f162aa --- /dev/null +++ b/Test/DurableTask.Test.Orchestrations/Perf/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.Perf +{ + 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/Perf/DriverOrchestrationData.cs b/Test/DurableTask.Test.Orchestrations/Perf/DriverOrchestrationData.cs new file mode 100644 index 000000000..e8d626e94 --- /dev/null +++ b/Test/DurableTask.Test.Orchestrations/Perf/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.Perf +{ + 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/Perf/ExecutionCountingActivity.cs b/Test/DurableTask.Test.Orchestrations/Perf/ExecutionCountingActivity.cs new file mode 100644 index 000000000..12e4f3e26 --- /dev/null +++ b/Test/DurableTask.Test.Orchestrations/Perf/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.Perf +{ + 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/Perf/ExecutionCountingOrchestration.cs b/Test/DurableTask.Test.Orchestrations/Perf/ExecutionCountingOrchestration.cs new file mode 100644 index 000000000..0c309a757 --- /dev/null +++ b/Test/DurableTask.Test.Orchestrations/Perf/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.Perf +{ + 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/Perf/RandomTimeWaitingTask.cs b/Test/DurableTask.Test.Orchestrations/Perf/RandomTimeWaitingTask.cs new file mode 100644 index 000000000..392e8feca --- /dev/null +++ b/Test/DurableTask.Test.Orchestrations/Perf/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.Perf +{ + 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/Perf/RandomTimeWaitingTaskInput.cs b/Test/DurableTask.Test.Orchestrations/Perf/RandomTimeWaitingTaskInput.cs new file mode 100644 index 000000000..395a41e68 --- /dev/null +++ b/Test/DurableTask.Test.Orchestrations/Perf/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.Perf +{ + 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/Perf/TestOrchestration.cs b/Test/DurableTask.Test.Orchestrations/Perf/TestOrchestration.cs new file mode 100644 index 000000000..df44a1a2d --- /dev/null +++ b/Test/DurableTask.Test.Orchestrations/Perf/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.Perf +{ + 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/Perf/TestOrchestrationData.cs b/Test/DurableTask.Test.Orchestrations/Perf/TestOrchestrationData.cs new file mode 100644 index 000000000..92a34fa07 --- /dev/null +++ b/Test/DurableTask.Test.Orchestrations/Perf/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.Perf +{ + 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/DurableTask.Test.Orchestrations/Stress/TestTask.cs b/Test/DurableTask.Test.Orchestrations/Stress/TestTask.cs index 2faf38c5b..49835adac 100644 --- a/Test/DurableTask.Test.Orchestrations/Stress/TestTask.cs +++ b/Test/DurableTask.Test.Orchestrations/Stress/TestTask.cs @@ -33,7 +33,7 @@ protected override async Task ExecuteAsync(TaskContext context, TestTaskDat Random random = new Random(); int minutesToSleep = random.Next(0, input.MaxDelayInMinutes); - Console.WriteLine(string.Format("[InstanceId: {0}, ExecutionId: {1}, TaskId: {2}, Counter: {3}] ---> Sleeping for '{4}'", + Console.WriteLine(string.Format("[InstanceId: {0}, ExecutionId: {1}, TaskId: {2}, Counter: {3}] ---> Sleeping for '{4}'", instance.InstanceId, instance.ExecutionId, input.TaskId, c, minutesToSleep)); await Task.Delay(TimeSpan.FromMinutes(minutesToSleep)); diff --git a/Test/TestFabricApplication/TestApplication.Common/IOwinAppBuilder.cs b/Test/TestFabricApplication/TestApplication.Common/IOwinAppBuilder.cs new file mode 100644 index 000000000..bfd7d02e4 --- /dev/null +++ b/Test/TestFabricApplication/TestApplication.Common/IOwinAppBuilder.cs @@ -0,0 +1,22 @@ +// ---------------------------------------------------------------------------------- +// 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 +{ + using Owin; + + public interface IOwinAppBuilder + { + void Configuration(IAppBuilder appBuilder); + } +} diff --git a/Test/TestFabricApplication/TestApplication.Common/IRemoteClient.cs b/Test/TestFabricApplication/TestApplication.Common/IRemoteClient.cs new file mode 100644 index 000000000..5cc34574e --- /dev/null +++ b/Test/TestFabricApplication/TestApplication.Common/IRemoteClient.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 TestApplication.Common +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using DurableTask.Core; + using DurableTask.Test.Orchestrations.Perf; + using Microsoft.ServiceFabric.Services.Remoting; + + public interface IRemoteClient : IService + { + Task> GetRunningOrchestrations(); + + Task GetOrchestrationRuntimeState(string instanceId); + + Task StartOrchestrationAsync(string orchestrationTypeName, object input); + + Task RunOrchestrationAsync(string orchestrationTypeName, object input, TimeSpan waitTimeout); + + Task RunDriverOrchestrationAsync(DriverOrchestrationData input, TimeSpan waitTimeout); + + Task StartTestOrchestrationAsync(TestOrchestrationData input); + + Task GetOrchestrationState(OrchestrationInstance instance); + + Task StartTestOrchestrationWithInstanceIdAsync(string instanceId, TestOrchestrationData input); + + Task GetOrchestrationStateWithInstanceId(string instanceId); + + Task WaitForOrchestration(OrchestrationInstance instance, TimeSpan waitTimeout); + + Task PurgeOrchestrationHistoryEventsAsync(); + + Task TerminateOrchestration(string instanceId, string reason); + } +} diff --git a/Test/TestFabricApplication/TestApplication.Common/OwinCommunicationListener.cs b/Test/TestFabricApplication/TestApplication.Common/OwinCommunicationListener.cs new file mode 100644 index 000000000..4726521b9 --- /dev/null +++ b/Test/TestFabricApplication/TestApplication.Common/OwinCommunicationListener.cs @@ -0,0 +1,139 @@ +// ---------------------------------------------------------------------------------- +// 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 +{ + using System; + using System.Diagnostics; + using System.Fabric; + using System.Fabric.Description; + using System.Globalization; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Owin.Hosting; + using Microsoft.ServiceFabric.Services.Communication.Runtime; + + public class OwinCommunicationListener : ICommunicationListener + { + private readonly ServiceContext serviceContext; + + /// + /// OWIN server handle. + /// + private IDisposable serverHandle; + + private IOwinAppBuilder startup; + private string publishAddress; + private string listeningAddress; + private string appRoot; + + public OwinCommunicationListener(IOwinAppBuilder startup, ServiceContext serviceContext) + : this(null, startup, serviceContext) + { + } + + public OwinCommunicationListener(string appRoot, IOwinAppBuilder startup, ServiceContext serviceContext) + { + this.startup = startup; + this.appRoot = appRoot; + this.serviceContext = serviceContext; + } + + public Task OpenAsync(CancellationToken cancellationToken) + { + Trace.WriteLine("Initialize"); + + EndpointResourceDescription serviceEndpoint = this.serviceContext.CodePackageActivationContext.GetEndpoint("ServiceEndpoint"); + int port = serviceEndpoint.Port; + + if (this.serviceContext is StatefulServiceContext) + { + StatefulServiceContext statefulInitParams = (StatefulServiceContext)this.serviceContext; + + this.listeningAddress = String.Format( + CultureInfo.InvariantCulture, + "http://+:{0}/{1}/{2}/{3}/", + port, + statefulInitParams.PartitionId, + statefulInitParams.ReplicaId, + Guid.NewGuid()); + } + else if (this.serviceContext is StatelessServiceContext) + { + this.listeningAddress = String.Format( + CultureInfo.InvariantCulture, + "http://+:{0}/{1}", + port, + string.IsNullOrWhiteSpace(this.appRoot) + ? "" + : this.appRoot.TrimEnd('/') + '/'); + } + else + { + throw new InvalidOperationException(); + } + + this.publishAddress = this.listeningAddress.Replace("+", FabricRuntime.GetNodeContext().IPAddressOrFQDN); + + Trace.WriteLine(String.Format("Opening on {0}", this.publishAddress)); + + try + { + Trace.WriteLine(String.Format("Starting web server on {0}", this.listeningAddress)); + + this.serverHandle = WebApp.Start(this.listeningAddress, appBuilder => this.startup.Configuration(appBuilder)); + + return Task.FromResult(this.publishAddress); + } + catch (Exception ex) + { + Trace.WriteLine(ex); + + this.StopWebServer(); + + throw; + } + } + + public Task CloseAsync(CancellationToken cancellationToken) + { + Trace.WriteLine("Close"); + + this.StopWebServer(); + + return Task.FromResult(true); + } + + public void Abort() + { + Trace.WriteLine("Abort"); + + this.StopWebServer(); + } + + private void StopWebServer() + { + if (this.serverHandle != null) + { + try + { + this.serverHandle.Dispose(); + } + catch (ObjectDisposedException) + { + // no-op + } + } + } + } +} diff --git a/Test/TestFabricApplication/TestApplication.Common/Properties/AssemblyInfo.cs b/Test/TestFabricApplication/TestApplication.Common/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..5944d33a1 --- /dev/null +++ b/Test/TestFabricApplication/TestApplication.Common/Properties/AssemblyInfo.cs @@ -0,0 +1,48 @@ +// ---------------------------------------------------------------------------------- +// 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.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("TestApplication.Common")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TestApplication.Common")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ea90334d-3194-4266-81eb-eeca9c760315")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Test/TestFabricApplication/TestApplication.Common/TestApplication.Common.csproj b/Test/TestFabricApplication/TestApplication.Common/TestApplication.Common.csproj new file mode 100644 index 000000000..91f58d5c2 --- /dev/null +++ b/Test/TestFabricApplication/TestApplication.Common/TestApplication.Common.csproj @@ -0,0 +1,118 @@ + + + + + Debug + AnyCPU + {EA90334D-3194-4266-81EB-EECA9C760315} + Library + Properties + TestApplication.Common + TestApplication.Common + v4.5.1 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + True + + + ..\..\..\packages\Microsoft.Owin.Hosting.3.0.1\lib\net45\Microsoft.Owin.Hosting.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.Data.2.5.216\lib\net45\Microsoft.ServiceFabric.Data.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.Data.2.5.216\lib\net45\Microsoft.ServiceFabric.Data.Interfaces.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.FabricTransport.Internal.2.5.216\lib\net45\Microsoft.ServiceFabric.FabricTransport.dll + + + ..\..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\Microsoft.ServiceFabric.Internal.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\Microsoft.ServiceFabric.Internal.Strings.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.Services.2.5.216\lib\net45\Microsoft.ServiceFabric.Services.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.Services.Remoting.2.5.216\lib\net45\Microsoft.ServiceFabric.Services.Remoting.dll + + + ..\..\..\packages\Owin.1.0\lib\net40\Owin.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.Services.2.5.216\lib\net45\ServiceFabricServiceModel.dll + True + + + + + ..\..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\System.Fabric.dll + True + + + + + + + + + + + + + + + + + + + + + + + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA} + DurableTask.Core + + + {2bc2a5ad-4200-41b9-a034-332bb1d7187b} + DurableTask.Test.Orchestrations + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestApplication.Common/app.config b/Test/TestFabricApplication/TestApplication.Common/app.config new file mode 100644 index 000000000..de5386a47 --- /dev/null +++ b/Test/TestFabricApplication/TestApplication.Common/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestApplication.Common/packages.config b/Test/TestFabricApplication/TestApplication.Common/packages.config new file mode 100644 index 000000000..67b4736a6 --- /dev/null +++ b/Test/TestFabricApplication/TestApplication.Common/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestFabricApplication/ApplicationPackageRoot/ApplicationManifest.xml b/Test/TestFabricApplication/TestFabricApplication/ApplicationPackageRoot/ApplicationManifest.xml new file mode 100644 index 000000000..288a24134 --- /dev/null +++ b/Test/TestFabricApplication/TestFabricApplication/ApplicationPackageRoot/ApplicationManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestFabricApplication/ApplicationParameters/Cloud.xml b/Test/TestFabricApplication/TestFabricApplication/ApplicationParameters/Cloud.xml new file mode 100644 index 000000000..683ffaa5b --- /dev/null +++ b/Test/TestFabricApplication/TestFabricApplication/ApplicationParameters/Cloud.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestFabricApplication/ApplicationParameters/Local.1Node.xml b/Test/TestFabricApplication/TestFabricApplication/ApplicationParameters/Local.1Node.xml new file mode 100644 index 000000000..3411afa5b --- /dev/null +++ b/Test/TestFabricApplication/TestFabricApplication/ApplicationParameters/Local.1Node.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestFabricApplication/ApplicationParameters/Local.5Node.xml b/Test/TestFabricApplication/TestFabricApplication/ApplicationParameters/Local.5Node.xml new file mode 100644 index 000000000..683ffaa5b --- /dev/null +++ b/Test/TestFabricApplication/TestFabricApplication/ApplicationParameters/Local.5Node.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestFabricApplication/PublishProfiles/Cloud.xml b/Test/TestFabricApplication/TestFabricApplication/PublishProfiles/Cloud.xml new file mode 100644 index 000000000..1301dd83f --- /dev/null +++ b/Test/TestFabricApplication/TestFabricApplication/PublishProfiles/Cloud.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestFabricApplication/PublishProfiles/Local.1Node.xml b/Test/TestFabricApplication/TestFabricApplication/PublishProfiles/Local.1Node.xml new file mode 100644 index 000000000..6e1403e96 --- /dev/null +++ b/Test/TestFabricApplication/TestFabricApplication/PublishProfiles/Local.1Node.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestFabricApplication/PublishProfiles/Local.5Node.xml b/Test/TestFabricApplication/TestFabricApplication/PublishProfiles/Local.5Node.xml new file mode 100644 index 000000000..f42d759c3 --- /dev/null +++ b/Test/TestFabricApplication/TestFabricApplication/PublishProfiles/Local.5Node.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestFabricApplication/Scripts/Deploy-FabricApplication.ps1 b/Test/TestFabricApplication/TestFabricApplication/Scripts/Deploy-FabricApplication.ps1 new file mode 100644 index 000000000..2897b10e8 --- /dev/null +++ b/Test/TestFabricApplication/TestFabricApplication/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/TestFabricApplication/TestFabricApplication.sfproj b/Test/TestFabricApplication/TestFabricApplication/TestFabricApplication.sfproj new file mode 100644 index 000000000..fdbc1b301 --- /dev/null +++ b/Test/TestFabricApplication/TestFabricApplication/TestFabricApplication.sfproj @@ -0,0 +1,47 @@ + + + + + ca924781-ae52-45fa-a817-c39c822fb782 + 2.1 + 1.5 + 1.6.6 + + + + 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/TestFabricApplication/app.config b/Test/TestFabricApplication/TestFabricApplication/app.config new file mode 100644 index 000000000..0b6858c0c --- /dev/null +++ b/Test/TestFabricApplication/TestFabricApplication/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestFabricApplication/packages.config b/Test/TestFabricApplication/TestFabricApplication/packages.config new file mode 100644 index 000000000..0b2350f2a --- /dev/null +++ b/Test/TestFabricApplication/TestFabricApplication/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestStatefulService/App.config b/Test/TestFabricApplication/TestStatefulService/App.config new file mode 100644 index 000000000..e58598c08 --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/App.config @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestStatefulService/PackageRoot/Config/Settings.xml b/Test/TestFabricApplication/TestStatefulService/PackageRoot/Config/Settings.xml new file mode 100644 index 000000000..be6a4cdfa --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/PackageRoot/Config/Settings.xml @@ -0,0 +1,16 @@ + + + +
+ +
+ +
+ + + + diff --git a/Test/TestFabricApplication/TestStatefulService/PackageRoot/ServiceManifest.xml b/Test/TestFabricApplication/TestStatefulService/PackageRoot/ServiceManifest.xml new file mode 100644 index 000000000..3d2fbe543 --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/PackageRoot/ServiceManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + TestStatefulService.exe + + + + + + + + + + + + + + + + + diff --git a/Test/TestFabricApplication/TestStatefulService/Program.cs b/Test/TestFabricApplication/TestStatefulService/Program.cs new file mode 100644 index 000000000..c5f965b25 --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/Program.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 TestStatefulService +{ + using System; + using System.Diagnostics; + using System.Threading; + 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("TestStatefulServiceType", + context => new TestStatefulService(context)).GetAwaiter().GetResult(); + + ServiceEventSource.Current.ServiceTypeRegistered(Process.GetCurrentProcess().Id, typeof(TestStatefulService).Name); + + // Prevents this host process from terminating so services keep running. + Thread.Sleep(Timeout.Infinite); + } + catch (Exception e) + { + ServiceEventSource.Current.ServiceHostInitializationFailed(e.ToString()); + throw; + } + } + } +} diff --git a/Test/TestFabricApplication/TestStatefulService/Properties/AssemblyInfo.cs b/Test/TestFabricApplication/TestStatefulService/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..82065103e --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/Properties/AssemblyInfo.cs @@ -0,0 +1,48 @@ +// ---------------------------------------------------------------------------------- +// 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.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("TestStatefulService")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TestStatefulService")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("95d6c9c7-f7a0-4125-840d-1854bec24978")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Test/TestFabricApplication/TestStatefulService/ServiceEventSource.cs b/Test/TestFabricApplication/TestStatefulService/ServiceEventSource.cs new file mode 100644 index 000000000..b1a8eb848 --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/ServiceEventSource.cs @@ -0,0 +1,189 @@ +// ---------------------------------------------------------------------------------- +// 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 TestStatefulService +{ + using System; + using System.Diagnostics.Tracing; + using System.Threading.Tasks; + using Microsoft.ServiceFabric.Services.Runtime; + + [EventSource(Name = "MyCompany-TestFabricApplication-TestStatefulService")] + internal sealed class ServiceEventSource : EventSource + { + public static readonly ServiceEventSource Current = new ServiceEventSource(); + + static ServiceEventSource() + { + // A workaround for the problem where ETW activities do not get tracked until Tasks infrastructure is initialized. + // This problem will be fixed in .NET Framework 4.6.2. + Task.Run(() => { }); + } + + // Instance constructor is private to enforce singleton semantics + private ServiceEventSource() : base() { } + + #region Keywords + // Event keywords can be used to categorize events. + // Each keyword is a bit flag. A single event can be associated with multiple keywords (via EventAttribute.Keywords property). + // Keywords must be defined as a public class named 'Keywords' inside EventSource that uses them. + public static class Keywords + { + public const EventKeywords Requests = (EventKeywords)0x1L; + public const EventKeywords ServiceInitialization = (EventKeywords)0x2L; + } + #endregion + + #region Events + // Define an instance method for each event you want to record and apply an [Event] attribute to it. + // The method name is the name of the event. + // Pass any parameters you want to record with the event (only primitive integer types, DateTime, Guid & string are allowed). + // Each event method implementation should check whether the event source is enabled, and if it is, call WriteEvent() method to raise the event. + // The number and types of arguments passed to every event method must exactly match what is passed to WriteEvent(). + // Put [NonEvent] attribute on all methods that do not define an event. + // For more information see https://msdn.microsoft.com/en-us/library/system.diagnostics.tracing.eventsource.aspx + + [NonEvent] + public void Message(string message, params object[] args) + { + if (this.IsEnabled()) + { + string finalMessage = string.Format(message, args); + Message(finalMessage); + } + } + + private const int MessageEventId = 1; + [Event(MessageEventId, Level = EventLevel.Informational, Message = "{0}")] + public void Message(string message) + { + if (this.IsEnabled()) + { + WriteEvent(MessageEventId, message); + } + } + + [NonEvent] + public void ServiceMessage(StatefulService service, string message, params object[] args) + { + if (this.IsEnabled()) + { + string finalMessage = string.Format(message, args); + ServiceMessage( + service.Context.ServiceName.ToString(), + service.Context.ServiceTypeName, + service.Context.ReplicaId, + service.Context.PartitionId, + service.Context.CodePackageActivationContext.ApplicationName, + service.Context.CodePackageActivationContext.ApplicationTypeName, + service.Context.NodeContext.NodeName, + finalMessage); + } + } + + // For very high-frequency events it might be advantageous to raise events using WriteEventCore API. + // This results in more efficient parameter handling, but requires explicit allocation of EventData structure and unsafe code. + // To enable this code path, define UNSAFE conditional compilation symbol and turn on unsafe code support in project properties. + private const int ServiceMessageEventId = 2; + [Event(ServiceMessageEventId, Level = EventLevel.Informational, Message = "{7}")] + private +#if UNSAFE + unsafe +#endif + void ServiceMessage( + string serviceName, + string serviceTypeName, + long replicaOrInstanceId, + Guid partitionId, + string applicationName, + string applicationTypeName, + string nodeName, + string message) + { +#if !UNSAFE + WriteEvent(ServiceMessageEventId, serviceName, serviceTypeName, replicaOrInstanceId, partitionId, applicationName, applicationTypeName, nodeName, message); +#else + const int numArgs = 8; + fixed (char* pServiceName = serviceName, pServiceTypeName = serviceTypeName, pApplicationName = applicationName, pApplicationTypeName = applicationTypeName, pNodeName = nodeName, pMessage = message) + { + EventData* eventData = stackalloc EventData[numArgs]; + eventData[0] = new EventData { DataPointer = (IntPtr) pServiceName, Size = SizeInBytes(serviceName) }; + eventData[1] = new EventData { DataPointer = (IntPtr) pServiceTypeName, Size = SizeInBytes(serviceTypeName) }; + eventData[2] = new EventData { DataPointer = (IntPtr) (&replicaOrInstanceId), Size = sizeof(long) }; + eventData[3] = new EventData { DataPointer = (IntPtr) (&partitionId), Size = sizeof(Guid) }; + eventData[4] = new EventData { DataPointer = (IntPtr) pApplicationName, Size = SizeInBytes(applicationName) }; + eventData[5] = new EventData { DataPointer = (IntPtr) pApplicationTypeName, Size = SizeInBytes(applicationTypeName) }; + eventData[6] = new EventData { DataPointer = (IntPtr) pNodeName, Size = SizeInBytes(nodeName) }; + eventData[7] = new EventData { DataPointer = (IntPtr) pMessage, Size = SizeInBytes(message) }; + + WriteEventCore(ServiceMessageEventId, numArgs, eventData); + } +#endif + } + + private const int ServiceTypeRegisteredEventId = 3; + [Event(ServiceTypeRegisteredEventId, Level = EventLevel.Informational, Message = "Service host process {0} registered service type {1}", Keywords = Keywords.ServiceInitialization)] + public void ServiceTypeRegistered(int hostProcessId, string serviceType) + { + WriteEvent(ServiceTypeRegisteredEventId, hostProcessId, serviceType); + } + + private const int ServiceHostInitializationFailedEventId = 4; + [Event(ServiceHostInitializationFailedEventId, Level = EventLevel.Error, Message = "Service host initialization failed", Keywords = Keywords.ServiceInitialization)] + public void ServiceHostInitializationFailed(string exception) + { + WriteEvent(ServiceHostInitializationFailedEventId, exception); + } + + // A pair of events sharing the same name prefix with a "Start"/"Stop" suffix implicitly marks boundaries of an event tracing activity. + // These activities can be automatically picked up by debugging and profiling tools, which can compute their execution time, child activities, + // and other statistics. + private const int ServiceRequestStartEventId = 5; + [Event(ServiceRequestStartEventId, Level = EventLevel.Informational, Message = "Service request '{0}' started", Keywords = Keywords.Requests)] + public void ServiceRequestStart(string requestTypeName) + { + WriteEvent(ServiceRequestStartEventId, requestTypeName); + } + + private const int ServiceRequestStopEventId = 6; + [Event(ServiceRequestStopEventId, Level = EventLevel.Informational, Message = "Service request '{0}' finished", Keywords = Keywords.Requests)] + public void ServiceRequestStop(string requestTypeName) + { + WriteEvent(ServiceRequestStopEventId, requestTypeName); + } + + private const int ServiceRequestFailedEventId = 7; + [Event(ServiceRequestFailedEventId, Level = EventLevel.Error, Message = "Service request '{0}' failed", Keywords = Keywords.Requests)] + public void ServiceRequestFailed(string requestTypeName, string exception) + { + WriteEvent(ServiceRequestFailedEventId, exception); + } + #endregion + + #region Private methods +#if UNSAFE + private int SizeInBytes(string s) + { + if (s == null) + { + return 0; + } + else + { + return (s.Length + 1) * sizeof(char); + } + } +#endif + #endregion + } +} diff --git a/Test/TestFabricApplication/TestStatefulService/Startup.cs b/Test/TestFabricApplication/TestStatefulService/Startup.cs new file mode 100644 index 000000000..f371d998e --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/Startup.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 TestStatefulService +{ + using System.Web.Http; + using Owin; + using TestApplication.Common; + + class Startup : IOwinAppBuilder + { + public void Configuration(IAppBuilder appBuilder) + { + System.Net.ServicePointManager.DefaultConnectionLimit = 256; + + HttpConfiguration config = new HttpConfiguration(); + + config.MapHttpAttributeRoutes(); + + appBuilder.UseWebApi(config); + } + } +} diff --git a/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/CounterException.cs b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/CounterException.cs new file mode 100644 index 000000000..2854d6895 --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/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 TestStatefulService.TestOrchestrations +{ + 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/TestStatefulService/TestOrchestrations/ExceptionThrowingTask.cs b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/ExceptionThrowingTask.cs new file mode 100644 index 000000000..b23bf45a6 --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/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 TestStatefulService.TestOrchestrations +{ + using DurableTask.Core; + + 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/TestStatefulService/TestOrchestrations/GenerationBasicOrchestration.cs b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/GenerationBasicOrchestration.cs new file mode 100644 index 000000000..91896b1b6 --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/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 TestStatefulService.TestOrchestrations +{ + 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/TestStatefulService/TestOrchestrations/GetUserTask.cs b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/GetUserTask.cs new file mode 100644 index 000000000..0251db70c --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/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 TestStatefulService.TestOrchestrations +{ + using DurableTask.Core; + + class GetUserTask : TaskActivity + { + protected override string Execute(TaskContext context, string input) + { + return "Gabbar"; + } + } +} diff --git a/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/GreetUserTask.cs b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/GreetUserTask.cs new file mode 100644 index 000000000..6a35c553c --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/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 TestStatefulService.TestOrchestrations +{ + using DurableTask.Core; + + class GreetUserTask : TaskActivity + { + protected override string Execute(TaskContext context, string user) + { + return "Hello " + user; + } + } +} diff --git a/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/OrchestrationRunningIntoRetry.cs b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/OrchestrationRunningIntoRetry.cs new file mode 100644 index 000000000..cbf400cf5 --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/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 TestStatefulService.TestOrchestrations +{ + 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/TestStatefulService/TestOrchestrations/SimpleOrchestrationWithSubOrchestration.cs b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/SimpleOrchestrationWithSubOrchestration.cs new file mode 100644 index 000000000..4b94cc0d3 --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/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 TestStatefulService.TestOrchestrations +{ + 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/TestStatefulService/TestOrchestrations/SimpleOrchestrationWithTasks.cs b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/SimpleOrchestrationWithTasks.cs new file mode 100644 index 000000000..db424a550 --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/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 TestStatefulService.TestOrchestrations +{ + 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/TestStatefulService/TestOrchestrations/SimpleOrchestrationWithTimer.cs b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/SimpleOrchestrationWithTimer.cs new file mode 100644 index 000000000..dc03181bc --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/TestOrchestrations/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 TestStatefulService.TestOrchestrations +{ + 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/TestStatefulService/TestStatefulService.cs b/Test/TestFabricApplication/TestStatefulService/TestStatefulService.cs new file mode 100644 index 000000000..18eb1c9c0 --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/TestStatefulService.cs @@ -0,0 +1,270 @@ +// ---------------------------------------------------------------------------------- +// 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 TestStatefulService +{ + using System; + using System.Collections.Generic; + using System.Fabric; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using DurableTask.Core; + using DurableTask.ServiceFabric; + using DurableTask.Test.Orchestrations.Perf; + using Microsoft.ServiceFabric.Services.Communication.Runtime; + using Microsoft.ServiceFabric.Services.Remoting.Runtime; + using Microsoft.ServiceFabric.Services.Runtime; + using TestApplication.Common; + using TestOrchestrations; + + /// + /// An instance of this class is created for each service replica by the Service Fabric runtime. + /// + internal sealed class TestStatefulService : StatefulService, IRemoteClient + { + readonly FabricOrchestrationProviderFactory fabricProviderFactory; + + FabricOrchestrationProvider fabricProvider; + TaskHubWorker worker; + TaskHubClient client; + ReplicaRole currentRole; + + public TestStatefulService(StatefulServiceContext context) : base(context) + { + var settings = new FabricOrchestrationProviderSettings(); + settings.TaskOrchestrationDispatcherSettings.DispatcherCount = 5; + settings.TaskActivityDispatcherSettings.DispatcherCount = 5; + + this.fabricProviderFactory = new FabricOrchestrationProviderFactory(this.StateManager, settings); + this.fabricProvider = this.fabricProviderFactory.CreateProvider(); + this.client = new TaskHubClient(fabricProvider.OrchestrationServiceClient); + } + + /// + /// 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 new[] + { + //new ServiceReplicaListener(initParams => new OwinCommunicationListener("TestStatefulService", new Startup(), initParams)) + new ServiceReplicaListener(this.CreateServiceRemotingListener) + }; + } + + /// + /// 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) + { + if (this.fabricProvider == null) + { + this.fabricProvider = this.fabricProviderFactory.CreateProvider(); + this.client = new TaskHubClient(this.fabricProvider.OrchestrationServiceClient); + } + + this.worker = new TaskHubWorker(this.fabricProvider.OrchestrationService); + + await this.worker + .AddTaskOrchestrations(KnownOrchestrationInstances.Values.Select(instance => new DefaultObjectCreator(instance)).ToArray()) + .AddTaskOrchestrations(KnownOrchestrationTypeNames.Values.ToArray()) + .AddTaskActivities(KnownActivities) + .StartAsync(); + + this.worker.TaskActivityDispatcher.IncludeDetails = true; + } + + protected override async Task OnChangeRoleAsync(ReplicaRole newRole, CancellationToken cancellationToken) + { + ServiceEventSource.Current.ServiceRequestStart($"Fabric On Change Role Async, current role = {this.currentRole}, new role = {newRole}"); + if (newRole != ReplicaRole.Primary && this.currentRole == ReplicaRole.Primary) + { + await ShutdownAsync(); + } + this.currentRole = newRole; + ServiceEventSource.Current.ServiceRequestStop($"Fabric On Change Role Async, current role = {this.currentRole}"); + } + + protected override async Task OnCloseAsync(CancellationToken cancellationToken) + { + ServiceEventSource.Current.ServiceMessage(this, "OnCloseAsync - will shutdown primary if not already done"); + await ShutdownAsync(); + } + + public async Task RunOrchestrationAsync(string orchestrationTypeName, object input, TimeSpan waitTimeout) + { + OrchestrationInstance instance = await StartOrchestrationAsync(orchestrationTypeName, input); + return await client.WaitForOrchestrationAsync(instance, waitTimeout); + } + + public async Task StartOrchestrationAsync(string orchestrationTypeName, object input) + { + if (orchestrationTypeName == typeof(ExecutionCountingOrchestration).Name) + { + // See ExecutionCountingActivity notes on why we do this. + ExecutionCountingActivity.Counter = 0; + } + + var instance = await client.CreateOrchestrationInstanceAsync(GetOrchestrationType(orchestrationTypeName), input); + return instance; + } + + public async Task RunDriverOrchestrationAsync(DriverOrchestrationData input, TimeSpan waitTimeout) + { + return await this.RunOrchestrationAsync(typeof(DriverOrchestration).Name, input, waitTimeout); + } + + public Task StartTestOrchestrationAsync(TestOrchestrationData input) + { + return client.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), input); + } + + public Task GetOrchestrationState(OrchestrationInstance instance) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + try + { + return client.GetOrchestrationStateAsync(instance); + } + catch(Exception ex) + { + ServiceEventSource.Current.ServiceRequestFailed($"{nameof(GetOrchestrationState)}", ex.ToString()); + throw; + } + } + + public async Task WaitForOrchestration(OrchestrationInstance instance, TimeSpan waitTimeout) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + try + { + return await client.WaitForOrchestrationAsync(instance, waitTimeout); + } + catch(Exception ex) + { + ServiceEventSource.Current.ServiceRequestFailed($"{nameof(WaitForOrchestration)}", ex.ToString()); + throw; + } + } + + public Task PurgeOrchestrationHistoryEventsAsync() + { + return this.client.PurgeOrchestrationInstanceHistoryAsync(DateTime.UtcNow, OrchestrationStateTimeRangeFilterType.OrchestrationCompletedTimeFilter); + } + + async Task ShutdownAsync() + { + try + { + if (this.worker != null) + { + ServiceEventSource.Current.ServiceMessage(this, "Stopping Taskhub Worker"); + await this.worker.StopAsync(isForced: true); + this.worker.Dispose(); + this.fabricProvider.Dispose(); + this.fabricProvider = null; + this.worker = null; + ServiceEventSource.Current.ServiceMessage(this, "Stopped Taskhub Worker"); + } + } + catch (Exception e) + { + ServiceEventSource.Current.ServiceRequestFailed("Exception when Stopping Worker On Primary Stop", e.ToString()); + throw; + } + } + + Type GetOrchestrationType(string typeName) + { + if (KnownOrchestrationInstances.ContainsKey(typeName)) + { + return KnownOrchestrationInstances[typeName].GetType(); + } + + if (!KnownOrchestrationTypeNames.ContainsKey(typeName)) + { + throw new Exception($"Unknown Orchestration Type Name : {typeName}"); + } + + return KnownOrchestrationTypeNames[typeName]; + } + + public Task StartTestOrchestrationWithInstanceIdAsync(string instanceId, TestOrchestrationData input) + { + return client.CreateOrchestrationInstanceAsync(typeof(TestOrchestration), instanceId, input); + } + + public async Task GetOrchestrationStateWithInstanceId(string instanceId) + { + var allStates = await this.client.GetOrchestrationStateAsync(instanceId, allExecutions: false); + return allStates.FirstOrDefault(); + } + + public Task> GetRunningOrchestrations() + { + return this.fabricProvider.FabricProviderClient.GetRunningOrchestrations(); + } + + public Task GetOrchestrationRuntimeState(string instanceId) + { + return this.fabricProvider.FabricProviderClient.GetOrchestrationRuntimeState(instanceId); + } + + public Task TerminateOrchestration(string instanceId, string reason) + { + var instance = new OrchestrationInstance() { InstanceId = instanceId}; + return client.TerminateInstanceAsync(instance, reason); + } + + static Dictionary KnownOrchestrationInstances = new Dictionary + { + { typeof(OrchestrationRunningIntoRetry).Name, new OrchestrationRunningIntoRetry() }, + }; + + static Dictionary KnownOrchestrationTypeNames = new Dictionary + { + { typeof(SimpleOrchestrationWithTasks).Name, typeof(SimpleOrchestrationWithTasks) }, + { typeof(SimpleOrchestrationWithTimer).Name, typeof(SimpleOrchestrationWithTimer) }, + { typeof(GenerationBasicOrchestration).Name, typeof(GenerationBasicOrchestration) }, + { typeof(SimpleOrchestrationWithSubOrchestration).Name, typeof(SimpleOrchestrationWithSubOrchestration) }, + { typeof(DriverOrchestration).Name, typeof(DriverOrchestration) }, + { typeof(TestOrchestration).Name, typeof(TestOrchestration) }, + { typeof(ExecutionCountingOrchestration).Name, typeof(ExecutionCountingOrchestration) }, + }; + + static Type[] KnownActivities = + { + typeof(GetUserTask), + typeof(GreetUserTask), + typeof(GenerationBasicTask), + typeof(RandomTimeWaitingTask), + typeof(ExceptionThrowingTask), + typeof(ExecutionCountingActivity), + }; + } +} diff --git a/Test/TestFabricApplication/TestStatefulService/TestStatefulService.csproj b/Test/TestFabricApplication/TestStatefulService/TestStatefulService.csproj new file mode 100644 index 000000000..5ecfdb87f --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/TestStatefulService.csproj @@ -0,0 +1,169 @@ + + + + + Debug + x64 + {95D6C9C7-F7A0-4125-840D-1854BEC24978} + Exe + Properties + TestStatefulService + TestStatefulService + v4.5.1 + 512 + true + True + + + true + full + false + bin\x64\Debug\ + DEBUG;TRACE + prompt + x64 + MinimumRecommendedRules.ruleset + + + pdbonly + true + bin\x64\Release\ + TRACE + prompt + x64 + MinimumRecommendedRules.ruleset + + + $(AdditionalFileItemNames);None + + + + False + ..\..\..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + True + + + ..\..\..\packages\Microsoft.Owin.Host.HttpListener.2.0.2\lib\net45\Microsoft.Owin.Host.HttpListener.dll + True + + + False + ..\..\..\packages\Microsoft.Owin.Hosting.3.0.1\lib\net45\Microsoft.Owin.Hosting.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.Data.2.5.216\lib\net45\Microsoft.ServiceFabric.Data.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.Data.2.5.216\lib\net45\Microsoft.ServiceFabric.Data.Interfaces.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.FabricTransport.Internal.2.5.216\lib\net45\Microsoft.ServiceFabric.FabricTransport.dll + + + ..\..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\Microsoft.ServiceFabric.Internal.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\Microsoft.ServiceFabric.Internal.Strings.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.Services.2.5.216\lib\net45\Microsoft.ServiceFabric.Services.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.Services.Remoting.2.5.216\lib\net45\Microsoft.ServiceFabric.Services.Remoting.dll + + + False + ..\..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\..\packages\Owin.1.0\lib\net40\Owin.dll + True + + + ..\..\..\packages\Microsoft.ServiceFabric.Services.2.5.216\lib\net45\ServiceFabricServiceModel.dll + True + + + + + ..\..\..\packages\Microsoft.ServiceFabric.5.5.216\lib\net45\System.Fabric.dll + True + + + + False + ..\..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll + True + + + + + False + ..\..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll + True + + + ..\..\..\packages\Microsoft.AspNet.WebApi.Owin.5.2.3\lib\net45\System.Web.Http.Owin.dll + True + + + + + + + + + + + + + + + + + + + + + + Designer + + + + Designer + + + + + {6B386E8F-20EB-43DE-BBD7-6408A232B4BA} + DurableTask.Core + + + {5c617b24-4902-4992-8f71-ab14f00b74a8} + DurableTask.ServiceFabric + + + {2bc2a5ad-4200-41b9-a034-332bb1d7187b} + DurableTask.Test.Orchestrations + + + {ea90334d-3194-4266-81eb-eeca9c760315} + TestApplication.Common + + + + + \ No newline at end of file diff --git a/Test/TestFabricApplication/TestStatefulService/packages.config b/Test/TestFabricApplication/TestStatefulService/packages.config new file mode 100644 index 000000000..7f7abeead --- /dev/null +++ b/Test/TestFabricApplication/TestStatefulService/packages.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/DurableTask.Samples/App.config b/samples/DurableTask.Samples/App.config index b3176b623..56c1d8eab 100644 --- a/samples/DurableTask.Samples/App.config +++ b/samples/DurableTask.Samples/App.config @@ -39,7 +39,7 @@ - + diff --git a/src/DurableTask.Core/WorkItemDispatcher.cs b/src/DurableTask.Core/WorkItemDispatcher.cs index 39dbb38c7..02c64a5d6 100644 --- a/src/DurableTask.Core/WorkItemDispatcher.cs +++ b/src/DurableTask.Core/WorkItemDispatcher.cs @@ -266,7 +266,7 @@ async Task DispatchAsync(string dispatcherId) { Interlocked.Increment(ref concurrentWorkItemCount); // We just want this to Run we intentionally don't wait - #pragma warning disable 4014 + #pragma warning disable 4014 Task.Run(() => ProcessWorkItemAsync(context, workItem)); #pragma warning restore 4014 diff --git a/src/DurableTask.ServiceBus/app.config b/src/DurableTask.ServiceBus/app.config index 381364de9..64dfb28d2 100644 --- a/src/DurableTask.ServiceBus/app.config +++ b/src/DurableTask.ServiceBus/app.config @@ -8,7 +8,7 @@ - + diff --git a/src/DurableTask.ServiceFabric/ActivityProvider.cs b/src/DurableTask.ServiceFabric/ActivityProvider.cs new file mode 100644 index 000000000..8309a2211 --- /dev/null +++ b/src/DurableTask.ServiceFabric/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.ServiceFabric +{ + using System; + using System.Collections.Concurrent; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.ServiceFabric.Data; + + class ActivityProvider : MessageProviderBase where TKey : IComparable, IEquatable + { + readonly ConcurrentQueue inMemoryQueue = new ConcurrentQueue(); + + public ActivityProvider(IReliableStateManager stateManager, string storeName, CancellationToken token) : base(stateManager, storeName, token) + { + } + + protected override void AddItemInMemory(TKey key, TValue value) + { + this.inMemoryQueue.Enqueue(key); + } + + public async Task> ReceiveAsync(TimeSpan receiveTimeout) + { + if (!IsStopped()) + { + TKey key; + bool newItemsBeforeTimeout = true; + while (newItemsBeforeTimeout) + { + if (this.inMemoryQueue.TryDequeue(out key)) + { + try + { + return await GetValueAsync(key); + } + catch (Exception) + { + this.inMemoryQueue.Enqueue(key); + throw; + } + } + + newItemsBeforeTimeout = await WaitForItemsAsync(receiveTimeout); + } + } + return null; + } + + public void Abandon(TKey key) + { + this.inMemoryQueue.Enqueue(key); + SetWaiterForNewItems(); + } + } +} diff --git a/src/DurableTask.ServiceFabric/AsyncManualResetEvent.cs b/src/DurableTask.ServiceFabric/AsyncManualResetEvent.cs new file mode 100644 index 000000000..36cdf3844 --- /dev/null +++ b/src/DurableTask.ServiceFabric/AsyncManualResetEvent.cs @@ -0,0 +1,59 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + sealed class AsyncManualResetEvent + { + TaskCompletionSource tcs; + + public AsyncManualResetEvent() + { + this.tcs = 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.tcs.Task); + return delayTask != resultTask; + } + + public void Set() + { + this.tcs.TrySetResult(true); + } + + public void Reset() + { + while (true) + { + var thisTcs = this.tcs; + + if (!thisTcs.Task.IsCompleted || Interlocked.CompareExchange(ref this.tcs, new TaskCompletionSource(), thisTcs) == thisTcs) + { + return; + } + } + } + } +} diff --git a/src/DurableTask.ServiceFabric/CompletedTask.cs b/src/DurableTask.ServiceFabric/CompletedTask.cs new file mode 100644 index 000000000..1bb88ad75 --- /dev/null +++ b/src/DurableTask.ServiceFabric/CompletedTask.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.ServiceFabric +{ + using System.Threading.Tasks; + + static class CompletedTask + { + internal readonly static Task Default = CompletedTask.Default; + } + + static class CompletedTask + { + static CompletedTask() + { + var tcs = new TaskCompletionSource(); + tcs.TrySetResult(default(TResult)); + Default = tcs.Task; + } + + internal readonly static Task Default; + } +} diff --git a/src/DurableTask.ServiceFabric/Constants.cs b/src/DurableTask.ServiceFabric/Constants.cs new file mode 100644 index 000000000..f46fccde0 --- /dev/null +++ b/src/DurableTask.ServiceFabric/Constants.cs @@ -0,0 +1,26 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + 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"; + } +} diff --git a/src/DurableTask.ServiceFabric/CountBasedFixedDelayRetryPolicy.cs b/src/DurableTask.ServiceFabric/CountBasedFixedDelayRetryPolicy.cs new file mode 100644 index 000000000..5d651ad27 --- /dev/null +++ b/src/DurableTask.ServiceFabric/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.ServiceFabric +{ + 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.ServiceFabric/DebugSerializationUtil.cs b/src/DurableTask.ServiceFabric/DebugSerializationUtil.cs new file mode 100644 index 000000000..0199d59f1 --- /dev/null +++ b/src/DurableTask.ServiceFabric/DebugSerializationUtil.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.ServiceFabric +{ + using System.IO; + using System.Runtime.Serialization; + +#if DEBUG + class DebugSerializationUtil + { + public static long GetDataContractSerializationSize(T testObject) + { + using (var stream = new MemoryStream()) + { + var serializer = new DataContractSerializer(typeof(T)); + serializer.WriteObject(stream, testObject); + return stream.Length; + } + } + } +#endif +} diff --git a/src/DurableTask.ServiceFabric/DurableTask.ServiceFabric.csproj b/src/DurableTask.ServiceFabric/DurableTask.ServiceFabric.csproj new file mode 100644 index 000000000..fb68534ae --- /dev/null +++ b/src/DurableTask.ServiceFabric/DurableTask.ServiceFabric.csproj @@ -0,0 +1,38 @@ + + + + + net451 + true + Microsoft.Azure.DurableTask.ServiceFabric + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/DurableTask.ServiceFabric/DurableTask.ServiceFabric.nuspec b/src/DurableTask.ServiceFabric/DurableTask.ServiceFabric.nuspec new file mode 100644 index 000000000..f9bfd1c00 --- /dev/null +++ b/src/DurableTask.ServiceFabric/DurableTask.ServiceFabric.nuspec @@ -0,0 +1,33 @@ + + + + DurableTask.ServiceFabric + 0.0.1-beta1 + DurableTask.ServiceFabric + pradeek + pradeek + false + Service fabric based storage provider for Durable Task Framework. + Service fabric based storage provider for Durable Task Framework. + Copyright © 2016 + ServiceFabric Azure Task Durable Orchestration Workflow Activity Reliable + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/DurableTask.ServiceFabric/ExceptionUtilities.cs b/src/DurableTask.ServiceFabric/ExceptionUtilities.cs new file mode 100644 index 000000000..4d152e268 --- /dev/null +++ b/src/DurableTask.ServiceFabric/ExceptionUtilities.cs @@ -0,0 +1,38 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + using System; + using System.Fabric; + + 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) + { + ProviderEventSource.Log.RetryableFabricException(uniqueIdentifier, attemptNumber, e.ToString()); + } + else + { + ProviderEventSource.Log.ExceptionInReliableCollectionOperations(uniqueIdentifier, e.ToString()); + } + } + } +} diff --git a/src/DurableTask.ServiceFabric/FabricOrchestrationInstanceStore.cs b/src/DurableTask.ServiceFabric/FabricOrchestrationInstanceStore.cs new file mode 100644 index 000000000..54e46e853 --- /dev/null +++ b/src/DurableTask.ServiceFabric/FabricOrchestrationInstanceStore.cs @@ -0,0 +1,365 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + 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 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(StringComparer.Ordinal); + + IReliableDictionary instanceStore; + IReliableDictionary executionIdStore; + + public FabricOrchestrationInstanceStore(IReliableStateManager stateManager, CancellationToken token) + { + this.stateManager = stateManager; + this.cancellationToken = token; + } + + public Task InitializeStoreAsync(bool recreate) + { + if (recreate) + { + return DeleteStoreAsync(); + } + + return CompletedTask.Default; + } + + public async Task StartAsync() + { + await EnsureStoreInitialized(); + var nowait = CleanupOldDictionaries(); + } + + public async Task DeleteStoreAsync() + { + await this.stateManager.RemoveAsync(Constants.InstanceStoreDictionaryName); + await this.stateManager.RemoveAsync(Constants.ExecutionStoreDictionaryName); + } + + public async Task WriteEntitesAsync(ITransaction transaction, IEnumerable entities) + { + await EnsureStoreInitialized(); + 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()) + { + await this.executionIdStore.AddOrUpdateAsync(transaction, instance.InstanceId, instance.ExecutionId, (k, old) => instance.ExecutionId); + 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 EnsureStoreInitialized(); + + string latestExecutionId = await GetLatestExecutionId(instanceId); + + if (latestExecutionId != null) + { + var state = await GetOrchestrationStateAsync(instanceId, latestExecutionId); + if (state != null) + { + return new List() { state }; + } + } + + return ImmutableList.Empty; + } + + public async Task GetLatestExecutionId(string instanceId) + { + await this.EnsureStoreInitialized(); + + string latestExecutionId = null; + await RetryHelper.ExecuteWithRetryOnTransient(async () => + { + using (var tx = this.stateManager.CreateTransaction()) + { + var executionIdValue = await this.executionIdStore.TryGetValueAsync(tx, instanceId); + if (executionIdValue.HasValue) + { + latestExecutionId = executionIdValue.Value; + } + } + }, uniqueActionIdentifier: $"Orchestration Instance Id = {instanceId}, Action = {nameof(FabricOrchestrationInstanceStore)}.{nameof(GetLatestExecutionId)}"); + + return latestExecutionId; + } + + public async Task GetOrchestrationStateAsync(string instanceId, string executionId) + { + await EnsureStoreInitialized(); + 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 implemenation 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 Task PurgeOrchestrationHistoryEventsAsync(DateTime threshholdHourlyDateTimeUtc) + { + return this.stateManager.RemoveAsync(GetDictionaryKeyFromTimeFormat(threshholdHourlyDateTimeUtc)); + } + + 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 CleanupDayOldDictionaries() + { + 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(CleanupDayOldDictionaries)}", token: this.cancellationToken); + } + + Task CleanupOldDictionaries() + { + 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; + if (DateTime.TryParseExact(storeName.Substring(InstanceStoreCollectionNamePrefix.Length), TimeFormatString, CultureInfo.InvariantCulture, DateTimeStyles.None, out storeTime) + && (currentTime - storeTime > ttl)) + { + toDelete.Add(storeName); + continue; + } + } + + toKeep.Add(storeName); + } + }); + ProviderEventSource.Log.LogTimeTaken($"Enumerating all reliable states (count: {toDelete.Count + toKeep.Count})", enumerationTime.TotalMilliseconds); + + ProviderEventSource.Log.ReliableStateManagement($"Deleting {toDelete.Count} stores", String.Join(",", toDelete)); + ProviderEventSource.Log.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); + }); + ProviderEventSource.Log.LogTimeTaken($"Deleting reliable state {storeName}", deleteTime.TotalMilliseconds); + } + }, initialDelay: TimeSpan.FromMinutes(5), delayOnSuccess: TimeSpan.FromHours(1), delayOnException: TimeSpan.FromMinutes(10), actionName: $"{nameof(CleanupOldDictionaries)}", token: this.cancellationToken); + } + + async Task EnsureStoreInitialized() + { + 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(string instanceId, TimeSpan timeout) + { + var executionId = await this.GetLatestExecutionId(instanceId); + if (executionId == null) + { + return null; + } + + var currentState = await this.GetOrchestrationStateAsync(instanceId, 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(instanceId, new AsyncManualResetEvent()); + bool completed = await waiter.WaitAsync(timeout, this.cancellationToken); + + if (!completed) + { + this.orchestrationWaiters.TryRemove(instanceId, out waiter); + } + + var latestStates = await this.GetOrchestrationStateAsync(instanceId, allInstances: false); + currentState = latestStates.FirstOrDefault(); + if (currentState?.State != null && currentState.State.OrchestrationStatus.IsTerminalState()) + { + return currentState; + } + + return null; + } + + public void OnOrchestrationCompleted(OrchestrationInstance instance) + { + AsyncManualResetEvent resetEvent; + if (this.orchestrationWaiters.TryRemove(instance.InstanceId, out resetEvent)) + { + resetEvent.Set(); + } + } + } +} diff --git a/src/DurableTask.ServiceFabric/FabricOrchestrationProvider.cs b/src/DurableTask.ServiceFabric/FabricOrchestrationProvider.cs new file mode 100644 index 000000000..755c3c630 --- /dev/null +++ b/src/DurableTask.ServiceFabric/FabricOrchestrationProvider.cs @@ -0,0 +1,101 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + using System; + using System.Threading; + using DurableTask.Core; + 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 sessionsProvider = new SessionsProvider(stateManager, cancellationTokenSource.Token); + var instanceStore = new FabricOrchestrationInstanceStore(stateManager, cancellationTokenSource.Token); + this.orchestrationService = new FabricOrchestrationService(stateManager, sessionsProvider, instanceStore, settings, cancellationTokenSource); + this.orchestrationClient = new FabricOrchestrationServiceClient(stateManager, sessionsProvider, instanceStore); + this.fabricProviderClient = new FabricProviderClient(stateManager, sessionsProvider); + } + + /// + /// 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; + } + } + + /// + /// An instance of . + /// + public IFabricProviderClient FabricProviderClient + { + get + { + EnsureValidInstance(); + return this.fabricProviderClient; + } + } + + /// + /// 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.ServiceFabric/FabricOrchestrationProviderFactory.cs b/src/DurableTask.ServiceFabric/FabricOrchestrationProviderFactory.cs new file mode 100644 index 000000000..e2f5c4872 --- /dev/null +++ b/src/DurableTask.ServiceFabric/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.ServiceFabric +{ + 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.ServiceFabric/FabricOrchestrationProviderSettings.cs b/src/DurableTask.ServiceFabric/FabricOrchestrationProviderSettings.cs new file mode 100644 index 000000000..c2b87677d --- /dev/null +++ b/src/DurableTask.ServiceFabric/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.ServiceFabric +{ + 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.ServiceFabric/FabricOrchestrationService.cs b/src/DurableTask.ServiceFabric/FabricOrchestrationService.cs new file mode 100644 index 000000000..41e01e315 --- /dev/null +++ b/src/DurableTask.ServiceFabric/FabricOrchestrationService.cs @@ -0,0 +1,531 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + 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 Microsoft.ServiceFabric.Data; + + class FabricOrchestrationService : IOrchestrationService + { + readonly IReliableStateManager stateManager; + readonly IFabricOrchestrationServiceInstanceStore instanceStore; + readonly SessionsProvider orchestrationProvider; + readonly ActivityProvider activitiesProvider; + readonly ScheduledMessageProvider scheduledMessagesProvider; + readonly FabricOrchestrationProviderSettings settings; + readonly CancellationTokenSource cancellationTokenSource; + + ConcurrentDictionary sessionInfos = new ConcurrentDictionary(); + + public FabricOrchestrationService(IReliableStateManager stateManager, + SessionsProvider 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 CompletedTask.Default; + } + + 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 CompletedTask.Default; + } + + 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) + { + ProviderEventSource.Log.UnexpectedCodeCondition($"Orchestration with no execution started event found: {currentSession.SessionId}"); + return null; + } + + bool isComplete = this.IsOrchestrationComplete(currentRuntimeState.OrchestrationStatus); + 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)) + { + ProviderEventSource.Log.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); + + bool isComplete = this.IsOrchestrationComplete(workItem.OrchestrationRuntimeState.OrchestrationStatus); + + 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().ToString(), 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(); + } + } + + if (continuedAsNewMessage != null) + { + await this.orchestrationProvider.AppendMessageAsync(txn, new TaskMessageItem(continuedAsNewMessage)); + sessionsToEnqueue = new List() { continuedAsNewMessage.OrchestrationInstance }; + } + + 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'. + var newInstance = continuedAsNewMessage != null ? continuedAsNewMessage.OrchestrationInstance : sessionInfo.Instance; + await this.orchestrationProvider.UpdateSessionState(txn, newInstance, 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.WriteEntitesAsync(txn, new InstanceEntityBase[] + { + new OrchestrationStateInstanceEntity() + { + State = orchestrationState + } + }); + } + await txn.CommitAsync(); + } + } + catch (FabricReplicationOperationTooLargeException ex) + { + ProviderEventSource.Log.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.WriteEntitesAsync(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); + + ProviderEventSource.Log.OrchestrationFinished(workItem.InstanceId, + workItem.OrchestrationRuntimeState.OrchestrationStatus.ToString(), + (workItem.OrchestrationRuntimeState.CompletedTime - workItem.OrchestrationRuntimeState.CreatedTime).TotalSeconds, + workItem.OrchestrationRuntimeState.Output, + workItem.OrchestrationRuntimeState.OrchestrationInstance.ExecutionId); + } + + public Task AbandonTaskOrchestrationWorkItemAsync(TaskOrchestrationWorkItem workItem) + { + SessionInformation sessionInfo = TryRemoveSessionInfo(workItem.InstanceId); + if (sessionInfo == null) + { + ProviderEventSource.Log.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 CompletedTask.Default; + } + + public Task ReleaseTaskOrchestrationWorkItemAsync(TaskOrchestrationWorkItem workItem) + { + bool isComplete = this.IsOrchestrationComplete(workItem.OrchestrationRuntimeState.OrchestrationStatus); + + SessionInformation sessionInfo = TryRemoveSessionInfo(workItem.InstanceId); + if (sessionInfo != null) + { + this.orchestrationProvider.TryUnlockSession(sessionInfo.Instance, isComplete: isComplete); + } + + return CompletedTask.Default; + } + + public int TaskActivityDispatcherCount => this.settings.TaskActivityDispatcherSettings.DispatcherCount; + public int MaxConcurrentTaskActivityWorkItems => this.settings.TaskActivityDispatcherSettings.MaxConcurrentActivities; + + // 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) + { + ProviderEventSource.Log.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 CompletedTask.Default; + } + + 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; + } + + bool IsOrchestrationComplete(OrchestrationStatus status) + { + return !(status.IsRunningOrPending() || status == OrchestrationStatus.ContinuedAsNew); + } + + SessionInformation GetSessionInfo(string sessionId) + { + ProviderEventSource.Log.TraceMessage(sessionId, $"{nameof(GetSessionInfo)} - Getting session info"); + SessionInformation sessionInfo; + if (!this.sessionInfos.TryGetValue(sessionId, out sessionInfo)) + { + var message = $"{nameof(GetSessionInfo)}. Trying to get a session that's not in locked sessions {sessionId}"; + ProviderEventSource.Log.UnexpectedCodeCondition(message); + throw new Exception(message); + } + + return sessionInfo; + } + + SessionInformation TryRemoveSessionInfo(string sessionId) + { + SessionInformation sessionInfo; + var removed = this.sessionInfos.TryRemove(sessionId, out sessionInfo); + ProviderEventSource.Log.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.ServiceFabric/FabricOrchestrationServiceClient.cs b/src/DurableTask.ServiceFabric/FabricOrchestrationServiceClient.cs new file mode 100644 index 000000000..4127cd75c --- /dev/null +++ b/src/DurableTask.ServiceFabric/FabricOrchestrationServiceClient.cs @@ -0,0 +1,202 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using DurableTask.Core; + using DurableTask.Core.History; + using DurableTask.Core.Tracking; + using Microsoft.ServiceFabric.Data; + + class FabricOrchestrationServiceClient : IOrchestrationServiceClient + { + readonly IReliableStateManager stateManager; + readonly IFabricOrchestrationServiceInstanceStore instanceStore; + readonly SessionsProvider orchestrationProvider; + + public FabricOrchestrationServiceClient(IReliableStateManager stateManager, SessionsProvider 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) + { + 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) + { + ProviderEventSource.Log.OrchestrationCreated(instance.InstanceId, instance.ExecutionId); + this.orchestrationProvider.TryEnqueueSession(creationMessage.OrchestrationInstance); + } + else + { + throw new InvalidOperationException($"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"); + } + + return CreateTaskOrchestrationAsync(creationMessage); + } + + public Task SendTaskOrchestrationMessageAsync(TaskMessage message) + { + return this.orchestrationProvider.AppendMessageAsync(new TaskMessageItem(message)); + } + + public async Task SendTaskOrchestrationMessageBatchAsync(params TaskMessage[] messages) + { + foreach(var message in messages) + { + await this.SendTaskOrchestrationMessageAsync(message); + } + } + + public async Task ForceTerminateTaskOrchestrationAsync(string instanceId, string reason) + { + var latestExecutionId = await this.instanceStore.GetLatestExecutionId(instanceId); + + 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) + { + 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) + { + var stateInstance = await this.instanceStore.GetOrchestrationStateAsync(instanceId, executionId); + return stateInstance?.State; + } + + public Task GetOrchestrationHistoryAsync(string instanceId, string executionId) + { + throw new NotImplementedException(); + } + + 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); + } + + /// + /// Wait till the specified time span for the latest orchestration execution to complete. + /// The service bus client implementation of this API ignores the input executionId parameter and + /// only returns if the latest execution of a given orchestration instance is completed before + /// the specified time out. Perhaps there is a good reason - for an orchestration which uses ContinueAsNew, + /// the orchestration is not really complete though one execution may have been completed (typically with + /// ContinueAsNew status), the user of this API would most likely want to return from this + /// method only if the latest execution of such an orchestration is completed instead of a given + /// iteration. Hence, we match the service fabric provider functionality to be the same. + /// + /// The instanceId of an orchestation to wait to complete. + /// Ignored and instead the latest execution of Orchestration is considered. + /// Timespan of wait. If the orchestration completes before the specified + /// time span, the API immediately returns with the state of Orchestration. Otherwise, the API + /// waits till the time span and returns the state of Orchestration at that time. + /// Cancellation token for preempting the waiting before the timeout. + /// The state of orchestration with the given instanceId if it completes before the timespace. Otherwise null. + public async Task WaitForOrchestrationAsync(string instanceId, string executionId, TimeSpan timeout, CancellationToken cancellationToken) + { + var state = await this.instanceStore.WaitForOrchestrationAsync(instanceId, 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.WriteEntitesAsync(tx, new InstanceEntityBase[] + { + new OrchestrationStateInstanceEntity() + { + State = initialState + } + }); + } + } +} diff --git a/src/DurableTask.ServiceFabric/FabricProviderClient.cs b/src/DurableTask.ServiceFabric/FabricProviderClient.cs new file mode 100644 index 000000000..f3f2e7cab --- /dev/null +++ b/src/DurableTask.ServiceFabric/FabricProviderClient.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.ServiceFabric +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using DurableTask.Core; + using DurableTask.Core.Serializing; + using Microsoft.ServiceFabric.Data; + using Newtonsoft.Json; + + internal class FabricProviderClient : IFabricProviderClient + { + readonly IReliableStateManager stateManager; + readonly SessionsProvider orchestrationProvider; + readonly JsonDataConverter FormattingConverter = new JsonDataConverter(new JsonSerializerSettings() { Formatting = Formatting.Indented }); + + public FabricProviderClient(IReliableStateManager stateManager, SessionsProvider orchestrationProvider) + { + this.stateManager = stateManager ?? throw new ArgumentNullException(nameof(stateManager)); + this.orchestrationProvider = orchestrationProvider ?? throw new ArgumentNullException(nameof(orchestrationProvider)); + } + + public async Task> GetRunningOrchestrations() + { + var sessions = await this.orchestrationProvider.GetSessions(); + return sessions.Select(s => s.SessionId); + } + + public async Task GetOrchestrationRuntimeState(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.ServiceFabric/IFabricOrchestrationServiceInstanceStore.cs b/src/DurableTask.ServiceFabric/IFabricOrchestrationServiceInstanceStore.cs new file mode 100644 index 000000000..6559fec41 --- /dev/null +++ b/src/DurableTask.ServiceFabric/IFabricOrchestrationServiceInstanceStore.cs @@ -0,0 +1,87 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using DurableTask.Core; + using DurableTask.Core.Tracking; + using Microsoft.ServiceFabric.Data; + + // Todo: Figure out a way to not include internal XML documentation in the generated documentation file + // or change all the below comments to non-xml style. + 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 WriteEntitesAsync(ITransaction transaction, IEnumerable entities); + + /// + /// Gets a list of orchestration states for a given instance + /// + /// The instance id to return state for + /// Flag indiciation 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(string instanceId, TimeSpan timeout); + + Task GetLatestExecutionId(string instanceId); + } +} diff --git a/src/DurableTask.ServiceFabric/IFabricProviderClient.cs b/src/DurableTask.ServiceFabric/IFabricProviderClient.cs new file mode 100644 index 000000000..b19b389de --- /dev/null +++ b/src/DurableTask.ServiceFabric/IFabricProviderClient.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.ServiceFabric +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using DurableTask.Core; + + /// + /// Provides additional useful functionality which is not available through . + /// + public interface IFabricProviderClient + { + /// + /// Gets all the orchestration instances which are currently running or pending. + /// + /// + Task> GetRunningOrchestrations(); + + /// + /// 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 GetOrchestrationRuntimeState(string instanceId); + } +} diff --git a/src/DurableTask.ServiceFabric/Message.cs b/src/DurableTask.ServiceFabric/Message.cs new file mode 100644 index 000000000..acf4b375f --- /dev/null +++ b/src/DurableTask.ServiceFabric/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.ServiceFabric +{ + 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.ServiceFabric/MessageProviderBase.cs b/src/DurableTask.ServiceFabric/MessageProviderBase.cs new file mode 100644 index 000000000..3967d8e82 --- /dev/null +++ b/src/DurableTask.ServiceFabric/MessageProviderBase.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.ServiceFabric +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Threading; + using System.Threading.Tasks; + 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 InitializeStore(); + await EnumerateItems(kvp => AddItemInMemory(kvp.Key, kvp.Value)); + var nowait = 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 (var 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) + { + 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 (var item in items) + { + await this.Store.TryAddAsync(tx, item.Key, item.Value); + } + } + + public void SendBatchComplete(IEnumerable> items) + { + foreach (var item in items) + { + 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"; + ProviderEventSource.Log.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 InitializeStore(); + } + + return CompletedTask.Default; + } + + 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 () => + { + long count = 0; + using (var tx = this.StateManager.CreateTransaction()) + { + count = await this.Store.GetCountAsync(tx); + } + + ProviderEventSource.Log.LogStoreCount(this.storeName, count); + }, initialDelay: metricsInterval, delayOnSuccess: metricsInterval, delayOnException: metricsInterval, actionName: $"Log Store Count of {this.storeName}", token: this.CancellationToken); + } + } +} diff --git a/src/DurableTask.ServiceFabric/OrchestrationInstanceComparer.cs b/src/DurableTask.ServiceFabric/OrchestrationInstanceComparer.cs new file mode 100644 index 000000000..e83fe4dc8 --- /dev/null +++ b/src/DurableTask.ServiceFabric/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.ServiceFabric +{ + 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.ServiceFabric/OrchestrationStatusExtensions.cs b/src/DurableTask.ServiceFabric/OrchestrationStatusExtensions.cs new file mode 100644 index 000000000..731557ad8 --- /dev/null +++ b/src/DurableTask.ServiceFabric/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.ServiceFabric +{ + 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.ServiceFabric/PersistentSession.cs b/src/DurableTask.ServiceFabric/PersistentSession.cs new file mode 100644 index 000000000..2acea3db9 --- /dev/null +++ b/src/DurableTask.ServiceFabric/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.ServiceFabric +{ + 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.ServiceFabric/Properties/AssemblyInfo.cs b/src/DurableTask.ServiceFabric/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..15f589a18 --- /dev/null +++ b/src/DurableTask.ServiceFabric/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +#if !SIGN_ASSEMBLY +[assembly: InternalsVisibleTo("DurableTask.ServiceFabric.UnitTests")] +#endif \ No newline at end of file diff --git a/src/DurableTask.ServiceFabric/ProviderEventSource.cs b/src/DurableTask.ServiceFabric/ProviderEventSource.cs new file mode 100644 index 000000000..04447974a --- /dev/null +++ b/src/DurableTask.ServiceFabric/ProviderEventSource.cs @@ -0,0 +1,178 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + using Microsoft.Diagnostics.Tracing; + using System.Threading.Tasks; + using System; + + /// + /// The event source which emits ETW events for service fabric based provider functionality. + /// + [EventSource(Name = "DurableTask-ServiceFabricProvider")] + public sealed class ProviderEventSource : EventSource + { + /// + /// Singleton instance of the event source. + /// + public static readonly ProviderEventSource Log = new ProviderEventSource(); + + static ProviderEventSource() + { + // A workaround for the problem where ETW activities do not get tracked until Tasks infrastructure is initialized. + // This problem will be fixed in .NET Framework 4.6.2. + Task.Run(() => { }); + } + + private ProviderEventSource() : base() + { + } + + private static class Keywords + { + public const EventKeywords Orchestration = (EventKeywords) 0x1L; + public const EventKeywords Activity = (EventKeywords) 0x2L; + public const EventKeywords Common = (EventKeywords)0x4L; + public const EventKeywords Warning = (EventKeywords)0x8L; + public const EventKeywords Error = (EventKeywords)0x10L; + } + + #region Informational 1-500 + [Event(1, Level = EventLevel.Informational, Message = "Orchestration with instanceId : '{0}' and executionId : '{1}' is Created.", Channel = EventChannel.Operational)] + internal void OrchestrationCreated(string instanceId, string executionId) + { + if (IsEnabled(EventLevel.Informational, Keywords.Orchestration)) + { + WriteEvent(1, instanceId, executionId); + } + } + + [Event(2, Level = EventLevel.Informational, Message = "Orchestration with instanceId : '{0}' and executionId : '{4}' Finished with the status {1} and result {3} in {2} seconds.", Channel = EventChannel.Operational)] + internal void OrchestrationFinished(string instanceId, string terminalStatus, double runningTimeInSeconds, string result, string executionId) + { + if (IsEnabled(EventLevel.Informational, Keywords.Orchestration)) + { + WriteEvent(2, instanceId, terminalStatus, runningTimeInSeconds, result, executionId); + } + } + + [Event(4, Level = EventLevel.Informational, Message = "Current number of entries in store {0} : {1}", Channel = EventChannel.Operational)] + internal void LogStoreCount(string storeName, long count) + { + if (IsEnabled(EventLevel.Informational, Keywords.Common)) + { + WriteEvent(4, storeName, count); + } + } + + [Event(8, Level = EventLevel.Informational, Message = "Time taken for {0} : {1} milli seconds.", Channel = EventChannel.Operational)] + internal void LogTimeTaken(string uniqueActionIdentifier, double elapsedMilliseconds) + { + if (IsEnabled(EventLevel.Informational, Keywords.Common)) + { + WriteEvent(8, uniqueActionIdentifier, elapsedMilliseconds); + } + } + + [Event(9, Level = EventLevel.Informational, Message = "{0} : {1}", Channel = EventChannel.Operational)] + internal void ReliableStateManagement(string operationIdentifier, string operationData) + { + if (IsEnabled(EventLevel.Informational, Keywords.Common)) + { + WriteEvent(9, operationIdentifier, operationData); + } + } + #endregion + + #region Warnings 1001-1500 + [Event(1001, Level = EventLevel.Warning, Message = "Exception in the background job {0} : {1}", Channel = EventChannel.Operational)] + internal void ExceptionWhileRunningBackgroundJob(string operationIdentifier, string exception) + { + if (IsEnabled(EventLevel.Warning, Keywords.Common | Keywords.Warning)) + { + WriteEvent(1001, operationIdentifier, exception); + } + } + + [Event(1002, Level = EventLevel.Warning, Message = "Hint : {0}, AttemptNumber : {1}, Exception: {2}", Channel = EventChannel.Operational)] + internal void RetryableFabricException(string uniqueIdentifier, int attemptNumber, string exception) + { + if (IsEnabled(EventLevel.Warning, Keywords.Common | Keywords.Warning)) + { + WriteEvent(1002, uniqueIdentifier, attemptNumber, exception); + } + } + #endregion + + #region Errors 1501-2000 + [Event(1501, Level = EventLevel.Error, Message = "We are seeing something that we don't expect to see : {0}", Channel = EventChannel.Operational)] + internal void UnexpectedCodeCondition(string uniqueMessage) + { + if (IsEnabled(EventLevel.Error, Keywords.Common | Keywords.Error)) + { + WriteEvent(1501, uniqueMessage); + } + +#if DEBUG + // This is so that tests fail when this event happens. + throw new Exception(uniqueMessage); +#endif + } + + [Event(1502, Level = EventLevel.Error, Message = "Hint : {0}, Exception: {1}", Channel = EventChannel.Operational)] + internal void ExceptionInReliableCollectionOperations(string uniqueIdentifier, string exception) + { + if (IsEnabled(EventLevel.Error, Keywords.Common | Keywords.Error)) + { + WriteEvent(1502, uniqueIdentifier, exception); + } + } + #endregion + + #region Verbose Events 501-1000 + [Event(501, Level = EventLevel.Verbose, Message = "Trace Message for Session {0} : {1}", Channel = EventChannel.Debug)] + internal void TraceMessage(string instanceId, string message) + { +#if DEBUG + if (IsEnabled(EventLevel.Verbose, Keywords.Common)) + { + WriteEvent(501, instanceId, message); + } +#endif + } + + [Event(502, Level = EventLevel.Verbose, Message = "Time taken for {0} : {1} milli seconds.", Channel = EventChannel.Analytic)] + internal void LogMeasurement(string uniqueActionIdentifier, long elapsedMilliseconds) + { +#if DEBUG + if (IsEnabled(EventLevel.Verbose, Keywords.Common)) + { + WriteEvent(502, uniqueActionIdentifier, elapsedMilliseconds); + } +#endif + } + + [Event(503, Level = EventLevel.Verbose, Message = "Size of {0} : {1} bytes.", Channel = EventChannel.Analytic)] + internal void LogSizeMeasure(string uniqueObjectIdentifier, long sizeInBytes) + { +#if DEBUG + if (IsEnabled(EventLevel.Verbose, Keywords.Common)) + { + WriteEvent(503, uniqueObjectIdentifier, sizeInBytes); + } +#endif + } + #endregion + } +} diff --git a/src/DurableTask.ServiceFabric/README.md b/src/DurableTask.ServiceFabric/README.md new file mode 100644 index 000000000..1400e8d70 --- /dev/null +++ b/src/DurableTask.ServiceFabric/README.md @@ -0,0 +1,44 @@ +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) \ No newline at end of file diff --git a/src/DurableTask.ServiceFabric/RetryHelper.cs b/src/DurableTask.ServiceFabric/RetryHelper.cs new file mode 100644 index 000000000..21e4ffaf7 --- /dev/null +++ b/src/DurableTask.ServiceFabric/RetryHelper.cs @@ -0,0 +1,110 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + using System; + using System.Diagnostics; + using System.Threading.Tasks; + + static class RetryHelper + { + public static Task ExecuteWithRetryOnTransient(Func action, string uniqueActionIdentifier) + { + return ExecuteWithRetryOnTransient(action, CountBasedFixedDelayRetryPolicy.GetNewDefaultPolicy(), uniqueActionIdentifier); + } + + static async Task ExecuteWithRetryOnTransient(Func action, RetryPolicy retryPolicy, string uniqueActionIdentifier) + { + Exception lastException = null; + + int attemptNumber = 0; + while (retryPolicy.ShouldExecute()) + { + attemptNumber++; + Stopwatch timer = Stopwatch.StartNew(); + try + { + await action(); + timer.Stop(); + ProviderEventSource.Log.LogMeasurement($"{uniqueActionIdentifier}, Attempt Number : {attemptNumber}, Result : Success", timer.ElapsedMilliseconds); + return; + } + catch (Exception e) + { + timer.Stop(); + lastException = e; + bool shouldRetry = ExceptionUtilities.IsRetryableFabricException(e); + + ExceptionUtilities.LogReliableCollectionException(uniqueActionIdentifier, attemptNumber, e, shouldRetry); + ProviderEventSource.Log.LogMeasurement($"{uniqueActionIdentifier}, Attempt Number : {attemptNumber}, ShouldRetry : {shouldRetry}", timer.ElapsedMilliseconds); + + if (shouldRetry) + { + await Task.Delay(retryPolicy.GetNextDelay()); + } + else + { + throw; + } + } + } + + throw lastException; + } + + public static Task ExecuteWithRetryOnTransient(Func> action, string uniqueActionIdentifier) + { + return ExecuteWithRetryOnTransient(action, CountBasedFixedDelayRetryPolicy.GetNewDefaultPolicy(), uniqueActionIdentifier); + } + + static async Task ExecuteWithRetryOnTransient(Func> action, RetryPolicy retryPolicy, string uniqueActionIdentifier) + { + Exception lastException = null; + + int attemptNumber = 0; + while (retryPolicy.ShouldExecute()) + { + attemptNumber++; + Stopwatch timer = Stopwatch.StartNew(); + try + { + var result = await action(); + timer.Stop(); + ProviderEventSource.Log.LogMeasurement($"{uniqueActionIdentifier}, Attempt Number : {attemptNumber}, Result : Success", timer.ElapsedMilliseconds); + return result; + } + catch (Exception e) + { + timer.Stop(); + lastException = e; + bool shouldRetry = ExceptionUtilities.IsRetryableFabricException(e); + + ExceptionUtilities.LogReliableCollectionException(uniqueActionIdentifier, attemptNumber, e, shouldRetry); + ProviderEventSource.Log.LogMeasurement($"{uniqueActionIdentifier}, Attempt Number : {attemptNumber}, ShouldRetry : {shouldRetry}", timer.ElapsedMilliseconds); + + if (shouldRetry) + { + await Task.Delay(retryPolicy.GetNextDelay()); + } + else + { + throw; + } + } + } + + throw lastException; + } + } +} diff --git a/src/DurableTask.ServiceFabric/RetryPolicy.cs b/src/DurableTask.ServiceFabric/RetryPolicy.cs new file mode 100644 index 000000000..00bc01ee0 --- /dev/null +++ b/src/DurableTask.ServiceFabric/RetryPolicy.cs @@ -0,0 +1,24 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + using System; + + abstract class RetryPolicy + { + public abstract bool ShouldExecute(); + + public abstract TimeSpan GetNextDelay(); + } +} diff --git a/src/DurableTask.ServiceFabric/ScheduledMessageProvider.cs b/src/DurableTask.ServiceFabric/ScheduledMessageProvider.cs new file mode 100644 index 000000000..eadc36ea6 --- /dev/null +++ b/src/DurableTask.ServiceFabric/ScheduledMessageProvider.cs @@ -0,0 +1,169 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + 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 Microsoft.ServiceFabric.Data; + + class ScheduledMessageProvider : MessageProviderBase + { + readonly SessionsProvider sessionsProvider; + readonly object @lock = new object(); + + ImmutableSortedSet> inMemorySet = ImmutableSortedSet>.Empty.WithComparer(TimerFiredEventComparer.Instance); + DateTime nextActivationCheck; + + public ScheduledMessageProvider(IReliableStateManager stateManager, string storeName, SessionsProvider sessionsProvider, CancellationToken token) : base(stateManager, storeName, token) + { + this.sessionsProvider = sessionsProvider; + } + + 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) + { + ProviderEventSource.Log.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) + { + ProviderEventSource.Log.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(string 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.sessionsProvider.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.sessionsProvider.TryEnqueueSession(sessionId); + } + } + } + + this.nextActivationCheck = nextCheck; + await WaitForItemsAsync(this.nextActivationCheck - DateTime.UtcNow); + } + catch (Exception e) + { + ProviderEventSource.Log.ExceptionWhileRunningBackgroundJob($"{nameof(ScheduledMessageProvider)}.{nameof(ProcessScheduledMessages)}", e.ToString()); + await Task.Delay(TimeSpan.FromMilliseconds(100)); + } + } + } + } +} diff --git a/src/DurableTask.ServiceFabric/SessionMessagesProvider.cs b/src/DurableTask.ServiceFabric/SessionMessagesProvider.cs new file mode 100644 index 000000000..df7054366 --- /dev/null +++ b/src/DurableTask.ServiceFabric/SessionMessagesProvider.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.ServiceFabric +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.ServiceFabric.Data; + using Microsoft.ServiceFabric.Data.Collections; + + class SessionMessagesProvider : MessageProviderBase where TKey : IComparable, IEquatable + { + public SessionMessagesProvider(IReliableStateManager stateManager, string storeName, CancellationToken token) + : base(stateManager, storeName, token) + { + } + + protected override void AddItemInMemory(TKey key, TValue 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.ServiceFabric/SessionsProvider.cs b/src/DurableTask.ServiceFabric/SessionsProvider.cs new file mode 100644 index 000000000..8894576b5 --- /dev/null +++ b/src/DurableTask.ServiceFabric/SessionsProvider.cs @@ -0,0 +1,356 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + 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 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 SessionsProvider : MessageProviderBase + { + ConcurrentQueue fetchQueue = new ConcurrentQueue(); + ConcurrentDictionary lockedSessions = new ConcurrentDictionary(); + + ConcurrentDictionary> sessionMessageProviders = new ConcurrentDictionary>(OrchestrationInstanceComparer.Default); + + public SessionsProvider(IReliableStateManager stateManager, CancellationToken token) : base(stateManager, Constants.OrchestrationDictionaryName, token) + { + } + + public async Task AcceptSessionAsync(TimeSpan receiveTimeout) + { + if (!IsStopped()) + { + string returnInstanceId; + bool newItemsBeforeTimeout = true; + while (newItemsBeforeTimeout) + { + if (this.fetchQueue.TryDequeue(out 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)) + { + ProviderEventSource.Log.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"; + ProviderEventSource.Log.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"; + ProviderEventSource.Log.UnexpectedCodeCondition(errorMessage); + throw new Exception(errorMessage); + } + } + }, uniqueActionIdentifier: $"{nameof(SessionsProvider)}.{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(); + ProviderEventSource.Log.TraceMessage(session.SessionId.InstanceId, $"Number of received messages {messages.Count}"); + return messages; + } + + public async Task CompleteMessages(ITransaction transaction, OrchestrationInstance instance, List lockTokens) + { + SessionMessagesProvider sessionMessageProvider; + if (this.sessionMessageProviders.TryGetValue(instance, out sessionMessageProvider)) + { + ProviderEventSource.Log.TraceMessage(instance.InstanceId, $"Number of completed messages {lockTokens.Count}"); + await sessionMessageProvider.CompleteBatchAsync(transaction, lockTokens); + } + else + { + ProviderEventSource.Log.UnexpectedCodeCondition($"{nameof(SessionsProvider)}.{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); +#if DEBUG + ProviderEventSource.Log.LogSizeMeasure($"Value in SessionsProvider for SessionId = {instance.InstanceId}", DebugSerializationUtil.GetDataContractSerializationSize(result)); +#endif + 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(SessionsProvider)}.{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) + { + ProviderEventSource.Log.TraceMessage(instance.InstanceId, $"Session Unlock Begin, Abandon = {abandon}"); + LockState lockState; + if (!this.lockedSessions.TryRemove(instance.InstanceId, out lockState) || lockState == LockState.InFetchQueue) + { + var errorMessage = $"{nameof(SessionsProvider)}.{nameof(TryUnlockSession)} : Trying to unlock the session {instance.InstanceId} which was not locked."; + ProviderEventSource.Log.UnexpectedCodeCondition(errorMessage); + throw new Exception(errorMessage); + } + + if (!isComplete && (abandon || lockState == LockState.NewMessagesWhileLocked)) + { + this.TryEnqueueSession(instance); + } + ProviderEventSource.Log.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(SessionsProvider)}.{nameof(GetSession)}"); + } + + public void TryEnqueueSession(OrchestrationInstance instance) + { + this.TryEnqueueSession(instance.InstanceId); + } + + void TryEnqueueSession(string instanceId) + { + if (this.lockedSessions.TryAdd(instanceId, LockState.InFetchQueue)) + { + ProviderEventSource.Log.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); + + SessionMessagesProvider sessionMessagesProvider; + this.sessionMessageProviders.TryRemove(instance, out sessionMessagesProvider); + + var noWait = RetryHelper.ExecuteWithRetryOnTransient(() => this.StateManager.RemoveAsync(GetSessionMessagesDictionaryName(instance)), + uniqueActionIdentifier: $"Orchestration = '{instance}', Action = 'DropSessionMessagesDictionaryBackgroundTask'"); + } + + async Task> GetOrAddSessionMessagesInstance(OrchestrationInstance instance) + { + var newInstance = new SessionMessagesProvider(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.ServiceFabric/TaskMessageItem.cs b/src/DurableTask.ServiceFabric/TaskMessageItem.cs new file mode 100644 index 000000000..94d19fa15 --- /dev/null +++ b/src/DurableTask.ServiceFabric/TaskMessageItem.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.ServiceFabric +{ + using System; + using System.Runtime.Serialization; + using DurableTask.Core; + + [DataContract] + sealed class TaskMessageItem : IExtensibleDataObject + { + public TaskMessageItem(TaskMessage taskMessage) + { + this.TaskMessage = taskMessage ?? throw new ArgumentNullException(nameof(taskMessage)); + } + + [DataMember] + public TaskMessage TaskMessage { get; private set; } + + public ExtensionDataObject ExtensionData { get; set; } + } +} diff --git a/src/DurableTask.ServiceFabric/TimerFiredEventComparer.cs b/src/DurableTask.ServiceFabric/TimerFiredEventComparer.cs new file mode 100644 index 000000000..0679aa7a9 --- /dev/null +++ b/src/DurableTask.ServiceFabric/TimerFiredEventComparer.cs @@ -0,0 +1,57 @@ +// ---------------------------------------------------------------------------------- +// 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.ServiceFabric +{ + using System; + using System.Collections.Generic; + using DurableTask.Core.History; + + sealed class TimerFiredEventComparer : IComparer> + { + public static readonly TimerFiredEventComparer Instance = new TimerFiredEventComparer(); + + public int Compare(Message first, Message second) + { + if (first == null) + { + throw new ArgumentNullException(nameof(first)); + } + + if (second == null) + { + throw new ArgumentNullException(nameof(second)); + } + + var firstTimer = first.Value.TaskMessage.Event as TimerFiredEvent; + var secondTimer = second.Value.TaskMessage.Event as TimerFiredEvent; + + if (firstTimer == null) + { + throw new ArgumentException(nameof(first)); + } + if (secondTimer == null) + { + throw new ArgumentException(nameof(second)); + } + + int firedAtCompareValue = DateTime.Compare(firstTimer.FireAt, secondTimer.FireAt); + if (firedAtCompareValue != 0) + { + return firedAtCompareValue; + } + + return string.Compare(first.Key, second.Key, StringComparison.Ordinal); + } + } +} diff --git a/src/DurableTask.ServiceFabric/Utils.cs b/src/DurableTask.ServiceFabric/Utils.cs new file mode 100644 index 000000000..b5adbfbfa --- /dev/null +++ b/src/DurableTask.ServiceFabric/Utils.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.ServiceFabric +{ + using System; + using System.Diagnostics; + using System.Threading; + using System.Threading.Tasks; + using DurableTask.Core; + + internal static class Utils + { + public static OrchestrationState BuildOrchestrationState(OrchestrationRuntimeState runtimeState) + { + return new OrchestrationState + { + OrchestrationInstance = runtimeState.OrchestrationInstance, + ParentInstance = runtimeState.ParentInstance, + Name = runtimeState.Name, + Version = runtimeState.Version, + Status = runtimeState.Status, + Tags = runtimeState.Tags, + OrchestrationStatus = runtimeState.OrchestrationStatus, + CreatedTime = runtimeState.CreatedTime, + CompletedTime = runtimeState.CompletedTime, + LastUpdatedTime = DateTime.UtcNow, + Size = runtimeState.Size, + CompressedSize = runtimeState.CompressedSize, + Input = runtimeState.Input, + Output = runtimeState.Output + }; + } + + public static TimeSpan Measure(Action action) + { + var timer = Stopwatch.StartNew(); + action(); + timer.Stop(); + return timer.Elapsed; + } + + public static async Task MeasureAsync(Func asyncAction) + { + var timer = Stopwatch.StartNew(); + await asyncAction(); + timer.Stop(); + return timer.Elapsed; + } + + public static async Task RunBackgroundJob(Func loopAction, TimeSpan initialDelay, TimeSpan delayOnSuccess, TimeSpan delayOnException, string actionName, CancellationToken token) + { + try + { + await Task.Delay(initialDelay, token); + } + catch(TaskCanceledException) + { + } + + while (!token.IsCancellationRequested) + { + try + { + await loopAction(); + await Task.Delay(delayOnSuccess).ConfigureAwait(false); + } + catch(Exception e) + { + ProviderEventSource.Log.ExceptionWhileRunningBackgroundJob(actionName, e.ToString()); + await Task.Delay(delayOnException).ConfigureAwait(false); + } + } + } + } +} diff --git a/src/DurableTask.ServiceFabric/app.config b/src/DurableTask.ServiceFabric/app.config new file mode 100644 index 000000000..de5386a47 --- /dev/null +++ b/src/DurableTask.ServiceFabric/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/DurableTask.ServiceFabric/packages.config b/src/DurableTask.ServiceFabric/packages.config new file mode 100644 index 000000000..273fa63ad --- /dev/null +++ b/src/DurableTask.ServiceFabric/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/test/DurableTask.ServiceBus.Tests/DurableTask.ServiceBus.Tests.csproj b/test/DurableTask.ServiceBus.Tests/DurableTask.ServiceBus.Tests.csproj index ca02c8e55..2e8fe7c2a 100644 --- a/test/DurableTask.ServiceBus.Tests/DurableTask.ServiceBus.Tests.csproj +++ b/test/DurableTask.ServiceBus.Tests/DurableTask.ServiceBus.Tests.csproj @@ -45,4 +45,4 @@ - \ No newline at end of file + diff --git a/test/DurableTask.ServiceBus.Tests/app.config b/test/DurableTask.ServiceBus.Tests/app.config index f61f18af3..d7902b6cf 100644 --- a/test/DurableTask.ServiceBus.Tests/app.config +++ b/test/DurableTask.ServiceBus.Tests/app.config @@ -21,7 +21,7 @@ - + diff --git a/test/DurableTask.Stress.Tests/App.config b/test/DurableTask.Stress.Tests/App.config index d3e637ad3..4deacd0dd 100644 --- a/test/DurableTask.Stress.Tests/App.config +++ b/test/DurableTask.Stress.Tests/App.config @@ -37,7 +37,7 @@ - + diff --git a/test/DurableTask.Stress.Tests/DurableTask.Stress.Tests.csproj b/test/DurableTask.Stress.Tests/DurableTask.Stress.Tests.csproj index d79942393..2d1ac5d57 100644 --- a/test/DurableTask.Stress.Tests/DurableTask.Stress.Tests.csproj +++ b/test/DurableTask.Stress.Tests/DurableTask.Stress.Tests.csproj @@ -1,4 +1,4 @@ - +