diff --git a/.gitignore b/.gitignore index d5983cb5512..aab78dc4dff 100644 --- a/.gitignore +++ b/.gitignore @@ -214,3 +214,10 @@ FakesAssemblies/ /src/.Akka.boltdata/TestResults.json resetdev.bat /src/packages/repositories.config +/src/.nuget + +# FAKE build folder +.fake/ + +# Akka.Persistence Test Output +target/ diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 05d54cfef9a..0cadbf7ecc6 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,73 @@ +#### 1.0.5 December 3 2015 #### +**Maintenance release for Akka.NET v1.0.4** +This release is a collection of bug fixes, performance enhancements, and general improvements contributed by 29 individual contributors. + +**Fixes & Changes - Akka.NET Core** +* [Bugfix: Make the Put on the SimpleDnsCache idempotent](https://github.com/akkadotnet/akka.net/commit/2ed1d574f76491707deac236db3fd7c1e5af5757) +* [Add CircuitBreaker Initial based on akka 2.0.5](https://github.com/akkadotnet/akka.net/commit/7e16834ef0ff8551cdd3530eacb1016d40cb1cb8) +* [Fix for receive timeout in async await actor](https://github.com/akkadotnet/akka.net/commit/6474bd7dc3d27756e255d12ef21f331108d9922d) +* [akka-io: fixed High CPU load using the Akka.IO TCP server](https://github.com/akkadotnet/akka.net/commit/4af2cfbcaafa33ea04a1a8b1aa6486e78bd6f821) +* [akka-io: Stop select loop on idle](https://github.com/akkadotnet/akka.net/commit/e545780d36cfb805b2014746a2e97006894c2e00) +* [Serialization fixes](https://github.com/akkadotnet/akka.net/commit/6385cc20a3d310efc0bb2f9e29710c5b7bceaa87) +* [Fix issue #1301 - inprecise resizing of resizable routers](https://github.com/akkadotnet/akka.net/commit/cf714333b25190249f01d79bad606d4ce5863e47) +* [Stashing now checks message equality by reference](https://github.com/akkadotnet/akka.net/commit/884330dfb5d69b523f25a59b98450322fe3b34f4) +* [rewrote ActorPath.TryParseAddrss to now test Uris by try / catch and use Uri.TryCreate instead](https://github.com/akkadotnet/akka.net/commit/8eaf32147a08f213db818bf19d74ed9d1aadaed2) +* [Port EventBusUnsubscribers](https://github.com/akkadotnet/akka.net/commit/bd91bcd50d918e5e8ee4b085e53d603cfd46c89a) +* [Add optional PipeTo completion handlers](https://github.com/akkadotnet/akka.net/commit/dfb7f61026d5d0b2d23efe1dd73af820f70a1d1c) +* [Akka context props output to Serilog](https://github.com/akkadotnet/akka.net/commit/409cd7f4ed0b285827b681685af59ec19c5a4b73) + + +**Fixes & Changes - Akka.Remote, Akka.Cluster** +* [MultiNode tests can now be skipped by specifying a SkipReason](https://github.com/akkadotnet/akka.net/commit/75f966cb7d2f2c0d859e0e3a90a38d251a10c5e5) +* [Akka.Remote: Discard msg if payload size > max allowed.](https://github.com/akkadotnet/akka.net/commit/05f57b9b1ff256145bc085f94d49a591e51e1304) +* [Throw `TypeLoadException` when an actor type or dependency cannot be found in a remote actor deploy scenario](https://github.com/akkadotnet/akka.net/commit/ffed3eb088bc00f90a5e4b7367d4598fda007401) +* [MultiNode Test Visualizer](https://github.com/akkadotnet/akka.net/commit/7706bb242719b7f7197058e89f8579af5b82dfc3) +* [Fix for Akka.Cluster.Routing.ClusterRouterGroupSettings Mono Linux issue](https://github.com/akkadotnet/akka.net/commit/dbbd2ac9b16772af8f8e35d3d1c8bf5dcf354f42) +* [Added RemoteDeploymentWatcher](https://github.com/akkadotnet/akka.net/commit/44c29ccefaeca0abdc4fd1f81daf1dc27a285f66) +* [Akka IO Transport: framing support](https://github.com/akkadotnet/akka.net/commit/60b5d2a318b485652e0888190aaa930fe43b1bbc) +* [#1443 fix for cluster shutdown](https://github.com/akkadotnet/akka.net/commit/941688aead57266b454b76530a7fb5446f68e15d) + +**Fixes & Changes - Akka.Persistence** +* [Fixes the NullReferenceException in #1235 and appears to adhere to the practice of including an addres with the serialized binary.](https://github.com/akkadotnet/akka.net/commit/3df119ff614c3298299f863e18efd6e0fa848858) +* [Port Finite State Machine DSL to Akka.Persistence](https://github.com/akkadotnet/akka.net/commit/dce684d907df86f5039eb2ca20727ab48d4b218a) +* [Become and BecomeStacked for ReceivePersistentActor](https://github.com/akkadotnet/akka.net/commit/b11dafc86eb9284c2d515fd9da3599fe463a5681) +* [Persistent actor stops on recovery failures](https://github.com/akkadotnet/akka.net/commit/03105719a8866e8eadac268bc8f813e738f989b9) +* [Fixed: data races inside sql journal engine](https://github.com/akkadotnet/akka.net/commit/f088f0c681fdc7ba1b4eaf7f823c2a9535d3045d) +* [fix sqlite.conf and readme](https://github.com/akkadotnet/akka.net/commit/c7e925ba624eee7e386855251169aecbafd6ae7d) +* [#1416 created ReceiveActor implementation of AtLeastOnceDeliveryActor base class](https://github.com/akkadotnet/akka.net/commit/4d1d79b568bdae6565423c3ed914f8a9606dc0e8) + +A special thanks to all of our contributors, organized below by the number of changes made: + +23369 5258 18111 Aaron Stannard +18827 16329 2498 Bartosz Sypytkowski +11994 9496 2498 Steffen Forkmann +6031 4637 1394 maxim.salamatko +1987 1667 320 Graeme Bradbury +1556 1149 407 Sean Gilliam +1118 1118 0 moliver +706 370 336 rogeralsing +616 576 40 Marek Kadek +501 5 496 Alex Koshelev +377 269 108 Jeff Cyr +280 208 72 willieferguson +150 98 52 Christian Palmstierna +85 63 22 Willie Ferguson +77 71 6 Emil Ingerslev +66 61 5 Grover Jackson +60 39 21 Alexander Pantyukhin +56 33 23 Uladzimir Makarau +55 54 1 rdavisau +51 18 33 alex-kondrashov +42 26 16 Silv3rcircl3 +36 30 6 evertmulder +33 19 14 Filip Malachowicz +13 11 2 Suhas Chatekar +7 6 1 tintoy +4 2 2 Jonathan +2 1 1 neekgreen +2 1 1 Christopher Martin +2 1 1 Artem Borzilov + #### 1.0.4 August 07 2015 #### **Maintenance release for Akka.NET v1.0.3** diff --git a/build.cmd b/build.cmd index 1ab7b81ac20..2d3c363daa4 100644 --- a/build.cmd +++ b/build.cmd @@ -1,15 +1,35 @@ @echo off +pushd %~dp0 + +SETLOCAL +SET CACHED_NUGET=%LocalAppData%\NuGet\NuGet.exe + +IF EXIST %CACHED_NUGET% goto copynuget +echo Downloading latest version of NuGet.exe... +IF NOT EXIST %LocalAppData%\NuGet md %LocalAppData%\NuGet +@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://www.nuget.org/nuget.exe' -OutFile '%CACHED_NUGET%'" + +:copynuget +IF EXIST src\.nuget\nuget.exe goto restore +md src\.nuget +copy %CACHED_NUGET% src\.nuget\nuget.exe > nul + +:restore + +src\.nuget\NuGet.exe update -self + + pushd %~dp0 src\.nuget\NuGet.exe update -self -src\.nuget\NuGet.exe install FAKE -ConfigFile src\.nuget\Nuget.Config -OutputDirectory src\packages -ExcludeVersion -Version 3.28.8 +src\.nuget\NuGet.exe install FAKE -ConfigFile src\.nuget\Nuget.Config -OutputDirectory src\packages -ExcludeVersion -Version 4.1.0 src\.nuget\NuGet.exe install xunit.runner.console -ConfigFile src\.nuget\Nuget.Config -OutputDirectory src\packages\FAKE -ExcludeVersion -Version 2.0.0 src\.nuget\NuGet.exe install nunit.runners -ConfigFile src\.nuget\Nuget.Config -OutputDirectory src\packages\FAKE -ExcludeVersion -Version 2.6.4 -if not exist src\packages\SourceLink.Fake\tools\SourceLink.fsx ( +if not exist src\packages\SourceLink.Fake\tools\SourceLink.fsx ( src\.nuget\nuget.exe install SourceLink.Fake -ConfigFile src\.nuget\Nuget.Config -OutputDirectory src\packages -ExcludeVersion ) rem cls @@ -17,4 +37,4 @@ rem cls set encoding=utf-8 src\packages\FAKE\tools\FAKE.exe build.fsx %* -popd \ No newline at end of file +popd diff --git a/build.fsx b/build.fsx index 09b32b0e406..0ba301dad97 100644 --- a/build.fsx +++ b/build.fsx @@ -51,7 +51,7 @@ printfn "Assembly version: %s\nNuget version; %s\n" release.AssemblyVersion rele // Directories let binDir = "bin" -let testOutput = "TestResults" +let testOutput = FullName "TestResults" let nugetDir = binDir @@ "nuget" let workingDir = binDir @@ "build" @@ -213,7 +213,7 @@ Target "CleanTests" <| fun _ -> //-------------------------------------------------------------------------------- // Run tests -open XUnit2Helper +open Fake.Testing Target "RunTests" <| fun _ -> let msTestAssemblies = !! "src/**/bin/Release/Akka.TestKit.VsTest.Tests.dll" let nunitTestAssemblies = !! "src/**/bin/Release/Akka.TestKit.NUnit.Tests.dll" @@ -233,7 +233,7 @@ Target "RunTests" <| fun _ -> let xunitToolPath = findToolInSubPath "xunit.console.exe" "src/packages/xunit.runner.console*/tools" printfn "Using XUnit runner: %s" xunitToolPath xUnit2 - (fun p -> { p with OutputDir = testOutput; ToolPath = xunitToolPath }) + (fun p -> { p with XmlOutputPath = Some (testOutput + @"\XUnitTestResults.xml"); HtmlOutputPath = Some (testOutput + @"\XUnitTestResults.HTML"); ToolPath = xunitToolPath; TimeOut = System.TimeSpan.FromMinutes 30.0; Parallel = ParallelMode.NoParallelization }) xunitTestAssemblies Target "RunTestsMono" <| fun _ -> @@ -244,26 +244,37 @@ Target "RunTestsMono" <| fun _ -> let xunitToolPath = findToolInSubPath "xunit.console.exe" "src/packages/xunit.runner.console*/tools" printfn "Using XUnit runner: %s" xunitToolPath xUnit2 - (fun p -> { p with OutputDir = testOutput; ToolPath = xunitToolPath }) + (fun p -> { p with XmlOutputPath = Some (testOutput + @"\XUnitTestResults.xml"); HtmlOutputPath = Some (testOutput + @"\XUnitTestResults.HTML"); ToolPath = xunitToolPath; TimeOut = System.TimeSpan.FromMinutes 30.0; Parallel = ParallelMode.NoParallelization }) xunitTestAssemblies Target "MultiNodeTests" <| fun _ -> + let testSearchPath = + let assemblyFilter = getBuildParamOrDefault "spec-assembly" String.Empty + sprintf "src/**/bin/Release/*%s*.Tests.MultiNode.dll" assemblyFilter + + mkdir testOutput let multiNodeTestPath = findToolInSubPath "Akka.MultiNodeTestRunner.exe" "bin/core/Akka.MultiNodeTestRunner*" + let multiNodeTestAssemblies = !! testSearchPath printfn "Using MultiNodeTestRunner: %s" multiNodeTestPath - let spec = getBuildParam "spec" + let runMultiNodeSpec assembly = + let spec = getBuildParam "spec" - let args = new StringBuilder() - |> append "Akka.MultiNodeTests.dll" + let args = new StringBuilder() + |> append assembly |> append "-Dmultinode.enable-filesink=on" + |> append (sprintf "-Dmultinode.output-directory=\"%s\"" testOutput) |> appendIfNotNullOrEmpty spec "-Dmultinode.test-spec=" |> toText - let result = ExecProcess(fun info -> - info.FileName <- multiNodeTestPath - info.WorkingDirectory <- (Path.GetDirectoryName (FullName multiNodeTestPath)) - info.Arguments <- args) (System.TimeSpan.FromMinutes 60.0) (* This is a VERY long running task. *) - if result <> 0 then failwithf "MultiNodeTestRunner failed. %s %s" multiNodeTestPath args + let result = ExecProcess(fun info -> + info.FileName <- multiNodeTestPath + info.WorkingDirectory <- (Path.GetDirectoryName (FullName multiNodeTestPath)) + info.Arguments <- args) (System.TimeSpan.FromMinutes 60.0) (* This is a VERY long running task. *) + if result <> 0 then failwithf "MultiNodeTestRunner failed. %s %s" multiNodeTestPath args + + multiNodeTestAssemblies |> Seq.iter (runMultiNodeSpec) + //-------------------------------------------------------------------------------- // Nuget targets @@ -400,12 +411,18 @@ let publishNugetPackages _ = !! (nugetDir @@ "*.nupkg") -- (nugetDir @@ "*.symbols.nupkg") |> Seq.sortBy(fun x -> x.ToLower()) for package in normalPackages do - publishPackage (getBuildParamOrDefault "nugetpublishurl" "") (getBuildParam "nugetkey") 3 package + try + publishPackage (getBuildParamOrDefault "nugetpublishurl" "") (getBuildParam "nugetkey") 3 package + with exn -> + printfn "%s" exn.Message if shouldPushSymbolsPackages then let symbolPackages= !! (nugetDir @@ "*.symbols.nupkg") |> Seq.sortBy(fun x -> x.ToLower()) for package in symbolPackages do - publishPackage (getBuildParam "symbolspublishurl") (getBuildParam "symbolskey") 3 package + try + publishPackage (getBuildParam "symbolspublishurl") (getBuildParam "symbolskey") 3 package + with exn -> + printfn "%s" exn.Message Target "Nuget" <| fun _ -> @@ -433,12 +450,14 @@ Target "Help" <| fun _ -> " * Build Builds" " * Nuget Create and optionally publish nugets packages" " * RunTests Runs tests" + " * MultiNodeTests Runs the slower multiple node specifications" " * All Builds, run tests, creates and optionally publish nuget packages" "" " Other Targets" " * Help Display this help" " * HelpNuget Display help about creating and pushing nuget packages" - " * HelpDocs Display help about creating and pushing API docs" + " * HelpDocs Display help about creating and pushing API docs" + " * HelpMultiNodeTests Display help about running the multiple node specifications" ""] Target "HelpNuget" <| fun _ -> @@ -517,6 +536,19 @@ Target "HelpDocs" <| fun _ -> " Build and publish docs to http://fooaccount.blob.core.windows.net/docs/unstable" ""] +Target "HelpMultiNodeTests" <| fun _ -> + List.iter printfn [ + "usage: " + "build MultiNodeTests [spec-assembly=]" + "Just runs the MultiNodeTests. Does not build the projects." + "" + "Arguments for MultiNodeTests target:" + " [spec-assembly=] Restrict which spec projects are run." + "" + " Alters the discovery filter to enable restricting which specs are run." + " If not supplied the filter used is '*.Tests.Multinode.Dll'" + " When supplied this is altered to '**.Tests.Multinode.Dll'" + ""] //-------------------------------------------------------------------------------- // Target dependencies //-------------------------------------------------------------------------------- @@ -526,6 +558,7 @@ Target "HelpDocs" <| fun _ -> // tests dependencies "CleanTests" ==> "RunTests" +"BuildRelease" ==> "CleanTests" ==> "MultiNodeTests" // nuget dependencies "CleanNuget" ==> "CreateNuget" diff --git a/build.sh b/build.sh index 16e418d34b2..7004e2907eb 100755 --- a/build.sh +++ b/build.sh @@ -1,4 +1,20 @@ #!/bin/bash +SCRIPT_PATH="${BASH_SOURCE[0]}"; +if ([ -h "${SCRIPT_PATH}" ]) then + while([ -h "${SCRIPT_PATH}" ]) do SCRIPT_PATH=`readlink "${SCRIPT_PATH}"`; done +fi +pushd . > /dev/null +cd `dirname ${SCRIPT_PATH}` > /dev/null +SCRIPT_PATH=`pwd`; +popd > /dev/null + +if ! [ -f $SCRIPT_PATH/src/.nuget/nuget.exe ] + then + wget "https://www.nuget.org/nuget.exe" -P $SCRIPT_PATH/src/.nuget/ +fi + +mono $SCRIPT_PATH/src/.nuget/nuget.exe update -self + SCRIPT_PATH="${BASH_SOURCE[0]}"; if ([ -h "${SCRIPT_PATH}" ]) then diff --git a/src/.nuget/packages.config b/src/.nuget/packages.config index 086897c3cb9..569e1bea867 100644 --- a/src/.nuget/packages.config +++ b/src/.nuget/packages.config @@ -1,4 +1,3 @@  - - \ No newline at end of file + diff --git a/src/.vs/config/applicationhost.config b/src/.vs/config/applicationhost.config new file mode 100644 index 00000000000..c2abfb4801d --- /dev/null +++ b/src/.vs/config/applicationhost.config @@ -0,0 +1,1030 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Akka.sln b/src/Akka.sln index 8e8d1546a30..a1c730d72c5 100644 --- a/src/Akka.sln +++ b/src/Akka.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22823.1 +VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{69279534-1DBA-4115-BF8B-03F77FC8125E}" EndProject @@ -135,7 +135,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{8A15341C-5596-4F4D-949D-2CD9A0006E12}" ProjectSection(SolutionItems) = preProject .nuget\NuGet.Config = .nuget\NuGet.Config - .nuget\NuGet.exe = .nuget\NuGet.exe + .nuget\nuget.exe = .nuget\nuget.exe .nuget\NuGet.targets = .nuget\NuGet.targets EndProjectSection EndProject @@ -208,10 +208,23 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.DI.StructureMap.Tests" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.DI.Unity.Tests", "contrib\dependencyInjection\Akka.DI.Unity.Tests\Akka.DI.Unity.Tests.csproj", "{54C76459-D93B-4FF5-A051-4D9329EF4201}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.MultiNodeTests", "core\Akka.MultiNodeTests\Akka.MultiNodeTests.csproj", "{F0781BEA-5BA0-4AF0-BB15-E3F209B681F5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Cluster.Tests.MultiNode", "core\Akka.Cluster.Tests.MultiNode\Akka.Cluster.Tests.MultiNode.csproj", "{F0781BEA-5BA0-4AF0-BB15-E3F209B681F5}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PersistenceBenchmark", "benchmark\PersistenceBenchmark\PersistenceBenchmark.csproj", "{39E6F51F-FA1E-4C62-B8F8-19065DE6D55D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A59BAE84-70E2-46A0-9E26-7413C103E2D7}" + ProjectSection(SolutionItems) = preProject + WebEssentials-Settings.json = WebEssentials-Settings.json + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Remote.Tests.MultiNode", "core\Akka.Remote.Tests.MultiNode\Akka.Remote.Tests.MultiNode.csproj", "{C9105C76-B084-4DA1-9348-1C74A8F22F6B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.Sql.Common.TestKit", "contrib\persistence\Akka.Persistence.Sql.Common.TestKit\Akka.Persistence.Sql.Common.TestKit.csproj", "{E7BC4F35-B9FB-49CB-B12C-CD00F6A08EA2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.Sqlite", "contrib\persistence\Akka.Persistence.Sqlite\Akka.Persistence.Sqlite.csproj", "{453EFD22-7C53-4887-9DBF-FCFC9172E909}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.Sqlite.Tests", "contrib\persistence\Akka.Persistence.Sqlite.Tests\Akka.Persistence.Sqlite.Tests.csproj", "{7A832BBF-053E-4E9F-BD83-D988A0130CC8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug Mono|Any CPU = Debug Mono|Any CPU @@ -777,6 +790,38 @@ Global {39E6F51F-FA1E-4C62-B8F8-19065DE6D55D}.Release Mono|Any CPU.Build.0 = Release|Any CPU {39E6F51F-FA1E-4C62-B8F8-19065DE6D55D}.Release|Any CPU.ActiveCfg = Release|Any CPU {39E6F51F-FA1E-4C62-B8F8-19065DE6D55D}.Release|Any CPU.Build.0 = Release|Any CPU + {C9105C76-B084-4DA1-9348-1C74A8F22F6B}.Debug Mono|Any CPU.ActiveCfg = Debug|Any CPU + {C9105C76-B084-4DA1-9348-1C74A8F22F6B}.Debug Mono|Any CPU.Build.0 = Debug|Any CPU + {C9105C76-B084-4DA1-9348-1C74A8F22F6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9105C76-B084-4DA1-9348-1C74A8F22F6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9105C76-B084-4DA1-9348-1C74A8F22F6B}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU + {C9105C76-B084-4DA1-9348-1C74A8F22F6B}.Release Mono|Any CPU.Build.0 = Release|Any CPU + {C9105C76-B084-4DA1-9348-1C74A8F22F6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9105C76-B084-4DA1-9348-1C74A8F22F6B}.Release|Any CPU.Build.0 = Release|Any CPU + {E7BC4F35-B9FB-49CB-B12C-CD00F6A08EA2}.Debug Mono|Any CPU.ActiveCfg = Debug|Any CPU + {E7BC4F35-B9FB-49CB-B12C-CD00F6A08EA2}.Debug Mono|Any CPU.Build.0 = Debug|Any CPU + {E7BC4F35-B9FB-49CB-B12C-CD00F6A08EA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7BC4F35-B9FB-49CB-B12C-CD00F6A08EA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7BC4F35-B9FB-49CB-B12C-CD00F6A08EA2}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU + {E7BC4F35-B9FB-49CB-B12C-CD00F6A08EA2}.Release Mono|Any CPU.Build.0 = Release|Any CPU + {E7BC4F35-B9FB-49CB-B12C-CD00F6A08EA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7BC4F35-B9FB-49CB-B12C-CD00F6A08EA2}.Release|Any CPU.Build.0 = Release|Any CPU + {453EFD22-7C53-4887-9DBF-FCFC9172E909}.Debug Mono|Any CPU.ActiveCfg = Debug|Any CPU + {453EFD22-7C53-4887-9DBF-FCFC9172E909}.Debug Mono|Any CPU.Build.0 = Debug|Any CPU + {453EFD22-7C53-4887-9DBF-FCFC9172E909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {453EFD22-7C53-4887-9DBF-FCFC9172E909}.Debug|Any CPU.Build.0 = Debug|Any CPU + {453EFD22-7C53-4887-9DBF-FCFC9172E909}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU + {453EFD22-7C53-4887-9DBF-FCFC9172E909}.Release Mono|Any CPU.Build.0 = Release|Any CPU + {453EFD22-7C53-4887-9DBF-FCFC9172E909}.Release|Any CPU.ActiveCfg = Release|Any CPU + {453EFD22-7C53-4887-9DBF-FCFC9172E909}.Release|Any CPU.Build.0 = Release|Any CPU + {7A832BBF-053E-4E9F-BD83-D988A0130CC8}.Debug Mono|Any CPU.ActiveCfg = Debug|Any CPU + {7A832BBF-053E-4E9F-BD83-D988A0130CC8}.Debug Mono|Any CPU.Build.0 = Debug|Any CPU + {7A832BBF-053E-4E9F-BD83-D988A0130CC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A832BBF-053E-4E9F-BD83-D988A0130CC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A832BBF-053E-4E9F-BD83-D988A0130CC8}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU + {7A832BBF-053E-4E9F-BD83-D988A0130CC8}.Release Mono|Any CPU.Build.0 = Release|Any CPU + {7A832BBF-053E-4E9F-BD83-D988A0130CC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A832BBF-053E-4E9F-BD83-D988A0130CC8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -872,5 +917,9 @@ Global {54C76459-D93B-4FF5-A051-4D9329EF4201} = {B1D10183-8FAE-4506-B935-403FCED89BDB} {F0781BEA-5BA0-4AF0-BB15-E3F209B681F5} = {01167D3C-49C4-4CDE-9787-C176D139ACDD} {39E6F51F-FA1E-4C62-B8F8-19065DE6D55D} = {73108242-625A-4D7B-AA09-63375DBAE464} + {C9105C76-B084-4DA1-9348-1C74A8F22F6B} = {01167D3C-49C4-4CDE-9787-C176D139ACDD} + {E7BC4F35-B9FB-49CB-B12C-CD00F6A08EA2} = {264C22A4-CAFC-41F6-B82C-4DDC5C196767} + {453EFD22-7C53-4887-9DBF-FCFC9172E909} = {264C22A4-CAFC-41F6-B82C-4DDC5C196767} + {7A832BBF-053E-4E9F-BD83-D988A0130CC8} = {264C22A4-CAFC-41F6-B82C-4DDC5C196767} EndGlobalSection EndGlobal diff --git a/src/SharedAssemblyInfo.cs b/src/SharedAssemblyInfo.cs index 42a94a0fe1a..fcb5f369368 100644 --- a/src/SharedAssemblyInfo.cs +++ b/src/SharedAssemblyInfo.cs @@ -4,5 +4,5 @@ [assembly: AssemblyCompanyAttribute("Akka.NET Team")] [assembly: AssemblyCopyrightAttribute("Copyright © 2013-2015 Akka.NET Team")] [assembly: AssemblyTrademarkAttribute("")] -[assembly: AssemblyVersionAttribute("1.0.4.0")] -[assembly: AssemblyFileVersionAttribute("1.0.4.0")] +[assembly: AssemblyVersionAttribute("1.0.5.0")] +[assembly: AssemblyFileVersionAttribute("1.0.5.0")] diff --git a/src/benchmark/PersistenceBenchmark/PersistenceBenchmark.csproj b/src/benchmark/PersistenceBenchmark/PersistenceBenchmark.csproj index 9a6a870ba41..11b1a369112 100644 --- a/src/benchmark/PersistenceBenchmark/PersistenceBenchmark.csproj +++ b/src/benchmark/PersistenceBenchmark/PersistenceBenchmark.csproj @@ -11,6 +11,8 @@ PersistenceBenchmark v4.5 512 + ..\..\ + true AnyCPU @@ -74,6 +76,13 @@ + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common.TestKit/Properties/AssemblyInfo.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common.TestKit/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..c7e4a04c7d3 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common.TestKit/Properties/AssemblyInfo.cs @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +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("Akka.Persistence.Sql.Common.TestKit")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Akka.Persistence.Sql.Common.TestKit")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[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("e7bc4f35-b9fb-49cb-b12c-cd00f6a08ea2")] + +// 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/src/contrib/persistence/Akka.Persistence.Sql.Common.TestKit/SqlJournalQuerySpec.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common.TestKit/SqlJournalQuerySpec.cs new file mode 100644 index 00000000000..aecbd04bda0 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common.TestKit/SqlJournalQuerySpec.cs @@ -0,0 +1,147 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Akka.Actor; +using Akka.Configuration; +using Akka.Persistence.Sql.Common.Journal; +using Akka.Persistence.Sql.Common.Queries; +using Akka.Persistence.TestKit; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Persistence.Sql.Common.TestKit +{ + public abstract class SqlJournalQuerySpec : PluginSpec + { + class TestTimestampProvider : ITimestampProvider + { + private static IDictionary, DateTime> KnownEventTimestampMappings = new Dictionary, DateTime> + { + {Tuple.Create("p-1", 1L), new DateTime(2001, 1, 1) }, + {Tuple.Create("p-1", 2L), new DateTime(2001, 1, 2) }, + {Tuple.Create("p-1", 3L), new DateTime(2001, 1, 3) }, + {Tuple.Create("p-2", 1L), new DateTime(2001, 1, 1) }, + {Tuple.Create("p-2", 2L), new DateTime(2001, 2, 1) }, + {Tuple.Create("p-3", 1L), new DateTime(2001, 1, 1) }, + {Tuple.Create("p-3", 2L), new DateTime(2003, 1, 1) }, + }; + + public DateTime GenerateTimestamp(IPersistentRepresentation message) + { + return KnownEventTimestampMappings[Tuple.Create(message.PersistenceId, message.SequenceNr)]; + } + } + + public static string TimestampConfig(string plugin) + { + return plugin + ".timestamp-provider =\"" + typeof(TestTimestampProvider).FullName + ", Akka.Persistence.Sql.Common.TestKit\""; + } + + private static readonly IPersistentRepresentation[] Events = + { + new Persistent("a-1", 1, "System.String", "p-1"), + new Persistent("a-2", 2, "System.String", "p-1"), + new Persistent("a-3", 3, "System.String", "p-1"), + new Persistent("a-4", 1, "System.String", "p-2"), + new Persistent(5, 2, "System.Int32", "p-2"), + new Persistent(6, 1, "System.Int32", "p-3"), + new Persistent("a-7", 2, "System.String", "p-3"), + }; + + public IActorRef JournalRef { get; protected set; } + + protected SqlJournalQuerySpec(Config config = null, string actorSystemName = null, ITestOutputHelper output = null) + : base(config, actorSystemName, output) + { + JournalRef = Extension.JournalFor(null); + } + + [Fact] + public void Journal_queried_on_PersistenceIdRange_returns_events_for_particular_persistent_ids() + { + var query = new Query(1, Hints.PersistenceIds(new[] { "p-1", "p-2" })); + QueryAndExpectSuccess(query, Events[0], Events[1], Events[2], Events[3], Events[4]); + } + + [Fact] + public void Journal_queried_on_Manifest_returns_events_with_particular_manifest() + { + var query = new Query(2, Hints.Manifest("System.Int32")); + QueryAndExpectSuccess(query, Events[4], Events[5]); + } + + [Fact] + public void Journal_queried_on_Timestamp_returns_events_occurred_after_or_equal_From_value() + { + var query = new Query(3, Hints.TimestampAfter(new DateTime(2001, 1, 3))); + QueryAndExpectSuccess(query, Events[2], Events[4], Events[6]); + } + + [Fact] + public void Journal_queried_on_Timestamp_returns_events_occurred_before_To_value() + { + var query = new Query(4, Hints.TimestampBefore(new DateTime(2001, 2, 1))); + QueryAndExpectSuccess(query, Events[0], Events[1], Events[2], Events[3], Events[5]); + } + + [Fact] + public void Journal_queried_on_Timestamp_returns_events_occurred_between_both_range_values() + { + var query = new Query(5, Hints.TimestampBetween(new DateTime(2001, 1, 3), new DateTime(2003, 1, 1))); + QueryAndExpectSuccess(query, Events[2], Events[4]); + } + + [Fact] + public void Journal_queried_using_multiple_hints_should_apply_all_of_them() + { + var query = new Query(6, + Hints.TimestampBefore(new DateTime(2001, 1, 3)), + Hints.PersistenceIds(new[] { "p-1", "p-2" }), + Hints.Manifest("System.String")); + + QueryAndExpectSuccess(query, Events[0], Events[1], Events[3]); + } + + protected void Initialize() + { + WriteEvents(); + } + + private void WriteEvents() + { + var probe = CreateTestProbe(); + var message = new WriteMessages(Events, probe.Ref, ActorInstanceId); + + JournalRef.Tell(message); + probe.ExpectMsg(); + foreach (var persistent in Events) + { + probe.ExpectMsg(new WriteMessageSuccess(persistent, ActorInstanceId)); + } + } + + private void QueryAndExpectSuccess(Query query, params IPersistentRepresentation[] events) + { + JournalRef.Tell(query, TestActor); + + foreach (var e in events) + { + ExpectMsg(q => + q.QueryId == query.QueryId && + q.Message.PersistenceId == e.PersistenceId && + q.Message.SequenceNr == e.SequenceNr && + q.Message.Manifest == e.Manifest && + q.Message.IsDeleted == e.IsDeleted && + Equals(q.Message.Payload, e.Payload)); + } + + ExpectMsg(new QuerySuccess(query.QueryId)); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNodeTests/packages.config b/src/contrib/persistence/Akka.Persistence.Sql.Common.TestKit/packages.config similarity index 81% rename from src/core/Akka.MultiNodeTests/packages.config rename to src/contrib/persistence/Akka.Persistence.Sql.Common.TestKit/packages.config index 597b89ce463..3109db107fb 100644 --- a/src/core/Akka.MultiNodeTests/packages.config +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common.TestKit/packages.config @@ -1,6 +1,5 @@  - diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Akka.Persistence.Sql.Common.csproj b/src/contrib/persistence/Akka.Persistence.Sql.Common/Akka.Persistence.Sql.Common.csproj index fb06306d447..1e26d5753ad 100644 --- a/src/contrib/persistence/Akka.Persistence.Sql.Common/Akka.Persistence.Sql.Common.csproj +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Akka.Persistence.Sql.Common.csproj @@ -31,6 +31,7 @@ + @@ -40,12 +41,16 @@ + + + + - + diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/ITimestampProvider.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/ITimestampProvider.cs new file mode 100644 index 00000000000..489d65abbb2 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/ITimestampProvider.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; + +namespace Akka.Persistence.Sql.Common.Journal +{ + /// + /// Interface responsible for generation of timestamps for persisted messages in SQL-based journals. + /// + public interface ITimestampProvider + { + /// + /// Generates timestamp for provided message. + /// + DateTime GenerateTimestamp(IPersistentRepresentation message); + } + + /// + /// Default implementation of timestamp provider. Returns for any message. + /// + public sealed class DefaultTimestampProvider : ITimestampProvider + { + public DateTime GenerateTimestamp(IPersistentRepresentation message) + { + return DateTime.UtcNow; + } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/JournalDbEngine.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/JournalDbEngine.cs index df199d4042b..12b9136bd3b 100644 --- a/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/JournalDbEngine.cs +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/JournalDbEngine.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; -using System.Data; +using System.Configuration; using System.Data.Common; using System.Linq; using System.Threading; using System.Threading.Tasks; using Akka.Actor; +using Akka.Event; +using Akka.Persistence.Sql.Common.Queries; namespace Akka.Persistence.Sql.Common.Journal { @@ -20,21 +22,23 @@ namespace Akka.Persistence.Sql.Common.Journal /// Class used for storing intermediate result of the /// in form which is ready to be stored directly in the SQL table. /// - public class JournalEntry + public sealed class JournalEntry { public readonly string PersistenceId; public readonly long SequenceNr; public readonly bool IsDeleted; - public readonly string PayloadType; + public readonly string Manifest; + public readonly DateTime Timestamp; public readonly byte[] Payload; - public JournalEntry(string persistenceId, long sequenceNr, bool isDeleted, string payloadType, byte[] payload) + public JournalEntry(string persistenceId, long sequenceNr, bool isDeleted, string manifest, DateTime timestamp, byte[] payload) { PersistenceId = persistenceId; SequenceNr = sequenceNr; IsDeleted = isDeleted; - PayloadType = payloadType; + Manifest = manifest; Payload = payload; + Timestamp = timestamp; } } @@ -47,29 +51,40 @@ public abstract class JournalDbEngine : IDisposable /// Settings applied to journal mapped from HOCON config file. /// public readonly JournalSettings Settings; - + /// - /// List of cancellation tokens for each of the currently pending database operations. + /// Timestamp provider used for generation of timestamps for incoming persistent messages. /// - protected readonly LinkedList PendingOperations; + protected readonly ITimestampProvider TimestampProvider; - private readonly Akka.Serialization.Serialization _serialization; - private DbConnection _dbConnection; + private readonly ActorSystem _system; + private readonly CancellationTokenSource _pendingRequestsCancellation; - protected JournalDbEngine(JournalSettings settings, Akka.Serialization.Serialization serialization) + protected JournalDbEngine(ActorSystem system) { - Settings = settings; - _serialization = serialization; + _system = system; - QueryMapper = new DefaultJournalQueryMapper(serialization); + Settings = new JournalSettings(system.Settings.Config.GetConfig(JournalConfigPath)); + QueryMapper = new DefaultJournalQueryMapper(_system.Serialization); + TimestampProvider = CreateTimestampProvider(); - PendingOperations = new LinkedList(); + _pendingRequestsCancellation = new CancellationTokenSource(); } + /// + /// Returns a HOCON config path to associated journal. + /// + protected abstract string JournalConfigPath { get; } + + /// + /// System logger. + /// + protected ILoggingAdapter Log { get { return _system.Log; } } + /// /// Initializes a database connection. /// - protected abstract DbConnection CreateDbConnection(); + protected abstract DbConnection CreateDbConnection(string connectionString); /// /// Copies values from entities to database command. @@ -78,31 +93,20 @@ protected JournalDbEngine(JournalSettings settings, Akka.Serialization.Serializa /// protected abstract void CopyParamsToCommand(DbCommand sqlCommand, JournalEntry entry); - /// - /// Gets database connection. - /// - public IDbConnection DbConnection { get { return _dbConnection; } } - /// /// Used for generating SQL commands for journal-related database operations. /// - public IJournalQueryBuilder QueryBuilder { get; protected set; } + public IJournalQueryBuilder QueryBuilder { get; set; } /// /// Used for mapping results returned from database into objects. /// - public IJournalQueryMapper QueryMapper { get; protected set; } + public IJournalQueryMapper QueryMapper { get; set; } - /// - /// Initializes and opens a database connection. - /// - public void Open() + public DbConnection CreateDbConnection() { - // close connection if it was open - Close(); - - _dbConnection = CreateDbConnection(); - _dbConnection.Open(); + var connectionString = GetConnectionString(); + return CreateDbConnection(connectionString); } /// @@ -110,37 +114,43 @@ public void Open() /// public void Close() { - if (_dbConnection != null) - { - StopPendingOperations(); + _pendingRequestsCancellation.Cancel(); + } - _dbConnection.Dispose(); - _dbConnection = null; - } + void IDisposable.Dispose() + { + Close(); } /// - /// Stops all currently executing database operations. + /// Performs /// - protected void StopPendingOperations() + public async Task ReadEvents(object queryId, IEnumerable hints, IActorRef sender, Action replayCallback) { - // stop all operations executed in the background - var node = PendingOperations.First; - while (node != null) + using (var connection = CreateDbConnection()) { - var curr = node; - node = node.Next; + await connection.OpenAsync(); - curr.Value.Cancel(); - PendingOperations.Remove(curr); + var sqlCommand = QueryBuilder.SelectEvents(hints); + CompleteCommand(sqlCommand, connection); + + var reader = await sqlCommand.ExecuteReaderAsync(_pendingRequestsCancellation.Token); + try + { + while (reader.Read()) + { + var persistent = QueryMapper.Map(reader, sender); + if (persistent != null) + replayCallback(persistent); + } + } + finally + { + reader.Close(); + } } } - void IDisposable.Dispose() - { - Close(); - } - /// /// Asynchronously replays all requested messages related to provided , /// using provided sequence ranges (inclusive) with number of messages replayed @@ -151,169 +161,130 @@ void IDisposable.Dispose() /// Upper inclusive sequence number bound. Unbound by default. /// Maximum number of messages to be replayed. Unbound by default. /// Action invoked for each replayed message. - public Task ReplayMessagesAsync(string persistenceId, long fromSequenceNr, long toSequenceNr, long max, IActorRef sender, Action replayCallback) + public async Task ReplayMessagesAsync(string persistenceId, long fromSequenceNr, long toSequenceNr, long max, IActorRef sender, Action replayCallback) { - var sqlCommand = QueryBuilder.SelectMessages(persistenceId, fromSequenceNr, toSequenceNr, max); - CompleteCommand(sqlCommand); + using (var connection = CreateDbConnection()) + { + await connection.OpenAsync(); - var tokenSource = GetCancellationTokenSource(); + var sqlCommand = QueryBuilder.SelectMessages(persistenceId, fromSequenceNr, toSequenceNr, max); + CompleteCommand(sqlCommand, connection); + + var reader = await sqlCommand.ExecuteReaderAsync(_pendingRequestsCancellation.Token); - return sqlCommand - .ExecuteReaderAsync(tokenSource.Token) - .ContinueWith(task => + try { - var reader = task.Result; - try + while (reader.Read()) { - while (reader.Read()) - { - var persistent = QueryMapper.Map(reader, sender); - if (persistent != null) - { - replayCallback(persistent); - } - } + var persistent = QueryMapper.Map(reader, sender); + if (persistent != null) + replayCallback(persistent); } - finally - { - PendingOperations.Remove(tokenSource); - reader.Close(); - } - }, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.AttachedToParent); + } + finally + { + reader.Close(); + } + } } /// /// Asynchronously reads a highest sequence number of the event stream related with provided . /// - public Task ReadHighestSequenceNrAsync(string persistenceId, long fromSequenceNr) + public async Task ReadHighestSequenceNrAsync(string persistenceId, long fromSequenceNr) { - var sqlCommand = QueryBuilder.SelectHighestSequenceNr(persistenceId); - CompleteCommand(sqlCommand); + using (var connection = CreateDbConnection()) + { + await connection.OpenAsync(); - var tokenSource = GetCancellationTokenSource(); + var sqlCommand = QueryBuilder.SelectHighestSequenceNr(persistenceId); + CompleteCommand(sqlCommand, connection); - return sqlCommand - .ExecuteScalarAsync(tokenSource.Token) - .ContinueWith(task => - { - PendingOperations.Remove(tokenSource); - var result = task.Result; - return result is long ? Convert.ToInt64(task.Result) : 0L; - }, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.AttachedToParent); + var seqNr = await sqlCommand.ExecuteScalarAsync(_pendingRequestsCancellation.Token); + return seqNr is long ? Convert.ToInt64(seqNr) : 0L; + } } /// - /// Synchronously writes all persistent inside SQL Server database. + /// Asynchronously writes all persistent inside SQL Server database. /// /// Specific table used for message persistence may be defined through configuration within /// 'akka.persistence.journal.sql-server' scope with 'schema-name' and 'table-name' keys. /// - public void WriteMessages(IEnumerable messages) + public async Task WriteMessagesAsync(IEnumerable messages) { - var persistentMessages = messages.ToArray(); - var sqlCommand = QueryBuilder.InsertBatchMessages(persistentMessages); - CompleteCommand(sqlCommand); + using (var connection = CreateDbConnection()) + { + await connection.OpenAsync(); - var journalEntries = persistentMessages.Select(ToJournalEntry).ToList(); + var persistentMessages = messages.ToArray(); + var sqlCommand = QueryBuilder.InsertBatchMessages(persistentMessages); + CompleteCommand(sqlCommand, connection); - InsertInTransaction(sqlCommand, journalEntries); + var journalEntries = persistentMessages.Select(ToJournalEntry).ToList(); + await InsertInTransactionAsync(sqlCommand, journalEntries); + } } /// - /// Synchronously deletes all persisted messages identified by provided + /// Asynchronously deletes all persisted messages identified by provided /// up to provided message sequence number (inclusive). If flag is cleared, /// messages will still reside inside database, but will be logically counted as deleted. /// - public void DeleteMessagesTo(string persistenceId, long toSequenceNr, bool isPermanent) - { - var sqlCommand = QueryBuilder.DeleteBatchMessages(persistenceId, toSequenceNr, isPermanent); - CompleteCommand(sqlCommand); - - sqlCommand.ExecuteNonQuery(); - } - - /// - /// Asynchronously writes all persistent inside SQL Server database. - /// - /// Specific table used for message persistence may be defined through configuration within - /// 'akka.persistence.journal.sql-server' scope with 'schema-name' and 'table-name' keys. - /// - public async Task WriteMessagesAsync(IEnumerable messages) + public async Task DeleteMessagesToAsync(string persistenceId, long toSequenceNr, bool isPermanent) { - var persistentMessages = messages.ToArray(); - var sqlCommand = QueryBuilder.InsertBatchMessages(persistentMessages); - CompleteCommand(sqlCommand); + using (var connection = CreateDbConnection()) + { + await connection.OpenAsync(); - var journalEntries = persistentMessages.Select(ToJournalEntry).ToList(); + var sqlCommand = QueryBuilder.DeleteBatchMessages(persistenceId, toSequenceNr, isPermanent); + CompleteCommand(sqlCommand, connection); - await InsertInTransactionAsync(sqlCommand, journalEntries); + await sqlCommand.ExecuteNonQueryAsync(); + } } /// - /// Asynchronously deletes all persisted messages identified by provided - /// up to provided message sequence number (inclusive). If flag is cleared, - /// messages will still reside inside database, but will be logically counted as deleted. + /// Returns connection string from either HOCON configuration or <connectionStrings> section of app.config. /// - public async Task DeleteMessagesToAsync(string persistenceId, long toSequenceNr, bool isPermanent) + protected virtual string GetConnectionString() { - var sqlCommand = QueryBuilder.DeleteBatchMessages(persistenceId, toSequenceNr, isPermanent); - CompleteCommand(sqlCommand); + var connectionString = Settings.ConnectionString; + if (string.IsNullOrEmpty(connectionString)) + { + connectionString = ConfigurationManager.ConnectionStrings[Settings.ConnectionStringName].ConnectionString; + } - await sqlCommand.ExecuteNonQueryAsync(); + return connectionString; } - private void CompleteCommand(DbCommand sqlCommand) + private void CompleteCommand(DbCommand sqlCommand, DbConnection connection) { - sqlCommand.Connection = _dbConnection; + sqlCommand.Connection = connection; sqlCommand.CommandTimeout = (int)Settings.ConnectionTimeout.TotalMilliseconds; } - private CancellationTokenSource GetCancellationTokenSource() - { - var source = new CancellationTokenSource(); - PendingOperations.AddLast(source); - return source; - } - private JournalEntry ToJournalEntry(IPersistentRepresentation message) { var payloadType = message.Payload.GetType(); - var serializer = _serialization.FindSerializerForType(payloadType); + var serializer = _system.Serialization.FindSerializerForType(payloadType); + var manifest = string.IsNullOrEmpty(message.Manifest) ? payloadType.QualifiedTypeName() : message.Manifest; + var timestamp = TimestampProvider.GenerateTimestamp(message); + var payload = serializer.ToBinary(message.Payload); - return new JournalEntry(message.PersistenceId, message.SequenceNr, message.IsDeleted, - payloadType.QualifiedTypeName(), serializer.ToBinary(message.Payload)); + return new JournalEntry(message.PersistenceId, message.SequenceNr, message.IsDeleted, manifest, timestamp, payload); } - private void InsertInTransaction(DbCommand sqlCommand, IEnumerable journalEntries) + private ITimestampProvider CreateTimestampProvider() { - using (var tx = _dbConnection.BeginTransaction()) - { - sqlCommand.Transaction = tx; - try - { - foreach (var entry in journalEntries) - { - CopyParamsToCommand(sqlCommand, entry); - - if (sqlCommand.ExecuteNonQuery() != 1) - { - //TODO: something went wrong, ExecuteNonQuery() should return 1 (number of rows added) - } - } - - tx.Commit(); - } - catch (Exception) - { - tx.Rollback(); - throw; - } - } + var type = Type.GetType(Settings.TimestampProvider, true); + var instance = Activator.CreateInstance(type); + return (ITimestampProvider) instance; } private async Task InsertInTransactionAsync(DbCommand sqlCommand, IEnumerable journalEntries) { - using (var tx = _dbConnection.BeginTransaction()) + using (var tx = sqlCommand.Connection.BeginTransaction()) { sqlCommand.Transaction = tx; try @@ -322,10 +293,10 @@ private async Task InsertInTransactionAsync(DbCommand sqlCommand, IEnumerable //----------------------------------------------------------------------- +using System.Collections.Generic; using System.Data.Common; +using Akka.Persistence.Sql.Common.Queries; namespace Akka.Persistence.Sql.Common.Journal { @@ -14,6 +16,11 @@ namespace Akka.Persistence.Sql.Common.Journal /// public interface IJournalQueryBuilder { + /// + /// Returns query which should return events filtered accordingly to provided set of . + /// + DbCommand SelectEvents(IEnumerable hints); + /// /// Returns query which should return a frame of messages filtered accordingly to provided parameters. /// diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/QueryMapper.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/QueryMapper.cs index b713e7cdea9..9f0b8b59599 100644 --- a/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/QueryMapper.cs +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/QueryMapper.cs @@ -29,6 +29,12 @@ public interface IJournalQueryMapper /// internal class DefaultJournalQueryMapper : IJournalQueryMapper { + public const int PersistenceIdIndex = 0; + public const int SequenceNrIndex = 1; + public const int IsDeletedIndex = 2; + public const int ManifestIndex = 3; + public const int PayloadIndex = 4; + private readonly Akka.Serialization.Serialization _serialization; public DefaultJournalQueryMapper(Akka.Serialization.Serialization serialization) @@ -38,19 +44,21 @@ public DefaultJournalQueryMapper(Akka.Serialization.Serialization serialization) public IPersistentRepresentation Map(DbDataReader reader, IActorRef sender = null) { - var persistenceId = reader.GetString(0); - var sequenceNr = reader.GetInt64(1); - var isDeleted = reader.GetBoolean(2); - var payload = GetPayload(reader); + var persistenceId = reader.GetString(PersistenceIdIndex); + var sequenceNr = reader.GetInt64(SequenceNrIndex); + var isDeleted = reader.GetBoolean(IsDeletedIndex); + var manifest = reader.GetString(ManifestIndex); + + // timestamp is SQL-journal specific field, it's not a part of casual Persistent instance + var payload = GetPayload(reader, manifest); - return new Persistent(payload, sequenceNr, persistenceId, isDeleted, sender); + return new Persistent(payload, sequenceNr, manifest, persistenceId, isDeleted, sender); } - private object GetPayload(DbDataReader reader) + private object GetPayload(DbDataReader reader, string manifest) { - var payloadType = reader.GetString(3); - var type = Type.GetType(payloadType, true); - var binary = (byte[]) reader[4]; + var type = Type.GetType(manifest, true); + var binary = (byte[]) reader[PayloadIndex]; var serializer = _serialization.FindSerializerForType(type); return serializer.FromBinary(binary, type); diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/SqlJournal.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/SqlJournal.cs new file mode 100644 index 00000000000..717043ba8fa --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/SqlJournal.cs @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Persistence.Journal; +using Akka.Persistence.Sql.Common.Queries; + +namespace Akka.Persistence.Sql.Common.Journal +{ + public abstract class SqlJournal : AsyncWriteJournal + { + protected readonly JournalDbEngine DbEngine; + + protected SqlJournal(JournalDbEngine dbEngine) + { + if (dbEngine == null) + throw new ArgumentNullException("dbEngine", "Database engine provided to sql journal cannot be null"); + + DbEngine = dbEngine; + } + + protected override void PostStop() + { + base.PostStop(); + DbEngine.Close(); + } + + protected override bool Receive(object message) + { + var wasHandled = base.Receive(message); + if (!wasHandled && message is Query) + { + HandleEventQuery(message as Query); + return true; + } + + return false; + } + + private void HandleEventQuery(Query query) + { + var queryId = query.QueryId; + var sender = Context.Sender; + DbEngine.ReadEvents(queryId, query.Hints, Context.Sender, reply => + { + foreach (var adapted in AdaptFromJournal(reply)) + { + sender.Tell(new QueryResponse(queryId, adapted)); + } + }) + .ContinueWith(task => + task.IsFaulted || task.IsCanceled ? (IQueryReply)new QueryFailure(queryId, task.Exception) : new QuerySuccess(queryId), + TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.AttachedToParent) + .PipeTo(Context.Sender); + } + + public override Task ReplayMessagesAsync(string persistenceId, long fromSequenceNr, long toSequenceNr, long max, Action replayCallback) + { + return DbEngine.ReplayMessagesAsync(persistenceId, fromSequenceNr, toSequenceNr, max, Context.Sender, replayCallback); + } + + public override Task ReadHighestSequenceNrAsync(string persistenceId, long fromSequenceNr) + { + return DbEngine.ReadHighestSequenceNrAsync(persistenceId, fromSequenceNr); + } + + protected override Task WriteMessagesAsync(IEnumerable messages) + { + return DbEngine.WriteMessagesAsync(messages); + } + + protected override Task DeleteMessagesToAsync(string persistenceId, long toSequenceNr, bool isPermanent) + { + return DbEngine.DeleteMessagesToAsync(persistenceId, toSequenceNr, isPermanent); + } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Properties/AssemblyInfo.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common/Properties/AssemblyInfo.cs index e4be5070d3a..2a17173352c 100644 --- a/src/contrib/persistence/Akka.Persistence.Sql.Common/Properties/AssemblyInfo.cs +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Properties/AssemblyInfo.cs @@ -1,4 +1,11 @@ -using System.Reflection; +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Queries/Hints.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common/Queries/Hints.cs new file mode 100644 index 00000000000..fc689d8ab38 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Queries/Hints.cs @@ -0,0 +1,183 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Akka.Persistence.Sql.Common.Journal; + +namespace Akka.Persistence.Sql.Common.Queries +{ + public interface IHint { } + + public static class Hints + { + /// + /// Returns a hint that expects a reply with events with matching manifest. + /// + public static IHint Manifest(string manifest) + { + return new WithManifest(manifest); + } + + /// + /// Returns a hint that expects a reply with events from provided set of persistence ids. + /// + public static IHint PersistenceIds(IEnumerable persistenceIds) + { + return new PersistenceIdRange(persistenceIds); + } + + /// + /// Returns a hint that expects a reply with events, that have timestamp value before provided date. + /// + public static IHint TimestampBefore(DateTime to) + { + return new TimestampRange(null, to); + } + + /// + /// Returns a hint that expects a reply with events, that have timestamp value after or equal provided date. + /// + public static IHint TimestampAfter(DateTime from) + { + return new TimestampRange(from, null); + } + + /// + /// Returns a hint that expects a reply with events, that have timestamp from between provided range of values (left side inclusive). + /// + public static IHint TimestampBetween(DateTime from, DateTime to) + { + return new TimestampRange(from, to); + } + } + + /// + /// Hint for the SQL journal used to filter journal entries returned in the response based on the manifest. + /// + [Serializable] + public sealed class WithManifest : IHint, IEquatable + { + + public readonly string Manifest; + + public WithManifest(string manifest) + { + if (string.IsNullOrEmpty(manifest)) throw new ArgumentException("Hint expected manifest, but none has been provided", "manifest"); + Manifest = manifest; + } + + public bool Equals(WithManifest other) + { + return other != null && other.Manifest.Equals(Manifest); + } + + public override bool Equals(object obj) + { + return Equals(obj as WithManifest); + } + + public override int GetHashCode() + { + return (Manifest != null ? Manifest.GetHashCode() : 0); + } + + public override string ToString() + { + return string.Format("WithManifest", Manifest); + } + } + + /// + /// Hint for the SQL journal used to filter journal entries returned in the response based on set of perisistence ids provided. + /// + [Serializable] + public sealed class PersistenceIdRange : IHint, IEquatable + { + public readonly ISet PersistenceIds; + + public PersistenceIdRange(IEnumerable persistenceIds) + { + if (persistenceIds == null) throw new ArgumentException("Hint expected persistence ids, but none has been provided", "persistenceIds"); + PersistenceIds = new HashSet(persistenceIds); + } + + public bool Equals(PersistenceIdRange other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return other.PersistenceIds.SetEquals(PersistenceIds); + } + + public override bool Equals(object obj) + { + return Equals(obj as PersistenceIdRange); + } + + public override int GetHashCode() + { + return (PersistenceIds != null ? PersistenceIds.GetHashCode() : 0); + } + + public override string ToString() + { + return string.Format("PersistenceIdRange", string.Join(", ", PersistenceIds)); + } + } + + /// + /// Hint for the SQL journal used to filter journal entries returned in the response based on their timestamp range. + /// Desired behavior of timestamp range is <from, to) - left side inclusive, right side exclusive. + /// Timestamp is generated by method, which may be overloaded. + /// + [Serializable] + public sealed class TimestampRange : IHint, IEquatable + { + public readonly DateTime? From; + public readonly DateTime? To; + + public TimestampRange(DateTime? @from, DateTime? to) + { + if (!from.HasValue && !to.HasValue) + throw new ArgumentException("TimestampRange hint requires either 'From' or 'To' or both range limiters provided"); + + if (from.HasValue && to.HasValue && from > to) + throw new ArgumentException("TimestampRange hint requires 'From' date to occur before 'To' date"); + + From = @from; + To = to; + } + + public bool Equals(TimestampRange other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Equals(From, other.From) && Equals(To, other.To); + } + + public override bool Equals(object obj) + { + return Equals(obj as TimestampRange); + } + + public override int GetHashCode() + { + unchecked + { + return (From.GetHashCode() * 397) ^ To.GetHashCode(); + } + } + + public override string ToString() + { + return string.Format("TimestampRange", + From.HasValue ? From.Value.ToString() : "undefined", + To.HasValue ? To.Value.ToString() : "undefined"); + } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Queries/Query.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common/Queries/Query.cs new file mode 100644 index 00000000000..0b4fbf00896 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Queries/Query.cs @@ -0,0 +1,192 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Akka.Actor; + +namespace Akka.Persistence.Sql.Common.Queries +{ + public interface IQueryReply { } + + /// + /// Message send to particular SQL-based journal . It may be parametrized + /// using set of hints. SQL-based journal will respond with collection of + /// messages followed by when request succeed or the + /// message when request has failed for some reason. + /// + /// Since SQL journals can store events in linearized fashion, they are able to provide deterministic + /// set of events not based on any partition key. Therefore query request don't need to contain + /// partition id of the persistent actor. + /// + [Serializable] + public sealed class Query: IEquatable + { + public readonly object QueryId; + public readonly ISet Hints; + + public Query(object queryId, ISet hints) + { + if(hints == null) + throw new ArgumentNullException("hints", "Query expects set of hints passed not to be null"); + + QueryId = queryId; + Hints = hints; + } + + public Query(object queryId, params IHint[] hints) : this(queryId, new HashSet(hints)) { } + + public bool Equals(Query other) + { + if (ReferenceEquals(other, null)) return false; + if (ReferenceEquals(this, other)) return true; + + return Equals(QueryId, other.QueryId) && Hints.SetEquals(other.Hints); + } + + public override bool Equals(object obj) + { + return Equals(obj as Query); + } + + public override int GetHashCode() + { + unchecked + { + return ((QueryId != null ? QueryId.GetHashCode() : 0) * 397) ^ Hints.GetHashCode(); + } + } + + public override string ToString() + { + return string.Format("Query", QueryId, string.Join(",", Hints)); + } + } + + /// + /// Message send back from SQL-based journal to sender, + /// when the query execution has been completed and result is returned. + /// + [Serializable] + public sealed class QueryResponse : IQueryReply, IEquatable + { + public readonly object QueryId; + public readonly IPersistentRepresentation Message; + + public QueryResponse(object queryId, IPersistentRepresentation message) + { + QueryId = queryId; + Message = message; + } + + public bool Equals(QueryResponse other) + { + if (Equals(other, null)) return false; + if (ReferenceEquals(this, other)) return true; + + return Equals(QueryId, other.QueryId) && Equals(Message, other.Message); + } + + public override bool Equals(object obj) + { + return Equals(obj as QueryResponse); + } + + public override int GetHashCode() + { + unchecked + { + return ((QueryId != null ? QueryId.GetHashCode() : 0) * 397) ^ (Message != null ? Message.GetHashCode() : 0); + } + } + + public override string ToString() + { + return string.Format("QueryResponse", QueryId, Message); + } + } + + /// + /// Message send back from SQL-based journal, when has been successfully responded. + /// + [Serializable] + public sealed class QuerySuccess : IQueryReply, IEquatable + { + public readonly object QueryId; + + public QuerySuccess(object queryId) + { + QueryId = queryId; + } + + public bool Equals(QuerySuccess other) + { + if (ReferenceEquals(other, null)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(QueryId, other.QueryId); + } + + public override bool Equals(object obj) + { + return Equals(obj as QuerySuccess); + } + + public override int GetHashCode() + { + return (QueryId != null ? QueryId.GetHashCode() : 0); + } + + public override string ToString() + { + return string.Format("QuerySuccess", QueryId); + } + } + + /// + /// Message send back from SQL-based journal to sender, when the query execution has failed. + /// + [Serializable] + public sealed class QueryFailure : IQueryReply, IEquatable + { + /// + /// Identifier of the correlated . + /// + public readonly object QueryId; + public readonly Exception Reason; + + public QueryFailure(object queryId, Exception reason) + { + QueryId = queryId; + Reason = reason; + } + + public bool Equals(QueryFailure other) + { + if (ReferenceEquals(other, null)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(QueryId, other.QueryId) && Equals(Reason, other.Reason); + } + + public override bool Equals(object obj) + { + return Equals(obj as QueryFailure); + } + + public override int GetHashCode() + { + unchecked + { + return ((QueryId != null ? QueryId.GetHashCode() : 0) * 397) ^ (Reason != null ? Reason.GetHashCode() : 0); + } + } + + public override string ToString() + { + return string.Format("QueryFailure", QueryId, Reason); + } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Settings.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common/Settings.cs index 4ac9be9ba2d..993697dd2ac 100644 --- a/src/contrib/persistence/Akka.Persistence.Sql.Common/Settings.cs +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Settings.cs @@ -7,6 +7,7 @@ using System; using Akka.Configuration; +using Akka.Persistence.Sql.Common.Journal; namespace Akka.Persistence.Sql.Common { @@ -40,6 +41,11 @@ public class JournalSettings /// public string TableName { get; private set; } + /// + /// Fully qualified type name for used to generate journal timestamps. + /// + public string TimestampProvider { get; set; } + public JournalSettings(Config config) { if (config == null) throw new ArgumentNullException("config", "SqlServer journal settings cannot be initialized, because required HOCON section couldn't been found"); @@ -49,6 +55,7 @@ public JournalSettings(Config config) ConnectionTimeout = config.GetTimeSpan("connection-timeout"); SchemaName = config.GetString("schema-name"); TableName = config.GetString("table-name"); + TimestampProvider = config.GetString("timestamp-provider"); } } diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Snapshot/DbSnapshotStore.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common/Snapshot/SqlSnapshotStore.cs similarity index 53% rename from src/contrib/persistence/Akka.Persistence.Sql.Common/Snapshot/DbSnapshotStore.cs rename to src/contrib/persistence/Akka.Persistence.Sql.Common/Snapshot/SqlSnapshotStore.cs index 9a52bc6c806..4652afb58fe 100644 --- a/src/contrib/persistence/Akka.Persistence.Sql.Common/Snapshot/DbSnapshotStore.cs +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Snapshot/SqlSnapshotStore.cs @@ -1,11 +1,12 @@ //----------------------------------------------------------------------- -// +// // Copyright (C) 2009-2015 Typesafe Inc. // Copyright (C) 2013-2015 Akka.NET project // //----------------------------------------------------------------------- using System.Collections.Generic; +using System.Configuration; using System.Data.Common; using System.Threading; using System.Threading.Tasks; @@ -16,16 +17,14 @@ namespace Akka.Persistence.Sql.Common.Snapshot /// /// Abstract snapshot store implementation, customized to work with SQL-based persistence providers. /// - public abstract class DbSnapshotStore : SnapshotStore + public abstract class SqlSnapshotStore : SnapshotStore { /// /// List of cancellation tokens for all pending asynchronous database operations. /// protected readonly LinkedList PendingOperations; - - private DbConnection _connection; - - protected DbSnapshotStore() + + protected SqlSnapshotStore() { QueryMapper = new DefaultSnapshotQueryMapper(Context.System.Serialization); PendingOperations = new LinkedList(); @@ -34,17 +33,20 @@ protected DbSnapshotStore() /// /// Returns a new instance of database connection. /// - protected abstract DbConnection CreateDbConnection(); + protected abstract DbConnection CreateDbConnection(string connectionString); /// - /// Gets settings for the current snapshot store. + /// Returns a new instance of database connection. /// - protected abstract SnapshotStoreSettings Settings { get; } + public DbConnection CreateDbConnection() + { + return CreateDbConnection(GetConnectionString()); + } /// - /// Gets current database connection. + /// Gets settings for the current snapshot store. /// - public DbConnection DbConnection { get { return _connection; } } + protected abstract SnapshotStoreSettings Settings { get; } /// /// Query builder used to convert snapshot store related operations into corresponding SQL queries. @@ -55,15 +57,7 @@ protected DbSnapshotStore() /// Query mapper used to map SQL query results into snapshots. /// public ISnapshotQueryMapper QueryMapper { get; set; } - - protected override void PreStart() - { - base.PreStart(); - - _connection = CreateDbConnection(); - _connection.Open(); - } - + protected override void PostStop() { base.PostStop(); @@ -78,75 +72,96 @@ protected override void PostStop() curr.Value.Cancel(); PendingOperations.Remove(curr); } + } - _connection.Close(); + protected virtual string GetConnectionString() + { + var connectionString = Settings.ConnectionString; + return string.IsNullOrEmpty(connectionString) + ? ConfigurationManager.ConnectionStrings[Settings.ConnectionStringName].ConnectionString + : connectionString; } /// /// Asynchronously loads snapshot with the highest sequence number for a persistent actor/view matching specified criteria. /// - protected override Task LoadAsync(string persistenceId, SnapshotSelectionCriteria criteria) + protected override async Task LoadAsync(string persistenceId, SnapshotSelectionCriteria criteria) { - var sqlCommand = QueryBuilder.SelectSnapshot(persistenceId, criteria.MaxSequenceNr, criteria.MaxTimeStamp); - CompleteCommand(sqlCommand); + using (var connection = CreateDbConnection()) + { + await connection.OpenAsync(); + + var sqlCommand = QueryBuilder.SelectSnapshot(persistenceId, criteria.MaxSequenceNr, criteria.MaxTimeStamp); + CompleteCommand(sqlCommand, connection); - var tokenSource = GetCancellationTokenSource(); - return sqlCommand - .ExecuteReaderAsync(tokenSource.Token) - .ContinueWith(task => + var tokenSource = GetCancellationTokenSource(); + var reader = await sqlCommand.ExecuteReaderAsync(tokenSource.Token); + try { - var reader = task.Result; - try - { - return reader.Read() ? QueryMapper.Map(reader) : null; - } - finally - { - PendingOperations.Remove(tokenSource); - reader.Close(); - } - }, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.AttachedToParent); + return reader.Read() ? QueryMapper.Map(reader) : null; + } + finally + { + PendingOperations.Remove(tokenSource); + reader.Close(); + } + } } /// /// Asynchronously stores a snapshot with metadata as record in SQL table. /// - protected override Task SaveAsync(SnapshotMetadata metadata, object snapshot) + protected override async Task SaveAsync(SnapshotMetadata metadata, object snapshot) { - var entry = ToSnapshotEntry(metadata, snapshot); - var sqlCommand = QueryBuilder.InsertSnapshot(entry); - CompleteCommand(sqlCommand); + using (var connection = CreateDbConnection()) + { + await connection.OpenAsync(); - var tokenSource = GetCancellationTokenSource(); + var entry = ToSnapshotEntry(metadata, snapshot); + var sqlCommand = QueryBuilder.InsertSnapshot(entry); + CompleteCommand(sqlCommand, connection); - return sqlCommand.ExecuteNonQueryAsync(tokenSource.Token) - .ContinueWith(task => + var tokenSource = GetCancellationTokenSource(); + try + { + await sqlCommand.ExecuteNonQueryAsync(tokenSource.Token); + } + finally { PendingOperations.Remove(tokenSource); - }, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.AttachedToParent); + } + } } protected override void Saved(SnapshotMetadata metadata) { } - protected override void Delete(SnapshotMetadata metadata) + protected override async Task DeleteAsync(SnapshotMetadata metadata) { - var sqlCommand = QueryBuilder.DeleteOne(metadata.PersistenceId, metadata.SequenceNr, metadata.Timestamp); - CompleteCommand(sqlCommand); + using (var connection = CreateDbConnection()) + { + await connection.OpenAsync(); + var sqlCommand = QueryBuilder.DeleteOne(metadata.PersistenceId, metadata.SequenceNr, metadata.Timestamp); + CompleteCommand(sqlCommand, connection); - sqlCommand.ExecuteNonQuery(); + await sqlCommand.ExecuteNonQueryAsync(); + } } - protected override void Delete(string persistenceId, SnapshotSelectionCriteria criteria) + protected override async Task DeleteAsync(string persistenceId, SnapshotSelectionCriteria criteria) { - var sqlCommand = QueryBuilder.DeleteMany(persistenceId, criteria.MaxSequenceNr, criteria.MaxTimeStamp); - CompleteCommand(sqlCommand); + using (var connection = CreateDbConnection()) + { + await connection.OpenAsync(); + var sqlCommand = QueryBuilder.DeleteMany(persistenceId, criteria.MaxSequenceNr, criteria.MaxTimeStamp); + CompleteCommand(sqlCommand, connection); - sqlCommand.ExecuteNonQuery(); + await sqlCommand.ExecuteNonQueryAsync(); + } } - private void CompleteCommand(DbCommand command) + private void CompleteCommand(DbCommand command, DbConnection connection) { - command.Connection = _connection; + command.Connection = connection; command.CommandTimeout = (int)Settings.ConnectionTimeout.TotalMilliseconds; } diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Akka.Persistence.Sqlite.Tests.csproj b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Akka.Persistence.Sqlite.Tests.csproj new file mode 100644 index 00000000000..2b9d576cba9 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Akka.Persistence.Sqlite.Tests.csproj @@ -0,0 +1,120 @@ + + + + + + Debug + AnyCPU + {7A832BBF-053E-4E9F-BD83-D988A0130CC8} + Library + Properties + Akka.Persistence.Sqlite.Tests + Akka.Persistence.Sqlite.Tests + v4.5 + 512 + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + ..\..\..\packages\System.Data.SQLite.Core.1.0.98.1\lib\net45\System.Data.SQLite.dll + True + + + + + + + + + ..\..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True + + + ..\..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True + + + ..\..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True + + + + + + + + + + + {ad9418b6-c452-4169-94fb-d43de0bfa966} + Akka.Persistence.TestKit + + + {fca84dea-c118-424b-9eb8-34375dfef18a} + Akka.Persistence + + + {0d3cbad0-bbdb-43e5-afc4-ed1d3ecdc224} + Akka.TestKit + + + {5deddf90-37f0-48d3-a0b0-a5cbd8a7e377} + Akka + + + {7dbd5c17-5e9d-40c4-9201-d092751532a7} + Akka.TestKit.Xunit2 + + + {e7bc4f35-b9fb-49cb-b12c-cd00f6a08ea2} + Akka.Persistence.Sql.Common.TestKit + + + {3b9e6211-9488-4db5-b714-24248693b38f} + Akka.Persistence.Sql.Common + + + {453efd22-7c53-4887-9dbf-fcfc9172e909} + Akka.Persistence.Sqlite + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Properties/AssemblyInfo.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..a1ea9b52d10 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.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("Akka.Persistence.Sqlite.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Akka.Persistence.Sqlite.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[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("7a832bbf-053e-4e9f-bd83-d988a0130cc8")] + +// 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/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/SqliteJournalQuerySpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/SqliteJournalQuerySpec.cs new file mode 100644 index 00000000000..dee6ead9ced --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/SqliteJournalQuerySpec.cs @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using Akka.Configuration; +using Akka.Persistence.Sql.Common.TestKit; +using Akka.Util.Internal; +using Xunit.Abstractions; + +namespace Akka.Persistence.Sqlite.Tests +{ + public class SqliteJournalQuerySpec : SqlJournalQuerySpec + { + private static AtomicCounter counter = new AtomicCounter(0); + + public SqliteJournalQuerySpec(ITestOutputHelper output) + : base(CreateSpecConfig("FullUri=file:memdb-journal-query-" + counter.IncrementAndGet() + ".db?mode=memory&cache=shared;"), "SqliteJournalQuerySpec", output: output) + { + Initialize(); + } + + private static Config CreateSpecConfig(string connectionString) + { + return ConfigurationFactory.ParseString(@" + akka.persistence { + publish-plugin-commands = on + journal { + plugin = ""akka.persistence.journal.sqlite"" + sqlite { + class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite"" + plugin-dispatcher = ""akka.actor.default-dispatcher"" + table-name = event_journal + auto-initialize = on + connection-string = """ + connectionString + @""" + } + } + }" + TimestampConfig("akka.persistence.journal.sqlite")); + } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/SqliteJournalSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/SqliteJournalSpec.cs new file mode 100644 index 00000000000..d013853bb37 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/SqliteJournalSpec.cs @@ -0,0 +1,45 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using Akka.Configuration; +using Akka.Persistence.TestKit.Journal; +using Akka.Util.Internal; +using Xunit.Abstractions; + +namespace Akka.Persistence.Sqlite.Tests +{ + public class SqliteJournalSpec : JournalSpec + { + private static AtomicCounter counter = new AtomicCounter(0); + + public SqliteJournalSpec(ITestOutputHelper output) + : base(CreateSpecConfig("FullUri=file:memdb-journal-" + counter.IncrementAndGet() + ".db?mode=memory&cache=shared;"), "SqliteJournalSpec", output) + { + SqlitePersistence.Get(Sys); + + Initialize(); + } + + private static Config CreateSpecConfig(string connectionString) + { + return ConfigurationFactory.ParseString(@" + akka.persistence { + publish-plugin-commands = on + journal { + plugin = ""akka.persistence.journal.sqlite"" + sqlite { + class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite"" + plugin-dispatcher = ""akka.actor.default-dispatcher"" + table-name = event_journal + auto-initialize = on + connection-string = """ + connectionString + @""" + } + } + }"); + } + } +} diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/SqliteSnapshotStoreSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/SqliteSnapshotStoreSpec.cs new file mode 100644 index 00000000000..0ae89f72389 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/SqliteSnapshotStoreSpec.cs @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using Akka.Configuration; +using Akka.Persistence.TestKit.Snapshot; +using Akka.Util.Internal; +using Xunit.Abstractions; + +namespace Akka.Persistence.Sqlite.Tests +{ + public class SqliteSnapshotStoreSpec : SnapshotStoreSpec + { + private static AtomicCounter counter = new AtomicCounter(0); + public SqliteSnapshotStoreSpec(ITestOutputHelper output) + : base(CreateSpecConfig("FullUri=file:memdb-snapshot-" + counter.IncrementAndGet() + ".db?mode=memory&cache=shared;"), "SqliteSnapshotStoreSpec", output) + { + SqlitePersistence.Get(Sys); + + Initialize(); + } + + private static Config CreateSpecConfig(string connectionString) + { + return ConfigurationFactory.ParseString(@" + akka.persistence { + publish-plugin-commands = on + snapshot-store { + plugin = ""akka.persistence.snapshot-store.sqlite"" + sqlite { + class = ""Akka.Persistence.Sqlite.Snapshot.SqliteSnapshotStore, Akka.Persistence.Sqlite"" + plugin-dispatcher = ""akka.actor.default-dispatcher"" + table-name = snapshot_store + auto-initialize = on + connection-string = """ + connectionString + @""" + } + } + }"); + } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/packages.config b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/packages.config new file mode 100644 index 00000000000..39b7f0fdb27 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/Akka.Persistence.Sqlite.csproj b/src/contrib/persistence/Akka.Persistence.Sqlite/Akka.Persistence.Sqlite.csproj new file mode 100644 index 00000000000..add2bd97214 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/Akka.Persistence.Sqlite.csproj @@ -0,0 +1,94 @@ + + + + + Debug + AnyCPU + {453EFD22-7C53-4887-9DBF-FCFC9172E909} + Library + Properties + Akka.Persistence.Sqlite + Akka.Persistence.Sqlite + v4.5 + 512 + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + ..\..\..\packages\System.Data.SQLite.Core.1.0.98.1\lib\net45\System.Data.SQLite.dll + True + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + {fca84dea-c118-424b-9eb8-34375dfef18a} + Akka.Persistence + + + {5deddf90-37f0-48d3-a0b0-a5cbd8a7e377} + Akka + + + {3b9e6211-9488-4db5-b714-24248693b38f} + Akka.Persistence.Sql.Common + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/Akka.Persistence.Sqlite.nuspec b/src/contrib/persistence/Akka.Persistence.Sqlite/Akka.Persistence.Sqlite.nuspec new file mode 100644 index 00000000000..0dbcafe25ce --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/Akka.Persistence.Sqlite.nuspec @@ -0,0 +1,20 @@ + + + + @project@ + @project@@title@ + @build.number@ + @authors@ + @authors@ + Akka.NET Persistence journal and snapshot store backed by SQLite. + https://github.com/akkadotnet/akka.net/blob/master/LICENSE + https://github.com/akkadotnet/akka.net + http://getakka.net/images/AkkaNetLogo.Normal.png + false + @releaseNotes@ + @copyright@ + @tags@ persistence eventsource sqlserver + @dependencies@ + @references@ + + diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/ConnectionContext.cs b/src/contrib/persistence/Akka.Persistence.Sqlite/ConnectionContext.cs new file mode 100644 index 00000000000..1a3686d51da --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/ConnectionContext.cs @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Data; +using System.Data.SQLite; + +namespace Akka.Persistence.Sqlite +{ + /// + /// This class has been made to make memory connections safe. In SQLite shared memory database exists as long, as there exists at least one opened connection to it. + /// + internal static class ConnectionContext + { + private static readonly ConcurrentDictionary Remembered = new ConcurrentDictionary(); + + public static SQLiteConnection Remember(string connectionString) + { + if (string.IsNullOrEmpty(connectionString)) throw new ArgumentNullException("connectionString", "No connection string with connection to remember"); + + var conn = Remembered.GetOrAdd(connectionString, s => new SQLiteConnection(connectionString)); + + if (conn.State != ConnectionState.Open) + conn.Open(); + + return conn; + } + + public static void Forget(string connectionString) + { + SQLiteConnection conn; + if (Remembered.TryRemove(connectionString, out conn)) + { + conn.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/DbHelper.cs b/src/contrib/persistence/Akka.Persistence.Sqlite/DbHelper.cs new file mode 100644 index 00000000000..e38d7f5fd66 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/DbHelper.cs @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Data.SQLite; + +namespace Akka.Persistence.Sqlite +{ + internal static class DbHelper + { + private const string JournalFormat = @" + CREATE TABLE IF NOT EXISTS {0} ( + persistence_id VARCHAR(255) NOT NULL, + sequence_nr INTEGER(8) NOT NULL, + is_deleted INTEGER(1) NOT NULL, + manifest VARCHAR(255) NOT NULL, + timestamp INTEGER NOT NULL, + payload BLOB NOT NULL, + PRIMARY KEY (persistence_id, sequence_nr) + );"; + + private const string SnapshotStoreFormat = @" + CREATE TABLE IF NOT EXISTS {0} ( + persistence_id VARCHAR(255) NOT NULL, + sequence_nr INTEGER(8) NOT NULL, + created_at INTEGER(8) NOT NULL, + manifest VARCHAR(255) NOT NULL, + snapshot BLOB NOT NULL, + PRIMARY KEY (persistence_id, sequence_nr) + );"; + + public static void CreateJournalTable(string connectionString, string tableName) + { + if (string.IsNullOrEmpty(connectionString)) throw new ArgumentNullException("connectionString", "SqlitePersistence requires connection string to be provided"); + if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException("tableName", "SqlitePersistence requires journal table name to be provided"); + + using (var connection = new SQLiteConnection(connectionString)) + using (var command = new SQLiteCommand(string.Format(JournalFormat, tableName), connection)) + { + connection.Open(); + command.ExecuteNonQuery(); + } + } + + public static void CreateSnapshotStoreTable(string connectionString, string tableName) + { + if (string.IsNullOrEmpty(connectionString)) throw new ArgumentNullException("connectionString", "SqlitePersistence requires connection string to be provided"); + if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException("tableName", "SqlitePersistence requires snapshot store table name to be provided"); + + using (var connection = new SQLiteConnection(connectionString)) + using (var command = new SQLiteCommand(string.Format(SnapshotStoreFormat, tableName), connection)) + { + connection.Open(); + command.ExecuteNonQuery(); + } + } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/Extension.cs b/src/contrib/persistence/Akka.Persistence.Sqlite/Extension.cs new file mode 100644 index 00000000000..d39b34cd739 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/Extension.cs @@ -0,0 +1,106 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using Akka.Actor; +using Akka.Configuration; +using Akka.Persistence.Sql.Common; + +namespace Akka.Persistence.Sqlite +{ + public class SqliteJournalSettings : JournalSettings + { + public const string ConfigPath = "akka.persistence.journal.sqlite"; + + /// + /// Flag determining in in case of event journal table missing, it should be automatically initialized. + /// + public bool AutoInitialize { get; private set; } + + public SqliteJournalSettings(Config config) : base(config) + { + AutoInitialize = config.GetBoolean("auto-initialize"); + } + } + + public class SqliteSnapshotSettings : SnapshotStoreSettings + { + public const string ConfigPath = "akka.persistence.snapshot-store.sqlite"; + + /// + /// Flag determining in in case of snapshot store table missing, it should be automatically initialized. + /// + public bool AutoInitialize { get; private set; } + + public SqliteSnapshotSettings(Config config) : base(config) + { + AutoInitialize = config.GetBoolean("auto-initialize"); + } + } + + public class SqlitePersistence : IExtension + { + /// + /// Returns a default configuration for akka persistence SQLite-based journals and snapshot stores. + /// + /// + public static Config DefaultConfiguration() + { + return ConfigurationFactory.FromResource("Akka.Persistence.Sqlite.sqlite.conf"); + } + + public static SqlitePersistence Get(ActorSystem system) + { + return system.WithExtension(); + } + + /// + /// Journal-related settings loaded from HOCON configuration. + /// + public readonly SqliteJournalSettings JournalSettings; + + /// + /// Snapshot store related settings loaded from HOCON configuration. + /// + public readonly SqliteSnapshotSettings SnapshotSettings; + + public SqlitePersistence(ExtendedActorSystem system) + { + system.Settings.InjectTopLevelFallback(DefaultConfiguration()); + + JournalSettings = new SqliteJournalSettings(system.Settings.Config.GetConfig(SqliteJournalSettings.ConfigPath)); + SnapshotSettings = new SqliteSnapshotSettings(system.Settings.Config.GetConfig(SqliteSnapshotSettings.ConfigPath)); + + if (!string.IsNullOrEmpty(JournalSettings.ConnectionString)) + { + ConnectionContext.Remember(JournalSettings.ConnectionString); + system.TerminationTask.ContinueWith(t => ConnectionContext.Forget(JournalSettings.ConnectionString)); + + if (JournalSettings.AutoInitialize) + DbHelper.CreateJournalTable(JournalSettings.ConnectionString, JournalSettings.TableName); + } + + if (!string.IsNullOrEmpty(SnapshotSettings.ConnectionString)) + { + ConnectionContext.Remember(SnapshotSettings.ConnectionString); + system.TerminationTask.ContinueWith(t => ConnectionContext.Forget(SnapshotSettings.ConnectionString)); + + if (SnapshotSettings.AutoInitialize) + { + DbHelper.CreateSnapshotStoreTable(SnapshotSettings.ConnectionString, SnapshotSettings.TableName); + } + } + } + } + + public class SqlitePersistenceProvder : ExtensionIdProvider + { + public override SqlitePersistence CreateExtension(ExtendedActorSystem system) + { + return new SqlitePersistence(system); + } + } +} diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/Journal/SqliteJournal.cs b/src/contrib/persistence/Akka.Persistence.Sqlite/Journal/SqliteJournal.cs new file mode 100644 index 00000000000..9a57055fa5a --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/Journal/SqliteJournal.cs @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System.Data.Common; +using System.Data.SQLite; +using Akka.Actor; +using Akka.Persistence.Sql.Common.Journal; + +namespace Akka.Persistence.Sqlite.Journal +{ + public class SqliteJournalEngine : JournalDbEngine + { + public SqliteJournalEngine(ActorSystem system) + : base(system) + { + QueryBuilder = new SqliteQueryBuilder(Settings.TableName); + } + + protected override string JournalConfigPath { get { return SqliteJournalSettings.ConfigPath; } } + + protected override DbConnection CreateDbConnection(string connectionString) + { + return new SQLiteConnection(connectionString); + } + + protected override void CopyParamsToCommand(DbCommand sqlCommand, JournalEntry entry) + { + sqlCommand.Parameters["@PersistenceId"].Value = entry.PersistenceId; + sqlCommand.Parameters["@SequenceNr"].Value = entry.SequenceNr; + sqlCommand.Parameters["@IsDeleted"].Value = entry.IsDeleted; + sqlCommand.Parameters["@Manifest"].Value = entry.Manifest; + sqlCommand.Parameters["@Timestamp"].Value = entry.Timestamp; + sqlCommand.Parameters["@Payload"].Value = entry.Payload; + } + } + + public class SqliteJournal : SqlJournal + { + public readonly SqlitePersistence Extension = SqlitePersistence.Get(Context.System); + public SqliteJournal() : base(new SqliteJournalEngine(Context.System)) + { + } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/Journal/SqliteQueryBuilder.cs b/src/contrib/persistence/Akka.Persistence.Sqlite/Journal/SqliteQueryBuilder.cs new file mode 100644 index 00000000000..d211b021057 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/Journal/SqliteQueryBuilder.cs @@ -0,0 +1,179 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Data.SQLite; +using System.Linq; +using System.Text; +using Akka.Persistence.Sql.Common.Journal; +using Akka.Persistence.Sql.Common.Queries; + +namespace Akka.Persistence.Sqlite.Journal +{ + internal class SqliteQueryBuilder : IJournalQueryBuilder + { + private readonly string _tableName; + + private readonly string _selectHighestSequenceNrSql; + private readonly string _insertMessagesSql; + + public SqliteQueryBuilder(string tableName) + { + _tableName = tableName; + _insertMessagesSql = string.Format( + "INSERT INTO {0} (persistence_id, sequence_nr, is_deleted, manifest, timestamp, payload) VALUES (@PersistenceId, @SequenceNr, @IsDeleted, @Manifest, @Timestamp, @Payload)", _tableName); + _selectHighestSequenceNrSql = string.Format(@"SELECT MAX(sequence_nr) FROM {0} WHERE persistence_id = ? ", _tableName); + } + + public DbCommand SelectEvents(IEnumerable hints) + { + var sqlCommand = new SQLiteCommand(); + + var sqlized = hints + .Select(h => HintToSql(h, sqlCommand)) + .Where(x => !string.IsNullOrEmpty(x)); + + var where = string.Join(" AND ", sqlized); + var sql = new StringBuilder("SELECT persistence_id, sequence_nr, is_deleted, manifest, payload FROM " + _tableName); + if (!string.IsNullOrEmpty(where)) + { + sql.Append(" WHERE ").Append(where); + } + + sqlCommand.CommandText = sql.ToString(); + return sqlCommand; + } + + private string HintToSql(IHint hint, SQLiteCommand command) + { + if (hint is TimestampRange) + { + var range = (TimestampRange)hint; + var sb = new StringBuilder(); + + if (range.From.HasValue) + { + sb.Append(" timestamp >= @TimestampFrom "); + command.Parameters.AddWithValue("@TimestampFrom", range.From.Value); + } + if (range.From.HasValue && range.To.HasValue) sb.Append("AND"); + if (range.To.HasValue) + { + sb.Append(" timestamp < @TimestampTo "); + command.Parameters.AddWithValue("@TimestampTo", range.To.Value); + } + + return sb.ToString(); + } + if (hint is PersistenceIdRange) + { + var range = (PersistenceIdRange)hint; + var sb = new StringBuilder(" persistence_id IN ("); + var i = 0; + foreach (var persistenceId in range.PersistenceIds) + { + var paramName = "@Pid" + (i++); + sb.Append(paramName).Append(','); + command.Parameters.AddWithValue(paramName, persistenceId); + } + return range.PersistenceIds.Count == 0 + ? string.Empty + : sb.Remove(sb.Length - 1, 1).Append(')').ToString(); + } + else if (hint is WithManifest) + { + var manifest = (WithManifest)hint; + command.Parameters.AddWithValue("@Manifest", manifest.Manifest); + return " manifest = @Manifest"; + } + else throw new NotSupportedException(string.Format("Sqlite journal doesn't support query with hint [{0}]", hint.GetType())); + } + + public DbCommand SelectMessages(string persistenceId, long fromSequenceNr, long toSequenceNr, long max) + { + var sb = new StringBuilder(@" + SELECT persistence_id, sequence_nr, is_deleted, manifest, payload FROM ").Append(_tableName) + .Append(" WHERE persistence_id = ? "); + + if (fromSequenceNr > 0) + { + if (toSequenceNr != long.MaxValue) + sb.Append(" AND sequence_nr BETWEEN ") + .Append(fromSequenceNr) + .Append(" AND ") + .Append(toSequenceNr); + else + sb.Append(" AND sequence_nr >= ").Append(fromSequenceNr); + } + + if (toSequenceNr != long.MaxValue) + sb.Append(" AND sequence_nr <= ").Append(toSequenceNr); + + if (max != long.MaxValue) + { + sb.Append(" LIMIT ").Append(max); + } + + var command = new SQLiteCommand(sb.ToString()) + { + Parameters = { new SQLiteParameter { Value = persistenceId } } + }; + + return command; + } + + public DbCommand SelectHighestSequenceNr(string persistenceId) + { + return new SQLiteCommand(_selectHighestSequenceNrSql) + { + Parameters = { new SQLiteParameter { Value = persistenceId } } + }; + } + + public DbCommand InsertBatchMessages(IPersistentRepresentation[] messages) + { + var command = new SQLiteCommand(_insertMessagesSql); + command.Parameters.Add("@PersistenceId", DbType.String); + command.Parameters.Add("@SequenceNr", DbType.Int64); + command.Parameters.Add("@IsDeleted", DbType.Boolean); + command.Parameters.Add("@Manifest", DbType.String); + command.Parameters.Add("@Timestamp", DbType.DateTime); + command.Parameters.Add("@Payload", DbType.Binary); + + return command; + } + + public DbCommand DeleteBatchMessages(string persistenceId, long toSequenceNr, bool permanent) + { + var sb = new StringBuilder(); + + if (permanent) + { + sb.Append("DELETE FROM ").Append(_tableName); + } + else + { + sb.AppendFormat("UPDATE {0} SET is_deleted = 1", _tableName); + } + + sb.Append(" WHERE persistence_id = ?"); + + if (toSequenceNr != long.MaxValue) + { + sb.Append(" AND sequence_nr <= ").Append(toSequenceNr); + } + + return new SQLiteCommand(sb.ToString()) + { + Parameters = { new SQLiteParameter { Value = persistenceId } } + }; + } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/Properties/AssemblyInfo.cs b/src/contrib/persistence/Akka.Persistence.Sqlite/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..54e9a7dba57 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/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("Akka.Persistence.Sqlite")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Akka.Persistence.Sqlite")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[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("453efd22-7c53-4887-9dbf-fcfc9172e909")] + +// 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/src/contrib/persistence/Akka.Persistence.Sqlite/README.md b/src/contrib/persistence/Akka.Persistence.Sqlite/README.md new file mode 100644 index 00000000000..974908dfed7 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/README.md @@ -0,0 +1,73 @@ +## Akka.Persistence.Sqlite + +Akka Persistence journal and snapshot store backed by SQLite database. + +**WARNING: Akka.Persistence.Sqlite plugin is still in beta and it's mechanics described bellow may be still subject to change**. + +### Setup + +To activate the journal plugin, add the following lines to actor system configuration file: + +``` +akka.persistence.journal.plugin = "akka.persistence.journal.sqlite" +akka.persistence.journal.sqlite.connection-string = "" +``` + +Similar configuration may be used to setup a SQLite snapshot store: + +``` +akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.sqlite" +akka.persistence.snapshot-store.sqlite.connection-string = "" +``` + +Remember that connection string must be provided separately to Journal and Snapshot Store. To finish setup simply initialize plugin using: `SqlitePersistence.Get(actorSystem);` + +### Configuration + +Both journal and snapshot store share the same configuration keys (however they resides in separate scopes, so they are definied distinctly for either journal or snapshot store): + +- `class` (string with fully qualified type name) - determines class to be used as a persistent journal. Default: *Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite* (for journal) and *Akka.Persistence.Sqlite.Snapshot.SqliteSnapshotStore, Akka.Persistence.Sqlite* (for snapshot store). +- `plugin-dispatcher` (string with configuration path) - describes a message dispatcher for persistent journal. Default: *akka.actor.default-dispatcher* +- `connection-string` - connection string used to access SQLite database. Default: *none*. +- `connection-timeout` - timespan determining default connection timeouts on database-related operations. Default: *30s* +- `table-name` - name of the table used by either journal or snapshot store. Default: *event_journal* (for journal) or *snapshot_store* (for snapshot store) +- `auto-initialize` - flag determining if journal or snapshot store related tables should by automatically created when they have not been found in connected database. Default: *false* + +In addition, journal configuration specifies additional field: + +- `timestamp-provider` - fully qualified type name (with assembly) of the class responsible for generating timestamp values based on persisted message type. By default this points to *Akka.Persistence.Sql.Common.Journal.DefaultTimestampProvider, Akka.Persistence.Sql.Common*, which returns current UTC DateTime value. + +### In-memory databases + +Akka.Persistence.Sqlite plugin allows to use in-memory databases, however requires to use them in shared mode in order to work correctly. Example connection strings for such configurations are described below: + +- `FullUri=file::memory:?cache=shared;` for anonymous in-memory database instances. +- `FullUri=file:.db?mode=memory&cache=shared;` for named in-memory database instances. This way you can provide many separate databases residing in memory. + +### Custom SQL data queries + +SQLite persistence plugin defines a default table schema used for both journal and snapshot store. + +**EventJournal table**: + + +----------------+-------------+------------+----------------+------------+---------+ + | persistence_id | sequence_nr | is_deleted | manifest | timestamp | payload | + +----------------+-------------+------------+----------------+------------+---------+ + | varchar(255) | integer(8) | integer(1) | varchar(255) | integer(8) | blob | + +----------------+-------------+------------+----------------+------------+---------+ + +**SnapshotStore table**: + + +----------------+-------------+------------+----------------+----------+ + | persistence_id | sequence_nr | created_at | manifest | snapshot | + +----------------+-------------+------------+----------------+----------+ + | varchar(255) | integer(8) | integer(8) | varchar(255) | blob | + +----------------+-------------+------------+----------------+----------+ + +`created_at` column maps to `System.DateTime` value represented by it's ticks, to achieve 1 to 1 precision of dates between SQLite and .NET environment. + +Underneath Akka.Persistence.Sqlite uses a raw ADO.NET commands. You may choose not to use a dedicated built in ones, but to create your own being better fit for your use case. To do so, you have to create your own versions of `IJournalQueryBuilder` and `IJournalQueryMapper` (for custom journals) or `ISnapshotQueryBuilder` and `ISnapshotQueryMapper` (for custom snapshot store). + +### Tests + +The SQLite tests are packaged and run as part of the default "All" build task. They use dedicated shared in memory instances of SQLite database and can be executed in parallel. diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/Snapshot/QueryBuilder.cs b/src/contrib/persistence/Akka.Persistence.Sqlite/Snapshot/QueryBuilder.cs new file mode 100644 index 00000000000..a17e63d2db6 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/Snapshot/QueryBuilder.cs @@ -0,0 +1,110 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Data; +using System.Data.Common; +using System.Data.SQLite; +using System.Text; +using Akka.Persistence.Sql.Common.Snapshot; + +namespace Akka.Persistence.Sqlite.Snapshot +{ + internal class QueryBuilder : ISnapshotQueryBuilder + { + private readonly string _deleteSql; + private readonly string _insertSql; + private readonly string _selectSql; + + public QueryBuilder(SqliteSnapshotSettings settings) + { + _deleteSql = string.Format(@"DELETE FROM {0} WHERE persistence_id = ? ", settings.TableName); + _insertSql = string.Format(@"INSERT INTO {0} (persistence_id, sequence_nr, created_at, manifest, snapshot) VALUES (@PersistenceId, @SequenceNr, @Timestamp, @Manifest, @Snapshot)", settings.TableName); + _selectSql = string.Format(@"SELECT persistence_id, sequence_nr, created_at, manifest, snapshot FROM {0} WHERE persistence_id = ? ", settings.TableName); + } + + public DbCommand DeleteOne(string persistenceId, long sequenceNr, DateTime timestamp) + { + var sqlCommand = new SQLiteCommand(); + sqlCommand.Parameters.Add(new SQLiteParameter { Value = persistenceId }); + var sb = new StringBuilder(_deleteSql); + + if (sequenceNr < long.MaxValue && sequenceNr > 0) + { + sb.Append(@" AND sequence_nr = ").Append(sequenceNr); + } + + if (timestamp > DateTime.MinValue && timestamp < DateTime.MaxValue) + { + sb.AppendFormat(@" AND created_at = {0}", timestamp.Ticks); + } + + sqlCommand.CommandText = sb.ToString(); + + return sqlCommand; + } + + public DbCommand DeleteMany(string persistenceId, long maxSequenceNr, DateTime maxTimestamp) + { + var sqlCommand = new SQLiteCommand(); + sqlCommand.Parameters.Add(new SQLiteParameter { Value = persistenceId }); + var sb = new StringBuilder(_deleteSql); + + if (maxSequenceNr < long.MaxValue && maxSequenceNr > 0) + { + sb.Append(@" AND sequence_nr <= ").Append(maxSequenceNr); + } + + if (maxTimestamp > DateTime.MinValue && maxTimestamp < DateTime.MaxValue) + { + sb.AppendFormat(@" AND created_at <= {0}", maxTimestamp.Ticks); + } + + sqlCommand.CommandText = sb.ToString(); + + return sqlCommand; + } + + public DbCommand InsertSnapshot(SnapshotEntry entry) + { + var sqlCommand = new SQLiteCommand(_insertSql) + { + Parameters = + { + new SQLiteParameter("@PersistenceId", DbType.String, entry.PersistenceId.Length) { Value = entry.PersistenceId }, + new SQLiteParameter("@SequenceNr", DbType.Int64) { Value = entry.SequenceNr }, + new SQLiteParameter("@Timestamp", DbType.Int64) { Value = entry.Timestamp.Ticks }, + new SQLiteParameter("@Manifest", DbType.String, entry.SnapshotType.Length) { Value = entry.SnapshotType }, + new SQLiteParameter("@Snapshot", DbType.Binary, entry.Snapshot.Length) { Value = entry.Snapshot } + } + }; + + return sqlCommand; + } + + public DbCommand SelectSnapshot(string persistenceId, long maxSequenceNr, DateTime maxTimestamp) + { + var sqlCommand = new SQLiteCommand(); + sqlCommand.Parameters.Add(new SQLiteParameter { Value = persistenceId }); + + var sb = new StringBuilder(_selectSql); + if (maxSequenceNr > 0 && maxSequenceNr < long.MaxValue) + { + sb.Append(" AND sequence_nr <= ").Append(maxSequenceNr); + } + + if (maxTimestamp > DateTime.MinValue && maxTimestamp < DateTime.MaxValue) + { + sb.AppendFormat(" AND created_at <= {0} ", maxTimestamp.Ticks); + } + + sb.Append(" ORDER BY sequence_nr DESC"); + sqlCommand.CommandText = sb.ToString(); + return sqlCommand; + } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/Snapshot/SqliteQueryMapper.cs b/src/contrib/persistence/Akka.Persistence.Sqlite/Snapshot/SqliteQueryMapper.cs new file mode 100644 index 00000000000..db8d6f20e64 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/Snapshot/SqliteQueryMapper.cs @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Data.Common; +using Akka.Persistence.Sql.Common.Snapshot; + +namespace Akka.Persistence.Sqlite.Snapshot +{ + internal class SqliteQueryMapper : ISnapshotQueryMapper + { + private readonly Akka.Serialization.Serialization _serialization; + + public SqliteQueryMapper(Akka.Serialization.Serialization serialization) + { + _serialization = serialization; + } + + public SelectedSnapshot Map(DbDataReader reader) + { + var persistenceId = reader.GetString(0); + var sequenceNr = reader.GetInt64(1); + var timestamp = new DateTime(reader.GetInt64(2)); + + var metadata = new SnapshotMetadata(persistenceId, sequenceNr, timestamp); + var snapshot = GetSnapshot(reader); + + return new SelectedSnapshot(metadata, snapshot); + } + + private object GetSnapshot(DbDataReader reader) + { + var type = Type.GetType(reader.GetString(3), true); + var serializer = _serialization.FindSerializerForType(type); + var binary = (byte[])reader[4]; + + var obj = serializer.FromBinary(binary, type); + + return obj; + } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/Snapshot/SqliteSnapshotStore.cs b/src/contrib/persistence/Akka.Persistence.Sqlite/Snapshot/SqliteSnapshotStore.cs new file mode 100644 index 00000000000..3fe65a3b3d7 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/Snapshot/SqliteSnapshotStore.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System.Data.Common; +using System.Data.SQLite; +using Akka.Persistence.Sql.Common; +using Akka.Persistence.Sql.Common.Snapshot; + +namespace Akka.Persistence.Sqlite.Snapshot +{ + public class SqliteSnapshotStore : SqlSnapshotStore + { + private readonly SqlitePersistence _extension; + + public SqliteSnapshotStore() + { + _extension = SqlitePersistence.Get(Context.System); + QueryBuilder = new QueryBuilder(_extension.SnapshotSettings); + QueryMapper = new SqliteQueryMapper(Context.System.Serialization); + } + + + protected override DbConnection CreateDbConnection(string connectionString) + { + return new SQLiteConnection(connectionString); + } + + protected override SnapshotStoreSettings Settings { get { return _extension.SnapshotSettings; } } + } +} \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/packages.config b/src/contrib/persistence/Akka.Persistence.Sqlite/packages.config new file mode 100644 index 00000000000..038e00b11e3 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite/sqlite.conf b/src/contrib/persistence/Akka.Persistence.Sqlite/sqlite.conf new file mode 100644 index 00000000000..cce8b8c9b42 --- /dev/null +++ b/src/contrib/persistence/Akka.Persistence.Sqlite/sqlite.conf @@ -0,0 +1,67 @@ +akka.persistence{ + + journal { + + sqlite { + + # qualified type name of the SQLite persistence journal actor + class = "Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite" + + # dispatcher used to drive journal actor + plugin-dispatcher = "akka.actor.default-dispatcher" + + # connection string used for database access + connection-string = "" + + # connection string name for .config file used when no connection string has been provided + connection-string-name = "" + + # default SQLite commands timeout + connection-timeout = 30s + + # SQLite schema name to table corresponding with persistent journal + schema-name = dbo + + # SQLite table corresponding with persistent journal + table-name = event_journal + + # should corresponding journal table be initialized automatically + auto-initialize = off + + # timestamp provider used for generation of journal entries timestamps + timestamp-provider = "Akka.Persistence.Sql.Common.Journal.DefaultTimestampProvider, Akka.Persistence.Sql.Common" + + } + } + + snapshot-store { + + sqlite { + + # qualified type name of the SQLite persistence journal actor + class = "Akka.Persistence.Sqlite.Snapshot.SqliteSnapshotStore, Akka.Persistence.Sqlite" + + # dispatcher used to drive journal actor + plugin-dispatcher = "akka.actor.default-dispatcher" + + # connection string used for database access + connection-string = "" + + # connection string name for .config file used when no connection string has been provided + connection-string-name = "" + + # default SQLite commands timeout + connection-timeout = 30s + + # SQLite schema name to table corresponding with persistent journal + schema-name = dbo + + # SQLite table corresponding with persistent journal + table-name = snapshot_store + + # should corresponding journal table be initialized automatically + auto-initialize = off + + } + } +} \ No newline at end of file diff --git a/src/contrib/testkits/Akka.TestKit.NUnit.Tests/Akka.TestKit.NUnit.Tests.csproj b/src/contrib/testkits/Akka.TestKit.NUnit.Tests/Akka.TestKit.NUnit.Tests.csproj index 13931dd9a86..a5f010ce778 100644 --- a/src/contrib/testkits/Akka.TestKit.NUnit.Tests/Akka.TestKit.NUnit.Tests.csproj +++ b/src/contrib/testkits/Akka.TestKit.NUnit.Tests/Akka.TestKit.NUnit.Tests.csproj @@ -66,6 +66,9 @@ Akka.TestKit.NUnit + + + diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/Akka.TestKit.Xunit.csproj b/src/contrib/testkits/Akka.TestKit.Xunit/Akka.TestKit.Xunit.csproj index 646d9db3556..1a0b02d4d71 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit/Akka.TestKit.Xunit.csproj +++ b/src/contrib/testkits/Akka.TestKit.Xunit/Akka.TestKit.Xunit.csproj @@ -37,7 +37,7 @@ - + ..\..\..\packages\xunit.1.9.2\lib\net20\xunit.dll True diff --git a/src/contrib/testkits/Akka.TestKit.Xunit2/Akka.TestKit.Xunit2.csproj b/src/contrib/testkits/Akka.TestKit.Xunit2/Akka.TestKit.Xunit2.csproj index 3bea9ece7d3..07f3e43d953 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit2/Akka.TestKit.Xunit2.csproj +++ b/src/contrib/testkits/Akka.TestKit.Xunit2/Akka.TestKit.Xunit2.csproj @@ -14,7 +14,8 @@ 512 ..\..\..\ true - 6577902d + + true @@ -36,14 +37,17 @@ - + ..\..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True - + ..\..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True - + ..\..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True @@ -53,13 +57,11 @@ + - - - {0d3cbad0-bbdb-43e5-afc4-ed1d3ecdc224} @@ -70,6 +72,9 @@ Akka + + + diff --git a/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/Loggers.cs b/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/Loggers.cs new file mode 100644 index 00000000000..4d4bff5202e --- /dev/null +++ b/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/Loggers.cs @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using Akka.Actor; +using Akka.Event; +using Xunit.Abstractions; + +namespace Akka.TestKit.Xunit2.Internals +{ + public class TestOutputLogger : ReceiveActor + { + public TestOutputLogger(ITestOutputHelper output) + { + Receive(e => output.WriteLine(e.ToString())); + Receive(e => output.WriteLine(e.ToString())); + Receive(e => output.WriteLine(e.ToString())); + Receive(e => output.WriteLine(e.ToString())); + Receive(e => + { + e.LoggingBus.Subscribe(Self, typeof (LogEvent)); + }); + } + } +} \ No newline at end of file diff --git a/src/contrib/testkits/Akka.TestKit.Xunit2/TestKit.cs b/src/contrib/testkits/Akka.TestKit.Xunit2/TestKit.cs index 63b5e604ceb..18e6c821bf6 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit2/TestKit.cs +++ b/src/contrib/testkits/Akka.TestKit.Xunit2/TestKit.cs @@ -7,7 +7,11 @@ using System; using Akka.Actor; +using Akka.Actor.Internal; using Akka.Configuration; +using Akka.Event; +using Akka.TestKit.Xunit2.Internals; +using Xunit.Abstractions; namespace Akka.TestKit.Xunit2 { @@ -25,10 +29,10 @@ public class TestKit : TestKitBase , IDisposable /// with will be created. /// /// Optional: The actor system. - public TestKit(ActorSystem system = null) + public TestKit(ActorSystem system = null, ITestOutputHelper output = null) : base(_assertions, system) { - //Intentionally left blank + InitializeLogger(output); } /// @@ -37,10 +41,10 @@ public TestKit(ActorSystem system = null) /// /// The configuration to use for the system. /// Optional: the name of the system. Default: "test" - public TestKit(Config config, string actorSystemName=null) + public TestKit(Config config, string actorSystemName = null, ITestOutputHelper output = null) : base(_assertions, config, actorSystemName) { - //Intentionally left blank + InitializeLogger(output); } @@ -49,9 +53,9 @@ public TestKit(Config config, string actorSystemName=null) /// A new system with the specified configuration will be created. /// /// The configuration to use for the system. - public TestKit(string config): base(_assertions, ConfigurationFactory.ParseString(config)) + public TestKit(string config, ITestOutputHelper output = null) : base(_assertions, ConfigurationFactory.ParseString(config)) { - //Intentionally left blank + InitializeLogger(output); } public new static Config DefaultConfig { get { return TestKitBase.DefaultConfig; } } @@ -91,6 +95,15 @@ public void Dispose() GC.SuppressFinalize(this); } + private void InitializeLogger(ITestOutputHelper output) + { + if (output != null) + { + var system = (ExtendedActorSystem) Sys; + var logger = system.SystemActorOf(Props.Create(() => new TestOutputLogger(output)), "log-test"); + logger.Tell(new InitializeLogger(system.EventStream)); + } + } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// if set to true the method has been called directly or indirectly by a diff --git a/src/contrib/transports/Akka.Remote.AkkaIOTransport/AddressConverters.cs b/src/contrib/transports/Akka.Remote.AkkaIOTransport/AddressConverters.cs index 40af6fc016a..8c17c58e884 100644 --- a/src/contrib/transports/Akka.Remote.AkkaIOTransport/AddressConverters.cs +++ b/src/contrib/transports/Akka.Remote.AkkaIOTransport/AddressConverters.cs @@ -21,13 +21,15 @@ public static Address ToAddress(this EndPoint endpoint, ActorSystem system) return new Address(AkkaIOTransport.Protocal, system.Name, dns.Host, dns.Port); var ip = endpoint as IPEndPoint; if (ip != null) - return new Address(AkkaIOTransport.Protocal, system.Name, "127.0.0.1", ip.Port); + return new Address(AkkaIOTransport.Protocal, system.Name, ip.Address.MapToIPv4().ToString(), ip.Port); throw new ArgumentException("endpoint"); } public static EndPoint ToEndpoint(this Address address) { - return new DnsEndPoint(address.Host, address.Port.GetValueOrDefault(9099), AddressFamily.InterNetwork); + if (address == null || address.Host == null || !address.Port.HasValue) + throw new ArgumentException("Invalid address", "address"); + return new DnsEndPoint(address.Host, address.Port.Value, AddressFamily.InterNetwork); } } } \ No newline at end of file diff --git a/src/contrib/transports/Akka.Remote.AkkaIOTransport/Akka.Remote.AkkaIOTransport.csproj b/src/contrib/transports/Akka.Remote.AkkaIOTransport/Akka.Remote.AkkaIOTransport.csproj index cecc6c6aa1d..25a411138a0 100644 --- a/src/contrib/transports/Akka.Remote.AkkaIOTransport/Akka.Remote.AkkaIOTransport.csproj +++ b/src/contrib/transports/Akka.Remote.AkkaIOTransport/Akka.Remote.AkkaIOTransport.csproj @@ -32,6 +32,24 @@ 4 + + True + + + True + + + True + + + True + + + True + + + True + ..\..\..\packages\Google.ProtocolBuffers.2.4.1.521\lib\net40\Google.ProtocolBuffers.dll diff --git a/src/contrib/transports/Akka.Remote.AkkaIOTransport/AkkaIOTransport.cs b/src/contrib/transports/Akka.Remote.AkkaIOTransport/AkkaIOTransport.cs index 236a129c82a..6f4031fe975 100644 --- a/src/contrib/transports/Akka.Remote.AkkaIOTransport/AkkaIOTransport.cs +++ b/src/contrib/transports/Akka.Remote.AkkaIOTransport/AkkaIOTransport.cs @@ -22,9 +22,11 @@ class Settings public Settings(Config config) { Port = config.GetInt("port"); + Hostname = config.GetString("hostname"); } - public int Port { get; set; } + public int Port { get; private set; } + public string Hostname { get; private set; } } private readonly IActorRef _manager; @@ -37,6 +39,9 @@ public AkkaIOTransport(ActorSystem system, Config config) } public override string SchemeIdentifier { get { return Protocal; } } + + public override long MaximumPayloadBytes { get { return 128000; } } + public override bool IsResponsibleFor(Address remote) { return true; @@ -44,7 +49,7 @@ public override bool IsResponsibleFor(Address remote) public override Task>> Listen() { - return _manager.Ask>>(new Listen(_settings.Port)); + return _manager.Ask>>(new Listen(_settings.Hostname, _settings.Port)); } public override Task Associate(Address remoteAddress) { diff --git a/src/core/Akka.Persistence.Tests/CHANGES.txt b/src/contrib/transports/Akka.Remote.AkkaIOTransport/CHANGES.txt similarity index 100% rename from src/core/Akka.Persistence.Tests/CHANGES.txt rename to src/contrib/transports/Akka.Remote.AkkaIOTransport/CHANGES.txt diff --git a/src/contrib/transports/Akka.Remote.AkkaIOTransport/ConnectionAssociation.cs b/src/contrib/transports/Akka.Remote.AkkaIOTransport/ConnectionAssociation.cs index 891d02fe7fe..a8ea5eddd72 100644 --- a/src/contrib/transports/Akka.Remote.AkkaIOTransport/ConnectionAssociation.cs +++ b/src/contrib/transports/Akka.Remote.AkkaIOTransport/ConnectionAssociation.cs @@ -5,6 +5,7 @@ // //----------------------------------------------------------------------- +using System; using System.Threading.Tasks; using Akka.Actor; using Akka.IO; @@ -53,7 +54,7 @@ protected override void OnReceive(object message) if (message is IHandleEventListener) { var el = message as IHandleEventListener; - Context.Become(Receiving(el)); + Context.Become(WaitingForPrefix(el, IO.ByteString.Empty)); Stash.UnstashAll(); } else @@ -62,27 +63,57 @@ protected override void OnReceive(object message) } } - private UntypedReceive Receiving(IHandleEventListener el) + private UntypedReceive WaitingForPrefix(IHandleEventListener el, IO.ByteString buffer) { + if (buffer.Count >= 4) + { + var length = buffer.Iterator().GetInt(); + return WaitingForBody(el, buffer.Drop(4), length); + } return message => { if (message is Tcp.Received) { var received = message as Tcp.Received; - el.Notify(new InboundPayload(ByteString.CopyFrom(received.Data.ToArray()))); + Become(WaitingForPrefix(el, buffer.Concat(received.Data))); } - if (message is ByteString) - { - var bs = message as ByteString; - _connection.Tell(Tcp.Write.Create(IO.ByteString.Create(bs.ToByteArray()))); - } - else + else HandleWrite(message); + }; + } + + private UntypedReceive WaitingForBody(IHandleEventListener el, IO.ByteString buffer, int length) + { + if (buffer.Count >= length) + { + var parts = buffer.SplitAt(length); + el.Notify(new InboundPayload(ByteString.CopyFrom(parts.Item1.ToArray()))); + return WaitingForPrefix(el, parts.Item2); + } + return message => + { + if (message is Tcp.Received) { - Unhandled(message); + var received = message as Tcp.Received; + Become(WaitingForBody(el, buffer.Concat(received.Data), length)); } + else HandleWrite(message); }; } + private void HandleWrite(object message) + { + if (message is ByteString) + { + var bs = message as ByteString; + var buffer = ByteString.Unsafe.GetBuffer(bs); + var builder = new ByteStringBuilder(); + builder.PutInt(buffer.Length, ByteOrder.BigEndian); + builder.PutBytes(buffer); + _connection.Tell(Tcp.Write.Create(builder.Result())); + } + else Unhandled(message); + } + public IStash Stash { get; set; } } } \ No newline at end of file diff --git a/src/contrib/transports/Akka.Remote.AkkaIOTransport/TransportManager.cs b/src/contrib/transports/Akka.Remote.AkkaIOTransport/TransportManager.cs index c05c5d5f27c..768b507fe40 100644 --- a/src/contrib/transports/Akka.Remote.AkkaIOTransport/TransportManager.cs +++ b/src/contrib/transports/Akka.Remote.AkkaIOTransport/TransportManager.cs @@ -26,10 +26,13 @@ public Associate(Address remoteAddress) } class Listen { - public Listen(int port) + public Listen(string hostname, int port) { + Hostname = hostname; Port = port; } + + public string Hostname { get; private set; } public int Port { get; private set; } } @@ -42,14 +45,14 @@ protected override void OnReceive(object message) { var associate = message as Associate; Context.System.Tcp().Tell(new Tcp.Connect(associate.RemoteAddress.ToEndpoint())); - BecomeStacked(WaitingForConnected(Sender)); + BecomeStacked(WaitingForConnected(Sender, associate)); } else if (message is Listen) { var listen = message as Listen; var handler = Context.ActorOf(Props.Create(() => new TransportListener())); - Context.System.Tcp().Tell(new Tcp.Bind(handler, new IPEndPoint(IPAddress.Loopback, listen.Port))); - BecomeStacked(WaitingForBound(Sender, handler)); + Context.System.Tcp().Tell(new Tcp.Bind(handler, new IPEndPoint(IPAddress.Any, listen.Port))); + BecomeStacked(WaitingForBound(Sender, handler, listen)); } else { @@ -57,7 +60,7 @@ protected override void OnReceive(object message) } } - private Receive WaitingForBound(IActorRef replyTo, IActorRef handler) + private Receive WaitingForBound(IActorRef replyTo, IActorRef handler, Listen listen) { return message => { @@ -66,7 +69,7 @@ private Receive WaitingForBound(IActorRef replyTo, IActorRef handler) var bound = message as Tcp.Bound; var promise = new TaskCompletionSource(); promise.Task.PipeTo(handler); - replyTo.Tell(Tuple.Create(bound.LocalAddress.ToAddress(Context.System), promise)); + replyTo.Tell(Tuple.Create(new Address(AkkaIOTransport.Protocal, Context.System.Name, listen.Hostname, ((IPEndPoint) bound.LocalAddress).Port), promise)); UnbecomeStacked(); Stash.Unstash(); return true; @@ -75,7 +78,7 @@ private Receive WaitingForBound(IActorRef replyTo, IActorRef handler) }; } - private Receive WaitingForConnected(IActorRef replyTo) + private Receive WaitingForConnected(IActorRef replyTo, Associate associate) { return message => { @@ -84,7 +87,7 @@ private Receive WaitingForConnected(IActorRef replyTo) var connected = message as Tcp.Connected; var handler = Context.ActorOf(Props.Create(() => new ConnectionAssociationActor(Sender))); Sender.Tell(new Tcp.Register(handler)); - replyTo.Tell(new ConnectionAssociationHandle(handler, connected.LocalAddress.ToAddress(Context.System), connected.RemoteAddress.ToAddress(Context.System))); + replyTo.Tell(new ConnectionAssociationHandle(handler, connected.LocalAddress.ToAddress(Context.System), associate.RemoteAddress)); UnbecomeStacked(); Stash.Unstash(); return true; diff --git a/src/core/Akka.Persistence.Tests/licenses/license.txt b/src/contrib/transports/Akka.Remote.AkkaIOTransport/licenses/license.txt similarity index 100% rename from src/core/Akka.Persistence.Tests/licenses/license.txt rename to src/contrib/transports/Akka.Remote.AkkaIOTransport/licenses/license.txt diff --git a/src/core/Akka.Persistence.Tests/licenses/protoc-license.txt b/src/contrib/transports/Akka.Remote.AkkaIOTransport/licenses/protoc-license.txt similarity index 100% rename from src/core/Akka.Persistence.Tests/licenses/protoc-license.txt rename to src/contrib/transports/Akka.Remote.AkkaIOTransport/licenses/protoc-license.txt diff --git a/src/core/Akka.Persistence.Tests/protos/google/protobuf/csharp_options.proto b/src/contrib/transports/Akka.Remote.AkkaIOTransport/protos/google/protobuf/csharp_options.proto similarity index 100% rename from src/core/Akka.Persistence.Tests/protos/google/protobuf/csharp_options.proto rename to src/contrib/transports/Akka.Remote.AkkaIOTransport/protos/google/protobuf/csharp_options.proto diff --git a/src/core/Akka.Persistence.Tests/protos/google/protobuf/descriptor.proto b/src/contrib/transports/Akka.Remote.AkkaIOTransport/protos/google/protobuf/descriptor.proto similarity index 100% rename from src/core/Akka.Persistence.Tests/protos/google/protobuf/descriptor.proto rename to src/contrib/transports/Akka.Remote.AkkaIOTransport/protos/google/protobuf/descriptor.proto diff --git a/src/core/Akka.Persistence.Tests/protos/tutorial/addressbook.proto b/src/contrib/transports/Akka.Remote.AkkaIOTransport/protos/tutorial/addressbook.proto similarity index 100% rename from src/core/Akka.Persistence.Tests/protos/tutorial/addressbook.proto rename to src/contrib/transports/Akka.Remote.AkkaIOTransport/protos/tutorial/addressbook.proto diff --git a/src/core/Akka.MultiNodeTests/Akka.MultiNodeTests.csproj b/src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj similarity index 85% rename from src/core/Akka.MultiNodeTests/Akka.MultiNodeTests.csproj rename to src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj index 855a38b74c6..ef0c889964a 100644 --- a/src/core/Akka.MultiNodeTests/Akka.MultiNodeTests.csproj +++ b/src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj @@ -8,13 +8,14 @@ {F0781BEA-5BA0-4AF0-BB15-E3F209B681F5} Library Properties - Akka.MultiNodeTests - Akka.MultiNodeTests + Akka.Cluster.Tests.MultiNode + Akka.Cluster.Tests.MultiNode v4.5 512 ..\..\ true - 1a218765 + + true @@ -35,8 +36,9 @@ - - ..\..\packages\Microsoft.Bcl.Immutable.1.0.34\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + + ..\..\packages\System.Collections.Immutable.1.1.37\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True @@ -44,14 +46,17 @@ - + ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True - + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True - + ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True @@ -63,7 +68,6 @@ - @@ -100,10 +104,11 @@ - + - + + diff --git a/src/core/Akka.MultiNodeTests/ClusterDeathWatchSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/ClusterDeathWatchSpec.cs similarity index 99% rename from src/core/Akka.MultiNodeTests/ClusterDeathWatchSpec.cs rename to src/core/Akka.Cluster.Tests.MultiNode/ClusterDeathWatchSpec.cs index a34aec837a0..c2532ed09a5 100644 --- a/src/core/Akka.MultiNodeTests/ClusterDeathWatchSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/ClusterDeathWatchSpec.cs @@ -15,7 +15,7 @@ using Akka.TestKit.TestActors; using Xunit; -namespace Akka.MultiNodeTests +namespace Akka.Cluster.Tests.MultiNode { public class ClusterDeathWatchSpecConfig : MultiNodeConfig { diff --git a/src/core/Akka.MultiNodeTests/ConvergenceSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/ConvergenceSpec.cs similarity index 99% rename from src/core/Akka.MultiNodeTests/ConvergenceSpec.cs rename to src/core/Akka.Cluster.Tests.MultiNode/ConvergenceSpec.cs index d2818efba11..ac947756899 100644 --- a/src/core/Akka.MultiNodeTests/ConvergenceSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/ConvergenceSpec.cs @@ -9,13 +9,12 @@ using System.Linq; using System.Threading; using Akka.Actor; -using Akka.Cluster; using Akka.Configuration; using Akka.Remote.TestKit; using Akka.TestKit; using Xunit; -namespace Akka.MultiNodeTests +namespace Akka.Cluster.Tests.MultiNode { public class ConvergenceSpecConfig : MultiNodeConfig { diff --git a/src/core/Akka.MultiNodeTests/FailureDetectorPuppet.cs b/src/core/Akka.Cluster.Tests.MultiNode/FailureDetectorPuppet.cs similarity index 98% rename from src/core/Akka.MultiNodeTests/FailureDetectorPuppet.cs rename to src/core/Akka.Cluster.Tests.MultiNode/FailureDetectorPuppet.cs index 4af8d961398..6e7d7df09cf 100644 --- a/src/core/Akka.MultiNodeTests/FailureDetectorPuppet.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/FailureDetectorPuppet.cs @@ -10,7 +10,7 @@ using Akka.Remote; using Akka.Util; -namespace Akka.MultiNodeTests +namespace Akka.Cluster.Tests.MultiNode { /// /// User controllable "puppet" failure detector. diff --git a/src/core/Akka.MultiNodeTests/InitialHeartbeatSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/InitialHeartbeatSpec.cs similarity index 99% rename from src/core/Akka.MultiNodeTests/InitialHeartbeatSpec.cs rename to src/core/Akka.Cluster.Tests.MultiNode/InitialHeartbeatSpec.cs index 7f8a4d4a0fe..855f01b7b3e 100644 --- a/src/core/Akka.MultiNodeTests/InitialHeartbeatSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/InitialHeartbeatSpec.cs @@ -7,13 +7,12 @@ using System; using System.Linq; -using Akka.Cluster; using Akka.Configuration; using Akka.Remote.TestKit; using Akka.Remote.Transport; using Xunit; -namespace Akka.MultiNodeTests +namespace Akka.Cluster.Tests.MultiNode { public class InitialHeartbeatMultiNodeConfig : MultiNodeConfig { diff --git a/src/core/Akka.MultiNodeTests/JoinInProgressSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/JoinInProgressSpec.cs similarity index 98% rename from src/core/Akka.MultiNodeTests/JoinInProgressSpec.cs rename to src/core/Akka.Cluster.Tests.MultiNode/JoinInProgressSpec.cs index 6b2e81d3f38..189d249e5f0 100644 --- a/src/core/Akka.MultiNodeTests/JoinInProgressSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/JoinInProgressSpec.cs @@ -12,7 +12,7 @@ using Akka.Remote.TestKit; using Xunit; -namespace Akka.MultiNodeTests +namespace Akka.Cluster.Tests.MultiNode { public class JoinInProgressMultiNodeConfig : MultiNodeConfig { diff --git a/src/core/Akka.MultiNodeTests/JoinSeedNodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/JoinSeedNodeSpec.cs similarity index 98% rename from src/core/Akka.MultiNodeTests/JoinSeedNodeSpec.cs rename to src/core/Akka.Cluster.Tests.MultiNode/JoinSeedNodeSpec.cs index 51b9eb33cb3..88e95a71f8f 100644 --- a/src/core/Akka.MultiNodeTests/JoinSeedNodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/JoinSeedNodeSpec.cs @@ -11,7 +11,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; -namespace Akka.MultiNodeTests +namespace Akka.Cluster.Tests.MultiNode { public class JoinSeedNodeConfig : MultiNodeConfig { diff --git a/src/core/Akka.MultiNodeTests/LeaderLeavingSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/LeaderLeavingSpec.cs similarity index 99% rename from src/core/Akka.MultiNodeTests/LeaderLeavingSpec.cs rename to src/core/Akka.Cluster.Tests.MultiNode/LeaderLeavingSpec.cs index 4b29c58ac02..5d26c160341 100644 --- a/src/core/Akka.MultiNodeTests/LeaderLeavingSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/LeaderLeavingSpec.cs @@ -8,11 +8,10 @@ using System; using System.Linq; using Akka.Actor; -using Akka.Cluster; using Akka.Remote.TestKit; using Akka.TestKit; -namespace Akka.MultiNodeTests +namespace Akka.Cluster.Tests.MultiNode { public class LeaderLeavingSpecConfig : MultiNodeConfig { diff --git a/src/core/Akka.MultiNodeTests/MultiNodeClusterSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/MultiNodeClusterSpec.cs similarity index 98% rename from src/core/Akka.MultiNodeTests/MultiNodeClusterSpec.cs rename to src/core/Akka.Cluster.Tests.MultiNode/MultiNodeClusterSpec.cs index a5af205496c..03712385dd5 100644 --- a/src/core/Akka.MultiNodeTests/MultiNodeClusterSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/MultiNodeClusterSpec.cs @@ -21,7 +21,7 @@ using Akka.TestKit.Xunit2; using Xunit; -namespace Akka.MultiNodeTests +namespace Akka.Cluster.Tests.MultiNode { //TODO: WatchedByCoroner? //@Aaronontheweb: Coroner is a JVM-specific instrument used to report deadlocks and other fun stuff. @@ -31,7 +31,7 @@ public abstract class MultiNodeClusterSpec : MultiNodeSpec public static Config ClusterConfigWithFailureDetectorPuppet() { return ConfigurationFactory.ParseString( - @"akka.cluster.failure-detector.implementation-class = ""Akka.MultiNodeTests.FailureDetectorPuppet, Akka.MultiNodeTests""") + string.Format(@"akka.cluster.failure-detector.implementation-class = ""{0}""", typeof(FailureDetectorPuppet).AssemblyQualifiedName)) .WithFallback(ClusterConfig()); } @@ -230,7 +230,7 @@ public Address GetAddress(RoleName role) /// /// Get the cluster node to use. /// - public Cluster.Cluster Cluster { get { return Akka.Cluster.Cluster.Get(Sys); } } + public Akka.Cluster.Cluster Cluster { get { return Akka.Cluster.Cluster.Get(Sys); } } /// /// Use this method for the initial startup of the cluster node diff --git a/src/core/Akka.MultiNodeTests/MultiNodeLoggingConfig.cs b/src/core/Akka.Cluster.Tests.MultiNode/MultiNodeLoggingConfig.cs similarity index 92% rename from src/core/Akka.MultiNodeTests/MultiNodeLoggingConfig.cs rename to src/core/Akka.Cluster.Tests.MultiNode/MultiNodeLoggingConfig.cs index f25adab5e8e..4a784aa20ef 100644 --- a/src/core/Akka.MultiNodeTests/MultiNodeLoggingConfig.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/MultiNodeLoggingConfig.cs @@ -8,7 +8,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; -namespace Akka.MultiNodeTests +namespace Akka.Cluster.Tests.MultiNode { /// /// Static provider that allows toggleable logging @@ -19,7 +19,7 @@ public static class MultiNodeLoggingConfig // ReSharper disable once InconsistentNaming private static readonly Config _loggingConfig = ConfigurationFactory.ParseString(@" - akka.loggers = [""Akka.Event.DefaultLogger""]"); + akka.loggers = []"); /// /// Used to specify which loggers to enable for the instances diff --git a/src/core/Akka.MultiNodeTests/Properties/AssemblyInfo.cs b/src/core/Akka.Cluster.Tests.MultiNode/Properties/AssemblyInfo.cs similarity index 91% rename from src/core/Akka.MultiNodeTests/Properties/AssemblyInfo.cs rename to src/core/Akka.Cluster.Tests.MultiNode/Properties/AssemblyInfo.cs index a064f2d3bfb..ebe8d053261 100644 --- a/src/core/Akka.MultiNodeTests/Properties/AssemblyInfo.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Properties/AssemblyInfo.cs @@ -5,11 +5,11 @@ // 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("Akka.MultiNodeTests")] +[assembly: AssemblyTitle("Akka.Cluster.Tests.MultiNode")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Akka.MultiNodeTests")] +[assembly: AssemblyProduct("Akka.Cluster.Tests.MultiNode")] [assembly: AssemblyCopyright("Copyright © 2015")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/core/Akka.MultiNodeTests/Routing/ClusterConsistentHashingGroupSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingGroupSpec.cs similarity index 99% rename from src/core/Akka.MultiNodeTests/Routing/ClusterConsistentHashingGroupSpec.cs rename to src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingGroupSpec.cs index 1b7b92275f3..09865bc7801 100644 --- a/src/core/Akka.MultiNodeTests/Routing/ClusterConsistentHashingGroupSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingGroupSpec.cs @@ -15,7 +15,7 @@ using Akka.Routing; using Akka.TestKit; -namespace Akka.MultiNodeTests.Routing +namespace Akka.Cluster.Tests.MultiNode.Routing { public class ClusterConsistentHashingGroupSpecConfig : MultiNodeConfig { diff --git a/src/core/Akka.MultiNodeTests/Routing/ClusterConsistentHashingRouterSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingRouterSpec.cs similarity index 99% rename from src/core/Akka.MultiNodeTests/Routing/ClusterConsistentHashingRouterSpec.cs rename to src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingRouterSpec.cs index fb78e6f6bc1..b0caeb9c372 100644 --- a/src/core/Akka.MultiNodeTests/Routing/ClusterConsistentHashingRouterSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingRouterSpec.cs @@ -15,7 +15,7 @@ using Akka.TestKit; using Xunit; -namespace Akka.MultiNodeTests.Routing +namespace Akka.Cluster.Tests.MultiNode.Routing { public class ConsistentHashingRouterMultiNodeConfig : MultiNodeConfig { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/app.config b/src/core/Akka.Cluster.Tests.MultiNode/app.config new file mode 100644 index 00000000000..f0bd3189cf4 --- /dev/null +++ b/src/core/Akka.Cluster.Tests.MultiNode/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/core/Akka.Cluster.Tests.MultiNode/packages.config b/src/core/Akka.Cluster.Tests.MultiNode/packages.config new file mode 100644 index 00000000000..15502b4d7f9 --- /dev/null +++ b/src/core/Akka.Cluster.Tests.MultiNode/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/core/Akka.Cluster.Tests/Akka.Cluster.Tests.csproj b/src/core/Akka.Cluster.Tests/Akka.Cluster.Tests.csproj index 3caab4879d0..da9c4c5db4d 100644 --- a/src/core/Akka.Cluster.Tests/Akka.Cluster.Tests.csproj +++ b/src/core/Akka.Cluster.Tests/Akka.Cluster.Tests.csproj @@ -13,7 +13,8 @@ 512 ..\..\ true - d8ea7d64 + + true @@ -33,32 +34,40 @@ 4 - + ..\..\packages\FluentAssertions.3.3.0\lib\net45\FluentAssertions.dll + True - + ..\..\packages\FluentAssertions.3.3.0\lib\net45\FluentAssertions.Core.dll + True - + + ..\..\packages\Google.ProtocolBuffers.2.4.1.521\lib\net40\Google.ProtocolBuffers.dll + True + + ..\..\packages\Google.ProtocolBuffers.2.4.1.521\lib\net40\Google.ProtocolBuffers.Serialization.dll + True + + ..\..\packages\System.Collections.Immutable.1.1.37\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + - - ..\..\packages\Google.ProtocolBuffers.2.4.1.521\lib\net40\Google.ProtocolBuffers.dll - - - ..\..\packages\Microsoft.Bcl.Immutable.1.0.34\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll - - + ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True - + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True - + ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True @@ -89,6 +98,7 @@ + @@ -125,12 +135,20 @@ + + + + + + + - + + + - diff --git a/src/core/Akka.Cluster.Tests/ClusterDomainEventPublisherSpec.cs b/src/core/Akka.Cluster.Tests/ClusterDomainEventPublisherSpec.cs index 6a63917770c..27142e71a88 100644 --- a/src/core/Akka.Cluster.Tests/ClusterDomainEventPublisherSpec.cs +++ b/src/core/Akka.Cluster.Tests/ClusterDomainEventPublisherSpec.cs @@ -20,7 +20,6 @@ public class ClusterDomainEventPublisherSpec : AkkaSpec auto-down-unreachable-after = 0s periodic-tasks-initial-delay = 120 s // turn off scheduled tasks publish-stats-interval = 0 s # always, when it happens - failure-detector.implementation-class = ""Akka.MultiNodeTests.FailureDetectorPuppet, Akka.MultiNodeTests"" } akka.actor.provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster"" akka.remote.helios.tcp.port = 0"; diff --git a/src/core/Akka.Cluster.Tests/ClusterSpec.cs b/src/core/Akka.Cluster.Tests/ClusterSpec.cs index a1352b472cd..aa58ff71b90 100644 --- a/src/core/Akka.Cluster.Tests/ClusterSpec.cs +++ b/src/core/Akka.Cluster.Tests/ClusterSpec.cs @@ -21,7 +21,6 @@ public class ClusterSpec : AkkaSpec auto-down-unreachable-after = 0s periodic-tasks-initial-delay = 120 s // turn off scheduled tasks publish-stats-interval = 0 s # always, when it happens - failure-detector.implementation-class = ""Akka.MultiNodeTests.FailureDetectorPuppet, Akka.MultiNodeTests"" } akka.actor.provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster"" akka.remote.helios.tcp.port = 0"; diff --git a/src/core/Akka.Cluster.Tests/Routing/ClusterRouterAsk1343BugFixSpec.cs b/src/core/Akka.Cluster.Tests/Routing/ClusterRouterAsk1343BugFixSpec.cs new file mode 100644 index 00000000000..fd31ef6f2b0 --- /dev/null +++ b/src/core/Akka.Cluster.Tests/Routing/ClusterRouterAsk1343BugFixSpec.cs @@ -0,0 +1,109 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Cluster.Routing; +using Akka.Routing; +using Akka.TestKit; +using Akka.TestKit.TestActors; +using Xunit; + +namespace Akka.Cluster.Tests.Routing +{ + /// + /// Spec to get to the bottom of https://github.com/akkadotnet/akka.net/issues/1343 + /// + public class ClusterRouterAsk1343BugFixSpec : AkkaSpec + { + public ClusterRouterAsk1343BugFixSpec() + : base(@" + akka{ + actor{ + ask-timeout = 0.5s # use a default ask timeout + provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster"" + deployment { + /router1 { + router = round-robin-pool + nr-of-instances = 1 + cluster { + enabled = on + max-nr-of-instances-per-node = 1 + allow-local-routees = true + } + } + /router2 { + router = round-robin-group + nr-of-instances = 1 + routees.paths = [""/user/echo""] + cluster { + enabled = on + max-nr-of-instances-per-node = 1 + allow-local-routees = true + } + } + /router3 { + router = round-robin-group + nr-of-instances = 1 + routees.paths = [""/user/echo""] + cluster { + enabled = on + max-nr-of-instances-per-node = 1 + allow-local-routees = false #no one to route to! + } + } + } + } + + remote.helios.tcp.port = 0 + }") + { + } + + + [Fact] + public async Task Should_Ask_Clustered_Pool_Router_and_forward_ask_to_routee() + { + var router = Sys.ActorOf(EchoActor.Props(this, true).WithRouter(FromConfig.Instance), "router1"); + Assert.IsType(router); + + var result = await router.Ask("foo"); + ExpectMsg().ShouldBe(result); + } + + [Fact] + public async Task Should_Ask_Clustered_Group_Router_and_forward_ask_to_routee() + { + var echo = Sys.ActorOf(EchoActor.Props(this, true), "echo"); + var router = Sys.ActorOf(Props.Empty.WithRouter(FromConfig.Instance), "router2"); + Assert.IsType(router); + + var result = await router.Ask("foo"); + ExpectMsg().ShouldBe(result); + } + + [Fact] + public async Task Should_Ask_Clustered_Group_Router_and_with_no_routees_and_timeout() + { + var router = Sys.ActorOf(Props.Empty.WithRouter(FromConfig.Instance), "router3"); + Assert.IsType(router); + + try + { + var result = await router.Ask("foo"); + } + catch (Exception ex) + { + Assert.IsType(ex); + } + } + } +} diff --git a/src/core/Akka.Cluster.Tests/app.config b/src/core/Akka.Cluster.Tests/app.config new file mode 100644 index 00000000000..f0bd3189cf4 --- /dev/null +++ b/src/core/Akka.Cluster.Tests/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/core/Akka.Cluster.Tests/packages.config b/src/core/Akka.Cluster.Tests/packages.config index e870267b673..b603d1619c6 100644 --- a/src/core/Akka.Cluster.Tests/packages.config +++ b/src/core/Akka.Cluster.Tests/packages.config @@ -2,7 +2,7 @@ - + diff --git a/src/core/Akka.Cluster/Akka.Cluster.csproj b/src/core/Akka.Cluster/Akka.Cluster.csproj index d6f51329c34..f368cd524ce 100644 --- a/src/core/Akka.Cluster/Akka.Cluster.csproj +++ b/src/core/Akka.Cluster/Akka.Cluster.csproj @@ -50,19 +50,24 @@ bin\Release\Akka.Cluster.xml - + + False + ..\..\packages\Google.ProtocolBuffers.2.4.1.521\lib\net40\Google.ProtocolBuffers.dll + True + + + False ..\..\packages\Google.ProtocolBuffers.2.4.1.521\lib\net40\Google.ProtocolBuffers.Serialization.dll + True + + ..\..\packages\System.Collections.Immutable.1.1.37\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + - - ..\..\packages\Google.ProtocolBuffers.2.4.1.521\lib\net40\Google.ProtocolBuffers.dll - - - ..\..\packages\Microsoft.Bcl.Immutable.1.0.34\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll - @@ -103,8 +108,15 @@ + + + + + + + + - diff --git a/src/core/Akka.Cluster/Cluster.cs b/src/core/Akka.Cluster/Cluster.cs index 5a89beeb916..6e4d6e1fe98 100644 --- a/src/core/Akka.Cluster/Cluster.cs +++ b/src/core/Akka.Cluster/Cluster.cs @@ -27,13 +27,13 @@ public override Cluster CreateExtension(ExtendedActorSystem system) //TODO: xmldoc /// - /// This module is responsible cluster membership information. Changes to the cluster - /// information is retrieved through [[#subscribe]]. Commands to operate the cluster is - /// available through methods in this class, such as [[#join]], [[#down]] and [[#leave]]. + /// This module is responsible for cluster membership information. Changes to the cluster + /// information is retrieved through . Commands to operate the cluster is + /// available through methods in this class, such as , and . /// - /// Each cluster [[Member]] is identified by its [[akka.actor.Address]], and + /// Each cluster is identified by its , and /// the cluster address of this actor system is [[#selfAddress]]. A member also has a status; - /// initially [[MemberStatus.Joining]] followed by [[MemberStatus.Up]]. + /// initially followed by . /// public class Cluster :IExtension { @@ -111,7 +111,7 @@ private async Task GetClusterCoreRef() /// /// The actor who'll receive the cluster domain events /// subclasses - /// A snapshot of will be sent to as the first message + /// A snapshot of will be sent to as the first message public void Subscribe(IActorRef subscriber, Type[] to) { Subscribe(subscriber, ClusterEvent.SubscriptionInitialStateMode.InitialStateAsSnapshot, to); @@ -123,10 +123,10 @@ public void Subscribe(IActorRef subscriber, Type[] to) /// The actor who'll receive the cluster domain events /// /// If set to the events corresponding to the current state - /// will be sent to to mimic what it would have seen if it were listening to the events when they occurred in the past. + /// will be sent to to mimic what it would have seen if it were listening to the events when they occurred in the past. /// /// If set to - /// a snapshot of will be sent to as the first message. + /// a snapshot of will be sent to as the first message. /// subclasses public void Subscribe(IActorRef subscriber, ClusterEvent.SubscriptionInitialStateMode initialStateMode, Type[] to) { @@ -161,7 +161,7 @@ public void SendCurrentClusterState(IActorRef receiver) } /// - /// Try to join this cluster node specified by . + /// Try to join this cluster node specified by . /// A command is sent to the node to join. /// /// An actor system can only join a cluster once. Additional attempts will be ignored. @@ -187,7 +187,7 @@ public void JoinSeedNodes(ImmutableList
seedNodes) } /// - /// Send command to issue state transition to LEAVING for the node specified by . + /// Send command to issue state transition to LEAVING for the node specified by . /// The member will go through the status changes (not published to /// subscribers) followed by and finally . /// @@ -205,7 +205,7 @@ public void Leave(Address address) } /// - /// Send command to DOWN the node specified by . + /// Send command to DOWN the node specified by . /// /// When a member is considered by the failure detector to be unreachable the leader is not /// allowed to perform its duties, such as changing status of new joining members to . @@ -278,7 +278,11 @@ public void Shutdown() { LogInfo("Shutting down..."); System.Stop(_clusterDaemons); - _readView.Dispose(); + + if (_readView != null) + { + _readView.Dispose(); + } LogInfo("Successfully shut down"); } diff --git a/src/core/Akka.Cluster/ClusterDaemon.cs b/src/core/Akka.Cluster/ClusterDaemon.cs index 2bc25933d8e..14b844b9dc0 100644 --- a/src/core/Akka.Cluster/ClusterDaemon.cs +++ b/src/core/Akka.Cluster/ClusterDaemon.cs @@ -985,8 +985,7 @@ public void Join(Address address) } else if (address.System != _cluster.SelfAddress.System) { - _log.Warning( - "Trying to join member with wrong ActorSystem name, but was ignored, expected [{0}] but was [{1}]", + _log.Warning("Trying to join member with wrong ActorSystem name, but was ignored, expected [{0}] but was [{1}]", _cluster.SelfAddress.System, address.System); } else @@ -1036,8 +1035,7 @@ public void Joining(UniqueAddress node, ImmutableHashSet roles) } else if (node.Address.System != _cluster.SelfAddress.System) { - _log.Warning( - "Member with wrong ActorSystem name tried to join, but was ignored, expected [{0}] but was [{1}]", + _log.Warning("Member with wrong ActorSystem name tried to join, but was ignored, expected [{0}] but was [{1}]", _cluster.SelfAddress.System, node.Address.System); } else diff --git a/src/core/Akka.Cluster/ClusterEvent.cs b/src/core/Akka.Cluster/ClusterEvent.cs index b22a5784bc3..26c49aac5e3 100644 --- a/src/core/Akka.Cluster/ClusterEvent.cs +++ b/src/core/Akka.Cluster/ClusterEvent.cs @@ -29,7 +29,7 @@ public enum SubscriptionInitialStateMode //TODO: Sort out xml doc references /// /// When using this subscription mode a snapshot of - /// [[akka.cluster.ClusterEvent.CurrentClusterState]] will be sent to the + /// will be sent to the /// subscriber as the first message. /// InitialStateAsSnapshot, @@ -215,7 +215,7 @@ public MemberUp(Member member) //TODO: Sort out xml doc references /// - /// Member status changed to [[MemberStatus.Exiting]] and will be removed + /// Member status changed to and will be removed /// when all members have seen the `Exiting` status. /// public sealed class MemberExited : MemberStatusChange @@ -363,10 +363,9 @@ public sealed class ClusterShuttingDown : IClusterDomainEvent public static readonly IClusterDomainEvent Instance = new ClusterShuttingDown(); } - //TODO: xml doc /// - /// Marker interface to facilitate subscription of - /// both [[UnreachableMember]] and [[ReachableMember]]. + /// A marker interface to facilitate the subscription of + /// both and . /// public interface IReachabilityEvent : IClusterDomainEvent { @@ -419,7 +418,7 @@ public UnreachableMember(Member member) /// /// A member is considered as reachable by the failure detector /// after having been unreachable. - /// @see [[UnreachableMember]] + /// /// public sealed class ReachableMember : ReachabilityEvent { diff --git a/src/core/Akka.Cluster/ClusterMetricsCollector.cs b/src/core/Akka.Cluster/ClusterMetricsCollector.cs index 8853b7e201e..412c53512bc 100644 --- a/src/core/Akka.Cluster/ClusterMetricsCollector.cs +++ b/src/core/Akka.Cluster/ClusterMetricsCollector.cs @@ -223,7 +223,7 @@ public MetricsGossip Remove(Address node) } /// - /// Only the nodes that are in the set. + /// Only the nodes that are in the set. /// public MetricsGossip Filter(ImmutableHashSet
includeNodes) { @@ -355,7 +355,7 @@ public NodeMetrics(Address address, long timestamp, ImmutableHashSet met public NodeMetrics(Address address, long timestamp) : this(address, timestamp, ImmutableHashSet.Create()) { } /// - /// Return the metric that matches . Returns null if not found. + /// Return the metric that matches . Returns null if not found. /// public Metric Metric(string key) { @@ -478,7 +478,7 @@ public override bool Equals(object obj) #region Static methods /// - /// Creates a new instance if is valid, otherwise + /// Creates a new instance if is valid, otherwise /// returns null. Invalid numeric values are negative and NaN/Infinite. /// public static Metric Create(string name, double value, double? decayFactor = null) @@ -662,7 +662,7 @@ public static SystemMemory ExtractSystemMemory(NodeMetrics nodeMetrics) } /** - * @param address [[akka.actor.Address]] of the node the metrics are gathered at + * @param address of the node the metrics are gathered at * @param timestamp the time of sampling, in milliseconds since midnight, January 1, 1970 UTC * @param systemLoadAverage OS-specific average load on the CPUs in the system, for the past 1 minute, * The system is possibly nearing a bottleneck if the system load average is nearing number of cpus/cores. @@ -747,17 +747,17 @@ public PerformanceCounterMetricsCollector(Address address, double decayFactor) private PerformanceCounter _systemLoadAverageCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total", true); private PerformanceCounter _systemAvailableMemory = new PerformanceCounter("Memory", "Available MBytes", true); - private static readonly bool IsRunningOnMono = Type.GetType("Mono.Runtime") != null; + private static readonly bool IsRunningOnMono = Type.GetType("Mono.Runtime") != null; - // Mono doesn't support Microsoft.VisualBasic, so need an alternative way of sampling this value - // see http://stackoverflow.com/questions/105031/how-do-you-get-total-amount-of-ram-the-computer-has - private PerformanceCounter _monoSystemMaxMemory = IsRunningOnMono - ? new PerformanceCounter("Mono Memory", "Total Physical Memory") - : null; + // Mono doesn't support Microsoft.VisualBasic, so need an alternative way of sampling this value + // see http://stackoverflow.com/questions/105031/how-do-you-get-total-amount-of-ram-the-computer-has + private PerformanceCounter _monoSystemMaxMemory = IsRunningOnMono + ? new PerformanceCounter("Mono Memory", "Total Physical Memory") + : null; - #endregion + #endregion public Address Address { get; private set; } @@ -817,20 +817,20 @@ public Metric SystemMemoryAvailable() ///
public Metric SystemMaxMemory() { - return Metric.Create(StandardMetrics.SystemMemoryMax, - IsRunningOnMono - ? _monoSystemMaxMemory.RawValue - : GetVbTotalPhysicalMemory()); + return Metric.Create(StandardMetrics.SystemMemoryMax, + IsRunningOnMono + ? _monoSystemMaxMemory.RawValue + : GetVbTotalPhysicalMemory()); } - double GetVbTotalPhysicalMemory() - { + double GetVbTotalPhysicalMemory() + { #if __MonoCS__ - throw new NotImplementedException(); + throw new NotImplementedException(); #else - return new Microsoft.VisualBasic.Devices.ComputerInfo().TotalPhysicalMemory; + return new Microsoft.VisualBasic.Devices.ComputerInfo().TotalPhysicalMemory; #endif - } + } #endregion diff --git a/src/core/Akka.Cluster/Configuration/ClusterConfigFactory.cs b/src/core/Akka.Cluster/Configuration/ClusterConfigFactory.cs index 01c026aea9b..67f4b3ff673 100644 --- a/src/core/Akka.Cluster/Configuration/ClusterConfigFactory.cs +++ b/src/core/Akka.Cluster/Configuration/ClusterConfigFactory.cs @@ -12,24 +12,26 @@ namespace Akka.Cluster.Configuration { /// - /// Internal class used for loading akka-cluster configuration values + /// This class contains methods used to retrieve cluster configuration options from this assembly's resources. + /// + /// Note! Part of internal API. Breaking changes may occur without notice. Use at own risk. /// internal static class ClusterConfigFactory { /// - /// Defaults this instance. + /// Retrieves the default cluster options that Akka.NET uses when no configuration has been defined. /// - /// Config. + /// The configuration that contains default values for all cluster options. public static Config Default() { return FromResource("Akka.Cluster.Configuration.Cluster.conf"); } /// - /// Froms the resource. + /// Retrieves a configuration defined in a resource of the current executing assembly. /// - /// Name of the resource. - /// Config. + /// The name of the resource that contains the configuration. + /// The configuration defined in the current executing assembly. internal static Config FromResource(string resourceName) { var assembly = typeof(ClusterConfigFactory).Assembly; @@ -47,4 +49,3 @@ internal static Config FromResource(string resourceName) } } } - diff --git a/src/core/Akka.Cluster/Gossip.cs b/src/core/Akka.Cluster/Gossip.cs index 8ec7109ac85..61c60214017 100644 --- a/src/core/Akka.Cluster/Gossip.cs +++ b/src/core/Akka.Cluster/Gossip.cs @@ -205,7 +205,6 @@ public Gossip Merge(Gossip that) return new Gossip(mergedMembers, new GossipOverview(mergedSeen, mergedReachability), mergedVClock); } - /// // First check that: // 1. we don't have any members that are unreachable, or // 2. all unreachable members in the set have status DOWN or EXITING @@ -213,7 +212,6 @@ public Gossip Merge(Gossip that) // When that is done we check that all members with a convergence // status is in the seen table and has the latest vector clock // version - /// public bool Convergence(UniqueAddress selfUniqueAddress) { var unreachable = _overview.Reachability.AllUnreachableOrTerminated diff --git a/src/core/Akka.Cluster/Properties/AssemblyInfo.cs b/src/core/Akka.Cluster/Properties/AssemblyInfo.cs index f1e3e9f79d6..74129f0a4fd 100644 --- a/src/core/Akka.Cluster/Properties/AssemblyInfo.cs +++ b/src/core/Akka.Cluster/Properties/AssemblyInfo.cs @@ -18,7 +18,7 @@ [assembly: AssemblyProduct("Akka.Cluster")] [assembly: AssemblyCulture("")] [assembly: InternalsVisibleTo("Akka.Cluster.Tests")] -[assembly: InternalsVisibleTo("Akka.MultiNodeTests")] +[assembly: InternalsVisibleTo("Akka.Cluster.Tests.MultiNode")] // 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 diff --git a/src/core/Akka.Cluster/packages.config b/src/core/Akka.Cluster/packages.config index 8bc12acb513..9a9d74d4213 100644 --- a/src/core/Akka.Cluster/packages.config +++ b/src/core/Akka.Cluster/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/src/core/Akka.FSharp.Tests/Akka.FSharp.Tests.fsproj b/src/core/Akka.FSharp.Tests/Akka.FSharp.Tests.fsproj index 09b0a5050f5..d219f39613c 100644 --- a/src/core/Akka.FSharp.Tests/Akka.FSharp.Tests.fsproj +++ b/src/core/Akka.FSharp.Tests/Akka.FSharp.Tests.fsproj @@ -14,7 +14,8 @@ Akka.FSharp.Tests ..\..\ true - 802fc494 + + true @@ -70,16 +71,19 @@ - - + + + ..\..\packages\FsCheck.0.9.4.0\lib\net40-Client\FsCheck.dll + True ..\..\packages\FsCheck.Xunit.0.4.1.0\lib\net40-Client\FsCheck.Xunit.dll + True diff --git a/src/core/Akka.FSharp.Tests/ApiTests.fs b/src/core/Akka.FSharp.Tests/ApiTests.fs index bd064857fa8..5511865c1fb 100644 --- a/src/core/Akka.FSharp.Tests/ApiTests.fs +++ b/src/core/Akka.FSharp.Tests/ApiTests.fs @@ -38,37 +38,108 @@ type TestUnion = type TestUnion2 = | C of string * TestUnion | D of int +// +//[] +//let ``can serialize and deserialize discriminated unions over remote nodes using wire serializer`` () = +// let remoteConfig port = +// sprintf """ +// akka { +// actor { +// ask-timeout = 5s +// provider = "Akka.Remote.RemoteActorRefProvider, Akka.Remote" +// serialization-bindings { +// "System.Object" = wire +// } +// } +// remote { +// helios.tcp { +// port = %i +// hostname = localhost +// } +// } +// } +// """ port +// |> Configuration.parse +// +// use server = System.create "server-system" (remoteConfig 9911) +// use client = System.create "client-system" (remoteConfig 0) +// +// let aref = +// spawne client "a-1" <@ actorOf2 (fun mailbox msg -> +// match msg with +// | C("a-11", B(11, "a-12")) -> mailbox.Sender() mailbox.Unhandled msg) @> +// [SpawnOption.Deploy (Deploy(RemoteScope (Address.Parse "akka.tcp://server-system@localhost:9911")))] +// let msg = C("a-11", B(11, "a-12")) +// let response = aref Async.RunSynchronously +// response +// |> equals msg -[] -let ``can serialize and deserialize discriminated unions over remote nodes`` () = - let remoteConfig port = - sprintf """ +//[] +// FAILS +let ``actor that accepts _ will receive unit message`` () = + let timeoutConfig = + """ akka { actor { ask-timeout = 5s - provider = "Akka.Remote.RemoteActorRefProvider, Akka.Remote" } - remote { - helios.tcp { - port = %i - hostname = localhost - } + } + """ + |> Configuration.parse + + let getWhateverHandler (mailbox : Actor<_>) _ = + mailbox.Sender() Async.RunSynchronously + response + |> equals "SomethingToReturn" + +[] +// SUCCEEDS +let ``actor that accepts _ will receive string message`` () = + let timeoutConfig = + """ + akka { + actor { + ask-timeout = 5s } } - """ port - |> Configuration.parse - - use server = System.create "server-system" (remoteConfig 9911) - use client = System.create "client-system" (remoteConfig 0) - - let aref = - spawne client "a-1" <@ actorOf2 (fun mailbox msg -> - match msg with - | C("a-11", B(11, "a-12")) -> mailbox.Sender() mailbox.Unhandled msg) @> - [SpawnOption.Deploy (Deploy(RemoteScope (Address.Parse "akka.tcp://server-system@localhost:9911")))] - let msg = C("a-11", B(11, "a-12")) - let response = aref Async.RunSynchronously + """ + |> Configuration.parse + + let getWhateverHandler (mailbox : Actor<_>) _ = + mailbox.Sender() Async.RunSynchronously response - |> equals msg + |> equals "SomethingToReturn" + +[] +// SUCCEEDS +let ``actor that accepts unit will receive unit message`` () = + let timeoutConfig = + """ + akka { + actor { + ask-timeout = 5s + } + } + """ + |> Configuration.parse + let getWhateverHandler (mailbox : Actor) () = + mailbox.Sender() Async.RunSynchronously + response + |> equals "SomethingToReturn" \ No newline at end of file diff --git a/src/core/Akka.FSharp.Tests/InfrastructureTests.fs b/src/core/Akka.FSharp.Tests/InfrastructureTests.fs new file mode 100644 index 00000000000..05bc8a62286 --- /dev/null +++ b/src/core/Akka.FSharp.Tests/InfrastructureTests.fs @@ -0,0 +1,35 @@ + +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +module Akka.FSharp.Tests.InfrastructureTests + +open Akka.FSharp +open Akka.Actor +open System +open Xunit + + +[] +let ``IActorRef should be possible to use as a Key`` () = + let timeoutConfig = + """ + akka { + actor { + ask-timeout = 5s + } + } + """ + |> Configuration.parse + + let getWhateverHandler (mailbox : Actor<_>) _ = + mailbox.Sender() equals 1 \ No newline at end of file diff --git a/src/core/Akka.FSharp/FsApi.fs b/src/core/Akka.FSharp/FsApi.fs index e22338f11db..7044cc60d22 100644 --- a/src/core/Akka.FSharp/FsApi.fs +++ b/src/core/Akka.FSharp/FsApi.fs @@ -307,11 +307,7 @@ module Actors = | Func f -> match msg with | :? 'Message as m -> state <- f m - | _ -> - let serializer = UntypedActor.Context.System.Serialization.FindSerializerForType typeof :?> Akka.Serialization.NewtonSoftJsonSerializer - match Serialization.tryDeserializeJObject serializer.Serializer msg with - | Some(m) -> state <- f m - | None -> x.Unhandled msg + | _ -> x.Unhandled msg | Return _ -> x.PostStop() override x.PostStop() = base.PostStop () @@ -319,7 +315,7 @@ module Actors = /// Builds an actor message handler using an actor expression syntax. - let actor = ActorBuilder() + let actor = ActorBuilder() [] module Logging = diff --git a/src/core/Akka.FSharp/Properties/AssemblyInfo.fs b/src/core/Akka.FSharp/Properties/AssemblyInfo.fs index ce9b997f942..45ed219319b 100644 --- a/src/core/Akka.FSharp/Properties/AssemblyInfo.fs +++ b/src/core/Akka.FSharp/Properties/AssemblyInfo.fs @@ -10,9 +10,9 @@ open System.Runtime.InteropServices [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = - let [] Version = "1.0.4.0" + let [] Version = "1.0.5.0" diff --git a/src/core/Akka.MultiNodeTestRunner.Shared.Tests/Akka.MultiNodeTestRunner.Shared.Tests.csproj b/src/core/Akka.MultiNodeTestRunner.Shared.Tests/Akka.MultiNodeTestRunner.Shared.Tests.csproj index 7de85e8c081..bfb895e3fce 100644 --- a/src/core/Akka.MultiNodeTestRunner.Shared.Tests/Akka.MultiNodeTestRunner.Shared.Tests.csproj +++ b/src/core/Akka.MultiNodeTestRunner.Shared.Tests/Akka.MultiNodeTestRunner.Shared.Tests.csproj @@ -14,7 +14,8 @@ 512 ..\..\ true - 46f5f680 + + true @@ -34,22 +35,24 @@ 4 - + ..\..\packages\faker-csharp.1.2.0\lib\net4\Faker.dll + True - False ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True - False ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True - + ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True @@ -89,6 +92,7 @@ + diff --git a/src/core/Akka.MultiNodeTestRunner.Shared.Tests/app.config b/src/core/Akka.MultiNodeTestRunner.Shared.Tests/app.config new file mode 100644 index 00000000000..f0bd3189cf4 --- /dev/null +++ b/src/core/Akka.MultiNodeTestRunner.Shared.Tests/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Akka.MultiNodeTestRunner.Shared.csproj b/src/core/Akka.MultiNodeTestRunner.Shared/Akka.MultiNodeTestRunner.Shared.csproj index 030790c905d..dc72f3b9170 100644 --- a/src/core/Akka.MultiNodeTestRunner.Shared/Akka.MultiNodeTestRunner.Shared.csproj +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Akka.MultiNodeTestRunner.Shared.csproj @@ -22,6 +22,7 @@ DEBUG;TRACE prompt 4 + 5 pdbonly @@ -43,8 +44,20 @@ + + + + + + + + + True + True + VisualizerRuntimeTemplate.tt + @@ -69,6 +82,15 @@ + + + + + + TextTemplatingFilePreprocessor + VisualizerRuntimeTemplate.cs + + diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Akka.MultiNodeTestRunner.Shared.csproj.DotSettings b/src/core/Akka.MultiNodeTestRunner.Shared/Akka.MultiNodeTestRunner.Shared.csproj.DotSettings new file mode 100644 index 00000000000..662f95686eb --- /dev/null +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Akka.MultiNodeTestRunner.Shared.csproj.DotSettings @@ -0,0 +1,2 @@ + + CSharp50 \ No newline at end of file diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/NodeTest.cs b/src/core/Akka.MultiNodeTestRunner.Shared/NodeTest.cs index 8c30218f9eb..509f763aa6e 100644 --- a/src/core/Akka.MultiNodeTestRunner.Shared/NodeTest.cs +++ b/src/core/Akka.MultiNodeTestRunner.Shared/NodeTest.cs @@ -13,6 +13,7 @@ public class NodeTest public string TestName { get; set; } public string TypeName { get; set; } public string MethodName { get; set; } + public string SkipReason { get; set; } } } diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/EnumerableExtensions.cs b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/EnumerableExtensions.cs new file mode 100644 index 00000000000..2e0ffdcd996 --- /dev/null +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/EnumerableExtensions.cs @@ -0,0 +1,22 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2015 Akka.NET project +// +// ----------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Akka.MultiNodeTestRunner.Shared.Persistence +{ + public static class EnumerableExtensions + { + public static IEnumerable Concat(this IEnumerable source, T item) + { + foreach (var cur in source) + { + yield return cur; + } + yield return item; + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/FileNameGenerator.cs b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/FileNameGenerator.cs new file mode 100644 index 00000000000..2b6062cffe8 --- /dev/null +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/FileNameGenerator.cs @@ -0,0 +1,32 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2015 Akka.NET project +// +// ----------------------------------------------------------------------- + +using System; +using System.IO; + +namespace Akka.MultiNodeTestRunner.Shared.Persistence +{ + public class FileNameGenerator + { + public static string GenerateFileName(string assemblyName, string fileExtension) + { + return GenerateFileName(assemblyName, fileExtension, DateTime.UtcNow); + } + + public static string GenerateFileName(string assemblyName, string fileExtension, DateTime utcNow) + { + return string.Format("{0}-{1}{2}", assemblyName.Replace(".dll", ""), utcNow.Ticks, fileExtension); + } + + public static string GenerateFileName(string folderPath, string assemblyName, string fileExtension, DateTime utcNow) + { + if(string.IsNullOrEmpty(folderPath)) + return GenerateFileName(assemblyName, fileExtension, utcNow); + var assemblyNameOnly = Path.GetFileName(assemblyName); + return Path.Combine(folderPath, GenerateFileName(assemblyNameOnly, fileExtension, utcNow)); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/IPersistentTestRunStore.cs b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/IPersistentTestRunStore.cs index 1c7e4a152fc..dde84469d5e 100644 --- a/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/IPersistentTestRunStore.cs +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/IPersistentTestRunStore.cs @@ -10,16 +10,12 @@ namespace Akka.MultiNodeTestRunner.Shared.Persistence { /// - /// Persistent store for saving and retrieving instances + /// Persistent store for saving instances /// from disk. /// public interface IPersistentTestRunStore { bool SaveTestRun(string filePath, TestRunTree data); - - bool TestRunExists(string filePath); - - TestRunTree FetchTestRun(string filePath); } } diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/IRetrievableTestRunStore.cs b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/IRetrievableTestRunStore.cs new file mode 100644 index 00000000000..b6844b3939d --- /dev/null +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/IRetrievableTestRunStore.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2015 Akka.NET project +// +// ----------------------------------------------------------------------- + +using Akka.MultiNodeTestRunner.Shared.Reporting; + +namespace Akka.MultiNodeTestRunner.Shared.Persistence +{ + /// + /// Persistent store for retreiving instances + /// from disk. + /// + public interface IRetrievableTestRunStore :IPersistentTestRunStore + { + bool TestRunExists(string filePath); + + TestRunTree FetchTestRun(string filePath); + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/JsonPersistentTestRunStore.cs b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/JsonPersistentTestRunStore.cs index bd6826fb767..484803f9078 100644 --- a/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/JsonPersistentTestRunStore.cs +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/JsonPersistentTestRunStore.cs @@ -16,9 +16,9 @@ namespace Akka.MultiNodeTestRunner.Shared.Persistence { /// - /// JavaScript Object Notation (JSON) implementation of the + /// JavaScript Object Notation (JSON) implementation of the /// - public class JsonPersistentTestRunStore : IPersistentTestRunStore + public class JsonPersistentTestRunStore : IRetrievableTestRunStore { //Internal version of the contract resolver private class AkkaContractResolver : DefaultContractResolver diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/TimelineItem.cs b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/TimelineItem.cs new file mode 100644 index 00000000000..dfa3cfe44fe --- /dev/null +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/TimelineItem.cs @@ -0,0 +1,38 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2015 Akka.NET project +// +// ----------------------------------------------------------------------- +using System; + +namespace Akka.MultiNodeTestRunner.Shared.Persistence +{ + public class TimelineItem + { + private const string EventFormat = "{{ className:'{0}', content:'{1}', start:'{2}', group:{3}, title:'{4}' }}"; + + public TimelineItem(string cssClass, string content, string title, DateTime dateTime, int groupId) + { + Classname = cssClass; + Content = content; + Start = dateTime; + GroupId = groupId; + Title = title; + } + + public string Classname { get; private set; } + + public string Content { get; private set; } + + public string Title { get; private set; } + + public DateTime Start { get; private set; } + + public int GroupId { get; private set; } + + public string ToJavascriptString() + { + return string.Format(EventFormat, Classname, Content, Start.ToString("o"), GroupId, Title); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/TimelineItemFactory.cs b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/TimelineItemFactory.cs new file mode 100644 index 00000000000..f695aa7e3c5 --- /dev/null +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/TimelineItemFactory.cs @@ -0,0 +1,49 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2015 Akka.NET project +// +// ----------------------------------------------------------------------- +using System; + +namespace Akka.MultiNodeTestRunner.Shared.Persistence +{ + public static class TimelineItemFactory + { + private static readonly string[] CssClasses = + { + "vis-item-one", + "vis-item-two", + "vis-item-three", + "vis-item-four", + "vis-item-five", + "vis-item-six", + "vis-item-seven", + "vis-item-eight", + "vis-item-nine", + "vis-item-ten", + "vis-item-eleven", + "vis-item-twelve", + "vis-item-thirteen", + "vis-item-fourteen", + "vis-item-fifteen" + }; + + private static readonly string passedTestContent = @"
"; + + public static TimelineItem CreateSpecMessage(string prefix, string title, int groupId, long startTimeStamp) + { + var content = title.Replace(prefix, string.Empty); + return new TimelineItem("timeline-message", content, title, new DateTime(startTimeStamp), groupId); + } + + public static TimelineItem CreateNodeFact(string prefix, string title, int groupId, long startTimeStamp) + { + var content = title.Replace(prefix, string.Empty); + if (title.EndsWith("PASS") || title.EndsWith("passed.")) + { + content = passedTestContent; + } + return new TimelineItem(CssClasses[startTimeStamp%15], content, title, new DateTime(startTimeStamp), groupId); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/VisualizerPersistentTestRunStore.cs b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/VisualizerPersistentTestRunStore.cs new file mode 100644 index 00000000000..f4eb62fe2e6 --- /dev/null +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/VisualizerPersistentTestRunStore.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2015 Akka.NET project +// +// ----------------------------------------------------------------------- + +using System.IO; + +using Akka.MultiNodeTestRunner.Shared.Reporting; + +namespace Akka.MultiNodeTestRunner.Shared.Persistence +{ + /// + /// Stores test run as a html page. + /// + public class VisualizerPersistentTestRunStore : IPersistentTestRunStore + { + public bool SaveTestRun(string filePath, TestRunTree data) + { + var template = new VisualizerRuntimeTemplate { Tree = data }; + var content = template.TransformText(); + var fullPath = Path.GetFullPath(filePath); + File.WriteAllText(fullPath, content); + + return true; + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/VisualizerRuntimeTemplate.Tree.cs b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/VisualizerRuntimeTemplate.Tree.cs new file mode 100644 index 00000000000..087ff7df83c --- /dev/null +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/VisualizerRuntimeTemplate.Tree.cs @@ -0,0 +1,147 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2015 Akka.NET project +// +// ----------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Linq; +using Akka.MultiNodeTestRunner.Shared.Reporting; + +namespace Akka.MultiNodeTestRunner.Shared.Persistence +{ + partial class VisualizerRuntimeTemplate + { + private TestRunTree _tree; + public string Prefix { get; private set; } + + public TestRunTree Tree + { + get { return _tree; } + set + { + _tree = value; + Prefix = LongestCommonPrefix( + value.Specs + .Select(s => s.FactName) + .ToArray()); + } + } + + public string BuildSpecificationId(FactData spec) + { + return spec.FactName.Replace(".", "_"); + } + + public string BuildTimelineItem(FactData spec) + { + var messages = spec.RunnerMessages + .Select(m => TimelineItemFactory.CreateSpecMessage(Prefix, m.Message, m.NodeIndex, m.TimeStamp)); + + var facts = + spec.NodeFacts.SelectMany( + nodeFact => + nodeFact.Value.EventStream.Select( + nodeMessage => + TimelineItemFactory.CreateNodeFact( + Prefix, + nodeMessage.Message, + nodeMessage.NodeIndex, + nodeMessage.TimeStamp))); + + var itemStrings = messages.Concat(facts) + .Select(i => i.ToJavascriptString()); + + return string.Join(",\r\n", itemStrings); + } + + public string BuildGroupItems(FactData spec) + { + var groups = spec.NodeFacts + .Select( + nf => + string.Format("{{ id:{0}, content:'Node {0}' }}", nf.Value.NodeIndex)) + .Concat(@"{ id:-1, content:'Misc' }"); + + return string.Join(",\r\n", groups); + } + + public string BuildOptions(FactData spec) + { + var events = + spec.NodeFacts.SelectMany( + nodeFact => + nodeFact.Value.EventStream + .Select( + nodeMessage => + nodeMessage.TimeStamp)) + .ToList(); + + var startEventTimeParameter = "null"; + var endEventTimeParameter = "null"; + + if (events.Count > 0) + { + var firstEventTimeStamp = events.Aggregate( + (aggregate, nextValue) => + aggregate > nextValue + ? nextValue + : aggregate); + + var lastEventTimeStamp = events.Aggregate( + (aggregate, nextValue) => + aggregate < nextValue + ? nextValue + : aggregate); + + + var startEventTime = new DateTime(firstEventTimeStamp); + var endDisplayTime = new DateTime(lastEventTimeStamp); + + // TODO: Find a better way of calculating additional time from message length + // The last message is the 3 second wait. Which is about half the delta from start to end in length. + var startEndDelta = (endDisplayTime - startEventTime).Ticks / 2; + endDisplayTime = endDisplayTime.AddTicks(startEndDelta); + + startEventTimeParameter = string.Format("'{0}'", startEventTime.ToString("o")); + endEventTimeParameter = string.Format("'{0}'", endDisplayTime.ToString("o")); + } + + + return string.Format( + "{{ start:{0}, end:{1}, align:'left', clickToUse:true }}", + startEventTimeParameter, + endEventTimeParameter); + } + + private static string LongestCommonPrefix(IReadOnlyList strings) + { + if (strings == null || strings.Count == 0) + { + return string.Empty; + } + + var commonPrefix = strings[0]; + + for (var i = 1; i < strings.Count; i++) + { + var j = 0; + for (; j < commonPrefix.Length && j < strings[i].Length; j++) + { + if (commonPrefix[j] != strings[i][j]) + { + commonPrefix = commonPrefix.Substring(0, j); + break; + } + } + + if (j == strings[i].Length) + { + commonPrefix = strings[i]; + } + } + + return commonPrefix; + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/VisualizerRuntimeTemplate.cs b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/VisualizerRuntimeTemplate.cs new file mode 100644 index 00000000000..01eab9aa223 --- /dev/null +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Persistence/VisualizerRuntimeTemplate.cs @@ -0,0 +1,425 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version: 12.0.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +namespace Akka.MultiNodeTestRunner.Shared.Persistence +{ + using System.Linq; + using System.Text; + using System.Collections.Generic; + using System; + + /// + /// Class to produce the template output + /// + + #line 1 "C:\akka\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\Persistence\VisualizerRuntimeTemplate.tt" + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "12.0.0.0")] + public partial class VisualizerRuntimeTemplate : VisualizerRuntimeTemplateBase + { +#line hidden + /// + /// Create the template output + /// + public virtual string TransformText() + { + this.Write("\r\n\r\n\r\n\t\r\n\t\r\n\t\r\n\t" + + ""); + + #line 43 "C:\akka\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\Persistence\VisualizerRuntimeTemplate.tt" + Write(Prefix); + + #line default + #line hidden + this.Write(@" + + +
+ Help / Instructions +

Click on a timeline to activate. Click off the timeline or press ESC to deactivate

+

Scroll up to zoom into an active timeline. Scroll down to zoom out of an active timeline

+

Click and hold to move an active timeline.

+
+"); + + #line 52 "C:\akka\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\Persistence\VisualizerRuntimeTemplate.tt" + foreach (var spec in Tree.Specs) { + + #line default + #line hidden + this.Write("
\r\n

"); + + #line 54 "C:\akka\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\Persistence\VisualizerRuntimeTemplate.tt" + Write(spec.FactName.Replace(Prefix, "")); + + #line default + #line hidden + this.Write("

\r\n\r\n\r\n + + + <# Write(Prefix); #> + + +
+ Help / Instructions +

Click on a timeline to activate. Click off the timeline or press ESC to deactivate

+

Scroll up to zoom into an active timeline. Scroll down to zoom out of an active timeline

+

Click and hold to move an active timeline.

+
+<# foreach (var spec in Tree.Specs) { #> +
+

<# Write(spec.FactName.Replace(Prefix, "")); #>

+
+ + +
+<# } #> + + \ No newline at end of file diff --git a/src/core/Akka.MultiNodeTestRunner.Shared/Sinks/FileSystemMessageSinkActor.cs b/src/core/Akka.MultiNodeTestRunner.Shared/Sinks/FileSystemMessageSinkActor.cs index 62e0ecd4ee5..ffec7159941 100644 --- a/src/core/Akka.MultiNodeTestRunner.Shared/Sinks/FileSystemMessageSinkActor.cs +++ b/src/core/Akka.MultiNodeTestRunner.Shared/Sinks/FileSystemMessageSinkActor.cs @@ -22,7 +22,7 @@ public FileSystemMessageSink(string assemblyName) : this( Props.Create( () => - new FileSystemMessageSinkActor(new JsonPersistentTestRunStore(), GenerateFileName(assemblyName), + new FileSystemMessageSinkActor(new JsonPersistentTestRunStore(), FileNameGenerator.GenerateFileName(assemblyName, ".json"), true))) { @@ -36,15 +36,6 @@ protected override void HandleUnknownMessageType(string message) { //do nothing } - - #region Static methods - - public static string GenerateFileName(string assemblyName) - { - return string.Format("{0}-{1}.json", assemblyName.Replace(".dll", ""), DateTime.UtcNow.Ticks); - } - - #endregion } /// diff --git a/src/core/Akka.MultiNodeTestRunner/Akka.MultiNodeTestRunner.csproj b/src/core/Akka.MultiNodeTestRunner/Akka.MultiNodeTestRunner.csproj index 70d399839a9..dd8a6c268d4 100644 --- a/src/core/Akka.MultiNodeTestRunner/Akka.MultiNodeTestRunner.csproj +++ b/src/core/Akka.MultiNodeTestRunner/Akka.MultiNodeTestRunner.csproj @@ -1,5 +1,6 @@  + Debug @@ -13,6 +14,7 @@ 512 ..\..\ true + 1b257c9f AnyCPU @@ -40,6 +42,9 @@ False ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + + ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + False ..\..\packages\xunit.runner.utility.2.1.0-beta2-build2981\lib\net35\xunit.runner.utility.desktop.dll @@ -66,10 +71,6 @@ {964F0EC5-FBE6-47C5-8AE6-145114D5DB8C} Akka.MultiNodeTestRunner.Shared - - {f0781bea-5ba0-4af0-bb15-e3f209b681f5} - Akka.MultiNodeTests - {28520F30-2868-4BD3-9CAE-AC27226C24E3} Akka.NodeTestRunner @@ -91,6 +92,7 @@ This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests.MultiNode/LookupRemoteActorMultiNetSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/LookupRemoteActorMultiNetSpec.cs new file mode 100644 index 00000000000..9f9c17006c8 --- /dev/null +++ b/src/core/Akka.Remote.Tests.MultiNode/LookupRemoteActorMultiNetSpec.cs @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.Remote.TestKit; +using Xunit; + +namespace Akka.Remote.Tests.MultiNode +{ + public class LookupRemoteActorMultiNetSpec : MultiNodeConfig + { + public RoleName Master { get; private set; } + public RoleName Slave { get; private set; } + + public LookupRemoteActorMultiNetSpec() + { + CommonConfig = DebugConfig(false); + + Master = Role("master"); + Slave = Role("slave"); + } + + public class SomeActor : UntypedActor + { + protected override void OnReceive(object message) + { + if (message is Identify) + { + Sender.Tell(Self); + } + } + } + } + + public class LookupRemoteActorMultiNetNode1 : LookupRemoteActorSpec + { + } + public class LookupRemoteActorMultiNetNode2 : LookupRemoteActorSpec + { + } + + public class LookupRemoteActorSpec : MultiNodeSpec + { + private LookupRemoteActorMultiNetSpec _config; + + public LookupRemoteActorSpec() + : this(new LookupRemoteActorMultiNetSpec()) + { + + } + public LookupRemoteActorSpec(LookupRemoteActorMultiNetSpec config) + : base(config) + { + _config = config; + } + + protected override int InitialParticipantsValueFactory + { + get + { + return Roles.Count; + } + } + + [MultiNodeFact] + public void LookupRemoteActorSpecs() + { + RunOn( + () => Sys.ActorOf("service-hello"), + _config.Master); + + RemotingMustLookupRemoteActor(); + } + + public void RemotingMustLookupRemoteActor() + { + RunOn( + () => + { + Sys.ActorSelection(Node(_config.Master) / "user" / "service-hello") + .Tell(new Identify("id1")); + var hello = ExpectMsg() + .Subject; + + Assert.IsType(hello); + + var masterAddress = TestConductor.GetAddressFor(_config.Master).Result; + + Assert.StrictEqual(masterAddress, hello.Path.Address); + }, + _config.Slave); + + EnterBarrier("done"); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests.MultiNode/NewRemoteActorSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/NewRemoteActorSpec.cs new file mode 100644 index 00000000000..d556fa6052d --- /dev/null +++ b/src/core/Akka.Remote.Tests.MultiNode/NewRemoteActorSpec.cs @@ -0,0 +1,178 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.Configuration; +using Akka.Remote.TestKit; +using Akka.TestKit; +using Xunit; + +namespace Akka.Remote.Tests.MultiNode +{ + public class NewRemoteActorMultiNodeSpecConfig : MultiNodeConfig + { + #region Internal actor classes + + public class SomeActor : UntypedActor + { + protected override void OnReceive(object message) + { + if(message.Equals("identify")) + Sender.Tell(Self); + } + } + + public class SomeActorWithParam : UntypedActor + { + private string _ignored; + + public SomeActorWithParam(string ignored) + { + _ignored = ignored; + } + + + protected override void OnReceive(object message) + { + if (message.Equals("identify")) + Sender.Tell(Self); + } + } + + #endregion + + readonly RoleName _master; + public RoleName Master { get { return _master; } } + readonly RoleName _slave; + public RoleName Slave { get { return _slave; } } + + public NewRemoteActorMultiNodeSpecConfig() + { + _master = Role("master"); + _slave = Role("slave"); + + CommonConfig = + DebugConfig(false) + .WithFallback(ConfigurationFactory.ParseString("akka.remote.log-remote-lifecycle-events = off")); + + DeployOn(Master, @" + /service-hello.remote = ""@slave@"" + /service-hello-null.remote = ""@slave@"" + /service-hello3.remote = ""@slave@"" + "); + + DeployOnAll(@"/service-hello2.remote = ""@slave@"""); + } + } + + public class NewRemoteActorSpecNode1 : NewRemoteActorSpec { } + public class NewRemoteActorSpecNode2 : NewRemoteActorSpec { } + + public class NewRemoteActorSpec : MultiNodeSpec + { + private NewRemoteActorMultiNodeSpecConfig _config; + + public NewRemoteActorSpec() + : this(new NewRemoteActorMultiNodeSpecConfig()) + { + } + + public NewRemoteActorSpec(NewRemoteActorMultiNodeSpecConfig config) : base(config) + { + _config = config; + } + + protected override int InitialParticipantsValueFactory + { + get { return Roles.Count; } + } + + protected override bool VerifySystemShutdown + { + get { return true; } + } + + [MultiNodeFact] + public void NewRemoteActorSpecs() + { + ANewRemoteActorMustBeLocallyInstantiatedOnARemoteNodeAndBeAbleToCommunicateThroughItsRemoteActorRef(); + ANewRemoteActorMustBeLocallyInstantiatedOnARemoteNodeWithNullParameterAndBeAbleToCommunicateThroughItsRemoteActorRef(); + ANewRemoteActorMustBeAbleToShutdownSystemWhenUsingRemoteDeployedActor(); + } + + public void ANewRemoteActorMustBeLocallyInstantiatedOnARemoteNodeAndBeAbleToCommunicateThroughItsRemoteActorRef() + { + RunOn(() => + { + var actor = Sys.ActorOf(Props.Create(() => new NewRemoteActorMultiNodeSpecConfig.SomeActor()), + "service-hello"); + var foo = Assert.IsType(actor); + actor.Path.Address.ShouldBe(Node(_config.Slave).Address); + + var slaveAddress = TestConductor.GetAddressFor(_config.Slave).Result; + actor.Tell("identify"); + ExpectMsg().Path.Address.ShouldBe(slaveAddress); + }, _config.Master); + + EnterBarrier("done"); + } + + public void ANewRemoteActorMustBeLocallyInstantiatedOnARemoteNodeWithNullParameterAndBeAbleToCommunicateThroughItsRemoteActorRef() + { + RunOn(() => + { + var actor = Sys.ActorOf(Props.Create(() => new NewRemoteActorMultiNodeSpecConfig.SomeActorWithParam(null)), + "service-hello2"); + var foo = Assert.IsType(actor); + actor.Path.Address.ShouldBe(Node(_config.Slave).Address); + + var slaveAddress = TestConductor.GetAddressFor(_config.Slave).Result; + actor.Tell("identify"); + ExpectMsg().Path.Address.ShouldBe(slaveAddress); + }, _config.Master); + + EnterBarrier("done"); + } + + public void ANewRemoteActorMustBeAbleToShutdownSystemWhenUsingRemoteDeployedActor() + { + Within(TimeSpan.FromSeconds(20), () => + { + RunOn(() => + { + var actor = Sys.ActorOf(Props.Create(() => new NewRemoteActorMultiNodeSpecConfig.SomeActor()), + "service-hello3"); + var foo = Assert.IsType(actor); + actor.Path.Address.ShouldBe(Node(_config.Slave).Address); + + // This watch is in race with the shutdown of the watched system. This race should remain, as the test should + // handle both cases: + // - remote system receives watch, replies with DeathWatchNotification + // - remote system never gets watch, but DeathWatch heartbeats time out, and AddressTerminated is generated + // (this needs some time to happen) + Watch(actor); + EnterBarrier("deployed"); + + // master system is supposed to be shutdown after slave + // this should be triggered by slave system shutdown + ExpectTerminated(actor); + }, _config.Master); + + RunOn(() => + { + EnterBarrier("deployed"); + }, _config.Slave); + + // Important that this is the last test. + // It should not be any barriers here. + // verifySystemShutdown = true will ensure that system shutdown is successful + VerifySystemShutdown.ShouldBeTrue("Shutdown should be verified!"); + }); + } + } +} diff --git a/src/core/Akka.Remote.Tests.MultiNode/PiercingShouldKeepQuarantineSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/PiercingShouldKeepQuarantineSpec.cs new file mode 100644 index 00000000000..375698f5f69 --- /dev/null +++ b/src/core/Akka.Remote.Tests.MultiNode/PiercingShouldKeepQuarantineSpec.cs @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Configuration; +using Akka.Remote.TestKit; + +namespace Akka.Remote.Tests.MultiNode +{ + public class PiercingShouldKeepQuarantineMultiNodeConfig : MultiNodeConfig + { + public RoleName First { get; private set; } + public RoleName Second { get; private set; } + + public PiercingShouldKeepQuarantineMultiNodeConfig() + { + First = Role("first"); + Second = Role("second"); + + CommonConfig = DebugConfig(true) + .WithFallback(ConfigurationFactory.ParseString(@" + akka.loglevel = INFO + akka.remote.log-remote-lifecycle-events = INFO + akka.remote.retry-gate-closed-for = 10s + ")); + } + } + + public class PiercingShouldKeepQuarantineMultiNode1 : PiercingShouldKeepQuarantineSpec + { + } + + public class PiercingShouldKeepQuarantineMultiNode2 : PiercingShouldKeepQuarantineSpec + { + } + + public abstract class PiercingShouldKeepQuarantineSpec : MultiNodeSpec + { + private readonly PiercingShouldKeepQuarantineMultiNodeConfig _config; + + protected PiercingShouldKeepQuarantineSpec() : this(new PiercingShouldKeepQuarantineMultiNodeConfig()) + { + } + + protected PiercingShouldKeepQuarantineSpec(PiercingShouldKeepQuarantineMultiNodeConfig config) : base(config) + { + _config = config; + } + + protected override int InitialParticipantsValueFactory + { + get { return Roles.Count; } + } + + public class Subject : UntypedActor + { + protected override void OnReceive(object message) + { + if (message.Equals("getuid")) + Sender.Tell(AddressUidExtension.Uid(Context.System)); + } + } + + [MultiNodeFact] + public void PiercingShouldKeepQuarantineSpecs() + { + WhileProbingThroughTheQuarantineRemotingMustNotLoseExistingQuarantineMarker(); + } + + public void WhileProbingThroughTheQuarantineRemotingMustNotLoseExistingQuarantineMarker() + { + RunOn(() => + { + EnterBarrier("actors-started"); + + // Communicate with second system + Sys.ActorSelection(Node(_config.Second) / "user" / "subject").Tell("getuid"); + var uid = ExpectMsg(TimeSpan.FromSeconds(10)); + EnterBarrier("actor-identified"); + + // Manually Quarantine the other system + RARP.For(Sys).Provider.Transport.Quarantine(Node(_config.Second).Address, uid); + + // Quarantining is not immedeiate + Thread.Sleep(1000); + + // Quarantine is up - Should not be able to communicate with remote system any more + for (var i = 1; i <= 4; i++) + { + Sys.ActorSelection(Node(_config.Second) / "user" / "subject").Tell("getuid"); + + ExpectNoMsg(TimeSpan.FromSeconds(2)); + } + + EnterBarrier("quarantine-intact"); + + }, _config.First); + + RunOn(() => + { + Sys.ActorOf("subject"); + EnterBarrier("actors-started"); + EnterBarrier("actor-identified"); + EnterBarrier("quarantine-intact"); + }, _config.Second); + } + } +} diff --git a/src/core/Akka.Remote.Tests.MultiNode/Properties/AssemblyInfo.cs b/src/core/Akka.Remote.Tests.MultiNode/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..f27b1862c05 --- /dev/null +++ b/src/core/Akka.Remote.Tests.MultiNode/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("Akka.Remote.Tests.MultiNode")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Akka.Remote.Tests.MultiNode")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[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("8b3e94f7-0f92-4955-93ab-93bdd9d2fa34")] + +// 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/src/core/Akka.Remote.Tests.MultiNode/RemoteDeliverySpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteDeliverySpec.cs new file mode 100644 index 00000000000..d820a161e90 --- /dev/null +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteDeliverySpec.cs @@ -0,0 +1,138 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using Akka.Actor; +using Akka.Remote.TestKit; + +namespace Akka.Remote.Tests.MultiNode +{ + public class RemoteDeliveryMultiNetSpec : MultiNodeConfig + { + public RemoteDeliveryMultiNetSpec() + { + First = Role("first"); + Second = Role("second"); + Third = Role("third"); + + CommonConfig = DebugConfig(false); + } + + public RoleName First { get; private set; } + public RoleName Second { get; private set; } + public RoleName Third { get; private set; } + + public sealed class Letter + { + public Letter(int n, List route) + { + N = n; + Route = route; + } + + public int N { get; private set; } + public List Route { get; private set; } + } + + public class Postman : UntypedActor + { + protected override void OnReceive(object message) + { + var letter = message as Letter; + if (letter != null) + { + letter.Route[0].Tell(new Letter(letter.N, letter.Route.Skip(1).ToList())); + } + } + } + } + + public class RemoteDeliveryMultiNetNode1 : RemoteDeliverySpec + { + } + + public class RemoteDeliveryMultiNetNode2 : RemoteDeliverySpec + { + } + + public class RemoteDeliveryMultiNetNode3 : RemoteDeliverySpec + { + } + + public class RemoteDeliverySpec : MultiNodeSpec + { + private readonly RemoteDeliveryMultiNetSpec _config; + private readonly Func _identify; + + protected RemoteDeliverySpec() : this(new RemoteDeliveryMultiNetSpec()) + { + } + + protected RemoteDeliverySpec(RemoteDeliveryMultiNetSpec config) : base(config) + { + _config = config; + + _identify = (role, actorName) => Within(TimeSpan.FromSeconds(10), () => + { + Sys.ActorSelection(Node(role)/"user"/actorName) + .Tell(new Identify(actorName)); + return ExpectMsg() + .Subject; + }); + } + + protected override int InitialParticipantsValueFactory + { + get + { + return Roles.Count; + } + } + + [MultiNodeFact] + public void Remoting_with_TCP_must_not_drop_messages_under_normal_circumstances() + { + Sys.ActorOf("postman-" + Myself.Name); + EnterBarrier("actors-started"); + + RunOn(() => + { + var p1 = _identify(_config.First, "postman-first"); + var p2 = _identify(_config.Second, "postman-second"); + var p3 = _identify(_config.Third, "postman-third"); + var route = new List + { + p2, + p3, + p2, + p3, + TestActor + }; + + for (var n = 1; n <= 500; n++) + { + p1.Tell(new RemoteDeliveryMultiNetSpec.Letter(n, route)); + var letterNumber = n; + ExpectMsg( + letter => letter.N == letterNumber && letter.Route.Count == 0, + TimeSpan.FromSeconds(5)); + + // in case the loop count is increased it is good with some progress feedback + if (n%10000 == 0) + { + Log.Info("Passed [{0}]", n); + } + } + }, + _config.First); + + EnterBarrier("after-1"); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteDeploymentDeathWatchSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteDeploymentDeathWatchSpec.cs new file mode 100644 index 00000000000..6553b4a41f6 --- /dev/null +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteDeploymentDeathWatchSpec.cs @@ -0,0 +1,160 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Threading; +using Akka.Actor; +using Akka.Configuration; +using Akka.Remote.TestKit; +using Akka.TestKit; +using Akka.TestKit.Xunit2; +using Xunit; + +namespace Akka.Remote.Tests.MultiNode +{ + public abstract class RemoteDeploymentDeathWatchSpec : MultiNodeSpec + { + private readonly RemoteDeploymentDeathWatchSpecConfig _specConfig; + private readonly ITestKitAssertions _assertions; + + protected RemoteDeploymentDeathWatchSpec() + : this(new RemoteDeploymentDeathWatchSpecConfig()) + { + } + + protected RemoteDeploymentDeathWatchSpec(RemoteDeploymentDeathWatchSpecConfig specConfig) + : base(specConfig) + { + _specConfig = specConfig; + _assertions = new XunitAssertions(); + } + + protected override int InitialParticipantsValueFactory + { + get { return Roles.Count; } + } + + // Possible to override to let them heartbeat for a while. + protected virtual void Sleep() + { + } + + + protected virtual string Scenario + { + get { return string.Empty; } + } + + [MultiNodeFact] + public void AnActorSystemThatDeploysActorsOnAnotherNodeMustBeAbleToShutdownWhenRemoteNodeCrash() + { + RunOn(() => + { + var hello = Sys.ActorOf(Props.Create(() => new Hello()), "hello"); + hello.Path.Address.ShouldBe(Node(_specConfig.Third).Address); + EnterBarrier("hello-deployed"); + EnterBarrier("third-crashed"); + Sleep(); + + // if the remote deployed actor is not removed the system will not shutdown + var timeOut = RemainingOrDefault; + try + { + Sys.AwaitTermination(timeOut); + } + catch (TimeoutException ex) + { + //TODO: add printTree + + _assertions.Fail("Failed to stop {0} within {1} ", Sys.Name, timeOut); + } + }, _specConfig.Second); + + RunOn(() => + { + EnterBarrier("hello-deployed"); + EnterBarrier("third-crashed"); + }, _specConfig.Third); + + RunOn(() => + { + EnterBarrier("hello-deployed"); + Sleep(); + TestConductor.Exit(_specConfig.Third, 0).GetAwaiter().GetResult(); + EnterBarrier("third-crashed"); + + //second system will be shutdown + TestConductor.Shutdown(_specConfig.Second).GetAwaiter().GetResult(); + + EnterBarrier("after-3"); + }, _specConfig.First); + } + + internal class Hello : UntypedActor + { + protected override void OnReceive(object message) + { + } + } + } + + #region Several different variations of the test + public class RemoteDeploymentDeathWatchMultiNode1 : RemoteDeploymentDeathWatchSpec + { + } + + public class RemoteDeploymentDeathWatchMultiNode2 : RemoteDeploymentDeathWatchSpec + { + } + + public class RemoteDeploymentDeathWatchMultiNode3 : RemoteDeploymentDeathWatchSpec + { + } + + public class RemoteDeploymentNodeDeathWatchSlowSpec : RemoteDeploymentDeathWatchSpec + { + protected override void Sleep() + { + Thread.Sleep(3000); + } + + protected override string Scenario + { + get { return "slow"; } + } + } + + public class RemoteDeploymentNodeDeathWatchFastSpec : RemoteDeploymentDeathWatchSpec + { + protected override string Scenario + { + get { return "fast"; } + } + } + #endregion + + public class RemoteDeploymentDeathWatchSpecConfig : MultiNodeConfig + { + public RemoteDeploymentDeathWatchSpecConfig() : base() + { + First = Role("first"); + Second = Role("second"); + Third = Role("third"); + + CommonConfig = new Config(DebugConfig(false), ConfigurationFactory.ParseString( + @"akka.loglevel = INFO + akka.remote.log-remote-lifecycle-events = off" + )); + + DeployOn(Second, @"/hello.remote = ""@third@"""); + } + + public RoleName First { get; private set; } + public RoleName Second { get; private set; } + public RoleName Third { get; private set; } + } +} \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteRandomSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteRandomSpec.cs new file mode 100644 index 00000000000..c477e04b8a1 --- /dev/null +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteRandomSpec.cs @@ -0,0 +1,150 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Remote.TestKit; +using Akka.Routing; +using Akka.TestKit; +using Akka.Util.Internal; +using Xunit; + +namespace Akka.Remote.Tests.MultiNode +{ + public class RemoteRandomMultiNodeConfig : MultiNodeConfig + { + public RoleName First { get; private set; } + public RoleName Second { get; private set; } + public RoleName Third { get; private set; } + public RoleName Fourth { get; private set; } + + public RemoteRandomMultiNodeConfig() + { + First = Role("first"); + Second = Role("second"); + Third = Role("third"); + Fourth = Role("fourth"); + + CommonConfig = DebugConfig(false); + + DeployOnAll(@" + /service-hello { + router = ""random-pool"" + nr-of-instances = 3 + target.nodes = [""@first@"", ""@second@"", ""@third@""] + } + "); + } + } + + public class RemoteRandomMultiNode1 : RemoteRandomSpec + { + } + + public class RemoteRandomMultiNode2 : RemoteRandomSpec + { + } + + public class RemoteRandomMultiNode3 : RemoteRandomSpec + { + } + + public class RemoteRandomMultiNode4 : RemoteRandomSpec + { + } + + public abstract class RemoteRandomSpec : MultiNodeSpec + { + private readonly RemoteRandomMultiNodeConfig _config; + + protected RemoteRandomSpec() : this(new RemoteRandomMultiNodeConfig()) + { + } + + protected RemoteRandomSpec(RemoteRandomMultiNodeConfig config) : base(config) + { + _config = config; + } + + protected override int InitialParticipantsValueFactory + { + get { return Roles.Count; } + } + + public class SomeActor : UntypedActor + { + protected override void OnReceive(object message) + { + if (message.Equals("hit")) + { + Sender.Tell(Self); + } + } + } + + [MultiNodeFact] + public void RemoteRandomSpecs() + { + ARemoteRandomPoolMustBeLocallyInstantiatedOnARemoteNodeAndBeAbleToCommunicateThroughItsRemoteActorRef(); + } + + public void + ARemoteRandomPoolMustBeLocallyInstantiatedOnARemoteNodeAndBeAbleToCommunicateThroughItsRemoteActorRef() + { + RunOn(() => { EnterBarrier("start", "broadcast-end", "end", "done"); }, + _config.First, _config.Second, _config.Third); + + var runOnFourth = new Action(() => + { + EnterBarrier("start"); + var actor = Sys.ActorOf(new RandomPool(nrOfInstances: 0) + .Props(Props.Create()), "service-hello"); + + Assert.IsType(actor); + + var connectionCount = 3; + var iterationCount = 100; + + for (var i = 0; i < iterationCount; i++) + for (var k = 0; k < connectionCount; k++) + actor.Tell("hit"); + + var replies = ReceiveWhile(TimeSpan.FromSeconds(5), x => + { + if (x is IActorRef) return x.AsInstanceOf().Path.Address; + return null; + }, connectionCount * iterationCount) + .Aggregate(ImmutableDictionary.Empty + .Add(Node(_config.First).Address, 0) + .Add(Node(_config.Second).Address, 0) + .Add(Node(_config.Third).Address, 0), + (map, address) => + { + var previous = map[address]; + return map.Remove(address).Add(address, previous + 1); + }); + + EnterBarrier("broadcast-end"); + actor.Tell(new Broadcast(PoisonPill.Instance)); + + EnterBarrier("end"); + // since it's random we can't be too strict in the assert + replies.Values.Count(x => x > 0).ShouldBeGreaterThan(connectionCount - 2); + Assert.False(replies.ContainsKey(Node(_config.Fourth).Address)); + + Sys.Stop(actor); + EnterBarrier("done"); + }); + RunOn(runOnFourth, _config.Fourth); + } + } +} diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteRoundRobinSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteRoundRobinSpec.cs new file mode 100644 index 00000000000..4f7495a1c18 --- /dev/null +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteRoundRobinSpec.cs @@ -0,0 +1,282 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Akka.Actor; +using Akka.Remote.TestKit; +using Akka.Routing; +using Akka.TestKit; +using Akka.TestKit.TestActors; +using Akka.Util.Internal; +using Xunit; + +namespace Akka.Remote.Tests.MultiNode +{ + public class RoundRobinMultiNodeConfig : MultiNodeConfig + { + public RoleName First { get; private set; } + public RoleName Second { get; private set; } + public RoleName Third { get; private set; } + public RoleName Fourth { get; private set; } + + public RoundRobinMultiNodeConfig() + { + First = Role("first"); + Second = Role("second"); + Third = Role("third"); + Fourth = Role("fourth"); + + CommonConfig = DebugConfig(false); + + DeployOnAll(@" + /service-hello { + router = round-robin-pool + nr-of-instances = 3 + target.nodes = [""@first@"", ""@second@"", ""@third@""] + } + /service-hello2 { + router = round-robin-pool + target.nodes = [""@first@"", ""@second@"", ""@third@""] + } + /service-hello3 { + router = round-robin-group + routees.paths = [ + ""@first@/user/target-first"", + ""@second@/user/target-second"", + ""@third@/user/target-third""] + } + "); + } + } + + public class RemoteRoundRobinMultiNode1 : RemoteRoundRobinSpec + { + } + + public class RemoteRoundRobinMultiNode2 : RemoteRoundRobinSpec + { + } + + public class RemoteRoundRobinMultiNode3 : RemoteRoundRobinSpec + { + } + + public class RemoteRoundRobinMultiNode4 : RemoteRoundRobinSpec + { + } + + public abstract class RemoteRoundRobinSpec : MultiNodeSpec + { + private readonly RoundRobinMultiNodeConfig _config; + + protected RemoteRoundRobinSpec() : this(new RoundRobinMultiNodeConfig()) + { + } + + protected RemoteRoundRobinSpec(RoundRobinMultiNodeConfig config) : base(config) + { + _config = config; + } + + protected override int InitialParticipantsValueFactory + { + get { return Roles.Count; } + } + + public class SomeActor : UntypedActor + { + protected override void OnReceive(object message) + { + if (message.Equals("hit")) + { + Sender.Tell(Self); + } + } + } + + public class TestResizer : Resizer + { + public override bool IsTimeForResize(long messageCounter) + { + return messageCounter <= 10; + } + + public override int Resize(IEnumerable currentRoutees) + { + return 1; + } + } + + [MultiNodeFact] + public void RemoteRoundRobinSpecs() + { + ARemoteRoundRobinMustBeLocallyInstantiatedOnARemoteNodeAndBeAbleToCommunicateThroughItsRemoteActorRef(); + + /* + Test is commented out until it is no longer flaky (issue1311 https://github.com/akkadotnet/akka.net/issues/1311). + */ + //ARemoteRoundRobinPoolWithResizerMustBeLocallyInstantiatedOnARemoteNodeAfterSeveralResizeRounds(); + + ARemoteRoundRobinGroupMustSendMessagesWithActorSelectionToRemotePaths(); + } + + public void + ARemoteRoundRobinMustBeLocallyInstantiatedOnARemoteNodeAndBeAbleToCommunicateThroughItsRemoteActorRef() + { + RunOn(() => { EnterBarrier("start", "broadcast-end", "end"); }, + _config.First, _config.Second, _config.Third); + + var runOnFourth = new Action(() => + { + EnterBarrier("start"); + var actor = Sys.ActorOf(new RoundRobinPool(nrOfInstances: 0) + .Props(Props.Create()), "service-hello"); + + Assert.IsType(actor); + + var connectionCount = 3; + var iterationCount = 10; + + for (var i = 0; i < iterationCount; i++) + for (var k = 0; k < connectionCount; k++) + actor.Tell("hit"); + + var replies = ReceiveWhile(TimeSpan.FromSeconds(5), x => + { + if (x is IActorRef) return x.AsInstanceOf().Path.Address; + return null; + }, connectionCount*iterationCount) + .Aggregate(ImmutableDictionary.Empty + .Add(Node(_config.First).Address, 0) + .Add(Node(_config.Second).Address, 0) + .Add(Node(_config.Third).Address, 0), + (map, address) => + { + var previous = map[address]; + return map.Remove(address).Add(address, previous + 1); + }); + + + EnterBarrier("broadcast-end"); + actor.Tell(new Broadcast(PoisonPill.Instance)); + + EnterBarrier("end"); + replies.Values.ForEach(x => Assert.Equal(x, iterationCount)); + Assert.False(replies.ContainsKey(Node(_config.Fourth).Address)); + + Sys.Stop(actor); + }); + + RunOn(runOnFourth, _config.Fourth); + EnterBarrier("done"); + } + + public void ARemoteRoundRobinPoolWithResizerMustBeLocallyInstantiatedOnARemoteNodeAfterSeveralResizeRounds() + { + Within(TimeSpan.FromSeconds(10), () => + { + RunOn(() => { EnterBarrier("start", "broadcast-end", "end"); }, + _config.First, _config.Second, _config.Third); + + var runOnFourth = new Action(() => + { + EnterBarrier("start"); + var actor = Sys.ActorOf(new RoundRobinPool( + nrOfInstances: 1, + resizer: new TestResizer() + ).Props(Props.Create()), "service-hello2"); + + Assert.IsType(actor); + + actor.Tell(RouterMessage.GetRoutees); + ExpectMsg().Members.Count().ShouldBe(2); + + var repliesFrom = Enumerable.Range(3, 7).Select(n => + { + //each message triggers a resize, incrementing number of routees with 1 + actor.Tell("hit"); + var routees = actor.AskAndWait(RouterMessage.GetRoutees, TimeSpan.FromSeconds(5)); + routees.Members.Count().ShouldBe(n); + return ExpectMsg(); + }).ToImmutableHashSet(); + + EnterBarrier("broadcast-end"); + actor.Tell(new Broadcast(PoisonPill.Instance)); + + EnterBarrier("end"); + Assert.Equal(repliesFrom.Count, 7); + var repliesFromAddresses = repliesFrom.Select(x => x.Path.Address).Distinct(); + var expectedAddresses = new List + { + Node(_config.First), + Node(_config.Second), + Node(_config.Third) + } + .Select(x => x.Address); + + // check if they have same elements (ignoring order) + Assert.All(repliesFromAddresses, x => expectedAddresses.Contains(x)); + Assert.True(repliesFromAddresses.Count() == expectedAddresses.Count()); + + Sys.Stop(actor); + }); + + RunOn(runOnFourth, _config.Fourth); + EnterBarrier("done"); + }); + } + + public void ARemoteRoundRobinGroupMustSendMessagesWithActorSelectionToRemotePaths() + { + RunOn(() => + { + Sys.ActorOf(name: "target-" + Myself.Name); + EnterBarrier("start", "end"); + }, _config.First, _config.Second, _config.Third); + + var runOnFourth = new Action(() => + { + EnterBarrier("start"); + var actor = Sys.ActorOf(Props.Create().WithRouter(FromConfig.Instance), "service-hello3"); + + Assert.IsType(actor); + + var connectionCount = 3; + var iterationCount = 10; + + for (var i = 0; i < iterationCount; i++) + for (var k = 0; k < connectionCount; k++) + actor.Tell("hit"); + + var replies = ReceiveWhile(TimeSpan.FromSeconds(5), x => + { + if (x is IActorRef) return x.AsInstanceOf().Path.Address; + return null; + }, connectionCount*iterationCount) + .Aggregate(ImmutableDictionary.Empty + .Add(Node(_config.First).Address, 0) + .Add(Node(_config.Second).Address, 0) + .Add(Node(_config.Third).Address, 0), + (map, address) => + { + var previous = map[address]; + return map.Remove(address).Add(address, previous + 1); + }); + + EnterBarrier("end"); + replies.Values.ForEach(x => Assert.Equal(x, iterationCount)); + Assert.False(replies.ContainsKey(Node(_config.Fourth).Address)); + }); + + RunOn(runOnFourth, _config.Fourth); + EnterBarrier("done"); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests.MultiNode/app.config b/src/core/Akka.Remote.Tests.MultiNode/app.config new file mode 100644 index 00000000000..f0bd3189cf4 --- /dev/null +++ b/src/core/Akka.Remote.Tests.MultiNode/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests.MultiNode/packages.config b/src/core/Akka.Remote.Tests.MultiNode/packages.config new file mode 100644 index 00000000000..15502b4d7f9 --- /dev/null +++ b/src/core/Akka.Remote.Tests.MultiNode/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests/Akka.Remote.Tests.csproj b/src/core/Akka.Remote.Tests/Akka.Remote.Tests.csproj index 6f71f7d3f51..859cd12742b 100644 --- a/src/core/Akka.Remote.Tests/Akka.Remote.Tests.csproj +++ b/src/core/Akka.Remote.Tests/Akka.Remote.Tests.csproj @@ -13,7 +13,8 @@ 512 ..\..\ true - 338f57d2 + + true @@ -54,21 +55,34 @@ false - - + + ..\..\packages\FluentAssertions.3.3.0\lib\net45\FluentAssertions.dll + + + False ..\..\packages\Google.ProtocolBuffers.2.4.1.521\lib\net40\Google.ProtocolBuffers.dll + True - + + False ..\..\packages\Google.ProtocolBuffers.2.4.1.521\lib\net40\Google.ProtocolBuffers.Serialization.dll + True - + + + False ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True - + + False ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True - + + False ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True @@ -89,6 +103,7 @@ + @@ -125,10 +140,18 @@ + + + + + + + + @@ -150,12 +173,6 @@ - - - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests/CHANGES.txt b/src/core/Akka.Remote.Tests/CHANGES.txt new file mode 100644 index 00000000000..d80368c78f2 --- /dev/null +++ b/src/core/Akka.Remote.Tests/CHANGES.txt @@ -0,0 +1,99 @@ +=============================================================================== +Welcome to the C# port of Google Protocol Buffers, written by Jon Skeet +(skeet@pobox.com) based on the work of many talented people. + +For more information about this port, visit its homepage: +http://protobuf-csharp-port.googlecode.com + +For more information about Protocol Buffers in general, visit the project page +for the C++, Java and Python project: +http://protobuf.googlecode.com +=============================================================================== +RELEASE NOTES - Version 2.4.1.473 +=============================================================================== + +Features: +- Added option service_generator_type to control service generation with + NONE, GENERIC, INTERFACE, or IRPCDISPATCH +- Added interfaces IRpcDispatch and IRpcServerStub to provide for blocking + services and implementations. +- Added ProtoGen.exe command-line argument "--protoc_dir=" to specify the + location of protoc.exe. +- Extracted interfaces for ICodedInputStream and ICodedOutputStream to allow + custom implementation of writers with both speed and size optimizations. +- Addition of the "Google.ProtoBuffers.Serialization" assembly to support + reading and writing messages to/from XML, JSON, IDictionary<,> and others. +- Several performance related fixes and tweeks +- Issue 3: Add option to mark generated code with attribute +- Issue 20: Support for decorating classes [Serializable] +- Issue 21: Decorate fields with [deprecated=true] as [System.Obsolete] +- Issue 22: Reusable Builder classes +- Issue 24: Support for using Json/Xml formats with ICodedInputStream +- Issue 25: Added support for NuGet packages +- Issue 31: Upgraded protoc.exe and descriptor to 2.4.1 + +Fixes: +- Issue 13: Message with Field same name as message causes uncompilable .cs +- Issue 16: Does not integrate well with other tooling +- Issue 19: Support for negative enum values +- Issue 26: AddRange in GeneratedBuilder iterates twice. +- Issue 27: Remove XML documentation output from test projects to clear + warnings/errors. +- Issue 28: Circular message dependencies result in null default values for + Message fields. +- Issue 29: Message classes generated have a public default constructor. You + can disable private ctor generation with the option generate_private_ctor. +- Issue 35: Fixed a bug in ProtoGen handling of arguments with trailing \ +- Big-endian support for float, and double on Silverlight +- Packed and Unpacked parsing allow for all repeated, as per version 2.3 +- Fix for leaving Builder a public ctor on internal classes for use with + generic "where T: new()" constraints. + +Other: +- Changed the code signing key to a privately held key +- Reformatted all code and line-endings to C# defaults +- Reworking of performance benchmarks to produce reliable results, option /v2 +- Issue 34: Silverlight assemblies are now unit tested + +=============================================================================== +RELEASE NOTES - Version 2.3.0.277 +=============================================================================== + +Features: +- Added cls_compliance option to generate attributes indicating + non-CLS-compliance. +- Added file_extension option to control the generated output file's extension. +- Added umbrella_namespace option to place the umbrella class into a nested + namespace to address issues with proto files having the same name as a + message it contains. +- Added output_directory option to set the output path for the source file(s). +- Added ignore_google_protobuf option to avoid generating code for includes + from the google.protobuf package. +- Added the LITE framework (Google.ProtoBuffersLite.dll) and the ability to + generate code with "option optimize_for = LITE_RUNTIME;". +- Added ability to invoke protoc.exe from within ProtoGen.exe. +- Upgraded to protoc.exe (2.3) compiler. + +Fixes: +- Issue 9: Class cannot be static and sealed error +- Issue 12: default value for enumerate fields must be filled out + +Other: +- Rewrite of build using MSBbuild instead of NAnt +- Moved to NUnit Version 2.2.8.0 +- Changed to using secure .snk for releases + +=============================================================================== +RELEASE NOTES - Version 0.9.1 +=============================================================================== + +Fixes: +- issue 10: Incorrect encoding of packed fields when serialized + +=============================================================================== +RELEASE NOTES - Version 0.9.0 +=============================================================================== + +- Initial release + +=============================================================================== \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests/RemoteConsistentHashingRouterSpec.cs b/src/core/Akka.Remote.Tests/RemoteConsistentHashingRouterSpec.cs new file mode 100644 index 00000000000..08c76abd4f6 --- /dev/null +++ b/src/core/Akka.Remote.Tests/RemoteConsistentHashingRouterSpec.cs @@ -0,0 +1,50 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; +using Akka.Actor; +using Akka.Routing; +using Akka.TestKit; +using FluentAssertions; +using Xunit; + +namespace Akka.Remote.Tests +{ + + public class RemoteConsistentHashingRouterSpec : AkkaSpec + { + + public RemoteConsistentHashingRouterSpec() + : base(@"akka.actor.provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote""") + { + + } + + [Fact] + public void ConsistentHashingGroup_must_use_same_hash_ring_indepenent_of_self_address() + { + // simulating running router on two different nodes (a1, a2) with target routees on 3 other nodes (s1, s2, s3) + var a1 = new Address("akka.tcp", "Sys", "client1", 2552); + var a2 = new Address("akka.tcp", "Sys", "client2", 2552); + var s1 = new ActorSelectionRoutee(Sys.ActorSelection("akka.tcp://Sys@server1:2552/user/a/b")); + var s2 = new ActorSelectionRoutee(Sys.ActorSelection("akka.tcp://Sys@server2:2552/user/a/b")); + var s3 = new ActorSelectionRoutee(Sys.ActorSelection("akka.tcp://Sys@server3:2552/user/a/b")); + var nodes1 = new List(new [] { new ConsistentRoutee(s1, a1), new ConsistentRoutee(s2, a1), new ConsistentRoutee(s3, a1)}); + var nodes2 = + new List(new[] + {new ConsistentRoutee(s1, a2), new ConsistentRoutee(s2, a2), new ConsistentRoutee(s3, a2)}); + var consistentHash1 = ConsistentHash.Create(nodes1, 10); + var consistentHash2 = ConsistentHash.Create(nodes2, 10); + var keys = new List(new [] { "A", "B", "C", "D", "E", "F", "G"}); + var result1 = keys.Select(k => consistentHash1.NodeFor(k).Routee); + var result2 = keys.Select(k => consistentHash2.NodeFor(k).Routee); + result1.ShouldBeEquivalentTo(result2); + } + } +} + diff --git a/src/core/Akka.Remote.Tests/RemotingSpec.cs b/src/core/Akka.Remote.Tests/RemotingSpec.cs index db98979edf7..3814a24ed0d 100644 --- a/src/core/Akka.Remote.Tests/RemotingSpec.cs +++ b/src/core/Akka.Remote.Tests/RemotingSpec.cs @@ -70,7 +70,7 @@ private static string GetConfig() applied-adapters = [] registry-key = aX33k0jWKg local-address = ""test://RemotingSpec@localhost:12345"" - maximum-payload-bytes = 32000 bytes + maximum-payload-bytes = 32000b scheme-identifier = test } } @@ -117,7 +117,7 @@ protected string GetOtherRemoteSysConfig() applied-adapters = [] registry-key = aX33k0jWKg local-address = ""test://remote-sys@localhost:12346"" - maximum-payload-bytes = 48000 bytes + maximum-payload-bytes = 128000b scheme-identifier = test } } @@ -166,7 +166,6 @@ public async Task Remoting_must_support_Ask() Assert.IsType(msg.Item2); } - [Fact] public void Remoting_must_create_and_supervise_children_on_remote_Node() { @@ -230,6 +229,31 @@ public async Task Bug_884_Remoting_must_support_reply_to_child_of_Routee() Assert.Equal(reporter, msg.Item2); } + [Fact] + public void Drop_sent_messages_over_payload_size() + { + var oversized = ByteStringOfSize(MaxPayloadBytes + 1); + EventFilter.Exception(start: "Discarding oversized payload sent to ").ExpectOne(() => + { + VerifySend(oversized, () => + { + ExpectNoMsg(); + }); + }); + } + + [Fact] + public void Drop_received_messages_over_payload_size() + { + EventFilter.Exception(start: "Discarding oversized payload received").ExpectOne(() => + { + VerifySend(MaxPayloadBytes + 1, () => + { + ExpectNoMsg(); + }); + }); + } + #endregion #region Internal Methods @@ -239,7 +263,7 @@ private int MaxPayloadBytes get { var byteSize = Sys.Settings.Config.GetByteSize("akka.remote.test.maximum-payload-bytes"); - if (byteSize != null) + if (byteSize != null) return (int)byteSize.Value; return 0; } @@ -290,7 +314,7 @@ private void VerifySend(object msg, Action afterSend) { bigBounceHere.Tell(msg, TestActor); afterSend(); - ExpectNoMsg(TimeSpan.FromMilliseconds(500)); + ExpectNoMsg(); } finally { diff --git a/src/core/Akka.Remote.Tests/Serialization/DaemonMsgCreateSerializerSpec.cs b/src/core/Akka.Remote.Tests/Serialization/DaemonMsgCreateSerializerSpec.cs index 90da49148a5..652087de53d 100644 --- a/src/core/Akka.Remote.Tests/Serialization/DaemonMsgCreateSerializerSpec.cs +++ b/src/core/Akka.Remote.Tests/Serialization/DaemonMsgCreateSerializerSpec.cs @@ -64,7 +64,10 @@ public void Serialization_must_serialize_and_deserialize_DaemonMsgCreate_with_fu [Fact] public void Serialization_must_serialize_and_deserialize_DaemonMsgCreate_with_Deploy_and_RouterConfig() { - var supervisorStrategy = new OneForOneStrategy(3, TimeSpan.FromSeconds(10), exception => Directive.Escalate); + var decider = Decider.From( + Directive.Escalate); + + var supervisorStrategy = new OneForOneStrategy(3, TimeSpan.FromSeconds(10), decider); var deploy1 = new Deploy("path1", ConfigurationFactory.ParseString("a=1"), new RoundRobinPool(5, null, supervisorStrategy, null), diff --git a/src/core/Akka.Remote.Tests/licenses/license.txt b/src/core/Akka.Remote.Tests/licenses/license.txt new file mode 100644 index 00000000000..b8e773b2e05 --- /dev/null +++ b/src/core/Akka.Remote.Tests/licenses/license.txt @@ -0,0 +1,31 @@ +Protocol Buffers - Google's data interchange format +Copyright 2008-2010 Google Inc. All rights reserved. +http://github.com/jskeet/dotnet-protobufs/ +Original C++/Java/Python code: +http://code.google.com/p/protobuf/ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/core/Akka.Remote.Tests/licenses/protoc-license.txt b/src/core/Akka.Remote.Tests/licenses/protoc-license.txt new file mode 100644 index 00000000000..c779cb0e1ed --- /dev/null +++ b/src/core/Akka.Remote.Tests/licenses/protoc-license.txt @@ -0,0 +1,36 @@ +protoc.exe was built from the original source at http://code.google.com/p/protobuf/ +The licence for this code is as follows: + +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests/protos/google/protobuf/csharp_options.proto b/src/core/Akka.Remote.Tests/protos/google/protobuf/csharp_options.proto new file mode 100644 index 00000000000..152df766f03 --- /dev/null +++ b/src/core/Akka.Remote.Tests/protos/google/protobuf/csharp_options.proto @@ -0,0 +1,115 @@ +// Extra options for C# generator + +import "google/protobuf/descriptor.proto"; + +package google.protobuf; + +message CSharpFileOptions { + + // Namespace for generated classes; defaults to the package. + optional string namespace = 1; + + // Name of the "umbrella" class used for metadata about all + // the messages within this file. Default is based on the name + // of the file. + optional string umbrella_classname = 2; + + // Whether classes should be public (true) or internal (false) + optional bool public_classes = 3 [default = true]; + + // Whether to generate a single file for everything within the + // .proto file (false), or one file per message (true). + // This option is not currently honored; please log a feature + // request if you really want it. + optional bool multiple_files = 4; + + // Whether to nest messages within a single umbrella class (true) + // or create the umbrella class as a peer, with messages as + // top-level classes in the namespace (false) + optional bool nest_classes = 5; + + // Generate appropriate support for Code Contracts + // (Ongoing; support should improve over time) + optional bool code_contracts = 6; + + // Create subdirectories for namespaces, e.g. namespace "Foo.Bar" + // would generate files within [output directory]/Foo/Bar + optional bool expand_namespace_directories = 7; + + // Generate attributes indicating non-CLS-compliance + optional bool cls_compliance = 8 [default = true]; + + // Generate messages/builders with the [Serializable] attribute + optional bool add_serializable = 9 [default = false]; + + // Generates a private ctor for Message types + optional bool generate_private_ctor = 10 [default = true]; + + // The extension that should be appended to the umbrella_classname when creating files. + optional string file_extension = 221 [default = ".cs"]; + + // A nested namespace for the umbrella class. Helpful for name collisions caused by + // umbrella_classname conflicting with an existing type. This will be automatically + // set to 'Proto' if a collision is detected with types being generated. This value + // is ignored when nest_classes == true + optional string umbrella_namespace = 222; + + // The output path for the source file(s) generated + optional string output_directory = 223 [default = "."]; + + // Will ignore the type generations and remove dependencies for the descriptor proto + // files that declare their package to be "google.protobuf" + optional bool ignore_google_protobuf = 224 [default = false]; + + // Controls how services are generated, GENERIC is the deprecated original implementation + // INTERFACE generates service interfaces only, RPCINTEROP generates interfaces and + // implementations using the included Windows RPC interop libarary. + optional CSharpServiceType service_generator_type = 225 [default = NONE]; + + // Used to add the System.Runtime.CompilerServices.CompilerGeneratedAttribute and + // System.CodeDom.Compiler.GeneratedCodeAttribute attributes to generated code. + optional bool generated_code_attributes = 226 [default = false]; +} + +enum CSharpServiceType { + // Services are ignored by the generator + NONE = 0; + // Generates the original Java generic service implementations + GENERIC = 1; + // Generates an interface for the service and nothing else + INTERFACE = 2; + // Generates an interface for the service and client/server wrappers for the interface + IRPCDISPATCH = 3; +} + +extend FileOptions { + optional CSharpFileOptions csharp_file_options = 1000; +} + +extend FieldOptions { + optional CSharpFieldOptions csharp_field_options = 1000; +} + +message CSharpFieldOptions { + // Provides the ability to override the name of the property + // generated for this field. This is applied to all properties + // and methods to do with this field, including HasFoo, FooCount, + // FooList etc. + optional string property_name = 1; +} + +message CSharpServiceOptions { + optional string interface_id = 1; +} + +extend ServiceOptions { + optional CSharpServiceOptions csharp_service_options = 1000; +} + +message CSharpMethodOptions { + optional int32 dispatch_id = 1; +} + +extend MethodOptions { + optional CSharpMethodOptions csharp_method_options = 1000; +} \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests/protos/google/protobuf/descriptor.proto b/src/core/Akka.Remote.Tests/protos/google/protobuf/descriptor.proto new file mode 100644 index 00000000000..233f879410e --- /dev/null +++ b/src/core/Akka.Remote.Tests/protos/google/protobuf/descriptor.proto @@ -0,0 +1,533 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// http://code.google.com/p/protobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: kenton@google.com (Kenton Varda) +// Based on original Protocol Buffers design by +// Sanjay Ghemawat, Jeff Dean, and others. +// +// The messages in this file describe the definitions found in .proto files. +// A valid .proto file can be translated directly to a FileDescriptorProto +// without any other information (e.g. without reading its imports). + + + +package google.protobuf; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DescriptorProtos"; + +// descriptor.proto must be optimized for speed because reflection-based +// algorithms don't work during bootstrapping. +option optimize_for = SPEED; + +// The protocol compiler can output a FileDescriptorSet containing the .proto +// files it parses. +message FileDescriptorSet { + repeated FileDescriptorProto file = 1; +} + +// Describes a complete .proto file. +message FileDescriptorProto { + optional string name = 1; // file name, relative to root of source tree + optional string package = 2; // e.g. "foo", "foo.bar", etc. + + // Names of files imported by this file. + repeated string dependency = 3; + + // All top-level definitions in this file. + repeated DescriptorProto message_type = 4; + repeated EnumDescriptorProto enum_type = 5; + repeated ServiceDescriptorProto service = 6; + repeated FieldDescriptorProto extension = 7; + + optional FileOptions options = 8; + + // This field contains optional information about the original source code. + // You may safely remove this entire field whithout harming runtime + // functionality of the descriptors -- the information is needed only by + // development tools. + optional SourceCodeInfo source_code_info = 9; +} + +// Describes a message type. +message DescriptorProto { + optional string name = 1; + + repeated FieldDescriptorProto field = 2; + repeated FieldDescriptorProto extension = 6; + + repeated DescriptorProto nested_type = 3; + repeated EnumDescriptorProto enum_type = 4; + + message ExtensionRange { + optional int32 start = 1; + optional int32 end = 2; + } + repeated ExtensionRange extension_range = 5; + + optional MessageOptions options = 7; +} + +// Describes a field within a message. +message FieldDescriptorProto { + enum Type { + // 0 is reserved for errors. + // Order is weird for historical reasons. + TYPE_DOUBLE = 1; + TYPE_FLOAT = 2; + TYPE_INT64 = 3; // Not ZigZag encoded. Negative numbers + // take 10 bytes. Use TYPE_SINT64 if negative + // values are likely. + TYPE_UINT64 = 4; + TYPE_INT32 = 5; // Not ZigZag encoded. Negative numbers + // take 10 bytes. Use TYPE_SINT32 if negative + // values are likely. + TYPE_FIXED64 = 6; + TYPE_FIXED32 = 7; + TYPE_BOOL = 8; + TYPE_STRING = 9; + TYPE_GROUP = 10; // Tag-delimited aggregate. + TYPE_MESSAGE = 11; // Length-delimited aggregate. + + // New in version 2. + TYPE_BYTES = 12; + TYPE_UINT32 = 13; + TYPE_ENUM = 14; + TYPE_SFIXED32 = 15; + TYPE_SFIXED64 = 16; + TYPE_SINT32 = 17; // Uses ZigZag encoding. + TYPE_SINT64 = 18; // Uses ZigZag encoding. + }; + + enum Label { + // 0 is reserved for errors + LABEL_OPTIONAL = 1; + LABEL_REQUIRED = 2; + LABEL_REPEATED = 3; + // TODO(sanjay): Should we add LABEL_MAP? + }; + + optional string name = 1; + optional int32 number = 3; + optional Label label = 4; + + // If type_name is set, this need not be set. If both this and type_name + // are set, this must be either TYPE_ENUM or TYPE_MESSAGE. + optional Type type = 5; + + // For message and enum types, this is the name of the type. If the name + // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping + // rules are used to find the type (i.e. first the nested types within this + // message are searched, then within the parent, on up to the root + // namespace). + optional string type_name = 6; + + // For extensions, this is the name of the type being extended. It is + // resolved in the same manner as type_name. + optional string extendee = 2; + + // For numeric types, contains the original text representation of the value. + // For booleans, "true" or "false". + // For strings, contains the default text contents (not escaped in any way). + // For bytes, contains the C escaped value. All bytes >= 128 are escaped. + // TODO(kenton): Base-64 encode? + optional string default_value = 7; + + optional FieldOptions options = 8; +} + +// Describes an enum type. +message EnumDescriptorProto { + optional string name = 1; + + repeated EnumValueDescriptorProto value = 2; + + optional EnumOptions options = 3; +} + +// Describes a value within an enum. +message EnumValueDescriptorProto { + optional string name = 1; + optional int32 number = 2; + + optional EnumValueOptions options = 3; +} + +// Describes a service. +message ServiceDescriptorProto { + optional string name = 1; + repeated MethodDescriptorProto method = 2; + + optional ServiceOptions options = 3; +} + +// Describes a method of a service. +message MethodDescriptorProto { + optional string name = 1; + + // Input and output type names. These are resolved in the same way as + // FieldDescriptorProto.type_name, but must refer to a message type. + optional string input_type = 2; + optional string output_type = 3; + + optional MethodOptions options = 4; +} + +// =================================================================== +// Options + +// Each of the definitions above may have "options" attached. These are +// just annotations which may cause code to be generated slightly differently +// or may contain hints for code that manipulates protocol messages. +// +// Clients may define custom options as extensions of the *Options messages. +// These extensions may not yet be known at parsing time, so the parser cannot +// store the values in them. Instead it stores them in a field in the *Options +// message called uninterpreted_option. This field must have the same name +// across all *Options messages. We then use this field to populate the +// extensions when we build a descriptor, at which point all protos have been +// parsed and so all extensions are known. +// +// Extension numbers for custom options may be chosen as follows: +// * For options which will only be used within a single application or +// organization, or for experimental options, use field numbers 50000 +// through 99999. It is up to you to ensure that you do not use the +// same number for multiple options. +// * For options which will be published and used publicly by multiple +// independent entities, e-mail kenton@google.com to reserve extension +// numbers. Simply tell me how many you need and I'll send you back a +// set of numbers to use -- there's no need to explain how you intend to +// use them. If this turns out to be popular, a web service will be set up +// to automatically assign option numbers. + + +message FileOptions { + + // Sets the Java package where classes generated from this .proto will be + // placed. By default, the proto package is used, but this is often + // inappropriate because proto packages do not normally start with backwards + // domain names. + optional string java_package = 1; + + + // If set, all the classes from the .proto file are wrapped in a single + // outer class with the given name. This applies to both Proto1 + // (equivalent to the old "--one_java_file" option) and Proto2 (where + // a .proto always translates to a single class, but you may want to + // explicitly choose the class name). + optional string java_outer_classname = 8; + + // If set true, then the Java code generator will generate a separate .java + // file for each top-level message, enum, and service defined in the .proto + // file. Thus, these types will *not* be nested inside the outer class + // named by java_outer_classname. However, the outer class will still be + // generated to contain the file's getDescriptor() method as well as any + // top-level extensions defined in the file. + optional bool java_multiple_files = 10 [default=false]; + + // If set true, then the Java code generator will generate equals() and + // hashCode() methods for all messages defined in the .proto file. This is + // purely a speed optimization, as the AbstractMessage base class includes + // reflection-based implementations of these methods. + optional bool java_generate_equals_and_hash = 20 [default=false]; + + // Generated classes can be optimized for speed or code size. + enum OptimizeMode { + SPEED = 1; // Generate complete code for parsing, serialization, + // etc. + CODE_SIZE = 2; // Use ReflectionOps to implement these methods. + LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. + } + optional OptimizeMode optimize_for = 9 [default=SPEED]; + + + + + // Should generic services be generated in each language? "Generic" services + // are not specific to any particular RPC system. They are generated by the + // main code generators in each language (without additional plugins). + // Generic services were the only kind of service generation supported by + // early versions of proto2. + // + // Generic services are now considered deprecated in favor of using plugins + // that generate code specific to your particular RPC system. Therefore, + // these default to false. Old code which depends on generic services should + // explicitly set them to true. + optional bool cc_generic_services = 16 [default=false]; + optional bool java_generic_services = 17 [default=false]; + optional bool py_generic_services = 18 [default=false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message MessageOptions { + // Set true to use the old proto1 MessageSet wire format for extensions. + // This is provided for backwards-compatibility with the MessageSet wire + // format. You should not use this for any other reason: It's less + // efficient, has fewer features, and is more complicated. + // + // The message must be defined exactly as follows: + // message Foo { + // option message_set_wire_format = true; + // extensions 4 to max; + // } + // Note that the message cannot have any defined fields; MessageSets only + // have extensions. + // + // All extensions of your type must be singular messages; e.g. they cannot + // be int32s, enums, or repeated messages. + // + // Because this is an option, the above two restrictions are not enforced by + // the protocol compiler. + optional bool message_set_wire_format = 1 [default=false]; + + // Disables the generation of the standard "descriptor()" accessor, which can + // conflict with a field of the same name. This is meant to make migration + // from proto1 easier; new code should avoid fields named "descriptor". + optional bool no_standard_descriptor_accessor = 2 [default=false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message FieldOptions { + // The ctype option instructs the C++ code generator to use a different + // representation of the field than it normally would. See the specific + // options below. This option is not yet implemented in the open source + // release -- sorry, we'll try to include it in a future version! + optional CType ctype = 1 [default = STRING]; + enum CType { + // Default mode. + STRING = 0; + + CORD = 1; + + STRING_PIECE = 2; + } + // The packed option can be enabled for repeated primitive fields to enable + // a more efficient representation on the wire. Rather than repeatedly + // writing the tag and type for each element, the entire array is encoded as + // a single length-delimited blob. + optional bool packed = 2; + + + // Is this field deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for accessors, or it will be completely ignored; in the very least, this + // is a formalization for deprecating fields. + optional bool deprecated = 3 [default=false]; + + // EXPERIMENTAL. DO NOT USE. + // For "map" fields, the name of the field in the enclosed type that + // is the key for this map. For example, suppose we have: + // message Item { + // required string name = 1; + // required string value = 2; + // } + // message Config { + // repeated Item items = 1 [experimental_map_key="name"]; + // } + // In this situation, the map key for Item will be set to "name". + // TODO: Fully-implement this, then remove the "experimental_" prefix. + optional string experimental_map_key = 9; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumOptions { + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumValueOptions { + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message ServiceOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message MethodOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +// A message representing a option the parser does not recognize. This only +// appears in options protos created by the compiler::Parser class. +// DescriptorPool resolves these when building Descriptor objects. Therefore, +// options protos in descriptor objects (e.g. returned by Descriptor::options(), +// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions +// in them. +message UninterpretedOption { + // The name of the uninterpreted option. Each string represents a segment in + // a dot-separated name. is_extension is true iff a segment represents an + // extension (denoted with parentheses in options specs in .proto files). + // E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents + // "foo.(bar.baz).qux". + message NamePart { + required string name_part = 1; + required bool is_extension = 2; + } + repeated NamePart name = 2; + + // The value of the uninterpreted option, in whatever type the tokenizer + // identified it as during parsing. Exactly one of these should be set. + optional string identifier_value = 3; + optional uint64 positive_int_value = 4; + optional int64 negative_int_value = 5; + optional double double_value = 6; + optional bytes string_value = 7; + optional string aggregate_value = 8; +} + +// =================================================================== +// Optional source code info + +// Encapsulates information about the original source file from which a +// FileDescriptorProto was generated. +message SourceCodeInfo { + // A Location identifies a piece of source code in a .proto file which + // corresponds to a particular definition. This information is intended + // to be useful to IDEs, code indexers, documentation generators, and similar + // tools. + // + // For example, say we have a file like: + // message Foo { + // optional string foo = 1; + // } + // Let's look at just the field definition: + // optional string foo = 1; + // ^ ^^ ^^ ^ ^^^ + // a bc de f ghi + // We have the following locations: + // span path represents + // [a,i) [ 4, 0, 2, 0 ] The whole field definition. + // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). + // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). + // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). + // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). + // + // Notes: + // - A location may refer to a repeated field itself (i.e. not to any + // particular index within it). This is used whenever a set of elements are + // logically enclosed in a single code segment. For example, an entire + // extend block (possibly containing multiple extension definitions) will + // have an outer location whose path refers to the "extensions" repeated + // field without an index. + // - Multiple locations may have the same path. This happens when a single + // logical declaration is spread out across multiple places. The most + // obvious example is the "extend" block again -- there may be multiple + // extend blocks in the same scope, each of which will have the same path. + // - A location's span is not always a subset of its parent's span. For + // example, the "extendee" of an extension declaration appears at the + // beginning of the "extend" block and is shared by all extensions within + // the block. + // - Just because a location's span is a subset of some other location's span + // does not mean that it is a descendent. For example, a "group" defines + // both a type and a field in a single declaration. Thus, the locations + // corresponding to the type and field and their components will overlap. + // - Code which tries to interpret locations should probably be designed to + // ignore those that it doesn't understand, as more types of locations could + // be recorded in the future. + repeated Location location = 1; + message Location { + // Identifies which part of the FileDescriptorProto was defined at this + // location. + // + // Each element is a field number or an index. They form a path from + // the root FileDescriptorProto to the place where the definition. For + // example, this path: + // [ 4, 3, 2, 7, 1 ] + // refers to: + // file.message_type(3) // 4, 3 + // .field(7) // 2, 7 + // .name() // 1 + // This is because FileDescriptorProto.message_type has field number 4: + // repeated DescriptorProto message_type = 4; + // and DescriptorProto.field has field number 2: + // repeated FieldDescriptorProto field = 2; + // and FieldDescriptorProto.name has field number 1: + // optional string name = 1; + // + // Thus, the above path gives the location of a field name. If we removed + // the last element: + // [ 4, 3, 2, 7 ] + // this path refers to the whole field declaration (from the beginning + // of the label to the terminating semicolon). + repeated int32 path = 1 [packed=true]; + + // Always has exactly three or four elements: start line, start column, + // end line (optional, otherwise assumed same as start line), end column. + // These are packed into a single field for efficiency. Note that line + // and column numbers are zero-based -- typically you will want to add + // 1 to each before displaying to a user. + repeated int32 span = 2 [packed=true]; + + // TODO(kenton): Record comments appearing before and after the + // declaration. + } +} diff --git a/src/core/Akka.Remote.Tests/protos/tutorial/addressbook.proto b/src/core/Akka.Remote.Tests/protos/tutorial/addressbook.proto new file mode 100644 index 00000000000..5abe35ce39b --- /dev/null +++ b/src/core/Akka.Remote.Tests/protos/tutorial/addressbook.proto @@ -0,0 +1,31 @@ +package tutorial; + +import "google/protobuf/csharp_options.proto"; +option (google.protobuf.csharp_file_options).namespace = "Google.ProtocolBuffers.Examples.AddressBook"; +option (google.protobuf.csharp_file_options).umbrella_classname = "AddressBookProtos"; + +option optimize_for = SPEED; + +message Person { + required string name = 1; + required int32 id = 2; // Unique ID number for this person. + optional string email = 3; + + enum PhoneType { + MOBILE = 0; + HOME = 1; + WORK = 2; + } + + message PhoneNumber { + required string number = 1; + optional PhoneType type = 2 [default = HOME]; + } + + repeated PhoneNumber phone = 4; +} + +// Our address book file is just one of these. +message AddressBook { + repeated Person person = 1; +} diff --git a/src/core/Akka.Remote/AckedDelivery.cs b/src/core/Akka.Remote/AckedDelivery.cs index cbb03620dad..3fe022840a7 100644 --- a/src/core/Akka.Remote/AckedDelivery.cs +++ b/src/core/Akka.Remote/AckedDelivery.cs @@ -196,7 +196,7 @@ sealed class Ack /// Class representing an acknowledgement with select negative acknowledgements. /// /// Represents the highest sequence number received - /// Set of sequence numbers between the last delivered one and that has not been received. + /// Set of sequence numbers between the last delivered one and that has not been received. public Ack(SeqNo cumulativeAck, IEnumerable nacks) { Nacks = new SortedSet(nacks, SeqNo.Comparer); @@ -219,21 +219,39 @@ public override string ToString() } } + /// + /// This exception is thrown when the Resent buffer is filled beyond its capacity. + /// class ResendBufferCapacityReachedException : AkkaException { + /// + /// Initializes a new instance of the class. + /// + /// The capacity of the buffer public ResendBufferCapacityReachedException(int c) : base(string.Format("Resent buffer capacity of {0} has been reached.", c)) { } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected ResendBufferCapacityReachedException(SerializationInfo info, StreamingContext context) : base(info, context) { } } + /// + /// This exception is thrown when the system is unable to fulfill a resend request since negatively acknowledged payload is no longer in buffer. + /// class ResendUnfulfillableException : AkkaException { + /// + /// Initializes a new instance of the class. + /// public ResendUnfulfillableException() : base("Unable to fulfill resend request since negatively acknowledged payload is no longer in buffer. " + "The resend states between two systems are compromised and cannot be recovered") { } diff --git a/src/core/Akka.Remote/Akka.Remote.csproj b/src/core/Akka.Remote/Akka.Remote.csproj index cfb97c32851..7a2d6e37faf 100644 --- a/src/core/Akka.Remote/Akka.Remote.csproj +++ b/src/core/Akka.Remote/Akka.Remote.csproj @@ -51,6 +51,24 @@ false + + True + + + True + + + True + + + True + + + True + + + True + ..\..\packages\Google.ProtocolBuffers.2.4.1.521\lib\net40\Google.ProtocolBuffers.dll @@ -88,6 +106,7 @@ + diff --git a/src/core/Akka.Remote/Configuration/RemoteConfigFactory.cs b/src/core/Akka.Remote/Configuration/RemoteConfigFactory.cs index 44d5fdf9c47..f29607ddce1 100644 --- a/src/core/Akka.Remote/Configuration/RemoteConfigFactory.cs +++ b/src/core/Akka.Remote/Configuration/RemoteConfigFactory.cs @@ -12,24 +12,26 @@ namespace Akka.Remote.Configuration { /// - /// Internal class used for loading remote configuration values + /// This class contains methods used to retrieve remote configuration options from this assembly's resources. + /// + /// Note! Part of internal API. Breaking changes may occur without notice. Use at own risk. /// internal static class RemoteConfigFactory { /// - /// Defaults this instance. + /// Retrieves the default remote options that Akka.NET uses when no configuration has been defined. /// - /// Config. + /// The configuration that contains default values for all remote options. public static Config Default() { return FromResource("Akka.Remote.Configuration.Remote.conf"); } /// - /// Froms the resource. + /// Retrieves a configuration defined in a resource of the current executing assembly. /// - /// Name of the resource. - /// Config. + /// The name of the resource that contains the configuration. + /// The configuration defined in the current executing assembly. internal static Config FromResource(string resourceName) { var assembly = typeof (RemoteConfigFactory).Assembly; @@ -47,4 +49,3 @@ internal static Config FromResource(string resourceName) } } } - diff --git a/src/core/Akka.Remote/Endpoint.cs b/src/core/Akka.Remote/Endpoint.cs index e4e430c5293..ebc167ad28f 100644 --- a/src/core/Akka.Remote/Endpoint.cs +++ b/src/core/Akka.Remote/Endpoint.cs @@ -160,9 +160,9 @@ internal interface IAssociationProblem { } /// /// INTERNAL API /// - internal sealed class ShutDownAssociation : EndpointException, IAssociationProblem + internal sealed class ShutDownAssociationException : EndpointException, IAssociationProblem { - public ShutDownAssociation(Address localAddress, Address remoteAddress, Exception cause = null) + public ShutDownAssociationException(Address localAddress, Address remoteAddress, Exception cause = null) : base(string.Format("Shut down address: {0}", remoteAddress), cause) { RemoteAddress = remoteAddress; @@ -174,9 +174,9 @@ public ShutDownAssociation(Address localAddress, Address remoteAddress, Exceptio public Address RemoteAddress { get; private set; } } - internal sealed class InvalidAssociation : EndpointException, IAssociationProblem + internal sealed class InvalidAddressAssociationException : EndpointException, IAssociationProblem { - public InvalidAssociation(Address localAddress, Address remoteAddress, Exception cause = null) + public InvalidAddressAssociationException(Address localAddress, Address remoteAddress, Exception cause = null) : base(string.Format("Invalid address: {0}", remoteAddress), cause) { RemoteAddress = remoteAddress; @@ -191,9 +191,9 @@ public InvalidAssociation(Address localAddress, Address remoteAddress, Exception /// /// INTERNAL API /// - internal sealed class HopelessAssociation : EndpointException, IAssociationProblem + internal sealed class HopelessAssociationException : EndpointException, IAssociationProblem { - public HopelessAssociation(Address localAddress, Address remoteAddress, int? uid = null, Exception cause = null) + public HopelessAssociationException(Address localAddress, Address remoteAddress, int? uid = null, Exception cause = null) : base("Catastrophic association error.", cause) { RemoteAddress = remoteAddress; @@ -488,7 +488,7 @@ protected void Gated(object message) // In other words, this action is safe. if (!UidConfirmed && BailoutAt.IsOverdue) { - throw new InvalidAssociation(_localAddress, _remoteAddress, + throw new InvalidAddressAssociationException(_localAddress, _remoteAddress, new TimeoutException("Delivery of system messages timed out and they were dropped")); } @@ -628,7 +628,7 @@ private void TryBuffer(EndpointManager.Send s) } catch (Exception ex) { - throw new HopelessAssociation(_localAddress, _remoteAddress, Uid, ex); + throw new HopelessAssociationException(_localAddress, _remoteAddress, Uid, ex); } } @@ -878,16 +878,19 @@ protected override void PreStart() protected override void PostStop() { _ackIdleTimerCancelable.CancelIfNotNull(); - while (_prioBuffer.Any()) + + foreach (var msg in _prioBuffer) { - _system.DeadLetters.Tell(_prioBuffer.First); - _prioBuffer.RemoveFirst(); + _system.DeadLetters.Tell(msg); } - while (_buffer.Any()) + _prioBuffer.Clear(); + + foreach (var msg in _buffer) { - _system.DeadLetters.Tell(_buffer.First); - _buffer.RemoveFirst(); + _system.DeadLetters.Tell(msg); } + _buffer.Clear(); + if (_handle != null) _handle.Disassociate(_stopReason); EventPublisher.NotifyListeners(new DisassociatedEvent(LocalAddress, RemoteAddress, Inbound)); } @@ -913,7 +916,7 @@ private void Initializing(object message) var failure = message as Status.Failure; if (failure.Cause is InvalidAssociationException) { - PublishAndThrow(new InvalidAssociation(LocalAddress, RemoteAddress, failure.Cause), + PublishAndThrow(new InvalidAddressAssociationException(LocalAddress, RemoteAddress, failure.Cause), LogLevel.WarningLevel); } else @@ -1234,17 +1237,28 @@ private bool WriteSend(EndpointManager.Send send) //todo: RemoteMetrics https://github.com/akka/akka/blob/dc0547dd73b54b5de9c3e0b45a21aa865c5db8e2/akka-remote/src/main/scala/akka/remote/Endpoint.scala#L742 - //todo: max payload size validation - - var ok = _handle.Write(pdu); - - if (ok) + if (pdu.Length > Transport.MaximumPayloadBytes) { - _ackDeadline = NewAckDeadline(); - _lastAck = null; + var reason = new OversizedPayloadException( + string.Format("Discarding oversized payload sent to {0}: max allowed size {1} bytes, actual size of encoded {2} was {3} bytes.", + send.Recipient, + Transport.MaximumPayloadBytes, + send.Message.GetType(), + pdu.Length)); + _log.Error(reason, "Transient association error (association remains live)"); return true; } + else + { + var ok = _handle.Write(pdu); + if (ok) + { + _ackDeadline = NewAckDeadline(); + _lastAck = null; + return true; + } + } return false; } catch (SerializationException ex) @@ -1346,8 +1360,7 @@ private void SendBufferedMessages() var now = MonotonicClock.GetNanos(); if (now - _largeBufferLogTimestamp >= LogBufferSizeInterval) { - _log.Warning("[{0}] buffered messages in EndpointWriter for [{1}]. " + - "You should probably implement flow control to avoid flooding the remote connection.", size, RemoteAddress); + _log.Warning("[{0}] buffered messages in EndpointWriter for [{1}]. You should probably implement flow control to avoid flooding the remote connection.", size, RemoteAddress); _largeBufferLogTimestamp = now; } } @@ -1519,6 +1532,7 @@ internal class EndpointReader : EndpointActor _provider = RARP.For(Context.System).Provider; } + private readonly ILoggingAdapter _log = Context.GetLogger(); private readonly AkkaPduCodec _codec; private readonly IActorRef _reliableDeliverySupervisor; private readonly ConcurrentDictionary _receiveBuffers; @@ -1553,23 +1567,34 @@ protected override void OnReceive(object message) } else if (message is InboundPayload) { - var payload = message as InboundPayload; - var ackAndMessage = TryDecodeMessageAndAck(payload.Payload); - if (ackAndMessage.AckOption != null && _reliableDeliverySupervisor != null) - _reliableDeliverySupervisor.Tell(ackAndMessage.AckOption); - if (ackAndMessage.MessageOption != null) + var payload = ((InboundPayload)message).Payload; + if (payload.Length > Transport.MaximumPayloadBytes) { - if (ackAndMessage.MessageOption.ReliableDeliveryEnabled) - { - _ackedReceiveBuffer = _ackedReceiveBuffer.Receive(ackAndMessage.MessageOption); - DeliverAndAck(); - } - else + var reason = new OversizedPayloadException( + string.Format("Discarding oversized payload received: max allowed size {0} bytes, actual size {1} bytes.", + Transport.MaximumPayloadBytes, + payload.Length)); + _log.Error(reason, "Transient error while reading from association (association remains live)"); + } + else + { + var ackAndMessage = TryDecodeMessageAndAck(payload); + if (ackAndMessage.AckOption != null && _reliableDeliverySupervisor != null) + _reliableDeliverySupervisor.Tell(ackAndMessage.AckOption); + if (ackAndMessage.MessageOption != null) { - _msgDispatch.Dispatch(ackAndMessage.MessageOption.Recipient, - ackAndMessage.MessageOption.RecipientAddress, - ackAndMessage.MessageOption.SerializedMessage, - ackAndMessage.MessageOption.SenderOptional); + if (ackAndMessage.MessageOption.ReliableDeliveryEnabled) + { + _ackedReceiveBuffer = _ackedReceiveBuffer.Receive(ackAndMessage.MessageOption); + DeliverAndAck(); + } + else + { + _msgDispatch.Dispatch(ackAndMessage.MessageOption.Recipient, + ackAndMessage.MessageOption.RecipientAddress, + ackAndMessage.MessageOption.SerializedMessage, + ackAndMessage.MessageOption.SenderOptional); + } } } } @@ -1663,10 +1688,10 @@ private void HandleDisassociated(DisassociateInfo info) switch (info) { case DisassociateInfo.Quarantined: - throw new InvalidAssociation(LocalAddress, RemoteAddress, new InvalidAssociationException("The remote system has quarantined this system. No further associations " + + throw new InvalidAddressAssociationException(LocalAddress, RemoteAddress, new InvalidAssociationException("The remote system has quarantined this system. No further associations " + "to the remote system are possible until this system is restarted.")); case DisassociateInfo.Shutdown: - throw new ShutDownAssociation(LocalAddress, RemoteAddress, new InvalidAssociationException("The remote system terminated the association because it is shutting down.")); + throw new ShutDownAssociationException(LocalAddress, RemoteAddress, new InvalidAssociationException("The remote system terminated the association because it is shutting down.")); case DisassociateInfo.Unknown: default: Context.Stop(Self); diff --git a/src/core/Akka.Remote/EndpointManager.cs b/src/core/Akka.Remote/EndpointManager.cs index 844fe79cd43..9c3695f0a4d 100644 --- a/src/core/Akka.Remote/EndpointManager.cs +++ b/src/core/Akka.Remote/EndpointManager.cs @@ -340,25 +340,23 @@ protected override SupervisorStrategy SupervisorStrategy() var directive = Directive.Stop; ex.Match() - .With(ia => + .With(ia => { - log.Warning("Tried to associate with unreachable remote address [{0}]. " + - "Address is now gated for {1} ms, all messages to this address will be delivered to dead letters. Reason: [{2}]", + log.Warning("Tried to associate with unreachable remote address [{0}]. Address is now gated for {1} ms, all messages to this address will be delivered to dead letters. Reason: [{2}]", ia.RemoteAddress, settings.RetryGateClosedFor.TotalMilliseconds, ia.Message); endpoints.MarkAsFailed(Sender, Deadline.Now + settings.RetryGateClosedFor); AddressTerminatedTopic.Get(Context.System).Publish(new AddressTerminated(ia.RemoteAddress)); directive = Directive.Stop; }) - .With(shutdown => + .With(shutdown => { - log.Debug("Remote system with address [{0}] has shut down. " + - "Address is now gated for {1}ms, all messages to this address will be delivered to dead letters.", + log.Debug("Remote system with address [{0}] has shut down. Address is now gated for {1}ms, all messages to this address will be delivered to dead letters.", shutdown.RemoteAddress, settings.RetryGateClosedFor.TotalMilliseconds); endpoints.MarkAsFailed(Sender, Deadline.Now + settings.RetryGateClosedFor); AddressTerminatedTopic.Get(Context.System).Publish(new AddressTerminated(shutdown.RemoteAddress)); directive = Directive.Stop; }) - .With(hopeless => + .With(hopeless => { if (settings.QuarantineDuration.HasValue && hopeless.Uid.HasValue) { @@ -369,8 +367,7 @@ protected override SupervisorStrategy SupervisorStrategy() } else { - log.Warning("Association to [{0}] with unknown UID is irrecoverably failed. " + - "Address cannot be quarantined without knowing the UID, gating instead for {1} ms.", + log.Warning("Association to [{0}] with unknown UID is irrecoverably failed. Address cannot be quarantined without knowing the UID, gating instead for {1} ms.", hopeless.RemoteAddress, settings.RetryGateClosedFor.TotalMilliseconds); endpoints.MarkAsFailed(Sender, Deadline.Now + settings.RetryGateClosedFor); } @@ -508,8 +505,7 @@ protected void Accepting(object message) Context.Stop(pass.Endpoint); if (!pass.Uid.HasValue) { - log.Warning("Association to [{0}] with unknown UID is reported as quarantined, but " + - "address cannot be quarantined without knowing the UID, gated instead for {0} ms", + log.Warning("Association to [{0}] with unknown UID is reported as quarantined, but address cannot be quarantined without knowing the UID, gated instead for {0} ms", quarantine.RemoteAddress, settings.RetryGateClosedFor.TotalMilliseconds); endpoints.MarkAsFailed(pass.Endpoint, Deadline.Now + settings.RetryGateClosedFor); } diff --git a/src/core/Akka.Remote/Properties/AssemblyInfo.cs b/src/core/Akka.Remote/Properties/AssemblyInfo.cs index f35e64af12a..9bd4bd1060b 100644 --- a/src/core/Akka.Remote/Properties/AssemblyInfo.cs +++ b/src/core/Akka.Remote/Properties/AssemblyInfo.cs @@ -29,6 +29,7 @@ [assembly: Guid("78986bdb-73f7-4532-8e03-1c9ccbe8148e")] [assembly: InternalsVisibleTo("Akka.Remote.TestKit")] [assembly: InternalsVisibleTo("Akka.Remote.Tests")] +[assembly: InternalsVisibleTo("Akka.Remote.Tests.MultiNode")] [assembly: InternalsVisibleTo("Akka.Cluster")] [assembly: InternalsVisibleTo("Akka.Cluster.Tests")] -[assembly: InternalsVisibleTo("Akka.MultiNodeTests")] +[assembly: InternalsVisibleTo("Akka.Cluster.Tests.MultiNode")] diff --git a/src/core/Akka.Remote/RemoteActorRefProvider.cs b/src/core/Akka.Remote/RemoteActorRefProvider.cs index e5c52e4e040..dd38470541a 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -95,6 +95,7 @@ public void UnregisterTempActor(ActorPath path) private volatile IActorRef _remotingTerminator; private volatile IActorRef _remoteWatcher; + private volatile IActorRef _remoteDeploymentWatcher; public virtual void Init(ActorSystemImpl system) { @@ -111,6 +112,7 @@ public virtual void Init(ActorSystemImpl system) Transport.Start(); _remoteWatcher = CreateRemoteWatcher(system); + _remoteDeploymentWatcher = CreateRemoteDeploymentWatcher(system); } protected virtual IActorRef CreateRemoteWatcher(ActorSystemImpl system) @@ -124,6 +126,12 @@ protected virtual IActorRef CreateRemoteWatcher(ActorSystemImpl system) RemoteSettings.WatchHeartbeatExpectedResponseAfter)), "remote-watcher"); } + protected virtual IActorRef CreateRemoteDeploymentWatcher(ActorSystemImpl system) + { + return system.SystemActorOf(RemoteSettings.ConfigureDispatcher(Props.Create()), + "remote-deployment-watcher"); + } + protected DefaultFailureDetectorRegistry
CreateRemoteWatcherFailureDetector(ActorSystem system) { return new DefaultFailureDetectorRegistry
(() => @@ -358,6 +366,7 @@ public void UseActorOnNode(RemoteActorRef actor, Props props, Deploy deploy, IIn _log.Debug("[{0}] Instantiating Remote Actor [{1}]", RootPath, actor.Path); IActorRef remoteNode = ResolveActorRef(new RootActorPath(actor.Path.Address) / "remote"); remoteNode.Tell(new DaemonMsgCreate(props, deploy, actor.Path.ToSerializationFormat(), supervisor)); + _remoteDeploymentWatcher.Tell(new RemoteDeploymentWatcher.WatchRemote(actor, supervisor)); } /// @@ -520,23 +529,30 @@ public RemoteDeadLetterActorRef(IActorRefProvider provider, ActorPath actorPath, protected override void TellInternal(object message, IActorRef sender) { - var send = message as EndpointManager.Send; - if (send != null) - { - // else ignore: it is a reliably delivered message that might be retried later, and it has not yet deserved - // the dead letter status - //TODO: Seems to have started causing endless cycle of messages (and stack overflow) - //if (send.Seq == null) Tell(message, sender); - return; - } - var deadLetter = message as DeadLetter; - if (deadLetter != null) - { - // else ignore: it is a reliably delivered message that might be retried later, and it has not yet deserved - // the dead letter status - //TODO: if(deadLetter.Message) - } - + message + .Match() + .With( + send => + { + // else ignore: it is a reliably delivered message that might be retried later, and it has not yet deserved + // the dead letter status + if (send.Seq == null) + { + base.TellInternal(send.Message, send.SenderOption ?? ActorRefs.NoSender); + } + }) + .With( + deadLetter => + { + // else ignore: it is a reliably delivered message that might be retried later, and it has not yet deserved + // the dead letter status + var deadSend = deadLetter.Message as EndpointManager.Send; + if (deadSend != null && deadSend.Seq == null) + { + base.TellInternal(deadSend.Message, deadSend.SenderOption ?? ActorRefs.NoSender); + } + }) + .Default(_ => base.TellInternal(message, sender)); } } } diff --git a/src/core/Akka.Remote/RemoteDeployer.cs b/src/core/Akka.Remote/RemoteDeployer.cs index 3e7566e4531..b972a325fb3 100644 --- a/src/core/Akka.Remote/RemoteDeployer.cs +++ b/src/core/Akka.Remote/RemoteDeployer.cs @@ -47,7 +47,7 @@ public override Deploy ParseConfig(string key, Config config) } /// - /// Used to determine if a given is an instance of . + /// Used to determine if a given is an instance of . /// private static Deploy CheckRemoteRouterConfig(Deploy deploy) { diff --git a/src/core/Akka.Remote/RemoteDeploymentWatcher.cs b/src/core/Akka.Remote/RemoteDeploymentWatcher.cs new file mode 100644 index 00000000000..0668ee13ab1 --- /dev/null +++ b/src/core/Akka.Remote/RemoteDeploymentWatcher.cs @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Dispatch; +using Akka.Dispatch.SysMsg; +using Akka.Util.Internal.Collections; + +namespace Akka.Remote +{ + /// + /// Responsible for cleaning up child references of remote deployed actors when remote node + /// goes down (crash, network failure), i.e. triggered by Akka.Actor.Terminated.AddressTerminated + /// + internal class RemoteDeploymentWatcher : ActorBase, IRequiresMessageQueue + { + + private readonly IImmutableMap _supervisors = + ImmutableTreeMap.Empty; + protected override bool Receive(object message) + { + if (message == null) + { + return false; + } + return message.Match().With(w => + { + _supervisors.Add(w.Actor, w.Supervisor); + Context.Watch(w.Actor); + }).With(t => + { + IInternalActorRef supervisor; + if (_supervisors.TryGet(t.ActorRef, out supervisor)) + { + supervisor.SendSystemMessage(new DeathWatchNotification(t.ActorRef, t.ExistenceConfirmed, t.AddressTerminated), supervisor); + _supervisors.Remove(t.ActorRef); + } + }).WasHandled; + } + + internal class WatchRemote + { + public WatchRemote(IActorRef actor, IInternalActorRef supervisor) + { + Actor = actor; + Supervisor = supervisor; + } + + public IActorRef Actor { get; private set; } + public IInternalActorRef Supervisor { get; private set; } + } + } +} diff --git a/src/core/Akka.Remote/RemoteSystemDaemon.cs b/src/core/Akka.Remote/RemoteSystemDaemon.cs index dfcd9bf36b7..f1517a810bc 100644 --- a/src/core/Akka.Remote/RemoteSystemDaemon.cs +++ b/src/core/Akka.Remote/RemoteSystemDaemon.cs @@ -73,7 +73,7 @@ public DaemonMsgCreate(Props props, Deploy deploy, string path, IActorRef superv /// /// Internal system "daemon" actor for remote internal communication. /// - /// It acts as the brain of the remote that response to system remote messages and executes actions accordingly. + /// It acts as the brain of the remote that responds to system remote messages and executes actions accordingly. /// internal class RemoteSystemDaemon : VirtualPathContainer { @@ -102,7 +102,8 @@ public RemoteSystemDaemon(ActorSystemImpl system, ActorPath path, IInternalActor /// /// Called when [receive]. /// - /// The message. + /// The message that was received. + /// The actor that sent the message. protected void OnReceive(object message, IActorRef sender) { //note: RemoteDaemon does not handle ActorSelection messages - those are handled directly by the RemoteActorRefProvider. diff --git a/src/core/Akka.Remote/RemoteTransport.cs b/src/core/Akka.Remote/RemoteTransport.cs index d519d85f0d3..d5cba46e687 100644 --- a/src/core/Akka.Remote/RemoteTransport.cs +++ b/src/core/Akka.Remote/RemoteTransport.cs @@ -68,7 +68,7 @@ protected RemoteTransport(ExtendedActorSystem system, RemoteActorRefProvider pro public abstract Task Shutdown(); /// - /// Sends the given message to the recipient, supplying if any. + /// Sends the given message to the recipient, supplying if any. /// public abstract void Send(object message, IActorRef sender, RemoteActorRef recipient); @@ -97,16 +97,26 @@ protected RemoteTransport(ExtendedActorSystem system, RemoteActorRefProvider pro } /// - /// Represents a general failure within a , such as + /// This exception is thrown when a general failure within a occurs, such as /// the inability to start, wrong configuration, etc... /// public class RemoteTransportException : AkkaException { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. public RemoteTransportException(string message, Exception cause = null) : base(message, cause) { } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected RemoteTransportException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/core/Akka.Remote/Remoting.cs b/src/core/Akka.Remote/Remoting.cs index 1d0e78b285d..e7e01f1457f 100644 --- a/src/core/Akka.Remote/Remoting.cs +++ b/src/core/Akka.Remote/Remoting.cs @@ -222,8 +222,7 @@ public override Task Shutdown() { if (!result.Result) { - log.Warning( - "Shutdown finished, but flushing might not have been successful and some messages might have been dropped. " + + log.Warning("Shutdown finished, but flushing might not have been successful and some messages might have been dropped. " + "Increase akka.remote.flush-wait-on-shutdown to a larger value to avoid this."); } finalize(); diff --git a/src/core/Akka.Remote/Serialization/DaemonMsgCreateSerializer.cs b/src/core/Akka.Remote/Serialization/DaemonMsgCreateSerializer.cs index 1d4f2d91252..b0cac6d8462 100644 --- a/src/core/Akka.Remote/Serialization/DaemonMsgCreateSerializer.cs +++ b/src/core/Akka.Remote/Serialization/DaemonMsgCreateSerializer.cs @@ -15,17 +15,33 @@ namespace Akka.Remote.Serialization { + /// + /// This is a special that serializes and deserializes only. + /// Serialization of contained , , and is done with the + /// configured serializer for those classes. + /// public class DaemonMsgCreateSerializer : Serializer { + /// + /// Initializes a new instance of the class. + /// + /// The actor system to associate with this serializer. public DaemonMsgCreateSerializer(ExtendedActorSystem system) : base(system) { } + /// + /// Completely unique value to identify this implementation of Serializer, used to optimize network traffic + /// Values from 0 to 16 is reserved for Akka internal usage + /// public override int Identifier { get { return 3; } } + /// + /// Returns whether this serializer needs a manifest in the fromBinary method + /// public override bool IncludeManifest { get { return false; } @@ -52,6 +68,12 @@ private object Deserialize(ByteString bytes, Type type) return o; } + /// + /// Serializes the given object into a byte array + /// + /// The object to serialize + /// A byte array containing the serialized object + /// Can't serialize a non- message using public override byte[] ToBinary(object obj) { var msg = obj as DaemonMsgCreate; @@ -78,7 +100,7 @@ private PropsData GetPropsData(Props props) .SetDeploy(GetDeployData(props.Deploy)); foreach (object arg in props.Arguments) - { + { if (arg == null) { builder = builder.AddArgs(ByteString.Empty); @@ -88,7 +110,7 @@ private PropsData GetPropsData(Props props) { builder = builder.AddArgs(Serialize(arg)); builder = builder.AddClasses(arg.GetType().AssemblyQualifiedName); - } + } } return builder.Build(); @@ -110,10 +132,36 @@ private DeployData GetDeployData(Deploy deploy) return res.Build(); } + /// + /// Deserializes a byte array into an object of type . + /// + /// The array containing the serialized object + /// The type of object contained in the array + /// The object contained in the array + /// + /// Could not find type on the remote system. + /// Ensure that the remote system has an assembly that contains the type in its assembly search path. + /// public override object FromBinary(byte[] bytes, Type type) { var proto = DaemonMsgCreateData.ParseFrom(bytes); - var clazz = Type.GetType(proto.Props.Clazz); + Type clazz; + + try + { + clazz = Type.GetType(proto.Props.Clazz, true); + } + catch (TypeLoadException ex) + { + var msg = string.Format( + "Could not find type '{0}' on the remote system. " + + "Ensure that the remote system has an assembly that contains the type {0} in its assembly search path", + proto.Props.Clazz); + + + throw new TypeLoadException(msg, ex); + } + var args = GetArgs(proto); var props = new Props(GetDeploy(proto.Props.Deploy), clazz, args); return new DaemonMsgCreate( @@ -156,7 +204,7 @@ private IEnumerable GetArgs(DaemonMsgCreateData proto) { var args = new object[proto.Props.ArgsCount]; for (int i = 0; i < args.Length; i++) - { + { var typeName = proto.Props.GetClasses(i); var arg = proto.Props.GetArgs(i); if (typeName == "" && ByteString.Empty.Equals(arg)) @@ -183,4 +231,3 @@ private IActorRef DeserializeActorRef(ActorRefData actorRefData) } } } - diff --git a/src/core/Akka.Remote/Serialization/MessageContainerSerializer.cs b/src/core/Akka.Remote/Serialization/MessageContainerSerializer.cs index 939ff1ca167..2d8c9d4135d 100644 --- a/src/core/Akka.Remote/Serialization/MessageContainerSerializer.cs +++ b/src/core/Akka.Remote/Serialization/MessageContainerSerializer.cs @@ -13,22 +13,42 @@ namespace Akka.Remote.Serialization { + /// + /// This is a special that serializes and deserializes only. + /// public class MessageContainerSerializer : Serializer { + /// + /// Initializes a new instance of the class. + /// + /// The actor system to associate with this serializer. public MessageContainerSerializer(ExtendedActorSystem system) : base(system) { } + /// + /// Completely unique value to identify this implementation of Serializer, used to optimize network traffic + /// Values from 0 to 16 is reserved for Akka internal usage + /// public override int Identifier { get { return 6; } } + /// + /// Returns whether this serializer needs a manifest in the fromBinary method + /// public override bool IncludeManifest { get { return false; } } + /// + /// Serializes the given object into a byte array + /// + /// The object to serialize + /// A byte array containing the serialized object + /// Object must be of type public override byte[] ToBinary(object obj) { if (!(obj is ActorSelectionMessage)) @@ -84,6 +104,13 @@ private byte[] SerializeActorSelectionMessage(ActorSelectionMessage sel) return builder.Build().ToByteArray(); } + /// + /// Deserializes a byte array into an object of type . + /// + /// The array containing the serialized object + /// The type of object contained in the array + /// The object contained in the array + /// Unknown SelectionEnvelope.Elements.Type public override object FromBinary(byte[] bytes, Type type) { SelectionEnvelope selectionEnvelope = SelectionEnvelope.ParseFrom(bytes); @@ -108,4 +135,3 @@ public override object FromBinary(byte[] bytes, Type type) } } } - diff --git a/src/core/Akka.Remote/Serialization/ProtobufSerializer.cs b/src/core/Akka.Remote/Serialization/ProtobufSerializer.cs index 64ca91d08c5..324dd30a77e 100644 --- a/src/core/Akka.Remote/Serialization/ProtobufSerializer.cs +++ b/src/core/Akka.Remote/Serialization/ProtobufSerializer.cs @@ -11,22 +11,42 @@ namespace Akka.Remote.Serialization { + /// + /// This is a special that serializes and deserializes Google protobuf messages only. + /// public class ProtobufSerializer : Serializer { + /// + /// Initializes a new instance of the class. + /// + /// The actor system to associate with this serializer. public ProtobufSerializer(ExtendedActorSystem system) : base(system) { } - public override bool IncludeManifest + /// + /// Completely unique value to identify this implementation of Serializer, used to optimize network traffic + /// Values from 0 to 16 is reserved for Akka internal usage + /// + public override int Identifier { - get { return true; } + get { return 2; } } - public override int Identifier + /// + /// Returns whether this serializer needs a manifest in the fromBinary method + /// + public override bool IncludeManifest { - get { return 2; } + get { return true; } } + /// + /// Serializes the given object into a byte array + /// + /// The object to serialize + /// A byte array containing the serialized object + /// This method is not currently implemented. public override byte[] ToBinary(object obj) { throw new NotImplementedException(); @@ -37,6 +57,13 @@ public override byte[] ToBinary(object obj) //} } + /// + /// Deserializes a byte array into an object of type . + /// + /// The array containing the serialized object + /// The type of object contained in the array + /// The object contained in the array + /// This method is not currently implemented. public override object FromBinary(byte[] bytes, Type type) { throw new NotImplementedException(); @@ -47,4 +74,3 @@ public override object FromBinary(byte[] bytes, Type type) } } } - diff --git a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs index 0a45b7ef0ac..8edbb4ce4ac 100644 --- a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs +++ b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs @@ -105,7 +105,7 @@ public AckAndMessage(Ack ackOption, Message messageOption) /// /// INTERNAL API /// - /// A Codec that is able to convert Akka PDUs from and to + /// A codec that is able to convert Akka PDUs from and to /// internal abstract class AkkaPduCodec { diff --git a/src/core/Akka.Remote/Transport/AkkaProtocolTransport.cs b/src/core/Akka.Remote/Transport/AkkaProtocolTransport.cs index 4061dc76984..8b5916189d9 100644 --- a/src/core/Akka.Remote/Transport/AkkaProtocolTransport.cs +++ b/src/core/Akka.Remote/Transport/AkkaProtocolTransport.cs @@ -37,17 +37,22 @@ public ProtocolTransportAddressPair(AkkaProtocolTransport protocolTransport, Add } /// - /// An that can occur during the course of an Akka Protocol handshake. + /// This exception is thrown when an error occurred during the Akka protocol handshake. /// public class AkkaProtocolException : AkkaException { /// - /// Constructor. + /// Initializes a new instance of the class. /// - /// The error message. - /// The internal exception (null by default.) + /// The message that describes the error. + /// The exception that is the cause of the current exception. public AkkaProtocolException(string message, Exception cause = null) : base(message, cause) { } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected AkkaProtocolException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/core/Akka.Remote/Transport/FailureInjectorTransportAdapter.cs b/src/core/Akka.Remote/Transport/FailureInjectorTransportAdapter.cs index 203cf031c5f..4537682d2c1 100644 --- a/src/core/Akka.Remote/Transport/FailureInjectorTransportAdapter.cs +++ b/src/core/Akka.Remote/Transport/FailureInjectorTransportAdapter.cs @@ -29,20 +29,32 @@ public Transport Create(Transport wrappedTransport, ExtendedActorSystem system) } /// - /// The failure we're going to inject into a transport, of course :) + /// This exception is used to indicate a simulated failure in an association. /// public sealed class FailureInjectorException : AkkaException { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. public FailureInjectorException(string msg) { Msg = msg; } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. private FailureInjectorException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// + /// Retrieves the message of the simulated failure. + /// public string Msg { get; private set; } } diff --git a/src/core/Akka.Remote/Transport/Helios/HeliosTransport.cs b/src/core/Akka.Remote/Transport/Helios/HeliosTransport.cs index 9b5c3069225..cdb356b3f22 100644 --- a/src/core/Akka.Remote/Transport/Helios/HeliosTransport.cs +++ b/src/core/Akka.Remote/Transport/Helios/HeliosTransport.cs @@ -172,6 +172,14 @@ public override string SchemeIdentifier } } + public override long MaximumPayloadBytes + { + get + { + return Settings.MaxFrameSize; + } + } + protected ILoggingAdapter Log; /// diff --git a/src/core/Akka.Remote/Transport/TestTransport.cs b/src/core/Akka.Remote/Transport/TestTransport.cs index 8d7514af021..20caaaa93a0 100644 --- a/src/core/Akka.Remote/Transport/TestTransport.cs +++ b/src/core/Akka.Remote/Transport/TestTransport.cs @@ -41,8 +41,11 @@ private TaskCompletionSource _associationListenerProm public TestTransport(ActorSystem system, Config conf) : this( - Address.Parse(GetConfigString(conf, "local-address")), AssociationRegistry.Get(GetConfigString(conf,"registry-key")), - GetConfigString(conf,"scheme-identifier")) { } + Address.Parse(GetConfigString(conf, "local-address")), + AssociationRegistry.Get(GetConfigString(conf,"registry-key")), + conf.GetByteSize("maximum-payload-bytes") ?? 32000, + GetConfigString(conf,"scheme-identifier") + ) { } private static string GetConfigString(Config conf, string name) { @@ -51,10 +54,11 @@ private static string GetConfigString(Config conf, string name) return value; } - public TestTransport(Address localAddress, AssociationRegistry registry, string schemeIdentifier = "test") + public TestTransport(Address localAddress, AssociationRegistry registry, long maximumPayloadBytes = 32000, string schemeIdentifier = "test") { LocalAddress = localAddress; _registry = registry; + MaximumPayloadBytes = maximumPayloadBytes; SchemeIdentifier = schemeIdentifier; ListenBehavior = new SwitchableLoggedBehavior>>( @@ -329,7 +333,7 @@ public SwitchableLoggedBehavior(Func> defaultBehavior, Action /// Changes the current behavior to the provided one /// - /// Function that takes a parameter type and returns a Task + /// Function that takes a parameter type and returns a Task. public void Push(Func> behavior) { _behaviorStack.Push(behavior); @@ -480,7 +484,7 @@ public bool TransportsReady(params Address[] addresses) /// /// Ordered pair of addresses representing an association. First element must be the address of the initiator. /// A pair of listeners that will be responsible for handling the events of the two endpoints - /// of the association. Elements in the Tuple must be in the same order as the addresses in . + /// of the association. Elements in the Tuple must be in the same order as the addresses in . public void RegisterListenerPair(Tuple key, Tuple listeners) { diff --git a/src/core/Akka.Remote/Transport/Transport.cs b/src/core/Akka.Remote/Transport/Transport.cs index 95a7eb9cf16..3e68988ad12 100644 --- a/src/core/Akka.Remote/Transport/Transport.cs +++ b/src/core/Akka.Remote/Transport/Transport.cs @@ -21,6 +21,7 @@ public abstract class Transport public ActorSystem System { get; protected set; } public virtual string SchemeIdentifier { get; protected set; } + public virtual long MaximumPayloadBytes { get; protected set; } public abstract Task>> Listen(); public abstract bool IsResponsibleFor(Address remote); @@ -59,15 +60,25 @@ public virtual Task ManagementCommand(object message) } /// - /// Indicates that the association setup request is invalid and it is impossible to recover (malformed IP address, unknown hostname, etc...) + /// This exception is thrown when an association setup request is invalid and it is impossible to recover (malformed IP address, unknown hostname, etc...). /// public class InvalidAssociationException : AkkaException { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. public InvalidAssociationException(string message, Exception cause = null) : base(message, cause) { } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected InvalidAssociationException(SerializationInfo info, StreamingContext context) : base(info, context) { @@ -138,22 +149,37 @@ public enum DisassociateInfo /// public interface IHandleEventListener { + /// + /// Notify the listener about an . + /// + /// The to notify the listener about void Notify(IHandleEvent ev); } /// - /// Converts an instance into an , so messages + /// Converts an into an , so messages /// can be passed directly to the Actor. /// public sealed class ActorHandleEventListener : IHandleEventListener { + /// + /// The Actor to notify about messages. + /// public readonly IActorRef Actor; + /// + /// Initializes a new instance of the class. + /// + /// The Actor to notify about messages. public ActorHandleEventListener(IActorRef actor) { Actor = actor; } + /// + /// Notify the Actor about an message. + /// + /// The message to notify the Actor about public void Notify(IHandleEvent ev) { Actor.Tell(ev); @@ -188,22 +214,37 @@ public InboundAssociation(AssociationHandle association) /// public interface IAssociationEventListener { + /// + /// Notify the listener about an message. + /// + /// The message to notify the listener about void Notify(IAssociationEvent ev); } /// - /// Converts an instance into an , so messages + /// Converts an into an , so messages /// can be passed directly to the Actor. /// public sealed class ActorAssociationEventListener : IAssociationEventListener { + /// + /// Initializes a new instance of the class. + /// + /// The Actor to notify about messages. public ActorAssociationEventListener(IActorRef actor) { Actor = actor; } + /// + /// The Actor to notify about messages. + /// public IActorRef Actor { get; private set; } + /// + /// Notify the Actor about an . + /// + /// The message to notify the Actor about public void Notify(IAssociationEvent ev) { Actor.Tell(ev); @@ -244,7 +285,7 @@ protected AssociationHandle(Address localAddress, Address remoteAddress) public TaskCompletionSource ReadHandlerSource { get; protected set; } /// - /// Asynchronously sends the specified to the remote endpoint. This method's implementation MUST be thread-safe + /// Asynchronously sends the specified to the remote endpoint. This method's implementation MUST be thread-safe /// as it might be called from different threads. This method MUST NOT block. /// /// Writes guarantee ordering of messages, but not their reception. The call to write returns with a boolean indicating if the diff --git a/src/core/Akka.Remote/Transport/TransportAdapters.cs b/src/core/Akka.Remote/Transport/TransportAdapters.cs index 9b929dde0af..454181e6441 100644 --- a/src/core/Akka.Remote/Transport/TransportAdapters.cs +++ b/src/core/Akka.Remote/Transport/TransportAdapters.cs @@ -147,6 +147,14 @@ public override string SchemeIdentifier } } + public override long MaximumPayloadBytes + { + get + { + return WrappedTransport.MaximumPayloadBytes; + } + } + protected abstract Task InterceptListen(Address listenAddress, Task listenerTask); diff --git a/src/core/Akka.TestKit.Tests/Akka.TestKit.Tests.csproj b/src/core/Akka.TestKit.Tests/Akka.TestKit.Tests.csproj index 12b8528062a..a371197b9ec 100644 --- a/src/core/Akka.TestKit.Tests/Akka.TestKit.Tests.csproj +++ b/src/core/Akka.TestKit.Tests/Akka.TestKit.Tests.csproj @@ -13,7 +13,8 @@ 512 ..\..\ true - 71eec8b3 + + true @@ -34,14 +35,17 @@ - + ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True - + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True - + ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True @@ -113,7 +117,6 @@ - diff --git a/src/core/Akka.TestKit/Akka.TestKit.csproj b/src/core/Akka.TestKit/Akka.TestKit.csproj index edc98f4d41f..21e25311a7b 100644 --- a/src/core/Akka.TestKit/Akka.TestKit.csproj +++ b/src/core/Akka.TestKit/Akka.TestKit.csproj @@ -111,6 +111,7 @@ + diff --git a/src/core/Akka.TestKit/Configs/TestConfigs.cs b/src/core/Akka.TestKit/Configs/TestConfigs.cs index ad4540e4ae7..cb2866935f3 100644 --- a/src/core/Akka.TestKit/Configs/TestConfigs.cs +++ b/src/core/Akka.TestKit/Configs/TestConfigs.cs @@ -1,4 +1,11 @@ -using Akka.Configuration; +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using Akka.Configuration; namespace Akka.TestKit.Configs { diff --git a/src/core/Akka.TestKit/EventFilter/IEventFilterApplier.cs b/src/core/Akka.TestKit/EventFilter/IEventFilterApplier.cs index 574363bf0d0..41642b3aa94 100644 --- a/src/core/Akka.TestKit/EventFilter/IEventFilterApplier.cs +++ b/src/core/Akka.TestKit/EventFilter/IEventFilterApplier.cs @@ -123,7 +123,7 @@ public interface IEventFilterApplier /// /// Prevents events from being logged from now on. To allow events to be logged again, call - /// Unmute on the returned object. + /// on the returned object. /// /// /// var filter = EventFilter.Debug().Mute(); diff --git a/src/core/Akka.TestKit/INoImplicitSender.cs b/src/core/Akka.TestKit/INoImplicitSender.cs index 91e19e444a1..4f77057476a 100644 --- a/src/core/Akka.TestKit/INoImplicitSender.cs +++ b/src/core/Akka.TestKit/INoImplicitSender.cs @@ -10,8 +10,8 @@ namespace Akka.TestKit { /// - /// Normally test classes has TestActor as implicit sender. - /// So when no sender is specified when sending messages, TestActor + /// Normally test classes has as implicit sender. + /// So when no sender is specified when sending messages, /// is used. /// When a a test class implements this behavior is removed and the normal /// behavior is restored, i.e. is used as sender when no sender has been specified. diff --git a/src/core/Akka.TestKit/Internal/BlockingQueue.cs b/src/core/Akka.TestKit/Internal/BlockingQueue.cs index 4cbb12a7588..51cf4659dda 100644 --- a/src/core/Akka.TestKit/Internal/BlockingQueue.cs +++ b/src/core/Akka.TestKit/Internal/BlockingQueue.cs @@ -193,9 +193,6 @@ IEnumerator IEnumerable.GetEnumerator() public object SyncRoot { get { throw new NotImplementedException(); } } public bool IsSynchronized { get { return false; } } - - - } } } diff --git a/src/core/Akka.TestKit/TestActorRef.cs b/src/core/Akka.TestKit/TestActorRef.cs index 69df849b013..2ea4d12fd1c 100644 --- a/src/core/Akka.TestKit/TestActorRef.cs +++ b/src/core/Akka.TestKit/TestActorRef.cs @@ -14,7 +14,7 @@ namespace Akka.TestKit /// overrides the dispatcher to and sets the receiveTimeout to None. Otherwise, /// it acts just like a normal ActorRef. You may retrieve a reference to the underlying actor to test internal logic. /// A can be implicitly casted to an or you can get the actual - /// from the Ref property. + /// from the property. /// /// The type of actor public class TestActorRef : TestActorRefBase where TActor : ActorBase diff --git a/src/core/Akka.TestKit/TestActorRefBase.cs b/src/core/Akka.TestKit/TestActorRefBase.cs index f32c99d8614..d984dd146f7 100644 --- a/src/core/Akka.TestKit/TestActorRefBase.cs +++ b/src/core/Akka.TestKit/TestActorRefBase.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using Akka.Actor; using Akka.Dispatch; +using Akka.Dispatch.SysMsg; using Akka.TestKit.Internal; using Akka.Util; @@ -139,6 +140,11 @@ public override int GetHashCode() return _internalRef.GetHashCode(); } + public int CompareTo(object obj) + { + return ((IComparable) _internalRef).CompareTo(obj); + } + public bool Equals(IActorRef other) { return _internalRef.Equals(other); @@ -233,6 +239,11 @@ void IInternalActorRef.Suspend() { _internalRef.Suspend(); } + + public void SendSystemMessage(ISystemMessage message, IActorRef sender) + { + _internalRef .SendSystemMessage(message, sender); + } } } diff --git a/src/core/Akka.TestKit/TestActors/EchoActor.cs b/src/core/Akka.TestKit/TestActors/EchoActor.cs index b0eabd81d61..679dc2c2477 100644 --- a/src/core/Akka.TestKit/TestActors/EchoActor.cs +++ b/src/core/Akka.TestKit/TestActors/EchoActor.cs @@ -11,9 +11,9 @@ namespace Akka.TestKit.TestActors { /// /// An is an actor that echoes whatever is sent to it, to the - /// TestKit's TestActor. - /// By default it also echoes back to the sender, unless the sender is the TestActor - /// (in this case the TestActor will only receive one message). + /// TestKit's . + /// By default it also echoes back to the sender, unless the sender is the + /// (in this case the will only receive one message). /// public class EchoActor : ReceiveActor { @@ -32,9 +32,9 @@ public EchoActor(TestKitBase testkit, bool echoBackToSenderAsWell=true) /// /// Returns a object that can be used to create an . /// The echoes whatever is sent to it, to the - /// TestKit's TestActor. - /// By default it also echoes back to the sender, unless the sender is the TestActor - /// (in this case the TestActor will only receive one message) or unless + /// TestKit's . + /// By default it also echoes back to the sender, unless the sender is the + /// (in this case the will only receive one message) or unless /// has been set to false. /// public static Props Props(TestKitBase testkit, bool echoBackToSenderAsWell = true) diff --git a/src/core/Akka.TestKit/TestBreaker.cs b/src/core/Akka.TestKit/TestBreaker.cs new file mode 100644 index 00000000000..1d3f858b98f --- /dev/null +++ b/src/core/Akka.TestKit/TestBreaker.cs @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Akka.Pattern; + +namespace Akka.TestKit +{ + public class TestBreaker + { + public CountdownEvent HalfOpenLatch { get; private set; } + public CountdownEvent OpenLatch { get; private set; } + public CountdownEvent ClosedLatch { get; private set; } + public CircuitBreaker Instance { get; private set; } + + public TestBreaker( CircuitBreaker instance ) + { + HalfOpenLatch = new CountdownEvent( 1 ); + OpenLatch = new CountdownEvent( 1 ); + ClosedLatch = new CountdownEvent( 1 ); + Instance = instance; + Instance.OnClose( ( ) => { if ( !ClosedLatch.IsSet ) ClosedLatch.Signal( ); } ) + .OnHalfOpen( ( ) => { if ( !HalfOpenLatch.IsSet ) HalfOpenLatch.Signal( ); } ) + .OnOpen( ( ) => { if ( !OpenLatch.IsSet ) OpenLatch.Signal( ); } ); + } + + + } +} \ No newline at end of file diff --git a/src/core/Akka.TestKit/TestKitBase_Receive.cs b/src/core/Akka.TestKit/TestKitBase_Receive.cs index b80c827b15d..bd71e859b9c 100644 --- a/src/core/Akka.TestKit/TestKitBase_Receive.cs +++ b/src/core/Akka.TestKit/TestKitBase_Receive.cs @@ -246,7 +246,7 @@ private bool InternalTryReceiveOne(out MessageEnvelope envelope, TimeSpan? max, /// /// Receive a series of messages. - /// It will continue to receive messages until the predicate returns false or the idle + /// It will continue to receive messages until the predicate returns false or the idle /// timeout is met (disabled by default) or the overall /// maximum duration is elapsed or expected messages count is reached. /// If a message that isn't of type the parameter diff --git a/src/core/Akka.TestKit/TestKitBase_Within.cs b/src/core/Akka.TestKit/TestKitBase_Within.cs index d9f82ce0633..88051f8ac82 100644 --- a/src/core/Akka.TestKit/TestKitBase_Within.cs +++ b/src/core/Akka.TestKit/TestKitBase_Within.cs @@ -15,7 +15,7 @@ public abstract partial class TestKitBase { /// - /// Execute code block while bounding its execution time between 0 seconds and . + /// Execute code block while bounding its execution time between 0 seconds and . /// `within` blocks may be nested. All methods in this class which take maximum wait times /// are available in a version which implicitly uses the remaining time governed by /// the innermost enclosing `within` block. @@ -27,7 +27,7 @@ public void Within(TimeSpan max, Action action) } /// - /// Execute code block while bounding its execution time between and . + /// Execute code block while bounding its execution time between and . /// `within` blocks may be nested. All methods in this class which take maximum wait times /// are available in a version which implicitly uses the remaining time governed by /// the innermost enclosing `within` block. @@ -40,7 +40,7 @@ public void Within(TimeSpan min, TimeSpan max, Action action, string hint = null /// - /// Execute code block while bounding its execution time between 0 seconds and . + /// Execute code block while bounding its execution time between 0 seconds and . /// `within` blocks may be nested. All methods in this class which take maximum wait times /// are available in a version which implicitly uses the remaining time governed by /// the innermost enclosing `within` block. @@ -52,7 +52,7 @@ public T Within(TimeSpan max, Func function) } /// - /// Execute code block while bounding its execution time between and . + /// Execute code block while bounding its execution time between and . /// `within` blocks may be nested. All methods in this class which take maximum wait times /// are available in a version which implicitly uses the remaining time governed by /// the innermost enclosing `within` block. diff --git a/src/core/Akka.TestKit/TestKitSettings.cs b/src/core/Akka.TestKit/TestKitSettings.cs index 39d75e118b4..0d899cf49ba 100644 --- a/src/core/Akka.TestKit/TestKitSettings.cs +++ b/src/core/Akka.TestKit/TestKitSettings.cs @@ -64,7 +64,7 @@ public TestKitSettings(Config config) /// /// If set to true calls to testkit will be logged. - /// This is enabled by seting configuration "akka.test.testkit.debug" value to a true. + /// This is enabled by setting the configuration value "akka.test.testkit.debug" to a true. /// public bool LogTestKitCalls { get { return _logTestKitCalls; } } } diff --git a/src/core/Akka.TestKit/TestProbe.cs b/src/core/Akka.TestKit/TestProbe.cs index 2ef8083829b..a84bc3b4c29 100644 --- a/src/core/Akka.TestKit/TestProbe.cs +++ b/src/core/Akka.TestKit/TestProbe.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using Akka.Actor; +using Akka.Dispatch.SysMsg; using Akka.Util; namespace Akka.TestKit @@ -138,6 +139,16 @@ void IInternalActorRef.Suspend() { ((IInternalActorRef)TestActor).Suspend(); } + + public void SendSystemMessage(ISystemMessage message, IActorRef sender) + { + ((IInternalActorRef)TestActor).SendSystemMessage(message, sender); + } + + public int CompareTo(object obj) + { + return TestActor.CompareTo(obj); + } } } diff --git a/src/core/Akka.Tests.Shared.Internals/Akka.Tests.Shared.Internals.csproj b/src/core/Akka.Tests.Shared.Internals/Akka.Tests.Shared.Internals.csproj index a4718baa02a..40c5822ac6f 100644 --- a/src/core/Akka.Tests.Shared.Internals/Akka.Tests.Shared.Internals.csproj +++ b/src/core/Akka.Tests.Shared.Internals/Akka.Tests.Shared.Internals.csproj @@ -14,7 +14,8 @@ 512 ..\..\ true - 39789108 + + true @@ -39,14 +40,17 @@ - + ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True - + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True - + ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True diff --git a/src/core/Akka.Tests.Shared.Internals/AkkaSpec.cs b/src/core/Akka.Tests.Shared.Internals/AkkaSpec.cs index e55f9a8a343..4c968439557 100644 --- a/src/core/Akka.Tests.Shared.Internals/AkkaSpec.cs +++ b/src/core/Akka.Tests.Shared.Internals/AkkaSpec.cs @@ -12,6 +12,7 @@ using System.Threading; using Akka.Configuration; using Xunit; +using Xunit.Abstractions; using Xunit.Sdk; // ReSharper disable once CheckNamespace @@ -39,13 +40,13 @@ public abstract class AkkaSpec : Xunit2.TestKit //AkkaSpec is not part of Tes private static int _systemNumber = 0; - public AkkaSpec(string config) - : this(ConfigurationFactory.ParseString(config).WithFallback(_akkaSpecConfig)) + public AkkaSpec(string config, ITestOutputHelper output = null) + : this(ConfigurationFactory.ParseString(config).WithFallback(_akkaSpecConfig), output) { } - public AkkaSpec(Config config = null) - : base(config.SafeWithFallback(_akkaSpecConfig), GetCallerName()) + public AkkaSpec(Config config = null, ITestOutputHelper output = null) + : base(config.SafeWithFallback(_akkaSpecConfig), GetCallerName(), output) { BeforeAll(); } diff --git a/src/core/Akka.Tests/Actor/PipeToSupportSpec.cs b/src/core/Akka.Tests/Actor/PipeToSupportSpec.cs new file mode 100644 index 00000000000..2b07a60a66e --- /dev/null +++ b/src/core/Akka.Tests/Actor/PipeToSupportSpec.cs @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Event; +using Akka.TestKit; +using Xunit; + +namespace Akka.Tests.Actor +{ + public class PipeToSupportSpec : AkkaSpec + { + private TaskCompletionSource _taskCompletionSource; + private Task _task; + + public PipeToSupportSpec() + { + _taskCompletionSource = new TaskCompletionSource(); + _task = _taskCompletionSource.Task; + Sys.EventStream.Subscribe(TestActor, typeof(DeadLetter)); + } + + [Fact] + public void Should_by_default_send_task_result_as_message() + { + _task.PipeTo(TestActor); + _taskCompletionSource.SetResult("Hello"); + ExpectMsg("Hello"); + } + + [Fact] + public void Should_by_default_send_task_exception_as_status_failure_message() + { + _task.PipeTo(TestActor); + _taskCompletionSource.SetException(new Exception("Boom")); + ExpectMsg(x => x.Cause.InnerException.Message == "Boom"); + } + + [Fact] + public void Should_use_success_handling_to_transform_task_result() + { + _task.PipeTo(TestActor, success: x => "Hello " + x); + _taskCompletionSource.SetResult("World"); + ExpectMsg("Hello World"); + } + + [Fact] + public void Should_use_failure_handling_to_transform_task_exception() + { + _task.PipeTo(TestActor, failure: e => "Such a " + e.InnerException.Message); + _taskCompletionSource.SetException(new Exception("failure...")); + ExpectMsg("Such a failure..."); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Tests/Actor/PropsSpec.cs b/src/core/Akka.Tests/Actor/PropsSpec.cs index fc40ea51b31..e2df96090dc 100644 --- a/src/core/Akka.Tests/Actor/PropsSpec.cs +++ b/src/core/Akka.Tests/Actor/PropsSpec.cs @@ -8,6 +8,7 @@ using Akka.Actor; using Akka.TestKit; using System; +using System.Linq; using Xunit; namespace Akka.Tests.Actor @@ -56,6 +57,25 @@ public void Props_created_with_strategy_must_have_it_set() Assert.Equal(strategy, props.SupervisorStrategy); } + [Fact] + public void Props_created_with_null_type_must_throw() + { + Type missingType = null; + object[] args = new object[0]; + var argsEnumerable = Enumerable.Empty(); + var defaultStrategy = SupervisorStrategy.DefaultStrategy; + var defaultDeploy = Deploy.Local; + + Props p = null; + + Assert.Throws("type", () => p = new Props(missingType, args)); + Assert.Throws("type", () => p = new Props(missingType)); + Assert.Throws("type", () => p = new Props(missingType, defaultStrategy, argsEnumerable)); + Assert.Throws("type", () => p = new Props(missingType, defaultStrategy, args)); + Assert.Throws("type", () => p = new Props(defaultDeploy, missingType, argsEnumerable)); + Assert.Throws("type", () => p = Props.Create(missingType, args)); + } + private class TestProducer : IIndirectActorProducer { TestLatch latchActor; diff --git a/src/core/Akka.Tests/Actor/RelativeActorPathSpec.cs b/src/core/Akka.Tests/Actor/RelativeActorPathSpec.cs index f782d62b9b1..d4856bd4054 100644 --- a/src/core/Akka.Tests/Actor/RelativeActorPathSpec.cs +++ b/src/core/Akka.Tests/Actor/RelativeActorPathSpec.cs @@ -27,6 +27,13 @@ public void RelativeActorPath_must_match_single_name() Elements("foo").ShouldBe(new List(){"foo"}); } + [Fact] + public void RelativeActorPath_starting_with_slash_must_match_single_name() + { + Elements("/foo").ShouldBe(new List() { "foo" }); + } + + [Fact] public void RelativeActorPath_must_match_path_separated_names() { diff --git a/src/core/Akka.Tests/Actor/Stash/ActorWithStashSpec.cs b/src/core/Akka.Tests/Actor/Stash/ActorWithStashSpec.cs index 49658389780..b525d9fb044 100644 --- a/src/core/Akka.Tests/Actor/Stash/ActorWithStashSpec.cs +++ b/src/core/Akka.Tests/Actor/Stash/ActorWithStashSpec.cs @@ -302,8 +302,85 @@ public StateObj(TestKitBase testKit) public TestBarrier Finished; public TestLatch ExpectedException; } - } + [Fact] + public void An_actor_should_not_throw_an_exception_if_sent_two_messages_with_same_value_different_reference() + { + _state.ExpectedException = new TestLatch(); + var stasher = ActorOf("stashing-actor"); + stasher.Tell(new CustomMessageOverrideEquals("A")); + stasher.Tell(new CustomMessageOverrideEquals("A")); + + // NOTE: + // here we should test for no exception thrown.. + // but I don't know how.... + } + + public class CustomMessageOverrideEquals + { + + public CustomMessageOverrideEquals(string cargo) + { + Cargo = cargo; + } + public override int GetHashCode() + { + return base.GetHashCode() ^ 314; + } + public override bool Equals(System.Object obj) + { + // If parameter is null return false. + if (obj == null) + { + return false; + } + + // If parameter cannot be cast to Point return false. + CustomMessageOverrideEquals p = obj as CustomMessageOverrideEquals; + if ((System.Object)p == null) + { + return false; + } + + // Return true if the fields match: + return (Cargo == p.Cargo); + } + + public bool Equals(CustomMessageOverrideEquals p) + { + // If parameter is null return false: + if ((object)p == null) + { + return false; + } + + // Return true if the fields match: + return (Cargo == p.Cargo); + } + public static bool operator ==(CustomMessageOverrideEquals a, CustomMessageOverrideEquals b) + { + // If both are null, or both are same instance, return true. + if (System.Object.ReferenceEquals(a, b)) + { + return true; + } + // If one is null, but not both, return false. + if (((object)a == null) || ((object)b == null)) + { + return false; + } + + // Return true if the fields match: + return (a.Cargo == b.Cargo); + } + + public static bool operator !=(CustomMessageOverrideEquals a, CustomMessageOverrideEquals b) + { + return !(a == b); + } + public string Cargo { get; private set; } + } + } } diff --git a/src/core/Akka.Tests/Akka.Tests.csproj b/src/core/Akka.Tests/Akka.Tests.csproj index 16e21f4965a..130e568c8d7 100644 --- a/src/core/Akka.Tests/Akka.Tests.csproj +++ b/src/core/Akka.Tests/Akka.Tests.csproj @@ -14,7 +14,8 @@ 512 ..\..\ true - 1318f3e8 + + true @@ -55,28 +56,42 @@ false - + + ..\..\packages\fastJSON.2.0.27.1\lib\net40\fastjson.dll + True + + ..\..\packages\FluentAssertions.3.3.0\lib\net45\FluentAssertions.dll + True - + ..\..\packages\FluentAssertions.3.3.0\lib\net45\FluentAssertions.Core.dll + True + + + ..\..\packages\FsCheck.2.0.5\lib\net45\FsCheck.dll + True + + + ..\..\packages\FSharp.Core.3.1.2.5\lib\net40\FSharp.Core.dll + True - - ..\..\packages\fastJSON.2.0.27.1\lib\net40\fastjson.dll - ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True - + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True - + ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True @@ -101,6 +116,7 @@ + @@ -150,6 +166,7 @@ + @@ -165,6 +182,7 @@ + @@ -197,11 +215,13 @@ Always - + + + @@ -225,7 +245,7 @@ - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. diff --git a/src/core/Akka.Tests/Configuration/ConfigurationSpec.cs b/src/core/Akka.Tests/Configuration/ConfigurationSpec.cs index 2684924ff6d..8eff7d3c34b 100644 --- a/src/core/Akka.Tests/Configuration/ConfigurationSpec.cs +++ b/src/core/Akka.Tests/Configuration/ConfigurationSpec.cs @@ -94,6 +94,19 @@ public class MyObjectConfig public bool BoolProperty { get; set; } public int[] IntergerArray { get; set; } } + + [Fact] + public void ParsingEmptyStringShouldProduceEmptyHoconRoot() + { + var value = Parser.Parse(string.Empty, null).Value; + value.IsEmpty.ShouldBeTrue(); + } + + [Fact] + public void Config_Empty_is_Empty() + { + ConfigurationFactory.Empty.IsEmpty.ShouldBeTrue(); + } } } diff --git a/src/core/Akka.Tests/Dispatch/AsyncAwaitSpec.cs b/src/core/Akka.Tests/Dispatch/AsyncAwaitSpec.cs index 47d23ce82fb..53d49d538ed 100644 --- a/src/core/Akka.Tests/Dispatch/AsyncAwaitSpec.cs +++ b/src/core/Akka.Tests/Dispatch/AsyncAwaitSpec.cs @@ -13,6 +13,24 @@ namespace Akka.Tests.Dispatch { + class ReceiveTimeoutAsyncActor : ReceiveActor + { + private IActorRef _replyTo; + public ReceiveTimeoutAsyncActor() + { + Receive(t => + { + _replyTo.Tell("GotIt"); + }); + Receive(async s => + { + _replyTo = Sender; + + await Task.Delay(TimeSpan.FromMilliseconds(100)); + SetReceiveTimeout(TimeSpan.FromMilliseconds(100)); + }); + } + } class AsyncActor : ReceiveActor { public AsyncActor() @@ -325,6 +343,16 @@ public async Task Actor_should_be_able_to_resume_suspend() var res = await asker.Ask("stop", TimeSpan.FromSeconds(5)); res.ShouldBe("done"); } + + + [Fact] + public void Actor_should_be_able_to_ReceiveTimeout_after_async_operation() + { + var actor = Sys.ActorOf(); + + actor.Tell("hello"); + ExpectMsg(m => m == "GotIt"); + } } } diff --git a/src/core/Akka.Tests/Event/EventStreamSpec.cs b/src/core/Akka.Tests/Event/EventStreamSpec.cs index 271417fb891..1a570bee5dd 100644 --- a/src/core/Akka.Tests/Event/EventStreamSpec.cs +++ b/src/core/Akka.Tests/Event/EventStreamSpec.cs @@ -12,6 +12,7 @@ using Akka.Tests.TestUtils; using System; using System.Linq; +using Akka.Util.Internal; using Xunit; namespace Akka.Tests.Event @@ -62,7 +63,9 @@ private class CCATBT : CC, ATT, BTT { } [Fact] public void ManageSubscriptions() { + var bus = new EventStream(true); + bus.StartUnsubscriber(Sys.AsInstanceOf()); bus.Subscribe(TestActor, typeof(M)); bus.Publish(new M { Value = 42 }); diff --git a/src/core/Akka.Tests/IO/SimpleDnsCacheSpec.cs b/src/core/Akka.Tests/IO/SimpleDnsCacheSpec.cs index bc003ba9112..9a518b3572f 100644 --- a/src/core/Akka.Tests/IO/SimpleDnsCacheSpec.cs +++ b/src/core/Akka.Tests/IO/SimpleDnsCacheSpec.cs @@ -65,5 +65,19 @@ public void Cache_should_sweep_out_expired_entries_on_cleanup() cache.Cached("test.local").ShouldBe(null); } + + [Fact] + public void Cache_should_be_updated_with_the_latest_resolved() + { + var localClock = new AtomicReference(0); + var cache = new SimpleDnsCacheTestDouble(localClock); + var cacheEntryOne = Dns.Resolved.Create("test.local", System.Net.Dns.GetHostEntry("127.0.0.1").AddressList); + var cacheEntryTwo = Dns.Resolved.Create("test.local", System.Net.Dns.GetHostEntry("127.0.0.1").AddressList); + long ttl = 500; + cache.Put(cacheEntryOne, ttl); + cache.Cached("test.local").ShouldBe(cacheEntryOne); + cache.Put(cacheEntryTwo, ttl); + cache.Cached("test.local").ShouldBe(cacheEntryTwo); + } } } diff --git a/src/core/Akka.Tests/IO/TcpConnectionSpec.cs b/src/core/Akka.Tests/IO/TcpConnectionSpec.cs index 2378fab1d4b..978b01a319d 100644 --- a/src/core/Akka.Tests/IO/TcpConnectionSpec.cs +++ b/src/core/Akka.Tests/IO/TcpConnectionSpec.cs @@ -25,7 +25,7 @@ namespace Akka.Tests.IO { - public class TcpConnectionSpec : AkkaSpec + class TcpConnectionSpec : AkkaSpec { internal class Ack : Tcp.Event { diff --git a/src/core/Akka.Tests/Pattern/CircuitBreakerSpec.cs b/src/core/Akka.Tests/Pattern/CircuitBreakerSpec.cs new file mode 100644 index 00000000000..12495d94d8c --- /dev/null +++ b/src/core/Akka.Tests/Pattern/CircuitBreakerSpec.cs @@ -0,0 +1,351 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Akka.Pattern; +using Akka.TestKit; +using Xunit; + +namespace Akka.Tests.Pattern +{ + public class ASynchronousCircuitBreakerThatIsClosed : CircuitBreakerSpecBase + { + [Fact(DisplayName = "A synchronous circuit breaker that is closed should allow call through")] + public void Should_Allow_Call_Through( ) + { + var breaker = LongCallTimeoutCb( ); + var result = breaker.Instance.WithSyncCircuitBreaker( ( ) => "Test" ); + + Assert.Equal( "Test", result ); + } + + [Fact( DisplayName = "A synchronous circuit breaker that is closed should increment failure count when call fails" )] + public void Should_Increment_FailureCount_When_Call_Fails( ) + { + var breaker = LongCallTimeoutCb( ); + + Assert.Equal( breaker.Instance.CurrentFailureCount, 0 ); + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithSyncCircuitBreaker( ThrowException ) ) ); + Assert.True( CheckLatch( breaker.OpenLatch ) ); + Assert.Equal( 1, breaker.Instance.CurrentFailureCount ); + } + + [Fact( DisplayName = "A synchronous circuit breaker that is closed should reset failure count when call succeeds" )] + public void Should_Reset_FailureCount_When_Call_Succeeds( ) + { + var breaker = MultiFailureCb( ); + + Assert.Equal( breaker.Instance.CurrentFailureCount, 0 ); + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithSyncCircuitBreaker( ThrowException ) ) ); + Assert.Equal( breaker.Instance.CurrentFailureCount, 1 ); + + breaker.Instance.WithSyncCircuitBreaker( ( ) => "Test" ); + + Assert.Equal( 0, breaker.Instance.CurrentFailureCount ); + } + + [Fact(DisplayName = "A synchronous circuit breaker that is closed should increment failure count when call times out")] + public void Should_Increment_FailureCount_When_Call_Times_Out( ) + { + var breaker = ShortCallTimeoutCb( ); + + breaker.Instance.WithSyncCircuitBreaker( ( ) => Thread.Sleep( 500 ) ); + + Assert.True( CheckLatch( breaker.OpenLatch ) ); + Assert.Equal( 1, breaker.Instance.CurrentFailureCount ); + } + } + + public class ASynchronousCircuitBreakerThatIsHalfOpen : CircuitBreakerSpecBase + { + [Fact(DisplayName = "A synchronous circuit breaker that is half open should pass call and transition to close on success")] + public void Should_Pass_Call_And_Transition_To_Close_On_Success( ) + { + var breaker = ShortResetTimeoutCb( ); + InterceptExceptionType( ( ) => breaker.Instance.WithSyncCircuitBreaker( ThrowException ) ); + Assert.True( CheckLatch( breaker.HalfOpenLatch ) ); + + var result = breaker.Instance.WithSyncCircuitBreaker( ( ) => SayTest( ) ); + + Assert.True( CheckLatch( breaker.ClosedLatch ) ); + Assert.Equal( SayTest( ), result ); + } + + [Fact(DisplayName = "A synchronous circuit breaker that is half open should pass call and transition to open on exception")] + public void Should_Pass_Call_And_Transition_To_Open_On_Exception( ) + { + var breaker = ShortResetTimeoutCb( ); + + + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithSyncCircuitBreaker( ThrowException ) ) ); + Assert.True( CheckLatch( breaker.HalfOpenLatch ) ); + + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithSyncCircuitBreaker( ThrowException ) ) ); + Assert.True( CheckLatch( breaker.OpenLatch ) ); + } + } + + public class ASynchronousCircuitBreakerThatIsOpen : CircuitBreakerSpecBase + { + [Fact(DisplayName = "A synchronous circuit breaker that is open should throw exceptions before reset timeout")] + public void Should_Throw_Exceptions_Before_Reset_Timeout( ) + { + var breaker = LongResetTimeoutCb( ); + + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithSyncCircuitBreaker( ThrowException ) ) ); + Assert.True( CheckLatch( breaker.OpenLatch ) ); + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithSyncCircuitBreaker( ThrowException ) ) ); + } + + [Fact(DisplayName = "A synchronous circuit breaker that is open should transition to half open when reset times out")] + public void Should_Transition_To_Half_Open_When_Reset_Times_Out( ) + { + var breaker = ShortResetTimeoutCb( ); + + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithSyncCircuitBreaker( ThrowException ) ) ); + Assert.True( CheckLatch( breaker.HalfOpenLatch ) ); + } + } + + public class AnAsynchronousCircuitBreakerThatIsClosed : CircuitBreakerSpecBase + { + [Fact(DisplayName = "An asynchronous circuit breaker that is closed should allow call through")] + public void Should_Allow_Call_Through( ) + { + var breaker = LongCallTimeoutCb( ); + var result = breaker.Instance.WithCircuitBreaker( () => Task.Run( ( ) => SayTest( ) ) ); + + Assert.Equal( SayTest( ), result.Result ); + } + + [Fact(DisplayName = "An asynchronous circuit breaker that is closed should increment failure count when call fails")] + public void Should_Increment_Failure_Count_When_Call_Fails( ) + { + var breaker = LongCallTimeoutCb( ); + + Assert.Equal( breaker.Instance.CurrentFailureCount, 0 ); + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithCircuitBreaker( () => Task.Run( ( ) => ThrowException( ) ) ).Wait( AwaitTimeout ) ) ); + Assert.True( CheckLatch( breaker.OpenLatch ) ); + Assert.Equal( 1, breaker.Instance.CurrentFailureCount ); + } + + [Fact(DisplayName = "An asynchronous circuit breaker that is closed should reset failure count when call succeeds after failure")] + public void Should_Reset_Failure_Count_When_Call_Succeeds_After_Failure( ) + { + var breaker = MultiFailureCb( ); + + Assert.Equal( breaker.Instance.CurrentFailureCount, 0 ); + + var whenall = Task.WhenAll( + breaker.Instance.WithCircuitBreaker(() => Task.Factory.StartNew(ThrowException)) + , breaker.Instance.WithCircuitBreaker(() => Task.Factory.StartNew(ThrowException)) + , breaker.Instance.WithCircuitBreaker(() => Task.Factory.StartNew(ThrowException)) + , breaker.Instance.WithCircuitBreaker(() => Task.Factory.StartNew(ThrowException))); + + Assert.True( InterceptExceptionType( ( ) => whenall.Wait( AwaitTimeout ) ) ); + + Assert.Equal( breaker.Instance.CurrentFailureCount, 4 ); + + var result = breaker.Instance.WithCircuitBreaker(() => Task.Run( ( ) => SayTest( ) ) ).Result; + + Assert.Equal( SayTest( ), result ); + Assert.Equal( 0, breaker.Instance.CurrentFailureCount ); + } + + [Fact(DisplayName = "An asynchronous circuit breaker that is closed should increment failure count when call times out")] + public void Should_Increment_Failure_Count_When_Call_Times_Out( ) + { + var breaker = ShortCallTimeoutCb( ); + + breaker.Instance.WithCircuitBreaker( () => Task.Factory.StartNew( ( ) => + { + Thread.Sleep( 500 ); + return SayTest( ); + } ) ); + + Assert.True( CheckLatch( breaker.OpenLatch ) ); + Assert.Equal( 1, breaker.Instance.CurrentFailureCount ); + } + } + + public class AnAsynchronousCircuitBreakerThatIsHalfOpen : CircuitBreakerSpecBase + { + [Fact(DisplayName = "An asynchronous circuit breaker that is half open should pass call and transition to close on success")] + public void Should_Pass_Call_And_Transition_To_Close_On_Success( ) + { + var breaker = ShortResetTimeoutCb( ); + InterceptExceptionType( ( ) => breaker.Instance.WithCircuitBreaker( () => Task.Factory.StartNew( ThrowException ) ) ); + Assert.True( CheckLatch( breaker.HalfOpenLatch ) ); + + var result = breaker.Instance.WithCircuitBreaker( () => Task.Factory.StartNew( ( ) => SayTest( ) ) ); + + Assert.True( CheckLatch( breaker.ClosedLatch ) ); + Assert.Equal( SayTest( ), result.Result ); + } + + [Fact(DisplayName = "An asynchronous circuit breaker that is half open should pass call and transition to open on exception")] + public void Should_Pass_Call_And_Transition_To_Open_On_Exception( ) + { + var breaker = ShortResetTimeoutCb( ); + + + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithCircuitBreaker( () => Task.Factory.StartNew( ThrowException ) ).Wait( ) ) ); + Assert.True( CheckLatch( breaker.HalfOpenLatch ) ); + + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithCircuitBreaker( () => Task.Factory.StartNew( ThrowException ) ).Wait( ) ) ); + Assert.True( CheckLatch( breaker.OpenLatch ) ); + } + + [Fact(DisplayName = "An asynchronous circuit breaker that is half open should pass call and transition to open on async failure")] + public void Should_Pass_Call_And_Transition_To_Open_On_Async_Failure( ) + { + var breaker = ShortResetTimeoutCb( ); + + breaker.Instance.WithCircuitBreaker( () => Task.Factory.StartNew( ThrowException ) ); + Assert.True( CheckLatch( breaker.HalfOpenLatch ) ); + + breaker.Instance.WithCircuitBreaker( () => Task.Factory.StartNew( ThrowException ) ); + Assert.True( CheckLatch( breaker.OpenLatch ) ); + } + } + + public class AnAsynchronousCircuitBreakerThatIsOpen : CircuitBreakerSpecBase + { + [Fact(DisplayName = "An asynchronous circuit breaker that is open should throw exceptions when called before reset timeout")] + public void Should_Throw_Exceptions_When_Called_Before_Reset_Timeout( ) + { + var breaker = LongResetTimeoutCb( ); + + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithCircuitBreaker( () => Task.Factory.StartNew( ThrowException ) ).Wait( ) ) ); + Assert.True( CheckLatch( breaker.OpenLatch ) ); + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithCircuitBreaker( () => Task.Factory.StartNew( ThrowException ) ).Wait( ) ) ); + } + + [Fact(DisplayName = "An asynchronous circuit breaker that is open should transition to half open when reset timeout")] + public void Should_Transition_To_Half_Open_When_Reset_Timeout( ) + { + var breaker = ShortResetTimeoutCb( ); + + Assert.True( InterceptExceptionType( ( ) => breaker.Instance.WithCircuitBreaker( () => Task.Factory.StartNew( ThrowException ) ).Wait( ) ) ); + Assert.True( CheckLatch( breaker.HalfOpenLatch ) ); + } + } + + public class CircuitBreakerSpecBase : AkkaSpec + { + private readonly TimeSpan _awaitTimeout = TimeSpan.FromSeconds(2); + public TimeSpan AwaitTimeout { get { return _awaitTimeout; } } + + public bool CheckLatch( CountdownEvent latch ) + { + return latch.Wait( AwaitTimeout ); + } + + public Task Delay( TimeSpan toDelay, CancellationToken? token ) + { + return token.HasValue ? Task.Delay( toDelay, token.Value ) : Task.Delay( toDelay ); + } + + public void ThrowException( ) + { + throw new TestException( "Test Exception" ); + } + + public string SayTest( ) + { + return "Test"; + } + + [SuppressMessage( "Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter" )] + public bool InterceptExceptionType( Action action ) where T : Exception + { + try + { + action.Invoke( ); + return false; + } + catch ( Exception ex ) + { + var aggregate = ex as AggregateException; + if ( aggregate != null ) + { + + // ReSharper disable once UnusedVariable + foreach ( var temp in aggregate.InnerExceptions.Select( innerException => innerException as T ).Where( temp => temp == null ) ) + { + throw; + } + } + else + { + var temp = ex as T; + + if ( temp == null ) + { + throw; + } + } + + } + return true; + } + + public TestBreaker ShortCallTimeoutCb( ) + { + return new TestBreaker( new CircuitBreaker( 1, TimeSpan.FromMilliseconds( 50 ), TimeSpan.FromMilliseconds( 500 ) ) ); + } + + public TestBreaker ShortResetTimeoutCb( ) + { + return new TestBreaker( new CircuitBreaker( 1, TimeSpan.FromMilliseconds( 1000 ), TimeSpan.FromMilliseconds( 50 ) ) ); + } + + public TestBreaker LongCallTimeoutCb( ) + { + return new TestBreaker( new CircuitBreaker( 1, TimeSpan.FromMilliseconds( 5000 ), TimeSpan.FromMilliseconds( 500 ) ) ); + } + + public TestBreaker LongResetTimeoutCb( ) + { + return new TestBreaker( new CircuitBreaker( 1, TimeSpan.FromMilliseconds( 100 ), TimeSpan.FromMilliseconds( 5000 ) ) ); + } + + public TestBreaker MultiFailureCb( ) + { + return new TestBreaker( new CircuitBreaker( 5, TimeSpan.FromMilliseconds( 200 ), TimeSpan.FromMilliseconds( 500 ) ) ); + } + } + + + internal class TestException : ApplicationException + { + public TestException( ) + { + } + + public TestException( string message ) + : base( message ) + { + } + + public TestException( string message, Exception innerException ) + : base( message, innerException ) + { + } + + protected TestException( SerializationInfo info, StreamingContext context ) + : base( info, context ) + { + } + } + +} \ No newline at end of file diff --git a/src/core/Akka.Tests/Routing/BroadcastSpec.cs b/src/core/Akka.Tests/Routing/BroadcastSpec.cs index 3cd4beac958..4226c68cc64 100644 --- a/src/core/Akka.Tests/Routing/BroadcastSpec.cs +++ b/src/core/Akka.Tests/Routing/BroadcastSpec.cs @@ -11,12 +11,12 @@ using Akka.TestKit; using Akka.Util.Internal; using Xunit; +using Xunit.Abstractions; namespace Akka.Tests.Routing { public class BroadcastSpec : AkkaSpec { - public new class TestActor : UntypedActor { protected override void OnReceive(object message) @@ -51,8 +51,7 @@ protected override void OnReceive(object message) } } } - - + [Fact] public void BroadcastGroup_router_must_broadcast_message_using_Tell() { diff --git a/src/core/Akka.Tests/Serialization/SerializationSpec.cs b/src/core/Akka.Tests/Serialization/SerializationSpec.cs index d6d4dc4e5a4..f2edeca1fe3 100644 --- a/src/core/Akka.Tests/Serialization/SerializationSpec.cs +++ b/src/core/Akka.Tests/Serialization/SerializationSpec.cs @@ -283,7 +283,14 @@ public void CanSerializeLocalScope() [Fact] public void CanSerializeRoundRobinPool() { - var message = new RoundRobinPool(10, new DefaultResizer(0,1)); + var decider = Decider.From( + Directive.Restart, + Directive.Stop.When(), + Directive.Stop.When()); + + var supervisor = new OneForOneStrategy(decider); + + var message = new RoundRobinPool(10, new DefaultResizer(0,1),supervisor,"abc"); AssertEqual(message); } @@ -297,7 +304,14 @@ public void CanSerializeRoundRobinGroup() [Fact] public void CanSerializeRandomPool() { - var message = new RandomPool(10, new DefaultResizer(0, 1)); + var decider = Decider.From( + Directive.Restart, + Directive.Stop.When(), + Directive.Stop.When()); + + var supervisor = new OneForOneStrategy(decider); + + var message = new RandomPool(10, new DefaultResizer(0, 1),supervisor,"abc"); AssertEqual(message); } @@ -311,29 +325,57 @@ public void CanSerializeRandomGroup() [Fact] public void CanSerializeConsistentHashPool() { - var message = new ConsistentHashingPool(10); + var decider = Decider.From( + Directive.Restart, + Directive.Stop.When(), + Directive.Stop.When()); + + var supervisor = new OneForOneStrategy(decider); + + var message = new ConsistentHashingPool(10,null,supervisor,"abc"); AssertEqual(message); } [Fact] public void CanSerializeTailChoppingPool() - { - var message = new TailChoppingPool(10,TimeSpan.FromSeconds(10),TimeSpan.FromSeconds(2)); + { + var decider = Decider.From( + Directive.Restart, + Directive.Stop.When(), + Directive.Stop.When()); + + var supervisor = new OneForOneStrategy(decider); + + var message = new TailChoppingPool(10,null,supervisor,"abc",TimeSpan.FromSeconds(10),TimeSpan.FromSeconds(2)); AssertEqual(message); } [Fact] public void CanSerializeScatterGatherFirstCompletedPool() { - var message = new ScatterGatherFirstCompletedPool(10); + var decider = Decider.From( + Directive.Restart, + Directive.Stop.When(), + Directive.Stop.When()); + + var supervisor = new OneForOneStrategy(decider); + + var message = new ScatterGatherFirstCompletedPool(10,null,supervisor,"abc",TimeSpan.MaxValue); AssertEqual(message); } [Fact] public void CanSerializeSmallestMailboxPool() { - var message = new SmallestMailboxPool(10); + var decider = Decider.From( + Directive.Restart, + Directive.Stop.When(), + Directive.Stop.When()); + + var supervisor = new OneForOneStrategy(decider); + + var message = new SmallestMailboxPool(10,null,supervisor,"abc"); AssertEqual(message); } @@ -348,7 +390,8 @@ private void AssertEqual(T message) { var serializer = Sys.Serialization.FindSerializerFor(message); var serialized = serializer.ToBinary(message); - var deserialized = (T)serializer.FromBinary(serialized, typeof(T)); + var result = serializer.FromBinary(serialized, typeof(T)); + var deserialized = (T) result; // Assert.True(message.Equals(deserialized)); Assert.Equal(message, deserialized); diff --git a/src/core/Akka.Tests/Util/ByteStringSpec.cs b/src/core/Akka.Tests/Util/ByteStringSpec.cs new file mode 100644 index 00000000000..ce5a3ca5f58 --- /dev/null +++ b/src/core/Akka.Tests/Util/ByteStringSpec.cs @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System.Linq; +using System.Text; +using Akka.IO; +using FsCheck; +using Xunit; + +namespace Akka.Tests.Util +{ + + /// + /// TODO: Should we use the FsCheck.XUnit integration when they upgrade to xUnit 2 + /// + public class ByteStringSpec + { + class Generators + { + + // TODO: Align with JVM Akka Generator + public static Arbitrary ByteStrings() + { + return Arb.From(Arb.Generate().Select(ByteString.Create)); + } + } + + public ByteStringSpec() + { + Arb.Register(); + } + + [Fact] + public void A_ByteString_must_have_correct_size_when_concatenating() + { + Prop.ForAll((ByteString a, ByteString b) => (a + b).Count == a.Count + b.Count) + .QuickCheckThrowOnFailure(); + } + + [Fact] + public void A_ByteString_must_have_correct_size_when_dropping() + { + Prop.ForAll((ByteString a, ByteString b) => (a + b).Drop(b.Count).Count == a.Count) + .QuickCheckThrowOnFailure(); + } + + [Fact] + public void A_ByteString_must_be_sequential_when_taking() + { + Prop.ForAll((ByteString a, ByteString b) => (a + b).Take(a.Count).SequenceEqual(a)) + .QuickCheckThrowOnFailure(); + } + [Fact] + public void A_ByteString_must_be_sequential_when_dropping() + { + Prop.ForAll((ByteString a, ByteString b) => (a + b).Drop(a.Count).SequenceEqual(b)) + .QuickCheckThrowOnFailure(); + } + + [Fact] + public void A_ByteString_must_be_equal_to_the_original_when_compacting() + { + Prop.ForAll((ByteString xs) => + { + var ys = xs.Compact(); + return xs.SequenceEqual(ys) && ys.IsCompact(); + }).QuickCheckThrowOnFailure(); + } + [Fact] + public void A_ByteString_must_be_equal_to_the_original_when_recombining() + { + Prop.ForAll((ByteString xs, int from, int until) => + { + var tmp1 = xs.SplitAt(until); + var tmp2 = tmp1.Item1.SplitAt(until); + return (tmp2.Item1 + tmp2.Item2 + tmp1.Item2).SequenceEqual(xs); + }).QuickCheckThrowOnFailure(); + } + + [Fact] + public void A_ByteString_must_behave_as_expected_when_created_from_and_decoding_to_String() + { + Prop.ForAll((string s) => ByteString.FromString(s, Encoding.UTF8).DecodeString(Encoding.UTF8) == (s ?? "")) // TODO: What should we do with null string? + .QuickCheckThrowOnFailure(); + } + [Fact] + public void A_ByteString_must_behave_as_expected_when_compacting() + { + Prop.ForAll((ByteString a) => + { + var wasCompact = a.IsCompact(); + var b = a.Compact(); + return ((!wasCompact) || (b == a)) && + b.SequenceEqual(a) && + b.IsCompact() && + b.Compact() == b; + }).QuickCheckThrowOnFailure(); + } + } +} diff --git a/src/core/Akka.Tests/packages.config b/src/core/Akka.Tests/packages.config index 011a76f37f0..09bb0a85065 100644 --- a/src/core/Akka.Tests/packages.config +++ b/src/core/Akka.Tests/packages.config @@ -2,6 +2,8 @@ + + diff --git a/src/core/Akka/Actor/ActorBase.cs b/src/core/Akka/Actor/ActorBase.cs index 95d0913fa79..2c9b910d8f2 100644 --- a/src/core/Akka/Actor/ActorBase.cs +++ b/src/core/Akka/Actor/ActorBase.cs @@ -199,7 +199,7 @@ protected void Become(Receive receive, bool discardOld = true) } /// - /// Changes the actor's behavior and replaces the current receive handler with the specified handler. + /// Changes the actor's command behavior and replaces the current receive handler with the specified handler. /// /// The new message handler. protected void Become(Receive receive) diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 75aa8db2492..92e045496f4 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -86,8 +86,8 @@ public override bool Equals(object obj) if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; var actorPath = obj as ActorPath; - if (actorPath != null) return Equals(actorPath); - return Equals((Surrogate) obj); + + return Equals(actorPath); } public override int GetHashCode() @@ -297,14 +297,10 @@ private static bool TryParseAddress(string path, out Address address, out Uri ur //This code corresponds to AddressFromURIString.unapply uri = null; address = null; - try - { - uri = new Uri(path); - } - catch (UriFormatException) - { + + if (!Uri.TryCreate(path, UriKind.Absolute, out uri)) return false; - } + var protocol = uri.Scheme; //Typically "akka" if (!protocol.StartsWith("akka", StringComparison.OrdinalIgnoreCase)) { diff --git a/src/core/Akka/Actor/ActorProducerPipeline.cs b/src/core/Akka/Actor/ActorProducerPipeline.cs index 6849006b9e7..5ffcdff1049 100644 --- a/src/core/Akka/Actor/ActorProducerPipeline.cs +++ b/src/core/Akka/Actor/ActorProducerPipeline.cs @@ -65,7 +65,7 @@ public virtual bool CanBeAppliedTo(Type actorType) public abstract class ActorProducerPluginBase : IActorProducerPlugin where TActor : ActorBase { /// - /// By default derivatives of this plugin will be applied to all actors inheriting from actor generic type. + /// By default derivatives of this plugin will be applied to all actors inheriting from actor generic type. /// public virtual bool CanBeAppliedTo(Type actorType) { diff --git a/src/core/Akka/Actor/ActorRef.cs b/src/core/Akka/Actor/ActorRef.cs index 9a1deec9bf2..95c2ecd32e0 100644 --- a/src/core/Akka/Actor/ActorRef.cs +++ b/src/core/Akka/Actor/ActorRef.cs @@ -100,19 +100,6 @@ protected override void TellInternal(object message, IActorRef sender) } } } - - protected void SendSystemMessage(ISystemMessage message, IActorRef sender) - { - var d = message as DeathWatchNotification; - if (message is Terminate) - { - Stop(); - } - else if (d != null) - { - this.Tell(new Terminated(d.Actor, d.ExistenceConfirmed, d.AddressTerminated)); - } - } } @@ -125,7 +112,7 @@ public static IActorRef GetSelfOrNoSender() return actorCell != null ? actorCell.Self : ActorRefs.NoSender; } } - public interface IActorRef : ICanTell, IEquatable, IComparable, ISurrogated + public interface IActorRef : ICanTell, IEquatable, IComparable, ISurrogated, IComparable { ActorPath Path { get; } } @@ -215,6 +202,13 @@ public override int GetHashCode() } } + public int CompareTo(object obj) + { + if (obj != null && !(obj is IActorRef)) + throw new ArgumentException("Object must be of type IActorRef."); + return CompareTo((IActorRef) obj); + } + public bool Equals(IActorRef other) { return Path.Uid == other.Path.Uid && Path.Equals(other.Path); @@ -256,6 +250,7 @@ public interface IInternalActorRef : IActorRef, IActorRefScope void Stop(); void Restart(Exception cause); void Suspend(); + void SendSystemMessage(ISystemMessage message, IActorRef sender); } public abstract class InternalActorRefBase : ActorRefBase, IInternalActorRef @@ -280,6 +275,18 @@ public abstract class InternalActorRefBase : ActorRefBase, IInternalActorRef public abstract bool IsTerminated { get; } public abstract bool IsLocal { get; } + public void SendSystemMessage(ISystemMessage message, IActorRef sender) + { + var d = message as DeathWatchNotification; + if (message is Terminate) + { + Stop(); + } + else if (d != null) + { + this.Tell(new Terminated(d.Actor, d.ExistenceConfirmed, d.AddressTerminated)); + } + } } public abstract class MinimalActorRef : InternalActorRefBase, ILocalRef diff --git a/src/core/Akka/Actor/ActorRefProvider.cs b/src/core/Akka/Actor/ActorRefProvider.cs index 627792ba824..336aa862228 100644 --- a/src/core/Akka/Actor/ActorRefProvider.cs +++ b/src/core/Akka/Actor/ActorRefProvider.cs @@ -356,10 +356,8 @@ public IInternalActorRef ActorOf(ActorSystemImpl system, Props props, IInternalA { var d = Deployer.Lookup(path); if (d != null && d.RouterConfig != RouterConfig.NoRouter) - Log.Warning( - string.Format( - "Configuration says that [{0}] should be a router, but code disagrees. Remove the config or add a RouterConfig to its Props.", - path)); + Log.Warning("Configuration says that [{0}] should be a router, but code disagrees. Remove the config or add a RouterConfig to its Props.", + path); } var props2 = props; diff --git a/src/core/Akka/Actor/ActorSystem.cs b/src/core/Akka/Actor/ActorSystem.cs index a2d6fd1a805..64d27d7e467 100644 --- a/src/core/Akka/Actor/ActorSystem.cs +++ b/src/core/Akka/Actor/ActorSystem.cs @@ -19,7 +19,7 @@ namespace Akka.Actor /// An actor system is a hierarchical group of actors which share common /// configuration, e.g. dispatchers, deployments, remote capabilities and /// addresses. It is also the entry point for creating or looking up actors. - /// There are several possibilities for creating actors (see [[Akka.Actor.Props]] + /// There are several possibilities for creating actors (see /// for details on `props`): /// /// system.ActorOf(props, "name"); diff --git a/src/core/Akka/Actor/Address.cs b/src/core/Akka/Actor/Address.cs index 41c34dc684e..fc417cece62 100644 --- a/src/core/Akka/Actor/Address.cs +++ b/src/core/Akka/Actor/Address.cs @@ -217,12 +217,12 @@ public static IEnumerable Unapply(string addr) { try { + Uri uri; + bool isRelative = Uri.TryCreate(addr, UriKind.Relative, out uri); + if (!isRelative) return null; + var finalAddr = addr; - // need to add a special case for URI fragments containing #, since those don't get counted - // as relative URIs by C# - if(Uri.IsWellFormedUriString(addr, UriKind.Absolute) || (!Uri.IsWellFormedUriString(addr, UriKind.Relative) - && !addr.Contains("#"))) return null; - if(!addr.StartsWith("/")) + if (!addr.StartsWith("/")) { //hack to cause the URI not to explode when we're only given an actor name finalAddr = "/" + addr; diff --git a/src/core/Akka/Actor/Exceptions.cs b/src/core/Akka/Actor/Exceptions.cs index b58d9c503c1..07ed1323d8d 100644 --- a/src/core/Akka/Actor/Exceptions.cs +++ b/src/core/Akka/Actor/Exceptions.cs @@ -11,52 +11,72 @@ namespace Akka.Actor { /// - /// Class AkkaException. + /// This exception provides the base for all Akka.NET specific exceptions within the system. /// public abstract class AkkaException : Exception { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// protected AkkaException() { } /// - /// Initializes a new instance of the class with a specified error message. + /// Initializes a new instance of the class. /// /// The message that describes the error. - /// An inner exception responsible for this error. + /// The exception that is the cause of the current exception. protected AkkaException(string message, Exception cause = null) : base(message, cause) { } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected AkkaException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// + /// The exception that is the cause of the current exception. + /// protected Exception Cause { get { return InnerException; } } } /// - /// An InvalidActorNameException is thrown when the actor name is invalid + /// This exception is thrown when the actor name is invalid. /// public class InvalidActorNameException : AkkaException { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. public InvalidActorNameException(string message) : base(message) { - //Intentionally left blank } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. public InvalidActorNameException(string message, Exception innerException) : base(message, innerException) { - //Intentionally left blank } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected InvalidActorNameException(SerializationInfo info, StreamingContext context) : base(info, context) { @@ -64,16 +84,24 @@ protected InvalidActorNameException(SerializationInfo info, StreamingContext con } /// - /// Thrown when an Ask operation times out + /// This exception is thrown when an Ask operation times out. /// public class AskTimeoutException : AkkaException { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. public AskTimeoutException(string message) : base(message) { - //Intentionally left blank } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected AskTimeoutException(SerializationInfo info, StreamingContext context) : base(info, context) { @@ -81,27 +109,72 @@ protected AskTimeoutException(SerializationInfo info, StreamingContext context) } /// + /// This exception is thrown when the initialization logic for an Actor fails. /// public class ActorInitializationException : AkkaException { private readonly IActorRef _actor; - protected ActorInitializationException() : base(){} - public ActorInitializationException(string message) : base(message) { } + /// + /// Initializes a new instance of the class. + /// + protected ActorInitializationException() + : base() + { + } - public ActorInitializationException(string message, Exception cause) : base(message, cause) { } - public ActorInitializationException(IActorRef actor, string message, Exception cause = null) : base(message, cause) + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public ActorInitializationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public ActorInitializationException(string message, Exception cause) + : base(message, cause) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The actor whose initialization logic failed. + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public ActorInitializationException(IActorRef actor, string message, Exception cause = null) + : base(message, cause) { _actor = actor; } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected ActorInitializationException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// + /// Retrieves the actor whose initialization logic failed. + /// public IActorRef Actor { get { return _actor; } } + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// public override string ToString() { if (_actor == null) return base.ToString(); @@ -110,37 +183,68 @@ public override string ToString() } /// - /// Class LoggerInitializationException is thrown to indicate that there was a problem initializing a logger. + /// This exception is thrown when there was a problem initializing a logger. /// public class LoggerInitializationException : AkkaException { - public LoggerInitializationException() : base() { } + /// + /// Initializes a new instance of the class. + /// + public LoggerInitializationException() + : base() + { + } - public LoggerInitializationException(string message) : base(message) { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public LoggerInitializationException(string message) + : base(message) + { + } - public LoggerInitializationException(string message, Exception cause = null) : base(message, cause) { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public LoggerInitializationException(string message, Exception cause = null) + : base(message, cause) + { + } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected LoggerInitializationException(SerializationInfo info, StreamingContext context) : base(info, context) { } } - - /// - /// Thrown when a message has been sent to an actor. will by default stop the actor. + /// This exception is thrown when a message has been sent to an Actor. + /// will by default stop the actor. /// public class ActorKilledException : AkkaException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The message. - public ActorKilledException(string message) : base(message) + /// The message that describes the error. + public ActorKilledException(string message) + : base(message) { } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected ActorKilledException(SerializationInfo info, StreamingContext context) : base(info, context) { @@ -148,13 +252,25 @@ protected ActorKilledException(SerializationInfo info, StreamingContext context) } /// - /// IllegalActorStateException is thrown when a core invariant in the Actor implementation has been violated. - /// For instance, if you try to create an Actor that doesn't inherit from . + /// This exception is thrown when a core invariant in the Actor implementation has been violated. + /// For instance, if you try to create an Actor that doesn't inherit from . /// public class IllegalActorStateException : AkkaException { - public IllegalActorStateException(string msg) : base(msg) { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public IllegalActorStateException(string message) + : base(message) + { + } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected IllegalActorStateException(SerializationInfo info, StreamingContext context) : base(info, context) { @@ -162,12 +278,24 @@ protected IllegalActorStateException(SerializationInfo info, StreamingContext co } /// - /// IllegalActorNameException is thrown when an Actor with an invalid name is deployed our bound. + /// This exception is thrown when an Actor with an invalid name is deployed. /// public class IllegalActorNameException : AkkaException { - public IllegalActorNameException(string msg) : base(msg) { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public IllegalActorNameException(string message) + : base(message) + { + } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected IllegalActorNameException(SerializationInfo info, StreamingContext context) : base(info, context) { @@ -175,24 +303,36 @@ protected IllegalActorNameException(SerializationInfo info, StreamingContext con } /// - /// A DeathPactException is thrown by an Actor that receives a Terminated(someActor) message + /// This exception is thrown by an Actor that receives a Terminated(someActor) message /// that it doesn't handle itself, effectively crashing the Actor and escalating to the supervisor. /// public class DeathPactException : AkkaException { private readonly IActorRef _deadActor; + /// + /// Initializes a new instance of the class. + /// + /// The actor that has been terminated. public DeathPactException(IActorRef deadActor) : base("Monitored actor [" + deadActor + "] terminated") { _deadActor = deadActor; } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected DeathPactException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// + /// Retrieves the actor that has been terminated. + /// public IActorRef DeadActor { get { return _deadActor; } @@ -200,7 +340,12 @@ public IActorRef DeadActor } /// - /// Class PreRestartException. + /// This exception is thrown when the method fails during a restart attempt. + /// + /// + /// This exception is not propagated to the supervisor, as it originates from the already failed instance, + /// hence it is only visible as log entry on the event stream. + /// /// public class PreRestartException : AkkaException { @@ -209,8 +354,14 @@ public class PreRestartException : AkkaException private Exception exception; private object optionalMessage; - public PreRestartException(IActorRef actor, Exception restartException, Exception cause, - object optionalMessage) + /// + /// Initializes a new instance of the class. + /// + /// The actor whose hook failed. + /// The exception thrown by the within . + /// The exception which caused the restart in the first place. + /// The message which was optionally passed into . + public PreRestartException(IActorRef actor, Exception restartException, Exception cause, object optionalMessage) { Actor = actor; e = restartException; @@ -218,6 +369,11 @@ public class PreRestartException : AkkaException this.optionalMessage = optionalMessage; } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected PreRestartException(SerializationInfo info, StreamingContext context) : base(info, context) { @@ -225,11 +381,8 @@ protected PreRestartException(SerializationInfo info, StreamingContext context) } /// - /// A PostRestartException is thrown when constructor or postRestart() method + /// This exception is thrown when the Actor constructor or method /// fails during a restart attempt. - /// : actor is the actor whose constructor or postRestart() hook failed. - /// : cause is the exception thrown by that actor within preRestart() - /// : originalCause is the exception which caused the restart in the first place /// public class PostRestartException : ActorInitializationException { @@ -238,8 +391,8 @@ public class PostRestartException : ActorInitializationException /// /// Initializes a new instance of the class. /// - /// The actor whose constructor or postRestart() hook failed. - /// Cause is the exception thrown by that actor within preRestart(). + /// The actor whose constructor or hook failed. + /// The exception thrown by the within . /// The original cause is the exception which caused the restart in the first place. public PostRestartException(IActorRef actor, Exception cause, Exception originalCause) :base(actor,"Exception post restart (" + (originalCause == null ?"null" : originalCause.GetType().ToString()) + ")", cause) @@ -247,22 +400,40 @@ public PostRestartException(IActorRef actor, Exception cause, Exception original _originalCause = originalCause; } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected PostRestartException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// + /// Retrieves the exception which caused the restart in the first place. + /// public Exception OriginalCause { get { return _originalCause; } } } - /// - /// Class ActorNotFoundException. + /// This exception is thrown when an Actor can not be found. /// public class ActorNotFoundException : AkkaException { - public ActorNotFoundException() : base() { } - + /// + /// Initializes a new instance of the class. + /// + public ActorNotFoundException() + : base() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected ActorNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) { @@ -270,19 +441,36 @@ protected ActorNotFoundException(SerializationInfo info, StreamingContext contex } /// - /// InvalidMessageException is thrown when an invalid message is sent to an Actor. + /// This exception is thrown when an invalid message is sent to an Actor. + /// + /// /// Currently only null is an invalid message. + /// /// public class InvalidMessageException : AkkaException { - public InvalidMessageException() : this("Message is null") + /// + /// Initializes a new instance of the class. + /// + public InvalidMessageException() + : this("Message is null") { } - public InvalidMessageException(string message):base(message) + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public InvalidMessageException(string message) + : base(message) { } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected InvalidMessageException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/core/Akka/Actor/FSM.cs b/src/core/Akka/Actor/FSM.cs index 3d5d233d95d..8c7542fe3b3 100644 --- a/src/core/Akka/Actor/FSM.cs +++ b/src/core/Akka/Actor/FSM.cs @@ -250,7 +250,7 @@ public State(TS stateName, TD stateData, TimeSpan? timeout = null, Reason stopRe public Reason StopReason { get; private set; } - public List Replies { get; private set; } + public List Replies { get; protected set; } public State Copy(TimeSpan? timeout, Reason stopReason = null, List replies = null) { @@ -729,8 +729,8 @@ private void HandleTransition(TState previous, TState next) /// See http://scalachina.com/api/scala/PartialFunction.html /// /// The original to be called - /// The to be called if returns null - /// A which combines both the results of and + /// The to be called if returns null + /// A which combines both the results of and private static StateFunction OrElse(StateFunction original, StateFunction fallback) { StateFunction chained = delegate(Event @event) diff --git a/src/core/Akka/Actor/Inbox.Actor.cs b/src/core/Akka/Actor/Inbox.Actor.cs index 8c55d9afffe..3b5e12e19c4 100644 --- a/src/core/Akka/Actor/Inbox.Actor.cs +++ b/src/core/Akka/Actor/Inbox.Actor.cs @@ -49,7 +49,7 @@ public void EnqueueMessage(object msg) { if (!_printedWarning) { - _log.Warning("Dropping message: Inbox size has been exceeded, use akka.actor.inbox.inbox-size to increase maximum allowed inbox size. Current is " + _size); + _log.Warning("Dropping message: Inbox size has been exceeded, use akka.actor.inbox.inbox-size to increase maximum allowed inbox size. Current is {0}", _size); _printedWarning = true; } } @@ -180,4 +180,3 @@ protected override bool Receive(object message) } } - diff --git a/src/core/Akka/Actor/Internal/ActorSystemImpl.cs b/src/core/Akka/Actor/Internal/ActorSystemImpl.cs index 43015bee047..01b318e996d 100644 --- a/src/core/Akka/Actor/Internal/ActorSystemImpl.cs +++ b/src/core/Akka/Actor/Internal/ActorSystemImpl.cs @@ -102,8 +102,10 @@ public void Start() if(_settings.LogDeadLetters > 0) _logDeadLetterListener = SystemActorOf("deadLetterListener"); + _eventStream.StartUnsubscriber(this); - if(_settings.LogConfigOnStart) + + if (_settings.LogConfigOnStart) { _log.Warning(Settings.ToString()); } diff --git a/src/core/Akka/Actor/LocalActorRefProvider.cs b/src/core/Akka/Actor/LocalActorRefProvider.cs deleted file mode 100644 index d4b559d470c..00000000000 --- a/src/core/Akka/Actor/LocalActorRefProvider.cs +++ /dev/null @@ -1,344 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2015 Typesafe Inc. -// Copyright (C) 2013-2015 Akka.NET project -// -//----------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Akka.Actor.Internals; -using Akka.Dispatch; -using Akka.Dispatch.SysMsg; -using Akka.Event; -using Akka.Routing; -using Akka.Util; - -namespace Akka.Actor -{ - - /// - /// Class LocalActorRefProvider. This class cannot be inherited. - /// - public sealed class LocalActorRefProvider : ActorRefProvider - { - private readonly Settings _settings; - private readonly EventStream _eventStream; - private readonly Deployer _deployer; - private readonly InternalActorRef _deadLetters; - private readonly RootActorPath _rootPath; - private readonly LoggingAdapter _log; - private readonly AtomicCounterLong _tempNumber; - private readonly ActorPath _tempNode; - private ActorSystemImpl _system; - private readonly Dictionary _extraNames = new Dictionary(); - private readonly TaskCompletionSource _terminationPromise = new TaskCompletionSource(); - private SupervisorStrategy _systemGuardianStrategy; - private VirtualPathContainer _tempContainer; - private RootGuardianActorRef _rootGuardian; - private LocalActorRef _userGuardian; //This is called guardian in Akka - private Func _defaultMailbox; //TODO: switch to MailboxType - private LocalActorRef _systemGuardian; - - public LocalActorRefProvider(string systemName, Settings settings, EventStream eventStream) - : this(systemName, settings, eventStream, null, null) - { - //Intentionally left blank - } - - public LocalActorRefProvider(string systemName, Settings settings, EventStream eventStream, Deployer deployer, Func deadLettersFactory) - { - _settings = settings; - _eventStream = eventStream; - _deployer = deployer ?? new Deployer(settings); - _rootPath = new RootActorPath(new Address("akka", systemName)); - _log = Logging.GetLogger(eventStream, "LocalActorRefProvider(" + _rootPath.Address + ")"); - if (deadLettersFactory == null) - deadLettersFactory = p => new DeadLetterActorRef(this, p, _eventStream); - _deadLetters = deadLettersFactory(_rootPath / "deadLetters"); - _tempNumber = new AtomicCounterLong(1); - _tempNode = _rootPath / "temp"; - - //TODO: _guardianSupervisorStrategyConfigurator = dynamicAccess.createInstanceFor[SupervisorStrategyConfigurator](settings.SupervisorStrategyClass, EmptyImmutableSeq).get - _systemGuardianStrategy = SupervisorStrategy.DefaultStrategy; - - } - - public ActorRef DeadLetters { get { return _deadLetters; } } - - public Deployer Deployer { get { return _deployer; } } - - public InternalActorRef RootGuardian { get { return _rootGuardian; } } - - public ActorPath RootPath { get { return _rootPath; } } - - public Settings Settings { get { return _settings; } } - - public LocalActorRef SystemGuardian { get { return _systemGuardian; } } - - public InternalActorRef TempContainer { get { return _tempContainer; } } - - public Task TerminationTask { get { return _terminationPromise.Task; } } - - public LocalActorRef Guardian { get { return _userGuardian; } } - - private MessageDispatcher DefaultDispatcher { get { return _system.Dispatchers.DefaultGlobalDispatcher; } } - - private SupervisorStrategy UserGuardianSupervisorStrategy { get { return SupervisorStrategy.DefaultStrategy; } } //TODO: Implement Akka's _guardianSupervisorStrategyConfigurator.create() - - public ActorPath TempPath() - { - return _tempNode / GetNextTempName(); - } - - private string GetNextTempName() - { - return _tempNumber.GetAndIncrement().Base64Encode(); - } - - /// - /// Higher-level providers (or extensions) might want to register new synthetic - /// top-level paths for doing special stuff. This is the way to do just that. - /// Just be careful to complete all this before finishes, - /// or before you start your own auto-spawned actors. - /// - public void RegisterExtraName(string name, InternalActorRef actor) - { - _extraNames.Add(name, actor); - } - - - - private RootGuardianActorRef CreateRootGuardian(ActorSystemImpl system) - { - var supervisor = new RootGuardianSupervisor(_rootPath, this, _terminationPromise, _log); - var rootGuardianStrategy = new OneForOneStrategy(ex => - { - _log.Error(ex, "Guardian failed. Shutting down system"); - return Directive.Stop; - }); - var props = Props.Create(rootGuardianStrategy); - var rootGuardian = new RootGuardianActorRef(system, props, DefaultDispatcher, _defaultMailbox, supervisor, _rootPath, _deadLetters, _extraNames); - return rootGuardian; - } - - public ActorRef RootGuardianAt(Address address) - { - return address == _rootPath.Address ? _rootGuardian : _deadLetters; - } - - private LocalActorRef CreateUserGuardian(LocalActorRef rootGuardian, string name) //Corresponds to Akka's: override lazy val guardian: LocalActorRef - { - return CreateRootGuardianChild(rootGuardian, name, () => - { - var props = Props.Create(UserGuardianSupervisorStrategy); - - var userGuardian = new LocalActorRef(_system, props, DefaultDispatcher, _defaultMailbox, rootGuardian, RootPath / name); - return userGuardian; - }); - } - - private LocalActorRef CreateSystemGuardian(LocalActorRef rootGuardian, string name, LocalActorRef userGuardian) //Corresponds to Akka's: override lazy val guardian: systemGuardian - { - //TODO: When SystemGuardianActor has been implemented switch to this: - //return CreateRootGuardianChild(rootGuardian, name, () => - //{ - // var props = Props.Create(() => new SystemGuardianActor(userGuardian), _systemGuardianStrategy); - - // var systemGuardian = new LocalActorRef(_system, props, DefaultDispatcher, _defaultMailbox, rootGuardian, RootPath / name); - // return systemGuardian; - //}); - return (LocalActorRef)rootGuardian.Cell.ActorOf(name); - } - - private LocalActorRef CreateRootGuardianChild(LocalActorRef rootGuardian, string name, Func childCreator) - { - var cell = rootGuardian.Cell; - cell.ReserveChild(name); - var child = childCreator(); - cell.InitChild(child); - child.Start(); - return child; - } - - public void RegisterTempActor(InternalActorRef actorRef, ActorPath path) - { - if (path.Parent != _tempNode) - throw new Exception("Cannot RegisterTempActor() with anything not obtained from tempPath()"); - _tempContainer.AddChild(path.Name, actorRef); - } - - public void UnregisterTempActor(ActorPath path) - { - if (path.Parent != _tempNode) - throw new Exception("Cannot UnregisterTempActor() with anything not obtained from tempPath()"); - _tempContainer.RemoveChild(path.Name); - } - - public void Init(ActorSystemImpl system) - { - _system = system; - //The following are the lazy val statements in Akka - var defaultDispatcher = system.Dispatchers.DefaultGlobalDispatcher; - _defaultMailbox = () => new ConcurrentQueueMailbox(); //TODO:system.Mailboxes.FromConfig(Mailboxes.DefaultMailboxId) - _rootGuardian = CreateRootGuardian(system); - _tempContainer = new VirtualPathContainer(system.Provider, _tempNode, _rootGuardian, _log); - _rootGuardian.SetTempContainer(_tempContainer); - _userGuardian = CreateUserGuardian(_rootGuardian, "user"); - _systemGuardian = CreateSystemGuardian(_rootGuardian, "system", _userGuardian); - //End of lazy val - - _rootGuardian.Start(); - // chain death watchers so that killing guardian stops the application - _systemGuardian.Tell(new Watch(_userGuardian, _systemGuardian)); //Should be SendSystemMessage - _rootGuardian.Tell(new Watch(_systemGuardian, _rootGuardian)); //Should be SendSystemMessage - _eventStream.StartDefaultLoggers(_system); - } - - public ActorRef ResolveActorRef(string path) - { - ActorPath actorPath; - if (ActorPath.TryParse(path, out actorPath) && actorPath.Address == _rootPath.Address) - return ResolveActorRef(_rootGuardian, actorPath.Elements); - _log.Debug("Resolve of unknown path [{0}] failed. Invalid format.", path); - return _deadLetters; - } - - /// - /// Resolves the actor reference. - /// - /// The actor path. - /// ActorRef. - /// The provided actor path is not valid in the LocalActorRefProvider - public ActorRef ResolveActorRef(ActorPath path) - { - if (path.Root == _rootPath) - return ResolveActorRef(_rootGuardian, path.Elements); - _log.Debug("Resolve of foreign ActorPath [{0}] failed", path); - return _deadLetters; - - //Used to be this, but the code above is what Akka has - //if(_rootPath.Address==actorPath.Address) - //{ - // if(actorPath.Elements.Head() == "temp") - // { - // //skip ""/"temp", - // string[] parts = actorPath.Elements.Drop(1).ToArray(); - // return _tempContainer.GetChild(parts); - // } - // //standard - // ActorCell currentContext = _rootGuardian.Cell; - // foreach(string part in actorPath.Elements) - // { - // currentContext = ((LocalActorRef)currentContext.Child(part)).Cell; - // } - // return currentContext.Self; - //} - //throw new NotSupportedException("The provided actor path is not valid in the LocalActorRefProvider"); - } - - private ActorRef ResolveActorRef(InternalActorRef actorRef, IReadOnlyCollection pathElements) - { - if (pathElements.Count == 0) - { - _log.Debug("Resolve of empty path sequence fails (per definition)"); - return _deadLetters; - } - var child = actorRef.GetChild(pathElements); - if (child.IsNobody()) - { - _log.Debug("Resolve of path sequence [/{0}] failed", ActorPath.FormatPathElements(pathElements)); - return new EmptyLocalActorRef(_system.Provider, actorRef.Path / pathElements, _eventStream); - } - return child; - } - - - public InternalActorRef ActorOf(ActorSystemImpl system, Props props, InternalActorRef supervisor, ActorPath path, bool systemService, Deploy deploy, bool lookupDeploy, bool async) - { - //TODO: This does not match Akka's ActorOf at all - - Deploy configDeploy = _system.Provider.Deployer.Lookup(path); - deploy = configDeploy ?? props.Deploy ?? Deploy.None; - if (deploy.Mailbox != null) - props = props.WithMailbox(deploy.Mailbox); - if (deploy.Dispatcher != null) - props = props.WithDispatcher(deploy.Dispatcher); - if (deploy.Scope is RemoteScope) - { - - } - - if (string.IsNullOrEmpty(props.Mailbox)) - { - // throw new NotSupportedException("Mailbox can not be configured as null or empty"); - } - if (string.IsNullOrEmpty(props.Dispatcher)) - { - //TODO: fix this.. - // throw new NotSupportedException("Dispatcher can not be configured as null or empty"); - } - - - //TODO: how should this be dealt with? - //akka simply passes the "deploy" var from remote daemon to ActorOf - //so it atleast seems like they ignore if remote scope is provided here. - //leaving this for now since it does work - - //if (props.Deploy != null && props.Deploy.Scope is RemoteScope) - //{ - // throw new NotSupportedException("LocalActorRefProvider can not deploy remote"); - //} - - if (props.RouterConfig is NoRouter || props.RouterConfig == null) - { - - props = props.WithDeploy(deploy); - var dispatcher = system.Dispatchers.FromConfig(props.Dispatcher); - var mailbox = _system.Mailboxes.FromConfig(props.Mailbox); - //TODO: Should be: system.mailboxes.getMailboxType(props2, dispatcher.configurator.config) - - if (async) - { - var reActorRef = new RepointableActorRef(system, props, dispatcher, () => mailbox, supervisor, path); - reActorRef.Initialize(async: true); - return reActorRef; - } - return new LocalActorRef(system, props, dispatcher, () => mailbox, supervisor, path); - } - else - { - //if no Router config value was specified, override with procedural input - if (deploy.RouterConfig is NoRouter) - { - deploy = deploy.WithRouterConfig(props.RouterConfig); - } - var routerDispatcher = system.Dispatchers.FromConfig(props.RouterConfig.RouterDispatcher); - var routerMailbox = _system.Mailboxes.FromConfig(props.Mailbox); - //TODO: Should be val routerMailbox = system.mailboxes.getMailboxType(routerProps, routerDispatcher.configurator.config) - - // routers use context.actorOf() to create the routees, which does not allow us to pass - // these through, but obtain them here for early verification - var routerProps = Props.Empty.WithDeploy(deploy); - var routeeProps = props.WithRouter(RouterConfig.NoRouter); - - var routedActorRef = new RoutedActorRef(system, routerProps, routerDispatcher, () => routerMailbox, routeeProps, supervisor, path); - routedActorRef.Initialize(async); - return routedActorRef; - } - } - - public Address GetExternalAddressFor(Address address) - { - return address == _rootPath.Address ? address : null; - } - - public Address DefaultAddress { get { return _rootPath.Address; } } - - public LoggingAdapter Log { get { return _log; } } - } -} - diff --git a/src/core/Akka/Actor/PipeToSupport.cs b/src/core/Akka/Actor/PipeToSupport.cs index 95eea539e04..ea1eb5cd79f 100644 --- a/src/core/Akka/Actor/PipeToSupport.cs +++ b/src/core/Akka/Actor/PipeToSupport.cs @@ -5,6 +5,7 @@ // //----------------------------------------------------------------------- +using System; using System.Threading.Tasks; namespace Akka.Actor @@ -16,24 +17,28 @@ namespace Akka.Actor public static class PipeToSupport { /// - /// Pipes the output of a Task directly to the 's mailbox once + /// Pipes the output of a Task directly to the 's mailbox once /// the task completes /// - public static Task PipeTo(this Task taskToPipe, ICanTell recipient, IActorRef sender = null) + public static Task PipeTo(this Task taskToPipe, ICanTell recipient, IActorRef sender = null, Func success = null, Func failure = null) { sender = sender ?? ActorRefs.NoSender; return taskToPipe.ContinueWith(tresult => { if (tresult.IsCanceled || tresult.IsFaulted) - recipient.Tell(new Status.Failure(tresult.Exception), sender); + recipient.Tell(failure != null + ? failure((Exception)tresult.Exception) + : new Status.Failure((Exception)tresult.Exception), sender); else if (tresult.IsCompleted) - recipient.Tell(tresult.Result, sender); + recipient.Tell(success != null + ? success(tresult.Result) + : tresult.Result, sender); }, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.AttachedToParent); } /// - /// Pipes the output of a Task directly to the 's mailbox once - /// the task completes. As this task has no result, only exceptions will be piped to the + /// Pipes the output of a Task directly to the 's mailbox once + /// the task completes. As this task has no result, only exceptions will be piped to the /// public static Task PipeTo(this Task taskToPipe, ICanTell recipient, IActorRef sender = null) { diff --git a/src/core/Akka/Actor/Props.cs b/src/core/Akka/Actor/Props.cs index 1e33b9d27c3..3217e5921e4 100644 --- a/src/core/Akka/Actor/Props.cs +++ b/src/core/Akka/Actor/Props.cs @@ -19,9 +19,9 @@ namespace Akka.Actor { /// - /// Props is a configuration object used in creating an [[Actor]]; it is + /// Props is a configuration object used in creating an Actor; it is /// immutable, so it is thread-safe and fully shareable. - /// Examples on C# API: + /// /// /// private Props props = Props.Empty(); /// private Props props = Props.Create(() => new MyActor(arg1, arg2)); @@ -29,9 +29,12 @@ namespace Akka.Actor /// private Props otherProps = props.WithDispatcher("dispatcher-id"); /// private Props otherProps = props.WithDeploy(deployment info); /// + /// /// public class Props : IEquatable , ISurrogated { + private const string NullActorTypeExceptionText = "Props must be instantiated with an actor type."; + public class PropsSurrogate : ISurrogate { public Type Type { get; set; } @@ -183,6 +186,8 @@ protected Props(Props copy) public Props(Type type, object[] args) : this(defaultDeploy, type, args) { + if (type == null) + throw new ArgumentNullException("type", NullActorTypeExceptionText); } /// @@ -192,6 +197,8 @@ public Props(Type type, object[] args) public Props(Type type) : this(defaultDeploy, type, noArgs) { + if (type == null) + throw new ArgumentNullException("type", NullActorTypeExceptionText); } /// @@ -203,6 +210,9 @@ public Props(Type type) public Props(Type type, SupervisorStrategy supervisorStrategy, IEnumerable args) : this(defaultDeploy, type, args.ToArray()) { + if (type == null) + throw new ArgumentNullException("type", NullActorTypeExceptionText); + SupervisorStrategy = supervisorStrategy; } @@ -215,6 +225,9 @@ public Props(Type type, SupervisorStrategy supervisorStrategy, IEnumerable args) : this(deploy, type, args.ToArray()) { + if (type == null) + throw new ArgumentNullException("type", NullActorTypeExceptionText); } /// @@ -394,6 +409,9 @@ public static Props Create(SupervisorStrategy supervisorStrategy) where /// Props. public static Props Create(Type type, params object[] args) { + if (type == null) + throw new ArgumentNullException("type", NullActorTypeExceptionText); + return new Props(type, args); } @@ -692,7 +710,7 @@ protected override Props Copy() /// /// This interface defines a class of actor creation strategies deviating from - /// the usual default of just reflectively instantiating the [[Actor]] + /// the usual default of just reflectively instantiating the Actor /// subclass. It can be used to allow a dependency injection framework to /// determine the actual actor class and how it shall be instantiated. /// @@ -707,13 +725,13 @@ public interface IIndirectActorProducer ActorBase Produce(); /// - /// This method is used by [[Props]] to determine the type of actor which will + /// This method is used by to determine the type of actor which will /// be created. The returned type is not used to produce the actor. /// Type ActorType { get; } /// - /// This method is used by [[Props]] to signal the Producer that it can + /// This method is used by to signal the Producer that it can /// release it's reference. HERE /// /// diff --git a/src/core/Akka/Actor/ReceiveActor.cs b/src/core/Akka/Actor/ReceiveActor.cs index c2181a73d00..bdcc0a3ed85 100644 --- a/src/core/Akka/Actor/ReceiveActor.cs +++ b/src/core/Akka/Actor/ReceiveActor.cs @@ -122,7 +122,7 @@ protected void Receive(Func handler) /// /// Registers a handler for incoming messages of the specified type . - /// If !=null then it must return true before a message is passed to . + /// If !=null then it must return true before a message is passed to . /// This method may only be called when constructing the actor or from or . /// Note that handlers registered prior to this may have handled the message already. /// In that case, this handler will not be invoked. @@ -138,8 +138,8 @@ protected void Receive(Action handler, Predicate shouldHandle = null) /// /// Registers a handler for incoming messages of the specified type . - /// If !=null then it must return true before a message is passed to . - /// This method may only be called when constructing the actor or from or . + /// If !=null then it must return true before a message is passed to . + /// This method may only be called when constructing the actor or from or . /// Note that handlers registered prior to this may have handled the message already. /// In that case, this handler will not be invoked. /// @@ -153,14 +153,14 @@ protected void Receive(Predicate shouldHandle, Action handler) /// - /// Registers a handler for incoming messages of the specified . - /// If !=null then it must return true before a message is passed to . - /// This method may only be called when constructing the actor or from or . + /// Registers a handler for incoming messages of the specified . + /// If !=null then it must return true before a message is passed to . + /// This method may only be called when constructing the actor or from or . /// Note that handlers registered prior to this may have handled the message already. /// In that case, this handler will not be invoked. /// /// The type of the message - /// The message handler that is invoked for incoming messages of the specified + /// The message handler that is invoked for incoming messages of the specified /// When not null it is used to determine if the message matches. protected void Receive(Type messageType, Action handler, Predicate shouldHandle = null) { @@ -170,14 +170,14 @@ protected void Receive(Type messageType, Action handler, Predicate - /// Registers a handler for incoming messages of the specified . - /// If !=null then it must return true before a message is passed to . - /// This method may only be called when constructing the actor or from or . + /// Registers a handler for incoming messages of the specified . + /// If !=null then it must return true before a message is passed to . + /// This method may only be called when constructing the actor or from or . /// Note that handlers registered prior to this may have handled the message already. /// In that case, this handler will not be invoked. /// /// The type of the message - /// The message handler that is invoked for incoming messages of the specified + /// The message handler that is invoked for incoming messages of the specified /// When not null it is used to determine if the message matches. protected void Receive(Type messageType, Predicate shouldHandle, Action handler) { @@ -192,7 +192,7 @@ protected void Receive(Type messageType, Predicate shouldHandle, Action< /// Registers a handler for incoming messages of the specified type . /// The handler should return true if it has handled the message. /// If the handler returns true no more handlers will be tried; otherwise the next registered handler will be tried. - /// This method may only be called when constructing the actor or from or . + /// This method may only be called when constructing the actor or from or . /// Note that handlers registered prior to this may have handled the message already. /// In that case, this handler will not be invoked. /// @@ -207,16 +207,16 @@ protected void Receive(Func handler) } /// - /// Registers a handler for incoming messages of the specified . + /// Registers a handler for incoming messages of the specified . /// The handler should return true if it has handled the message. /// If the handler returns true no more handlers will be tried; otherwise the next registered handler will be tried. - /// This method may only be called when constructing the actor or from or . + /// This method may only be called when constructing the actor or from or . /// Note that handlers registered prior to this may have handled the message already. /// In that case, this handler will not be invoked. /// /// The type of the message /// The message handler that is invoked for incoming messages of the - /// specified type . It should return trueif it handled/matched + /// specified type . It should return trueif it handled/matched /// the message; false otherwise. protected void Receive(Type messageType, Func handler) { @@ -230,7 +230,7 @@ protected void Receive(Type messageType, Func handler) /// /// Registers a handler for incoming messages of any type. - /// This method may only be called when constructing the actor or from or . + /// This method may only be called when constructing the actor or from or . /// Note that handlers registered prior to this may have handled the message already. /// In that case, this handler will not be invoked. /// diff --git a/src/core/Akka/Actor/Stash/Internal/AbstractStash.cs b/src/core/Akka/Actor/Stash/Internal/AbstractStash.cs index 3461b302ace..484e4d5999b 100644 --- a/src/core/Akka/Actor/Stash/Internal/AbstractStash.cs +++ b/src/core/Akka/Actor/Stash/Internal/AbstractStash.cs @@ -64,7 +64,7 @@ public void Stash() if(_theStash.Count > 0) { var lastEnvelope = _theStash.Last.Value; - if(lastEnvelope.Message.Equals(currMsg) && lastEnvelope.Sender == sender) + if(ReferenceEquals(lastEnvelope.Message,currMsg) && lastEnvelope.Sender == sender) throw new IllegalActorStateException(string.Format("Can't stash the same message {0} more than once", currMsg)); } if(_capacity <= 0 || _theStash.Count < _capacity) diff --git a/src/core/Akka/Actor/Stash/StashOverflowException.cs b/src/core/Akka/Actor/Stash/StashOverflowException.cs index 9dd0b1f3d1e..42e34d8e1ab 100644 --- a/src/core/Akka/Actor/Stash/StashOverflowException.cs +++ b/src/core/Akka/Actor/Stash/StashOverflowException.cs @@ -11,16 +11,25 @@ namespace Akka.Actor { /// - /// Is thrown when the size of the Stash exceeds the capacity of the stash + /// This exception is thrown when the size of the Stash exceeds the capacity of the stash. /// public class StashOverflowException : AkkaException { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. public StashOverflowException(string message, Exception cause = null) : base(message, cause) { } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected StashOverflowException(SerializationInfo info, StreamingContext context) : base(info, context) { } } } - diff --git a/src/core/Akka/Actor/SupervisorStrategy.cs b/src/core/Akka/Actor/SupervisorStrategy.cs index 32a29cfb9d8..cd298fda024 100644 --- a/src/core/Akka/Actor/SupervisorStrategy.cs +++ b/src/core/Akka/Actor/SupervisorStrategy.cs @@ -78,19 +78,11 @@ public bool HandleFailure(ActorCell actorCell, Exception cause, ChildRestartStat /// thrown. It will be restarted for other `Exception` types. /// The error is escalated if it's a `Exception`, i.e. `Error`. /// - /// The exception. /// Directive. - public static Directive DefaultDecider(Exception exception) - { - if (exception is ActorInitializationException) - return Directive.Stop; - if (exception is ActorKilledException) - return Directive.Stop; - if (exception is DeathPactException) - return Directive.Stop; - - return Directive.Restart; - } + public static IDecider DefaultDecider = Decider.From(Directive.Restart, + Directive.Stop.When(), + Directive.Stop.When(), + Directive.Stop.When()); /// /// Restarts the child. @@ -364,6 +356,8 @@ public ISurrogated FromSurrogate(ActorSystem system) public override ISurrogate ToSurrogate(ActorSystem system) { + if (Decider is LocalOnlyDecider) + throw new NotSupportedException("Can not serialize LocalOnlyDecider"); return new OneForOneStrategySurrogate { Decider = Decider, diff --git a/src/core/Akka/Akka.csproj b/src/core/Akka/Akka.csproj index 908f24e3b17..6c29d125ad2 100644 --- a/src/core/Akka/Akka.csproj +++ b/src/core/Akka/Akka.csproj @@ -217,6 +217,7 @@ + @@ -248,6 +249,9 @@ + + + @@ -266,6 +270,7 @@ + diff --git a/src/core/Akka/Configuration/Hocon/HoconValue.cs b/src/core/Akka/Configuration/Hocon/HoconValue.cs index cad11141b11..0edfbf8b113 100644 --- a/src/core/Akka/Configuration/Hocon/HoconValue.cs +++ b/src/core/Akka/Configuration/Hocon/HoconValue.cs @@ -32,7 +32,19 @@ public HoconValue() /// public bool IsEmpty { - get { return Values.Count == 0; } + get + { + if (Values.Count == 0) + return true; + + var first = Values[0] as HoconObject; + if (first != null) + { + if (first.Items.Count == 0) + return true; + } + return false; + } } /// diff --git a/src/core/Akka/Dispatch/ActorTaskScheduler.cs b/src/core/Akka/Dispatch/ActorTaskScheduler.cs index 659b9481080..550d664c496 100644 --- a/src/core/Akka/Dispatch/ActorTaskScheduler.cs +++ b/src/core/Akka/Dispatch/ActorTaskScheduler.cs @@ -128,6 +128,7 @@ await action() //if mailbox was suspended, make sure we re-enable message processing again mailbox.Resume(MailboxSuspendStatus.AwaitingTask); + context.CheckReceiveTimeout(); }, Outer, CancellationToken.None, diff --git a/src/core/Akka/Dispatch/DequeBasedMailbox.cs b/src/core/Akka/Dispatch/DequeBasedMailbox.cs index 8a564ab37ae..77f6f57b154 100644 --- a/src/core/Akka/Dispatch/DequeBasedMailbox.cs +++ b/src/core/Akka/Dispatch/DequeBasedMailbox.cs @@ -6,24 +6,25 @@ //----------------------------------------------------------------------- using Akka.Actor; +using Akka.Dispatch.MessageQueues; namespace Akka.Dispatch { /// - /// Used for instances that support double-ended queues. + /// Used for instances that support double-ended queues. /// public interface IDequeBasedMailbox { /// /// Enqueues an to the front of - /// the . Typically called during + /// the . Typically called during /// a or operation. /// /// The message that will be prepended to the queue. void EnqueueFirst(Envelope envelope); /// - /// Posts a message to the back of the + /// Posts a message to the back of the /// /// The intended recipient of the message. /// The message that will be appended to the queue. diff --git a/src/core/Akka/Dispatch/Dispatchers.cs b/src/core/Akka/Dispatch/Dispatchers.cs index bde518643fc..39626c5044b 100644 --- a/src/core/Akka/Dispatch/Dispatchers.cs +++ b/src/core/Akka/Dispatch/Dispatchers.cs @@ -226,7 +226,7 @@ private MessageDispatcherConfigurator LookupConfigurator(string id) /// /// The provided configuration section. /// An instance of the , if valid. - /// if the `id` property is missing from + /// if the `id` property is missing from /// thrown if the dispatcher path or type cannot be resolved. internal MessageDispatcher From(Config cfg) { diff --git a/src/core/Akka/Dispatch/MessageQueues/DequeWrapperMessageQueue.cs b/src/core/Akka/Dispatch/MessageQueues/DequeWrapperMessageQueue.cs index 47fa432a328..02a3cfa149c 100644 --- a/src/core/Akka/Dispatch/MessageQueues/DequeWrapperMessageQueue.cs +++ b/src/core/Akka/Dispatch/MessageQueues/DequeWrapperMessageQueue.cs @@ -20,7 +20,7 @@ public class DequeWrapperMessageQueue : IMessageQueue, IDequeBasedMessageQueueSe private readonly Stack _prependBuffer = new Stack(); private readonly IMessageQueue _messageQueue; /// - /// Takes another as an argument - wraps + /// Takes another as an argument - wraps /// in order to provide it with prepend () semantics. /// /// @@ -47,7 +47,7 @@ public int Count } /// - /// Enqueue a message to the back of the + /// Enqueue a message to the back of the /// /// public void Enqueue(Envelope envelope) @@ -59,7 +59,7 @@ public void Enqueue(Envelope envelope) /// Attempt to dequeue a message from the front of the prepend buffer. /// /// If the prepend buffer is empty, dequeue a message from the normal - /// wrapped but this wrapper. + /// wrapped but this wrapper. /// /// The message to return, if any /// true if a message was available, false otherwise. diff --git a/src/core/Akka/Dispatch/SysMsg/ISystemMessage.cs b/src/core/Akka/Dispatch/SysMsg/ISystemMessage.cs index 54694b7a68a..b0b112b8395 100644 --- a/src/core/Akka/Dispatch/SysMsg/ISystemMessage.cs +++ b/src/core/Akka/Dispatch/SysMsg/ISystemMessage.cs @@ -19,7 +19,6 @@ namespace Akka.Dispatch.SysMsg /// /// Class ISystemMessage. /// - /// ** public interface ISystemMessage : INoSerializationVerificationNeeded { } diff --git a/src/core/Akka/Event/EventBusUnsubscriber.cs b/src/core/Akka/Event/EventBusUnsubscriber.cs new file mode 100644 index 00000000000..67ed2d212ed --- /dev/null +++ b/src/core/Akka/Event/EventBusUnsubscriber.cs @@ -0,0 +1,132 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Akka.Actor; +using Akka.Actor.Internal; +using Akka.Util.Internal; + +namespace Akka.Event +{ + /// + /// INTERNAL API + /// + /// Watches all actors which subscribe on the given eventStream, and unsubscribes them from it when they are Terminated. + /// + /// Assumptions note: + /// We do not guarantee happens-before in the EventStream when 2 threads subscribe(a) / unsubscribe(a) on the same actor, + /// thus the messages sent to this actor may appear to be reordered - this is fine, because the worst-case is starting to + /// needlessly watch the actor which will not cause trouble for the stream. This is a trade-off between slowing down + /// subscribe calls * because of the need of linearizing the history message sequence and the possibility of sometimes + /// watching a few actors too much - we opt for the 2nd choice here. + /// + class EventStreamUnsubscriber : ActorBase + { + private readonly EventStream _eventStream; + private readonly bool _debug; + private readonly ActorSystem _system; + + public EventStreamUnsubscriber(EventStream eventStream, ActorSystem system, bool debug) + { + _eventStream = eventStream; + _system = system; + _debug = debug; + + } + + protected override bool Receive(object message) + { + return message.Match().With(register => + { + if (_debug) + _eventStream.Publish(new Debug(this.GetType().Name, GetType(), + string.Format("watching {0} in order to unsubscribe from EventStream when it terminates", register.Actor))); + Context.Watch(register.Actor); + }).With(unregister => + { + if (_debug) + _eventStream.Publish(new Debug(this.GetType().Name, GetType(), + string.Format("unwatching {0} since has no subscriptions", unregister.Actor))); + Context.Unwatch(unregister.Actor); + }).With(terminated => + { + if (_debug) + _eventStream.Publish(new Debug(this.GetType().Name, GetType(), + string.Format("unsubscribe {0} from {1}, because it was terminated", terminated.Actor , _eventStream ))); + _eventStream.Unsubscribe(terminated.Actor); + }) + .WasHandled; + } + + protected override void PreStart() + { + if (_debug) + _eventStream.Publish(new Debug(this.GetType().Name, GetType(), + string.Format("registering unsubscriber with {0}", _eventStream))); + _eventStream.InitUnsubscriber(Self, _system); + } + + internal class Register + { + public Register(IActorRef actor) + { + Actor = actor; + } + + public IActorRef Actor { get; private set; } + } + + + internal class Terminated + { + public Terminated(IActorRef actor) + { + Actor = actor; + } + + public IActorRef Actor { get; private set; } + } + + internal class UnregisterIfNoMoreSubscribedChannels + { + public UnregisterIfNoMoreSubscribedChannels(IActorRef actor) + { + Actor = actor; + } + + public IActorRef Actor { get; private set; } + } + } + + + + /// + /// Provides factory for Akka.Event.EventStreamUnsubscriber actors with unique names. + /// This is needed if someone spins up more EventStreams using the same ActorSystem, + /// each stream gets it's own unsubscriber. + /// + class EventStreamUnsubscribersProvider + { + private readonly AtomicCounter _unsubscribersCounter = new AtomicCounter(0); + private static readonly EventStreamUnsubscribersProvider _instance = new EventStreamUnsubscribersProvider(); + + + public static EventStreamUnsubscribersProvider Instance + { + get { return _instance; } + } + + public void Start(ActorSystemImpl system, EventStream eventStream, bool debug) + { + system.SystemActorOf(Props.Create(eventStream, system, debug), + string.Format("EventStreamUnsubscriber-{0}", _unsubscribersCounter.IncrementAndGet())); + } + } +} diff --git a/src/core/Akka/Event/EventStream.cs b/src/core/Akka/Event/EventStream.cs index 91f6bf6cbb9..968867f8f85 100644 --- a/src/core/Akka/Event/EventStream.cs +++ b/src/core/Akka/Event/EventStream.cs @@ -6,7 +6,13 @@ //----------------------------------------------------------------------- using System; +using System.Collections.Generic; +using System.Linq; using Akka.Actor; +using Akka.Actor.Internal; +using Akka.Util; +using Akka.Util.Internal; +using Akka.Util.Internal.Collections; namespace Akka.Event { @@ -25,6 +31,9 @@ public class EventStream : LoggingBus /// private readonly bool _debug; + + private readonly AtomicReference, IActorRef>> _initiallySubscribedOrUnsubscriber = + new AtomicReference, IActorRef>>(); /// /// Initializes a new instance of the class. /// @@ -50,7 +59,7 @@ public override bool Subscribe(IActorRef subscriber, Type channel) { Publish(new Debug(SimpleName(this), GetType(), "subscribing " + subscriber + " to channel " + channel)); } - + RegisterWithUnsubscriber(subscriber); return base.Subscribe(subscriber, channel); } @@ -70,7 +79,7 @@ public override bool Unsubscribe(IActorRef subscriber, Type channel) { Publish(new Debug(SimpleName(this), GetType(), "unsubscribing " + subscriber + " from channel " + channel)); } - + UnregisterIfNoMoreSubscribedChannels(subscriber); return base.Unsubscribe(subscriber, channel); } @@ -89,9 +98,83 @@ public override bool Unsubscribe(IActorRef subscriber) { Publish(new Debug(SimpleName(this), GetType(), "unsubscribing " + subscriber + " from all channels")); } - + UnregisterIfNoMoreSubscribedChannels(subscriber); return base.Unsubscribe(subscriber); } + + public void StartUnsubscriber(ActorSystemImpl system) + { + EventStreamUnsubscribersProvider.Instance.Start(system, this, _debug); + } + + public bool InitUnsubscriber(IActorRef unsubscriber, ActorSystem system) + { + if (system == null) + { + return false; + } + return _initiallySubscribedOrUnsubscriber.Match().With>>(v => + { + if (_initiallySubscribedOrUnsubscriber.CompareAndSet(v, Either.Right(unsubscriber))) + { + if (_debug) + { + Publish(new Debug(SimpleName(this), GetType(), + string.Format("initialized unsubscriber to: {0} registering {1} initial subscribers with it", unsubscriber, v.Value.Count))); + + } + v.Value.ForEach(RegisterWithUnsubscriber); + + + } + else + { + InitUnsubscriber(unsubscriber, system); + } + + + }).With>(presentUnsubscriber => + { + if (_debug) + { + Publish(new Debug(SimpleName(this), GetType(), + string.Format("not using unsubscriber {0}, because already initialized with {1}", unsubscriber, presentUnsubscriber))); + + } + }).WasHandled; + } + + private void RegisterWithUnsubscriber(IActorRef subscriber) + { + _initiallySubscribedOrUnsubscriber.Match().With>>(v => + { + if (_initiallySubscribedOrUnsubscriber.CompareAndSet(v, + Either.Left>(v.Value.Add(subscriber)))) + { + RegisterWithUnsubscriber(subscriber); + } + + }).With>(unsubscriber => + { + unsubscriber.Value.Tell( new EventStreamUnsubscriber.Register(subscriber)); + }); + } + + private void UnregisterIfNoMoreSubscribedChannels(IActorRef subscriber) + { + _initiallySubscribedOrUnsubscriber.Match().With>>(v => + { + if (_initiallySubscribedOrUnsubscriber.CompareAndSet(v, + Either.Left>(v.Value.Remove(subscriber)))) + { + UnregisterIfNoMoreSubscribedChannels(subscriber); + } + + }).With>(unsubscriber => + { + unsubscriber.Value.Tell(new EventStreamUnsubscriber.UnregisterIfNoMoreSubscribedChannels(subscriber)); + }); + } } } diff --git a/src/core/Akka/Event/LoggingAdapterBase.cs b/src/core/Akka/Event/LoggingAdapterBase.cs index 9f29f0f6f94..025c9c282c7 100644 --- a/src/core/Akka/Event/LoggingAdapterBase.cs +++ b/src/core/Akka/Event/LoggingAdapterBase.cs @@ -31,15 +31,21 @@ public abstract class LoggingAdapterBase : ILoggingAdapter /// Creates an instance of the LoggingAdapterBase. /// /// The log message formatter used by this logging adapter. - /// + /// This exception is thrown when the supplied message formatter is null. protected LoggingAdapterBase(ILogMessageFormatter logMessageFormatter) { if(logMessageFormatter == null) - throw new ArgumentException("logMessageFormatter"); + throw new ArgumentNullException("logMessageFormatter", "The message formatter must not be null."); _logMessageFormatter = logMessageFormatter; } - + + /// + /// Checks the logging adapter to see if the supplied is enabled. + /// + /// The log level to check if it is enabled in this logging adapter. + /// true if the supplied log level is enabled; otherwise false + /// This exception is thrown when the supplied log level is unknown. public bool IsEnabled(LogLevel logLevel) { switch(logLevel) @@ -62,7 +68,7 @@ public bool IsEnabled(LogLevel logLevel) /// /// The log level of the log event. /// The log message of the log event. - /// + /// This exception is thrown when the supplied log level is unknown. protected void NotifyLog(LogLevel logLevel, object message) { switch(logLevel) diff --git a/src/core/Akka/IO/SelectionHandler.cs b/src/core/Akka/IO/SelectionHandler.cs index 7438c8f4c64..eb4aa8d58cd 100644 --- a/src/core/Akka/IO/SelectionHandler.cs +++ b/src/core/Akka/IO/SelectionHandler.cs @@ -196,7 +196,6 @@ public ChannelRegistryImpl(ILoggingAdapter log) { _log = log; _executionContext = new SingleThreadExecutionContext(); - Execute(Select); } private void Execute(Action action) @@ -206,40 +205,39 @@ private void Execute(Action action) private void Select() { - if (_read.Count > 0 || _write.Count > 0) + if (_read.Count == 0 && _write.Count == 0) return; // Stop select loop when no more interested sockets. It will be started again once a socket is registered + + var readable = _read.Keys.ToList(); + var writeable = _write.Keys.ToList(); + try { - var readable = _read.Keys.ToList(); - var writeable = _write.Keys.ToList(); - try + Socket.Select(readable, writeable, null, 1); + foreach (var socket in readable) { - Socket.Select(readable, writeable, null, 0); - foreach (var socket in readable) - { - var channel = _read[socket]; - if (channel.IsOpen()) - channel.Connection.Tell(ChannelReadable.Instance); - else - channel.Connection.Tell(ChannelAcceptable.Instance); - _read.Remove(socket); - } - foreach (var socket in writeable) - { - var channel = _write[socket]; - if (channel.IsOpen()) - channel.Connection.Tell(ChannelWritable.Instance); - else - channel.Connection.Tell(ChannelConnectable.Instance); - _write.Remove(socket); - } + var channel = _read[socket]; + if (channel.IsOpen()) + channel.Connection.Tell(ChannelReadable.Instance); + else + channel.Connection.Tell(ChannelAcceptable.Instance); + _read.Remove(socket); } - catch (SocketException ex) + foreach (var socket in writeable) { - if (ex.SocketErrorCode == SocketError.NotSocket) - { - // One of the sockets has been closed - readable.Where(x => !x.Connected).ForEach(x =>_read.Remove(x)); - writeable.Where(x => !x.Connected).ForEach(x => _write.Remove(x)); - } + var channel = _write[socket]; + if (channel.IsOpen()) + channel.Connection.Tell(ChannelWritable.Instance); + else + channel.Connection.Tell(ChannelConnectable.Instance); + _write.Remove(socket); + } + } + catch (SocketException ex) + { + if (ex.SocketErrorCode == SocketError.NotSocket) + { + // One of the sockets has been closed + readable.Where(x => !x.Connected).ForEach(x =>_read.Remove(x)); + writeable.Where(x => !x.Connected).ForEach(x => _write.Remove(x)); } } Execute(Select); @@ -262,14 +260,24 @@ private void EnableInterest(SocketChannel channel, SocketAsyncOperation op) { switch (op) { - case SocketAsyncOperation.Accept: - case SocketAsyncOperation.Receive: - Execute(() => _read.Add(channel.Socket, channel)); - break; - case SocketAsyncOperation.Connect: - case SocketAsyncOperation.Send: - Execute(() => _write.Add(channel.Socket, channel)); - break; + case SocketAsyncOperation.Accept: + case SocketAsyncOperation.Receive: + Execute(() => + { + _read.Add(channel.Socket, channel); + if (_read.Count == 1 && _write.Count == 0) // Start the select loop on initial enable interest + Select(); // The select loop will stop itself if no more interested sockets + }); + break; + case SocketAsyncOperation.Connect: + case SocketAsyncOperation.Send: + Execute(() => + { + _write.Add(channel.Socket, channel); // Start the select loop on initial enable interest + if (_read.Count == 0 && _write.Count == 1) // The select loop will stop itself if no more interested sockets + Select(); + }); + break; } } private void DisableInterest(SocketChannel channel, SocketAsyncOperation op) @@ -345,7 +353,7 @@ protected override void LogFailure(IActorContext context, IActorRef child, Excep { try { - var e = (ActorInitializationException) cause; + var e = cause as ActorInitializationException; var logMessage = e != null ? e.GetBaseException() .Message : cause.Message; diff --git a/src/core/Akka/IO/SimpleDnsCache.cs b/src/core/Akka/IO/SimpleDnsCache.cs index 71e4bc67f1b..5a295417e1c 100644 --- a/src/core/Akka/IO/SimpleDnsCache.cs +++ b/src/core/Akka/IO/SimpleDnsCache.cs @@ -19,8 +19,8 @@ internal interface IPeriodicCacheCleanup public class SimpleDnsCache : DnsBase, IPeriodicCacheCleanup { - private AtomicReference _cache; - private long _ticksBase; + private readonly AtomicReference _cache; + private readonly long _ticksBase; public SimpleDnsCache() { @@ -58,10 +58,10 @@ public void CleanUp() class Cache { private readonly SortedSet _queue; - private readonly IDictionary _cache; + private readonly Dictionary _cache; private readonly Func _clock; - public Cache(SortedSet queue, IDictionary cache, Func clock) + public Cache(SortedSet queue, Dictionary cache, Func clock) { _queue = queue; _cache = cache; @@ -79,9 +79,17 @@ public Dns.Resolved Get(string name) public Cache Put(Dns.Resolved answer, long ttl) { var until = _clock() + ttl; - _queue.Add(new ExpiryEntry(answer.Name, until)); - _cache.Add(answer.Name, new CacheEntry(answer, until)); - return this; + + var cache = new Dictionary(_cache); + if (cache.ContainsKey(answer.Name)) + cache[answer.Name] = new CacheEntry(answer, until); + else + cache.Add(answer.Name, new CacheEntry(answer, until)); + + return new Cache( + queue: new SortedSet(_queue, new ExpiryEntryComparer()) { new ExpiryEntry(answer.Name, until) }, + cache: cache, + clock: _clock); } public Cache Cleanup() @@ -95,7 +103,7 @@ public Cache Cleanup() if (_cache.ContainsKey(name) && !_cache[name].IsValid(now)) _cache.Remove(name); } - return new Cache(_queue, _cache, _clock); + return new Cache(new SortedSet(), new Dictionary(_cache), _clock); } } diff --git a/src/core/Akka/IO/SimpleDnsManager.cs b/src/core/Akka/IO/SimpleDnsManager.cs index 7e87fcce606..d47ff175f3d 100644 --- a/src/core/Akka/IO/SimpleDnsManager.cs +++ b/src/core/Akka/IO/SimpleDnsManager.cs @@ -40,7 +40,7 @@ protected override bool Receive(object message) var r = message as Dns.Resolve; if (r != null) { - _log.Debug("Resolution request for {} from {}", r.Name, Sender); + _log.Debug("Resolution request for {0} from {1}", r.Name, Sender); _resolver.Forward(r); } if (message is CacheCleanup) diff --git a/src/core/Akka/IO/Tcp.cs b/src/core/Akka/IO/Tcp.cs index 8cdd1d41357..3312655abbf 100644 --- a/src/core/Akka/IO/Tcp.cs +++ b/src/core/Akka/IO/Tcp.cs @@ -358,7 +358,7 @@ public override Event Ack /// /// A write command which aggregates two other write commands. Using this construct - /// you can chain a number of and/or [[WriteFile]] commands together in a way + /// you can chain a number of and/or commands together in a way /// that allows them to be handled as a single write which gets written out to the /// network as quickly as possible. /// If the sub commands contain `ack` requests they will be honored as soon as the @@ -484,7 +484,7 @@ public class Event : Message /// Whenever data are read from a socket they will be transferred within this /// class to the handler actor which was designated in the message. /// - public sealed class Received + public sealed class Received : Event { public Received(ByteString data) { @@ -500,7 +500,7 @@ public Received(ByteString data) /// in the message. The connection is characterized by the `remoteAddress` /// and `localAddress` TCP endpoints. /// - public sealed class Connected + public sealed class Connected : Event { public Connected(EndPoint remoteAddress, EndPoint localAddress) { diff --git a/src/core/Akka/IO/TcpManager.cs b/src/core/Akka/IO/TcpManager.cs index 7dae326cd1b..69836e05fd4 100644 --- a/src/core/Akka/IO/TcpManager.cs +++ b/src/core/Akka/IO/TcpManager.cs @@ -10,6 +10,45 @@ namespace Akka.IO { + /** + * TODO: CLRify comment + * + * INTERNAL API + * + * TcpManager is a facade for accepting commands () to open client or server TCP connections. + * + * TcpManager is obtainable by calling {{{ IO(Tcp) }}} (see [[akka.io.IO]] and [[akka.io.Tcp]]) + * + * == Bind == + * + * To bind and listen to a local address, a command must be sent to this actor. If the binding + * was successful, the sender of the will be notified with a + * message. The sender() of the message is the Listener actor (an internal actor responsible for + * listening to server events). To unbind the port an message must be sent to the Listener actor. + * + * If the bind request is rejected because the Tcp system is not able to register more channels (see the nr-of-selectors + * and max-channels configuration options in the akka.io.tcp section of the configuration) the sender will be notified + * with a message. This message contains the original command for reference. + * + * When an inbound TCP connection is established, the handler will be notified by a message. + * The sender of this message is the Connection actor (an internal actor representing the TCP connection). At this point + * the procedure is the same as for outbound connections (see section below). + * + * == Connect == + * + * To initiate a connection to a remote server, a message must be sent to this actor. If the + * connection succeeds, the sender() will be notified with a message. The sender of the + * message is the Connection actor (an internal actor representing the TCP connection). Before + * starting to use the connection, a handler must be registered to the Connection actor by sending a + * command message. After a handler has been registered, all incoming data will be sent to the handler in the form of + * messages. To write data to the connection, a message must be sent + * to the Connection actor. + * + * If the connect request is rejected because the Tcp system is not able to register more channels (see the nr-of-selectors + * and max-channels configuration options in the akka.io.tcp section of the configuration) the sender will be notified + * with a message. This message contains the original command for reference. + * + */ internal class TcpManager : SelectionHandler.SelectorBasedManager { private readonly TcpExt _tcp; diff --git a/src/core/Akka/IO/TcpOutgoingConnection.cs b/src/core/Akka/IO/TcpOutgoingConnection.cs index c16df014fe5..1bf83e05a30 100644 --- a/src/core/Akka/IO/TcpOutgoingConnection.cs +++ b/src/core/Akka/IO/TcpOutgoingConnection.cs @@ -138,8 +138,7 @@ private Receive Connecting(ChannelRegistration registration, int remainingFinish } else { - Log.Debug("Could not establish connection because finishConnect " + - "never returned true (consider increasing akka.io.tcp.finish-connect-retries)"); + Log.Debug("Could not establish connection because finishConnect never returned true (consider increasing akka.io.tcp.finish-connect-retries)"); Stop(); } } diff --git a/src/core/Akka/IO/UdpManager.cs b/src/core/Akka/IO/UdpManager.cs index b2ad7b8734e..121babbee8b 100644 --- a/src/core/Akka/IO/UdpManager.cs +++ b/src/core/Akka/IO/UdpManager.cs @@ -25,31 +25,30 @@ namespace Akka.IO * * == Bind and send == * - * To bind and listen to a local address, a [[akka.io.Udp..Bind]] command must be sent to this actor. If the binding - * was successful, the sender of the [[akka.io.Udp.Bind]] will be notified with a [[akka.io.Udp.Bound]] - * message. The sender of the [[akka.io.Udp.Bound]] message is the Listener actor (an internal actor responsible for - * listening to server events). To unbind the port an [[akka.io.Tcp.Unbind]] message must be sent to the Listener actor. + * To bind and listen to a local address, a command must be sent to this actor. If the binding + * was successful, the sender of the will be notified with a + * message. The sender of the message is the Listener actor (an internal actor responsible for + * listening to server events). To unbind the port an message must be sent to the Listener actor. * - * If the bind request is rejected because the Udp system is not able to register more channels (see the nr-of-selectors - * and max-channels configuration options in the akka.io.udp section of the configuration) the sender will be notified - * with a [[akka.io.Udp.CommandFailed]] message. This message contains the original command for reference. + * If the bind request is rejected because the Udp system is not able to register more channels (see the nr-of-selectors + * and max-channels configuration options in the akka.io.udp section of the configuration) the sender will be notified + * with a message. This message contains the original command for reference. * - * The handler provided in the [[akka.io.Udp.Bind]] message will receive inbound datagrams to the bound port - * wrapped in [[akka.io.Udp.Received]] messages which contain the payload of the datagram and the sender address. + * The handler provided in the message will receive inbound datagrams to the bound port + * wrapped in messages which contain the payload of the datagram and the sender address. * - * UDP datagrams can be sent by sending [[akka.io.Udp.Send]] messages to the Listener actor. The sender port of the + * UDP datagrams can be sent by sending messages to the Listener actor. The sender port of the * outbound datagram will be the port to which the Listener is bound. * * == Simple send == * * Udp provides a simple method of sending UDP datagrams if no reply is expected. To acquire the Sender actor * a SimpleSend message has to be sent to the manager. The sender of the command will be notified by a SimpleSenderReady - * message that the service is available. UDP datagrams can be sent by sending [[akka.io.Udp.Send]] messages to the + * message that the service is available. UDP datagrams can be sent by sending messages to the * sender of SimpleSenderReady. All the datagrams will contain an ephemeral local port as sender and answers will be * discarded. * */ - internal class UdpManager : SelectionHandler.SelectorBasedManager { private readonly UdpExt _udp; diff --git a/src/core/Akka/Pattern/CircuitBreaker.cs b/src/core/Akka/Pattern/CircuitBreaker.cs new file mode 100644 index 00000000000..bf5b0b85113 --- /dev/null +++ b/src/core/Akka/Pattern/CircuitBreaker.cs @@ -0,0 +1,261 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Akka.Util.Internal; + +namespace Akka.Pattern +{ + /// + /// Provides circuit breaker functionality to provide stability when working with + /// "dangerous" operations, e.g. calls to remote systems + /// + /// + /// + /// Transitions through three states: + /// + /// + /// In *Closed* state, + /// calls pass through until the maxFailures count is reached. + /// This causes the circuit breaker to open. Both exceptions and calls exceeding + /// callTimeout are considered failures. + /// + /// + /// In *Open* state, + /// calls fail-fast with an exception. After resetTimeout, + /// circuit breaker transitions to half-open state. + /// + /// + /// In *Half-Open* state, + /// the first call will be allowed through, if it succeeds + /// the circuit breaker will reset to closed state. If it fails, the circuit + /// breaker will re-open to open state. All calls beyond the first that execute + /// while the first is running will fail-fast with an exception. + /// + /// + /// + public class CircuitBreaker + { + /// + /// The current state of the breaker -- Closed, Half-Open, or Closed -- *access only via helper methods* + /// + private AtomicState _currentState; + + /// + /// Helper method for access to the underlying state via Interlocked + /// + /// Previous state on transition + /// Next state on transition + /// Whether the previous state matched correctly + private bool SwapState( AtomicState oldState, AtomicState newState ) + { + return Interlocked.CompareExchange( ref _currentState, newState, oldState ) == oldState; + } + + /// + /// Helper method for access to the underlying state via Interlocked + /// + private AtomicState CurrentState + { + get + { + Interlocked.MemoryBarrier( ); + return _currentState; + } + } + + public int MaxFailures { get; private set; } + + public TimeSpan CallTimeout { get; private set; } + public TimeSpan ResetTimeout { get; private set; } + + //akka.io implementation is to use nested static classes and access parent member variables + //.Net static nested classes do not have access to parent member variables -- so we configure the states here and + //swap them above + private AtomicState Closed { get; set; } + private AtomicState Open { get; set; } + private AtomicState HalfOpen { get; set; } + + /// + /// Create a new CircuitBreaker + /// + /// Maximum number of failures before opening the circuit + /// of time after which to consider a call a failure + /// of time after which to attempt to close the circuit + /// + public static CircuitBreaker Create( int maxFailures, TimeSpan callTimeout, TimeSpan resetTimeout ) + { + return new CircuitBreaker( maxFailures, callTimeout, resetTimeout ); + } + + /// + /// Create a new CircuitBreaker + /// + /// Maximum number of failures before opening the circuit + /// of time after which to consider a call a failure + /// of time after which to attempt to close the circuit + /// + public CircuitBreaker( int maxFailures, TimeSpan callTimeout, TimeSpan resetTimeout ) + { + MaxFailures = maxFailures; + CallTimeout = callTimeout; + ResetTimeout = resetTimeout; + Closed = new Closed( this ); + Open = new Open( this ); + HalfOpen = new HalfOpen( this ); + _currentState = Closed; + //_failures = new AtomicInteger(); + } + + /// + /// Retrieves current failure count. + /// + public long CurrentFailureCount + { + get { return Closed.Current; } + } + + /// + /// Wraps invocation of asynchronous calls that need to be protected + /// + /// + /// Call needing protected + /// containing the call result + public async Task WithCircuitBreaker( Func> body ) + { + return await CurrentState.Invoke( body ); + } + + /// + /// Wraps invocation of asynchronous calls that need to be protected + /// + /// Call needing protected + /// + public async Task WithCircuitBreaker( Func body ) + { + await CurrentState.Invoke( body ); + } + + /// + /// The failure will be recorded farther down. + /// + /// + public void WithSyncCircuitBreaker( Action body ) + { + var cbTask = WithCircuitBreaker( () => Task.Factory.StartNew( body ) ); + if ( !cbTask.Wait( CallTimeout ) ) + { + //throw new TimeoutException( string.Format( "Execution did not complete within the time alotted {0} ms", CallTimeout.TotalMilliseconds ) ); + } + if ( cbTask.Exception != null ) + { + throw cbTask.Exception; + } + } + + /// + /// Wraps invocations of asynchronous calls that need to be protected + /// If this does not complete within the time allotted, it should return default() + /// + /// + /// Await.result( + /// withCircuitBreaker(try Future.successful(body) catch { case NonFatal(t) ⇒ Future.failed(t) }), + /// callTimeout) + /// + /// + /// + /// + /// + /// or default() + public T WithSyncCircuitBreaker( Func body ) + { + var cbTask = WithCircuitBreaker( () => Task.Factory.StartNew( body ) ); + return cbTask.Wait( CallTimeout ) ? cbTask.Result : default(T); + } + + /// + /// Adds a callback to execute when circuit breaker opens + /// + /// Handler to be invoked on state change + /// CircuitBreaker for fluent usage + public CircuitBreaker OnOpen( Action callback ) + { + Open.AddListener( callback ); + return this; + } + + /// + /// Adds a callback to execute when circuit breaker transitions to half-open + /// + /// Handler to be invoked on state change + /// CircuitBreaker for fluent usage + public CircuitBreaker OnHalfOpen( Action callback ) + { + HalfOpen.AddListener( callback ); + return this; + } + + /// + /// Adds a callback to execute when circuit breaker state closes + /// + /// Handler to be invoked on state change + /// CircuitBreaker for fluent usage + public CircuitBreaker OnClose( Action callback ) + { + Closed.AddListener( callback ); + return this; + } + + /// + /// Implements consistent transition between states. Throws IllegalStateException if an invalid transition is attempted. + /// + /// State being transitioning from + /// State being transitioned to + private void Transition( AtomicState fromState, AtomicState toState ) + { + if ( SwapState( fromState, toState ) ) + { + Debug.WriteLine( "Successful transition from {0} to {1}", fromState, toState ); + toState.Enter( ); + } + else + { + throw new IllegalStateException( string.Format( "Illegal transition attempted from {0} to {1}", fromState, toState ) ); + } + } + + /// + /// Trips breaker to an open state. This is valid from Closed or Half-Open states + /// + /// State we're coming from (Closed or Half-Open) + internal void TripBreaker( AtomicState fromState ) + { + Transition( fromState, Open ); + } + + /// + /// Resets breaker to a closed state. This is valid from an Half-Open state only. + /// + internal void ResetBreaker( ) + { + Transition( HalfOpen, Closed ); + } + + /// + /// Attempts to reset breaker by transitioning to a half-open state. This is valid from an Open state only. + /// + internal void AttemptReset( ) + { + Transition( Open, HalfOpen ); + } + + //private readonly Task timeoutTask = Task.FromResult(new TimeoutException("Circuit Breaker Timed out.")); + } +} diff --git a/src/core/Akka/Pattern/CircuitBreakerState.cs b/src/core/Akka/Pattern/CircuitBreakerState.cs new file mode 100644 index 00000000000..bcd0281cd80 --- /dev/null +++ b/src/core/Akka/Pattern/CircuitBreakerState.cs @@ -0,0 +1,227 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Globalization; +using System.Threading.Tasks; +using Akka.Util; +using Akka.Util.Internal; + +namespace Akka.Pattern +{ + /// + /// Concrete implementation of Open state + /// + internal class Open : AtomicState + { + private readonly CircuitBreaker _breaker; + + public Open( CircuitBreaker breaker ) + : base( breaker.CallTimeout, 0 ) + { + _breaker = breaker; + } + + /// + /// Fail-fast on any invocation + /// + /// + /// Implementation of the call that needs protected + /// containing result of protected call + public override Task Invoke( Func> body ) + { + throw new OpenCircuitException( ); + } + + /// + /// Implementation of invoke, which simply attempts the call + /// + /// Implementation of the call that needs protected + /// containing result of protected call + public override Task Invoke( Func body ) + { + throw new OpenCircuitException( ); + } + + /// + /// No-op for open, calls are never executed so cannot succeed or fail + /// + protected override void CallFails( ) + { + //throw new NotImplementedException(); + } + + /// + /// No-op for open, calls are never executed so cannot succeed or fail + /// + protected override void CallSucceeds( ) + { + //throw new NotImplementedException(); + } + + /// + /// On entering this state, schedule an attempted reset and store the entry time to + /// calculate remaining time before attempted reset. + /// + protected override void EnterInternal( ) + { + Task.Delay( _breaker.ResetTimeout ).ContinueWith( task => _breaker.AttemptReset( ) ); + } + } + + /// + /// Concrete implementation of half-open state + /// + internal class HalfOpen : AtomicState + { + private readonly CircuitBreaker _breaker; + private readonly AtomicBoolean _lock; + + public HalfOpen( CircuitBreaker breaker ) + : base( breaker.CallTimeout, 0 ) + { + _breaker = breaker; + _lock = new AtomicBoolean(); + } + + /// + /// Allows a single call through, during which all other callers fail-fast. If the call fails, the breaker reopens. + /// If the call succeeds, the breaker closes. + /// + /// + /// Implementation of the call that needs protected + /// containing result of protected call + public override async Task Invoke( Func> body ) + { + if ( !_lock.CompareAndSet( true, false) ) + { + throw new OpenCircuitException( ); + } + return await CallThrough( body ); + } + + /// + /// Allows a single call through, during which all other callers fail-fast. If the call fails, the breaker reopens. + /// If the call succeeds, the breaker closes. + /// + /// Implementation of the call that needs protected + /// containing result of protected call + public override async Task Invoke( Func body ) + { + if ( !_lock.CompareAndSet( true, false ) ) + { + throw new OpenCircuitException( ); + } + await CallThrough( body ); + } + + /// + /// Reopen breaker on failed call. + /// + protected override void CallFails( ) + { + _breaker.TripBreaker( this ); + } + + /// + /// Reset breaker on successful call. + /// + protected override void CallSucceeds( ) + { + _breaker.ResetBreaker( ); + } + + /// + /// On entry, guard should be reset for that first call to get in + /// + protected override void EnterInternal( ) + { + _lock.Value = true ; + } + + /// + /// Override for more descriptive toString + /// + /// + public override string ToString( ) + { + return string.Format( CultureInfo.InvariantCulture, "Half-Open currently testing call for success = {0}", ( _lock == true ) ); + } + } + + /// + /// Concrete implementation of Closed state + /// + internal class Closed : AtomicState + { + private readonly CircuitBreaker _breaker; + + public Closed( CircuitBreaker breaker ) + : base( breaker.CallTimeout, 0 ) + { + _breaker = breaker; + } + + /// + /// Implementation of invoke, which simply attempts the call + /// + /// + /// Implementation of the call that needs protected + /// containing result of protected call + public override async Task Invoke( Func> body ) + { + return await CallThrough( body ); + } + + /// + /// Implementation of invoke, which simply attempts the call + /// + /// Implementation of the call that needs protected + /// containing result of protected call + public override async Task Invoke( Func body ) + { + await CallThrough( body ); + } + + /// + /// On failed call, the failure count is incremented. The count is checked against the configured maxFailures, and + /// the breaker is tripped if we have reached maxFailures. + /// + protected override void CallFails( ) + { + if ( IncrementAndGet( ) == _breaker.MaxFailures ) + { + _breaker.TripBreaker( this ); + } + } + + /// + /// On successful call, the failure count is reset to 0 + /// + protected override void CallSucceeds( ) + { + Reset(); + } + + /// + /// On entry of this state, failure count is reset. + /// + protected override void EnterInternal( ) + { + Reset(); + } + + /// + /// Override for more descriptive toString + /// + /// + public override string ToString( ) + { + return string.Format( "Closed with failure count = {0}", Current ); + } + } +} \ No newline at end of file diff --git a/src/core/Akka/Pattern/IllegalStateException.cs b/src/core/Akka/Pattern/IllegalStateException.cs index b27a95958c1..0ffe86179d8 100644 --- a/src/core/Akka/Pattern/IllegalStateException.cs +++ b/src/core/Akka/Pattern/IllegalStateException.cs @@ -10,18 +10,27 @@ namespace Akka.Pattern { + /// + /// This exception is thrown when a method has been invoked at an illegal or inappropriate time. + /// public class IllegalStateException : AkkaException { - + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. public IllegalStateException(string message) : base(message) { - } + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. protected IllegalStateException(SerializationInfo info, StreamingContext context) : base(info, context) { } } } - diff --git a/src/core/Akka/Pattern/OpenCircuitException.cs b/src/core/Akka/Pattern/OpenCircuitException.cs new file mode 100644 index 00000000000..7b6f40d49b3 --- /dev/null +++ b/src/core/Akka/Pattern/OpenCircuitException.cs @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Runtime.Serialization; +using Akka.Actor; + +namespace Akka.Pattern +{ + /// + /// This exception is thrown when the CircuitBreaker is open. + /// + public class OpenCircuitException : AkkaException + { + /// + /// Initializes a new instance of the class. + /// + public OpenCircuitException() : base("Circuit Breaker is open; calls are failing fast") { } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public OpenCircuitException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public OpenCircuitException(string message, Exception cause) + : base(message, cause) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + protected OpenCircuitException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/core/Akka/Properties/AssemblyInfo.cs b/src/core/Akka/Properties/AssemblyInfo.cs index b407a191e1c..e0550a65f0a 100644 --- a/src/core/Akka/Properties/AssemblyInfo.cs +++ b/src/core/Akka/Properties/AssemblyInfo.cs @@ -35,4 +35,4 @@ [assembly: InternalsVisibleTo("Akka.Cluster")] [assembly: InternalsVisibleTo("Akka.Cluster.Tests")] [assembly: InternalsVisibleTo("Akka.MultiNodeTestRunner.Shared.Tests")] -[assembly: InternalsVisibleTo("Akka.MultiNodeTests")] +[assembly: InternalsVisibleTo("Akka.Cluster.Tests.MultiNode")] diff --git a/src/core/Akka/Routing/ConsistentHashRouter.cs b/src/core/Akka/Routing/ConsistentHashRouter.cs index 2d41b6baf0e..b5e6ecaeb9d 100644 --- a/src/core/Akka/Routing/ConsistentHashRouter.cs +++ b/src/core/Akka/Routing/ConsistentHashRouter.cs @@ -182,8 +182,7 @@ public override Routee Select(object message, Routee[] routees) } else { - _log.Value.Warning( - "Message [{0}] must be handled by hashMapping, or implement [{1}] or be wrapped in [{2}]", + _log.Value.Warning("Message [{0}] must be handled by hashMapping, or implement [{1}] or be wrapped in [{2}]", message.GetType().Name, typeof (IConsistentHashable).Name, typeof (ConsistentHashableEnvelope).Name); return Routee.NoRoutee; } diff --git a/src/core/Akka/Routing/ResizablePoolCell.cs b/src/core/Akka/Routing/ResizablePoolCell.cs index 047f416388d..d2d5e2744e3 100644 --- a/src/core/Akka/Routing/ResizablePoolCell.cs +++ b/src/core/Akka/Routing/ResizablePoolCell.cs @@ -11,6 +11,7 @@ using Akka.Actor; using Akka.Actor.Internal; using Akka.Dispatch; +using Akka.Dispatch.SysMsg; using Akka.Util; using Akka.Util.Internal; @@ -55,6 +56,7 @@ protected override void PreSuperStart() public override void Post(IActorRef sender, object message) { if(!(_routerProps.RouterConfig.IsManagementMessage(message)) && + !(message is ISystemMessage) && resizer.IsTimeForResize(_resizeCounter.GetAndIncrement()) && _resizeInProgress.CompareAndSet(false, true)) { @@ -93,7 +95,7 @@ internal void Resize(bool initial) } finally { - _resizeInProgress = false; + _resizeInProgress.Value = false; } } } diff --git a/src/core/Akka/Routing/Resizer.cs b/src/core/Akka/Routing/Resizer.cs index a48da810abf..8d772273c54 100644 --- a/src/core/Akka/Routing/Resizer.cs +++ b/src/core/Akka/Routing/Resizer.cs @@ -15,8 +15,8 @@ namespace Akka.Routing { /// - /// [[Pool]] routers with dynamically resizable number of routees are implemented by providing a Resizer - /// implementation in the [[akka.routing.Pool]] configuration + /// routers with dynamically resizable number of routees are implemented by providing a Resizer + /// implementation in the configuration /// public abstract class Resizer { @@ -50,7 +50,7 @@ public abstract class Resizer } /// - /// Implementation of [[Resizer]] that adjust the [[Pool]] based on specified thresholds. + /// Implementation of that adjust the based on specified thresholds. /// public class DefaultResizer : Resizer, IEquatable { @@ -93,6 +93,8 @@ public DefaultResizer(int lower, int upper, int pressureThreshold = 1, double ra BackoffThreshold = backoffThreshold; BackoffRate = backoffRate; MessagesPerResize = messagesPerResize; + + Validate(); } /// diff --git a/src/core/Akka/Routing/RouterConfig.cs b/src/core/Akka/Routing/RouterConfig.cs index 05893cfec53..368effc7531 100644 --- a/src/core/Akka/Routing/RouterConfig.cs +++ b/src/core/Akka/Routing/RouterConfig.cs @@ -412,7 +412,7 @@ protected RouterConfig OverrideUnsetConfig(RouterConfig other) /// public static SupervisorStrategy DefaultStrategy { - get { return new OneForOneStrategy(10, TimeSpan.FromSeconds(10), ex => Directive.Escalate); } + get { return new OneForOneStrategy(10, TimeSpan.FromSeconds(10), Decider.From(Directive.Escalate)); } } #endregion diff --git a/src/core/Akka/Util/AtomicBoolean.cs b/src/core/Akka/Util/AtomicBoolean.cs index 082586b7029..90c356d0176 100644 --- a/src/core/Akka/Util/AtomicBoolean.cs +++ b/src/core/Akka/Util/AtomicBoolean.cs @@ -23,7 +23,7 @@ internal class AtomicBoolean private int _value; /// - /// Sets the initial value of this to . + /// Sets the initial value of this to . /// public AtomicBoolean(bool initialValue = false) { @@ -47,10 +47,10 @@ public bool Value } /// - /// If equals , then set the Value to - /// . + /// If equals , then set the Value to + /// . /// - /// true if was set + /// true if was set public bool CompareAndSet(bool expected, bool newValue) { var expectedInt = expected ? _trueValue : _falseValue; diff --git a/src/core/Akka/Util/AtomicCounter.cs b/src/core/Akka/Util/AtomicCounter.cs deleted file mode 100644 index 1b5dfd78fc3..00000000000 --- a/src/core/Akka/Util/AtomicCounter.cs +++ /dev/null @@ -1,93 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2015 Typesafe Inc. -// Copyright (C) 2013-2015 Akka.NET project -// -//----------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Akka.Actor; - -namespace Akka.Util -{ - /// - /// Class used for atomic counters and increments. - /// - public class AtomicCounter - { - /// - /// Creates a new instance initialized to the value specified by . - /// If is not specified it defaults to -1. - /// - /// - public AtomicCounter(int initialValue=-1) - { - _value = initialValue; - } - - private int _value; - - /// - /// Retrieves the current value of the counter - /// - public int Current { get { return _value; } } - - /// - /// Increments the counter and returns the next value - /// - public int Next - { - get - { - return Interlocked.Increment(ref _value); - } - } - - /// - /// Returns the current value while simultaneously incrementing the counter - /// - public int GetAndIncrement() - { - var nextValue = Next; - return nextValue-1; - } - - /// - /// Increments the counter and returns the new value - /// - public int IncrementAndGet() - { - var nextValue = Next; - return nextValue; - } - - /// - /// Returns the current value and adds the specified value to the counter. - /// - /// - /// - public int GetAndAdd(int amount) - { - var newValue=Interlocked.Add(ref _value, amount); - return newValue-amount; - } - - - /// - /// Adds the specified value to the counter and returns the new value - /// - /// - /// - public int AddAndGet(int amount) - { - var newValue = Interlocked.Add(ref _value, amount); - return newValue; - } - } -} - diff --git a/src/core/Akka/Util/AtomicCounterLong.cs b/src/core/Akka/Util/AtomicCounterLong.cs deleted file mode 100644 index a6ebda22f2c..00000000000 --- a/src/core/Akka/Util/AtomicCounterLong.cs +++ /dev/null @@ -1,56 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2015 Typesafe Inc. -// Copyright (C) 2013-2015 Akka.NET project -// -//----------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Akka.Util -{ - /// - /// Atomic counter that uses longs internally - /// - public class AtomicCounterLong - { - public AtomicCounterLong(long seed) - { - _seed = seed; - } - - private long _seed; - - /// - /// Retrieves the current value of the counter - /// - public long Current { get { return _seed; } } - - /// - /// Increments the counter and returns the next value - /// - public long Next - { - get - { - return Interlocked.Increment(ref _seed); - } - } - - /// - /// Returns the current value while simultaneously incrementing the counter - /// - public long GetAndIncrement() - { - var rValue = Current; - var nextValue = Next; - return rValue; - } - } -} - diff --git a/src/core/Akka/Util/AtomicReference.cs b/src/core/Akka/Util/AtomicReference.cs index 3c4cd76e346..f0ea196e30b 100644 --- a/src/core/Akka/Util/AtomicReference.cs +++ b/src/core/Akka/Util/AtomicReference.cs @@ -19,7 +19,7 @@ namespace Akka.Util public class AtomicReference { /// - /// Sets the initial value of this to . + /// Sets the initial value of this to . /// public AtomicReference(T originalValue) { @@ -56,10 +56,10 @@ public T Value } /// - /// If equals , then set the Value to - /// . + /// If equals , then set the Value to + /// . /// - /// true if was set + /// true if was set public bool CompareAndSet(T expected, T newValue) { //special handling for null values @@ -84,7 +84,7 @@ public bool CompareAndSet(T expected, T newValue) #region Conversion operators /// - /// Implicit conversion operator = automatically casts the to an instance of + /// Implicit conversion operator = automatically casts the to an instance of . /// public static implicit operator T(AtomicReference aRef) { diff --git a/src/core/Akka/Util/ByteIterator.cs b/src/core/Akka/Util/ByteIterator.cs index 85eed113ab3..76bf806ce98 100644 --- a/src/core/Akka/Util/ByteIterator.cs +++ b/src/core/Akka/Util/ByteIterator.cs @@ -149,8 +149,7 @@ public override int CopyToBuffer(ByteBuffer buffer) internal class MultiByteIterator : ByteIterator { private ILinearSeq _iterators; - private ByteArrayIterator _current; - private static readonly ByteArrayIterator[] ClearedList = new ByteArrayIterator[0]; + private static readonly ILinearSeq ClearedList = new ArrayLinearSeq(new ByteArrayIterator[0]); public MultiByteIterator(params ByteArrayIterator[] iterators) { @@ -166,18 +165,26 @@ public MultiByteIterator(ILinearSeq iterators) private MultiByteIterator Normalize() { - Func, IEnumerable> norm = null; + Func, ILinearSeq> norm = null; norm = xs => { - if (!xs.Any()) return ClearedList; - if (!xs.First().HasNext) return norm(xs.Skip(1)); + if (xs.IsEmpty) return ClearedList; + if (!xs.Head.HasNext) return norm(xs.Tail()); return xs; }; - _iterators = new ArrayLinearSeq(norm(_iterators).ToArray()); + _iterators = norm(_iterators); return this; } + + private ByteArrayIterator Current + { + get + { + return _iterators.Head; + } + } private void DropCurrent() { _iterators = _iterators.Tail(); @@ -190,17 +197,17 @@ protected override void Clear() public override bool HasNext { - get { return _current.HasNext; } + get { return Current.HasNext; } } public override byte Head { - get { return _current.Head; } + get { return Current.Head; } } public override byte Next() { - var result = _current.Next(); + var result = Current.Next(); Normalize(); return result; } @@ -217,11 +224,11 @@ public override ByteIterator Take(int n) var builder = new List(); while (rest > 0 && !_iterators.IsEmpty) { - _current.Take(rest); - if (_current.HasNext) + Current.Take(rest); + if (Current.HasNext) { - rest -= _current.Len; - builder.Add(_current); + rest -= Current.Len; + builder.Add(Current); } _iterators = _iterators.Tail(); } @@ -233,8 +240,8 @@ public override ByteIterator Drop(int n) { if (n > 0 && Len > 0) { - var nCurrent = Math.Min(n, _current.Len); - _current.Drop(n); + var nCurrent = Math.Min(n, Current.Len); + Current.Drop(n); var rest = n - nCurrent; Normalize(); return Drop(rest); @@ -248,10 +255,10 @@ public override ByteIterator TakeWhile(Func p) var builder = new List(); while (!stop && !_iterators.IsEmpty) { - var lastLen = _current.Len; - _current.TakeWhile(p); - if (_current.HasNext) builder.Add(_current); - if (_current.Len < lastLen) stop = true; + var lastLen = Current.Len; + Current.TakeWhile(p); + if (Current.HasNext) builder.Add(Current); + if (Current.Len < lastLen) stop = true; DropCurrent(); } _iterators = new ArrayLinearSeq(builder.ToArray()); @@ -262,8 +269,8 @@ public override ByteIterator DropWhile(Func p) { if (Len > 0) { - _current.DropWhile(p); - var dropMore = _current.Len == 0; + Current.DropWhile(p); + var dropMore = Current.Len == 0; Normalize(); if (dropMore) return DropWhile(p); } @@ -280,12 +287,12 @@ public override ByteString ToByteString() protected MultiByteIterator GetToArray(T[] xs, int offset, int n, int elemSize, Func getSingle, Action getMulti) { - if(n >= 0) return this; + if(n <= 0) return this; Func nDoneF = () => { - if (_current.Len >= elemSize) + if (Current.Len >= elemSize) { - var nCurrent = Math.Min(n, _current.Len/elemSize); + var nCurrent = Math.Min(n, Current.Len/elemSize); getMulti(xs, offset, nCurrent); return nCurrent; } @@ -302,13 +309,13 @@ protected MultiByteIterator GetToArray(T[] xs, int offset, int n, int elemSiz public override ByteIterator GetBytes(byte[] xs, int offset, int n) { - return GetToArray(xs, offset, n, 1, GetByte, (a, b, c) => _current.GetBytes(a, b, c)); + return GetToArray(xs, offset, n, 1, GetByte, (a, b, c) => Current.GetBytes(a, b, c)); } public override byte[] ToArray() { - throw new NotImplementedException(); + return GetBytes(Len); } public override int CopyToBuffer(ByteBuffer buffer) @@ -484,7 +491,7 @@ private ArrayLinearSeq(T[] array, int offset, int length) public bool IsEmpty { - get { return _length > 0; } + get { return _length == 0; } } public T Head @@ -516,6 +523,7 @@ private class Enumerator : IEnumerator { private readonly ILinearSeq _orig; private ILinearSeq _seq; + private T _current; public Enumerator(ILinearSeq seq) { @@ -530,8 +538,11 @@ public void Dispose() public bool MoveNext() { + if (_seq.IsEmpty) + return false; + _current = _seq.Head; _seq = _seq.Tail(); - return !_seq.IsEmpty; + return true; } public void Reset() @@ -541,7 +552,7 @@ public void Reset() public T Current { - get { return _seq.Head; } + get { return _current; } } object IEnumerator.Current diff --git a/src/core/Akka/Util/ByteString.cs b/src/core/Akka/Util/ByteString.cs index e4985a6f7b7..72d724a361f 100644 --- a/src/core/Akka/Util/ByteString.cs +++ b/src/core/Akka/Util/ByteString.cs @@ -81,7 +81,7 @@ public static ByteString FromByteBuffer(ByteBuffer buffer) return Create(buffer); } - public static readonly ByteString Empty = new ByteString1C(new byte[0]); + public static readonly ByteString Empty = CompactByteString.EmptyCompactByteString; public static ByteStringBuilder NewBuilder() { @@ -225,6 +225,11 @@ public override string DecodeString(Encoding charset) { return charset.GetString(_length == _bytes.Length ? _bytes : ToArray()); } + + public override IEnumerator GetEnumerator() + { + return _bytes.Skip(_startIndex).Take(_length).GetEnumerator(); + } } internal class ByteStrings : ByteString @@ -268,7 +273,12 @@ public override ByteIterator Iterator() _byteStrings.Select(x => (ByteIterator.ByteArrayIterator) x.Iterator()).ToArray()); } - public override ByteString Concat(ByteString that) + public override IEnumerator GetEnumerator() + { + return _byteStrings.SelectMany(byteString => byteString).GetEnumerator(); + } + + public override ByteString Concat(ByteString that) { if (that.IsEmpty) { @@ -499,9 +509,13 @@ public static ByteString Create(byte[] buffer) partial /*object*/ class CompactByteString { + internal static readonly CompactByteString EmptyCompactByteString = new ByteString1C(new byte[0]); + public static CompactByteString FromString(string str, Encoding encoding) { - return new ByteString1C(encoding.GetBytes(str)); + return string.IsNullOrEmpty(str) + ? EmptyCompactByteString + : new ByteString1C(encoding.GetBytes(str)); } public static ByteString FromArray(byte[] array, int offset, int length) diff --git a/src/core/Akka/Util/ContinuousEnumerator.cs b/src/core/Akka/Util/ContinuousEnumerator.cs index 108bf383c2a..1a2d89e2476 100644 --- a/src/core/Akka/Util/ContinuousEnumerator.cs +++ b/src/core/Akka/Util/ContinuousEnumerator.cs @@ -63,9 +63,9 @@ object IEnumerator.Current internal static class ContinuousEnumeratorExtensions { /// - /// Provides a instance for . + /// Provides a instance for . /// - /// Internally, it just wraps 's internal iterator with circular iteration behavior. + /// Internally, it just wraps 's internal iterator with circular iteration behavior. /// public static ContinuousEnumerator GetContinuousEnumerator(this IEnumerable collection) { diff --git a/src/core/Akka/Util/Internal/ArrayExtensions.cs b/src/core/Akka/Util/Internal/ArrayExtensions.cs index 008fa5b61bf..861ec72321f 100644 --- a/src/core/Akka/Util/Internal/ArrayExtensions.cs +++ b/src/core/Akka/Util/Internal/ArrayExtensions.cs @@ -80,17 +80,19 @@ public static void Shuffle(this T[] array) /// The array of items to slice /// The starting position to begin the slice /// The number of items to take - /// A slice of size beginning from position in . + /// A slice of size beginning from position in . internal static IEnumerable Slice(this IEnumerable items, int startIndex, int count) { return items.Skip(startIndex).Take(count); } /// - /// Select all the items in this array beginning with and up until the end of the array. + /// Select all the items in this array beginning with and up until the end of the array. /// - /// If is not found in the array, From will return an empty set. - /// If is found at the end of the array, From will return the entire original array. + /// + /// If is not found in the array, From will return an empty set. + /// If is found at the end of the array, From will return the entire original array. + /// /// internal static IEnumerable From(this IEnumerable items, T startingItem) { @@ -103,10 +105,11 @@ internal static IEnumerable From(this IEnumerable items, T startingItem } /// - /// Select all the items in this array from the beginning until (but not including) - /// - /// If is not found in the array, Until will select all items. - /// If is the first item in the array, an empty array will be returned. + /// Select all the items in this array from the beginning until (but not including) + /// + /// If is not found in the array, Until will select all items. + /// If is the first item in the array, an empty array will be returned. + /// /// internal static IEnumerable Until(this IEnumerable items, T startingItem) { diff --git a/src/core/Akka/Util/Internal/AtomicState.cs b/src/core/Akka/Util/Internal/AtomicState.cs new file mode 100644 index 00000000000..521ada54897 --- /dev/null +++ b/src/core/Akka/Util/Internal/AtomicState.cs @@ -0,0 +1,198 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2015 Typesafe Inc. +// Copyright (C) 2013-2015 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; + +namespace Akka.Util.Internal +{ + /// + /// Internal state abstraction + /// + internal abstract class AtomicState : AtomicCounterLong, IAtomicState + { + private readonly ConcurrentQueue _listeners; + private readonly TimeSpan _callTimeout; + + protected AtomicState( TimeSpan callTimeout, long startingCount ) + : base( startingCount ) + { + _listeners = new ConcurrentQueue( ); + _callTimeout = callTimeout; + } + + /// + /// Add a listener function which is invoked on state entry + /// + /// listener implementation + public void AddListener( Action listener ) + { + _listeners.Enqueue( listener ); + } + + /// + /// Test for whether listeners exist + /// + public bool HasListeners + { + get { return !_listeners.IsEmpty; } + } + + /// + /// Notifies the listeners of the transition event via a + /// + protected async Task NotifyTransitionListeners( ) + { + if ( !HasListeners ) return; + await Task + .Factory + .StartNew + ( + ( ) => + { + foreach ( var listener in _listeners ) + { + listener.Invoke( ); + } + } + ); + } + + /// + /// Shared implementation of call across all states. Thrown exception or execution of the call beyond the allowed + /// call timeout is counted as a failed call, otherwise a successful call + /// + /// NOTE: In .Net there is no way to cancel an uncancellable task. We are merely cancelling the wait and marking this + /// as a failure. + /// + /// see http://blogs.msdn.com/b/pfxteam/archive/2011/11/10/10235834.aspx + /// + /// + /// Implementation of the call + /// result of the call + public async Task CallThrough( Func> task ) + { + var deadline = DateTime.UtcNow.Add( _callTimeout ); + ExceptionDispatchInfo capturedException = null; + T result = default(T); + try + { + result = await task(); + } + catch ( Exception ex ) + { + capturedException = ExceptionDispatchInfo.Capture( ex ); + } + + bool throwException = capturedException != null; + if ( throwException || DateTime.UtcNow.CompareTo( deadline ) >= 0 ) + { + CallFails( ); + if ( throwException ) + capturedException.Throw( ); + } + else + { + CallSucceeds( ); + } + return result; + } + + /// + /// Shared implementation of call across all states. Thrown exception or execution of the call beyond the allowed + /// call timeout is counted as a failed call, otherwise a successful call + /// + /// NOTE: In .Net there is no way to cancel an uncancellable task. We are merely cancelling the wait and marking this + /// as a failure. + /// + /// see http://blogs.msdn.com/b/pfxteam/archive/2011/11/10/10235834.aspx + /// + /// Implementation of the call + /// + public async Task CallThrough( Func task ) + { + var deadline = DateTime.UtcNow.Add( _callTimeout ); + ExceptionDispatchInfo capturedException = null; + + try + { + await task(); + } + catch ( Exception ex ) + { + capturedException = ExceptionDispatchInfo.Capture( ex ); + } + + bool throwException = capturedException != null; + if (throwException || DateTime.UtcNow.CompareTo(deadline) >= 0) + { + CallFails(); + if (throwException) capturedException.Throw(); + } + else + { + CallSucceeds(); + } + + + } + + /// + /// Abstract entry point for all states + /// + /// + /// Implementation of the call that needs protected + /// containing result of protected call + public abstract Task Invoke( Func> body ); + + /// + /// Abstract entry point for all states + /// + /// Implementation of the call that needs protected + /// containing result of protected call + public abstract Task Invoke( Func body ); + + /// + /// Invoked when call fails + /// + protected abstract void CallFails( ); + + /// + /// Invoked when call succeeds + /// + protected abstract void CallSucceeds( ); + + /// + /// Invoked on the transitioned-to state during transition. Notifies listeners after invoking subclass template method _enter + /// + protected abstract void EnterInternal( ); + + /// + /// Enter the state. NotifyTransitionListeners is not awaited -- its "fire and forget". + /// It is up to the user to handle any errors that occur in this state. + /// + public void Enter( ) + { + EnterInternal( ); + NotifyTransitionListeners( ); + } + + } + + /// + /// This interface represents the parts of the internal circuit breaker state; the behavior stack, watched by, watching and termination queue + /// + public interface IAtomicState + { + void AddListener( Action listener ); + bool HasListeners { get; } + Task Invoke( Func> body ); + void Enter( ); + } +} diff --git a/src/core/Akka/Util/MatchHandler/IPartialActionBuilder.cs b/src/core/Akka/Util/MatchHandler/IPartialActionBuilder.cs index 7c6d0ddd5ef..f2086bb5bff 100644 --- a/src/core/Akka/Util/MatchHandler/IPartialActionBuilder.cs +++ b/src/core/Akka/Util/MatchHandler/IPartialActionBuilder.cs @@ -12,7 +12,7 @@ namespace Akka.Tools.MatchHandler public interface IPartialActionBuilder { /// - /// Builds the specified delegate and arguments to a PartialAction<> + /// Builds the specified delegate and arguments to a /// If the number of arguments are 0, the delegate should be a Func<,bool> /// If the number of arguments are 1, the delegate should be a Func<,T1,bool> /// ... diff --git a/src/core/Akka/Util/MatchHandler/MatchBuilder.cs b/src/core/Akka/Util/MatchHandler/MatchBuilder.cs index 34e114d75b6..de567683577 100644 --- a/src/core/Akka/Util/MatchHandler/MatchBuilder.cs +++ b/src/core/Akka/Util/MatchHandler/MatchBuilder.cs @@ -125,9 +125,9 @@ public void MatchAny(Action handler) /// - /// Builds all added handlers and returns a PartialAction<object>. + /// Builds all added handlers and returns a . /// - /// Returns a PartialAction<object> + /// Returns a public PartialAction Build() { var partialAction = _compiler.Compile(_typeHandlers, _arguments, new MatchBuilderSignature(_signature)); diff --git a/src/core/Akka/Util/MatchHandler/PartialAction.cs b/src/core/Akka/Util/MatchHandler/PartialAction.cs index f69d1f406d5..9d1a4554845 100644 --- a/src/core/Akka/Util/MatchHandler/PartialAction.cs +++ b/src/core/Akka/Util/MatchHandler/PartialAction.cs @@ -8,11 +8,11 @@ namespace Akka.Tools.MatchHandler { /// - /// An action that returns true if the was handled. + /// An action that returns true if the was handled. /// /// The type of the argument /// The argument. - /// Returns true if the was handled + /// Returns true if the was handled public delegate bool PartialAction(T item); } diff --git a/src/core/Akka/Util/MurmurHash.cs b/src/core/Akka/Util/MurmurHash.cs index c1deb34f86f..f6b815004b7 100644 --- a/src/core/Akka/Util/MurmurHash.cs +++ b/src/core/Akka/Util/MurmurHash.cs @@ -125,7 +125,7 @@ public static uint FinalizeHash(uint hash) #region Internal 32-bit hashing helpers /// - /// Rotate a 32-bit unsigned integer to the left by bits + /// Rotate a 32-bit unsigned integer to the left by bits /// /// Original value /// The shift value @@ -136,7 +136,7 @@ private static uint RotateLeft32(uint original, int shift) } /// - /// Rotate a 64-bit unsigned integer to the left by bits + /// Rotate a 64-bit unsigned integer to the left by bits /// /// Original value /// The shift value diff --git a/src/examples/Chat/ChatMessages/ChatMessages.csproj b/src/examples/Chat/ChatMessages/ChatMessages.csproj index d4d41984048..bb36171c085 100644 --- a/src/examples/Chat/ChatMessages/ChatMessages.csproj +++ b/src/examples/Chat/ChatMessages/ChatMessages.csproj @@ -14,7 +14,8 @@ 512 ..\..\..\ true - ac874559 + + true @@ -55,41 +56,45 @@ false + + ..\..\..\packages\fastJSON.2.0.27.1\lib\net40\fastjson.dll + True + - - ..\..\..\packages\fastJSON.2.0.27.1\lib\net40\fastjson.dll - - + ..\..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True - + ..\..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True - + ..\..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True - - - {5DEDDF90-37F0-48D3-A0B0-A5CBD8A7E377} Akka + + + - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. diff --git a/src/examples/Chat/ChatMessages/packages.config b/src/examples/Chat/ChatMessages/packages.config index 9ef0ee3dcd8..fda430cff2d 100644 --- a/src/examples/Chat/ChatMessages/packages.config +++ b/src/examples/Chat/ChatMessages/packages.config @@ -1,7 +1,6 @@  - diff --git a/src/examples/Cluster/Roles/Samples.Cluster.Transformation/Samples.Cluster.Transformation.csproj b/src/examples/Cluster/Roles/Samples.Cluster.Transformation/Samples.Cluster.Transformation.csproj index 49d217bedbe..286cd23f4a4 100644 --- a/src/examples/Cluster/Roles/Samples.Cluster.Transformation/Samples.Cluster.Transformation.csproj +++ b/src/examples/Cluster/Roles/Samples.Cluster.Transformation/Samples.Cluster.Transformation.csproj @@ -35,8 +35,9 @@ - - ..\..\..\..\packages\Microsoft.Bcl.Immutable.1.0.34\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + + ..\..\..\..\packages\System.Collections.Immutable.1.1.37\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True diff --git a/src/examples/Cluster/Roles/Samples.Cluster.Transformation/packages.config b/src/examples/Cluster/Roles/Samples.Cluster.Transformation/packages.config index 0b3cc3386bd..8a2bcdbc2dd 100644 --- a/src/examples/Cluster/Roles/Samples.Cluster.Transformation/packages.config +++ b/src/examples/Cluster/Roles/Samples.Cluster.Transformation/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/examples/Cluster/Routing/Samples.Cluster.ConsistentHashRouting/Samples.Cluster.ConsistentHashRouting.csproj b/src/examples/Cluster/Routing/Samples.Cluster.ConsistentHashRouting/Samples.Cluster.ConsistentHashRouting.csproj index a1067e6b236..4314cc9a1fa 100644 --- a/src/examples/Cluster/Routing/Samples.Cluster.ConsistentHashRouting/Samples.Cluster.ConsistentHashRouting.csproj +++ b/src/examples/Cluster/Routing/Samples.Cluster.ConsistentHashRouting/Samples.Cluster.ConsistentHashRouting.csproj @@ -35,8 +35,9 @@ - - ..\..\..\..\packages\Microsoft.Bcl.Immutable.1.0.34\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + + ..\..\..\..\packages\System.Collections.Immutable.1.1.37\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True diff --git a/src/examples/Cluster/Routing/Samples.Cluster.ConsistentHashRouting/packages.config b/src/examples/Cluster/Routing/Samples.Cluster.ConsistentHashRouting/packages.config index 0b3cc3386bd..8a2bcdbc2dd 100644 --- a/src/examples/Cluster/Routing/Samples.Cluster.ConsistentHashRouting/packages.config +++ b/src/examples/Cluster/Routing/Samples.Cluster.ConsistentHashRouting/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/examples/Cluster/Samples.Cluster.Simple/Samples.Cluster.Simple.csproj b/src/examples/Cluster/Samples.Cluster.Simple/Samples.Cluster.Simple.csproj index 8582dee911a..1b142a16c8e 100644 --- a/src/examples/Cluster/Samples.Cluster.Simple/Samples.Cluster.Simple.csproj +++ b/src/examples/Cluster/Samples.Cluster.Simple/Samples.Cluster.Simple.csproj @@ -35,8 +35,9 @@ - - ..\..\..\packages\Microsoft.Bcl.Immutable.1.0.34\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + + ..\..\..\packages\System.Collections.Immutable.1.1.37\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True diff --git a/src/examples/Cluster/Samples.Cluster.Simple/packages.config b/src/examples/Cluster/Samples.Cluster.Simple/packages.config index 0b3cc3386bd..8a2bcdbc2dd 100644 --- a/src/examples/Cluster/Samples.Cluster.Simple/packages.config +++ b/src/examples/Cluster/Samples.Cluster.Simple/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/examples/FSharp.Api/Supervisioning.fs b/src/examples/FSharp.Api/Supervisioning.fs index 3a16f2bc7ad..fe11d672753 100644 --- a/src/examples/FSharp.Api/Supervisioning.fs +++ b/src/examples/FSharp.Api/Supervisioning.fs @@ -59,7 +59,7 @@ let main() = Strategy.OneForOne(fun e -> match e with | :? CustomException -> Directive.Restart - | _ -> SupervisorStrategy.DefaultDecider(e))) ] + | _ -> SupervisorStrategy.DefaultDecider.Decide(e))) ] async { let! response = parent false - + + ..\..\..\packages\fastJSON.2.0.27.1\lib\net40\fastjson.dll + True + + ..\..\..\packages\qdfeed.1.1.0\lib\net45\QDFeedParser.dll + True - - ..\..\..\packages\Microsoft.Bcl.Immutable.1.0.30\lib\portable-net45+win8+wp8\System.Collections.Immutable.dll + + ..\..\..\packages\System.Collections.Immutable.1.1.37\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True @@ -70,9 +76,6 @@ - - ..\..\..\packages\fastJSON.2.0.27.1\lib\net40\fastjson.dll - diff --git a/src/examples/Stocks/SymbolLookup/packages.config b/src/examples/Stocks/SymbolLookup/packages.config index 6c19783cbfb..6aa2a478aed 100644 --- a/src/examples/Stocks/SymbolLookup/packages.config +++ b/src/examples/Stocks/SymbolLookup/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/examples/TcpEchoService.Server/Actors.cs b/src/examples/TcpEchoService.Server/Actors.cs index 1072c52f2f2..5a9d128ef46 100644 --- a/src/examples/TcpEchoService.Server/Actors.cs +++ b/src/examples/TcpEchoService.Server/Actors.cs @@ -15,11 +15,11 @@ namespace TcpEchoService.Server { public class EchoService : ReceiveActor { - private readonly TcpExt _extension = Tcp.Instance.Apply(Context.System); + private readonly IActorRef _manager = Context.System.Tcp(); public EchoService(EndPoint endpoint) { - _extension.Manager.Tell(new Tcp.Bind(Self, endpoint)); + _manager.Tell(new Tcp.Bind(Self, endpoint)); // To behave as TCP listener, actor should be able to handle Tcp.Connected messages Receive(connected => diff --git a/src/settings.StyleCop b/src/settings.StyleCop new file mode 100644 index 00000000000..b8dcb050376 --- /dev/null +++ b/src/settings.StyleCop @@ -0,0 +1,5 @@ + + + False + + \ No newline at end of file