From a18c246f45d1080da7dd666ec073262eabf40f70 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Fri, 18 Mar 2022 11:18:55 -0500 Subject: [PATCH 01/17] v1.4.36 placeholder for nightlies (#5732) --- RELEASE_NOTES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1b446cc03ac..f9567a56d25 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,7 @@ +#### 1.4.36 March 18 2022 #### + +**Placeholder for nightlies** + #### 1.4.35 March 18 2022 #### Akka.NET v1.4.35 is a minor release that contains some bug fixes. Most of the changes have been aimed at improving our web documentation and code cleanup to modernize some of our code. From 5ef69e9a01cb8d6e7916a8ce6df0f98cde95006d Mon Sep 17 00:00:00 2001 From: Adrian Leonhard Date: Mon, 21 Mar 2022 13:18:00 +0100 Subject: [PATCH 02/17] Update PersistAsync to match docs. (#5736) This is supposed to be the example for persistAsync, but it doesn't get used at all. https://getakka.net/articles/persistence/event-sourcing.html#relaxed-local-consistency-requirements-and-high-throughput-use-cases I changed the code to match what is here: https://doc.akka.io/docs/akka/current/persistence.html#relaxed-local-consistency-requirements-and-high-throughput-use-cases --- .../Persistence/PersistentActor/PersistAsync.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/Akka.Docs.Tests/Persistence/PersistentActor/PersistAsync.cs b/src/core/Akka.Docs.Tests/Persistence/PersistentActor/PersistAsync.cs index df5d6340ba6..9654dc43525 100644 --- a/src/core/Akka.Docs.Tests/Persistence/PersistentActor/PersistAsync.cs +++ b/src/core/Akka.Docs.Tests/Persistence/PersistentActor/PersistAsync.cs @@ -27,9 +27,8 @@ protected override void OnCommand(object message) if (message is string c) { Sender.Tell(c); - Persist($"evt-{c}-1", e => Sender.Tell(e)); - Persist($"evt-{c}-2", e => Sender.Tell(e)); - DeferAsync($"evt-{c}-3", e => Sender.Tell(e)); + PersistAsync($"evt-{c}-1", e => Sender.Tell(e)); + PersistAsync($"evt-{c}-2", e => Sender.Tell(e)); } } } From ef6718e99b24f665dfafbac86b213d3759f7cd67 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Tue, 22 Mar 2022 05:13:36 +0700 Subject: [PATCH 03/17] [DI] DI fails to throw an exception when DI tried to create an actor with missing constructor parameter (#5735) * Add bug spec for DI bug * Fix unit test to reflect the correct failure behaviour --- .../BugFixSpec.cs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/BugFixSpec.cs diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/BugFixSpec.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/BugFixSpec.cs new file mode 100644 index 00000000000..b08838bd1f5 --- /dev/null +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/BugFixSpec.cs @@ -0,0 +1,101 @@ +// //----------------------------------------------------------------------- +// // +// // Copyright (C) 2009-2022 Lightbend Inc. +// // Copyright (C) 2013-2022 .NET Foundation +// // +// //----------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Event; +using Akka.TestKit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using Xunit.Abstractions; +using FluentAssertions; +using static FluentAssertions.FluentActions; + +namespace Akka.DependencyInjection.Tests +{ + public class BugFixSpec: AkkaSpec, IAsyncLifetime + { + private readonly IServiceProvider _serviceProvider; + private readonly AkkaService _akkaService; + + public BugFixSpec(ITestOutputHelper output) : base(output) + { + var services = new ServiceCollection() + .AddSingleton() + .AddHostedService(); + + _serviceProvider = services.BuildServiceProvider(); + _akkaService = _serviceProvider.GetRequiredService(); + } + + [Fact(DisplayName = "DI should log an error if DI provider does not contain required parameter")] + public void ShouldLogAnErrorIfParameterInjectionFailed() + { + var system = _serviceProvider.GetRequiredService().ActorSystem; + var probe = CreateTestProbe(system); + system.EventStream.Subscribe(probe, typeof(Error)); + + var props = DependencyResolver.For(system).Props(); + var actor = system.ActorOf(props.WithDeploy(Deploy.Local), "testDIActor"); + + probe.ExpectMsg().Cause.Should().BeOfType(); + } + + internal class TestDiActor : ReceiveActor + { + public TestDiActor(NotInServices doesNotExistInDi) + { + } + } + + internal class NotInServices + { + } + + + public async Task InitializeAsync() + { + await _akkaService.StartAsync(default); + InitializeLogger(_akkaService.ActorSystem); + } + + public async Task DisposeAsync() + { + var sys = _serviceProvider.GetRequiredService().ActorSystem; + await sys.Terminate(); + } + + internal class AkkaService : IHostedService + { + public ActorSystem ActorSystem { get; private set; } + + private readonly IServiceProvider _serviceProvider; + + public AkkaService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + var setup = DependencyResolverSetup.Create(_serviceProvider) + .And(BootstrapSetup.Create().WithConfig(TestKitBase.DefaultConfig)); + + ActorSystem = ActorSystem.Create("TestSystem", setup); + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await ActorSystem.Terminate(); + } + } + } +} \ No newline at end of file From 4e35c2a5db1b10773467ad0c7041c5801de5e918 Mon Sep 17 00:00:00 2001 From: Ebere Abanonu Date: Tue, 22 Mar 2022 15:46:10 +0100 Subject: [PATCH 04/17] Fix spelling (#5745) --- docs/articles/intro/getting-started/tutorial-1.md | 2 +- docs/articles/persistence/persistence-query.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/articles/intro/getting-started/tutorial-1.md b/docs/articles/intro/getting-started/tutorial-1.md index e5f7bfb3857..ce899f4840a 100644 --- a/docs/articles/intro/getting-started/tutorial-1.md +++ b/docs/articles/intro/getting-started/tutorial-1.md @@ -45,7 +45,7 @@ _supervising_ every actor living as a child of them, i.e. under their path. We will explain supervision in more detail, all you need to know now is that every unhandled failure from actors bubbles up to their parent that, in turn, can decide how to handle this failure. These predefined actors are guardians in the -sense that they are the final lines of defence, where all unhandled failures +sense that they are the final lines of defense, where all unhandled failures from user, or system, actors end up. > Does the root guardian (the root path `/`) have a parent? As it turns out, it diff --git a/docs/articles/persistence/persistence-query.md b/docs/articles/persistence/persistence-query.md index 192842a8506..cb9759bd285 100644 --- a/docs/articles/persistence/persistence-query.md +++ b/docs/articles/persistence/persistence-query.md @@ -249,11 +249,11 @@ query ## Performance and Denormalization -When building systems using Event sourcing and CQRS ([Command & Query Responsibility Segregation](https://msdn.microsoft.com/en-us/library/jj554200.aspx)) techniques it is tremendously important to realise that the write-side has completely different needs from the read-side, and separating those concerns into datastores that are optimized for either side makes it possible to offer the best experience for the write and read sides independently. +When building systems using Event sourcing and CQRS ([Command & Query Responsibility Segregation](https://msdn.microsoft.com/en-us/library/jj554200.aspx)) techniques it is tremendously important to realize that the write-side has completely different needs from the read-side, and separating those concerns into datastores that are optimized for either side makes it possible to offer the best experience for the write and read sides independently. For example, in a bidding system it is important to "take the write" and respond to the bidder that we have accepted the bid as soon as possible, which means that write-throughput is of highest importance for the write-side – often this means that data stores which are able to scale to accommodate these requirements have a less expressive query side. -On the other hand the same application may have some complex statistics view or we may have analysts working with the data to figure out best bidding strategies and trends – this often requires some kind of expressive query capabilities like for example SQL or writing Spark jobs to analyse the data. Therefore the data stored in the write-side needs to be projected into the other read-optimized datastore. +On the other hand the same application may have some complex statistics view or we may have analysts working with the data to figure out best bidding strategies and trends – this often requires some kind of expressive query capabilities like for example SQL or writing Spark jobs to analyze the data. Therefore the data stored in the write-side needs to be projected into the other read-optimized datastore. > [!NOTE] > When referring to Materialized Views in Akka Persistence think of it as "some persistent storage of the result of a Query". In other words, it means that the view is created once, in order to be afterwards queried multiple times, as in this format it may be more efficient or interesting to query it (instead of the source events directly). From 93a3d7e7ca1e7452a1e310e2133921c90e231e69 Mon Sep 17 00:00:00 2001 From: Ebere Abanonu Date: Tue, 22 Mar 2022 16:06:59 +0100 Subject: [PATCH 05/17] Lock cspell on version `5.18.5` (#5744) * Lock cspell on version `5.18.5` * Use version `5.17.0` Co-authored-by: Aaron Stannard --- build-system/pr-validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-system/pr-validation.yaml b/build-system/pr-validation.yaml index 315e664b808..5a1d268aa74 100644 --- a/build-system/pr-validation.yaml +++ b/build-system/pr-validation.yaml @@ -30,7 +30,7 @@ jobs: - task: Npm@1 inputs: command: "custom" - customCommand: "install -g cspell" + customCommand: "install -g cspell@5.17.0" - task: CmdLine@2 inputs: script: 'cspell --config ./docs/cSpell.json "docs/**/*.md"' From b4b390d0933164054c8e5de480a62fa69e0fe955 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 22 Mar 2022 10:57:04 -0500 Subject: [PATCH 06/17] cleanup XUnit TestKit output logger (#5741) --- src/contrib/testkits/Akka.TestKit.Xunit2/Internals/Loggers.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/Loggers.cs b/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/Loggers.cs index cfefe9758de..accd94f94f4 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/Loggers.cs +++ b/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/Loggers.cs @@ -8,6 +8,7 @@ using System; using Akka.Actor; using Akka.Event; +using Akka.Util; using Xunit.Abstractions; namespace Akka.TestKit.Xunit2.Internals @@ -58,7 +59,8 @@ private void HandleLogEvent(LogEvent e) } catch (InvalidOperationException ie) { - Console.WriteLine($"Received InvalidOperationException: {ie} - probably because the test had completed executing."); + StandardOutWriter.WriteLine($"Received InvalidOperationException: {ie} - probably because the test had completed executing."); + Context.Stop(Self); // shut ourselves down, can't do our job any longer } } } From 6d3d91f751eb0cb7957bfb6fe71405070b575558 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 22 Mar 2022 12:10:11 -0500 Subject: [PATCH 07/17] marked `EventBusUnsubscriber` messages as `INoSerializationVerificationNeeded` (#5743) Eliminates issues with some `MinimalActorRef` actors subscribing to the `EventStream` when `akka.actor.serialize-messages = on`. --- src/core/Akka/Event/EventBusUnsubscriber.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/Akka/Event/EventBusUnsubscriber.cs b/src/core/Akka/Event/EventBusUnsubscriber.cs index bf865c879bd..f4724a72c2f 100644 --- a/src/core/Akka/Event/EventBusUnsubscriber.cs +++ b/src/core/Akka/Event/EventBusUnsubscriber.cs @@ -90,7 +90,7 @@ protected override void PreStart() /// /// Registers a new subscriber to be death-watched and automatically unsubscribed. /// - internal class Register + internal class Register : INoSerializationVerificationNeeded { public Register(IActorRef actor) { @@ -108,7 +108,7 @@ public Register(IActorRef actor) /// /// Unsubscribes an actor that is no longer subscribed and does not need to be death-watched any longer. /// - internal class UnregisterIfNoMoreSubscribedChannels + internal class UnregisterIfNoMoreSubscribedChannels : INoSerializationVerificationNeeded { public UnregisterIfNoMoreSubscribedChannels(IActorRef actor) { From 0c85653f6c4e24e687a8bbc50953ae64b767d3cc Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Wed, 23 Mar 2022 19:56:34 +0700 Subject: [PATCH 08/17] Fix MSBuild does not copy xunit.runner dlls correctly (#5747) --- src/common.props | 1 + .../Akka.Cluster.Metrics.Tests.MultiNode.csproj | 4 ++++ .../Akka.Cluster.Sharding.Tests.MultiNode.csproj | 8 ++++++-- .../Akka.Cluster.Tools.Tests.MultiNode.csproj | 8 ++++++-- .../Akka.DistributedData.Tests.MultiNode.csproj | 8 ++++++-- .../Akka.Cluster.Tests.MultiNode.csproj | 8 ++++++-- .../Akka.Remote.Tests.MultiNode.csproj | 4 ++++ 7 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/common.props b/src/common.props index 7e23057b9b7..9f3fc117438 100644 --- a/src/common.props +++ b/src/common.props @@ -11,6 +11,7 @@ 2.4.1 + 2.4.3 17.0.0 0.12.0 [12.0.3,) diff --git a/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Akka.Cluster.Metrics.Tests.MultiNode.csproj b/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Akka.Cluster.Metrics.Tests.MultiNode.csproj index d2ec0c471c0..70ff3acc750 100644 --- a/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Akka.Cluster.Metrics.Tests.MultiNode.csproj +++ b/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Akka.Cluster.Metrics.Tests.MultiNode.csproj @@ -16,6 +16,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/Akka.Cluster.Sharding.Tests.MultiNode.csproj b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/Akka.Cluster.Sharding.Tests.MultiNode.csproj index 68959b328df..0bbb5d93da2 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/Akka.Cluster.Sharding.Tests.MultiNode.csproj +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/Akka.Cluster.Sharding.Tests.MultiNode.csproj @@ -15,9 +15,13 @@ - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Akka.Cluster.Tools.Tests.MultiNode.csproj b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Akka.Cluster.Tools.Tests.MultiNode.csproj index d0f9e768f29..26b4534b14c 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Akka.Cluster.Tools.Tests.MultiNode.csproj +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Akka.Cluster.Tools.Tests.MultiNode.csproj @@ -14,9 +14,13 @@ - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/Akka.DistributedData.Tests.MultiNode.csproj b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/Akka.DistributedData.Tests.MultiNode.csproj index cfdb48fe75c..18f474d7123 100644 --- a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/Akka.DistributedData.Tests.MultiNode.csproj +++ b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/Akka.DistributedData.Tests.MultiNode.csproj @@ -14,9 +14,13 @@ - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj b/src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj index 38652c8d6cc..88107065bcd 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj +++ b/src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj @@ -14,9 +14,13 @@ - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/core/Akka.Remote.Tests.MultiNode/Akka.Remote.Tests.MultiNode.csproj b/src/core/Akka.Remote.Tests.MultiNode/Akka.Remote.Tests.MultiNode.csproj index 14b3c52a0ff..917ee38dca9 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/Akka.Remote.Tests.MultiNode.csproj +++ b/src/core/Akka.Remote.Tests.MultiNode/Akka.Remote.Tests.MultiNode.csproj @@ -15,6 +15,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 9e0589453db2de973f63c7b48d3b8aa0f3134deb Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Thu, 24 Mar 2022 21:43:45 +0700 Subject: [PATCH 09/17] Add documentation on how to override serializer ids. (#5749) --- docs/articles/serialization/serialization.md | 37 +++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/articles/serialization/serialization.md b/docs/articles/serialization/serialization.md index 464478a783e..b42e8d7656c 100644 --- a/docs/articles/serialization/serialization.md +++ b/docs/articles/serialization/serialization.md @@ -186,6 +186,41 @@ The only thing left to do for this class would be to fill in the serialization l Afterwards the configuration would need to be updated to reflect which name to bind to and the classes that use this serializer. +### Overriding Default Serializer Ids + +> [!WARNING] +> Changing the identification does not change the serialization binding, it only change the identification used by the serializer when it serialize a message. In fact, depending on the code, this might actually break the serializer as it might expect a specific identifier. You have been warned. + +Generally, overriding a default serializer identification is not recommended. The more recommended way is to change the serialization binding as was done when we [replace the default serializer with Hyperion](xref:serialization#how-to-setup-hyperion-as-the-default-serializer). + +In the rare case where you do need to override them, you can do it in one of two ways: + +* Overriding the `Identifier` property in your custom serializer class that inherits from the `Akka.Serialization.Serializer` abstract class. The `Identifier` property will override any HOCON settings; in fact, you will get a warning in your log when you do that, reminding you that you actually did that. +* Overriding the identifier inside your HOCON settings. To do this, you have to both change the original default serializer id and declare your own serializer using the original default serializer id. + +```c# + serialization-identifiers { + "Akka.Serialization.NewtonSoftJsonSerializer, Akka" : 1000001 + "MyAssembly.MyDefaultSerializer, SomeAssembly" : 1 +} + ``` + +this would in effect results this final HOCON settings: + +```c# +serialization-identifiers : { + "Akka.Serialization.ByteArraySerializer, Akka" : 4 + "Akka.Serialization.NewtonSoftJsonSerializer, Akka" : 1000001 + "Akka.Remote.Serialization.ProtobufSerializer, Akka.Remote" : 2 + "Akka.Remote.Serialization.DaemonMsgCreateSerializer, Akka.Remote" : 3 + "Akka.Remote.Serialization.MessageContainerSerializer, Akka.Remote" : 6 + "Akka.Remote.Serialization.MiscMessageSerializer, Akka.Remote" : 16 + "Akka.Remote.Serialization.PrimitiveSerializers, Akka.Remote" : 17 + "Akka.Remote.Serialization.SystemMessageSerializer, Akka.Remote" : 22 + "MyAssembly.MyDefaultSerializer, SomeAssembly" : 1 +} +``` + ### Programmatically Change NewtonSoft JSON Serializer Settings You can change the JSON serializer behavior by using the `NewtonSoftJsonSerializerSetup` class to programmatically @@ -354,7 +389,7 @@ from being deserialized: * `System.Diagnostics.Process` * `System.Management.IWbemClassObjectFreeThreaded` -Be warned that these class can be used as a man in the middle attack vector, but if you need +Be warned that these class can be used as a man in the middle and arbitrary code injection attack vector, but if you need to serialize one of these class, you can turn off this feature using this inside your HOCON settings: ```hocon From b365c06974c505186e3fad7a0f1003321bc8afb2 Mon Sep 17 00:00:00 2001 From: Ebere Abanonu Date: Thu, 24 Mar 2022 19:31:48 +0100 Subject: [PATCH 10/17] Add `Member Roles` doc (#5742) * Add `Node Roles` doc Co-authored-by: Aaron Stannard --- docs/articles/clustering/cluster-overview.md | 1 + docs/articles/clustering/member-roles.md | 317 +++++++++++++++++++ docs/articles/clustering/toc.yml | 2 + docs/images/cluster/cluster-roles.png | Bin 0 -> 61801 bytes 4 files changed, 320 insertions(+) create mode 100644 docs/articles/clustering/member-roles.md create mode 100644 docs/images/cluster/cluster-roles.png diff --git a/docs/articles/clustering/cluster-overview.md b/docs/articles/clustering/cluster-overview.md index a022b6a1ecd..d2c40929a9c 100644 --- a/docs/articles/clustering/cluster-overview.md +++ b/docs/articles/clustering/cluster-overview.md @@ -236,6 +236,7 @@ A node might also exit the cluster gracefully, preventing it from being marked a +* [Cluster Member Roles](xref:member-roles) * [How to Create Scalable Clustered Akka.NET Apps Using Akka.Cluster](https://petabridge.com/blog/intro-to-akka-cluster/) * [Video: Introduction to Akka.Cluster](https://www.youtube.com/watch?v=mUTKvGyxbOA) * [Gossip Protocol](https://en.wikipedia.org/wiki/Gossip_protocol) diff --git a/docs/articles/clustering/member-roles.md b/docs/articles/clustering/member-roles.md new file mode 100644 index 00000000000..80bf89785ec --- /dev/null +++ b/docs/articles/clustering/member-roles.md @@ -0,0 +1,317 @@ +--- +uid: member-roles +title: Member Roles +--- + +# Member Roles + +![cluster roles](/images/cluster/cluster-roles.png) + +A cluster can have multiple nodes(machines/servers/vms) with different capabilities. +When you require an application to run on a node(machine/server/vm) with certain capabilities, roles helps you to distinguish the nodes so that application can be deployed on that node. +Specifying cluster role(s) is a best practice; you don't want an application that requires less computational power running and consuming resources meant for a mission-critical and resource-intensive application. +Even if you only have a single type of node in your cluster, you should still use roles for it so you can leverage this infrastructure as your cluster expands in the future; and they add zero overhead in any conceivable way. + +# How To Configure Cluster Roles + +Below I will show you how the cluster above can be reproduced. I will create a five-nodes(ActorSystems - all having same name, though, but living on different machine/server/vm) cluster with different roles applied: + +**Node1**: All of my code that receives requests from users and push same to the cluster will be deployed here! + +```hocon +akka +{ + cluster + { + roles = ["web"] + } +} +``` + +**Node2**: All of my code handling fraud detections will be deployed on this node + +```hocon +akka +{ + cluster + { + roles = ["fraud"] + } +} +``` + +**Node3**: All me code that retrieves, stores data will be deployed on this node + +```hocon +akka +{ + cluster + { + roles = ["storage"] + } +} +``` + +**Node4**: All my code that handles customer orders will be deployed on this node + +```hocon +akka +{ + cluster + { + roles = ["order"] + } +} +``` + +**Node5**: All my code that handles customer billing will be deployed on this node + +```hocon +akka +{ + cluster + { + roles = ["billing"] + } +} +``` + +Now that we have laid the foundation for what is to follow, Akka.Cluster is made of various ready-made extensions(or modules) you can deploy. +I will show you how you can deploy them on any of the nodes. Apart from the Akka.Cluster modules, if you just want to use the Akka.Cluster core, I will show you how you can deploy your own actor to the cluster node with the required role: + +**Cluster Sharding**: Sharding be will deployed on the nodes with the `order` role, `node4` + +```hocon +akka +{ + cluster + { + roles = ["order"] + sharding + { + role = "order" + } + } +} +``` + +**Distributed Pub-Sub**: DistributedPubSub will be deployed on the nodes with the `web` role, `node1`. + +```hocon +akka +{ + cluster + { + roles = ["web"] + pub-sub + { + role = "web" + } + } +} +``` + +**Distributed Data**: DistributedData will be deployed on the node with the `storage` role, `node3`. + +```hocon +akka +{ + cluster + { + roles = ["storage"] + distributed-data + { + role = "storage" + } + } +} +``` + +**Cluster Singleton**: To avoid over charging a customer more than once, my code will be deployed with `ClusterSingleton` on the node with the `billing` role, `node5` + +```hocon +akka +{ + cluster + { + roles = ["billing"] + singleton + { + role = "billing" + } + } +} +``` + +I have one more node, `node2`, with nothing running in it. I will deploy my custom fraud detection code there, and the way to do that is: + +```csharp +var selfMember = Cluster.Get(_actorSystem).SelfMember; +if (selfMember.HasRole("fraud")) +{ + context.ActorOf(Billing.Prop(), "bill-gate"); +} +else +{ + //sleep, probably! +} +``` + +Using the Cluster `SelfMember`, I am checking if the current node has the `billing` role and if yes, create the `Billing` actor. + +## Cluster-Aware Router + +Cluster-Aware routers automate how actors are deployed on the cluster and also how messages are routed based on the role specified! Routers in Akka.NET can be either grouped or pooled and you can read up on them [Routers](https://getakka.net/articles/actors/routers.html) + +**Router Group**: I will create Cluster-Aware Router Group for all my applications above! + +```hocon +akka +{ + actor + { + provider = "Akka.Cluster.ClusterActorRefProvider, Akka.Cluster" + deployment + { + /webdispatcher + { + router = consistent-hashing-group # routing strategy + routees.paths = ["/user/web"] # path of routee on each node + nr-of-instances = 3 # max number of total routees + cluster + { + enabled = on + use-role = "web" + } + } + /frauddispatcher + { + router = consistent-hashing-group # routing strategy + routees.paths = ["/user/fraud"] # path of routee on each node + nr-of-instances = 3 # max number of total routees + cluster + { + enabled = on + use-role = "fraud" + } + } + /billingdispatcher + { + router = consistent-hashing-group # routing strategy + routees.paths = ["/user/billing"] # path of routee on each node + nr-of-instances = 3 # max number of total routees + cluster + { + enabled = on + use-role = "billing" + } + } + /orderdispatcher + { + router = consistent-hashing-group # routing strategy + routees.paths = ["/user/order"] # path of routee on each node + nr-of-instances = 3 # max number of total routees + cluster + { + enabled = on + use-role = "order" + } + } + /storagedispatcher + { + router = consistent-hashing-group # routing strategy + routees.paths = ["/user/storage"] # path of routee on each node + nr-of-instances = 3 # max number of total routees + cluster + { + enabled = on + use-role = "storage" + } + } + } + } +} +``` + +```csharp +var web = system.ActorOf("web"); +var fraud = system.ActorOf("fraud"); +var order = system.ActorOf("order"); +var billing = system.ActorOf("billing"); +var storage = system.ActorOf("storage"); + +var webRouter = system.ActorOf(Props.Empty.WithRouter(FromConfig.Instance),"webdispatcher"); +var fraudRouter = system.ActorOf(Props.Empty.WithRouter(FromConfig.Instance),"frauddispatcher"); +var orderRouter = system.ActorOf(Props.Empty.WithRouter(FromConfig.Instance),"orderdispatcher"); +var billingRouter = system.ActorOf(Props.Empty.WithRouter(FromConfig.Instance),"billingispatcher"); +var storageRouter = system.ActorOf(Props.Empty.WithRouter(FromConfig.Instance),"storagedispatcher"); +``` + +**Router Pool**: I will create Cluster-Aware Router Pool for all my applications above! + +```hocon +akka +{ + actor + { + provider = "Akka.Cluster.ClusterActorRefProvider, Akka.Cluster" + deployment + { + /webdispatcher + { + router = round-robin-pool # routing strategy + max-nr-of-instances-per-node = 5 + cluster + { + enabled = on + use-role = "web" + } + } + /frauddispatcher + { + router = round-robin-pool # routing strategy + max-nr-of-instances-per-node = 5 + cluster + { + enabled = on + use-role = "fraud" + } + } + /billingdispatcher + { + router = round-robin-pool # routing strategy + max-nr-of-instances-per-node = 5 + cluster + { + enabled = on + use-role = "billing" + } + } + /orderdispatcher + { + router = round-robin-pool # routing strategy + max-nr-of-instances-per-node = 5 + cluster + { + enabled = on + use-role = "order" + } + } + /storagedispatcher + { + router = round-robin-pool # routing strategy + max-nr-of-instances-per-node = 5 + cluster + { + enabled = on + use-role = "storage" + } + } + } + } +} +``` + +```csharp +var routerProps = Props.Empty.WithRouter(FromConfig.Instance); +``` diff --git a/docs/articles/clustering/toc.yml b/docs/articles/clustering/toc.yml index 396a85065fe..fd54bdc1590 100644 --- a/docs/articles/clustering/toc.yml +++ b/docs/articles/clustering/toc.yml @@ -1,5 +1,7 @@ - name: Overview href: cluster-overview.md +- name: Member Roles + href: member-roles.md - name: Cluster Routing href: cluster-routing.md - name: Cluster Configuration diff --git a/docs/images/cluster/cluster-roles.png b/docs/images/cluster/cluster-roles.png new file mode 100644 index 0000000000000000000000000000000000000000..f448416b0fc4e2e57b0d6402eea3f6e9a2483d68 GIT binary patch literal 61801 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 Date: Wed, 30 Mar 2022 20:49:03 +0100 Subject: [PATCH 11/17] [DOCS]: Add `Examples` section to Akka.NET Doc (#5739) * Add initial commit * Fix linting and spell checks * Update examples * Fix lint issues * Fix spell check - American English * fix examples href * Update page title * Updated page with more examples * Update examples.md * Fix lint and markdown errors. * Fix docs conflict * Fix blanks * Delete examples.md.orig Co-authored-by: Aaron Stannard --- docs/articles/examples.md | 40 +++++++++++++++++++++++++++++++++++++++ docs/articles/toc.yml | 4 +++- docs/cSpell.json | 5 ++++- 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 docs/articles/examples.md diff --git a/docs/articles/examples.md b/docs/articles/examples.md new file mode 100644 index 00000000000..251e771e9be --- /dev/null +++ b/docs/articles/examples.md @@ -0,0 +1,40 @@ +--- +uid: akka-examples +title: Example Akka.NET Projects +--- + +A curated lists of projects that use various modules of Akka.NET + +## IoT + +* [Fire Alert AWS Kinesis Sample](https://github.com/petabridge/kinesis-sample): Akka.NET Streams + Cluster + AWS Kinesis Sample + +* [Home Temperature Monitor](https://github.com/grofab95/HomeTemperatureMonitor): Arduino temperature monitoring project built on **Akka.Cluster** + +## Machine Learning + +* [ML.NET ASP.NET Core Web API Akka.NET Sample](https://github.com/lqdev/MlNetAspNetAkkaSample): Sample application that uses Akka.NET Actors to scale predictions in an ASP.NET Core Web API. + +## Blockchain + +* [Blocksharp](https://github.com/revoltez/Blocksharp): A fully implemented Blockchain based on the Actor model using Akka.NET. + +## Web + +* [Cluster Web Crawler](https://github.com/petabridge/Cluster.WebCrawler): K8s, DevOps version of the Akka.Cluster WebCrawler code sample + +## Finance - Stock Trading + +* [In-Memory Stock Trading CQRS Sample](https://github.com/Aaronontheweb/InMemoryCQRSReplication): Akka.NET Reference Architecture - CQRS + Sharding + In-Memory Replication + +## Files + +* [File Processor](https://github.com/cgstevens/FileProcessor): The example demonstrates the use of Akka.NET Clustering to monitor the file system for new files. + +## Frameworks Built On Top Akka.NET + +* [Akkatecture](https://github.com/AfterLutz/Akkatecture): a cqrs and event sourcing framework for dotnet core using akka.NET. + +* [EventFly](https://github.com/Sporteco/EventFly): A cqrs and event sourcing framework for dotnet core, build on top of akka.net. Fully optimized around using akka's highly scalable message passing and event stream pub sub mechanisms. + +* [SharpPulsar](https://github.com/eaba/SharpPulsar): Apache Pulsar c# client built on top Akka.NET diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index cfa782be1ff..adaa0a51ad7 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -23,4 +23,6 @@ - name: Configurations href: configuration/toc.yml - name: Serialization - href: serialization/toc.yml \ No newline at end of file + href: serialization/toc.yml +- name: Examples + href: examples.md \ No newline at end of file diff --git a/docs/cSpell.json b/docs/cSpell.json index f88553d8de5..562380cd646 100644 --- a/docs/cSpell.json +++ b/docs/cSpell.json @@ -105,7 +105,10 @@ "titlecase", "Varghese", "Vertech", - "DeadLetter" + "DeadLetter", + "Blocksharp", + "Akkatecture", + "SharpPulsar" ], "ignoreRegExpList": [ "\\((.*)\\)", // Markdown links From 030949858b1fc28998fcac84bfb6d2485b3e0c0b Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Fri, 1 Apr 2022 21:08:01 +0700 Subject: [PATCH 12/17] Bump Hyperion to 0.12.2 (#5805) * Bump Hyperion to 0.12.2 * Add HyperionSerializerSettings immutable modifier methods * Fix unit tests --- src/common.props | 2 +- .../HyperionConfigTests.cs | 8 ++- .../HyperionSerializerSetupSpec.cs | 14 ++-- .../HyperionSerializer.cs | 70 +++++++++++++++++++ 4 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/common.props b/src/common.props index 9f3fc117438..204d9d08822 100644 --- a/src/common.props +++ b/src/common.props @@ -13,7 +13,7 @@ 2.4.1 2.4.3 17.0.0 - 0.12.0 + 0.12.2 [12.0.3,) 2.0.1 3.19.4 diff --git a/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionConfigTests.cs b/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionConfigTests.cs index 33f8ccd1f84..8894886ef63 100644 --- a/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionConfigTests.cs +++ b/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionConfigTests.cs @@ -93,12 +93,14 @@ public void TypeFilter_defined_in_config_should_filter_serializer_properly(objec "); using (var system = ActorSystem.Create(nameof(HyperionConfigTests), config)) { - var serializer = (HyperionSerializer)system.Serialization.FindSerializerForType(typeof(object)); + var deserializer = (HyperionSerializer)system.Serialization.FindSerializerForType(typeof(object)); + var serializer = new HyperionSerializer(null, deserializer.Settings.WithDisallowUnsafeType(false)); - ((TypeFilter)serializer.Settings.TypeFilter).FilteredTypes.Count.Should().Be(2); + ((TypeFilter)deserializer.Settings.TypeFilter).FilteredTypes.Count.Should().Be(2); + var serialized = serializer.ToBinary(sampleObject); object deserialized = null; - Action act = () => deserialized = serializer.FromBinary(serialized); + Action act = () => deserialized = deserializer.FromBinary(serialized); if (shouldSucceed) { act.Should().NotThrow(); diff --git a/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionSerializerSetupSpec.cs b/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionSerializerSetupSpec.cs index a5e0cc8be2e..021623632d0 100644 --- a/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionSerializerSetupSpec.cs +++ b/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionSerializerSetupSpec.cs @@ -130,9 +130,10 @@ public void Setup_surrogate_should_work() [MemberData(nameof(DangerousObjectFactory))] public void Setup_disallow_unsafe_type_should_work(object dangerousObject, Type type) { - var serializer = new HyperionSerializer((ExtendedActorSystem)Sys, HyperionSerializerSettings.Default); + var deserializer = new HyperionSerializer((ExtendedActorSystem)Sys, HyperionSerializerSettings.Default); + var serializer = new HyperionSerializer((ExtendedActorSystem)Sys, deserializer.Settings.WithDisallowUnsafeType(false)); var serialized = serializer.ToBinary(dangerousObject); - serializer.Invoking(s => s.FromBinary(serialized, type)).Should().Throw(); + deserializer.Invoking(s => s.FromBinary(serialized, type)).Should().Throw(); } [Theory] @@ -146,12 +147,13 @@ public void Setup_TypeFilter_should_filter_types_properly(object sampleObject, b .Build()); var settings = setup.ApplySettings(HyperionSerializerSettings.Default); - var serializer = new HyperionSerializer((ExtendedActorSystem)Sys, settings); - - ((TypeFilter)serializer.Settings.TypeFilter).FilteredTypes.Count.Should().Be(2); + var deserializer = new HyperionSerializer((ExtendedActorSystem)Sys, settings); + var serializer = new HyperionSerializer((ExtendedActorSystem)Sys, deserializer.Settings.WithDisallowUnsafeType(false)); var serialized = serializer.ToBinary(sampleObject); + + ((TypeFilter)deserializer.Settings.TypeFilter).FilteredTypes.Count.Should().Be(2); object deserialized = null; - Action act = () => deserialized = serializer.FromBinary(serialized); + Action act = () => deserialized = deserializer.FromBinary(serialized); if (shouldSucceed) { act.Should().NotThrow(); diff --git a/src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializer.cs b/src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializer.cs index b05f0cc3b50..dc525668441 100644 --- a/src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializer.cs +++ b/src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializer.cs @@ -401,5 +401,75 @@ public HyperionSerializerSettings( DisallowUnsafeType = disallowUnsafeType; TypeFilter = typeFilter; } + + public HyperionSerializerSettings WithPreserveObjectReference(bool preserveObjectReferences) + => new HyperionSerializerSettings( + preserveObjectReferences: preserveObjectReferences, + versionTolerance: VersionTolerance, + knownTypesProvider: KnownTypesProvider, + packageNameOverrides: PackageNameOverrides, + surrogates: Surrogates, + disallowUnsafeType: DisallowUnsafeType, + typeFilter: TypeFilter); + + public HyperionSerializerSettings WithVersionTolerance(bool versionTolerance) + => new HyperionSerializerSettings( + preserveObjectReferences: PreserveObjectReferences, + versionTolerance: versionTolerance, + knownTypesProvider: KnownTypesProvider, + packageNameOverrides: PackageNameOverrides, + surrogates: Surrogates, + disallowUnsafeType: DisallowUnsafeType, + typeFilter: TypeFilter); + + public HyperionSerializerSettings WithKnownTypesProvider(Type knownTypesProvider) + => new HyperionSerializerSettings( + preserveObjectReferences: PreserveObjectReferences, + versionTolerance: VersionTolerance, + knownTypesProvider: knownTypesProvider, + packageNameOverrides: PackageNameOverrides, + surrogates: Surrogates, + disallowUnsafeType: DisallowUnsafeType, + typeFilter: TypeFilter); + + public HyperionSerializerSettings WithPackageNameOverrides(IEnumerable> packageNameOverrides) + => new HyperionSerializerSettings( + preserveObjectReferences: PreserveObjectReferences, + versionTolerance: VersionTolerance, + knownTypesProvider: KnownTypesProvider, + packageNameOverrides: packageNameOverrides, + surrogates: Surrogates, + disallowUnsafeType: DisallowUnsafeType, + typeFilter: TypeFilter); + + public HyperionSerializerSettings WithSurrogates(IEnumerable surrogates) + => new HyperionSerializerSettings( + preserveObjectReferences: PreserveObjectReferences, + versionTolerance: VersionTolerance, + knownTypesProvider: KnownTypesProvider, + packageNameOverrides: PackageNameOverrides, + surrogates: surrogates, + disallowUnsafeType: DisallowUnsafeType, + typeFilter: TypeFilter); + + public HyperionSerializerSettings WithDisallowUnsafeType(bool disallowUnsafeType) + => new HyperionSerializerSettings( + preserveObjectReferences: PreserveObjectReferences, + versionTolerance: VersionTolerance, + knownTypesProvider: KnownTypesProvider, + packageNameOverrides: PackageNameOverrides, + surrogates: Surrogates, + disallowUnsafeType: disallowUnsafeType, + typeFilter: TypeFilter); + + public HyperionSerializerSettings WithTypeFilter(ITypeFilter typeFilter) + => new HyperionSerializerSettings( + preserveObjectReferences: PreserveObjectReferences, + versionTolerance: VersionTolerance, + knownTypesProvider: KnownTypesProvider, + packageNameOverrides: PackageNameOverrides, + surrogates: Surrogates, + disallowUnsafeType: DisallowUnsafeType, + typeFilter: typeFilter); } } From 3ee032d24dbc0ffc7d8b2f5d1e792b0972c6a753 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Tue, 5 Apr 2022 03:38:25 +0700 Subject: [PATCH 13/17] Fix leaky coordinated shutdown (#5816) * Fix CoordinatedShutdown infinite loop * Fix circular reference memory leak * Fix memory leak --- src/core/Akka/Actor/ActorSystem.cs | 2 +- src/core/Akka/Actor/CoordinatedShutdown.cs | 21 ++++++++++++------- .../Akka/Actor/Internal/ActorSystemImpl.cs | 11 +++++----- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/core/Akka/Actor/ActorSystem.cs b/src/core/Akka/Actor/ActorSystem.cs index 318e0ac6846..6b1524c55ea 100644 --- a/src/core/Akka/Actor/ActorSystem.cs +++ b/src/core/Akka/Actor/ActorSystem.cs @@ -364,7 +364,7 @@ private static ActorSystem CreateAndStartSystem(string name, Config withFallback /// public abstract Task Terminate(); - internal abstract void FinalTerminate(); + internal abstract Task FinalTerminate(); /// /// Returns a task which will be completed after the has been diff --git a/src/core/Akka/Actor/CoordinatedShutdown.cs b/src/core/Akka/Actor/CoordinatedShutdown.cs index f1b78cad7ae..623433178af 100644 --- a/src/core/Akka/Actor/CoordinatedShutdown.cs +++ b/src/core/Akka/Actor/CoordinatedShutdown.cs @@ -251,7 +251,7 @@ private ClusterJoinUnsuccessfulReason() { } /// /// The /// - public ExtendedActorSystem System { get; } + public ExtendedActorSystem System { get; private set; } /// /// The set of named s that will be executed during coordinated shutdown. @@ -261,7 +261,7 @@ private ClusterJoinUnsuccessfulReason() { } /// /// INTERNAL API /// - internal ILoggingAdapter Log { get; } + internal ILoggingAdapter Log { get; private set; } private readonly HashSet _knownPhases; @@ -270,7 +270,7 @@ private ClusterJoinUnsuccessfulReason() { } /// internal readonly List OrderedPhases; - private readonly ConcurrentBag>> _clrShutdownTasks = new ConcurrentBag>>(); + private readonly ConcurrentSet>> _clrShutdownTasks = new ConcurrentSet>>(); private readonly ConcurrentDictionary>)>> _tasks = new ConcurrentDictionary>)>>(); private readonly AtomicReference _runStarted = new AtomicReference(null); private readonly AtomicBoolean _clrHooksStarted = new AtomicBoolean(false); @@ -335,7 +335,7 @@ internal void AddClrShutdownHook(Func> hook) { if (!_clrHooksStarted) { - _clrShutdownTasks.Add(hook); + _clrShutdownTasks.TryAdd(hook); } } @@ -653,13 +653,16 @@ internal static void InitPhaseActorSystemTerminate(ActorSystem system, Config co if (terminateActorSystem) { - system.FinalTerminate(); - return system.Terminate().ContinueWith(tr => + return system.FinalTerminate().ContinueWith(tr => { if (exitClr && !coord._runningClrHook) { Environment.Exit(0); } + + coord.System = null; + coord.Log = null; + coord._tasks.Clear(); // Clear the dictionary, just in case it is retained in memory return Done.Instance; }); } @@ -691,7 +694,11 @@ internal static void InitClrHook(ActorSystem system, Config conf, CoordinatedShu var exitTask = TerminateOnClrExit(coord); // run all hooks during termination sequence AppDomain.CurrentDomain.ProcessExit += exitTask; - system.WhenTerminated.ContinueWith(tr => { AppDomain.CurrentDomain.ProcessExit -= exitTask; }); + system.WhenTerminated.ContinueWith(tr => + { + AppDomain.CurrentDomain.ProcessExit -= exitTask; + coord._clrShutdownTasks.Clear(); // Clear the tasks, just in case it is retained in memory + }); coord.AddClrShutdownHook(() => { diff --git a/src/core/Akka/Actor/Internal/ActorSystemImpl.cs b/src/core/Akka/Actor/Internal/ActorSystemImpl.cs index f7540c33f04..ed08b4acefa 100644 --- a/src/core/Akka/Actor/Internal/ActorSystemImpl.cs +++ b/src/core/Akka/Actor/Internal/ActorSystemImpl.cs @@ -525,20 +525,19 @@ public override Task Terminate() { if(Settings.CoordinatedShutdownRunByActorSystemTerminate) { - CoordinatedShutdown.Get(this).Run(CoordinatedShutdown.ActorSystemTerminateReason.Instance); - } else - { - FinalTerminate(); + return CoordinatedShutdown.Get(this) + .Run(CoordinatedShutdown.ActorSystemTerminateReason.Instance); } - return WhenTerminated; + return FinalTerminate(); } - internal override void FinalTerminate() + internal override Task FinalTerminate() { Log.Debug("System shutdown initiated"); if (!Settings.LogDeadLettersDuringShutdown && _logDeadLetterListener != null) Stop(_logDeadLetterListener); _provider.Guardian.Stop(); + return WhenTerminated; } /// From 13eea74b4c41228bfd79b48654e8645dcaa9c741 Mon Sep 17 00:00:00 2001 From: Ismael Hamed <1279846+ismaelhamed@users.noreply.github.com> Date: Mon, 4 Apr 2022 22:40:02 +0200 Subject: [PATCH 14/17] Couple of fixes for the TcpConnection (#5817) * Fix TcpConnection error handling * Try not to get stopped by death pact before Unregistration is complete --- src/core/Akka/IO/TcpConnection.cs | 72 +++++++++++++++---------------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/src/core/Akka/IO/TcpConnection.cs b/src/core/Akka/IO/TcpConnection.cs index 5d87c6f6114..a25a5ce799c 100644 --- a/src/core/Akka/IO/TcpConnection.cs +++ b/src/core/Akka/IO/TcpConnection.cs @@ -13,7 +13,6 @@ using System.Linq; using System.Net.Sockets; using System.Runtime.CompilerServices; -using System.Threading; using Akka.Actor; using Akka.Dispatch; using Akka.Event; @@ -108,14 +107,14 @@ enum ConnectionStatus protected TcpConnection(TcpExt tcp, Socket socket, bool pullMode, Option writeCommandsBufferMaxSize) { if (socket == null) throw new ArgumentNullException(nameof(socket)); - + _pullMode = pullMode; _writeCommandsQueue = new PendingSimpleWritesQueue(Log, writeCommandsBufferMaxSize); _traceLogging = tcp.Settings.TraceLogging; - + Tcp = tcp; Socket = socket; - + if (pullMode) SetStatus(ConnectionStatus.ReadingSuspended); } @@ -155,30 +154,26 @@ private Receive WaitingForRegistration(IActorRef commander) // up to this point we've been watching the commander, // but since registration is now complete we only need to watch the handler from here on if (!Equals(register.Handler, commander)) - { - Context.Unwatch(commander); - Context.Watch(register.Handler); - } + SignDeathPact(register.Handler); // will unsign death pact with commander automatically if (_traceLogging) Log.Debug("[{0}] registered as connection handler", register.Handler); var registerInfo = new ConnectionInfo(register.Handler, register.KeepOpenOnPeerClosed, register.UseResumeWriting); - // if we have resumed reading from pullMode while waiting for Register then read - if (_pullMode && !HasStatus(ConnectionStatus.ReadingSuspended)) ResumeReading(); - else if (!_pullMode) ReceiveAsync(); - Context.SetReceiveTimeout(null); Context.Become(Connected(registerInfo)); + // if we are in push mode or already have resumed reading in pullMode while waiting for Register then read + if (!_pullMode || !HasStatus(ConnectionStatus.ReadingSuspended)) ResumeReading(); + // If there is something buffered before we got Register message - put it all to the socket var bufferedWrite = GetNextWrite(); if (bufferedWrite.HasValue) { SetStatus(ConnectionStatus.Sending); DoWrite(registerInfo, bufferedWrite.Value); - } - + } + return true; case ResumeReading _: ClearStatus(ConnectionStatus.ReadingSuspended); return true; case SuspendReading _: SetStatus(ConnectionStatus.ReadingSuspended); return true; @@ -205,7 +200,7 @@ private Receive WaitingForRegistration(IActorRef commander) Log.Warning("Received Write command before Register command. " + "It will be buffered until Register will be received (buffered write size is {0} bytes)", commandSize); } - + return true; default: return false; } @@ -268,7 +263,7 @@ private Receive ClosingWithPendingWrite(ConnectionInfo info, IActorRef closeComm AcknowledgeSent(); if (IsWritePending) DoWrite(info, GetAllowedPendingWrite()); - else + else HandleClose(info, closeCommander, closedEvent); return true; case UpdatePendingWriteAndThen updatePendingWrite: @@ -277,7 +272,7 @@ private Receive ClosingWithPendingWrite(ConnectionInfo info, IActorRef closeComm if (nextWrite.HasValue) DoWrite(info, nextWrite); - else + else HandleClose(info, closeCommander, closedEvent); return true; case WriteFileFailed fail: HandleError(info.Handler, fail.Cause); return true; @@ -312,7 +307,7 @@ private Receive HandleWriteMessages(ConnectionInfo info) case SocketSent _: // Send ack to sender AcknowledgeSent(); - + // If there is something to send - send it var pendingWrite = GetAllowedPendingWrite(); if (pendingWrite.HasValue) @@ -320,14 +315,14 @@ private Receive HandleWriteMessages(ConnectionInfo info) SetStatus(ConnectionStatus.Sending); DoWrite(info, pendingWrite); } - + // If message is fully sent, notify sender who sent ResumeWriting command if (!IsWritePending && _interestedInResume != null) { _interestedInResume.Tell(WritingResumed.Instance); _interestedInResume = null; } - + return true; case WriteCommand write: if (HasStatus(ConnectionStatus.WritingSuspended)) @@ -356,7 +351,7 @@ private Receive HandleWriteMessages(ConnectionInfo info) DropWrite(info, write); return true; } - + nextWrite = GetNextWrite(headCommands: new []{ (simpleWriteCommand, Sender) }); } else @@ -364,15 +359,15 @@ private Receive HandleWriteMessages(ConnectionInfo info) _writeCommandsQueue.EnqueueSimpleWrites(write, Sender); nextWrite = GetNextWrite(); } - + // If there is something to send and we are allowed to, lets put the next command on the wire if (nextWrite.HasValue) { SetStatus(ConnectionStatus.Sending); DoWrite(info, nextWrite.Value); - } + } } - + return true; case ResumeWriting _: /* @@ -396,7 +391,7 @@ private Receive HandleWriteMessages(ConnectionInfo info) case UpdatePendingWriteAndThen updatePendingWrite: var updatedWrite = updatePendingWrite.RemainingWrite; updatePendingWrite.Work(); - if (updatedWrite.HasValue) + if (updatedWrite.HasValue) DoWrite(info, updatedWrite.Value); return true; case WriteFileFailed fail: @@ -461,7 +456,7 @@ private void AcknowledgeSent() { ackInfo.Commander.Tell(ackInfo.Ack); } - + ClearStatus(ConnectionStatus.Sending); } @@ -538,7 +533,7 @@ private void DoWrite(ConnectionInfo info, Option write) { _pendingAcks.Enqueue(pendingAck); } - + write.Value.DoWrite(info); } @@ -672,7 +667,7 @@ SocketCompleted ResolveMessage(SocketAsyncEventArgs e) throw new NotSupportedException($"Socket operation {e.LastOperation} is not supported"); } } - + var args = new SocketAsyncEventArgs(); args.UserToken = onCompleteNotificationsReceiver; args.Completed += (sender, e) => @@ -684,7 +679,7 @@ SocketCompleted ResolveMessage(SocketAsyncEventArgs e) return args; } - + protected void ReleaseSocketEventArgs(SocketAsyncEventArgs e) { e.UserToken = null; @@ -700,7 +695,7 @@ protected void ReleaseSocketEventArgs(SocketAsyncEventArgs e) catch (InvalidOperationException) { } e.Dispose(); - + } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -728,6 +723,7 @@ private void Abort() protected void StopWith(CloseInformation closeInfo) { _closedMessage = closeInfo; + UnsignDeathPact(); Context.Stop(Self); } @@ -810,7 +806,7 @@ private Option GetNextWrite(IEnumerable<(SimpleWriteCommand Comman { return CreatePendingBufferWrite(writeCommands); } - + // No more writes out there return Option.None; } @@ -986,7 +982,7 @@ public bool EnqueueSimpleWrites(WriteCommand command, IActorRef sender) { return EnqueueSimpleWrites(command, sender, out _); } - + /// /// Adds all subcommands stored in provided command. /// Performs buffer size checks @@ -997,7 +993,7 @@ public bool EnqueueSimpleWrites(WriteCommand command, IActorRef sender) public bool EnqueueSimpleWrites(WriteCommand command, IActorRef sender, out int bufferedSize) { bufferedSize = 0; - + foreach (var writeInfo in ExtractFromCommand(command)) { var sizeAfterAppending = _totalSizeInBytes + writeInfo.DataSize; @@ -1013,10 +1009,10 @@ public bool EnqueueSimpleWrites(WriteCommand command, IActorRef sender, out int _queue.Enqueue((writeInfo.Command, sender, writeInfo.DataSize)); bufferedSize += writeInfo.DataSize; } - + return true; } - + /// /// Adds all subcommands stored in provided command. /// Performs buffer size checks for all, except first one, that is not buffered @@ -1037,7 +1033,7 @@ public bool EnqueueSimpleWritesExceptFirst(WriteCommand command, IActorRef sende first = writeInfo.Command; continue; } - + var sizeAfterAppending = _totalSizeInBytes + writeInfo.DataSize; if (_maxQueueSizeInBytes.HasValue && _maxQueueSizeInBytes.Value < sizeAfterAppending) { @@ -1061,7 +1057,7 @@ public bool EnqueueSimpleWritesExceptFirst(WriteCommand command, IActorRef sende { if (_queue.Count == 0) throw new InvalidOperationException("Write commands queue is empty"); - + var (command, sender, size) = _queue.Dequeue(); _totalSizeInBytes -= size; return (command, sender); @@ -1076,7 +1072,7 @@ public bool EnqueueSimpleWritesExceptFirst(WriteCommand command, IActorRef sende while (TryGetNext(out var command)) yield return command; } - + /// /// Gets next command from the queue, if any /// From 9c29c847da830fb63e3ffaeb8787142184870638 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Tue, 5 Apr 2022 19:57:17 +0700 Subject: [PATCH 15/17] Update RELEASE_NOTES.md for 1.4.36 release (#5820) --- RELEASE_NOTES.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f9567a56d25..2b32b7bda2a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,21 @@ -#### 1.4.36 March 18 2022 #### +#### 1.4.36 April 4 2022 #### +Akka.NET v1.4.36 is a minor release that contains some bug fixes. Most of the changes have been aimed at improving our web documentation and code cleanup to modernize some of our code. -**Placeholder for nightlies** +* [Akka: Bump Hyperion to 0.12.2](https://github.com/akkadotnet/akka.net/pull/5805) + +__Bug fixes__: +* [Akka: Fix CoordinatedShutdown memory leak](https://github.com/akkadotnet/akka.net/pull/5816) +* [Akka: Fix TcpConnection error handling and death pact de-registration](https://github.com/akkadotnet/akka.net/pull/5817) + +If you want to see the [full set of changes made in Akka.NET v1.4.36, click here](https://github.com/akkadotnet/akka.net/milestone/67?closed=1). + +| COMMITS | LOC+ | LOC- | AUTHOR | +|---------|------|------|---------------------| +| 5 | 274 | 33 | Gregorius Soedharmo | +| 4 | 371 | 6 | Ebere Abanonu | +| 3 | 9 | 3 | Aaron Stannard | +| 1 | 34 | 38 | Ismael Hamed | +| 1 | 2 | 3 | Adrian Leonhard | #### 1.4.35 March 18 2022 #### Akka.NET v1.4.35 is a minor release that contains some bug fixes. Most of the changes have been aimed at improving our web documentation and code cleanup to modernize some of our code. From 161c9d59502f69f46b559e6e322170e2ef98472a Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Tue, 5 Apr 2022 22:12:12 +0700 Subject: [PATCH 16/17] Revert changes that are unrelated to the memory leak (#5822) --- src/core/Akka/Actor/CoordinatedShutdown.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/core/Akka/Actor/CoordinatedShutdown.cs b/src/core/Akka/Actor/CoordinatedShutdown.cs index 623433178af..99c456f6ad2 100644 --- a/src/core/Akka/Actor/CoordinatedShutdown.cs +++ b/src/core/Akka/Actor/CoordinatedShutdown.cs @@ -251,7 +251,7 @@ private ClusterJoinUnsuccessfulReason() { } /// /// The /// - public ExtendedActorSystem System { get; private set; } + public ExtendedActorSystem System { get; } /// /// The set of named s that will be executed during coordinated shutdown. @@ -261,7 +261,7 @@ private ClusterJoinUnsuccessfulReason() { } /// /// INTERNAL API /// - internal ILoggingAdapter Log { get; private set; } + internal ILoggingAdapter Log { get; } private readonly HashSet _knownPhases; @@ -660,9 +660,6 @@ internal static void InitPhaseActorSystemTerminate(ActorSystem system, Config co Environment.Exit(0); } - coord.System = null; - coord.Log = null; - coord._tasks.Clear(); // Clear the dictionary, just in case it is retained in memory return Done.Instance; }); } @@ -697,7 +694,6 @@ internal static void InitClrHook(ActorSystem system, Config conf, CoordinatedShu system.WhenTerminated.ContinueWith(tr => { AppDomain.CurrentDomain.ProcessExit -= exitTask; - coord._clrShutdownTasks.Clear(); // Clear the tasks, just in case it is retained in memory }); coord.AddClrShutdownHook(() => From da0f7349f5f6064d0bcf5465b27a58ea09204633 Mon Sep 17 00:00:00 2001 From: Ismael Hamed <1279846+ismaelhamed@users.noreply.github.com> Date: Tue, 5 Apr 2022 19:46:30 +0200 Subject: [PATCH 17/17] Be more explicit that a node is shutting down as it was marked as DOWN (#5821) Co-authored-by: Aaron Stannard --- src/core/Akka.Cluster/ClusterDaemon.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Akka.Cluster/ClusterDaemon.cs b/src/core/Akka.Cluster/ClusterDaemon.cs index 338b4a97469..188bfe4f0b2 100644 --- a/src/core/Akka.Cluster/ClusterDaemon.cs +++ b/src/core/Akka.Cluster/ClusterDaemon.cs @@ -2243,7 +2243,7 @@ private void ShutdownSelfWhenDown() { // the reason for not shutting down immediately is to give the gossip a chance to spread // the downing information to other downed nodes, so that they can shutdown themselves - _cluster.LogInfo("Shutting down myself"); + _cluster.LogInfo("Node has been marked as DOWN. Shutting down myself"); // not crucial to send gossip, but may speedup removal since fallback to failure detection is not needed // if other downed know that this node has seen the version SendGossipRandom(MaxGossipsBeforeShuttingDownMyself);