diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 91ed2154776..52c72994744 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,8 @@ +#### 0.6.3 Aug 13 2014 +* Made it so HOCON config sections chain properly +* Optimized actor memory footprint +* Fixed a Helios bug that caused Akka.NET to drop messages larger than 32kb + #### 0.6.2 Aug 05 2014 * Upgraded Helios dependency * Bug fixes diff --git a/VERSION b/VERSION deleted file mode 100644 index eebd406b001..00000000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.6.1.0 \ No newline at end of file diff --git a/src/SharedAssemblyInfo.cs b/src/SharedAssemblyInfo.cs index bbf600ee7cc..5a97b67c9d3 100644 --- a/src/SharedAssemblyInfo.cs +++ b/src/SharedAssemblyInfo.cs @@ -4,7 +4,7 @@ [assembly: AssemblyCompanyAttribute("Akka")] [assembly: AssemblyCopyrightAttribute("Copyright © Roger Alsing 2013-2014")] [assembly: AssemblyTrademarkAttribute("")] -[assembly: AssemblyVersionAttribute("0.6.2.0")] -[assembly: AssemblyFileVersionAttribute("0.6.2.0")] +[assembly: AssemblyVersionAttribute("0.6.3.0")] +[assembly: AssemblyFileVersionAttribute("0.6.3.0")] namespace System { } diff --git a/src/core/Akka.FSharp/Properties/AssemblyInfo.fs b/src/core/Akka.FSharp/Properties/AssemblyInfo.fs index 60593911467..b1ebc4aa014 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 = "0.6.2.0" + let [] Version = "0.6.3.0" diff --git a/src/core/Akka.Remote/Akka.Remote.csproj b/src/core/Akka.Remote/Akka.Remote.csproj index e7a56c17c7b..0bc49197e18 100644 --- a/src/core/Akka.Remote/Akka.Remote.csproj +++ b/src/core/Akka.Remote/Akka.Remote.csproj @@ -60,7 +60,7 @@ False - ..\..\packages\Helios.1.3.0.0\lib\net45\Helios.dll + ..\..\packages\Helios.1.3.4.0\lib\net45\Helios.dll diff --git a/src/core/Akka.Remote/packages.config b/src/core/Akka.Remote/packages.config index b1c9faed700..5b1d66581a2 100644 --- a/src/core/Akka.Remote/packages.config +++ b/src/core/Akka.Remote/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/src/core/Akka.TestKit/Akka.TestKit.csproj b/src/core/Akka.TestKit/Akka.TestKit.csproj index 20608c520a4..ab77ab15baf 100644 --- a/src/core/Akka.TestKit/Akka.TestKit.csproj +++ b/src/core/Akka.TestKit/Akka.TestKit.csproj @@ -82,6 +82,7 @@ + diff --git a/src/core/Akka.TestKit/AkkaSpec.cs b/src/core/Akka.TestKit/AkkaSpec.cs index 1de0ae6afbd..b3bd22e98f9 100644 --- a/src/core/Akka.TestKit/AkkaSpec.cs +++ b/src/core/Akka.TestKit/AkkaSpec.cs @@ -12,6 +12,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Xunit.Sdk; namespace Akka.Tests { @@ -124,6 +125,7 @@ public class AkkaSpec : TestKitBase, IDisposable public AkkaSpec() { + var config = ConfigurationFactory.ParseString(GetConfig()); queue = new BlockingCollection(); messages = new List(); @@ -134,7 +136,7 @@ public AkkaSpec() protected virtual string GetConfig() { - return ""; + return ""+""; } public virtual void Dispose() @@ -239,6 +241,12 @@ protected TMessage expectMsgType(TimeSpan timeout) return default(TMessage); } + protected T AwaitResult(Task ask, TimeSpan timeout) + { + ask.Wait(timeout); + return (T)ask.Result; + } + /// /// Uses an epsilon value to compare between floating point numbers. /// Uses a default epsilon value of 0.001d @@ -502,6 +510,30 @@ protected void intercept(Action intercept) where T : Exception Xunit.Assert.True(false, "Expected exception of type " + typeof(T).Name); } + protected void FilterEvents(ActorSystem system, EventFilter[] eventFilters, Action action) + { + sys.EventStream.Publish(new Mute(eventFilters)); + try + { + action(); + + var leeway = TestKitSettings.TestEventFilterLeeway; + var failed = eventFilters + .Where(x => !x.AwaitDone(leeway)) + .Select(x => string.Format("Timeout {0} waiting for {1}", leeway, x)) + .ToArray(); + + if (failed.Any()) + { + throw new AssertException("Filter completion error: " + string.Join("\n", failed)); + } + } + finally + { + sys.EventStream.Publish(new Unmute(eventFilters)); + } + } + protected void EventFilter(string message, int occurances, Action intercept) where T : Exception { sys.EventStream.Subscribe(testActor, typeof(Error)); diff --git a/src/core/Akka.TestKit/HoconTests.cs b/src/core/Akka.TestKit/HoconTests.cs index 182ffff566a..3fc8613e0f0 100644 --- a/src/core/Akka.TestKit/HoconTests.cs +++ b/src/core/Akka.TestKit/HoconTests.cs @@ -1,430 +1,430 @@ -using Xunit; -using Akka.Configuration; -using Akka.Configuration.Hocon; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Akka.Tests -{ +//using Xunit; +//using Akka.Configuration; +//using Akka.Configuration.Hocon; +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using System.Threading.Tasks; + +//namespace Akka.Tests +//{ - public class HoconTests - { - //Added tests to conform to the HOCON spec https://github.com/typesafehub/config/blob/master/HOCON.md - [Fact] - public void CanUsePathsAsKeys_3_14() - { - var hocon1 = @"3.14 : 42"; - var hocon2 = @"3 { 14 : 42}"; - Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("3.14"), ConfigurationFactory.ParseString(hocon2).GetString("3.14")); - } - - [Fact] - public void CanUsePathsAsKeys_3() - { - var hocon1 = @"3 : 42"; - var hocon2 = @"""3"" : 42"; - Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("3"), ConfigurationFactory.ParseString(hocon2).GetString("3")); - } - - [Fact] - public void CanUsePathsAsKeys_true() - { - var hocon1 = @"true : 42"; - var hocon2 = @"""true"" : 42"; - Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("true"), ConfigurationFactory.ParseString(hocon2).GetString("true")); - } - - [Fact] - public void CanUsePathsAsKeys_FooBar() - { - var hocon1 = @"foo.bar : 42"; - var hocon2 = @"foo { bar : 42 }"; - Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("foo.bar"), ConfigurationFactory.ParseString(hocon2).GetString("foo.bar")); - } - - [Fact] - public void CanUsePathsAsKeys_FooBarBaz() - { - var hocon1 = @"foo.bar.baz : 42"; - var hocon2 = @"foo { bar { baz : 42 } }"; - Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("foo.bar.baz"), ConfigurationFactory.ParseString(hocon2).GetString("foo.bar.baz")); - } - - [Fact] - public void CanUsePathsAsKeys_AX_AY() - { - var hocon1 = @"a.x : 42, a.y : 43"; - var hocon2 = @"a { x : 42, y : 43 }"; - Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("a.x"), ConfigurationFactory.ParseString(hocon2).GetString("a.x")); - Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("a.y"), ConfigurationFactory.ParseString(hocon2).GetString("a.y")); - } - - [Fact] - public void CanUsePathsAsKeys_A_B_C() - { - var hocon1 = @"a b c : 42"; - var hocon2 = @"""a b c"" : 42"; - Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("a b c"), ConfigurationFactory.ParseString(hocon2).GetString("a b c")); - } - - - [Fact] - public void CanConcatinateSubstitutedUnquotedString() - { - var hocon = @"a { - name = Roger - c = Hello my name is ${a.name} -}"; - Assert.Equal("Hello my name is Roger",ConfigurationFactory.ParseString(hocon).GetString("a.c")); - } - - [Fact] - public void CanConcatinateSubstitutedArray() - { - var hocon = @"a { - b = [1,2,3] - c = ${a.b} [4,5,6] -}"; - Assert.True(new[] { 1, 2, 3, 4, 5, 6 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("a.c"))); - } - - [Fact] - public void CanParseSubConfig() - { - var hocon = @" -a { - b { - c = 1 - d = true - } -}"; - var config = ConfigurationFactory.ParseString(hocon); - var subConfig = config.GetConfig("a"); - Assert.Equal(1, subConfig.GetInt("b.c")); - Assert.Equal(true, subConfig.GetBoolean("b.d")); - } - - - [Fact] - public void CanParseHocon() - { - var hocon = @" -root { - int = 1 - quoted-string = ""foo"" - unquoted-string = bar - concat-string = foo bar - object { - hasContent = true - } - array = [1,2,3,4] - array-concat = [[1,2] [3,4]] - array-single-element = [1 2 3 4] - array-newline-element = [ - 1 - 2 - 3 - 4 - ] - null = null - double = 1.23 - bool = true -} -"; - var config = ConfigurationFactory.ParseString(hocon); - Assert.Equal("1", config.GetString("root.int")); - Assert.Equal("1.23", config.GetString("root.double")); - Assert.Equal(true, config.GetBoolean("root.bool")); - Assert.Equal(true, config.GetBoolean("root.object.hasContent")); - Assert.Equal(null, config.GetString("root.null")); - Assert.Equal("foo", config.GetString("root.quoted-string")); - Assert.Equal("bar", config.GetString("root.unquoted-string")); - Assert.Equal("foo bar", config.GetString("root.concat-string")); - Assert.True(new[] { 1, 2, 3,4 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("root.array"))); - Assert.True(new[] { 1, 2, 3, 4 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("root.array-newline-element"))); - Assert.True(new[] { "1 2 3 4" }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetStringList("root.array-single-element"))); - } - - [Fact] - public void CanParseJson() - { - var hocon = @" -""root"" : { - ""int"" : 1, - ""string"" : ""foo"", - ""object"" : { - ""hasContent"" : true - }, - ""array"" : [1,2,3], - ""null"" : null, - ""double"" : 1.23, - ""bool"" : true -} -"; - var config = ConfigurationFactory.ParseString(hocon); - Assert.Equal("1", config.GetString("root.int")); - Assert.Equal("1.23", config.GetString("root.double")); - Assert.Equal(true, config.GetBoolean("root.bool")); - Assert.Equal(true, config.GetBoolean("root.object.hasContent")); - Assert.Equal(null, config.GetString("root.null")); - Assert.Equal("foo", config.GetString("root.string")); - Assert.True(new[] { 1, 2, 3 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("root.array"))); - - } - - [Fact] - public void CanMergeObject() - { - var hocon = @" -a.b.c = { - x = 1 - y = 2 - } -a.b.c = { - z = 3 - } -"; - var config = ConfigurationFactory.ParseString(hocon); - Assert.Equal("1", config.GetString("a.b.c.x")); - Assert.Equal("2", config.GetString("a.b.c.y")); - Assert.Equal("3", config.GetString("a.b.c.z")); - } - - [Fact] - public void CanOverrideObject() - { - var hocon = @" -a.b = 1 -a = null -a.c = 3 -"; - var config = ConfigurationFactory.ParseString(hocon); - Assert.Equal(null, config.GetString("a.b")); - Assert.Equal("3", config.GetString("a.c")); - } - - [Fact] - public void CanParseObject() - { - var hocon = @" -a { - b = 1 -} -"; - Assert.Equal("1", ConfigurationFactory.ParseString(hocon).GetString("a.b")); - } - - [Fact] - public void CanTrimValue() - { - var hocon = "a= \t \t 1 \t \t,"; - Assert.Equal("1", ConfigurationFactory.ParseString(hocon).GetString("a")); - } - - [Fact] - public void CanTrimConcatenatedValue() - { - var hocon = "a= \t \t 1 2 3 \t \t,"; - Assert.Equal("1 2 3", ConfigurationFactory.ParseString(hocon).GetString("a")); - } - - [Fact] - public void CanConsumeCommaAfterValue() - { - var hocon = "a=1,"; - Assert.Equal("1", ConfigurationFactory.ParseString(hocon).GetString("a")); - } - - [Fact] - public void CanAssignIpAddressToField() - { - var hocon = @"a=127.0.0.1"; - Assert.Equal("127.0.0.1", ConfigurationFactory.ParseString(hocon).GetString("a")); - } - - [Fact] - public void CanAssignConcatenatedValueToField() - { - var hocon = @"a=1 2 3"; - Assert.Equal("1 2 3", ConfigurationFactory.ParseString(hocon).GetString("a")); - } - - [Fact] - public void CanAssignValueToQuotedField() - { - var hocon = @"""a""=1"; - Assert.Equal(1L, ConfigurationFactory.ParseString(hocon).GetLong("a")); - } - - [Fact] - public void CanAssignValueToPathExpression() - { - var hocon = @"a.b.c=1"; - Assert.Equal(1L, ConfigurationFactory.ParseString(hocon).GetLong("a.b.c")); - } - - [Fact] - public void CanAssignValuesToPathExpressions() - { - var hocon = @" -a.b.c=1 -a.b.d=2 -a.b.e.f=3 -"; - var config = ConfigurationFactory.ParseString(hocon); - Assert.Equal(1L, config.GetLong("a.b.c")); - Assert.Equal(2L, config.GetLong("a.b.d")); - Assert.Equal(3L, config.GetLong("a.b.e.f")); - } - - [Fact] - public void CanAssignLongToField() - { - var hocon = @"a=1"; - Assert.Equal(1L, ConfigurationFactory.ParseString(hocon).GetLong("a")); - } - - [Fact] - public void CanAssignArrayToField() - { - var hocon = @"a= -[ - 1 - 2 - 3 -]"; - Assert.True(new[] { 1, 2, 3 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("a"))); - - //hocon = @"a= [ 1, 2, 3 ]"; - //Assert.True(new[] { 1, 2, 3 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("a"))); - } - - [Fact] - public void CanConcatenateArray() - { - var hocon = @"a=[1,2] [3,4]"; - Assert.True(new[] { 1, 2, 3, 4 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("a"))); - } - - [Fact] - public void CanAssignSubstitutionToField() - { - var hocon = @"a{ - b = 1 - c = ${a.b} - d = ${a.c}23 -}"; - Assert.Equal(1, ConfigurationFactory.ParseString(hocon).GetInt("a.c")); - Assert.Equal(123, ConfigurationFactory.ParseString(hocon).GetInt("a.d")); - } - - [Fact] - public void CanAssignDoubleToField() - { - var hocon = @"a=1.1"; - Assert.Equal(1.1, ConfigurationFactory.ParseString(hocon).GetDouble("a")); - } - - [Fact] - public void CanAssignNullToField() - { - var hocon = @"a=null"; - Assert.Null(ConfigurationFactory.ParseString(hocon).GetString("a")); - } - - [Fact] - public void CanAssignBooleanToField() - { - var hocon = @"a=true"; - Assert.Equal(true, ConfigurationFactory.ParseString(hocon).GetBoolean("a")); - hocon = @"a=false"; - Assert.Equal(false, ConfigurationFactory.ParseString(hocon).GetBoolean("a")); - - hocon = @"a=on"; - Assert.Equal(true, ConfigurationFactory.ParseString(hocon).GetBoolean("a")); - hocon = @"a=off"; - Assert.Equal(false, ConfigurationFactory.ParseString(hocon).GetBoolean("a")); - } - - [Fact] - public void CanAssignQuotedStringToField() - { - var hocon = @"a=""hello"""; - Assert.Equal("hello", ConfigurationFactory.ParseString(hocon).GetString("a")); - } - - [Fact] - public void CanAssignUnQuotedStringToField() - { - var hocon = @"a=hello"; - Assert.Equal("hello", ConfigurationFactory.ParseString(hocon).GetString("a")); - } - - [Fact] - public void CanAssignTrippleQuotedStringToField() - { - var hocon = @"a=""""""hello"""""""; - Assert.Equal("hello", ConfigurationFactory.ParseString(hocon).GetString("a")); - } - - [Fact] - public void CanUseFallback() - { - var hocon1 = @" -foo { - bar { - a=123 - } -}"; - var hocon2 = @" -foo { - bar { - a=1 - b=2 - c=3 - } -}"; - - var config1 = ConfigurationFactory.ParseString(hocon1); - var config2 = ConfigurationFactory.ParseString(hocon2); - - var config = config1.WithFallback(config2); - - Assert.Equal(123, config.GetInt("foo.bar.a")); - Assert.Equal(2, config.GetInt("foo.bar.b")); - Assert.Equal(3, config.GetInt("foo.bar.c")); - } - - [Fact] - public void CanUseFallbackInSubConfig() - { - var hocon1 = @" -foo { - bar { - a=123 - } -}"; - var hocon2 = @" -foo { - bar { - a=1 - b=2 - c=3 - } -}"; - - var config1 = ConfigurationFactory.ParseString(hocon1); - var config2 = ConfigurationFactory.ParseString(hocon2); - - var config = config1.WithFallback(config2).GetConfig("foo.bar"); - - Assert.Equal(123, config.GetInt("a")); - Assert.Equal(2, config.GetInt("b")); - Assert.Equal(3, config.GetInt("c")); - } - } -} +// public class HoconTests +// { +// //Added tests to conform to the HOCON spec https://github.com/typesafehub/config/blob/master/HOCON.md +// [Fact] +// public void CanUsePathsAsKeys_3_14() +// { +// var hocon1 = @"3.14 : 42"; +// var hocon2 = @"3 { 14 : 42}"; +// Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("3.14"), ConfigurationFactory.ParseString(hocon2).GetString("3.14")); +// } + +// [Fact] +// public void CanUsePathsAsKeys_3() +// { +// var hocon1 = @"3 : 42"; +// var hocon2 = @"""3"" : 42"; +// Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("3"), ConfigurationFactory.ParseString(hocon2).GetString("3")); +// } + +// [Fact] +// public void CanUsePathsAsKeys_true() +// { +// var hocon1 = @"true : 42"; +// var hocon2 = @"""true"" : 42"; +// Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("true"), ConfigurationFactory.ParseString(hocon2).GetString("true")); +// } + +// [Fact] +// public void CanUsePathsAsKeys_FooBar() +// { +// var hocon1 = @"foo.bar : 42"; +// var hocon2 = @"foo { bar : 42 }"; +// Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("foo.bar"), ConfigurationFactory.ParseString(hocon2).GetString("foo.bar")); +// } + +// [Fact] +// public void CanUsePathsAsKeys_FooBarBaz() +// { +// var hocon1 = @"foo.bar.baz : 42"; +// var hocon2 = @"foo { bar { baz : 42 } }"; +// Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("foo.bar.baz"), ConfigurationFactory.ParseString(hocon2).GetString("foo.bar.baz")); +// } + +// [Fact] +// public void CanUsePathsAsKeys_AX_AY() +// { +// var hocon1 = @"a.x : 42, a.y : 43"; +// var hocon2 = @"a { x : 42, y : 43 }"; +// Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("a.x"), ConfigurationFactory.ParseString(hocon2).GetString("a.x")); +// Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("a.y"), ConfigurationFactory.ParseString(hocon2).GetString("a.y")); +// } + +// [Fact] +// public void CanUsePathsAsKeys_A_B_C() +// { +// var hocon1 = @"a b c : 42"; +// var hocon2 = @"""a b c"" : 42"; +// Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("a b c"), ConfigurationFactory.ParseString(hocon2).GetString("a b c")); +// } + + +// [Fact] +// public void CanConcatinateSubstitutedUnquotedString() +// { +// var hocon = @"a { +// name = Roger +// c = Hello my name is ${a.name} +//}"; +// Assert.Equal("Hello my name is Roger",ConfigurationFactory.ParseString(hocon).GetString("a.c")); +// } + +// [Fact] +// public void CanConcatinateSubstitutedArray() +// { +// var hocon = @"a { +// b = [1,2,3] +// c = ${a.b} [4,5,6] +//}"; +// Assert.True(new[] { 1, 2, 3, 4, 5, 6 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("a.c"))); +// } + +// [Fact] +// public void CanParseSubConfig() +// { +// var hocon = @" +//a { +// b { +// c = 1 +// d = true +// } +//}"; +// var config = ConfigurationFactory.ParseString(hocon); +// var subConfig = config.GetConfig("a"); +// Assert.Equal(1, subConfig.GetInt("b.c")); +// Assert.Equal(true, subConfig.GetBoolean("b.d")); +// } + + +// [Fact] +// public void CanParseHocon() +// { +// var hocon = @" +//root { +// int = 1 +// quoted-string = ""foo"" +// unquoted-string = bar +// concat-string = foo bar +// object { +// hasContent = true +// } +// array = [1,2,3,4] +// array-concat = [[1,2] [3,4]] +// array-single-element = [1 2 3 4] +// array-newline-element = [ +// 1 +// 2 +// 3 +// 4 +// ] +// null = null +// double = 1.23 +// bool = true +//} +//"; +// var config = ConfigurationFactory.ParseString(hocon); +// Assert.Equal("1", config.GetString("root.int")); +// Assert.Equal("1.23", config.GetString("root.double")); +// Assert.Equal(true, config.GetBoolean("root.bool")); +// Assert.Equal(true, config.GetBoolean("root.object.hasContent")); +// Assert.Equal(null, config.GetString("root.null")); +// Assert.Equal("foo", config.GetString("root.quoted-string")); +// Assert.Equal("bar", config.GetString("root.unquoted-string")); +// Assert.Equal("foo bar", config.GetString("root.concat-string")); +// Assert.True(new[] { 1, 2, 3,4 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("root.array"))); +// Assert.True(new[] { 1, 2, 3, 4 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("root.array-newline-element"))); +// Assert.True(new[] { "1 2 3 4" }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetStringList("root.array-single-element"))); +// } + +// [Fact] +// public void CanParseJson() +// { +// var hocon = @" +//""root"" : { +// ""int"" : 1, +// ""string"" : ""foo"", +// ""object"" : { +// ""hasContent"" : true +// }, +// ""array"" : [1,2,3], +// ""null"" : null, +// ""double"" : 1.23, +// ""bool"" : true +//} +//"; +// var config = ConfigurationFactory.ParseString(hocon); +// Assert.Equal("1", config.GetString("root.int")); +// Assert.Equal("1.23", config.GetString("root.double")); +// Assert.Equal(true, config.GetBoolean("root.bool")); +// Assert.Equal(true, config.GetBoolean("root.object.hasContent")); +// Assert.Equal(null, config.GetString("root.null")); +// Assert.Equal("foo", config.GetString("root.string")); +// Assert.True(new[] { 1, 2, 3 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("root.array"))); + +// } + +// [Fact] +// public void CanMergeObject() +// { +// var hocon = @" +//a.b.c = { +// x = 1 +// y = 2 +// } +//a.b.c = { +// z = 3 +// } +//"; +// var config = ConfigurationFactory.ParseString(hocon); +// Assert.Equal("1", config.GetString("a.b.c.x")); +// Assert.Equal("2", config.GetString("a.b.c.y")); +// Assert.Equal("3", config.GetString("a.b.c.z")); +// } + +// [Fact] +// public void CanOverrideObject() +// { +// var hocon = @" +//a.b = 1 +//a = null +//a.c = 3 +//"; +// var config = ConfigurationFactory.ParseString(hocon); +// Assert.Equal(null, config.GetString("a.b")); +// Assert.Equal("3", config.GetString("a.c")); +// } + +// [Fact] +// public void CanParseObject() +// { +// var hocon = @" +//a { +// b = 1 +//} +//"; +// Assert.Equal("1", ConfigurationFactory.ParseString(hocon).GetString("a.b")); +// } + +// [Fact] +// public void CanTrimValue() +// { +// var hocon = "a= \t \t 1 \t \t,"; +// Assert.Equal("1", ConfigurationFactory.ParseString(hocon).GetString("a")); +// } + +// [Fact] +// public void CanTrimConcatenatedValue() +// { +// var hocon = "a= \t \t 1 2 3 \t \t,"; +// Assert.Equal("1 2 3", ConfigurationFactory.ParseString(hocon).GetString("a")); +// } + +// [Fact] +// public void CanConsumeCommaAfterValue() +// { +// var hocon = "a=1,"; +// Assert.Equal("1", ConfigurationFactory.ParseString(hocon).GetString("a")); +// } + +// [Fact] +// public void CanAssignIpAddressToField() +// { +// var hocon = @"a=127.0.0.1"; +// Assert.Equal("127.0.0.1", ConfigurationFactory.ParseString(hocon).GetString("a")); +// } + +// [Fact] +// public void CanAssignConcatenatedValueToField() +// { +// var hocon = @"a=1 2 3"; +// Assert.Equal("1 2 3", ConfigurationFactory.ParseString(hocon).GetString("a")); +// } + +// [Fact] +// public void CanAssignValueToQuotedField() +// { +// var hocon = @"""a""=1"; +// Assert.Equal(1L, ConfigurationFactory.ParseString(hocon).GetLong("a")); +// } + +// [Fact] +// public void CanAssignValueToPathExpression() +// { +// var hocon = @"a.b.c=1"; +// Assert.Equal(1L, ConfigurationFactory.ParseString(hocon).GetLong("a.b.c")); +// } + +// [Fact] +// public void CanAssignValuesToPathExpressions() +// { +// var hocon = @" +//a.b.c=1 +//a.b.d=2 +//a.b.e.f=3 +//"; +// var config = ConfigurationFactory.ParseString(hocon); +// Assert.Equal(1L, config.GetLong("a.b.c")); +// Assert.Equal(2L, config.GetLong("a.b.d")); +// Assert.Equal(3L, config.GetLong("a.b.e.f")); +// } + +// [Fact] +// public void CanAssignLongToField() +// { +// var hocon = @"a=1"; +// Assert.Equal(1L, ConfigurationFactory.ParseString(hocon).GetLong("a")); +// } + +// [Fact] +// public void CanAssignArrayToField() +// { +// var hocon = @"a= +//[ +// 1 +// 2 +// 3 +//]"; +// Assert.True(new[] { 1, 2, 3 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("a"))); + +// //hocon = @"a= [ 1, 2, 3 ]"; +// //Assert.True(new[] { 1, 2, 3 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("a"))); +// } + +// [Fact] +// public void CanConcatenateArray() +// { +// var hocon = @"a=[1,2] [3,4]"; +// Assert.True(new[] { 1, 2, 3, 4 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("a"))); +// } + +// [Fact] +// public void CanAssignSubstitutionToField() +// { +// var hocon = @"a{ +// b = 1 +// c = ${a.b} +// d = ${a.c}23 +//}"; +// Assert.Equal(1, ConfigurationFactory.ParseString(hocon).GetInt("a.c")); +// Assert.Equal(123, ConfigurationFactory.ParseString(hocon).GetInt("a.d")); +// } + +// [Fact] +// public void CanAssignDoubleToField() +// { +// var hocon = @"a=1.1"; +// Assert.Equal(1.1, ConfigurationFactory.ParseString(hocon).GetDouble("a")); +// } + +// [Fact] +// public void CanAssignNullToField() +// { +// var hocon = @"a=null"; +// Assert.Null(ConfigurationFactory.ParseString(hocon).GetString("a")); +// } + +// [Fact] +// public void CanAssignBooleanToField() +// { +// var hocon = @"a=true"; +// Assert.Equal(true, ConfigurationFactory.ParseString(hocon).GetBoolean("a")); +// hocon = @"a=false"; +// Assert.Equal(false, ConfigurationFactory.ParseString(hocon).GetBoolean("a")); + +// hocon = @"a=on"; +// Assert.Equal(true, ConfigurationFactory.ParseString(hocon).GetBoolean("a")); +// hocon = @"a=off"; +// Assert.Equal(false, ConfigurationFactory.ParseString(hocon).GetBoolean("a")); +// } + +// [Fact] +// public void CanAssignQuotedStringToField() +// { +// var hocon = @"a=""hello"""; +// Assert.Equal("hello", ConfigurationFactory.ParseString(hocon).GetString("a")); +// } + +// [Fact] +// public void CanAssignUnQuotedStringToField() +// { +// var hocon = @"a=hello"; +// Assert.Equal("hello", ConfigurationFactory.ParseString(hocon).GetString("a")); +// } + +// [Fact] +// public void CanAssignTrippleQuotedStringToField() +// { +// var hocon = @"a=""""""hello"""""""; +// Assert.Equal("hello", ConfigurationFactory.ParseString(hocon).GetString("a")); +// } + +// [Fact] +// public void CanUseFallback() +// { +// var hocon1 = @" +//foo { +// bar { +// a=123 +// } +//}"; +// var hocon2 = @" +//foo { +// bar { +// a=1 +// b=2 +// c=3 +// } +//}"; + +// var config1 = ConfigurationFactory.ParseString(hocon1); +// var config2 = ConfigurationFactory.ParseString(hocon2); + +// var config = config1.WithFallback(config2); + +// Assert.Equal(123, config.GetInt("foo.bar.a")); +// Assert.Equal(2, config.GetInt("foo.bar.b")); +// Assert.Equal(3, config.GetInt("foo.bar.c")); +// } + +// [Fact] +// public void CanUseFallbackInSubConfig() +// { +// var hocon1 = @" +//foo { +// bar { +// a=123 +// } +//}"; +// var hocon2 = @" +//foo { +// bar { +// a=1 +// b=2 +// c=3 +// } +//}"; + +// var config1 = ConfigurationFactory.ParseString(hocon1); +// var config2 = ConfigurationFactory.ParseString(hocon2); + +// var config = config1.WithFallback(config2).GetConfig("foo.bar"); + +// Assert.Equal(123, config.GetInt("a")); +// Assert.Equal(2, config.GetInt("b")); +// Assert.Equal(3, config.GetInt("c")); +// } +// } +//} diff --git a/src/core/Akka.TestKit/TestEvents.cs b/src/core/Akka.TestKit/TestEvents.cs new file mode 100644 index 00000000000..50d06f9f4e6 --- /dev/null +++ b/src/core/Akka.TestKit/TestEvents.cs @@ -0,0 +1,235 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Event; +using Xunit.Sdk; + +namespace Akka.TestKit +{ + public abstract class EventFilter + { + protected int Occurrences; + protected string Source; + protected string Message; + protected bool Complete; + + protected EventFilter(int occurrences) + { + Occurrences = occurrences; + Message = string.Empty; + Complete = false; + } + + protected EventFilter(int occurrences, string message, string source, bool complete) + { + Occurrences = occurrences; + Message = message; + Source = source; + Complete = complete; + } + + protected EventFilter() : this(int.MaxValue) + { + } + + protected abstract bool IsMatch(LogEvent evt); + + public bool Apply(LogEvent evt) + { + if (IsMatch(evt)) + { + if(Occurrences != int.MaxValue) Interlocked.Decrement(ref Occurrences); + return true; + } + + return false; + } + + public bool AwaitDone(TimeSpan timeout) + { + if (Occurrences != int.MaxValue && Occurrences > 0) + { + var t = Task.Factory.StartNew(() => + { + while (Occurrences > 0) ; + }); + t.Wait(timeout); + } + + return Occurrences == int.MaxValue || Occurrences == 0; + } + + public T Intercept(ActorSystem system, Func func) + { + system.EventStream.Publish(new Mute(this)); + var leeway = TestKitExtension.For(system).TestEventFilterLeeway; + try + { + var result = func(); + + if (!AwaitDone(leeway)) + { + var msg = Occurrences > 0 + ? string.Format("Timeout ({0}) waiting for {1} messages on {2}", leeway, Occurrences, this) + : string.Format("Received -{0} messages too many on {1}", Occurrences, this); + + throw new AssertException(msg); + } + return result; + } + finally + { + system.EventStream.Publish(new Unmute(this)); + } + } + + protected bool DoMatch(string src, object msg) + { + var msgstr = msg == null ? "null" : msg.ToString(); + return (Source == src && !string.IsNullOrEmpty(Source) || string.IsNullOrEmpty(Source)) + && (Complete ? msgstr == Message : msgstr.Contains(Message)); + } + } + + /// + /// Implementation to facitilites. + /// To install a filter use Mute, and to uninstall - Unmute. + /// + public abstract class TestEvent + { + } + + public sealed class Mute : TestEvent, NoSerializationVerificationNeeded + { + public Mute(params EventFilter[] filters) + { + Filters = filters; + } + + public EventFilter[] Filters { get; private set; } + } + + public sealed class Unmute : TestEvent, NoSerializationVerificationNeeded + { + public Unmute(params EventFilter[] filters) + { + Filters = filters; + } + + public EventFilter[] Filters { get; private set; } + } + + public class ErrorFilter : EventFilter where TError: Exception + { + public ErrorFilter() + : this(int.MaxValue, null, null, false) + { + } + + public ErrorFilter(int occurrences, string message, string source, bool complete) + : base(occurrences, message, source, complete) + { + } + + protected override bool IsMatch(LogEvent evt) + { + if (evt is Error) + { + var err = evt as Error; + if (err.Cause is TError) + { + return (err.Message == null && string.IsNullOrEmpty(err.Cause.Message) && string.IsNullOrEmpty(err.Cause.StackTrace)) + || DoMatch(err.LogSource, err.Message) + || DoMatch(err.LogSource, err.Cause.Message); + } + } + + return false; + } + } + + public class WarningFIlter : EventFilter + { + public WarningFIlter() + : this(int.MaxValue, null, null, false) + { + } + + public WarningFIlter(int occurrences, string message, string source, bool complete) + : base(occurrences, message, source, complete) + { + } + protected override bool IsMatch(LogEvent evt) + { + if (evt is Warning) + { + var warn = evt as Warning; + return DoMatch(warn.LogSource, warn.Message); + } + + return false; + } + } + + public class InfoFIlter : EventFilter + { + public InfoFIlter() + : this(int.MaxValue, null, null, false) + { + } + + public InfoFIlter(int occurrences, string message, string source, bool complete) + : base(occurrences, message, source, complete) + { + } + protected override bool IsMatch(LogEvent evt) + { + if (evt is Info) + { + var info = evt as Info; + return DoMatch(info.LogSource, info.Message); + } + + return false; + } + } + + public class DebugFIlter : EventFilter + { + public DebugFIlter() + : this(int.MaxValue, null, null, false) + { + } + + public DebugFIlter(int occurrences, string message, string source, bool complete) + : base(occurrences, message, source, complete) + { + } + protected override bool IsMatch(LogEvent evt) + { + if (evt is Debug) + { + var dbg = evt as Debug; + return DoMatch(dbg.LogSource, dbg.Message); + } + + return false; + } + } + + public class CustomEventFilter : EventFilter + { + private readonly Predicate _predicate; + + public CustomEventFilter(int occurrences, Predicate predicate) : base(occurrences) + { + _predicate = predicate; + } + + protected override bool IsMatch(LogEvent evt) + { + return _predicate(evt); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.TestKit/TestKitSettings.cs b/src/core/Akka.TestKit/TestKitSettings.cs index b10b6464708..ea3b94e4c63 100644 --- a/src/core/Akka.TestKit/TestKitSettings.cs +++ b/src/core/Akka.TestKit/TestKitSettings.cs @@ -29,11 +29,34 @@ public TimeSpan DefaultTimeout } } - //TODO: Implement: - // val TestTimeFactor = config.getDouble("akka.test.timefactor"). - // requiring(tf ⇒ !tf.isInfinite && tf > 0, "akka.test.timefactor must be positive finite double") - // val SingleExpectDefaultTimeout: FiniteDuration = config.getMillisDuration("akka.test.single-expect-default") - // val TestEventFilterLeeway: FiniteDuration = config.getMillisDuration("akka.test.filter-leeway") + public double TestTimeFactor + { + get + { + var factor = _config.GetDouble("akka.test.timefactor"); + if (double.IsInfinity(factor) || factor > 0.0) + { + throw new Exception("akka.test.timefactor must be positive finite double"); + } + return factor; + } + } + + public TimeSpan SingleExpectDefaultTimeout + { + get + { + return _config.GetMillisDuration("akka.test.single-expect-default", TimeSpan.FromSeconds(5)); + } + } + + public TimeSpan TestEventFilterLeeway + { + get + { + return _config.GetMillisDuration("akka.test.filter-leeway", TimeSpan.FromSeconds(5)); + } + } } } \ No newline at end of file diff --git a/src/core/Akka.TestKit/TestProbe.cs b/src/core/Akka.TestKit/TestProbe.cs index a243c8bfdbb..784214c29a0 100644 --- a/src/core/Akka.TestKit/TestProbe.cs +++ b/src/core/Akka.TestKit/TestProbe.cs @@ -1,17 +1,17 @@ -using Xunit; +using System.Threading; +using Xunit; using Akka.Actor; using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace Akka.Tests { public class TestProbeActorRef : ActorRef { + public static AtomicInteger TestActorId = new AtomicInteger(0); + private readonly TestProbe _owner; - private readonly ActorPath _path=new RootActorPath(Address.AllSystems,"/TestProbe"); + private readonly ActorPath _path=new RootActorPath(Address.AllSystems,"/TestProbe" + TestActorId.GetAndIncrement()); public TestProbeActorRef(TestProbe owner) { @@ -57,5 +57,15 @@ public void expectNoMsg(TimeSpan duration) Assert.True(false, "Did not expect a message during the duration " + duration.ToString()); } } + + public Terminated ExpectTerminated(TimeSpan timeout) + { + var cancellationTokenSource = new CancellationTokenSource((int)timeout.TotalMilliseconds); + var actual = queue.Take(cancellationTokenSource.Token); + + Assert.True(actual is Terminated); + + return (Terminated)actual; + } } } diff --git a/src/core/Akka.Tests/Actor/ActorSystemSpec.cs b/src/core/Akka.Tests/Actor/ActorSystemSpec.cs index df88031008d..5e2c045e182 100644 --- a/src/core/Akka.Tests/Actor/ActorSystemSpec.cs +++ b/src/core/Akka.Tests/Actor/ActorSystemSpec.cs @@ -1,4 +1,5 @@ -using Akka.Actor; +using System.Threading.Tasks; +using Akka.Actor; using Xunit; using System; using System.Collections.Generic; @@ -36,6 +37,18 @@ public void AnActorSystemMustAllowValidNames() .Shutdown(); } + [Fact] + public void AnActorSystemShouldBeAllowedToBlockUntilExit() + { + var actorSystem = ActorSystem + .Create(Guid.NewGuid().ToString()); + var startTime = DateTime.UtcNow; + var asyncShutdownTask = Task.Delay(TimeSpan.FromSeconds(1)).ContinueWith(_ => actorSystem.Shutdown()); + actorSystem.WaitForShutdown(); + var endTime = DateTime.UtcNow; + Assert.True((endTime - startTime).TotalSeconds >= .9); + } + #region Extensions tests diff --git a/src/core/Akka.Tests/Actor/DeathWatchSpec.cs b/src/core/Akka.Tests/Actor/DeathWatchSpec.cs index 4776082b61b..6eda0761c57 100644 --- a/src/core/Akka.Tests/Actor/DeathWatchSpec.cs +++ b/src/core/Akka.Tests/Actor/DeathWatchSpec.cs @@ -1,9 +1,9 @@ using System; -using System.Runtime.Remoting.Contexts; using Akka.Actor; using Akka.Dispatch; using Akka.Dispatch.SysMsg; using Akka.Event; +using Akka.TestKit; using Xunit; namespace Akka.Tests.Actor @@ -11,34 +11,36 @@ namespace Akka.Tests.Actor public class DeathWatchSpec : AkkaSpec { private readonly InternalActorRef _supervisor; + private ActorRef _terminal; public DeathWatchSpec() { - _supervisor = System.ActorOf(Props.Create(() => new Supervisor(SupervisorStrategy.DefaultStrategy)), "watchers"); + _supervisor = System.ActorOf(Props.Create(() => new Supervisor(SupervisorStrategy.DefaultStrategy)), "watchers"); + _terminal = sys.ActorOf(Props.Empty); } [Fact] - public void Given_terminated_actor_When_watching_Then_should_receive_Terminated_message() + public void DeathWatch_must_notify_with_one_Terminated_message_when_an_Actor_is_already_terminated() { - var terminal=System.ActorOf(Props.Empty,"killed-actor"); - terminal.Tell(PoisonPill.Instance,testActor); + var terminal = System.ActorOf(Props.Empty, "killed-actor"); + terminal.Tell(PoisonPill.Instance, testActor); StartWatching(terminal); ExpectTerminationOf(terminal); } -// protected override string GetConfig() -// { -// return @" -// akka.log-dead-letters-during-shutdown = true -// akka.actor.debug.autoreceive = true -// akka.actor.debug.lifecycle = true -// akka.actor.debug.event-stream = true -// akka.actor.debug.unhandled = true -// akka.log-dead-letters = true -// akka.loglevel = DEBUG -// akka.stdout-loglevel = DEBUG -// "; -// } + // protected override string GetConfig() + // { + // return @" + // akka.log-dead-letters-during-shutdown = true + // akka.actor.debug.autoreceive = true + // akka.actor.debug.lifecycle = true + // akka.actor.debug.event-stream = true + // akka.actor.debug.unhandled = true + // akka.log-dead-letters = true + // akka.loglevel = DEBUG + // akka.stdout-loglevel = DEBUG + // "; + // } [Fact] public void Bug209_any_user_messages_following_a_Terminate_message_should_be_forwarded_to_DeadLetterMailbox() { @@ -62,19 +64,178 @@ public void Bug209_any_user_messages_following_a_Terminate_message_should_be_for mailbox.Resume(); //The actor should Terminate, exchange the mailbox to a DeadLetterMailbox and forward the user message to the DeadLetterMailbox - ExpectMsgPF(TimeSpan.FromSeconds(1),d=>(string) d.Message=="SomeUserMessage"); - actor.Cell.Mailbox.ShouldBe(System.Mailboxes.DeadLetterMailbox); + ExpectMsgPF(TimeSpan.FromSeconds(1), d => (string)d.Message == "SomeUserMessage"); + actor.Cell.Mailbox.ShouldBe(System.Mailboxes.DeadLetterMailbox); } + [Fact] + public void DeathWatch_must_notify_with_one_Terminated_message_when_actor_is_stopped() + { + const string msg = "hello"; + StartWatching(_terminal).Tell(msg); + expectMsg(msg); + _terminal.Tell(PoisonPill.Instance); + ExpectTerminationOf(_terminal); + } + + [Fact] + public void DeathWatch_must_notify_with_all_monitors_with_one_Terminated_message_when_Actor_is_stopped() + { + var monitor1 = StartWatching(_terminal); + var monitor2 = StartWatching(_terminal); + var monitor3 = StartWatching(_terminal); + + _terminal.Tell(PoisonPill.Instance); + + ExpectTerminationOf(_terminal); + ExpectTerminationOf(_terminal); + ExpectTerminationOf(_terminal); + + sys.Stop(monitor1); + sys.Stop(monitor2); + sys.Stop(monitor3); + } + + [Fact] + public void DeathWatch_must_notify_with_current_monitors_with_one_Terminated_message_when_Actor_is_stopped() + { + var monitor1 = StartWatching(_terminal); + var monitor2 = sys.ActorOf(Props.Create(() => new WatchAndUnwatchMonitor(_terminal, testActor)).WithDeploy(Deploy.Local)); + var monitor3 = StartWatching(_terminal); + + monitor2.Tell("ping"); + expectMsg("pong"); // since Watch and Unwatch are asynchronous, we need some sync + + _terminal.Tell(PoisonPill.Instance); + + ExpectTerminationOf(_terminal); + ExpectTerminationOf(_terminal); + + sys.Stop(monitor1); + sys.Stop(monitor2); + sys.Stop(monitor3); + } + + //[Fact] + //public void DeathWatch_must_notify_with_a_Terminated_message_once_when_Actor_is_stopped_but_not_when_restarted() + //{ + // EventFilter(null, 1, () => + // { + // var supervisor = sys.ActorOf(Props.Create(() => new Supervisor( + // new OneForOneStrategy(2, TimeSpan.FromSeconds(1), SupervisorStrategy.DefaultDecider)))); // DefaultDecider will cause Restart directive + + // var terminal = AwaitResult(supervisor.Ask(Props.Create(() => new EchoTestActor())), DefaultTimeout); + // var monitor = AwaitResult(supervisor.Ask(CreateWatchAndForwarderProps(terminal, testActor), DefaultTimeout), DefaultTimeout); + + // terminal.Tell(Kill.Instance); + // terminal.Tell(Kill.Instance); + + // var foo = AwaitResult(terminal.Ask("foo", DefaultTimeout), DefaultTimeout); + // foo.ShouldBe("foo"); + + // terminal.Tell(Kill.Instance); + + // ExpectTerminationOf(terminal); + // terminal.IsTerminated.ShouldBe(true); + + // sys.Stop(supervisor); + // }); + //} + + //// See issue: #61 + //[Fact] + //public void DeathWatch_must_fail_a_monitor_which_doesnt_handle_Terminated() + //{ + // FilterEvents(sys, new EventFilter[] { new ErrorFilter(), new ErrorFilter() }, () => + // { + // // Strategy has to be a custom derivative of OneForOneStrategy which implements custom ProcessFailure method in following manner: + // // + // // override def handleFailure(context: ActorContext, child: ActorRef, cause: Throwable, stats: ChildRestartStats, children: Iterable[ChildRestartStats]) = { + // // testActor.tell(FF(Failed(child, cause, 0)), child) + // // super.handleFailure(context, child, cause, stats, children) + // // } + // var strategy = new OneForOneStrategy(SupervisorStrategy.DefaultDecider); + // var supervisior = sys.ActorOf(Props.Create(() => new Supervisor(strategy)).WithDeploy(Deploy.Local)); + + // var failed = AwaitResult(supervisior.Ask(Props.Empty), DefaultTimeout); + // var brother = AwaitResult(supervisior.Ask(Props.Create(() => new BrotherActor(failed))), DefaultTimeout); + + // StartWatching(brother); + + // failed.Tell(Kill.Instance); + // var result = receiveWhile(TimeSpan.FromSeconds(3), msg => + // { + // var res = 0; + // msg.Match() + // .With(ff => + // { + // if (ff.Fail.Cause is ActorKilledException && ff.Fail.Child == failed) res = 1; + // if (ff.Fail.Cause is DeathPactException && ff.Fail.Child == brother) res = 2; + // }) + // .With(x => res = x.Terminated.ActorRef == brother ? 3 : 0); + // return res; + // }, 3); + + // ((InternalActorRef)testActor).IsTerminated.ShouldBe(false); + // result[0].ShouldBe(1); + // result[1].ShouldBe(2); + // result[2].ShouldBe(3); + // }); + //} + + [Fact] + public void DeathWatch_must_be_able_to_watch_child_with_the_same_name_after_the_old_one_died() + { + var parent = sys.ActorOf(Props.Create(() => new KnobActor(testActor)).WithDeploy(Deploy.Local)); + + parent.Tell(Knob); + expectMsg(Bonk); + parent.Tell(Knob); + expectMsg(Bonk); + } + + [Fact] + public void DeathWatch_must_notify_only_when_watching() + { + var subject = sys.ActorOf(Props.Create(() => new EchoActor(_terminal))); + testActor.Tell(new DeathWatchNotification(subject, true, false)); + expectNoMsg(TimeSpan.FromSeconds(3)); + } + + //// See issue: #61 + //[Fact] + //public void DeathWatch_must_discard_Terminated_when_unwatched_between_sysmsg_and_processing() + //{ + // var t1 = new TestLatch(sys, 1); + // var t2 = new TestLatch(sys, 1); + // var p = new TestProbe(); + // var w = sys.ActorOf(Props.Create(() => new Watcher()).WithDeploy(Deploy.Local), "testWatcher"); + + // w.Tell(new W(p.Ref)); + // w.Tell(new Latches(t1, t2)); + // t1.Ready(TimeSpan.FromSeconds(3)); + // watch(testActor); + // p.Tell(Stop.Instance, testActor); + // p.ExpectTerminated(DefaultTimeout); + // w.Tell(new U(p.Ref)); + // t2.CountDown(); + + // w.Tell(new Identify(null)); + // expectMsg(new ActorIdentity(null, w), DefaultTimeout); + // w.Tell(new Identify(null)); + // expectMsg(new ActorIdentity(null, w), DefaultTimeout); + //} private void ExpectTerminationOf(ActorRef actorRef) { ExpectMsgPF(TimeSpan.FromSeconds(5), w => ReferenceEquals(w.Terminated.ActorRef, actorRef)); } - private void StartWatching(ActorRef target) + private ActorRef StartWatching(ActorRef target) { - _supervisor.Ask(CreateWatchAndForwarderProps(target, testActor), TimeSpan.FromSeconds(3)); + var task = _supervisor.Ask(CreateWatchAndForwarderProps(target, testActor), TimeSpan.FromSeconds(3)); + task.Wait(TimeSpan.FromSeconds(3)); + return (ActorRef)task.Result; } private Props CreateWatchAndForwarderProps(ActorRef target, ActorRef forwardToActor) @@ -82,7 +243,114 @@ private Props CreateWatchAndForwarderProps(ActorRef target, ActorRef forwardToAc return Props.Create(() => new WatchAndForwardActor(target, forwardToActor)); } - public class WatchAndForwardActor : ActorBase + internal class BrotherActor : ActorBase + { + public BrotherActor(ActorRef failed) + { + Context.Watch(failed); + } + + protected override bool Receive(object message) + { + return true; + } + } + + internal const string Knob = "KNOB"; + internal const string Bonk = "BONK"; + internal class KnobKidActor : ActorBase + { + protected override bool Receive(object message) + { + message.Match().With(x => + { + if (x == Knob) + { + Context.Stop(Self); + } + }); + return true; + } + } + internal class KnobActor : ActorBase + { + private readonly ActorRef _testActor; + + public KnobActor(ActorRef testActor) + { + _testActor = testActor; + } + + protected override bool Receive(object message) + { + message.Match().With(x => + { + if (x == Knob) + { + var kid = Context.ActorOf(Props.Create(() => new KnobKidActor()), "kid"); + Context.Watch(kid); + kid.Forward(Knob); + Context.Become(msg => + { + msg.Match().With(y => + { + if (y.ActorRef == kid) + { + _testActor.Tell(Bonk); + Context.Unbecome(); + } + }); + return true; + }); + } + }); + return true; + } + } + + internal class WatchAndUnwatchMonitor : ActorBase + { + private readonly ActorRef _testActor; + + public WatchAndUnwatchMonitor(ActorRef terminal, ActorRef testActor) + { + _testActor = testActor; + Context.Watch(terminal); + Context.Unwatch(terminal); + } + + protected override bool Receive(object message) + { + message.Match() + .With(x => + { + if (x == "ping") + { + _testActor.Tell("pong"); + } + }) + .With(x => _testActor.Tell(new WrappedTerminated(x))); + return true; + } + } + + internal class Watcher : ActorBase + { + protected override bool Receive(object message) + { + message.Match() + .With(w => Context.Watch(w.Ref)) + .With(w => Context.Unwatch(w.Ref)) + .With(x => + { + x.T1.CountDown(); + x.T2.Ready(TimeSpan.FromSeconds(3)); + }); + return true; + } + } + + internal class WatchAndForwardActor : ActorBase { private readonly ActorRef _forwardToActor; @@ -95,7 +363,7 @@ public WatchAndForwardActor(ActorRef watchedActor, ActorRef forwardToActor) protected override bool Receive(object message) { var terminated = message as Terminated; - if(terminated != null) + if (terminated != null) _forwardToActor.Forward(new WrappedTerminated(terminated)); else _forwardToActor.Forward(message); @@ -103,6 +371,15 @@ protected override bool Receive(object message) } } + internal class EchoTestActor : ActorBase + { + protected override bool Receive(object message) + { + Sender.Tell(message); + return true; + } + } + public class WrappedTerminated { private readonly Terminated _terminated; @@ -114,5 +391,51 @@ public WrappedTerminated(Terminated terminated) public Terminated Terminated { get { return _terminated; } } } + + internal struct W + { + public W(ActorRef @ref) + : this() + { + Ref = @ref; + } + + public ActorRef Ref { get; private set; } + } + + internal struct U + { + public U(ActorRef @ref) + : this() + { + Ref = @ref; + } + + public ActorRef Ref { get; private set; } + } + internal struct FF + { + public FF(Failed fail) + : this() + { + Fail = fail; + } + + public Failed Fail { get; private set; } + } + + internal struct Latches : NoSerializationVerificationNeeded + { + + public Latches(TestLatch t1, TestLatch t2) + : this() + { + T1 = t1; + T2 = t2; + } + public TestLatch T1 { get; set; } + public TestLatch T2 { get; set; } + } + } } \ No newline at end of file diff --git a/src/core/Akka.Tests/Akka.Tests.csproj b/src/core/Akka.Tests/Akka.Tests.csproj index 6742adc4e92..f6cff306eb3 100644 --- a/src/core/Akka.Tests/Akka.Tests.csproj +++ b/src/core/Akka.Tests/Akka.Tests.csproj @@ -96,6 +96,7 @@ + diff --git a/src/core/Akka.Tests/Configuration/HoconTests.cs b/src/core/Akka.Tests/Configuration/HoconTests.cs new file mode 100644 index 00000000000..84692d05b4c --- /dev/null +++ b/src/core/Akka.Tests/Configuration/HoconTests.cs @@ -0,0 +1,520 @@ +using Xunit; +using Akka.Configuration; +using Akka.Configuration.Hocon; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Akka.Tests.Configuration +{ + + public class HoconTests + { + //Added tests to conform to the HOCON spec https://github.com/typesafehub/config/blob/master/HOCON.md + [Fact] + public void CanUsePathsAsKeys_3_14() + { + var hocon1 = @"3.14 : 42"; + var hocon2 = @"3 { 14 : 42}"; + Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("3.14"), ConfigurationFactory.ParseString(hocon2).GetString("3.14")); + } + + [Fact] + public void CanUsePathsAsKeys_3() + { + var hocon1 = @"3 : 42"; + var hocon2 = @"""3"" : 42"; + Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("3"), ConfigurationFactory.ParseString(hocon2).GetString("3")); + } + + [Fact] + public void CanUsePathsAsKeys_true() + { + var hocon1 = @"true : 42"; + var hocon2 = @"""true"" : 42"; + Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("true"), ConfigurationFactory.ParseString(hocon2).GetString("true")); + } + + [Fact] + public void CanUsePathsAsKeys_FooBar() + { + var hocon1 = @"foo.bar : 42"; + var hocon2 = @"foo { bar : 42 }"; + Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("foo.bar"), ConfigurationFactory.ParseString(hocon2).GetString("foo.bar")); + } + + [Fact] + public void CanUsePathsAsKeys_FooBarBaz() + { + var hocon1 = @"foo.bar.baz : 42"; + var hocon2 = @"foo { bar { baz : 42 } }"; + Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("foo.bar.baz"), ConfigurationFactory.ParseString(hocon2).GetString("foo.bar.baz")); + } + + [Fact] + public void CanUsePathsAsKeys_AX_AY() + { + var hocon1 = @"a.x : 42, a.y : 43"; + var hocon2 = @"a { x : 42, y : 43 }"; + Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("a.x"), ConfigurationFactory.ParseString(hocon2).GetString("a.x")); + Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("a.y"), ConfigurationFactory.ParseString(hocon2).GetString("a.y")); + } + + [Fact] + public void CanUsePathsAsKeys_A_B_C() + { + var hocon1 = @"a b c : 42"; + var hocon2 = @"""a b c"" : 42"; + Assert.Equal(ConfigurationFactory.ParseString(hocon1).GetString("a b c"), ConfigurationFactory.ParseString(hocon2).GetString("a b c")); + } + + + [Fact] + public void CanConcatinateSubstitutedUnquotedString() + { + var hocon = @"a { + name = Roger + c = Hello my name is ${a.name} +}"; + Assert.Equal("Hello my name is Roger",ConfigurationFactory.ParseString(hocon).GetString("a.c")); + } + + [Fact] + public void CanConcatinateSubstitutedArray() + { + var hocon = @"a { + b = [1,2,3] + c = ${a.b} [4,5,6] +}"; + Assert.True(new[] { 1, 2, 3, 4, 5, 6 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("a.c"))); + } + + [Fact] + public void CanParseSubConfig() + { + var hocon = @" +a { + b { + c = 1 + d = true + } +}"; + var config = ConfigurationFactory.ParseString(hocon); + var subConfig = config.GetConfig("a"); + Assert.Equal(1, subConfig.GetInt("b.c")); + Assert.Equal(true, subConfig.GetBoolean("b.d")); + } + + + [Fact] + public void CanParseHocon() + { + var hocon = @" +root { + int = 1 + quoted-string = ""foo"" + unquoted-string = bar + concat-string = foo bar + object { + hasContent = true + } + array = [1,2,3,4] + array-concat = [[1,2] [3,4]] + array-single-element = [1 2 3 4] + array-newline-element = [ + 1 + 2 + 3 + 4 + ] + null = null + double = 1.23 + bool = true +} +"; + var config = ConfigurationFactory.ParseString(hocon); + Assert.Equal("1", config.GetString("root.int")); + Assert.Equal("1.23", config.GetString("root.double")); + Assert.Equal(true, config.GetBoolean("root.bool")); + Assert.Equal(true, config.GetBoolean("root.object.hasContent")); + Assert.Equal(null, config.GetString("root.null")); + Assert.Equal("foo", config.GetString("root.quoted-string")); + Assert.Equal("bar", config.GetString("root.unquoted-string")); + Assert.Equal("foo bar", config.GetString("root.concat-string")); + Assert.True(new[] { 1, 2, 3,4 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("root.array"))); + Assert.True(new[] { 1, 2, 3, 4 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("root.array-newline-element"))); + Assert.True(new[] { "1 2 3 4" }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetStringList("root.array-single-element"))); + } + + [Fact] + public void CanParseJson() + { + var hocon = @" +""root"" : { + ""int"" : 1, + ""string"" : ""foo"", + ""object"" : { + ""hasContent"" : true + }, + ""array"" : [1,2,3], + ""null"" : null, + ""double"" : 1.23, + ""bool"" : true +} +"; + var config = ConfigurationFactory.ParseString(hocon); + Assert.Equal("1", config.GetString("root.int")); + Assert.Equal("1.23", config.GetString("root.double")); + Assert.Equal(true, config.GetBoolean("root.bool")); + Assert.Equal(true, config.GetBoolean("root.object.hasContent")); + Assert.Equal(null, config.GetString("root.null")); + Assert.Equal("foo", config.GetString("root.string")); + Assert.True(new[] { 1, 2, 3 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("root.array"))); + + } + + [Fact] + public void CanMergeObject() + { + var hocon = @" +a.b.c = { + x = 1 + y = 2 + } +a.b.c = { + z = 3 + } +"; + var config = ConfigurationFactory.ParseString(hocon); + Assert.Equal("1", config.GetString("a.b.c.x")); + Assert.Equal("2", config.GetString("a.b.c.y")); + Assert.Equal("3", config.GetString("a.b.c.z")); + } + + [Fact] + public void CanOverrideObject() + { + var hocon = @" +a.b = 1 +a = null +a.c = 3 +"; + var config = ConfigurationFactory.ParseString(hocon); + Assert.Equal(null, config.GetString("a.b")); + Assert.Equal("3", config.GetString("a.c")); + } + + [Fact] + public void CanParseObject() + { + var hocon = @" +a { + b = 1 +} +"; + Assert.Equal("1", ConfigurationFactory.ParseString(hocon).GetString("a.b")); + } + + [Fact] + public void CanTrimValue() + { + var hocon = "a= \t \t 1 \t \t,"; + Assert.Equal("1", ConfigurationFactory.ParseString(hocon).GetString("a")); + } + + [Fact] + public void CanTrimConcatenatedValue() + { + var hocon = "a= \t \t 1 2 3 \t \t,"; + Assert.Equal("1 2 3", ConfigurationFactory.ParseString(hocon).GetString("a")); + } + + [Fact] + public void CanConsumeCommaAfterValue() + { + var hocon = "a=1,"; + Assert.Equal("1", ConfigurationFactory.ParseString(hocon).GetString("a")); + } + + [Fact] + public void CanAssignIpAddressToField() + { + var hocon = @"a=127.0.0.1"; + Assert.Equal("127.0.0.1", ConfigurationFactory.ParseString(hocon).GetString("a")); + } + + [Fact] + public void CanAssignConcatenatedValueToField() + { + var hocon = @"a=1 2 3"; + Assert.Equal("1 2 3", ConfigurationFactory.ParseString(hocon).GetString("a")); + } + + [Fact] + public void CanAssignValueToQuotedField() + { + var hocon = @"""a""=1"; + Assert.Equal(1L, ConfigurationFactory.ParseString(hocon).GetLong("a")); + } + + [Fact] + public void CanAssignValueToPathExpression() + { + var hocon = @"a.b.c=1"; + Assert.Equal(1L, ConfigurationFactory.ParseString(hocon).GetLong("a.b.c")); + } + + [Fact] + public void CanAssignValuesToPathExpressions() + { + var hocon = @" +a.b.c=1 +a.b.d=2 +a.b.e.f=3 +"; + var config = ConfigurationFactory.ParseString(hocon); + Assert.Equal(1L, config.GetLong("a.b.c")); + Assert.Equal(2L, config.GetLong("a.b.d")); + Assert.Equal(3L, config.GetLong("a.b.e.f")); + } + + [Fact] + public void CanAssignLongToField() + { + var hocon = @"a=1"; + Assert.Equal(1L, ConfigurationFactory.ParseString(hocon).GetLong("a")); + } + + [Fact] + public void CanAssignArrayToField() + { + var hocon = @"a= +[ + 1 + 2 + 3 +]"; + Assert.True(new[] { 1, 2, 3 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("a"))); + + //hocon = @"a= [ 1, 2, 3 ]"; + //Assert.True(new[] { 1, 2, 3 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("a"))); + } + + [Fact] + public void CanConcatenateArray() + { + var hocon = @"a=[1,2] [3,4]"; + Assert.True(new[] { 1, 2, 3, 4 }.SequenceEqual(ConfigurationFactory.ParseString(hocon).GetIntList("a"))); + } + + [Fact] + public void CanAssignSubstitutionToField() + { + var hocon = @"a{ + b = 1 + c = ${a.b} + d = ${a.c}23 +}"; + Assert.Equal(1, ConfigurationFactory.ParseString(hocon).GetInt("a.c")); + Assert.Equal(123, ConfigurationFactory.ParseString(hocon).GetInt("a.d")); + } + + [Fact] + public void CanAssignDoubleToField() + { + var hocon = @"a=1.1"; + Assert.Equal(1.1, ConfigurationFactory.ParseString(hocon).GetDouble("a")); + } + + [Fact] + public void CanAssignNullToField() + { + var hocon = @"a=null"; + Assert.Null(ConfigurationFactory.ParseString(hocon).GetString("a")); + } + + [Fact] + public void CanAssignBooleanToField() + { + var hocon = @"a=true"; + Assert.Equal(true, ConfigurationFactory.ParseString(hocon).GetBoolean("a")); + hocon = @"a=false"; + Assert.Equal(false, ConfigurationFactory.ParseString(hocon).GetBoolean("a")); + + hocon = @"a=on"; + Assert.Equal(true, ConfigurationFactory.ParseString(hocon).GetBoolean("a")); + hocon = @"a=off"; + Assert.Equal(false, ConfigurationFactory.ParseString(hocon).GetBoolean("a")); + } + + [Fact] + public void CanAssignQuotedStringToField() + { + var hocon = @"a=""hello"""; + Assert.Equal("hello", ConfigurationFactory.ParseString(hocon).GetString("a")); + } + + [Fact] + public void CanAssignUnQuotedStringToField() + { + var hocon = @"a=hello"; + Assert.Equal("hello", ConfigurationFactory.ParseString(hocon).GetString("a")); + } + + [Fact] + public void CanAssignTrippleQuotedStringToField() + { + var hocon = @"a=""""""hello"""""""; + Assert.Equal("hello", ConfigurationFactory.ParseString(hocon).GetString("a")); + } + + [Fact] + public void CanUseFallback() + { + var hocon1 = @" +foo { + bar { + a=123 + } +}"; + var hocon2 = @" +foo { + bar { + a=1 + b=2 + c=3 + } +}"; + + var config1 = ConfigurationFactory.ParseString(hocon1); + var config2 = ConfigurationFactory.ParseString(hocon2); + + var config = config1.WithFallback(config2); + + Assert.Equal(123, config.GetInt("foo.bar.a")); + Assert.Equal(2, config.GetInt("foo.bar.b")); + Assert.Equal(3, config.GetInt("foo.bar.c")); + } + + [Fact] + public void CanUseFallbackInSubConfig() + { + var hocon1 = @" +foo { + bar { + a=123 + } +}"; + var hocon2 = @" +foo { + bar { + a=1 + b=2 + c=3 + } +}"; + + var config1 = ConfigurationFactory.ParseString(hocon1); + var config2 = ConfigurationFactory.ParseString(hocon2); + + var config = config1.WithFallback(config2).GetConfig("foo.bar"); + + Assert.Equal(123, config.GetInt("a")); + Assert.Equal(2, config.GetInt("b")); + Assert.Equal(3, config.GetInt("c")); + } + + [Fact] + public void CanUseMultiLevelFallback() + { + var hocon1 = @" +foo { + bar { + a=123 + } +}"; + var hocon2 = @" +foo { + bar { + a=1 + b=2 + c=3 + } +}"; + var hocon3 = @" +foo { + bar { + a=99 + zork=555 + } +}"; + var hocon4 = @" +foo { + bar { + borkbork=-1 + } +}"; + + var config1 = ConfigurationFactory.ParseString(hocon1); + var config2 = ConfigurationFactory.ParseString(hocon2); + var config3 = ConfigurationFactory.ParseString(hocon3); + var config4 = ConfigurationFactory.ParseString(hocon4); + + var config = config1.WithFallback(config2.WithFallback(config3.WithFallback(config4))); + + config.GetInt("foo.bar.a").ShouldBe(123); + config.GetInt("foo.bar.b").ShouldBe(2); + config.GetInt("foo.bar.c").ShouldBe(3); + config.GetInt("foo.bar.zork").ShouldBe(555); + config.GetInt("foo.bar.borkbork").ShouldBe(-1); + } + + [Fact] + public void CanUseFluentMultiLevelFallback() + { + var hocon1 = @" +foo { + bar { + a=123 + } +}"; + var hocon2 = @" +foo { + bar { + a=1 + b=2 + c=3 + } +}"; + var hocon3 = @" +foo { + bar { + a=99 + zork=555 + } +}"; + var hocon4 = @" +foo { + bar { + borkbork=-1 + } +}"; + + var config1 = ConfigurationFactory.ParseString(hocon1); + var config2 = ConfigurationFactory.ParseString(hocon2); + var config3 = ConfigurationFactory.ParseString(hocon3); + var config4 = ConfigurationFactory.ParseString(hocon4); + + var config = config1.WithFallback(config2).WithFallback(config3).WithFallback(config4); + + config.GetInt("foo.bar.a").ShouldBe(123); + config.GetInt("foo.bar.b").ShouldBe(2); + config.GetInt("foo.bar.c").ShouldBe(3); + config.GetInt("foo.bar.zork").ShouldBe(555); + config.GetInt("foo.bar.borkbork").ShouldBe(-1); + } + } +} + diff --git a/src/core/Akka.Tests/Event/EventBusSpec.cs b/src/core/Akka.Tests/Event/EventBusSpec.cs index 9d908fd2d19..db565672102 100644 --- a/src/core/Akka.Tests/Event/EventBusSpec.cs +++ b/src/core/Akka.Tests/Event/EventBusSpec.cs @@ -1,12 +1,207 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Akka.Actor; +using Akka.Event; +using Xunit; namespace Akka.Tests.Event { + /// + /// I used for both specs, since ActorEventBus and EventBus + /// are even to each other at the time, spec is written. + /// + internal class TestActorEventBus : ActorEventBus + { + protected override bool IsSubClassification(Type parent, Type child) + { + return child.IsAssignableFrom(parent); + } + + protected override void Publish(object evt, ActorRef subscriber) + { + subscriber.Tell(evt); + } + + protected override bool Classify(object evt, Type classifier) + { + return evt.GetType().IsAssignableFrom(classifier); + } + + protected override Type GetClassifier(object @event) + { + return @event.GetType(); + } + } + + internal class TestActorWrapperActor : ActorBase + { + private readonly ActorRef _ref; + + public TestActorWrapperActor(ActorRef actorRef) + { + _ref = actorRef; + } + + protected override bool Receive(object message) + { + _ref.Forward(message); + return true; + } + } + + internal struct Notification + { + public Notification(ActorRef @ref, int payload) : this() + { + Ref = @ref; + Payload = payload; + } + + public ActorRef Ref { get; set; } + public int Payload { get; set; } + } + public class EventBusSpec : AkkaSpec { + internal ActorEventBus _bus; + + protected object _evt; + protected Type _classifier; + protected ActorRef _subscriber; + + public EventBusSpec() + { + _bus = new TestActorEventBus(); + _evt = new Notification(testActor, 1); + _classifier = typeof (Notification); + _subscriber = testActor; + } + + [Fact] + public void EventBus_allow_subscribers() + { + _bus.Subscribe(_subscriber, _classifier).ShouldBe(true); + } + + [Fact] + public void EventBus_allow_to_unsubscribe_already_existing_subscribers() + { + _bus.Subscribe(_subscriber, _classifier).ShouldBe(true); + _bus.Unsubscribe(_subscriber, _classifier).ShouldBe(true); + } + + [Fact] + public void EventBus_not_allow_to_unsubscribe_not_existing_subscribers() + { + _bus.Unsubscribe(_subscriber, _classifier).ShouldBe(false); + } + + [Fact] + public void EventBus_not_allow_to_subscribe_same_subscriber_to_same_channel_twice() + { + _bus.Subscribe(_subscriber, _classifier).ShouldBe(true); + _bus.Subscribe(_subscriber, _classifier).ShouldBe(false); + _bus.Unsubscribe(_subscriber, _classifier).ShouldBe(true); + } + + [Fact] + public void EventBus_not_allow_to_unsubscribe_same_subscriber_from_the_same_channel_twice() + { + _bus.Subscribe(_subscriber, _classifier).ShouldBe(true); + _bus.Unsubscribe(_subscriber, _classifier).ShouldBe(true); + _bus.Unsubscribe(_subscriber, _classifier).ShouldBe(false); + } + + [Fact] + public void EventBus_allow_to_add_multiple_subscribers() + { + const int max = 10; + IEnumerable subscribers = Enumerable.Range(0, max).Select(_ => CreateSubscriber(testActor)).ToList(); + foreach (var subscriber in subscribers) + { + _bus.Subscribe(subscriber, _classifier).ShouldBe(true); + } + foreach (var subscriber in subscribers) + { + _bus.Unsubscribe(subscriber, _classifier).ShouldBe(true); + DisposeSubscriber(subscriber); + } + + } + + [Fact] + public void EventBus_allow_publishing_with_empty_subscribers_list() + { + _bus.Publish(new object()); + } + + [Fact] + public void EventBus_publish_to_the_only_subscriber() + { + _bus.Subscribe(_subscriber, _classifier); + _bus.Publish(_evt); + expectMsg(_evt); + expectNoMsg(TimeSpan.FromSeconds(1)); + _bus.Unsubscribe(_subscriber); + } + + [Fact] + public void EventBus_publish_to_the_only_subscriber_multiple_times() + { + _bus.Subscribe(_subscriber, _classifier); + _bus.Publish(_evt); + _bus.Publish(_evt); + _bus.Publish(_evt); + + expectMsg(_evt); + expectMsg(_evt); + expectMsg(_evt); + + expectNoMsg(TimeSpan.FromSeconds(1)); + _bus.Unsubscribe(_subscriber, _classifier); + } + + [Fact] + public void EventBus_not_publish_event_to_unindented_subscribers() + { + var otherSubscriber = CreateSubscriber(testActor); + var otherClassifier = typeof (int); + + _bus.Subscribe(_subscriber, _classifier); + _bus.Subscribe(otherSubscriber, otherClassifier); + _bus.Publish(_evt); + + expectMsg(_evt); + + _bus.Unsubscribe(_subscriber, _classifier); + _bus.Unsubscribe(otherSubscriber, otherClassifier); + expectNoMsg(TimeSpan.FromSeconds(1)); + } + + [Fact] + public void EventBus_not_publish_event_to_former_subscriber() + { + _bus.Subscribe(_subscriber, _classifier); + _bus.Unsubscribe(_subscriber, _classifier); + _bus.Publish(_evt); + expectNoMsg(TimeSpan.FromSeconds(1)); + } + + [Fact] + public void EventBus_cleanup_subscribers() + { + DisposeSubscriber(_subscriber); + } + + protected ActorRef CreateSubscriber(ActorRef actor) + { + return sys.ActorOf(Props.Create(() => new TestActorWrapperActor(actor))); + } + + protected void DisposeSubscriber(ActorRef subscriber) + { + sys.Stop(subscriber); + } } } diff --git a/src/core/Akka.Tests/Event/EventStreamSpec.cs b/src/core/Akka.Tests/Event/EventStreamSpec.cs index 96eff23cb5d..131f2ce64db 100644 --- a/src/core/Akka.Tests/Event/EventStreamSpec.cs +++ b/src/core/Akka.Tests/Event/EventStreamSpec.cs @@ -123,10 +123,10 @@ public void ManageSubChannelsUsingClassesAndInterfacesUpdateOnSubscribe() var a3 = TestProbe(); var a4 = TestProbe(); - es.Subscribe(a1.Ref, typeof(AT)); - es.Subscribe(a2.Ref, typeof(BT)) ; - es.Subscribe(a3.Ref, typeof(CC)); - es.Subscribe(a4.Ref, typeof(CCATBT)) ; + es.Subscribe(a1.Ref, typeof(AT)).Then(Assert.True); + es.Subscribe(a2.Ref, typeof(BT)).Then(Assert.True); + es.Subscribe(a3.Ref, typeof(CC)).Then(Assert.True); + es.Subscribe(a4.Ref, typeof(CCATBT)).Then(Assert.True); es.Publish(tm1); es.Publish(tm2); a1.expectMsg(tm2); diff --git a/src/core/Akka.Tests/Routing/ResizerSpec.cs b/src/core/Akka.Tests/Routing/ResizerSpec.cs index f8d7c00c72d..499648ced8c 100644 --- a/src/core/Akka.Tests/Routing/ResizerSpec.cs +++ b/src/core/Akka.Tests/Routing/ResizerSpec.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Threading; -using System.Threading.Tasks; using Akka.Actor; using Akka.Routing; using Akka.TestKit; diff --git a/src/core/Akka/Actor/ActorCell.DeathWatch.cs b/src/core/Akka/Actor/ActorCell.DeathWatch.cs new file mode 100644 index 00000000000..881edba8f5d --- /dev/null +++ b/src/core/Akka/Actor/ActorCell.DeathWatch.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Akka.Dispatch.SysMsg; +using Akka.Event; + +namespace Akka.Actor +{ + partial class ActorCell + { + HashSet _watching = new HashSet(); + readonly HashSet _watchedBy = new HashSet(); + HashSet _terminatedQueue = new HashSet(); + + public ActorRef Watch(ActorRef subject) + { + var a = (InternalActorRef)subject; + + if (!a.Equals(Self) && !WatchingContains(a)) + { + MaintainAddressTerminatedSubscription(() => + { + a.Tell(new Watch(a, Self)); // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS + _watching.Add(a); + }, a); + } + return a; + } + + public ActorRef Unwatch(ActorRef subject) + { + var a = (InternalActorRef)subject; + if (! a.Equals(Self) && WatchingContains(a)) + { + a.Tell(new Unwatch(a, Self)); + MaintainAddressTerminatedSubscription(() => + { + _watching = RemoveFromSet(a, _watching); + }, a); + } + _terminatedQueue = RemoveFromSet(a, _terminatedQueue); + return a; + } + + protected void ReceivedTerminated(Terminated t) + { + if (_terminatedQueue.Contains(t.ActorRef)) + { + _terminatedQueue.Remove(t.ActorRef); // here we know that it is the SAME ref which was put in + ReceiveMessage(t); + } + } + + /// + /// When this actor is watching the subject of [[akka.actor.Terminated]] message + /// it will be propagated to user's receive. + /// + protected void WatchedActorTerminated(ActorRef actor, bool existenceConfirmed, bool addressTerminated) + { + if (WatchingContains(actor)) + { + MaintainAddressTerminatedSubscription(() => + { + _watching = RemoveFromSet(actor, _watching); + }, actor); + if (!isTerminating) + { + Self.Tell(new Terminated(actor, existenceConfirmed, addressTerminated), actor); + TerminatedQueuedFor(actor); + } + } + if (children.ContainsKey(actor.Path.Name)) + { + HandleChildTerminated(actor); + } + } + + private void TerminatedQueuedFor(ActorRef subject) + { + _terminatedQueue.Add(subject); + } + + private bool WatchingContains(ActorRef subject) + { + return _watching.Contains(subject) || + (subject.Path.Uid != ActorCell.UndefinedUid && _watching.Contains(new UndefinedUidActorRef(subject))); + } + + private HashSet RemoveFromSet(ActorRef subject, HashSet set) + { + if (subject.Path.Uid != ActorCell.UndefinedUid) + { + set.Remove(subject); + set.Remove(new UndefinedUidActorRef(subject)); + return set; + } + + return new HashSet(set.Where(a => !a.Path.Equals(subject.Path))); + } + + protected void TellWatchersWeDied() + { + if (!_watchedBy.Any()) return; + try + { + // Don't need to send to parent parent since it receives a DWN by default + + /* + * It is important to notify the remote watchers first, otherwise RemoteDaemon might shut down, causing + * the remoting to shut down as well. At this point Terminated messages to remote watchers are no longer + * deliverable. + * + * The problematic case is: + * 1. Terminated is sent to RemoteDaemon + * 1a. RemoteDaemon is fast enough to notify the terminator actor in RemoteActorRefProvider + * 1b. The terminator is fast enough to enqueue the shutdown command in the remoting + * 2. Only at this point is the Terminated (to be sent remotely) enqueued in the mailbox of remoting + * + * If the remote watchers are notified first, then the mailbox of the Remoting will guarantee the correct order. + */ + foreach (var w in _watchedBy) SendTerminated(false, w); + foreach (var w in _watchedBy) SendTerminated(true, w); + } + finally + { + _watching = new HashSet(); + } + } + + private void SendTerminated(bool ifLocal, ActorRef watcher) + { + if (((ActorRefScope)watcher).IsLocal && !watcher.Equals(Parent)) + { + ((InternalActorRef)watcher).Tell(new DeathWatchNotification(Self, true, false)); + } + } + + protected void UnwatchWatchedActors(ActorBase actor) + { + if (!_watchedBy.Any()) return; + MaintainAddressTerminatedSubscription(() => + { + try + { + foreach ( // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS + var watchee in _watching.OfType()) + watchee.Tell(new Unwatch(watchee, Self)); + } + finally + { + _watching = new HashSet(); + _terminatedQueue = new HashSet(); + } + }); + } + + protected void AddWatcher(ActorRef watchee, ActorRef watcher) + { + var watcheeSelf = watchee.Equals(Self); + var watcherSelf = watcher.Equals(Self); + + if (watcheeSelf && !watcherSelf) + { + if(!_watchedBy.Contains(watcher)) MaintainAddressTerminatedSubscription(() => + { + _watchedBy.Add(watcher); + if(System.Settings.DebugLifecycle) Publish(new Debug(Self.Path.ToString(), Actor.GetType(), string.Format("now watched by {0}", watcher))); + }, watcher); + } + else if (!watcheeSelf && watcherSelf) + { + Watch(watchee); + } + else + { + Publish(new Warning(Self.Path.ToString(), Actor.GetType(), string.Format("BUG: illegal Watch({0},{1} for {2}", watchee, watcher, Self))); + } + } + + protected void RemWatcher(ActorRef watchee, ActorRef watcher) + { + var watcheeSelf = watchee.Equals(Self); + var watcherSelf = watcher.Equals(Self); + + if (watcheeSelf && !watcherSelf) + { + if( _watchedBy.Contains(watcher)) MaintainAddressTerminatedSubscription(() => + { + _watchedBy.Remove(watcher); + if (System.Settings.DebugLifecycle) Publish(new Debug(Self.Path.ToString(), Actor.GetType(), string.Format("no longer watched by {0}", watcher))); + } , watcher); + } + else if (!watcheeSelf && watcherSelf) + { + Unwatch(watchee); + } + else + { + Publish(new Warning(Self.Path.ToString(), Actor.GetType(), string.Format("BUG: illegal Unwatch({0},{1} for {2}", watchee, watcher, Self))); + } + } + + protected void AddressTerminated(Address address) + { + // cleanup watchedBy since we know they are dead + MaintainAddressTerminatedSubscription(() => + { + foreach (var a in _watchedBy.Where(a => a.Path.Address == address)) _watchedBy.Remove(a); + }); + + // send DeathWatchNotification to self for all matching subjects + // that are not child with existenceConfirmed = false because we could have been watching a + // non-local ActorRef that had never resolved before the other node went down + // When a parent is watching a child and it terminates due to AddressTerminated + // it is removed by sending DeathWatchNotification with existenceConfirmed = true to support + // immediate creation of child with same name. + foreach(var a in _watching.Where(a => a.Path.Address == address)) + { + Self.Tell(new DeathWatchNotification(a, true /*TODO: childrenRefs.getByRef(a).isDefined*/, true)); + } + } + + /// + /// Starts subscription to AddressTerminated if not already subscribing and the + /// block adds a non-local ref to watching or watchedBy. + /// Ends subscription to AddressTerminated if subscribing and the + /// block removes the last non-local ref from watching and watchedBy. + /// + private void MaintainAddressTerminatedSubscription(Action block, ActorRef change= null) + { + if (IsNonLocal(change)) + { + var had = HasNonLocalAddress(); + block(); + var has = HasNonLocalAddress(); + if (had && !has) UnsubscribeAddressTerminated(); + else if (!had && has) SubscribeAddressTerminated(); + } + else + { + block(); + } + } + + private static bool IsNonLocal(ActorRef @ref) + { + if (@ref == null) return true; + var a = @ref as InternalActorRef; + if (a != null && !a.IsLocal) return true; + return false; + } + + private bool HasNonLocalAddress() + { + return _watching.Any(IsNonLocal) || _watchedBy.Any(IsNonLocal); + } + + private void UnsubscribeAddressTerminated() + { + AddressTerminatedTopic.Get(System).Unsubscribe(Self); + } + + private void SubscribeAddressTerminated() + { + AddressTerminatedTopic.Get(System).Subscribe(Self); + } + } + + class UndefinedUidActorRef : MinimalActorRef + { + readonly ActorRef _ref; + + public UndefinedUidActorRef(ActorRef @ref) + { + _ref = @ref; + } + + public override ActorPath Path + { + get { return _ref.Path.WithUid(ActorCell.UndefinedUid); } + } + + public override ActorRefProvider Provider + { + get + { + throw new NotImplementedException("UndefinedUidActorRef does not provide"); + } + } + } + + +} diff --git a/src/core/Akka/Actor/ActorCell.DefaultMessages.cs b/src/core/Akka/Actor/ActorCell.DefaultMessages.cs index 96579ea64ab..d0917fa4fcc 100644 --- a/src/core/Akka/Actor/ActorCell.DefaultMessages.cs +++ b/src/core/Akka/Actor/ActorCell.DefaultMessages.cs @@ -19,10 +19,6 @@ public partial class ActorCell { //terminatedqueue should never be used outside the message loop private readonly HashSet terminatedQueue = new HashSet(); - /// - /// The watched by - /// - private readonly BroadcastActorRef WatchedBy = new BroadcastActorRef(); /// /// The is terminating @@ -171,32 +167,6 @@ private void ReceiveSelection(ActorSelectionMessage m) selection.Tell(m.Message, Sender); } - /// - /// Receiveds the terminated. - /// - /// The m. - private void ReceivedTerminated(Terminated m) - { - if (terminatedQueue.Contains(m.ActorRef)) - { - terminatedQueue.Remove(m.ActorRef); - this.ReceiveMessage(m); - } - - ////TODO: we can get here from actors that we just watch, we should not try to stop things if we are not the parent of the actor(?) - //InternalActorRef child = Child(m.ActorRef.Path.Name); - //if (!child.IsNobody()) //this terminated actor is a valid child - //{ - // Stop(child); //unhooks the child from the supervisor container - //} - //if (System.Settings.DebugLifecycle) - // Publish(new Debug(Self.Path.ToString(), Actor.GetType(), - // string.Format("Terminated actor: {0}", m.ActorRef.Path))); - - // - - } - /// /// Systems the invoke. /// @@ -214,12 +184,12 @@ public void SystemInvoke(Envelope envelope) .Match() .With(HandleCompleteFuture) .With(HandleFailed) - .With(WatchedActorTerminated) + .With(m => WatchedActorTerminated(m.Actor, m.ExistenceConfirmed, m.AddressTerminated)) .With(HandleCreate) //TODO: see """final def init(sendSupervise: Boolean, mailboxType: MailboxType): this.type = {""" in dispatch.scala //case Create(failure) ⇒ create(failure) - .With(HandleWatch) - .With(HandleUnwatch) + .With(m => AddWatcher(m.Watchee, m.Watcher)) + .With(m => RemWatcher(m.Watchee, m.Watcher)) .With(FaultRecreate) .With(FaultSuspend) .With(FaultResume) @@ -272,46 +242,6 @@ private void SuspendNonRecursive() Mailbox.Suspend(); } - /// - /// Watcheds the actor terminated. - /// - /// The m. - private void WatchedActorTerminated(DeathWatchNotification m) - { - // AKKA: - // if (watchingContains(actor)) { - // maintainAddressTerminatedSubscription(actor) { - // watching = removeFromSet(actor, watching) - // } - // if (!isTerminating) { - // self.tell(Terminated(actor)(existenceConfirmed, addressTerminated), actor) - // terminatedQueuedFor(actor) - // } - //} - //if (childrenRefs.getByRef(actor).isDefined) handleChildTerminated(actor) - var actor = m.Actor; - if(WatchingContains(actor)) - { - watchees.Remove(actor); - if (!isTerminating) - { - //TODO: what params should be used for the bools? - - Self.Tell(new Terminated(actor,true,false), actor); - TerminatedQueueFor(actor); - } - } - if (children.ContainsKey(actor.Path.Name)) - { - HandleChildTerminated(actor); - } - } - - private bool WatchingContains(ActorRef actor) - { - return watchees.Contains(actor); - } - private void TerminatedQueueFor(ActorRef actorRef) { terminatedQueue.Add(actorRef); @@ -329,33 +259,6 @@ private void HandleChildTerminated(ActorRef actor) //global::System.Diagnostics.Debug.WriteLine("count " + Children.Count()); } - - /* -protected def terminate() { - - // prevent Deadletter(Terminated) messages - unwatchWatchedActors(actor) - - // stop all children, which will turn childrenRefs into TerminatingChildrenContainer (if there are children) - children foreach stop - - val wasTerminating = isTerminating - - if (setChildrenTerminationReason(ChildrenContainer.Termination)) { - if (!wasTerminating) { - // do not process normal messages while waiting for all children to terminate - suspendNonRecursive() - // do not propagate failures during shutdown to the supervisor - setFailed(self) - if (system.settings.DebugLifecycle) publish(Debug(self.path.toString, clazz(actor), "stopping")) - } - } else { - setTerminated() - finishTerminate() - } - } - */ - /// /// Terminates this instance. /// @@ -441,6 +344,7 @@ private void HandleNonFatalOrInterruptedException(Action action) } catch { + //TODO: Hmmm? } } @@ -456,53 +360,10 @@ private void Publish(LogEvent @event) } catch { + //TODO: Hmmm? } } - /// - /// Tries the catch. - /// - /// The action. - private void TryCatch(Action action) - { - try - { - action(); - } - catch - { - } - } - - /// - /// Tells the watchers we died. - /// - private void TellWatchersWeDied() - { - WatchedBy.Tell(new DeathWatchNotification(Self, true, false)); - } - - /// - /// Unwatches the watched actors. - /// - /// The actor base. - private void UnwatchWatchedActors(ActorBase actorBase) - { - try - { - foreach (ActorRef watchee in watchees) - { - watchee.Tell(new Unwatch(watchee, Self)); - } - } - finally - { - watchees.Clear(); - terminatedQueue.Clear(); - } - } - - private void Supervise(ActorRef child, bool async) { //TODO: complete this @@ -698,47 +559,6 @@ private void HandleFailed(Failed m) throw m.Cause; } - /// - /// Handles the watch. - /// - /// The m. - private void HandleWatch(Watch m) - { - var watchee = m.Watchee; //The actor to watch - var watcher = m.Watcher; //The actor that watches - var self = Self; - var watcheeIsSelf = watchee == self; - var watcherIsSelf = watcher == self; - if(watcheeIsSelf && !watcherIsSelf) //Check if someone else is trying to watch us - { - if(WatchedBy.TryAdd(m.Watcher)) - { - //TODO: Missing call to maintainAddressTerminatedSubscription - if(System.Settings.DebugLifecycle) - Publish(new Debug(Self.Path.ToString(), ActorType, "now watched by " + m.Watcher)); - } - } - else if(!watcheeIsSelf && watcherIsSelf) - { - Watch(watchee); - } - else - { - Publish(new Warning(self.Path.ToString(), ActorType, string.Format("BUG: illegal Watch({0},{1}) for {2}", watchee, watcher, self))); - } - } - - //TODO: find out why this is never called - /// - /// Handles the unwatch. - /// - /// The m. - private void HandleUnwatch(Unwatch m) - { - WatchedBy.Remove(m.Watcher); - terminatedQueue.Remove(m.Watchee); - } - /// /// Handles the complete future. /// diff --git a/src/core/Akka/Actor/ActorCell.cs b/src/core/Akka/Actor/ActorCell.cs index 1ac8973658b..6d58989a695 100644 --- a/src/core/Akka/Actor/ActorCell.cs +++ b/src/core/Akka/Actor/ActorCell.cs @@ -23,7 +23,6 @@ public partial class ActorCell : IActorContext, IUntypedActorContext, Cell protected ConcurrentDictionary children = new ConcurrentDictionary(); - protected HashSet watchees = new HashSet(); protected Stack behaviorStack = new Stack(); private long uid; private ActorBase _actor; @@ -173,31 +172,6 @@ void IUntypedActorContext.Become(UntypedReceive receive, bool discardOld) Become(m => { receive(m); return true; }, discardOld); } - /// - /// May only be called from the owner actor - /// - /// - public void Watch(ActorRef watchee) - { - watchees.Add(watchee); - watchee.Tell(new Watch(watchee, Self),Self); - //If watchee is terminated, its mailbox have been replaced by - //DeadLetterMailbox, which will forward the Watch message as a - //DeadLetter to DeadLetterActorRef. It inspects the message inside - //the DeadLetter, sees it is a Watch and sends watcher, i.e. us - //a DeathWatchNotification(watchee) - } - - /// - /// May only be called from the owner actor - /// - /// - public void Unwatch(ActorRef watchee) - { - watchees.Remove(watchee); - watchee.Tell(new Unwatch(watchee, Self)); - } - private InternalActorRef MakeChild(Props props, string name) { long childUid = NewUid(); diff --git a/src/core/Akka/Actor/ActorRef.cs b/src/core/Akka/Actor/ActorRef.cs index 9bfb780f51d..29309b4eea5 100644 --- a/src/core/Akka/Actor/ActorRef.cs +++ b/src/core/Akka/Actor/ActorRef.cs @@ -173,6 +173,24 @@ public static implicit operator ActorRef(ActorRefSurrogate surrogate) { return Serialization.Serialization.CurrentSystem.Provider.ResolveActorRef(surrogate.Path); } + + public override bool Equals(object obj) + { + var other = obj as ActorRef; + if (other == null) return false; + return Path.Uid == other.Path.Uid && Path.Equals(other.Path); + } + + public override int GetHashCode() + { + unchecked + { + var hash = 17; + hash = hash*23 + Path.Uid.GetHashCode(); + hash = hash*23 + Path.GetHashCode(); + return hash; + } + } } @@ -271,6 +289,17 @@ public override ActorRefProvider Provider { get { throw new NotSupportedException("Reserved does not provide"); } } + + public override bool Equals(object obj) + { + if (obj == null) return false; + return obj == Instance; + } + + public override int GetHashCode() + { + return 17; + } } public abstract class ActorRefWithCell : InternalActorRef diff --git a/src/core/Akka/Actor/ActorSystem.cs b/src/core/Akka/Actor/ActorSystem.cs index 16e3bf71621..5816f4a3b51 100644 --- a/src/core/Akka/Actor/ActorSystem.cs +++ b/src/core/Akka/Actor/ActorSystem.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; +using System.Threading; using Akka.Configuration; using Akka.Dispatch; using Akka.Dispatch.SysMsg; @@ -44,24 +45,38 @@ public class ActorSystem : IActorRefFactory, IDisposable /// public LoggingAdapter Log; + /// + /// A wait handle that an Akka.NET client or server can wait on until the actor system is shut down. + /// + /// Designed to make it easy for developers to build applications that can block the main application thread + /// from exiting while the system runs, particularly inside console applications. + /// + private ManualResetEvent _systemWaitHandle; + /// /// The log dead letter listener /// private InternalActorRef _logDeadLetterListener; + public ActorSystem(string name) : this(name,ConfigurationFactory.Load()) + { + } /// /// Initializes a new instance of the class. /// /// The name. /// The configuration. - public ActorSystem(string name, Config config = null) + public ActorSystem(string name, Config config) { if (!Regex.Match(name, "^[a-zA-Z0-9][a-zA-Z0-9-]*$").Success) throw new ArgumentException( "invalid ActorSystem name [" + name + "], must contain only word characters (i.e. [a-zA-Z0-9] plus non-leading '-')"); + if (config == null) + throw new ArgumentNullException("config"); Name = name; + _systemWaitHandle = new ManualResetEvent(false); //unsignaled ConfigureScheduler(); ConfigureSettings(config); ConfigureEventStream(); @@ -224,7 +239,7 @@ public void Dispose() /// ActorSystem. public static ActorSystem Create(string name, Config config) { - return new ActorSystem(name, config.WithFallback(ConfigurationFactory.Default())); + return new ActorSystem(name, config); } /// @@ -423,6 +438,7 @@ private void ConfigureDispatchers() public void Shutdown() { Provider.Guardian.Stop(); + _systemWaitHandle.Set(); //signal that the actorsystem is being shut down } /// @@ -459,6 +475,15 @@ public void Stop(ActorRef actor) ((InternalActorRef) actor).Stop(); } + /// + /// Block and prevent the main application thread from exiting unless + /// the actor system is shut down. + /// + public void WaitForShutdown() + { + _systemWaitHandle.WaitOne(); + } + #region Equality methods #endregion } diff --git a/src/core/Akka/Actor/AutoReceivedMessage.cs b/src/core/Akka/Actor/AutoReceivedMessage.cs index 967537c5e75..9376f8c9d22 100644 --- a/src/core/Akka/Actor/AutoReceivedMessage.cs +++ b/src/core/Akka/Actor/AutoReceivedMessage.cs @@ -7,7 +7,8 @@ public interface AutoReceivedMessage : NoSerializationVerificationNeeded { } - public sealed class Terminated : AutoReceivedMessage, PossiblyHarmful + public sealed class + Terminated : AutoReceivedMessage, PossiblyHarmful { public Terminated(ActorRef actorRef, bool existenceConfirmed, bool addressTerminated) { diff --git a/src/core/Akka/Actor/BroadcastActorRef.cs b/src/core/Akka/Actor/BroadcastActorRef.cs deleted file mode 100644 index b63bb20c1d0..00000000000 --- a/src/core/Akka/Actor/BroadcastActorRef.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Concurrent; -using System.Linq; -using Akka.Dispatch.SysMsg; - -namespace Akka.Actor -{ - public class BroadcastActorRef - { - private readonly ConcurrentDictionary actors = - new ConcurrentDictionary(); - - public BroadcastActorRef(params ActorRef[] actors) - { - foreach (ActorRef a in actors) - this.actors.TryAdd(a, a); - } - - public bool TryAdd(ActorRef actor) - { - return actors.TryAdd(actor, actor); - } - - internal void Remove(ActorRef actor) - { - ActorRef tmp; - actors.TryRemove(actor, out tmp); - } - - public void Tell(object message, ActorRef sender=null) - { - sender = sender ?? ActorRefSender.GetSelfOrNoSender(); - actors.Values.ToList().ForEach(a => a.Tell(message, sender)); - } - } -} \ No newline at end of file diff --git a/src/core/Akka/Actor/IActorContext.cs b/src/core/Akka/Actor/IActorContext.cs index 9a84c40e4ba..b890cb85d78 100644 --- a/src/core/Akka/Actor/IActorContext.cs +++ b/src/core/Akka/Actor/IActorContext.cs @@ -14,8 +14,8 @@ public interface IActorContext : IActorRefFactory void Unbecome(); ActorRef Child(string name); IEnumerable GetChildren(); - void Watch(ActorRef subject); - void Unwatch(ActorRef subject); + ActorRef Watch(ActorRef subject); + ActorRef Unwatch(ActorRef subject); void SetReceiveTimeout(TimeSpan? timeout); /* diff --git a/src/core/Akka/Actor/Settings.cs b/src/core/Akka/Actor/Settings.cs index 2b30373dfc4..9a9d2ec8748 100644 --- a/src/core/Akka/Actor/Settings.cs +++ b/src/core/Akka/Actor/Settings.cs @@ -20,7 +20,7 @@ public Settings(ActorSystem system, Config config) { Config fallback = ConfigurationFactory.Default(); - Config merged = config == null ? fallback : new Config(config, fallback); + Config merged = config == null ? fallback : config.WithFallback(fallback); System = system; Config = merged; diff --git a/src/core/Akka/Akka.csproj b/src/core/Akka/Akka.csproj index aee2e069fec..dd2e1d16896 100644 --- a/src/core/Akka/Akka.csproj +++ b/src/core/Akka/Akka.csproj @@ -68,6 +68,7 @@ Properties\SharedAssemblyInfo.cs + @@ -84,7 +85,6 @@ - @@ -121,6 +121,7 @@ + diff --git a/src/core/Akka/Configuration/Config.cs b/src/core/Akka/Configuration/Config.cs index d2d53d98fa9..8f23e0d7c7c 100644 --- a/src/core/Akka/Configuration/Config.cs +++ b/src/core/Akka/Configuration/Config.cs @@ -7,20 +7,40 @@ namespace Akka.Configuration { public class Config { - private readonly Config _fallback; - private readonly HoconValue _node; + private Config _fallback; + private HoconValue _node; + private IEnumerable _substitutions; + + + protected Config Copy() + { + //deep clone + return new Config() + { + _fallback = _fallback != null ? _fallback.Copy() : null, + _node = _node, + _substitutions = _substitutions + }; + } public Config() { } - public Config(HoconValue node) + public Config(HoconRoot root) { - this._node = node; + if (root.Value == null) + throw new ArgumentNullException("root.Value"); + + this._node = root.Value; + this._substitutions = root.Substitutions; } public Config(Config source, Config fallback) { + if (source == null) + throw new ArgumentNullException("source"); + _node = source._node; this._fallback = fallback; } @@ -41,10 +61,7 @@ private HoconValue GetNode(string path) HoconValue currentNode = _node; if (currentNode == null) { - if (_fallback != null) - return _fallback.GetNode(path); - - return null; + throw new Exception("Current node should not be null"); } foreach (string key in elements) { @@ -185,10 +202,18 @@ public Config GetConfig(string path) if (_fallback != null) { Config f = _fallback.GetConfig(path); - return new Config(new Config(value), f); + if (value == null && f == null) + return null; + if (value == null) + return f; + + return new Config(new HoconRoot(value)).WithFallback(f); } - return new Config(value); + if (value == null) + return null; + + return new Config(new HoconRoot(value)); } public HoconValue GetValue(string path) @@ -208,12 +233,27 @@ public TimeSpan GetMillisDuration(string path, TimeSpan? @default = null) public override string ToString() { + if (_node == null) + return ""; + return _node.ToString(); } public Config WithFallback(Config fallback) { - return new Config(this, fallback); + if (fallback == this) + throw new ArgumentException("Config can not have itself as fallback", "fallback"); + + var clone = this.Copy(); + + var current = clone; + while(current._fallback != null) + { + current = current._fallback; + } + current._fallback = fallback; + + return clone; } diff --git a/src/core/Akka/Configuration/ConfigurationFactory.cs b/src/core/Akka/Configuration/ConfigurationFactory.cs index f4581f3438c..1096fc11705 100644 --- a/src/core/Akka/Configuration/ConfigurationFactory.cs +++ b/src/core/Akka/Configuration/ConfigurationFactory.cs @@ -26,7 +26,7 @@ public static Config Empty /// Config. public static Config ParseString(string hocon) { - HoconValue res = Parser.Parse(hocon); + HoconRoot res = Parser.Parse(hocon); return new Config(res); } @@ -36,7 +36,10 @@ public static Config ParseString(string hocon) /// Config. public static Config Load() { - return ParseString(""); + var section = new AkkaConfigurationSection(); + var config = section.AkkaConfig; + + return config; } /// diff --git a/src/core/Akka/Configuration/Hocon/HoconParser.cs b/src/core/Akka/Configuration/Hocon/HoconParser.cs index 2b90c104ca8..5aa70602513 100644 --- a/src/core/Akka/Configuration/Hocon/HoconParser.cs +++ b/src/core/Akka/Configuration/Hocon/HoconParser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Akka.Configuration.Hocon { @@ -26,7 +27,7 @@ public class Parser /// /// The text. /// HoconValue. - public static HoconValue Parse(string text) + public static HoconRoot Parse(string text) { return new Parser().ParseText(text); } @@ -37,14 +38,14 @@ public static HoconValue Parse(string text) /// The text. /// HoconValue. /// Unresolved substitution: + sub.Path - private HoconValue ParseText(string text) + private HoconRoot ParseText(string text) { root = new HoconValue(); reader = new HoconTokenizer(text); reader.PullWhitespaceAndComments(); ParseObject(root, true); - var c = new Config(root); + var c = new Config(new HoconRoot(root,Enumerable.Empty())); foreach (HoconSubstitution sub in substitutions) { HoconValue res = c.GetValue(sub.Path); @@ -52,7 +53,7 @@ private HoconValue ParseText(string text) throw new Exception("Unresolved substitution:" + sub.Path); sub.ResolvedValue = res; } - return root; + return new HoconRoot(root, substitutions); } /// diff --git a/src/core/Akka/Configuration/Hocon/HoconRoot.cs b/src/core/Akka/Configuration/Hocon/HoconRoot.cs new file mode 100644 index 00000000000..c7fd64b8574 --- /dev/null +++ b/src/core/Akka/Configuration/Hocon/HoconRoot.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Akka.Configuration.Hocon +{ + public class HoconRoot + { + public HoconRoot(HoconValue value, IEnumerable substitutions) + { + Value = value; + Substitutions = Substitutions; + } + + public HoconRoot(HoconValue value) + { + this.Value = value; + this.Substitutions = Enumerable.Empty(); + } + public HoconValue Value { get;private set; } + public IEnumerable Substitutions { get;private set; } + } +} diff --git a/src/core/Akka/Configuration/Hocon/HoconValue.cs b/src/core/Akka/Configuration/Hocon/HoconValue.cs index b9ac338de7f..40ae94c3732 100644 --- a/src/core/Akka/Configuration/Hocon/HoconValue.cs +++ b/src/core/Akka/Configuration/Hocon/HoconValue.cs @@ -246,6 +246,9 @@ public virtual string ToString(int indent) private string QuoteIfNeeded(string text) { + if (text == null) + return null; + if (text.ToCharArray().Intersect(" \t".ToCharArray()).Any()) { return "\"" + text + "\""; diff --git a/src/core/Akka/Configuration/Pigeon.conf b/src/core/Akka/Configuration/Pigeon.conf index ad4254bcf74..4b719545689 100644 --- a/src/core/Akka/Configuration/Pigeon.conf +++ b/src/core/Akka/Configuration/Pigeon.conf @@ -697,7 +697,7 @@ akka { system-message-ack-piggyback-timeout = 0.3 s resend-interval = 2 s initial-system-message-delivery-timeout = 3 m - enabled-transports = ["akka.remote.helios.tcp","akka.remote.test-transport"] + enabled-transports = ["akka.remote.helios.tcp"] adapters { gremlin = "akka.remote.transport.FailureInjectorProvider" trttl = "akka.remote.transport.ThrottlerProvider" @@ -709,13 +709,13 @@ akka { port = 2552 hostname = "" } - test-transport { - transport-class = "Akka.Remote.Transport.TestTransport,Akka.Remote" - applied-adapters = [] - transport-protocol = test - port = null - hostname = "" - } +# test-transport { +# transport-class = "Akka.Remote.Transport.TestTransport,Akka.Remote" +# applied-adapters = [] +# transport-protocol = test +# port = null +# hostname = "" +# } default-remote-dispatcher { type = Dispatcher diff --git a/src/core/Akka/Event/AddressTerminatedTopic.cs b/src/core/Akka/Event/AddressTerminatedTopic.cs index 2c5cf91a2dc..ef4c1a53f9e 100644 --- a/src/core/Akka/Event/AddressTerminatedTopic.cs +++ b/src/core/Akka/Event/AddressTerminatedTopic.cs @@ -11,10 +11,15 @@ namespace Akka.Event /// notifications. Remote and cluster death watchers /// publish when a remote system is deemed dead. /// - internal sealed class AddressTerminatedTopic + internal sealed class AddressTerminatedTopic : IExtension { private readonly AtomicReference> _subscribers = new AtomicReference>(); + public static AddressTerminatedTopic Get(ActorSystem system) + { + return system.WithExtension(); + } + public void Subscribe(ActorRef subscriber) { while (true) diff --git a/src/core/Akka/Util/ConcurrentSet.cs b/src/core/Akka/Util/ConcurrentSet.cs index f89b926180a..7e25c0facb7 100644 --- a/src/core/Akka/Util/ConcurrentSet.cs +++ b/src/core/Akka/Util/ConcurrentSet.cs @@ -46,7 +46,7 @@ public ConcurrentSet(int concurrencyLevel, int capacity, IEqualityComparer co storage = new ConcurrentDictionary(concurrencyLevel, capacity, comparer); } - public bool IsEmptry + public bool IsEmpty { get { return storage.IsEmpty; } } diff --git a/src/examples/Chat/ChatServer/Program.cs b/src/examples/Chat/ChatServer/Program.cs index f73ba3e0362..7383cc4098a 100644 --- a/src/examples/Chat/ChatServer/Program.cs +++ b/src/examples/Chat/ChatServer/Program.cs @@ -1,13 +1,8 @@ using ChatMessages; using Akka; using Akka.Actor; -using Akka.Configuration; -using Akka.Remote; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Akka.Event; namespace ChatServer @@ -89,7 +84,7 @@ class ChatServerActor : TypedActor , ILogReceive { - private BroadcastActorRef clients = new BroadcastActorRef(); + private readonly HashSet _clients = new HashSet(); public void Handle(SayRequest message) { @@ -99,13 +94,13 @@ public void Handle(SayRequest message) Username = message.Username, Text = message.Text, }; - clients.Tell(response, Self); + foreach (var client in _clients) client.Tell(response, Self); } public void Handle(ConnectRequest message) { // Console.WriteLine("User {0} has connected", message.Username); - clients.TryAdd(this.Sender); + _clients.Add(this.Sender); Sender.Tell(new ConnectResponse { Message = "Hello and welcome to Akka .NET chat example", @@ -120,7 +115,7 @@ public void Handle(NickRequest message) NewUsername = message.NewUsername, }; - clients.Tell(response, Self); + foreach (var client in _clients) client.Tell(response, Self); } public void Handle(Disconnect message) diff --git a/src/examples/TimeServer/TimeClient/TimeClient.csproj b/src/examples/TimeServer/TimeClient/TimeClient.csproj index 954c1997eeb..76a80addb24 100644 --- a/src/examples/TimeServer/TimeClient/TimeClient.csproj +++ b/src/examples/TimeServer/TimeClient/TimeClient.csproj @@ -34,9 +34,9 @@ 4 - + False - ..\..\..\packages\Helios.1.3.0.0\lib\net45\Helios.dll + ..\..\..\packages\Helios.1.3.4.0\lib\net45\Helios.dll diff --git a/src/examples/TimeServer/TimeClient/packages.config b/src/examples/TimeServer/TimeClient/packages.config index 9d2dbce6b1f..9a7439311c9 100644 --- a/src/examples/TimeServer/TimeClient/packages.config +++ b/src/examples/TimeServer/TimeClient/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file