From c472d4f8c87780728e675f624813b3d82c2546d1 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Mon, 20 Dec 2021 18:33:57 -0600 Subject: [PATCH 01/30] Added v1.4.32 placeholder for nightlies (#5457) --- RELEASE_NOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b5944fa1f3c..b5b4c028093 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,6 @@ +#### 1.4.32 December 20 2021 #### +**Placeholder for nightlies** + #### 1.4.31 December 20 2021 #### Akka.NET v1.4.30 is a minor release that contains some bug fixes. From fbb580fe35dd4634224b2bdf62371a472f35a564 Mon Sep 17 00:00:00 2001 From: Brah McDude <77924970+brah-mcdude@users.noreply.github.com> Date: Tue, 28 Dec 2021 17:41:37 +0200 Subject: [PATCH 02/30] fixed wrong comment (#5462) --- src/core/Akka.TestKit/TestKitBase_Receive.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Akka.TestKit/TestKitBase_Receive.cs b/src/core/Akka.TestKit/TestKitBase_Receive.cs index faa6bf493d3..6708c9e4e3f 100644 --- a/src/core/Akka.TestKit/TestKitBase_Receive.cs +++ b/src/core/Akka.TestKit/TestKitBase_Receive.cs @@ -73,7 +73,7 @@ await Task.Run(() => ReceiveWhile(max: max, shouldContinue: x => { _assertions.AssertFalse(x is T, "did not expect a message of type {0}", typeof(T)); - return true; // we are not returning anything + return true; // please continue receiving, don't stop }); }); } From aac0c7cbfb8d6f560e1092ada5b2f1defdaf600d Mon Sep 17 00:00:00 2001 From: Brah McDude <77924970+brah-mcdude@users.noreply.github.com> Date: Tue, 28 Dec 2021 19:43:54 +0200 Subject: [PATCH 03/30] fixed FishUntilMessage test methods names and removed irrelevant comment (#5463) Co-authored-by: Aaron Stannard --- .../Akka.TestKit.Tests/TestKitBaseTests/ReceiveTests.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/core/Akka.TestKit.Tests/TestKitBaseTests/ReceiveTests.cs b/src/core/Akka.TestKit.Tests/TestKitBaseTests/ReceiveTests.cs index 85efaa4d4c8..94cd971f0aa 100644 --- a/src/core/Akka.TestKit.Tests/TestKitBaseTests/ReceiveTests.cs +++ b/src/core/Akka.TestKit.Tests/TestKitBaseTests/ReceiveTests.cs @@ -68,7 +68,7 @@ public void FishForMessage_should_timeout_if_to_few_messages() } [Fact] - public async Task InverseFishForMessage_should_succeed_with_good_input() + public async Task FishUntilMessage_should_succeed_with_good_input() { var probe = CreateTestProbe("probe"); probe.Ref.Tell(1d, TestActor); @@ -77,13 +77,10 @@ public async Task InverseFishForMessage_should_succeed_with_good_input() [Fact] - public async Task InverseFishForMessage_should_fail_with_bad_input() + public async Task FishUntilMessage_should_fail_with_bad_input() { var probe = CreateTestProbe("probe"); probe.Ref.Tell(3, TestActor); - - - // based on: https://getakka.net/articles/actors/testing-actor-systems.html#the-way-in-between-expecting-exceptions Func func = () => probe.FishUntilMessage(max: TimeSpan.FromMilliseconds(10)); await func.Should().ThrowAsync(); } From 01cfa200ac4b6a516a206081dd8881a61bae75a2 Mon Sep 17 00:00:00 2001 From: Sean Killeen Date: Tue, 28 Dec 2021 20:48:50 -0500 Subject: [PATCH 04/30] Docs markdown linting for title casing (#5465) * Add title casing to CI * fixed all titles to use appropriate case --- build-system/pr-validation.yaml | 4 +- docs/articles/actors/dependency-injection.md | 4 +- docs/articles/actors/di-core.md | 4 +- docs/articles/actors/dispatchers.md | 4 +- docs/articles/actors/finite-state-machine.md | 6 +-- docs/articles/actors/receive-actor-api.md | 36 +++++++++--------- docs/articles/actors/routers.md | 6 +-- docs/articles/actors/testing-actor-systems.md | 20 +++++----- docs/articles/actors/untyped-actor-api.md | 32 ++++++++-------- docs/articles/clustering/cluster-client.md | 4 +- docs/articles/clustering/cluster-extension.md | 8 ++-- docs/articles/clustering/cluster-metrics.md | 2 +- docs/articles/clustering/cluster-overview.md | 4 +- .../cluster-sharded-daemon-process.md | 2 +- docs/articles/clustering/cluster-sharding.md | 2 +- docs/articles/clustering/cluster-singleton.md | 2 +- docs/articles/clustering/distributed-data.md | 10 ++--- docs/articles/concepts/actor-systems.md | 2 +- docs/articles/concepts/addressing.md | 12 +++--- docs/articles/concepts/configuration.md | 14 +++---- .../concepts/location-transparency.md | 4 +- .../concepts/message-delivery-reliability.md | 14 +++---- docs/articles/concepts/supervision.md | 2 +- docs/articles/concepts/terminology.md | 12 +++--- docs/articles/discovery/index.md | 4 +- docs/articles/hocon/index.md | 22 +++++------ docs/articles/intro/akka-users.md | 14 +++---- docs/articles/intro/tutorial-1.md | 4 +- docs/articles/intro/tutorial-2.md | 4 +- docs/articles/intro/tutorial-3.md | 2 +- .../use-case-and-deployment-scenarios.md | 6 +-- docs/articles/intro/what-is-akka.md | 4 +- .../what-problems-does-actor-model-solve.md | 14 +++---- .../networking/multi-node-test-kit.md | 2 +- docs/articles/networking/serialization.md | 18 ++++----- .../persistence/at-least-once-delivery.md | 2 +- .../persistence/custom-serialization.md | 2 +- docs/articles/persistence/event-adapters.md | 2 +- docs/articles/persistence/event-sourcing.md | 26 ++++++------- .../articles/persistence/persistence-query.md | 14 +++---- .../persistence/persistence-testing.md | 12 +++--- docs/articles/persistence/persistent-fsm.md | 2 +- docs/articles/persistence/snapshots.md | 4 +- docs/articles/persistence/storage-plugins.md | 6 +-- docs/articles/remoting/deployment.md | 4 +- docs/articles/remoting/index.md | 6 +-- docs/articles/remoting/transports.md | 4 +- docs/articles/streams/basics.md | 22 +++++------ .../streams/buffersandworkingwithrate.md | 12 +++--- docs/articles/streams/builtinstages.md | 30 +++++++-------- docs/articles/streams/cookbook.md | 38 +++++++++---------- .../streams/custom-stream-processing.md | 26 ++++++------- docs/articles/streams/designprinciples.md | 16 ++++---- docs/articles/streams/error-handling.md | 6 +-- docs/articles/streams/integration.md | 4 +- docs/articles/streams/introduction.md | 2 +- .../articles/streams/modularitycomposition.md | 6 +-- .../streams/pipeliningandparallelism.md | 4 +- docs/articles/streams/reactivetweets.md | 10 ++--- docs/articles/streams/stream-dynamic.md | 10 ++--- docs/articles/streams/streamrefs.md | 8 ++-- docs/articles/streams/testingstreams.md | 4 +- docs/articles/streams/workingwithgraphs.md | 14 +++---- .../streams/workingwithstreamingio.md | 8 ++-- docs/articles/utilities/circuit-breaker.md | 4 +- docs/articles/utilities/event-bus.md | 6 +-- docs/articles/utilities/logging.md | 4 +- docs/articles/utilities/may-change.md | 2 +- docs/articles/utilities/scheduler.md | 8 ++-- docs/articles/utilities/serilog.md | 2 +- docs/community/books.md | 2 +- docs/community/contributor-guidelines.md | 6 +-- docs/community/documentation-guidelines.md | 2 +- docs/community/online-resources.md | 6 +-- .../akkadotnet-v1.4-upgrade-advisories.md | 6 +-- docs/community/whats-new/akkadotnet-v1.4.md | 6 +-- 76 files changed, 331 insertions(+), 331 deletions(-) diff --git a/build-system/pr-validation.yaml b/build-system/pr-validation.yaml index 50b9284af6e..eb8accd0fa7 100644 --- a/build-system/pr-validation.yaml +++ b/build-system/pr-validation.yaml @@ -48,10 +48,10 @@ jobs: - task: Npm@1 inputs: command: "custom" - customCommand: "install -g markdownlint-cli" + customCommand: "install -g markdownlint-cli markdownlint-rule-titlecase" - task: CmdLine@2 inputs: - script: 'markdownlint "docs/**/*.md"' + script: 'markdownlint "docs/**/*.md" --rules "markdownlint-rule-titlecase"' - job: WindowsBuild displayName: Windows Build pool: diff --git a/docs/articles/actors/dependency-injection.md b/docs/articles/actors/dependency-injection.md index 9bf21a8f0ef..62eed23d812 100644 --- a/docs/articles/actors/dependency-injection.md +++ b/docs/articles/actors/dependency-injection.md @@ -105,7 +105,7 @@ var worker1Ref = system.ActorOf(system.DI().Props(), "Worker1"); var worker2Ref = system.ActorOf(system.DI().Props(), "Worker2"); ``` -### Creating Child Actors using DI +### Creating Child Actors Using DI When you want to create child actors from within your existing actors using Dependency Injection you can use the Actor Content extension just like in @@ -203,7 +203,7 @@ var system = ActorSystem.Create("MySystem"); var propsResolver = new NinjectDependencyResolver(container,system); ``` -#### Other frameworks +#### Other Frameworks Support for additional dependency injection frameworks may be added in the future, but you can easily implement your own by implementing an diff --git a/docs/articles/actors/di-core.md b/docs/articles/actors/di-core.md index eb2ca2c43f5..7c517ca25e2 100644 --- a/docs/articles/actors/di-core.md +++ b/docs/articles/actors/di-core.md @@ -10,14 +10,14 @@ title: DI Core **Actor Producer Extension** library is used to create a Dependency Injection Container for the [Akka.NET](https://github.com/akkadotnet/akka.net) framework. -## What is it? +## What Is It? **Akka.DI.Core** is an **ActorSystem extension** library for the Akka.NET framework that provides a simple way to create an Actor Dependency Resolver that can be used an alternative to the basic capabilities of [Props](xref:receive-actor-api#props) when you have actors with multiple dependencies. -## How do you create an Extension? +## How Do You Create an Extension? * Create a new class library * Reference your favorite IoC Container, the Akka.DI.Core, and of course Akka diff --git a/docs/articles/actors/dispatchers.md b/docs/articles/actors/dispatchers.md index 23e1d888fe1..50dca0718e8 100644 --- a/docs/articles/actors/dispatchers.md +++ b/docs/articles/actors/dispatchers.md @@ -11,7 +11,7 @@ Dispatchers are responsible for scheduling all code that run inside the `ActorSy By default, all actors share a single **Global Dispatcher**. Unless you change the configuration, this dispatcher uses the *.NET Thread Pool* behind the scenes, which is optimized for most common scenarios. **That means the default configuration should be *good enough* for most cases.** -### Why should I use different dispatchers? +### Why Should I Use Different Dispatchers? When messages arrive in the [actor's mailbox](xref:mailboxes), the dispatcher schedules the delivery of messages in batches, and tries to deliver the entire batch before releasing the thread to another actor. While the default configuration is *good enough* for most scenarios, you may want to change ([through configuration](#configuring-dispatchers)) how much time the scheduler should spend running each actor. @@ -233,7 +233,7 @@ The following configuration keys are available for any dispatcher configuration: > [!NOTE] > The throughput-deadline-time is used as a *best effort*, not as a *hard limit*. This means that if a message takes more time than the deadline allows, Akka.NET won't interrupt the process. Instead it will wait for it to finish before giving turn to the next actor. -## Dispatcher aliases +## Dispatcher Aliases When a dispatcher is looked up, and the given setting contains a string rather than a dispatcher config block, the lookup will treat it as an alias, and follow that string to an alternate location for a dispatcher config. diff --git a/docs/articles/actors/finite-state-machine.md b/docs/articles/actors/finite-state-machine.md index bd84f517a95..61751b86933 100644 --- a/docs/articles/actors/finite-state-machine.md +++ b/docs/articles/actors/finite-state-machine.md @@ -52,7 +52,7 @@ To verify that this buncher actually works, it is quite easy to write a test usi ## Reference -### The FSM class +### The FSM Class The FSM class inherits directly from `ActorBase`, when you extend `FSM` you must be aware that an actor is actually created: @@ -221,7 +221,7 @@ IsTimerActive(name); These named timers complement state timeouts because they are not affected by intervening reception of other messages. -### Termination from Inside +### Termination From Inside The `FSM` is stopped by specifying the result state as @@ -268,7 +268,7 @@ OnTermination(termination => As for the `WhenUnhandled` case, this handler is not stacked, so each invocation of `OnTermination` replaces the previously installed handler. -### Termination from Outside +### Termination From Outside When an `IActorRef` associated to a `FSM` is stopped using the stop method, its `PostStop` hook will be executed. The default implementation by the `FSM` class is to execute the onTermination handler if that is prepared to handle a `StopEvent(Shutdown, ...)`. diff --git a/docs/articles/actors/receive-actor-api.md b/docs/articles/actors/receive-actor-api.md index d9445b296be..9f8b4f79f96 100644 --- a/docs/articles/actors/receive-actor-api.md +++ b/docs/articles/actors/receive-actor-api.md @@ -11,7 +11,7 @@ The Actor Model provides a higher level of abstraction for writing concurrent an > [!NOTE] > Since Akka.NET enforces parental supervision every actor is supervised and (potentially) the supervisor of its children, it is advisable that you familiarize yourself with [Actor Systems](xref:actor-systems) and [Supervision and Monitoring](xref:supervision) and it may also help to read [Actor References, Paths and Addresses](xref:addressing). -### Defining an Actor class +### Defining an Actor Class In order to use the `Receive()` method inside an actor, the actor must inherit from ReceiveActor. Inside the constructor, add a call to `Receive(Action handler)` for every type of message you want to handle: @@ -193,7 +193,7 @@ An `IActorRef` always represents an incarnation (path and UID) not just a given `ActorSelection` on the other hand points to the path (or multiple paths if wildcards are used) and is completely oblivious to which incarnation is currently occupying it. `ActorSelection` cannot be watched for this reason. It is possible to resolve the current incarnation's ActorRef living under the path by sending an `Identify` message to the `ActorSelection` which will be replied to with an `ActorIdentity` containing the correct reference (see [Identifying Actors via Actor Selection](#identifying-actors-via-actor-selection)). This can also be done with the resolveOne method of the `ActorSelection`, which returns a `Task` of the matching `IActorRef`. -### Lifecycle Monitoring aka DeathWatch +### Lifecycle Monitoring a.k.a. DeathWatch In order to be notified when another actor terminates (i.e. stops permanently, not temporary failure and restart), an actor may register itself for reception of the `Terminated` message dispatched by the other actor upon termination (see [Stopping Actors](#stopping-actors)). This service is provided by the DeathWatch component of the actor system. @@ -364,7 +364,7 @@ public class ImmutableMessage } ``` -## Send messages +## Send Messages Messages are sent to an Actor through one of the following methods. @@ -378,7 +378,7 @@ Message ordering is guaranteed on a per-sender basis. In all these methods you have the option of passing along your own `IActorRef`. Make it a practice of doing so because it will allow the receiver actors to be able to respond to your message, since the `Sender` reference is sent along with the message. -## Tell: Fire-forget +## Tell: Fire-Forget This is the preferred way of sending messages. No blocking waiting for a message. This gives the best concurrency and scalability characteristics. @@ -427,7 +427,7 @@ For more information on Tasks, check out the [MSDN documentation](https://msdn.m > [!WARNING] > When using task callbacks inside actors, you need to carefully avoid closing over the containing actor’s reference, i.e. do not call methods or access mutable state on the enclosing actor from within the callback. This would break the actor encapsulation and may introduce synchronization bugs and race conditions because the callback will be scheduled concurrently to the enclosing actor. Unfortunately there is not yet a way to detect these illegal accesses at compile time. -### Forward message +### Forward Message You can forward a message from one actor to another. This means that the original sender address/reference is maintained even though the message is going through a 'mediator'. This can be useful when writing actors that work as routers, load-balancers, replicators etc. @@ -435,7 +435,7 @@ You can forward a message from one actor to another. This means that the origina target.Forward(result); ``` -## Receive messages +## Receive Messages To receive a message you should create a `Receive` handler in a constructor. @@ -443,7 +443,7 @@ To receive a message you should create a `Receive` handler in a constructor. Receive(ms => Console.WriteLine("Received message: " + msg)); ``` -### Handler priority +### Handler Priority If more than one handler matches, the one that appears first is used while the others are ignored. @@ -456,7 +456,7 @@ Receive(o => Console.WriteLine("Received object: " + o)); //3 > **Example** > The actor receives a message of type string. Only the first handler is invoked, even though all three handlers can handle that message. -### Using predicates +### Using Predicates By specifying a predicate, you can choose which messages to handle. @@ -491,7 +491,7 @@ Receive(s => s.Length > 5, s => Console.WriteLine("Received string: " + Receive(s => Console.WriteLine("Received string: " + s), s => s.Length > 5); ``` -### Unmatched messages +### Unmatched Messages If the actor receives a message for which no handler matches, the unhandled message is published to the EventStream wrapped in an `UnhandledMessage`. To change this behavior override `Unhandled(object message)` @@ -528,7 +528,7 @@ ReceiveAny(o => Console.WriteLine("Received object: " + o); Receive(0 => Console.WriteLine("Received object: " + o); ``` -## Reply to messages +## Reply to Messages If you want to have a handle for replying to a message, you can use `Sender`, which gives you an `IActorRef`. You can reply by sending to that `IActorRef` with `Sender.Tell(replyMsg, Self)`. You can also store the `IActorRef` for replying later, or passing on to other actors. If there is no sender (a message was sent without an actor or task context) then the sender defaults to a 'dead-letter' actor ref. @@ -542,7 +542,7 @@ Receive(() => }) ``` -## Receive timeout +## Receive Timeout The `IActorContext` `SetReceiveTimeout` defines the inactivity timeout after which the sending of a `ReceiveTimeout` message is triggered. When specified, the receive function should be able to handle an `Akka.Actor.ReceiveTimeout` message. @@ -573,7 +573,7 @@ public class MyActor : ReceiveActor } ``` -## Stopping actors +## Stopping Actors Actors are stopped by invoking the `Stop` method of a `ActorRefFactory`, i.e. `ActorContext` or `ActorSystem`. Typically the context is used for stopping child actors and the system for stopping top level actors. The actual termination of the actor is performed asynchronously, i.e. stop may return before the actor is stopped. @@ -850,25 +850,25 @@ An exception can be thrown while a message is being processed by an actor, e.g. When this occurs and the exception is not handled via a `try` / `catch` block, the actor's parent will be notified that its child failed with a specific exception type and will use its [supervision strategy](xref:supervision#what-supervision-means) to restart that child. -### What happens to the Message +### What Happens to the Message If an exception is thrown while a message is being processed (i.e. taken out of its mailbox and handed over to the current behavior), then this message will be lost. It is important to understand that it is not put back on the mailbox. So if you want to retry processing of a message, you need to deal with it yourself by catching the exception and retry your flow. Make sure that you put a bound on the number of retries since you don't want a system to livelock (so consuming a lot of cpu cycles without making progress). -### What happens to the mailbox +### What Happens to the Mailbox If an exception is thrown while a message is being processed, nothing happens to the mailbox. If the actor is restarted, the same mailbox will be there. So all messages on that mailbox will be there as well. -### What happens to the actor +### What Happens to the Actor If code within an actor throws an exception, that actor is suspended and the supervision process is started (see Supervision and Monitoring). Depending on the supervisor’s decision the actor is resumed (as if nothing happened), restarted (wiping out its internal state and starting from scratch) or terminated. -## Initialization patterns +## Initialization Patterns The rich lifecycle hooks of `Actors` provide a useful toolkit to implement various initialization patterns. During the lifetime of an `IActorRef`, an actor can potentially go through several restarts, where the old instance is replaced by a fresh one, invisibly to the outside observer who only sees the `IActorRef`. One may think about the new instances as "incarnations". Initialization might be necessary for every incarnation of an actor, but sometimes one needs initialization to happen only at the birth of the first instance when the `IActorRef` is created. The following sections provide patterns for different initialization needs. -### Initialization via constructor +### Initialization via Constructor Using the constructor for initialization has various benefits. First of all, it makes it possible to use readonly fields to store any state that does not change during the life of the actor instance, making the implementation of the actor more robust. The constructor is invoked for every incarnation of the actor, therefore the internals of the actor can always assume that proper initialization happened. This is also the drawback of this approach, as there are cases when one would like to avoid re-initializing internals on restart. For example, it is often useful to preserve child actors across restarts. The following section provides a pattern for this case. @@ -903,7 +903,7 @@ Please note, that the child actors are *still restarted*, but no new `IActorRef` For more information see [What Restarting Means](xref:supervision#what-restarting-means). -#### Initialization via message passing +#### Initialization via Message Passing There are cases when it is impossible to pass all the information needed for actor initialization in the constructor, for example in the presence of circular dependencies. In this case the actor should listen for an initialization message, and use `Become()` or a finite state-machine state transition to encode the initialized and uninitialized states of the actor. diff --git a/docs/articles/actors/routers.md b/docs/articles/actors/routers.md index ce98c6bb7bb..8a8c054ff1c 100644 --- a/docs/articles/actors/routers.md +++ b/docs/articles/actors/routers.md @@ -19,7 +19,7 @@ Akka.NET comes with several useful routers you can choose right out of the box, Routers can be deployed in multiple ways, using code or configuration. -### Code deployment +### Code Deployment The example below shows how to deploy 5 workers using a round robin router: @@ -35,7 +35,7 @@ The above code can also be written as: var props = new RoundRobinPool(5).Props(Props.Create()); ``` -### Configuration deployment +### Configuration Deployment The same router may be defined using a [HOCON deployment configuration](xref:configuration). @@ -640,7 +640,7 @@ Sending one of the following messages to a router can be used to manage its rout ## Advanced -### How Routing is Designed within Akka.NET +### How Routing Is Designed Within Akka.NET On the surface routers look like normal actors, but they are actually implemented differently. Routers are designed to be extremely efficient at receiving messages and passing them quickly on to routees. diff --git a/docs/articles/actors/testing-actor-systems.md b/docs/articles/actors/testing-actor-systems.md index 955865897cf..270582c659e 100644 --- a/docs/articles/actors/testing-actor-systems.md +++ b/docs/articles/actors/testing-actor-systems.md @@ -126,7 +126,7 @@ You have complete flexibility here in mixing and matching the `TestKit` faciliti > [!WARNING] > Any message send from a `TestProbe` to another actor which runs on the `CallingThreadDispatcher` runs the risk of dead-lock, if that other actor might also send to this probe. The implementation of `TestProbe.Watch` and `TestProbe.Unwatch` will also send a message to the watchee, which means that it is dangerous to try watching e.g. `TestActorRef` from a `TestProbe`. -### Watching Other Actors from probes +### Watching Other Actors From Probes A `TestProbe` can register itself for DeathWatch of any other actor: @@ -161,11 +161,11 @@ Receiving messages in a queue for later inspection is nice, but in order to keep The `run` method must return the auto-pilot for the next message. There are multiple options here: You can return the `AutoPilot.NoAutoPilot` to stop the autopilot, or `AutoPilot.KeepRunning` to keep using the current `AutoPilot`. Obviously you can also chain a new `AutoPilot` instance to switch behaviors. -### Caution about Timing Assertions +### Caution About Timing Assertions The behavior of `Within` blocks when using test probes might be perceived as counter-intuitive: you need to remember that the nicely scoped deadline as described **above** is local to each probe. Hence, probes do not react to each other's deadlines or to the deadline set in an enclosing `TestKit` instance. -## Testing parent-child relationships +## Testing Parent-Child Relationships The parent of an actor is always the actor that created it. At times this leads to a coupling between the two that may not be straightforward to test. There are several approaches to improve testability of a child actor that needs to refer to its parent: @@ -181,25 +181,25 @@ For example, the structure of the code you want to test may follow this pattern: [!code-csharp[ParentStructure](../../../src/core/Akka.Docs.Tests/Testkit/ParentSampleTest.cs?name=ParentStructure_0)] -### Introduce child to its parent +### Introduce Child to Its Parent The first option is to avoid use of the `context.parent` function and create a child with a custom parent by passing an explicit reference to its parent instead. [!code-csharp[DependentChild](../../../src/core/Akka.Docs.Tests/Testkit/ParentSampleTest.cs?name=DependentChild_0)] -### Create the child using the TestProbe +### Create the Child Using the TestProbe The `TestProbe` class can directly create child actors using the `ChildActorOf` methods. [!code-csharp[TestProbeChild](../../../src/core/Akka.Docs.Tests/Testkit/ParentSampleTest.cs?name=TestProbeChild_0)] -### Using a fabricated parent +### Using a Fabricated Parent If you prefer to avoid modifying the parent or child constructor you can create a fabricated parent in your test. This, however, does not enable you to test the parent actor in isolation. [!code-csharp[FabrikatedParent](../../../src/core/Akka.Docs.Tests/Testkit/ParentSampleTest.cs?name=FabrikatedParent_0)] -### Externalize child making from the parent +### Externalize Child Making From the Parent Alternatively, you can tell the parent how to create its child. There are two ways to do this: by giving it a `Props` object or by giving it a function which takes care of creating the child actor: @@ -225,7 +225,7 @@ Which of these methods is the best depends on what is most important to test. Th The `CallingThreadDispatcher` serves good purposes in unit testing, as described above, but originally it was conceived in order to allow contiguous stack traces to be generated in case of an error. As this special dispatcher runs everything which would normally be queued directly on the current thread, the full history of a message's processing chain is recorded on the call stack, so long as all intervening actors run on this dispatcher. -### How to use it +### How to Use It Just set the dispatcher as you normally would @@ -233,7 +233,7 @@ Just set the dispatcher as you normally would Sys.ActorOf(Props.Create().WithDispatcher(CallingThreadDispatcher.Id)); ``` -### How it works +### How It Works When receiving an invocation, the `CallingThreadDispatcher` checks whether the receiving actor is already active on the current thread. The simplest example for this situation is an actor which sends a message to itself. In this case, processing cannot continue immediately as that would violate the actor model, so the invocation is queued and will be processed when the active invocation on that actor finishes its processing; thus, it will be processed on the calling thread, but simply after the actor finishes its previous work. In the other case, the invocation is simply processed immediately on the current thread. Tasks scheduled via this dispatcher are also executed immediately. @@ -327,7 +327,7 @@ Assert.False(fsm.IsTimerActive("test")); All methods shown above directly access the FSM state without any synchronization; this is perfectly alright if the `CallingThreadDispatcher` is used and no other threads are involved, but it may lead to surprises if you were to actually exercise timer events, because those are executed on the `Scheduler` thread. -## Testing the Actor's behavior +## Testing the Actor's Behavior When the dispatcher invokes the processing behavior of an actor on a message, it actually calls apply on the current behavior registered for the actor. This starts out with the return value of the declared receive method, but it may also be changed using become and unbecome in response to external messages. All of this contributes to the overall actor behavior and it does not lend itself to easy testing on the `Actor` itself. Therefore the TestActorRef offers a different mode of operation to complement the `Actor` testing: it supports all operations also valid on normal `IActorRef`. Messages sent to the actor are processed synchronously on the current thread and answers may be sent back as usual. This trick is made possible by the `CallingThreadDispatcher` described below; this dispatcher is set implicitly for any actor instantiated into a `TestActorRef`. diff --git a/docs/articles/actors/untyped-actor-api.md b/docs/articles/actors/untyped-actor-api.md index 3d77325c711..d80054d2756 100644 --- a/docs/articles/actors/untyped-actor-api.md +++ b/docs/articles/actors/untyped-actor-api.md @@ -14,7 +14,7 @@ The Actor Model provides a higher level of abstraction for writing concurrent an > [!NOTE] > Since Akka.NET enforces parental supervision every actor is supervised and (potentially) the supervisor of its children, it is advisable that you familiarize yourself with [Actor Systems](xref:actor-systems) and [Supervision and Monitoring](xref:supervision) and it may also help to read [Actor References, Paths and Addresses](xref:addressing). -### Defining an Actor class +### Defining an Actor Class Actors in C# are implemented by extending the `UntypedActor` class and and implementing the `OnReceive` method. This method takes the message as a parameter. @@ -217,7 +217,7 @@ An `IActorRef` always represents an incarnation (path and UID) not just a given `ActorSelection` on the other hand points to the path (or multiple paths if wildcards are used) and is completely oblivious to which incarnation is currently occupying it. `ActorSelection` cannot be watched for this reason. It is possible to resolve the current incarnation's ActorRef living under the path by sending an `Identify` message to the `ActorSelection` which will be replied to with an `ActorIdentity` containing the correct reference (see [Identifying Actors via Actor Selection](#identifying-actors-via-actor-selection)). This can also be done with the resolveOne method of the `ActorSelection`, which returns a `Task` of the matching `IActorRef`. -### Lifecycle Monitoring aka DeathWatch +### Lifecycle Monitoring a.k.a. DeathWatch In order to be notified when another actor terminates (i.e. stops permanently, not temporary failure and restart), an actor may register itself for reception of the `Terminated` message dispatched by the other actor upon termination (see [Stopping Actors](#stopping-actors)). This service is provided by the DeathWatch component of the actor system. @@ -349,7 +349,7 @@ public class ImmutableMessage } ``` -## Send messages +## Send Messages Messages are sent to an Actor through one of the following methods. @@ -363,7 +363,7 @@ Message ordering is guaranteed on a per-sender basis. In all these methods you have the option of passing along your own `IActorRef`. Make it a practice of doing so because it will allow the receiver actors to be able to respond to your message, since the `Sender` reference is sent along with the message. -## Tell: Fire-forget +## Tell: Fire-Forget This is the preferred way of sending messages. No blocking waiting for a message. This gives the best concurrency and scalability characteristics. @@ -412,7 +412,7 @@ For more information on Tasks, check out the [MSDN documentation](https://msdn.m > [!WARNING] > When using task callbacks inside actors, you need to carefully avoid closing over the containing actor’s reference, i.e. do not call methods or access mutable state on the enclosing actor from within the callback. This would break the actor encapsulation and may introduce synchronization bugs and race conditions because the callback will be scheduled concurrently to the enclosing actor. Unfortunately there is not yet a way to detect these illegal accesses at compile time. -### Forward message +### Forward Message You can forward a message from one actor to another. This means that the original sender address/reference is maintained even though the message is going through a 'mediator'. This can be useful when writing actors that work as routers, load-balancers, replicators etc. You need to pass along your context variable as well. @@ -420,7 +420,7 @@ You can forward a message from one actor to another. This means that the origina target.Forward(result, Context); ``` -## Receive messages +## Receive Messages When an actor receives a message it is passed into the `OnReceive` method, this is an abstract method on the `UntypedActor` base class that needs to be defined. @@ -446,7 +446,7 @@ public class MyActor : UntypedActor } ``` -## Reply to messages +## Reply to Messages If you want to have a handle for replying to a message, you can use `Sender`, which gives you an `IActorRef`. You can reply by sending to that `IActorRef` with `Sender.Tell(replyMsg, Self)`. You can also store the `IActorRef` for replying later, or passing on to other actors. If there is no sender (a message was sent without an actor or task context) then the sender defaults to a 'dead-letter' actor ref. @@ -460,7 +460,7 @@ protected override void OnReceive(object message) } ``` -## Receive timeout +## Receive Timeout The `IActorContext` `SetReceiveTimeout` defines the inactivity timeout after which the sending of a `ReceiveTimeout` message is triggered. When specified, the receive function should be able to handle an `Akka.Actor.ReceiveTimeout` message. @@ -489,7 +489,7 @@ public class MyActor : UntypedActor } ``` -## Stopping actors +## Stopping Actors Actors are stopped by invoking the `Stop` method of a `ActorRefFactory`, i.e. `ActorContext` or `ActorSystem`. Typically the context is used for stopping child actors and the system for stopping top level actors. The actual termination of the actor is performed asynchronously, i.e. stop may return before the actor is stopped. @@ -765,29 +765,29 @@ Use `Kill` like this: victim.Tell(Akka.Actor.Kill.Instance, ActorRef.NoSender); ``` -## Actors and exceptions +## Actors and Exceptions It can happen that while a message is being processed by an actor, that some kind of exception is thrown, e.g. a database exception. -### What happens to the Message +### What Happens to the Message If an exception is thrown while a message is being processed (i.e. taken out of its mailbox and handed over to the current behavior), then this message will be lost. It is important to understand that it is not put back on the mailbox. So if you want to retry processing of a message, you need to deal with it yourself by catching the exception and retry your flow. Make sure that you put a bound on the number of retries since you don't want a system to livelock (so consuming a lot of cpu cycles without making progress). -### What happens to the mailbox +### What Happens to the Mailbox If an exception is thrown while a message is being processed, nothing happens to the mailbox. If the actor is restarted, the same mailbox will be there. So all messages on that mailbox will be there as well. -### What happens to the actor +### What Happens to the Actor If code within an actor throws an exception, that actor is suspended and the supervision process is started (see Supervision and Monitoring). Depending on the supervisor’s decision the actor is resumed (as if nothing happened), restarted (wiping out its internal state and starting from scratch) or terminated. -## Initialization patterns +## Initialization Patterns The rich lifecycle hooks of `Actors` provide a useful toolkit to implement various initialization patterns. During the lifetime of an `IActorRef`, an actor can potentially go through several restarts, where the old instance is replaced by a fresh one, invisibly to the outside observer who only sees the `IActorRef`. One may think about the new instances as "incarnations". Initialization might be necessary for every incarnation of an actor, but sometimes one needs initialization to happen only at the birth of the first instance when the `IActorRef` is created. The following sections provide patterns for different initialization needs. -### Initialization via constructor +### Initialization via Constructor Using the constructor for initialization has various benefits. First of all, it makes it possible to use readonly fields to store any state that does not change during the life of the actor instance, making the implementation of the actor more robust. The constructor is invoked for every incarnation of the actor, therefore the internals of the actor can always assume that proper initialization happened. This is also the drawback of this approach, as there are cases when one would like to avoid re-initializing internals on restart. For example, it is often useful to preserve child actors across restarts. The following section provides a pattern for this case. @@ -822,7 +822,7 @@ Please note, that the child actors are *still restarted*, but no new `IActorRef` For more information see [What Restarting Means](xref:supervision#what-restarting-means). -#### Initialization via message passing +#### Initialization via Message Passing There are cases when it is impossible to pass all the information needed for actor initialization in the constructor, for example in the presence of circular dependencies. In this case the actor should listen for an initialization message, and use `Become()` or a finite state-machine state transition to encode the initialized and uninitialized states of the actor. diff --git a/docs/articles/clustering/cluster-client.md b/docs/articles/clustering/cluster-client.md index 47cdb696fc4..0a95fd80101 100644 --- a/docs/articles/clustering/cluster-client.md +++ b/docs/articles/clustering/cluster-client.md @@ -119,12 +119,12 @@ The `ClusterClientReceptionist` extension (or `ClusterReceptionistSettings`) can The 'akka.cluster.client' configuration properties are read by the `ClusterClientSettings` when created with a `ActorSystem` parameter. It is also possible to amend the `ClusterClientSettings` or create it from another config section with the same layout in the reference config. `ClusterClientSettings` is a parameter to the `ClusterClient.Props()` factory method, i.e. each client can be configured with different settings if needed. -## Failure handling +## Failure Handling When the cluster client is started it must be provided with a list of initial contacts which are cluster nodes where receptionists are running. It will then repeatedly (with an interval configurable by `establishing-get-contacts-interval`) try to contact those until it gets in contact with one of them. While running, the list of contacts are continuously updated with data from the receptionists (again, with an interval configurable with `refresh-contacts-interval`), so that if there are more receptionists in the cluster than the initial contacts provided to the client the client will learn about them. While the client is running it will detect failures in its connection to the receptionist by heartbeats if more than a configurable amount of heartbeats are missed the client will try to reconnect to its known set of contacts to find a receptionist it can access. -## When the cluster cannot be reached at all +## When the Cluster Cannot Be Reached at All It is possible to make the cluster client stop entirely if it cannot find a receptionist it can talk to within a configurable interval. This is configured with the `reconnect-timeout`, which defaults to off. This can be useful when initial contacts are provided from some kind of service registry, cluster node addresses are entirely dynamic and the entire cluster might shut down or crash, be restarted on new addresses. Since the client will be stopped in that case a monitoring actor can watch it and upon `Terminate` a new set of initial contacts can be fetched and a new cluster client started. diff --git a/docs/articles/clustering/cluster-extension.md b/docs/articles/clustering/cluster-extension.md index 2a0f2d6177d..96e2d0c4fd4 100644 --- a/docs/articles/clustering/cluster-extension.md +++ b/docs/articles/clustering/cluster-extension.md @@ -30,7 +30,7 @@ namespace Samples.Cluster.Simple We've shown a number of examples and referred to many different types of `ClusterEvent` messages throughout *Akka.NET Clustering* thus far. We're going to take a moment to show you all of the different types of messages you can subscribe to from the `Cluster` actor system extension and how the current state of the cluster can be replayed to new subscribers. -### Subscribing and Unsubscribing from Cluster Gossip +### Subscribing and Unsubscribing From Cluster Gossip We've seen a few code samples that showed actors who subscribed to gossip messages from the [`Cluster`](http://api.getakka.net/docs/stable/html/6FA3E0EC.htm "Akka.NET Stable API Docs - Cluster Class") actor system extension, but let's review the `Subscribe` and `Unsubscribe` methods and see what they do. @@ -43,7 +43,7 @@ This is an example of subscribing to `IMemberEvents` from the `Cluster`: Cluster.Subscribe(Self, new[] { typeof(ClusterEvent.IMemberEvent) }); ``` -#### Unsubscribing from Gossip +#### Unsubscribing From Gossip Let's suppose you need to unsubscribe from cluster gossip - here's how you can accomplish that. @@ -66,11 +66,11 @@ Gossip events fall into three categories: ### Cluster Event Categories -#### Member events +#### Member Events Member events refer to nodes joining / leaving / being removed from the cluster. These events are used by [Akka.Cluster routers](xref:cluster-routing) to automatically adjust their routee lists. -#### Reachability events +#### Reachability Events Reachability events refer to connectivity between nodes. diff --git a/docs/articles/clustering/cluster-metrics.md b/docs/articles/clustering/cluster-metrics.md index f583bf4289b..2a35c7e1d51 100644 --- a/docs/articles/clustering/cluster-metrics.md +++ b/docs/articles/clustering/cluster-metrics.md @@ -3,7 +3,7 @@ uid: cluster-metrics title: Akka.Cluster.Metrics module --- -# Akka.Cluster.Metrics module +# Akka.Cluster.Metrics Module The member nodes of the cluster can collect system health metrics and publish that to other cluster nodes and to the registered subscribers on the system event bus with the help of Cluster Metrics Extension. diff --git a/docs/articles/clustering/cluster-overview.md b/docs/articles/clustering/cluster-overview.md index 1293074e66a..4dd1c553115 100644 --- a/docs/articles/clustering/cluster-overview.md +++ b/docs/articles/clustering/cluster-overview.md @@ -5,7 +5,7 @@ title: Akka.Cluster Overview # Akka.Cluster Overview -## What is a "Cluster"? +## What Is a "Cluster"? A cluster represents a fault-tolerant, elastic, decentralized peer-to-peer network of Akka.NET applications with no single point of failure or bottleneck. Akka.Cluster is the module that gives you the ability to create these applications. @@ -29,7 +29,7 @@ In short, these are the benefits of a properly designed cluster: * **Peer-to-peer**: New nodes can contact existing peers, be notified about other peers, and fully integrate themselves into the network without any configuration changes. * **No single point of failure/bottleneck**: multiple nodes are able to service requests, increasing throughput and fault tolerance. -## How is Clustering Different From Remoting? +## How Is Clustering Different From Remoting? Akka.Cluster is a layer of abstraction on top of Akka.Remote, that puts Remoting to use for a specific structure: clusters of applications. Under the hood, Akka.Remote powers Akka.Cluster, so anything you could do with Akka.Remote is also supported by Akka.Cluster. diff --git a/docs/articles/clustering/cluster-sharded-daemon-process.md b/docs/articles/clustering/cluster-sharded-daemon-process.md index a1525528632..0c988747640 100644 --- a/docs/articles/clustering/cluster-sharded-daemon-process.md +++ b/docs/articles/clustering/cluster-sharded-daemon-process.md @@ -18,7 +18,7 @@ used to split the workload of consuming and updating a projection between `N` wo For cases where a single actor needs to be kept alive see [Cluster Singleton](cluster-singleton.md) -## Basic example +## Basic Example To set up a set of actors running with Sharded Daemon process each node in the cluster needs to run the same initialization when starting up: diff --git a/docs/articles/clustering/cluster-sharding.md b/docs/articles/clustering/cluster-sharding.md index 85b33f3c251..0e7e57ec72a 100644 --- a/docs/articles/clustering/cluster-sharding.md +++ b/docs/articles/clustering/cluster-sharding.md @@ -2,7 +2,7 @@ uid: cluster-sharding title: Akka.Cluster.Sharding module --- -# Akka.Cluster.Sharding module +# Akka.Cluster.Sharding Module Cluster sharding is useful in cases when you want to contact with cluster actors using their logical id's, but don't want to care about their physical location inside the cluster or manage their creation. Moreover it's able to re-balance them, as nodes join/leave the cluster. It's often used to represent i.e. Aggregate Roots in Domain Driven Design terminology. diff --git a/docs/articles/clustering/cluster-singleton.md b/docs/articles/clustering/cluster-singleton.md index cbfbec09fbc..e724bf5bef4 100644 --- a/docs/articles/clustering/cluster-singleton.md +++ b/docs/articles/clustering/cluster-singleton.md @@ -25,7 +25,7 @@ You can access the singleton actor by using the provided `Akka.Cluster.Tools.Sin It's worth noting that messages can always be lost because of the distributed nature of these actors. As always, additional logic should be implemented in the singleton (acknowledgement) and in the client (retry) actors to ensure at-least-once message delivery. -## Potential problems to be aware of +## Potential Problems to Be Aware Of This pattern may seem to be very tempting to use at first, but it has several drawbacks, some of them are listed below: diff --git a/docs/articles/clustering/distributed-data.md b/docs/articles/clustering/distributed-data.md index eda7900bf1c..59e7e957846 100644 --- a/docs/articles/clustering/distributed-data.md +++ b/docs/articles/clustering/distributed-data.md @@ -2,7 +2,7 @@ uid: distributed-data title: Distributed Data --- -# Distributed data +# Distributed Data Akka.DistributedData plugin can be used as in-memory, highly-available, distributed key-value store, where values conform to so called [Conflict-Free Replicated Data Types](http://hal.upmc.fr/inria-00555588/document) (CRDT). Those data types can have replicas across multiple nodes in the cluster, where DistributedData plugin has been initialized. We are free to perform concurrent updates on replicas with the same corresponding key without need of coordination (distributed locks or transactions) - all state changes will eventually converge with conflicts being automatically resolved, thanks to the nature of CRDTs. To use distributed data plugin, simply install it via NuGet: @@ -12,7 +12,7 @@ install-package Akka.DistributedData Keep in mind, that CRDTs are intended for high-availability, non-blocking read/write scenarios. However they are not a good fit, when you need strong consistency or are operating on big data. If you want to have millions of data entries, this is NOT a way to go. Keep in mind, that all data is kept in memory and, as state-based CRDTs, whole object state is replicated remotely across the nodes, when an update happens. A more efficient implementations (delta-based CRDTs) are considered for the future implementations. -## Basic operations +## Basic Operations Each CRDT defines few core operations, which are: reads, upserts and deletes. There's no explicit distinction between inserting a value and updating it. @@ -44,7 +44,7 @@ In response, you should receive `Replicator.IGetResponse` message. There are sev All `Get` requests follows the read-your-own-write rule - if you updated the data, and want to read the state under the same key immediately after, you'll always retrieve modified value, even if the `IGetResponse` message will arrive before `IUpdateResponse`. -#### Read consistency +#### Read Consistency What is a mentioned read consistency? As we said at the beginning, all updates performed within distributed data module will eventually converge. This means, we're not speaking about immediate consistency of a given value across all nodes. Therefore we can precise, what degree of consistency are we expecting: @@ -81,7 +81,7 @@ Just like in case of reads, there are several possible responses: You'll always see updates done on local node. When you perform two updates on the same key, second modify function will always see changes done by the first one. -#### Write consistency +#### Write Consistency Just like in case of reads, write consistency allows us to specify level of certainty of our updates before proceeding: @@ -143,7 +143,7 @@ class Subscriber : ReceiveActor All subscribers are removed automatically when terminated. This can be also done explicitly by sending `Replicator.Unsubscribe` message. -## Available replicated data types +## Available Replicated Data Types Akka.DistributedData specifies several data types, sharing the same `IReplicatedData` interface. All of them share some common members, such as (default) empty value or `Merge` method used to merge two replicas of the same data with automatic conflict resolution. All of those values are also immutable - this means, that any operations, which are supposed to change their state, produce new instance in result: diff --git a/docs/articles/concepts/actor-systems.md b/docs/articles/concepts/actor-systems.md index 435b02f7d5c..74f979eb816 100644 --- a/docs/articles/concepts/actor-systems.md +++ b/docs/articles/concepts/actor-systems.md @@ -55,6 +55,6 @@ The first possibility is especially well-suited for resources which are single-t > [!NOTE] > Configuring thread pools is a task best delegated to Akka.NET, simply configure in the application.conf and instantiate through an ActorSystem. -## What you should not concern yourself with +## What You Should Not Concern Yourself With An actor system manages the resources it is configured to use in order to run the actors which it contains. There may be millions of actors within one such system, after all the mantra is to view them as abundant and they weigh in at an overhead of only roughly 300 bytes per instance. Naturally, the exact order in which messages are processed in large systems is not controllable by the application author, but this is also not intended. Take a step back and relax while Akka.NET does the heavy lifting under the hood. diff --git a/docs/articles/concepts/addressing.md b/docs/articles/concepts/addressing.md index 23f75544d6b..59a9282a46f 100644 --- a/docs/articles/concepts/addressing.md +++ b/docs/articles/concepts/addressing.md @@ -11,7 +11,7 @@ This chapter describes how actors are identified and located within a possibly d The above image displays the relationship between the most important entities within an actor system, please read on for the details. -## What is an Actor Reference? +## What Is an Actor Reference? An actor reference is a subtype of `ActorRef`, whose foremost purpose is to support sending messages to the actor it represents. Each actor has access to its canonical (local) reference through the `Self` property; this reference is also included as sender reference by default for all messages sent to other actors. Conversely, during message processing the actor has access to a reference representing the sender of the current message through the sender method. @@ -29,13 +29,13 @@ There are several different types of actor references that are supported dependi * There is an actor reference which does not represent an actor but acts only as a pseudo-supervisor for the root guardian, we call it "the one who walks the bubbles of space-time". * The first logging service started before actually firing up actor creation facilities is a fake actor reference which accepts log events and prints them directly to standard output; it is `Logging.StandardOutLogger`. -## What is an Actor Path? +## What Is an Actor Path? Since actors are created in a strictly hierarchical fashion, there exists a unique sequence of actor names given by recursively following the supervision links between child and parent down towards the root of the actor system. This sequence can be seen as enclosing folders in a file system, hence we adopted the name "path" to refer to it, although actor hierarchy has some fundamental difference from file system hierarchy. An actor path consists of an anchor, which identifies the actor system, followed by the concatenation of the path elements, from root guardian to the designated actor; the path elements are the names of the traversed actors and are separated by slashes. -### What is the Difference Between Actor Reference and Path? +### What Is the Difference Between Actor Reference and Path? An actor reference designates a single actor and the life-cycle of the reference matches that actor's life-cycle; an actor path represents a name which may or may not be inhabited by an actor and the path itself does not have a life-cycle, it never becomes invalid. You can create an actor path without creating an actor, but you cannot create an actor reference without creating corresponding actor. @@ -62,11 +62,11 @@ While the logical actor path describes the functional location within one actor One important aspect is that a physical actor path never spans multiple actor systems or CLRs. This means that the logical path (supervision hierarchy) and the physical path (actor deployment) of an actor may diverge if one of its ancestors is remotely supervised. -### Actor path alias or symbolic link? +### Actor Path Alias or Symbolic Link? As in some real file-systems you might think of a "path alias" or "symbolic link" for an actor, i.e. one actor may be reachable using more than one path. However, you should note that actor hierarchy is different from file system hierarchy. You cannot freely create actor paths like symbolic links to refer to arbitrary actors. As described in the above logical and physical actor path sections, an actor path must be either logical path which represents supervision hierarchy, or physical path which represents actor deployment. -## How are Actor References obtained? +## How Are Actor References Obtained? There are two general categories to how actor references may be obtained: by creating actors or by looking them up, where the latter functionality comes in the two flavours of creating actor references from concrete actor paths and querying the logical actor hierarchy. @@ -134,7 +134,7 @@ When an actor creates a child, the actor system's deployer will decide whether t ![Remote Deployment](/images/RemoteDeployment.png) -## What is the Address part used for? +## What Is the Address Part Used For? When sending an actor reference across the network, it is represented by its path. Hence, the path must fully encode all information necessary to send messages to the underlying actor. This is achieved by encoding protocol, host and port in the address part of the path string. When an actor system receives an actor path from a remote node, it checks whether that path's address matches the address of this actor system, in which case it will be resolved to the actor's local reference. Otherwise, it will be represented by a remote actor reference. diff --git a/docs/articles/concepts/configuration.md b/docs/articles/concepts/configuration.md index 137123b688e..902dffc1f56 100644 --- a/docs/articles/concepts/configuration.md +++ b/docs/articles/concepts/configuration.md @@ -39,21 +39,21 @@ From there, we can create our `ActorSystem`: Akka.NET leverages a configuration format, called HOCON, to allow you to configure your Akka.NET applications with whatever level of granularity you want. -### What is HOCON? +### What Is HOCON? HOCON (Human-Optimized Config Object Notation) is a flexible and extensible configuration format. It allows you to configure everything from Akka.NET's `IActorRefProvider` implementation: logging, network transports, and (more commonly) how individual actors are deployed. Values returned by HOCON are strongly typed, which means you can fetch out an `int`, a `Timespan`, etc. -### What can I do with HOCON? +### What Can I Do with HOCON? HOCON allows you to embed easy-to-read configuration inside of the otherwise hard-to-read XML in App.config and Web.config. HOCON also lets you query configs by their section paths, and those sections are exposed strongly typed and parsed values you can use inside your applications. HOCON also lets you nest and/or chain sections of configuration, creating layers of granularity and providing you a semantically namespaced config. -### What is HOCON usually used for? +### What Is HOCON Usually Used For? HOCON is commonly used for tuning logging settings, enabling special modules (such as `Akka.Remote`), or configuring deployments such as the `Dispatcher` or `Router` used for a particular actor. @@ -73,7 +73,7 @@ var system = ActorSystem.Create("MyActorSystem", config); As you can see in that example, a HOCON `Config` object can be parsed from a `string` using the `ConfigurationFactory.ParseString` method. Once you have a `Config` object, you can then pass this to your `ActorSystem` inside the `ActorSystem.Create` method. -### "Deployment"? What's that? +### "Deployment"? What's That? Deployment is a vague concept, but it's closely tied to HOCON. An actor is "deployed" when it is instantiated and put into service within the `ActorSystem` somewhere. @@ -87,7 +87,7 @@ We haven't gone over what all these options mean, but **the key thing to know fo Flexible config FTW! -#### HOCON can be used inside `App.config` and `Web.config` +#### HOCON Can Be Used Inside `App.config` and `Web.config` Parsing HOCON from a `string` is handy for small configuration sections, but what if you want to be able to take advantage of [Configuration Transforms for `App.config` and `Web.config`](https://msdn.microsoft.com/en-us/library/dd465326.aspx) and all of the other nice tools we have in the `System.Configuration` namespace? @@ -173,11 +173,11 @@ var a = yourConfig.GetString("a"); Then the internal HOCON engine will match the first HOCON file that contains a definition for key `a`. In this case, that is `f0`, which returns the value "bar". -#### Why wasn't "foo" returned as the value for "a"? +#### Why Wasn't "Foo" Returned as the Value for "A"? The reason is because HOCON only searches through fallback `Config` objects if a match is NOT found earlier in the `Config` chain. If the top-level `Config` object has a match for `a`, then the fallbacks won't be searched. In this case, a match for `a` was found in `f0` so the `a=foo` in `f3` was never reached. -#### What happens when there is a HOCON key miss? +#### What Happens When There Is a HOCON Key Miss? What happens if we run the following code, given that `c` isn't defined in `f0` or `f1`? diff --git a/docs/articles/concepts/location-transparency.md b/docs/articles/concepts/location-transparency.md index 44348bdb10b..8cfc682b969 100644 --- a/docs/articles/concepts/location-transparency.md +++ b/docs/articles/concepts/location-transparency.md @@ -11,13 +11,13 @@ The previous section describes how actor paths are used to enable location trans Everything in Akka.NET is designed to work in a distributed setting: all interactions of actors use purely message passing and everything is asynchronous. This effort has been undertaken to ensure that all functions are available equally when running within a single CLR or on a cluster of hundreds of machines. The key for enabling this is to go from remote to local by way of optimization instead of trying to go from local to remote by way of generalization. -## Ways in which Transparency is Broken +## Ways in Which Transparency Is Broken What is true of Akka.NET need not be true of the application which uses it, since designing for distributed execution poses some restrictions on what is possible. The most obvious one is that all messages sent over the wire must be serializable. While being a little less obvious this includes closures which are used as actor factories (i.e. within `Props`) if the actor is to be created on a remote node. Another consequence is that everything needs to be aware of all interactions being fully asynchronous, which in a computer network might mean that it may take several minutes for a message to reach its recipient (depending on configuration). It also means that the probability for a message to be lost is much higher than within one CLR, where it is close to zero (still: no hard guarantee!). -## How is Remoting Used? +## How Is Remoting Used? We took the idea of transparency to the limit in that there is nearly no API for the remoting layer of Akka: it is purely driven by configuration. Just write your application according to the principles outlined in the previous sections, then specify remote deployment of actor sub-trees in the configuration file. This way, your application can be scaled out without having to touch the code. The only piece of the API which allows programmatic influence on remote deployment is that `Props` contain a field which may be set to a specific `Deploy` instance; this has the same effect as putting an equivalent deployment into the configuration file (if both are given, configuration file wins). diff --git a/docs/articles/concepts/message-delivery-reliability.md b/docs/articles/concepts/message-delivery-reliability.md index c2abd2b0cdf..94d83587823 100644 --- a/docs/articles/concepts/message-delivery-reliability.md +++ b/docs/articles/concepts/message-delivery-reliability.md @@ -46,7 +46,7 @@ also underlies the `Ask` pattern): The first rule is typically found also in other actor implementations while the second is specific to Akka.NET. -### Discussion: What does "at-most-once" mean? +### Discussion: What Does "At-Most-Once" Mean? When it comes to describing the semantics of a delivery mechanism, there are three basic categories: @@ -158,7 +158,7 @@ actor R1, send its reference to another remote actor R2 and have R2 send a message to R1. An example of well-defined ordering is a parent which creates an actor and immediately sends a message to it. -### Communication of failure +### Communication of Failure Please note, that the ordering guarantees discussed above only hold for user messages between actors. Failure of a child of an actor is communicated by special system messages that are not ordered relative to ordinary user messages. In @@ -173,7 +173,7 @@ a user and system message cannot guarantee the ordering of their dequeue times. ## The Rules for In-App (Local) Message Sends -### Be careful what you do with this section! +### Be Careful What You Do With This Section! Relying on the stronger reliability in this section is not recommended since it will bind your application to local-only deployment: an application may have to @@ -231,7 +231,7 @@ possibly non-exhaustive list of counter-indications is: This list has been compiled carefully, but other problematic scenarios may have escaped our analysis. -### How does Local Ordering relate to Network Ordering +### How Does Local Ordering Relate to Network Ordering The rule that for a given pair of actors, messages sent directly from the first to the second will not be received out-of-order holds for messages sent over the network with the TCP based Akka.NET remote transport protocol. @@ -244,7 +244,7 @@ As explained in the previous section local message sends obey transitive causal It might take longer time for M1 to "travel" to node-3 than it takes for M2 to "travel" to node-3 via node-2. -## Higher-level abstractions +## Higher-Level Abstractions Based on a small and consistent tool set in Akka's core, Akka.NET also provides powerful, higher-level abstractions on top it. @@ -310,7 +310,7 @@ The dead letter service follows the same rules with respect to delivery guarantees as all other message sends, hence it cannot be used to implement guaranteed delivery. -### How do I Receive Dead Letters? +### How Do I Receive Dead Letters? An actor can subscribe to class `Akka.Actor.DeadLetter` on the event stream, see [Event stream](xref:event-bus) for how to do that. The subscribed actor will then receive all dead @@ -322,7 +322,7 @@ determine that a send operation is failed, which for a remote send can be the local system (if no network connection can be established) or the remote one (if the actor you are sending to does not exist at that point in time). -### Dead Letters Which are (Usually) not Worrisome +### Dead Letters Which Are (Usually) Not Worrisome Every time an actor does not terminate by its own decision, there is a chance that some messages which it sends to itself are lost. There is one which diff --git a/docs/articles/concepts/supervision.md b/docs/articles/concepts/supervision.md index c50f11ada66..7934d835b65 100644 --- a/docs/articles/concepts/supervision.md +++ b/docs/articles/concepts/supervision.md @@ -80,7 +80,7 @@ Monitoring is particularly useful if a supervisor cannot simply restart its chil Another common use case is that an actor needs to fail in the absence of an external resource, which may also be one of its own children. If a third party terminates a child by way of the `system.Stop(child)` method or sending a `PoisonPill`, the supervisor might well be affected. -### Delayed restarts with the BackoffSupervisor pattern +### Delayed Restarts with the BackoffSupervisor Pattern Provided as a built-in pattern the `Akka.Pattern.BackoffSupervisor` implements the so-called exponential backoff supervision strategy, starting a child actor again when it fails, each time with a growing time delay between restarts. diff --git a/docs/articles/concepts/terminology.md b/docs/articles/concepts/terminology.md index c45574e0908..06cb101041b 100644 --- a/docs/articles/concepts/terminology.md +++ b/docs/articles/concepts/terminology.md @@ -25,13 +25,13 @@ A method call is considered synchronous if the caller cannot make progress until A synchronous API may use blocking to implement synchrony, but this is not a necessity. A very CPU intensive task might give a similar behavior as blocking. In general, it is preferred to use asynchronous APIs, as they guarantee that the system is able to progress. Actors are asynchronous by nature: an actor can progress after sending a message without waiting for the delivery to happen. -## Non-blocking vs. Blocking +## Non-Blocking vs. Blocking We talk about blocking if the delay of one thread can indefinitely delay some of the other threads. A good example is a resource which can be used exclusively by one thread using mutual exclusion. If a thread holds on to the resource indefinitely (for example accidentally running an infinite loop) other threads waiting on the resource can not progress. In contrast, non-blocking means that no thread is able to indefinitely delay others. Non-blocking operations are preferred to blocking ones, as the overall progress of the system is not trivially guaranteed when it contains blocking operations. -## Deadlock vs. Starvation vs. Live-lock +## Deadlock vs. Starvation vs. Live-Lock Deadlock arises when several participants are waiting on each other to reach a specific state to be able to progress. As none of them can progress without some other participant to reach a certain state (a "Catch-22" problem), all affected subsystems stall. Deadlock is closely related to blocking, as it is necessary that a participant thread be able to delay the progression of other threads indefinitely. @@ -46,21 +46,21 @@ A Race condition is when an assumption about the ordering of a set of events mig > [!NOTE] > The only guarantee that Akka.NET provides about messages sent between a given pair of actors is that their order is always preserved. see [Message Delivery Reliability](xref:message-delivery-reliability) -## Non-blocking Guarantees (Progress Conditions) +## Non-Blocking Guarantees (Progress Conditions) As discussed in the previous sections, blocking is undesirable for several reasons, including the dangers of deadlocks and reduced throughput in the system. In the following sections we discuss various non-blocking properties with different strength. -### Wait-freedom +`### Wait-Freedom A method is wait-free if every call is guaranteed to finish in a finite number of steps. If a method is bounded wait-free, then the number of steps has a finite upper bound. From this definition it follows that wait-free methods are never blocking, therefore deadlock can not happen. Additionally, as each participant can progress after a finite number of steps (when the call finishes), wait-free methods are free of starvation. -### Lock-freedom +### Lock-Freedom Lock-freedom is a weaker property than wait-freedom. In the case of lock-free calls, infinitely often some method finishes in a finite number of steps. This definition implies that no deadlock is possible for lock-free calls. On the other hand, the guarantee that some call finishes in a finite number of steps is not enough to guarantee that all of them eventually finish. In other words, lock-freedom is not enough to guarantee the lack of starvation. -### Obstruction-freedom +### Obstruction-Freedom Obstruction-freedom is the weakest non-blocking guarantee discussed here. A method is called obstruction-free if there is a point in time after which it executes in isolation (other threads make no steps, e.g.: become suspended), it finishes in a bounded number of steps. All lock-free objects are obstruction-free, but the opposite is generally not true. diff --git a/docs/articles/discovery/index.md b/docs/articles/discovery/index.md index ff754751c79..3b1217f2c57 100644 --- a/docs/articles/discovery/index.md +++ b/docs/articles/discovery/index.md @@ -10,7 +10,7 @@ Akka.NET Discovery provides an interface around various ways of locating service * DNS * Aggregate -## How it works +## How It Works Loading the extension: @@ -75,7 +75,7 @@ akka.discovery.config.services = { Where the above block defines two services, `service1` and `service2`. Each service can have multiple endpoints. -## Discovery Method: Aggregate multiple discovery methods +## Discovery Method: Aggregate Multiple Discovery Methods Aggregate discovery allows multiple discovery methods to be aggregated e.g. try and resolve via DNS and fall back to configuration. diff --git a/docs/articles/hocon/index.md b/docs/articles/hocon/index.md index 1316e51cac2..4019ba8b6d7 100644 --- a/docs/articles/hocon/index.md +++ b/docs/articles/hocon/index.md @@ -26,7 +26,7 @@ You can play around with HOCON syntax in real-time by going to [hocon-playground Much of this is defined with reference to JSON; you can find the JSON spec at . -### Unchanged from JSON +### Unchanged From JSON * files must be valid UTF-8 * quoted strings are in the same format as JSON strings @@ -37,7 +37,7 @@ Much of this is defined with reference to JSON; you can find the JSON spec at Sam Covington, IVC Business Systems: We had an in-house "Actor" system that we replaced with Akka.Net, which allowed us to innovate and be productive elsewhere, and not reinvent the wheel(not to mention test it to death). This back end of Microservices forms the basis of all of our products and services. We're using it in our Enterprise Social Product, and our new Livescan Office product for Livescan fingerprinting customers. -### Concurrency/parallelism (any app) +### Concurrency/Parallelism (Any App) Share of an article by Joel Mueller, Software Architect, SNL Financial @@ -30,19 +30,19 @@ Share of an article by Joel Mueller, Software Architect, SNL Financial Master/Worker, Compute Grid, MapReduce etc. -### Batch processing (any industry) +### Batch Processing (Any Industry) Camel integration to hook up with batch data sources Actors divide and conquer the batch workloads -### Communications Hub (Telecom, Web media, Mobile media) +### Communications Hub (Telecom, Web Media, Mobile Media) * [EventDay: real-time conference and event management at scale with Akka.NET](https://youtu.be/G3ZafPNI-hk?t=6m16s) -### Gaming and Betting (MOM, online gaming, betting) +### Gaming and Betting (MOM, Online Gaming, Betting) Scale up, scale out, fault-tolerance / HA -### Business Intelligence/Data Mining/general purpose crunching +### Business Intelligence/Data Mining/General Purpose Crunching Tweet from Philip Laureano with links. diff --git a/docs/articles/intro/tutorial-1.md b/docs/articles/intro/tutorial-1.md index 601ac9afd6c..4e367d4bc4b 100644 --- a/docs/articles/intro/tutorial-1.md +++ b/docs/articles/intro/tutorial-1.md @@ -3,7 +3,7 @@ uid: tutorial-1 title: Part 1. Top-level Architecture --- -# Part 1: Top-level Architecture +# Part 1: Top-Level Architecture In this and the following chapters, we will build a sample Akka.NET application to introduce you to the language of actors and how solutions can be formulated @@ -280,7 +280,7 @@ All we need now is to tie this up with a class with the `main` entry point: This application does very little for now, but we have the first actor in place and we are ready to extend it further. -## What is next? +## What Is Next? In the following chapters we will grow the application step-by-step: diff --git a/docs/articles/intro/tutorial-2.md b/docs/articles/intro/tutorial-2.md index 8b6313d59dd..efe1f0f3bb6 100644 --- a/docs/articles/intro/tutorial-2.md +++ b/docs/articles/intro/tutorial-2.md @@ -61,7 +61,7 @@ These are the rules in Akka.NET for message sends: * At-most-once delivery, i.e. no guaranteed delivery. * Message ordering is maintained per sender, receiver pair. -### What Does "at-most-once" Mean? +### What Does "At-Most-Once" Mean? When it comes to describing the semantics of a delivery mechanism, there are three basic categories: @@ -184,7 +184,7 @@ together: [!code-csharp[Main](../../../src/core/Akka.Docs.Tutorials/Tutorial2/DeviceSpec.cs?name=device-write-read-test)] -## What is Next? +## What Is Next? So far, we have started designing our overall architecture, and we wrote our first actor directly corresponding to the domain. We now have to create the component that is responsible for maintaining groups of devices and the device diff --git a/docs/articles/intro/tutorial-3.md b/docs/articles/intro/tutorial-3.md index 9fe3c04889d..e4b98c9aa5d 100644 --- a/docs/articles/intro/tutorial-3.md +++ b/docs/articles/intro/tutorial-3.md @@ -196,7 +196,7 @@ the device group actor, with the only difference that it creates device group ac We leave tests of the device manager as an exercise as it is very similar to the tests we have written for the group actor. -## What is Next? +## What Is Next? We have now a hierarchical component for registering and tracking devices and recording measurements. We have seen some conversation patterns like: diff --git a/docs/articles/intro/use-case-and-deployment-scenarios.md b/docs/articles/intro/use-case-and-deployment-scenarios.md index 43f3e4a3a71..2596243d856 100644 --- a/docs/articles/intro/use-case-and-deployment-scenarios.md +++ b/docs/articles/intro/use-case-and-deployment-scenarios.md @@ -2,7 +2,7 @@ uid: use-case-and-deployment-scenarios title: Use-case and Deployment Scenarios --- -# Use-case and Deployment Scenarios +# Use-Case and Deployment Scenarios ## Console Application @@ -41,7 +41,7 @@ namespace Foo.Bar ## ASP.NET -### Creating the Akka.NET resources +### Creating the Akka.NET Resources Hosting inside an ASP.NET application is easy. The `Global.asax` would be the designated place to start. @@ -71,7 +71,7 @@ As you can see the main point here is keeping a static reference to your `ActorS Typically you use a very lightweight `ActorSystem` inside ASP.NET applications, and offload heavy-duty work to a separate Windows Service via Akka.Remote / Akka.Cluster. -### Interaction between Controllers and Akka.NET +### Interaction Between Controllers and Akka.NET In the sample below, we use an Web API Controller: diff --git a/docs/articles/intro/what-is-akka.md b/docs/articles/intro/what-is-akka.md index 43a8690602c..ec7cd6c91ad 100644 --- a/docs/articles/intro/what-is-akka.md +++ b/docs/articles/intro/what-is-akka.md @@ -2,7 +2,7 @@ uid: what-is-akka title: What is Akka --- -# What is Akka.NET? +# What Is Akka.NET? Welcome to Akka.NET, a set of open-source libraries for designing scalable, resilient systems that span processor cores and networks. Akka allows you to focus on meeting business needs instead @@ -32,7 +32,7 @@ By learning Akka.NET and its actor model, you will gain access to a vast and dee distributed/parallel systems problems in a uniform programming model where everything fits together tightly and efficiently. -## What is the Actor Model? +## What Is the Actor Model? The characteristics of today's computing environments are vastly different from the ones in use when the programming models of yesterday were conceived. Actors were invented decades ago by [Carl Hewitt](https://en.wikipedia.org/wiki/Carl_Hewitt#Actor_model). diff --git a/docs/articles/intro/what-problems-does-actor-model-solve.md b/docs/articles/intro/what-problems-does-actor-model-solve.md index 0b96c0e2b35..7e4e30dae35 100644 --- a/docs/articles/intro/what-problems-does-actor-model-solve.md +++ b/docs/articles/intro/what-problems-does-actor-model-solve.md @@ -2,14 +2,14 @@ uid: what-problems-does-actor-model-solve title: What problems does the actor model solve? --- -# What problems does the actor model solve? +# What Problems Does the Actor Model Solve? Akka.NET uses the actor model to overcome the limitations of traditional object-oriented programming models and meet the unique challenges of highly distributed systems. To fully understand why the actor model is necessary, it helps to identify mismatches between traditional approaches to programming and the realities of concurrent and distributed computing. -## The illusion of encapsulation +## The Illusion of Encapsulation Object-oriented programming (OOP) is a widely-accepted, familiar programming model. One of its core pillars is _encapsulation_. Encapsulation dictates that the internal data of an object is not accessible directly from the outside; @@ -83,7 +83,7 @@ As a result, threads are what really drive execution: are inefficient and easily lead to deadlocks in any application of real-world scale.** * **Locks work locally, attempts to make them distributed exist, but offer limited potential for scaling out.** -## The illusion of shared memory on modern computer architectures +## The Illusion of Shared Memory on Modern Computer Architectures Programming models of the 80'-90's conceptualize that writing to a variable means writing to a memory location directly (which somewhat muddies the water that local variables might exist only in registers). On modern architectures - @@ -107,7 +107,7 @@ or which atomic structures to use is a dark art. * **There is no real shared memory anymore, CPU cores pass chunks of data (cache lines) explicitly to each other just as computers on a network do. Inter-CPU communication and network communication have more in common than many realize. Passing messages is the norm now be it across CPUs or networked computers.** * **Instead of hiding the message passing aspect through variables marked as shared or using atomic data structures, a more disciplined and principled approach is to keep state local to a concurrent entity and propagate data or events between concurrent entities explicitly via messages.** -## The illusion of a call stack +## The Illusion of a Call Stack Today, we often take call stacks for granted. But, they were invented in an era where concurrent programming was not as important because multi-CPU systems were not common. Call stacks do not cross threads and hence, @@ -154,7 +154,7 @@ involved (where message losses are to be expected).** (a long queue), delays caused by garbage collection, etc. In face of these, concurrent systems should handle response deadlines in the form of timeouts, just like networked/distributed systems.** -## How the actor model meets the needs of concurrent, distributed systems +## How the Actor Model Meets the Needs of Concurrent, Distributed Systems As described in the sections above, common programming practices cannot properly address the needs of modern concurrent and distributed systems. @@ -170,7 +170,7 @@ In particular, we would like to: The actor model accomplishes all of these goals. The following topics describe how. -## Usage of message passing avoids locking and blocking +## Usage of Message Passing Avoids Locking and Blocking Instead of calling methods, actors send messages to each other. Sending a message does not transfer the thread of execution from the sender to the destination. An actor can send a message and continue without blocking. @@ -230,7 +230,7 @@ This is a very simple model and it solves the issues enumerated previously: * State of actors is local and not shared, changes and data is propagated via messages, which maps to how modern memory hierarchy actually works. In many cases, this means transferring over only the cache lines that contain the data in the message while keeping local state and data cached at the original core. The same model maps exactly to remote communication where the state is kept in the RAM of machines and changes/data is propagated over the network as packets. -## Actors handle error situations gracefully +## Actors Handle Error Situations Gracefully Since we have no longer a shared call stack between actors that send messages to each other, we need to handle error situations differently. There are two kinds of errors we need to consider: diff --git a/docs/articles/networking/multi-node-test-kit.md b/docs/articles/networking/multi-node-test-kit.md index da5b9930d60..71bfdcd5556 100644 --- a/docs/articles/networking/multi-node-test-kit.md +++ b/docs/articles/networking/multi-node-test-kit.md @@ -149,7 +149,7 @@ In the `JoinInProgressMultiNodeConfig`, we define two `RoleName`s for the two no Also we configured each node to represent specific role `[frontend,backend]` in the cluster. You can attach arbitrary config instance(s) to individual node or group of nodes by calling `NodeConfig(IEnumerable roles, IEnumerable configs)`. -#### Step 2 - Define a Class for Your Spec, Inherit from `MultiNodeSpec` +#### Step 2 - Define a Class for Your Spec, Inherit From `MultiNodeSpec` The next step is to subclass `MultiNodeSpec` and create a class that each of your individual nodes will run. diff --git a/docs/articles/networking/serialization.md b/docs/articles/networking/serialization.md index e424bbfc079..464478a783e 100644 --- a/docs/articles/networking/serialization.md +++ b/docs/articles/networking/serialization.md @@ -176,7 +176,7 @@ Akka.NET makes it extremely easy to create custom serializers to handle a wide v All serializers in Akka.NET inherit from `Akka.Serialization.Serializer`. So, to create a custom serializer, all that is needed is a class that inherits from this base class. -### Creating new Serializers +### Creating New Serializers A custom `Serializer` has to inherit from `Akka.Serialization.Serializer` and can be defined like this: @@ -186,7 +186,7 @@ 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. -### programmatically change NewtonSoft JSON serializer settings +### Programmatically Change NewtonSoft JSON Serializer Settings You can change the JSON serializer behavior by using the `NewtonSoftJsonSerializerSetup` class to programmatically change the settings used inside the Json serializer by passing it into the an `ActorSystemSetup`. @@ -290,11 +290,11 @@ Sending messages to a reference pointing the old actor will not be delivered to This requires that you know at least which type of address will be supported by the system which will deserialize the resulting actor reference; if you have no concrete address handy you can create a dummy one for the right protocol using `new Address(protocol, "", "", 0)` (assuming that the actual transport used is as lenient as Akka's `RemoteActorRefProvider`). -### Deep serialization of Actors +### Deep Serialization of Actors The recommended approach to do deep serialization of internal actor state is to use [Akka Persistence](xref:persistence-architecture). -## How to setup Hyperion as default serializer +## How to Setup Hyperion as the Default Serializer Starting from Akka.NET v1.5, default Newtonsoft.Json serializer will be replaced in the favor of [Hyperion](https://github.com/akkadotnet/Hyperion). This change may break compatibility with older actors still using json serializer for remoting or persistence. If it's possible, it's advised to migrate to it already. To do so, first you need to reference hyperion serializer as NuGet package inside your project: @@ -315,7 +315,7 @@ akka { } ``` -## Danger of polymorphic serializer +## Danger of Polymorphic Serializer One of the danger of polymorphic serializers is the danger of unsafe object type injection into the serialization/de-serialization chain. This issue applies to any type of polymorphic serializer, @@ -370,7 +370,7 @@ akka.actor.serialization-settings.hyperion.disallow-unsafe-type = false > preferably inside a closed network system. -## Cross platform serialization compatibility in Hyperion +## Cross Platform Serialization Compatibility in Hyperion There are problems that can arise when migrating from old .NET Framework to the new .NET Core standard, mainly because of breaking namespace and assembly name changes between these platforms. Hyperion implements a generic way of addressing this issue by transforming the names of these incompatible names during deserialization. @@ -451,7 +451,7 @@ var system = ActorSystem.Create("actorSystem", bootstrap); In the example above, we're using compiler directives to make sure that the correct name transform are used during compilation. -## Complex object serialization using Hyperion +## Complex Object Serialization Using Hyperion One of the limitation of a reflection based serializer is that it would fail to serialize objects with complex internal looping references in its properties or fields and ended up throwing @@ -502,7 +502,7 @@ serializing and send across the wire: } ``` -### Creating and declaring `Surrogate`s via HOCON +### Creating and Declaring `Surrogate` via HOCON To create a serializer surrogate in HOCON, we would first create a class that inherits from the `Surrogate` class: @@ -539,7 +539,7 @@ akka.actor { } ``` -### Creating and declaring `Surrogate`s programmatically using `HyperionSerializerSetup` +### Creating and Declaring `Surrogate` Programmatically Using `HyperionSerializerSetup` We can also use `HyperionSerializerSetup` to declare our surrogates: diff --git a/docs/articles/persistence/at-least-once-delivery.md b/docs/articles/persistence/at-least-once-delivery.md index de7393edb0e..2f6f0ecad9b 100644 --- a/docs/articles/persistence/at-least-once-delivery.md +++ b/docs/articles/persistence/at-least-once-delivery.md @@ -17,7 +17,7 @@ Members: * `MaxUnconfirmedMessages` is a virtual property which determines the maximum number of unconfirmed deliveries to hold in memory. After this threshold is exceeded, any `Deliver` method will raise `MaxUnconfirmedMessagesExceededException`. It may be overridden or configured inside HOCON configuration under *akka.persistence.at-least-once-delivery.max-unconfirmed-messages* path (100 000 by default). * `UnconfirmedCount` property shows the number of unconfirmed messages. -## Relationship between Deliver and ConfirmDelivery +## Relationship Between Deliver and ConfirmDelivery To send messages to the destination path, use the `Deliver` method after you have persisted the intent to send the message. diff --git a/docs/articles/persistence/custom-serialization.md b/docs/articles/persistence/custom-serialization.md index b7ff3ea84d1..a1fa5e342a9 100644 --- a/docs/articles/persistence/custom-serialization.md +++ b/docs/articles/persistence/custom-serialization.md @@ -2,7 +2,7 @@ uid: custom-serialization title: Custom serialization --- -# Custom serialization +# Custom Serialization Serialization of snapshots and payloads of Persistent messages is configurable with Akka's Serialization infrastructure. For example, if an application wants to serialize diff --git a/docs/articles/persistence/event-adapters.md b/docs/articles/persistence/event-adapters.md index f69e96093a7..5751fd5b407 100644 --- a/docs/articles/persistence/event-adapters.md +++ b/docs/articles/persistence/event-adapters.md @@ -2,7 +2,7 @@ uid: event-adapters title: Event Adapters --- -# Event adapters +# Event Adapters In long running projects using event sourcing sometimes the need arises to detach the data model from the domain model completely. diff --git a/docs/articles/persistence/event-sourcing.md b/docs/articles/persistence/event-sourcing.md index 3053889bdb8..d552fa87e00 100644 --- a/docs/articles/persistence/event-sourcing.md +++ b/docs/articles/persistence/event-sourcing.md @@ -2,7 +2,7 @@ uid: persistent-actors title: Event sourcing --- -# Event sourcing +# Event Sourcing The basic idea behind Event Sourcing is quite simple. A persistent actor receives a (non-persistent) command which is first validated if it can be applied to the current state. Here validation can mean anything from simple inspection of a command message's fields up to a conversation with several external services, for example. If validation succeeds, events are generated from the command, representing the effect of the command. These events are then persisted and, after successful persistence, used to change the actor's state. When the persistent actor needs to be recovered, only the persisted events are replayed of which we know that they can be successfully applied. In other words, events cannot fail when being replayed to a persistent actor, in contrast to commands. Event sourced actors may of course also process commands that do not change application state such as query commands for example. @@ -46,7 +46,7 @@ The number of concurrent recoveries of recoveries that can be in progress at the > [!NOTE] > Accessing the `Sender` for replayed messages will always result in a `DeadLetters` reference, as the original sender is presumed to be long gone. If you indeed have to notify an actor during recovery in the future, store its `ActorPath` explicitly in your persisted events. -### Recovery customization +### Recovery Customization Applications may also customize how recovery is performed by returning a customized `Recovery` object in the recovery method of a `UntypedPersistentActor`. @@ -68,7 +68,7 @@ Recovery can be disabled by returning `Recovery.None` in the recovery property o public override Recovery Recovery => Recovery.None; ``` -### Recovery status +### Recovery Status A persistent actor can query its own recovery status via the methods @@ -101,7 +101,7 @@ The actor will always receive a `RecoveryCompleted` message, even if there are n If there is a problem with recovering the state of the actor from the journal, `OnRecoveryFailure` is called (logging the error by default) and the actor will be stopped. -## Internal stash +## Internal Stash The persistent actor has a private stash for internally caching incoming messages during `Recovery` or the `Persist` \ `PersistAll` method persisting events. However You can use inherited stash or create one or more stashes if needed. The internal stash doesn't interfere with these stashes apart from user inherited `UnstashAll` method, which prepends all messages in the inherited stash to the internal stash instead of mailbox. Hence, If the message in the inherited stash need to be handled after the messages in the internal stash, you should call inherited un-stash method. @@ -128,7 +128,7 @@ Context.System.DefaultInternalStashOverflowStrategy > [!NOTE] > The bounded mailbox should be avoid in the persistent actor, because it may be discarding the messages come from Storage backends. You can use bounded stash instead of bounded mailbox. -## Relaxed local consistency requirements and high throughput use-cases +## Relaxed Local Consistency Requirements and High Throughput Use-Cases If faced with relaxed local consistency requirements and high throughput demands sometimes `UntypedPersistentActor` and its persist may not be enough in terms of consuming incoming Commands at a high rate, because it has to wait until all Events related to a given Command are processed in order to start processing the next Command. While this abstraction is very useful for most cases, sometimes you may be faced with relaxed requirements about consistency – for example you may want to process commands as fast as you can, assuming that the Event will eventually be persisted and handled properly in the background, retroactively reacting to persistence failures if needed. @@ -144,7 +144,7 @@ In the below example, the event callbacks may be called "at any time", even afte > [!WARNING] > The callback will not be invoked if the actor is restarted (or stopped) in between the call to `PersistAsync` and the journal has confirmed the write. -## Deferring actions until preceding persist handlers have executed +## Deferring Actions Until Preceding Persist Handlers Have Executed Sometimes when working with `PersistAsync` or `Persist` you may find that it would be nice to define some actions in terms of happens-after the previous `PersistAsync`/`Persist` handlers have been invoked. `PersistentActor` provides an utility method called `DeferAsync`, which works similarly to `PersistAsync` yet does not persist the passed in event. It is recommended to use it for read operations, and actions which do not have corresponding events in your domain model. @@ -177,7 +177,7 @@ You can also call `DeferAsync` with `Persist`. > [!WARNING] > The callback will not be invoked if the actor is restarted (or stopped) in between the call to `DeferAsync` and the journal has processed and confirmed all preceding writes.. -## Nested persist calls +## Nested Persist Calls It is possible to call `Persist` and `PersistAsync` inside their respective callback blocks and they will properly retain both the thread safety (including the right value of `Sender`) as well as stashing guarantees. @@ -230,7 +230,7 @@ If persistence of an event is rejected before it is stored, e.g. due to serializ If there is a problem with recovering the state of the actor from the journal when the actor is started, `OnRecoveryFailure` is called (logging the error by default), and the actor will be stopped. Note that failure to load snapshot is also treated like this, but you can disable loading of snapshots if you for example know that serialization format has changed in an incompatible way, see [Recovery customization](#recovery-customization). -## Atomic writes +## Atomic Writes Each event is of course stored atomically, but it is also possible to store several events atomically by using the `PersistAll` or `PersistAllAsync` method. That means that all events passed to that method are stored or none of them are stored if there is an error. @@ -238,11 +238,11 @@ The recovery of a persistent actor will therefore never be done partially with o Some journals may not support atomic writes of several events and they will then reject the `PersistAll` command, i.e. `OnPersistRejected` is called with an exception (typically `NotSupportedException`). -## Batch writes +## Batch Writes In order to optimize throughput when using `PersistAsync`, a persistent actor internally batches events to be stored under high load before writing them to the journal (as a single batch). The batch size is dynamically determined by how many events are emitted during the time of a journal round-trip: after sending a batch to the journal no further batch can be sent before confirmation has been received that the previous batch has been written. Batch writes are never timer-based which keeps latencies at a minimum. -## Message deletion +## Message Deletion It is possible to delete all messages (journaled by a single persistent actor) up to a specified sequence number; Persistent actors may call the `DeleteMessages` method to this end. @@ -255,7 +255,7 @@ The result of the `DeleteMessages` request is signaled to the persistent actor w Message deletion doesn't affect the highest sequence number of the journal, even if all messages were deleted from it after `DeleteMessages` invocation. -## Persistence status handling +## Persistence Status Handling | Method | Success | Failure / Rejection | After failure handler invoked |------ |------ |------ |------ @@ -271,7 +271,7 @@ For critical failures such as recovery or persisting events failing the persiste > [!NOTE] > Journal implementations may choose to implement a retry mechanism, e.g. such that only after a write fails N number of times a persistence failure is signalled back to the user. In other words, once a journal returns a failure, it is considered fatal by Akka Persistence, and the persistent actor which caused the failure will be stopped. Check the documentation of the journal implementation you are using for details if/how it is using this technique. -## Safely shutting down persistent actors +## Safely Shutting Down Persistent Actors Special care should be given when shutting down persistent actors from the outside. With normal Actors it is often acceptable to use the special `PoisonPill` message to signal to an Actor that it should stop itself once it receives this message – in fact this message is handled automatically by Akka, leaving the target actor no way to refuse stopping itself when given a poison pill. @@ -286,7 +286,7 @@ The example below highlights how messages arrive in the Actor's mailbox and how [!code-csharp[Main](../../../src/core/Akka.Docs.Tests/Persistence/PersistentActor/AvoidPoisonPill.cs?name=AvoidPoisonPill2)] -## Replay filter +## Replay Filter There could be cases where event streams are corrupted and multiple writers (i.e. multiple persistent actor instances) journaled different messages with the same sequence number. In such a case, you can configure how you filter replayed messages from multiple writers, upon recovery. diff --git a/docs/articles/persistence/persistence-query.md b/docs/articles/persistence/persistence-query.md index 6c0b153b17c..192842a8506 100644 --- a/docs/articles/persistence/persistence-query.md +++ b/docs/articles/persistence/persistence-query.md @@ -8,7 +8,7 @@ Akka persistence query complements Persistence by providing a universal asynchro The most typical use case of persistence query is implementing the so-called query side (also known as "read side") in the popular CQRS architecture pattern - in which the writing side of the application (e.g. implemented using akka persistence) is completely separated from the "query side". Akka Persistence Query itself is not directly the query side of an application, however it can help to migrate data from the write side to the query side database. In very simple scenarios Persistence Query may be powerful enough to fulfill the query needs of your app, however we highly recommend (in the spirit of CQRS) of splitting up the write/read sides into separate datastores as the need arises. -## Design overview +## Design Overview Akka Persistence Query is purposely designed to be a very loosely specified API. This is in order to keep the provided APIs general enough for each journal implementation to be able to expose its best features, e.g. a SQL journal can use complex SQL queries or if a journal is able to subscribe to a live event stream this should also be possible to expose the same API - a typed stream of events. @@ -43,7 +43,7 @@ Journal implementers are encouraged to put this identifier in a variable known t Read journal implementations are available as Community plugins. -### Predefined queries +### Predefined Queries Akka persistence query comes with a number of query interfaces built in and suggests Journal implementors to implement them according to the semantics described below. It is important to notice that while these query types are very common a journal is not required to implement all of them - for example because in a given journal such query would be significantly inefficient. @@ -194,7 +194,7 @@ As you can see, we can use all the usual stream combinators available from Akka If your usage does not require a live stream, you can use the `CurrentEventsByTag` query. -### Materialized values of queries +### Materialized Values of Queries Journals are able to provide additional information related to a query by exposing materialized values, which are a feature of Akka Streams that allows to expose additional values at stream materialization time. @@ -247,7 +247,7 @@ query .RunWith(Sink.Ignore(), mat); ``` -## Performance and denormalization +## 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. @@ -258,7 +258,7 @@ On the other hand the same application may have some complex statistics view or > [!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). -### Materialize view to Reactive Streams compatible datastore +### Materialize View to Reactive Streams Compatible Datastore If the read datastore exposes a Reactive Streams interface then implementing a simple projection is as simple as, using the read-journal and feeding it into the databases driver interface, for example like so: @@ -280,7 +280,7 @@ readJournal .RunWith(Sink.FromSubscriber(dbBatchWriter), mat); // write batches to read-side database ``` -### Materialize view using SelectAsync +### Materialize View Using SelectAsync If the target database does not provide a reactive streams Subscriber that can perform writes, you may have to implement the write logic using plain functions or Actors instead. @@ -305,7 +305,7 @@ readJournal .RunWith(Sink.Ignore(), mat); ``` -### Resumable projections +### Resumable Projections Sometimes you may need to implement "resumable" projections, that will not start from the beginning of time each time when run. In this case you will need to store the sequence number (or offset) of the processed event and use it the next time this projection is started. This pattern is not built-in, however is rather simple to implement yourself. diff --git a/docs/articles/persistence/persistence-testing.md b/docs/articles/persistence/persistence-testing.md index b4124cda669..d437caec0ce 100644 --- a/docs/articles/persistence/persistence-testing.md +++ b/docs/articles/persistence/persistence-testing.md @@ -7,14 +7,14 @@ title: Persistence Testing It is hard to make persistence work properly. You can rely on Akka. Persistence does, but its own code can be made reliable only by writing tests. For this sake, Akka.Net includes a specialized journal and snapshot store to aid in testing persistent actors. -## How to get started +## How to Get Started Go and install an additional NuGet package `Akka.Persistence.TestKit.Xunit2`. That package includes a specialized persistent journal named `TestJournal` and a snapshot store named `TestSnpashotStore` which will allow controlling behavior of all persistence operations to simulate network failures, serialization problems, and other issues. For convenience, the package includes `PersistenceTestKit` class to aid in writing unit tests for Akka.Net actor system. This class has a set of methods to alter different aspects of the journal and snapshot store. -## Persistence testing in action +## Persistence Testing in Action We need a persistent actor that we will test. Our actor will do simple counting, upon request it will increase, decrease or return currently stored value. @@ -108,7 +108,7 @@ protected override void OnRecover(object message) So now we are ready to write some tests. -### Writing tests +### Writing Tests The current implementation has one fundamental flaw - actor persist changes in fire-n-forget style, that is no reliable as underlying persistence can fail due to hundreds of reasons. We can verify that by writing a test which simulates network @@ -135,7 +135,7 @@ public class CounterActorTests : PersistenceTestKit When we will launch this test it will fail, because the persistence journal failed when we tried to tell `inc` command to the actor. The actor failed with the journal and `read` was never delivered anb we had not received any answer. -### How to make things better +### How to Make Things Better ## Reference @@ -189,7 +189,7 @@ After the test code block is executed, journal and snapshot store will be switch **Important!** All methods are `async`, this means that they **must** be awaited for proper execution. -### Built-in journal behaviors +### Built-in Journal Behaviors Out of the box, the package has the following behaviors: @@ -207,7 +207,7 @@ All methods have additional overload to add artificial delay - `*WithDelay`, i.e When all mentioned above behaviors are not enough, it is always possible to implement custom one by implementing the `IJournalInterceptor` interface. An instance of a custom interceptor can be set using the `SetInterceptorAsync` method. -### Built-in snapshot store behaviors +### Built-in Snapshot Store Behaviors Snapshot store behaviors are following the same naming pattern as journal behaviors: diff --git a/docs/articles/persistence/persistent-fsm.md b/docs/articles/persistence/persistent-fsm.md index 3de525c290b..8592ee33ff5 100644 --- a/docs/articles/persistence/persistent-fsm.md +++ b/docs/articles/persistence/persistent-fsm.md @@ -66,7 +66,7 @@ Stop().Applying(OrderDiscarded.Instance).AndThen(cart => On recovery state data is initialized according to the latest available snapshot, then the remaining domain events are replayed, triggering the `ApplyEvent` method. -## Periodical snapshot by snapshot-after +## Periodical Snapshot by Snapshot-After You can enable periodical `SaveStateSnapshot()` calls in `PersistentFSM` if you turn the following flag on in `reference.conf` diff --git a/docs/articles/persistence/snapshots.md b/docs/articles/persistence/snapshots.md index 48a64b772e0..8e1d11295ea 100644 --- a/docs/articles/persistence/snapshots.md +++ b/docs/articles/persistence/snapshots.md @@ -42,7 +42,7 @@ If not specified, they default to `SnapshotSelectionCriteria.Latest` which selec > Since it is acceptable for some applications to not use any snap-shotting, it is legal to not configure a snapshot store. However, Akka will log a warning message when this situation is detected and then continue to operate until an actor tries to store a snapshot, at which point the operation will fail (by replying with an `SaveSnapshotFailure` for example). > Note that `Cluster Sharding` is using snapshots, so if you use Cluster Sharding you need to define a snapshot store plugin. -## Snapshot deletion +## Snapshot Deletion A persistent actor can delete individual snapshots by calling the `DeleteSnapshot` method with the sequence number of when the snapshot was taken. @@ -51,7 +51,7 @@ persistent actors should use the `deleteSnapshots` method. Depending on the jour best practice to do specific deletes with `deleteSnapshot` or to include a `minSequenceNr` as well as a `maxSequenceNr` for the `SnapshotSelectionCriteria`. -## Snapshot status handling +## Snapshot Status Handling Saving or deleting snapshots can either succeed or fail – this information is reported back to the persistent actor via status messages as illustrated in the following table. diff --git a/docs/articles/persistence/storage-plugins.md b/docs/articles/persistence/storage-plugins.md index b69f3f3770e..aa8c63c35bc 100644 --- a/docs/articles/persistence/storage-plugins.md +++ b/docs/articles/persistence/storage-plugins.md @@ -2,7 +2,7 @@ uid: storage-plugins title: Storage plugins --- -# Storage plugins +# Storage Plugins ## Journals @@ -10,13 +10,13 @@ Journal is a specialized type of actor which exposes an API to handle incoming e [!code-json[Main](../../../src/core/Akka.Persistence/persistence.conf#L201-L207)] -## Snapshot store +## Snapshot Store Snapshot store is a specialized type of actor which exposes an API to handle incoming snapshot-related requests and is able to save snapshots in some backend storage. By default Akka.Persistence uses a `LocalSnapshotStore`, which uses a local file system as storage. A custom snapshot store configuration path may be specified inside *akka.persistence.snapshot-store.plugin* path and by default it requires two keys set: *class* and *plugin-dispatcher*. Example configuration: [!code-json[Main](../../../src/core/Akka.Persistence/persistence.conf#L209-L215)] -### Eager initialization of persistence plugin +### Eager Initialization of Persistence Plugin By default, persistence plugins are started on-demand, as they are used. In some case, however, it might be beneficial to start a certain plugin eagerly. In order to do that, specify the IDs of plugins you wish to start automatically under `akka.persistence.journal.auto-start-journals` and `akka.persistence.snapshot-store.auto-start-snapshot-stores`. diff --git a/docs/articles/remoting/deployment.md b/docs/articles/remoting/deployment.md index 4c0f958d7db..4fab1190a12 100644 --- a/docs/articles/remoting/deployment.md +++ b/docs/articles/remoting/deployment.md @@ -52,7 +52,7 @@ public class Hello ``` -### DeployTarget (process that gets deployed onto) +### DeployTarget (Process that Gets Deployed Onto) ```csharp class Program @@ -76,7 +76,7 @@ class Program } ``` -### Deployer (process that does deploying) +### Deployer (Process that Does Deploying) ```csharp class Program diff --git a/docs/articles/remoting/index.md b/docs/articles/remoting/index.md index 7f643466431..d6be7675899 100644 --- a/docs/articles/remoting/index.md +++ b/docs/articles/remoting/index.md @@ -23,7 +23,7 @@ Akka.Remote introduces the following capabilities to Akka.NET applications: Everything in Akka.NET is designed to work in a distributed setting: all interactions of actors use purely message passing and everything is asynchronous. This effort has been undertaken to ensure that all functions are available equally when running within a single machine or on a cluster of hundreds of machines. The key for enabling this is to go from remote to local by way of optimization instead of trying to go from local to remote by way of generalization. See [this classic paper](http://doc.akka.io/docs/misc/smli_tr-94-29.pdf) for a detailed discussion on why the second approach is bound to fail. -## Ways in which Transparency is Broken +## Ways in Which Transparency Is Broken What is true of Akka need not be true of the application which uses it, since designing for distributed execution poses some restrictions on what is possible. The most obvious one is that all messages sent over the wire must be serializable. While being a little less obvious this includes closures which are used as actor factories (i.e. within Props) if the actor is to be created on a remote node. @@ -44,7 +44,7 @@ Messages exceeding the maximum size will be dropped. You also have to be aware that some protocols (e.g. UDP) might not support arbitrarily large messages. -## How is Remoting Used? +## How Is Remoting Used? We took the idea of transparency to the limit in that there is nearly no API for the remoting layer of Akka.NET: it is purely driven by configuration. Just write your application according to the principles outlined in the previous sections, then specify remote deployment of actor sub-trees in the configuration file. This way, your application can be scaled out without having to touch the code. The only piece of the API which allows programmatic influence on remote deployment is that Props contain a field which may be set to a specific Deploy instance; this has the same effect as putting an equivalent deployment into the configuration file (if both are given, configuration file wins). @@ -120,7 +120,7 @@ These terms form the basis for all remote interaction between `ActorSystem` inst So in the case of our previous example, `localhost:8080` is the inbound (listening) endpoint for the `DotNetty` TCP transport of the `ActorSystem` we configured. -## How to Form Associations between Remote Systems +## How to Form Associations Between Remote Systems So imagine we have the following two actor systems configured to both use the `dot-netty.tcp` Akka.Remote transport: diff --git a/docs/articles/remoting/transports.md b/docs/articles/remoting/transports.md index a85db51ff59..cc11b7a0d66 100644 --- a/docs/articles/remoting/transports.md +++ b/docs/articles/remoting/transports.md @@ -11,7 +11,7 @@ A "transport" refers to an actual network transport, such as TCP or UDP. By defa In this section we'll expand a bit more on what transports are and how Akka.Remote can support multiple transports simultaneously. -## What are Transports? +## What Are Transports? Transports in Akka.Remote are abstractions on top of actual network transports, such as TCP and UDP sockets, and in truth transports have pretty simple requirements. @@ -135,7 +135,7 @@ There are a couple of important caveats to bear in mind with transports in Akka. * Each transport must have its own distinct protocol scheme (`transport-protocol` in HOCON) - no two transports can share the same scheme. * Only one instance of a given transport can be active at a time, for the reason above. -### Separating Physical IP Address from Logical Address +### Separating Physical IP Address From Logical Address One common DevOps issue that comes up often with Akka.Remote is something along the lines of the following: diff --git a/docs/articles/streams/basics.md b/docs/articles/streams/basics.md index 525dc161789..7c70bde6ac2 100644 --- a/docs/articles/streams/basics.md +++ b/docs/articles/streams/basics.md @@ -3,9 +3,9 @@ uid: streams-basics title: Basics and working with Flows --- -# Basics and working with Flows +# Basics and Working with Flows -## Core concepts +## Core Concepts Akka Streams is a library to process and transfer a sequence of elements using bounded buffer space. This latter property is what we refer to as _boundedness_ and it is the defining feature of Akka Streams. @@ -45,7 +45,7 @@ and they will use asynchronous means to slow down a fast producer, without block This is a thread-pool friendly design, since entities that need to wait (a fast producer waiting on a slow consumer) will not block the thread but can hand it back for further use to an underlying thread-pool. -## Defining and running streams +## Defining and Running Streams Linear processing pipelines can be expressed in Akka Streams using the following core abstractions: @@ -144,7 +144,7 @@ var sum2 = runnable.Run(materializer); // sum1 and sum2 are different Tasks! ``` -### Defining sources, sinks and flows +### Defining Sources, Sinks and Flows The objects `Source` and `Sink` define various ways to create sources and sinks of elements. The following examples show some of the most useful constructs (refer to the API documentation for more details): @@ -202,13 +202,13 @@ var otherSink = Flow.Create().AlsoTo(sink).To(Sink.Ignore()); Source.From(Enumerable.Range(1, 6)).To(otherSink); ``` -### Illegal stream elements +### Illegal Stream Elements In accordance to the Reactive Streams specification ([Rule 2.13] ()) Akka Streams do not allow ``null`` to be passed through the stream as an element. In case you want to model the concept of absence of a value we recommend using ``Akka.Streams.Util.Option`` or ``Akka.Util.Either``. -## Back-pressure explained +## Back-Pressure Explained Akka Streams implement an asynchronous non-blocking back-pressure protocol standardized by the [Reactive Streams](http://reactive-streams.org/) specification, which Akka is a founding member of. @@ -235,7 +235,7 @@ with the upstream production rate or not. To illustrate this further let us consider both problem situations and how the back-pressure protocol handles them: -### Slow Publisher, fast Subscriber +### Slow Publisher, Fast Subscriber This is the happy case of course – we do not need to slow down the Publisher in this case. However signalling rates are rarely constant and could change at any point in time, suddenly ending up in a situation where the Subscriber is now @@ -251,7 +251,7 @@ that the Publisher should not ever have to wait (be back-pressured) with publish As we can see, in this scenario we effectively operate in so called push-mode since the Publisher can continue producing elements as fast as it can, since the pending demand will be recovered just-in-time while it is emitting elements. -### Fast Publisher, slow Subscriber +### Fast Publisher, Slow Subscriber This is the case when back-pressuring the ``Publisher`` is required, because the ``Subscriber`` is not able to cope with the rate at which its upstream would like to emit data elements. @@ -321,7 +321,7 @@ The new fusing behavior can be disabled by setting the configuration parameter ` In that case you can still manually fuse those graphs which shall run on less Actors. With the exception of the `SslTlsStage` and the ``GroupBy`` operator all built-in processing stages can be fused. -### Combining materialized values +### Combining Materialized Values Since every processing stage in Akka Streams can provide a materialized value after being materialized, it is necessary to somehow express how these values should be composed to a final value when we plug these stages together. For this, @@ -396,7 +396,7 @@ RunnableGraph, ICancelable, Task>> r12 = > In Graphs it is possible to access the materialized value from inside the stream processing graph. > For details see [Accessing the materialized value inside the Graph](xref:streams-working-with-graphs#accessing-the-materialized-value-inside-the-graph). -### Source pre-materialization +### Source Pre-Materialization There are situations in which you require a `Source` materialized value **before** the `Source` gets hooked up to the rest of the graph. This is particularly useful in the case of "materialized value powered" `Source`s, like `Source.Queue`, `Source.ActorRef` or `Source.Maybe`. @@ -405,7 +405,7 @@ By using the `PreMaterialize` operator on a `Source`, you can obtain its materia [!code-csharp[FlowDocTests.cs](../../../src/core/Akka.Docs.Tests/Streams/FlowDocTests.cs?name=source-prematerialization)] -## Stream ordering +## Stream Ordering In Akka Streams almost all computation stages *preserve input order* of elements. This means that if inputs ``{IA1,IA2,...,IAn}`` "cause" outputs ``{OA1,OA2,...,OAk}`` and inputs ``{IB1,IB2,...,IBm}`` "cause" outputs ``{OB1,OB2,...,OBl}`` and all of diff --git a/docs/articles/streams/buffersandworkingwithrate.md b/docs/articles/streams/buffersandworkingwithrate.md index 71a355ce23d..eac5da8ec3f 100644 --- a/docs/articles/streams/buffersandworkingwithrate.md +++ b/docs/articles/streams/buffersandworkingwithrate.md @@ -3,11 +3,11 @@ uid: streams-buffers title: Buffers and working with rate --- -# Buffers and working with rate +# Buffers and Working With Rate When upstream and downstream rates differ, especially when the throughput has spikes, it can be useful to introduce buffers in a stream. In this chapter we cover how buffers are used in Akka Streams. -## Buffers for asynchronous stages +## Buffers for Asynchronous Stages In this section we will discuss internal buffers that are introduced as an optimization when using asynchronous stages. To run a stage asynchronously it has to be marked explicitly as such using the .async method. Being run asynchronously means that a stage, after handing out an element to its downstream consumer is able to immediately process the next message. To demonstrate what we mean by this, let's take a look at the following example: @@ -39,7 +39,7 @@ While pipelining in general increases throughput, in practice there is a cost of While this internal protocol is mostly invisible to the user (apart form its throughput increasing effects) there are situations when these details get exposed. In all of our previous examples we always assumed that the rate of the processing chain is strictly coordinated through the backpressure signal causing all stages to process no faster than the throughput of the connected chain. There are tools in Akka Streams however that enable the rates of different segments of a processing chain to be "detached" or to define the maximum throughput of the stream through external timing sources. These situations are exactly those where the internal batching buffering strategy suddenly becomes non-transparent. -## Internal buffers and their effect +## Internal Buffers and Their Effect As we have explained, for performance reasons Akka Streams introduces a buffer for every asynchronous processing stage. The purpose of these buffers is solely optimization, in fact the size of 1 would be the most natural choice if there would be no need for throughput improvements. Therefore it is recommended to keep these buffer sizes small, and increase them only to a level suitable for the throughput requirements of the application. Default buffer sizes can be set through configuration: @@ -136,9 +136,9 @@ If our imaginary external job provider is a client using our API, we might want jobs.buffer(1000, OverflowStrategy.fail) ``` -## Rate transformation +## Rate Transformation -### Understanding conflate +### Understanding Conflate When a fast producer can not be informed to slow down by backpressure or some other signal, conflate might be useful to combine elements from a producer until a demand signal comes from a consumer. @@ -170,7 +170,7 @@ Another possible use of `conflate` is to not consider all elements for summary w }).Concat(identity); ``` -### Understanding expand +### Understanding Expand Expand helps to deal with slow producers which are unable to keep up with the demand coming from consumers. Expand allows to extrapolate a value to be sent as an element to a consumer. diff --git a/docs/articles/streams/builtinstages.md b/docs/articles/streams/builtinstages.md index 195671ed99b..4276ececfb5 100644 --- a/docs/articles/streams/builtinstages.md +++ b/docs/articles/streams/builtinstages.md @@ -3,9 +3,9 @@ uid: streams-builtin-stages title: Overview of built-in stages and their semantics --- -# Overview of built-in stages and their semantics +# Overview of Built-In Stages and Their Semantics -## Source stages +## Source Stages These built-in sources are available from ``akka.stream.scaladsl.Source``: @@ -231,7 +231,7 @@ Combine the elements of multiple streams into a stream of sequences using a comb **completes** when any upstream completes -## Sink stages +## Sink Stages These built-in sinks are available from ``Akka.Stream.DSL.Sink``: @@ -399,7 +399,7 @@ Integration with Reactive Streams, materializes into a ``Reactive.Streams.IPubli Integration with Reactive Streams, wraps a ``Reactive.Streams.ISubscriber`` as a sink -## Additional Sink and Source converters +## Additional Sink and Source Converters Sources and sinks for integrating with ``System.IO.Stream`` can be found on ``StreamConverters``. As they are blocking APIs the implementations of these stages are run on a separate @@ -462,7 +462,7 @@ a ``IOResult`` upon reaching the end of the file or if there is a failure. Create a sink which will write incoming ``ByteString`` s to a given file. -## Flow stages +## Flow Stages All flows by default backpressure if the computation they encapsulate is not fast enough to keep up with the rate of incoming elements from the preceding stage. There are differences though how the different stages handle when some of @@ -475,7 +475,7 @@ For in-band error handling of normal errors (dropping elements if a map fails fo supervision support, or explicitly wrap your element types in a proper container that can express error or success states (for example ``try`` in C#). -## Simple processing stages +## Simple Processing Stages These stages can transform the rate of incoming elements since there are stages that emit multiple elements for a single input (e.g. `ConcatMany`) or consume multiple elements before emitting one output (e.g. ``Where``). @@ -757,7 +757,7 @@ If the wire-tap ``Sink`` backpressures, elements that would've been sent to it w **cancels** when downstream cancels -## Asynchronous processing stages +## Asynchronous Processing Stages These stages encapsulate an asynchronous computation, properly handling backpressure while taking care of the asynchronous operation at the same time (usually handling the completion of a Task). @@ -789,7 +789,7 @@ If a Task fails, the stream also fails (unless a different supervision strategy **completes** upstream completes and all tasks has been completed and all elements has been emitted -## Timer driven stages +## Timer Driven Stages These stages process elements using timers, delaying, dropping or grouping elements for certain time durations. @@ -844,7 +844,7 @@ Delay every element passed through with a specific duration. **completes** when upstream completes and buffered elements has been drained -## Backpressure aware stages +## Backpressure Aware Stages These stages are aware of the backpressure provided by their downstreams and able to adapt their behavior to that signal. @@ -954,7 +954,7 @@ the flow with a ``BufferOverflowException``. **completes** when upstream completes and buffered elements has been drained -## Nesting and flattening stages +## Nesting and Flattening Stages These stages either take a stream and turn it into a stream of streams (nesting) or they take a stream that contains nested streams and turn them into a stream of elements instead (flattening). @@ -1021,7 +1021,7 @@ merging. The maximum number of merged sources has to be specified. **completes** when upstream completes and all consumed substreams complete -## Time aware stages +## Time Aware Stages Those stages operate taking time into consideration. @@ -1103,7 +1103,7 @@ Delays the initial element by the specified duration. **cancels** when downstream cancels -## Fan-in stages +## Fan-In Stages These stages take multiple streams as their input and provide a single output combining the elements from all of the inputs in different ways. @@ -1232,7 +1232,7 @@ source completes the rest of the other stream will be emitted. **completes** when both upstreams have completed -## Fan-out stages +## Fan-Out Stages These have one input and multiple outputs. They might route the elements between different outputs, or emit elements on multiple outputs at the same time. @@ -1257,7 +1257,7 @@ Splits each element of input into multiple downstreams using a function **completes** when upstream completes -### broadcast +### Broadcast Emit each incoming element each of ``n`` outputs. @@ -1290,7 +1290,7 @@ to the partitioner function applied to the element **cancels** when when all downstreams cancel -## Watching status stages +## Watching Status Stages ### WatchTermination diff --git a/docs/articles/streams/cookbook.md b/docs/articles/streams/cookbook.md index d7208a65b03..986c0d53c7f 100644 --- a/docs/articles/streams/cookbook.md +++ b/docs/articles/streams/cookbook.md @@ -24,7 +24,7 @@ If you need a quick reference of the available processing stages used in the rec In this collection we show simple recipes that involve linear flows. The recipes in this section are rather general, more targeted recipes are available as separate sections ( [Buffers and working with rate](xref:streams-buffers), [Working with streaming IO](xref:streams-io)). -### Logging elements of a stream +### Logging Elements of a Stream **Situation:** During development it is sometimes helpful to see what happens in a particular section of a stream. @@ -54,7 +54,7 @@ mySource.Log("before-select") mySource.Log("custom", null, Logging.GetLogger(sys, "customLogger")); ``` -### Flattening a stream of sequences +### Flattening a Stream of Sequences **Situation:** A stream is given as a stream of sequence of elements, but a stream of elements needed instead, streaming all the nested elements inside the sequences separately. @@ -68,7 +68,7 @@ Source,NotUsed > myData = someDataSource; Source flattened = myData.SelectMany(x => x); ``` -### Draining a stream to a strict collection +### Draining a Stream to a Strict Collection **Situation:** A possibly unbounded sequence of elements is given as a stream, which needs to be collected into a collection while ensuring boundedness @@ -97,7 +97,7 @@ var limited = mySource.Limit(MAX_ALLOWED_SIZE).RunWith(Sink.Seq(), mater var ignoreOverflow = mySource.Take(MAX_ALLOWED_SIZE).RunWith(Sink.Seq(), materializer); ``` -### Calculating the digest of a ByteString stream +### Calculating the Digest of a ByteString Stream **Situation:** A stream of bytes is given as a stream of ``ByteStrings`` and we want to calculate the cryptographic digest of the stream. @@ -159,7 +159,7 @@ var data = Source.Empty(); var digest = data.Via(new DigestCalculator("SHA-256")); ``` -### Parsing lines from a stream of ByteStrings +### Parsing Lines From a Stream of ByteStrings **Situation:** A stream of bytes is given as a stream of ``ByteStrings`` containing lines terminated by line ending characters (or, alternatively, containing binary frames delimited by a special delimiter byte sequence) which @@ -174,7 +174,7 @@ var linesStream = rawData .Select(b => b.DecodeString()); ``` -### Implementing reduce-by-key +### Implementing Reduce-By-Key **Situation:** Given a stream of elements, we want to calculate some aggregated value on different subgroups of the elements. @@ -246,7 +246,7 @@ var counts = words.Via(ReduceByKey(MaximumDistinctWords, > Please note that the reduce-by-key version we discussed above is sequential in reading the overall input stream, in other words it is **NOT** a parallelization pattern like MapReduce and similar frameworks. -### Sorting elements to multiple groups with groupBy +### Sorting Elements to Multiple Groups with groupBy **Situation:** The ``GroupBy`` operation strictly partitions incoming elements, each element belongs to exactly one group. Sometimes we want to map elements into multiple groups simultaneously. @@ -283,7 +283,7 @@ var multiGroups = messageAndTopic.GroupBy(2, tuple => tuple.Item2).Select(tuple In this collection we show recipes that use stream graph elements to achieve various goals. -### Triggering the flow of elements programmatically +### Triggering the Flow of Elements Programmatically **Situation:** Given a stream of elements we want to control the emission of those elements according to a trigger signal. In other words, even if the stream would be able to flow (not being backpressured) we want to hold back elements until a @@ -327,7 +327,7 @@ var graph = RunnableGraph.FromGraph(GraphDsl.Create(b => })); ``` -### Balancing jobs to a fixed pool of workers +### Balancing Jobs to a Fixed Pool of Workers **Situation:** Given a stream of jobs and a worker process expressed as a `Flow` create a pool of workers that automatically balances incoming jobs to available workers, then merges the results. @@ -362,12 +362,12 @@ var worker = Flow.Create().Select(j => new Done(j)); var processedJobs = myJobs.Via(Balancer(worker, 3)); ``` -## Working with rate +## Working with Rate This collection of recipes demonstrate various patterns where rate differences between upstream and downstream needs to be handled by other strategies than simple backpressure. -### Dropping elements +### Dropping Elements **Situation:** Given a fast producer and a slow consumer, we want to drop elements if necessary to not slow down the producer too much. @@ -386,7 +386,7 @@ var droppyStream = Flow.Create().Conflate((lastMessage, newMessage) => There is a more general version of ``Conflate`` named ``ConflateWithSeed`` that allows to express more complex aggregations, more similar to a ``Aggregate``. -### Dropping broadcast +### Dropping Broadcast **Situation:** The default ``Broadcast`` graph element is properly backpressured, but that means that a slow downstream consumer can hold back the other downstream consumers resulting in lowered throughput. In other words the rate of @@ -416,7 +416,7 @@ var graph = RunnableGraph.FromGraph(GraphDsl.Create(mysink1, mysink2, mysink3, T })); ``` -### Collecting missed ticks +### Collecting Missed Ticks **Situation:** Given a regular (stream) source of ticks, instead of trying to backpressure the producer of the ticks we want to keep a counter of the missed ticks instead and pass it down when possible. @@ -438,7 +438,7 @@ var missed = Flow.Create() .ConflateWithSeed(seed: _ => 0, aggregate: (missedTicks, tick) => missedTicks + 1); ``` -### Create a stream processor that repeats the last element seen +### Create a Stream Processor that Repeats the Last Element Seen **Situation:** Given a producer and consumer, where the rate of neither is known in advance, we want to ensure that none of them is slowing down the other by dropping earlier unconsumed elements from the upstream if necessary, and repeating @@ -554,7 +554,7 @@ public sealed class HoldWithWait : GraphStage> } ``` -### Globally limiting the rate of a set of streams +### Globally Limiting the Rate of a Set of Streams **Situation:** Given a set of independent streams that we cannot merge, we want to globally limit the aggregate throughput of the set of streams. @@ -685,7 +685,7 @@ public Flow LimitGlobal(IActorRef limiter, TimeSpan maxAllowed ## Working with IO -### Chunking up a stream of ByteStrings into limited size ByteStrings +### Chunking up a Stream of ByteStrings Into Limited Size ByteStrings **Situation:** Given a stream of ByteStrings we want to produce a stream of ByteStrings containing the same bytes in the same sequence, but capping the size of ByteStrings. In other words we want to slice up ByteStrings into smaller @@ -776,7 +776,7 @@ var rawBytes = Source.Empty(); var chunkStream = rawBytes.Via(new Chunker(ChunkLimit)); ``` -### Limit the number of bytes passing through a stream of ByteStrings +### Limit the Number of Bytes Passing Through a Stream of ByteStrings **Situation:** Given a stream of ByteStrings we want to fail the stream if more than a given maximum of bytes has been consumed. @@ -828,7 +828,7 @@ public class ByteLimiter : GraphStage> var limiter = Flow.Create().Via(new ByteLimiter(SizeLimit)); ``` -### Compact ByteStrings in a stream of ByteStrings +### Compact ByteStrings in a Stream of ByteStrings **Situation:** After a long stream of transformations, due to their immutable, structural sharing nature ByteStrings may refer to multiple original ByteString instances unnecessarily retaining memory. As the final step of a transformation @@ -842,7 +842,7 @@ var data = Source.Empty(); var compacted = data.Select(b => b.Compact()); ``` -### Injecting keep-alive messages into a stream of ByteStrings +### Injecting Keep-Alive Messages Into a Stream of ByteStrings **Situation:** Given a communication channel expressed as a stream of ByteStrings we want to inject keep-alive messages but only if this does not interfere with normal traffic. diff --git a/docs/articles/streams/custom-stream-processing.md b/docs/articles/streams/custom-stream-processing.md index 198cd3458fb..bd3a5ec6ca4 100644 --- a/docs/articles/streams/custom-stream-processing.md +++ b/docs/articles/streams/custom-stream-processing.md @@ -3,14 +3,14 @@ uid: custom-stream-processing title: Custom stream processing --- -# Custom stream processing +# Custom Stream Processing While the processing vocabulary of Akka Streams is quite rich (see the [Streams Cookbook](xref:streams-cookbook) for examples) it is sometimes necessary to define new transformation stages either because some functionality is missing from the stock operations, or for performance reasons. In this part we show how to build custom processing stages and graph junctions of various kinds. > [!NOTE] > A custom graph stage should not be the first tool you reach for, defining graphs using flows and the graph DSL is in general easier and does to a larger extent protect you from mistakes that might be easy to make with a custom `GraphStage` -## Custom processing with GraphStage +## Custom Processing with GraphStage The `GraphStage` abstraction can be used to create arbitrary graph processing stages with any number of input or output ports. It is a counterpart of the `GraphDSL.Create()` method which creates new stream processing stages by composing others. Where `GraphStage` differs is that it creates a stage that is itself not divisible into smaller ones, and allows state to be maintained inside it in a safe way. @@ -95,7 +95,7 @@ var result1Task = mySource.Take(10).RunAggregate(0, (sum, next) => sum + next, m var result2Task = mySource.Take(100).RunAggregate(0, (sum, next) => sum + next, materializer); ``` -### Port states, InHandler and OutHandler +### Port States, InHandler and OutHandler In order to interact with a port (`Inlet` or `Outlet`) of the stage we need to be able to receive events and generate new events belonging to the port. From the `GraphStageLogic` the following operations are available on an output port: @@ -156,7 +156,7 @@ Note that since the above methods are implemented by temporarily replacing the h An example of how this API simplifies a stage can be found below in the second version of the Duplicator. -### Custom linear processing stages using GraphStage +### Custom Linear Processing Stages Using GraphStage Graph stages allows for custom linear processing stages through letting them have one input and one output and using `FlowShape` as their shape. @@ -367,7 +367,7 @@ Completion handling usually (but not exclusively) comes into the picture when pr Stages by default automatically stop once all of their ports (input and output) have been closed externally or internally. It is possible to opt out from this behavior by invoking `SetKeepGoing(true)` (which is not supported from the stage's constructor and usually done in `PreStart`). In this case the stage **must** be explicitly closed by calling `CompleteStage()` or `FailStage(exception)`. This feature carries the risk of leaking streams and actors, therefore it should be used with care. -### Logging inside GraphStages +### Logging Inside GraphStages Logging debug or other important information in your stages is often a very good idea, especially when developing more advances stages which may need to be debugged at some point. @@ -437,7 +437,7 @@ public void A_GraphStageLogic_must_support_logging_in_custom_graphstage() > **SPI Note:** If you're implementing a Materializer, you can add this ability to your materializer by implementing `IMaterializerLoggingProvider` in your `Materializer`. -### Using timers +### Using Timers It is possible to use timers in `GraphStages` by using `TimerGraphStageLogic` as the base class for the returned logic. Timers can be scheduled by calling one of `ScheduleOnce(key,delay)`, `SchedulePeriodically(key,period)` or `SchedulePeriodicallyWithInitialDelay(key,delay,period)` and passing an object as a key for that timer (can be any object, for example a String). The `OnTimer(key)` method needs to be overridden and it will be called once the timer of key fires. It is possible to cancel a timer using `CancelTimer(key)` and check the status of a timer with `IsTimerActive(key)`. Timers will be automatically cleaned up when the stage completes. @@ -491,7 +491,7 @@ class TimedGate : GraphStage> } ``` -### Using asynchronous side-channels +### Using Asynchronous Side-Channels In order to receive asynchronous events that are not arriving as stream elements (for example a completion of a task or a callback from a 3rd party API) one must acquire a `AsyncCallback` by calling `GetAsyncCallback()` from the stage logic. The method `GetAsyncCallback` takes as a parameter a callback that will be called once the asynchronous event fires. It is important to **not call the callback directly**, instead, the external API must `invoke` the returned `Action`. The execution engine will take care of calling the provided callback in a thread-safe way. The callback can safely access the state of the `GraphStageLogic` implementation. @@ -539,7 +539,7 @@ class KillSwitch : GraphStage> } ``` -### Integration with actors +### Integration with Actors **This section is a stub and will be extended in the next release This is an experimental feature*** @@ -549,7 +549,7 @@ It is possible to acquire an ActorRef that can be addressed from the outside of * they cannot be returned as materialized values. * they cannot be accessed from the constructor of the `GraphStageLogic`, but they can be accessed from the `PreStart()` method. -### Custom materialized values +### Custom Materialized Values Custom stages can return materialized values instead of `NotUsed` by inheriting from `GraphStageWithMaterializedValue` instead of the simpler `GraphStage`. The difference is that in this case the method `CreateLogicAndMaterializedValue(inheritedAttributes)` needs to be overridden, and in addition to the stage logic the materialized value must be provided @@ -599,7 +599,7 @@ class FirstValue : GraphStageWithMaterializedValue, Task> } ``` -## Using attributes to affect the behavior of a stage +## Using Attributes to Affect the Behavior of a Stage > [!NOTE] > This section is a stub and will be extended in the next release. @@ -608,7 +608,7 @@ Stages can access the `Attributes` object created by the materializer. This cont See [Modularity, Composition and Hierarchy](xref:streams-modularity) for an explanation on how attributes work. -### Rate decoupled graph stages +### Rate Decoupled Graph Stages Sometimes it is desirable to decouple the rate of the upstream and downstream of a stage, synchronizing only when needed. @@ -703,7 +703,7 @@ class TwoBuffer : GraphStage> } ``` -## Thread safety of custom processing stages +## Thread Safety of Custom Processing Stages **All of the above custom stages (linear or graph) provide a few simple guarantees that implementors can rely on.** @@ -715,7 +715,7 @@ In essence, the above guarantees are similar to what `Actor`'s provide, if one t > [!WARNING] > It is **not** safe to access the state of any custom stage outside of the callbacks that it provides, just like it is unsafe to access the state of an actor from the outside. This means that Future callbacks should not close over internal state of custom stages because such access can be concurrent with the provided callbacks, leading to undefined behavior. -## Resources and the stage lifecycle +## Resources and the Stage Lifecycle If a stage manages a resource with a lifecycle, for example objects that need to be shutdown when they are not used anymore it is important to make sure this will happen in all circumstances when the stage shuts down. diff --git a/docs/articles/streams/designprinciples.md b/docs/articles/streams/designprinciples.md index 39a01fccc55..12fe00cb252 100644 --- a/docs/articles/streams/designprinciples.md +++ b/docs/articles/streams/designprinciples.md @@ -3,14 +3,14 @@ uid: streams-design-principles title: Design Principles behind Akka Streams --- -# Design Principles behind Akka Streams +# Design Principles Behind Akka Streams It took quite a while until we were reasonably happy with the look and feel of the API and the architecture of the implementation, and while being guided by intuition the design phase was very much exploratory research. This section details the findings and codifies them into a set of principles that have emerged during the process. > [!NOTE] > As detailed in the introduction keep in mind that the Akka Streams API is completely decoupled from the Reactive Streams interfaces which are just an implementation detail for how to pass stream data between individual processing stages. -## What shall users of Akka Streams expect? +## What Shall Users of Akka Streams Expect? Akka.NET is built upon a conscious decision to offer APIs that are minimal and consistent --as opposed to easy or intuitive. The credo is that we favour explicitness over magic, and if we provide a feature then it must work always, no exceptions. Another way to say this is that we minimize the number of rules a user has to learn instead of trying to keep the rules close to what we think users might expect. @@ -22,7 +22,7 @@ From this follows that the principles implemented by Akka Streams are: This means that we provide all the tools necessary to express any stream processing topology, that we model all the essential aspects of this domain (back-pressure, buffering, transformations, failure recovery, etc.) and that whatever the user builds is reusable in a larger context. -### Akka Streams does not send dropped stream elements to the dead letter office +### Akka Streams Does Not Send Dropped Stream Elements to the Dead Letter Office One important consequence of offering only features that can be relied upon is the restriction that Akka Streams cannot ensure that all objects sent through a processing topology will be processed. Elements can be dropped for a number of reason: @@ -33,13 +33,13 @@ One important consequence of offering only features that can be relied upon is t This means that sending CLR objects into a stream that needs to be cleaned up will require the user to ensure that this happens outside of the Akka Streams facilities (e.g. by cleaning them up after a time-out or when their results are observed on the stream output, or by other means like finalizers etc.) -### Resulting implementation Constraints +### Resulting Implementation Constraints Compositionality entails re-usability of partial stream topologies, which led us to the lifted approach of describing data flows as (partial) graphs that can act as composite sources, flows (a.k.a. pipes) and sinks of data. These building blocks shall then be freely shareable, with the ability to combine them freely to form larger graphs. The representation of these pieces must therefore be an immutable blueprint that is materialized in an explicit step in order to start the stream processing. The resulting stream processing engine is then also immutable in the sense of having a fixed topology that is prescribed by the blueprint. Dynamic networks need to be modelled by explicitly using the Reactive Streams interfaces for plugging different engines together. The process of materialization will often create specific objects that are useful to interact with the processing engine once it is running, for example for shutting it down or for extracting metrics. This means that the materialization function produces a result termed the *materialized value of a graph*. -## Inter-operation with other Reactive Streams implementations +## Inter-Operation with Other Reactive Streams Implementations Akka Streams fully implement the `Reactive Streams` specification and interoperate with all other conformant implementations. We chose to completely separate the Reactive Streams interfaces from the user-level API because we regard them to be an SPI that is not targeted at end users. In order to obtain a `Publisher` or `Subscriber` from an Akka Stream topology, a corresponding `Sink.AsPublisher` or `Source.AsSubscriber` element must be used. @@ -47,7 +47,7 @@ All stream Processors produced by the default materialization of Akka Streams ar This means that `Sink.AsPublisher(true)` (for enabling fan-out support) must be used where broadcast behavior is needed for inter-operation with other Reactive Streams implementations. -## What shall users of streaming libraries expect? +## What Shall Users of Streaming Libraries Expect? We expect libraries to be built on top of Akka Streams. In order to allow users to profit from the principles that are described for Akka Streams above, the following rules are established: @@ -75,7 +75,7 @@ Akka Streams must enable a library to express any stream processing utility in t > [!NOTE] > A source that emits a stream of streams is still just a normal Source, the kind of elements that are produced does not play a role in the static stream topology that is being expressed. -## The difference between Error and Failure +## The Difference Between Error and Failure The starting point for this discussion is the definition given by the [Reactive Manifesto](http://www.reactivemanifesto.org/glossary#Failure). Translated to streams this means that an error is accessible within the stream as a normal data element, while a failure means that the stream itself has failed and is collapsing. In concrete terms, on the Reactive Streams interface level data elements (including errors) are signalled via `OnNext` while failures raise the `OnError` signal. @@ -86,7 +86,7 @@ There is only limited support for treating `OnError` in Akka Streams compared to The ability for failures to propagate faster than data elements is essential for tearing down streams that are back-pressured --especially since back-pressure can be the failure mode (e.g. by tripping upstream buffers which then abort because they cannot do anything else; or if a dead-lock occurred). -## The semantics of stream recovery +## The Semantics of Stream Recovery A recovery element (i.e. any transformation that absorbs an `OnError` signal and turns that into possibly more data elements followed normal stream completion) acts as a bulkhead that confines a stream collapse to a given region of the stream topology. Within the collapsed region buffered elements may be lost, but the outside is not affected by the failure. diff --git a/docs/articles/streams/error-handling.md b/docs/articles/streams/error-handling.md index 1896abfe74f..0c764a48eea 100644 --- a/docs/articles/streams/error-handling.md +++ b/docs/articles/streams/error-handling.md @@ -49,7 +49,7 @@ This will output: stream truncated ``` -## Recover with retries +## Recover with Retries `RecoverWithRetries` allows you to put a new upstream in place of the failed one, recovering stream failures up to a specified maximum number of times. @@ -88,7 +88,7 @@ seven eight ``` -## Delayed restarts with a backoff stage +## Delayed Restarts with a Backoff Stage Just as Akka provides the [backoff supervision pattern for actors](xref:supervision#delayed-restarts-with-the-backoffsupervisor-pattern), Akka streams also provides a `RestartSource`, `RestartSink` and `RestartFlow` for implementing the so-called *exponential backoff @@ -221,7 +221,7 @@ var result = source.Limit(1000).RunWith(Sink.Seq(), materializer); // result here will be a Task completed with Success(List(0, 1, 4, 0, 5, 12)) ``` -## Errors from SelectAsync +## Errors From SelectAsync Stream supervision can also be applied to the tasks of `SelectAsync` and `SelectAsyncUnordered` even if such failures happen in the task rather than inside the stage itself. . diff --git a/docs/articles/streams/integration.md b/docs/articles/streams/integration.md index 47d2ab5c30b..779bf0ec421 100644 --- a/docs/articles/streams/integration.md +++ b/docs/articles/streams/integration.md @@ -225,7 +225,7 @@ var saveTweets = akkaTweets Note that if the ``Ask`` is not completed within the given timeout the stream is completed with failure. If that is not desired outcome you can use ``Recover`` on the ``Ask`` `Task`. -### Illustrating ordering and parallelism +### Illustrating Ordering and Parallelism Let us look at another example to get a better understanding of the ordering and parallelism characteristics of ``SelectAsync`` and ``SelectAsyncUnordered``. @@ -437,7 +437,7 @@ You may notice two extra parameters here. One of the advantages of Akka.Streams Any other `OverflowStrategy` option is not supported by `Source.FromObservable` stage. -### Integrating with event handlers +### Integrating with Event Handlers C# events can also be used as a potential source of an Akka.NET stream. It's possible using `Source.FromEvent` methods. Example: diff --git a/docs/articles/streams/introduction.md b/docs/articles/streams/introduction.md index 0b2f349efcc..2cb82188864 100644 --- a/docs/articles/streams/introduction.md +++ b/docs/articles/streams/introduction.md @@ -19,7 +19,7 @@ The Akka Streams API is completely decoupled from the Reactive Streams interface The relationship between these two is that the Akka Streams API is geared towards end-users while the Akka Streams implementation uses the Reactive Streams interfaces internally to pass data between the different processing stages. For this reason you will not find any resemblance between the Reactive Streams interfaces and the Akka Streams API. This is in line with the expectations of the Reactive Streams project, whose primary purpose is to define interfaces such that different streaming implementation can interoperate; it is not the purpose of Reactive Streams to describe an end-user API. -## How to read these docs +## How to Read These Docs Stream processing is a different paradigm to the Actor Model or to Task composition, therefore it may take some careful study of this subject until you feel familiar with the tools and techniques. The documentation is here to help and for best results we recommend the following approach: diff --git a/docs/articles/streams/modularitycomposition.md b/docs/articles/streams/modularitycomposition.md index 3b4541c9474..1640b182b82 100644 --- a/docs/articles/streams/modularitycomposition.md +++ b/docs/articles/streams/modularitycomposition.md @@ -7,7 +7,7 @@ title: Modularity, Composition and Hierarchy Akka Streams provide a uniform model of stream processing graphs, which allows flexible composition of reusable components. In this chapter we show how these look like from the conceptual and API perspective, demonstrating the modularity aspects of the library. -## Basics of composition and modularity +## Basics of Composition and Modularity Every processing stage used in Akka Streams can be imagined as a "box" with input and output ports where elements to be processed arrive and leave the stage. In this view, a `Source` is nothing else than a "box" with a single output port, or, a `BidiFlow` is a "box" with exactly two input and two output ports. In the figure below we illustrate the most common used stages viewed as "boxes". @@ -107,7 +107,7 @@ var runnableGraph = nestedSource.To(nestedSink); var runnableGraph2 = Source.Single(0).To(Sink.Aggregate(0, (sum, x) => sum + x)); ``` -## Composing complex systems +## Composing Complex Systems In the previous section we explored the possibility of composition, and hierarchy, but we stayed away from non-linear, generalized graph components. There is nothing in Akka Streams though that enforces that stream processing layouts @@ -280,7 +280,7 @@ We have also seen, that every module has a `Shape` (for example a `Sink` has a ` independently which DSL was used to create it. This uniform representation enables the rich composability of various stream processing entities in a convenient way. -## Materialized values +## Materialized Values After realizing that `RunnableGraph` is nothing more than a module with no unused ports (it is an island), it becomes clear that after materialization the only way to communicate with the running stream processing logic is via some side-channel. diff --git a/docs/articles/streams/pipeliningandparallelism.md b/docs/articles/streams/pipeliningandparallelism.md index b8e843367b1..62083591e6e 100644 --- a/docs/articles/streams/pipeliningandparallelism.md +++ b/docs/articles/streams/pipeliningandparallelism.md @@ -59,7 +59,7 @@ not be able to operate at full capacity [^foot-note-1]. For more details about the behavior of these and how to add additional buffers refer to [Buffers and working with rate](xref:streams-buffers). -## Parallel processing +## Parallel Processing Chris uses the two frying pans symmetrically. He uses both pans to fully fry a pancake on both sides, then puts the results on a shared plate. Whenever a pan becomes empty, he takes the next scoop from the shared bowl of batter. @@ -97,7 +97,7 @@ by strict round-robin balancing and merging stages that put in and take out panc A more detailed example of creating a worker pool can be found in the cookbook: [Balancing jobs to a fixed pool of workers](xref:streams-cookbook#balancing-jobs-to-a-fixed-pool-of-workers) -## Combining pipelining and parallel processing +## Combining Pipelining and Parallel Processing The two concurrency patterns that we demonstrated as means to increase throughput are not exclusive. In fact, it is rather simple to combine the two approaches and streams provide diff --git a/docs/articles/streams/reactivetweets.md b/docs/articles/streams/reactivetweets.md index 43afb5fdb3d..048085cb645 100644 --- a/docs/articles/streams/reactivetweets.md +++ b/docs/articles/streams/reactivetweets.md @@ -18,7 +18,7 @@ allow to control what should happen in such scenarios. > [!NOTE] > You can find an example implementation [here](https://github.com/Silv3rcircl3/Akka.Net-Streams-reactive-tweets), using [Tweetinvi](https://github.com/linvi/tweetinvi) to call the Twitter STREAM API. Due to the fact that Tweetinvi doesn't implement the Reactive Streams specifications, we push the tweets into the stream via the `IActorRef` that is materialized from the following Source `Source.ActorRef(100, OverflowStrategy.DropHead);`. -## Transforming and consuming simple streams +## Transforming and Consuming Simple Streams The example application we will be looking at is a simple Twitter feed stream from which we'll want to extract certain information, like for example the number of tweets a user has posted. @@ -92,7 +92,7 @@ using (var sys = ActorSystem.Create("Reactive-Tweets")) } ``` -## Flattening sequences in streams +## Flattening Sequences in Streams In the previous section we were working on 1:1 relationships of elements which is the most common case, but sometimes we might want to map from one element to a number of elements and receive a "flattened" stream, similarly like ``SelectMany`` @@ -103,7 +103,7 @@ combinator: Source hashTags = tweetSource.SelectMany(tweet => tweet.Hashtags); ``` -## Broadcasting a stream +## Broadcasting a Stream Now let's say we want to persist all hashtags, as well as all author names from this one live stream. For example we'd like to write all author handles into one file, and all hashtags into another file on disk. @@ -155,7 +155,7 @@ expresses a graph that is a *partial graph*. Concepts around composing and nesti explained in detail in [Modularity, Composition and Hierarchy](xref:streams-modularity#basics-of-composition-and-modularity). It is also possible to wrap complex computation graphs as Flows, Sinks or Sources, which will be explained in detail in [Constructing Sources, Sinks and Flows from Partial Graphs](xref:streams-working-with-graphs#constructing-sources-sinks-and-flows-from-partial-graphs). -## Back-pressure in action +## Back-Pressure in Action One of the main advantages of Akka Streams is that they *always* propagate back-pressure information from stream Sinks (Subscribers) to their Sources (Publishers). It is not an optional feature, and is enabled at all times. To learn more @@ -179,7 +179,7 @@ The ``Buffer`` element takes an explicit and required ``OverflowStrategy``, whic when it receives another element while it is full. Strategies provided include dropping the oldest element (``DropHead``), dropping the entire buffer, signalling errors etc. Be sure to pick and choose the strategy that fits your use case best. -## Materialized value +## Materialized Value So far we've been only processing data using Flows and consuming it into some kind of external Sink - be it by printing values or storing them in some external system. However sometimes we may be interested in some value that can be diff --git a/docs/articles/streams/stream-dynamic.md b/docs/articles/streams/stream-dynamic.md index 085c60b44e2..748051f7dcb 100644 --- a/docs/articles/streams/stream-dynamic.md +++ b/docs/articles/streams/stream-dynamic.md @@ -3,9 +3,9 @@ uid: streams-dynamic-handling title: Dynamic stream handling --- -# Dynamic stream handling +# Dynamic Stream Handling -## Controlling graph completion with KillSwitch +## Controlling Graph Completion with KillSwitch A `KillSwitch` allows the completion of graphs of `FlowShape` from the outside. It consists of a flow element that can be linked to a graph of `FlowShape` needing completion control. The `IKillSwitch` interface allows to: @@ -66,13 +66,13 @@ by the switch. Refer to the below for usage examples. > [!NOTE] > A `UniqueKillSwitch` is always a result of a materialization, whilst `SharedKillSwitch` needs to be constructed before any materialization takes place. -### Using `CancellationToken`s as kill switches +### Using `CancellationToken` as Kill Switches Plain old .NET cancellation tokens can also be used as kill switch stages via extension method: `cancellationToken.AsFlow(cancelGracefully: true)`. Their behavior is very similar to what a `SharedKillSwitch` has to offer with one exception - while normal kill switch recognizes difference between closing a stream gracefully (via. `Shutdown()`) and abruptly (via. `Abort(exception)`), .NET cancellation tokens have no such distinction. Therefore you need to explicitly specify at the moment of defining a flow stage, if cancellation token call should cause stream to close with completion or failure, by using `cancelGracefully` parameter. If it's set to `false`, calling cancel on a token's source will cause stream to fail with an `OperationCanceledException`. -## Dynamic fan-in and fan-out with MergeHub and BroadcastHub +## Dynamic Fan-in and Fan-Out with MergeHub and BroadcastHub There are many cases when consumers or producers of a certain service (represented as a Sink, Source, or possibly Flow) are dynamic and not known in advance. The Graph DSL does not allow to represent this, all connections of the graph must be known in advance and must be connected upfront. To allow dynamic fan-in and fan-out streaming, the Hubs should be used. They provide means to construct Sink and Source pairs that are “attached” to each other, but one of them can be materialized multiple times to implement dynamic fan-in or fan-out. @@ -92,7 +92,7 @@ A `BroadcastHub` can be used to consume elements from a common producer by a dyn The resulting `Source` can be materialized any number of times, each materialization effectively attaching a new subscriber. If there are no subscribers attached to this hub then it will not drop any elements but instead backpressure the upstream producer until subscribers arrive. This behavior can be tweaked by using the combinators `Buffer` for example with a drop strategy, or just attaching a subscriber that drops all messages. If there are no other subscribers, this will ensure that the producer is kept drained (dropping all elements) and once a new subscriber arrives it will adaptively slow down, ensuring no more messages are dropped. -### Combining dynamic stages to build a simple Publish-Subscribe service +### Combining Dynamic Stages to Build a Simple Publish-Subscribe Service The features provided by the Hub implementations are limited by default. This is by design, as various combinations can be used to express additional features like unsubscribing producers or consumers externally. We show here an example that builds a `Flow` representing a publish-subscribe channel. The input of the `Flow` is published to all subscribers while the output streams all the elements published. diff --git a/docs/articles/streams/streamrefs.md b/docs/articles/streams/streamrefs.md index 4fbec05d849..2a51a02bc4a 100644 --- a/docs/articles/streams/streamrefs.md +++ b/docs/articles/streams/streamrefs.md @@ -3,7 +3,7 @@ uid: stream-ref title: StreamRefs - Reactive Streams over the network --- -# StreamRefs - Reactive Streams over the network +# StreamRefs - Reactive Streams Over the Network Stream references, or “stream refs” for short, allow running Akka Streams across multiple nodes within an Akka Remote boundaries. @@ -24,7 +24,7 @@ Stream refs are not persistent, however it is simple to build a recover-able str Since the two sides (“local” and “remote”) of each reference may be confusing to simply refer to as “remote” and “local” – since either side can be seen as “local” or “remote” depending how we look at it – we propose to use the terminology “origin” and “target”, which is defined by where the stream ref was created. For SourceRefs, the “origin” is the side which has the data that it is going to stream out. For SinkRefs the “origin” side is the actor system that is ready to receive the data and has allocated the ref. Those two may be seen as duals of each other, however to explain patterns about sharing references, we found this wording to be rather useful. -### Source Refs - offering streaming data to a remote system +### Source Refs - Offering Streaming Data to a Remote System A `SourceRef` can be offered to a remote actor system in order for it to consume some source of data that we have prepared locally. @@ -43,7 +43,7 @@ The process of preparing and running a `ISourceRef` powered distributed strea > [!WARNING] > A `ISourceRef` is by design “single-shot”. i.e. it may only be materialized once. This is in order to not complicate the mental model what materializing such value would mean. While stream refs are designed to be single shot, you may use them to mimic multicast scenarios, simply by starting a `Broadcast` stage once, and attaching multiple new streams to it, for each emitting a new stream ref. This way each output of the broadcast is by itself an unique single-shot reference, however they can all be powered using a single `Source` – located before the `Broadcast` stage. -### Sink Refs - offering to receive streaming data from a remote system +### Sink Refs - Offering to Receive Streaming Data From a Remote System They can be used to offer the other side the capability to send to the origin side data in a streaming, flow-controlled fashion. The origin here allocates a Sink, which could be as simple as a `Sink.ForEach` or as advanced as a complex sink which streams the incoming data into various other systems (e.g. any of the Alpakka provided Sinks). @@ -65,7 +65,7 @@ The process of preparing and running a `ISinkRef<>` powered distributed stream i ## Configuration -### Stream reference subscription timeouts +### Stream Reference Subscription Timeouts All stream references have a subscription timeout, which is intended to prevent resource leaks in situations in which a remote node would requests the allocation of many streams yet never actually run them. In order to prevent this, each stream reference has a default timeout (of 30 seconds), after which the origin will abort the stream offer if the target has not materialized the stream ref in time. After the timeout has triggered, materialization of the target side will fail pointing out that the origin is missing. diff --git a/docs/articles/streams/testingstreams.md b/docs/articles/streams/testingstreams.md index 6578f693fea..496948453a2 100644 --- a/docs/articles/streams/testingstreams.md +++ b/docs/articles/streams/testingstreams.md @@ -3,7 +3,7 @@ uid: streams-testing title: Testing streams --- -# Testing streams +# Testing Streams Verifying behavior of Akka Stream sources, flows and sinks can be done using various code patterns and libraries. Here we will discuss testing these @@ -18,7 +18,7 @@ flows and sinks. This makes them easily testable by wiring them up to other sources or sinks, or some test harnesses that `Akka.Testkit` or `Akka.Streams.Testkit` provide. -## Built in sources, sinks and combinators +## Built in Sources, Sinks and Combinators Testing a custom sink can be as simple as attaching a source that emits elements from a predefined collection, running a constructed test flow and diff --git a/docs/articles/streams/workingwithgraphs.md b/docs/articles/streams/workingwithgraphs.md index 90528401309..664c6422bed 100644 --- a/docs/articles/streams/workingwithgraphs.md +++ b/docs/articles/streams/workingwithgraphs.md @@ -110,7 +110,7 @@ RunnableGraph.FromGraph(GraphDsl.Create(topHeadSink, bottomHeadSink, Keep.Both, })); ``` -## Constructing and combining Partial Graphs +## Constructing and Combining Partial Graphs Sometimes it is not possible (or needed) to construct the entire computation graph in one place, but instead construct all of its different phases in different places and in the end connect them all into a complete graph and run it. @@ -166,7 +166,7 @@ Then we import it (all of its nodes and connections) explicitly into the closed > [!WARNING] > Please note that `GraphDSL` is not able to provide compile time type-safety about whether or not all elements have been properly connected—this validation is performed as a runtime check during the graph's instantiation. A partial graph also verifies that all ports are either connected or part of the returned `Shape`. -## Constructing Sources, Sinks and Flows from Partial Graphs +## Constructing Sources, Sinks and Flows From Partial Graphs Instead of treating a partial graph as simply a collection of flows and junctions which may not yet all be connected it is sometimes useful to expose such a complex graph as a simpler structure, @@ -229,7 +229,7 @@ var pairUpWithToString = Flow.FromGraph( pairUpWithToString.RunWith(Source.From(new[] {1}), Sink.First>(), materializer); ``` -## Combining Sources and Sinks with simplified API +## Combining Sources and Sinks with Simplified API There is a simplified API you can use to combine sources and sinks with junctions like: ``Broadcast``, ``Balance``, ``Merge`` and ``Concat`` without the need for using the Graph DSL. The combine method takes care of constructing the necessary graph underneath. In following example we combine two sources into one (fan-in): @@ -253,7 +253,7 @@ var sink = Sink.Combine(i => new Broadcast(i), sendRemotely, localProcessin Source.From(new[] {0, 1, 2}).RunWith(sink, materializer); ``` -## Building reusable Graph components +## Building Reusable Graph Components It is possible to build reusable, encapsulated components of arbitrary input and output ports using the graph DSL. @@ -309,7 +309,7 @@ public class PriorityWorkerPoolShape : Shape } ``` -## Predefined shapes +## Predefined Shapes In general a custom `Shape` needs to be able to provide all its input and output ports, be able to copy itself, and also be able to create a new instance from given ports. There are some predefined shapes provided to avoid unnecessary @@ -664,7 +664,7 @@ result.Result.ShouldAllBeEquivalentTo(Enumerable.Range(0, 10)); This example demonstrates how `BidiFlow` subgraphs can be hooked together and also turned around with the ``.Reversed`` method. The test simulates both parties of a network communication protocol without actually having to open a network connection—the flows can just be connected directly. -## Accessing the materialized value inside the Graph +## Accessing the Materialized Value Inside the Graph In certain cases it might be necessary to feed back the materialized value of a Graph (partial, closed or backing a Source, Sink, Flow or BidiFlow). This is possible by using ``builder.MaterializedValue`` which gives an ``Outlet`` that @@ -700,7 +700,7 @@ var cyclicAggregate = Source.FromGraph(GraphDsl.Create(Sink.Aggregate( })); ``` -## Graph cycles, liveness and deadlocks +## Graph Cycles, Liveness and Deadlocks Cycles in bounded stream topologies need special considerations to avoid potential deadlocks and other liveness issues. This section shows several examples of problems that can arise from the presence of feedback arcs in stream processing graphs. diff --git a/docs/articles/streams/workingwithstreamingio.md b/docs/articles/streams/workingwithstreamingio.md index 584ee4bcb56..85182da2cb1 100644 --- a/docs/articles/streams/workingwithstreamingio.md +++ b/docs/articles/streams/workingwithstreamingio.md @@ -3,13 +3,13 @@ uid: streams-io title: Working with streaming IO --- -# Working with streaming IO +# Working with Streaming IO Akka Streams provides a way of handling File IO and TCP connections with Streams. While the general approach is very similar to the [Actor based TCP handling using Akka IO](xref:akka-io), by using Akka Streams you are freed of having to manually react to back-pressure signals, as the library does it transparently for you. ## Streaming TCP -### Accepting connections: Echo Server +### Accepting Connections: Echo Server In order to implement a simple EchoServer we bind to a given address, which returns a `Source>`, which will emit an `IncomingConnection` element for each new connection that the Server should handle: @@ -46,7 +46,7 @@ The `repl` flow we use to handle the server interaction first prints the servers A resilient REPL client would be more sophisticated than this, for example it should split out the input reading into a separate `SelectAsync` step and have a way to let the server write more data than one `ByteString` chunk at any given time, these improvements however are left as exercise for the reader. -### Avoiding deadlocks and liveness issues in back-pressured cycles +### Avoiding Deadlocks and Liveness Issues in Back-Pressured Cycles When writing such end-to-end back-pressured systems you may sometimes end up in a situation of a loop, in which either side is waiting for the other one to start the conversation. One does not need to look far to find examples of such back-pressure loops. In the two examples shown previously, we always assumed that the side we are connecting to would start the conversation, which effectively means both sides are back-pressured and can not get the conversation started. There are multiple ways of dealing with this which are explained in depth in [Graph cycles, liveness and deadlocks](xref:streams-working-with-graphs#graph-cycles-liveness-and-deadlocks), however in client-server scenarios it is often the simplest to make either side simply send an initial message. @@ -61,7 +61,7 @@ To emit the initial message we merge a `Source` with a single element, after the In this example both client and server may need to close the stream based on a parsed command - `BYE` in the case of the server, and `q` in the case of the client. This is implemented by taking from the stream until `q` and and concatenating a `Source` with a single `BYE` element which will then be sent after the original source completed. -### Using framing in your protocol +### Using Framing in Your Protocol Streaming transport protocols like TCP just pass streams of bytes, and does not know what is a logical chunk of bytes from the application's point of view. Often when implementing network protocols you will want to introduce your own framing. This can be done in two ways: An end-of-frame marker, e.g. end line `\n`, can do framing via `Framing.Delimiter`. Or a length-field can be used to build a framing protocol. diff --git a/docs/articles/utilities/circuit-breaker.md b/docs/articles/utilities/circuit-breaker.md index 7f346ae909a..1552bb1ed02 100644 --- a/docs/articles/utilities/circuit-breaker.md +++ b/docs/articles/utilities/circuit-breaker.md @@ -4,7 +4,7 @@ title: Circuit Breaker --- # Circuit Breaker -## Why are they used? +## Why Are They Used? A circuit breaker is used to provide stability and prevent cascading failures in distributed systems. These should be used in conjunction with judicious timeouts at the interfaces between remote systems to prevent the failure of a single component from bringing down all components. @@ -14,7 +14,7 @@ Introducing circuit breakers on the web service call would cause the requests to The Akka.NET library provides an implementation of a circuit breaker called `Akka.Pattern.CircuitBreaker` which has the behavior described below. -## What do they do? +## What Do They Do? * During normal operation, a circuit breaker is in the `Closed` state: * Exceptions or calls exceeding the configured `СallTimeout` increment a failure counter diff --git a/docs/articles/utilities/event-bus.md b/docs/articles/utilities/event-bus.md index 4a8757db426..cf62768cfa0 100644 --- a/docs/articles/utilities/event-bus.md +++ b/docs/articles/utilities/event-bus.md @@ -4,7 +4,7 @@ title: Event Bus --- # EventBus -## Subscribing to Dead letter messages +## Subscribing to Dead Letter Messages The following example demonstrates the capturing of dead letter messages generated from a stopped actor. The dedicated actor will output the message, sender and recipient of the captured dead letter to the console. @@ -58,7 +58,7 @@ sample capture DeadLetter captured: another message, sender: [akka://MySystem/deadLetters], recipient: [akka://MySystem/user/ExpendableActor#1469246785] ``` -## Subscribing to messages of type "string" +## Subscribing to Messages of Type `string` ```csharp var system = ActorSystem.Create("MySystem"); @@ -69,6 +69,6 @@ system.EventStream.Subscribe(subscriber,typeof(string)); system.EventStream.Publish("hello"); //this will be forwarded to subscriber ``` -### Further reading +### Further Reading * Read more on diff --git a/docs/articles/utilities/logging.md b/docs/articles/utilities/logging.md index c9a5ff4bc1e..6051533e4fe 100644 --- a/docs/articles/utilities/logging.md +++ b/docs/articles/utilities/logging.md @@ -84,7 +84,7 @@ akka { } ``` -## Logging Unhandled messages +## Logging Unhandled Messages It is possible to configure akka so that Unhandled messages are logged as Debug log events for debug purposes. This can be achieved using the following configuration setting: @@ -94,7 +94,7 @@ akka { } ``` -## Example configuration +## Example Configuration ```hocon akka { diff --git a/docs/articles/utilities/may-change.md b/docs/articles/utilities/may-change.md index 285ec769b4a..cf01c99bba4 100644 --- a/docs/articles/utilities/may-change.md +++ b/docs/articles/utilities/may-change.md @@ -3,7 +3,7 @@ uid: may-change title: Modules marked May Change --- -# Modules marked "May Change" +# Modules Marked "May Change" To be able to introduce new modules and APIs without freezing them the moment they are released we have introduced diff --git a/docs/articles/utilities/scheduler.md b/docs/articles/utilities/scheduler.md index 9f009de18a6..7ad56fdf04b 100644 --- a/docs/articles/utilities/scheduler.md +++ b/docs/articles/utilities/scheduler.md @@ -21,7 +21,7 @@ The Akka scheduler is **not** designed for long-term scheduling (see [Akka.Quart > [!WARNING] > The default implementation of Scheduler used by Akka is based on job buckets which are emptied according to a fixed schedule. It does not execute tasks at the exact time, but on every tick, it will run everything that is (over)due. The accuracy of the default Scheduler can be modified by the `akka.scheduler.tick-duration` configuration property. -## Some examples +## Some Examples ```csharp var system = ActorSystem.Create("MySystem"); @@ -45,7 +45,7 @@ invocation it is better to use the `Schedule()` variant accepting a message and an `IActorRef` to schedule a message to self (containing the necessary parameters) and then call the method when the message is received. -## From inside the actor +## From Inside the Actor ```csharp Context.System.Scheduler.ScheduleTellRepeatedly(....); @@ -54,11 +54,11 @@ Context.System.Scheduler.ScheduleTellRepeatedly(....); > [!WARNING] > All scheduled task will be executed when the `ActorSystem` is terminated. i.e. the task may execute before its timeout. -## The scheduler interface +## The Scheduler Interface The actual scheduler implementation is defined by config and loaded upon ActorSystem start-up, which means that it is possible to provide a different one using the `akka.scheduler.implementation` configuration property. The referenced class must implement the `Akka.Actor.IScheduler` and `Akka.Actor.IAdvancedScheduler` interfaces -## The cancellable interface +## The Cancellable Interface Scheduling a task will result in a `ICancellable` or (or throw an `Exception` if attempted after the scheduler's shutdown). This allows you to cancel something that has been scheduled for execution. diff --git a/docs/articles/utilities/serilog.md b/docs/articles/utilities/serilog.md index 94b0f1e25a2..96f3eb44aa3 100644 --- a/docs/articles/utilities/serilog.md +++ b/docs/articles/utilities/serilog.md @@ -83,7 +83,7 @@ var logger = new LoggerConfiguration() .CreateLogger(); ``` -## HOCON configuration +## HOCON Configuration In order to be able to change log level without the need to recompile, we need to employ some sort of application configuration. To use Serilog via HOCON configuration, add the following to the __App.config__ of the project. diff --git a/docs/community/books.md b/docs/community/books.md index 1fab07444e0..83542d56bf6 100644 --- a/docs/community/books.md +++ b/docs/community/books.md @@ -11,7 +11,7 @@ title: Books * [Akka in action](https://www.manning.com/books/akka-in-action) (Raymond Roestenburg, Rob Bakker, and Rob Williams, September 2016) * [Reactive Applications with Akka.NET](https://www.manning.com/books/reactive-applications-with-akka-net) (Anthony Brown, September 2018) -## Reactive programming +## Reactive Programming * [Reactive Messaging Patterns with the Actor Model](https://www.amazon.com/dp/B011S8YC5G) (Vaughn Vernon, July 2015) * [Functional Reactive Programming](https://www.manning.com/books/functional-reactive-programming) (Stephen Blackheath and Anthony Jones, July 2016) diff --git a/docs/community/contributor-guidelines.md b/docs/community/contributor-guidelines.md index 5aa7a80dc7b..36809a58344 100644 --- a/docs/community/contributor-guidelines.md +++ b/docs/community/contributor-guidelines.md @@ -2,9 +2,9 @@ uid: contributor-guidelines title: Contributor guidelines --- -# Contributor guidelines +# Contributor Guidelines -## To be considered while porting Akka to Akka.NET +## To Be Considered While Porting Akka to Akka.NET Here are some guidelines to keep in mind when you're considering making some changes to Akka.NET: @@ -22,7 +22,7 @@ changes to Akka.NET: Akka.NET developers before making a change, you can [create an issue](https://github.com/akkadotnet/akka.net/issues/new) with the `discussion` tag or reach out to [AkkaDotNet on Twitter](https://twitter.com/AkkaDotNet). -## Coding conventions +## Coding Conventions * Use the default Resharper guidelines for code * Start private member fields with `_`, i.e. `_camelCased` diff --git a/docs/community/documentation-guidelines.md b/docs/community/documentation-guidelines.md index fd70457e0ac..9aef558cbcc 100644 --- a/docs/community/documentation-guidelines.md +++ b/docs/community/documentation-guidelines.md @@ -2,7 +2,7 @@ uid: documentation-guidelines title: Documentation guidelines --- -# Documentation guidelines +# Documentation Guidelines When developers or users have problems with software the usual forum quip is to read the manual. Sometimes in nice tones and others not so nice. It's great when the documentation is succinct and easy to read and comprehend. All too often, though, there are huge swathes of missing, incomplete, or downright wrong bits that leave people more confused than they were before they read the documentation. diff --git a/docs/community/online-resources.md b/docs/community/online-resources.md index fc54f023288..b3a79ebf0ea 100644 --- a/docs/community/online-resources.md +++ b/docs/community/online-resources.md @@ -11,7 +11,7 @@ title: Resources [**Start Bootcamp here.**](http://learnakka.net/) -## Blog posts +## Blog Posts ### Petabridge @@ -96,11 +96,11 @@ title: Resources * [Hanselminutes (April 2015)](http://hanselminutes.com/472/inside-the-akkanet-open-source-project-and-the-actor-model-with-aaron-stannard) — Good overview of concepts in Akka.NET and high-level discussion with [Aaron Stannard](https://twitter.com/aaronontheweb). * [.NET Rocks! (November 2014)](http://www.dotnetrocks.com/default.aspx?showNum=1058) — Overview of the project discussed w/ [Roger Alsing](https://twitter.com/rogeralsing). -## Code samples / Demos +## Code Samples / Demos * [Using Akka.Cluster to build a web crawler](https://github.com/petabridge/akkadotnet-code-samples/tree/master/Cluster.WebCrawler) -## Non-English resources +## Non-English Resources * [Distributed Programming Using Akka.NET Framework (in Polish)](https://www.youtube.com/watch?v=_6vDp2-VCjc) (Bartosz Sypytkowski on March 6, 2015) * [Intro to Akka.NET (in Swedish)](https://www.youtube.com/watch?v=Ta6qLA9OsjE) (Håkan Canberger on March 23, 2015) diff --git a/docs/community/whats-new/akkadotnet-v1.4-upgrade-advisories.md b/docs/community/whats-new/akkadotnet-v1.4-upgrade-advisories.md index 2f52f82d783..4a612ba1558 100644 --- a/docs/community/whats-new/akkadotnet-v1.4-upgrade-advisories.md +++ b/docs/community/whats-new/akkadotnet-v1.4-upgrade-advisories.md @@ -7,7 +7,7 @@ title: Akka.NET v1.4 Upgrade Advisories This document contains specific upgrade suggestions, warnings, and notices that you will want to pay attention to when upgrading between versions within the Akka.NET v1.4 roadmap. -## Upgrading to Akka.NET v1.4.20 from Older Versions +## Upgrading to Akka.NET v1.4.20 From Older Versions > [!NOTE] > This is an edge-case issue that only affects users who are sending primitive data types (i.e. `int`, `long`, `string`) directly over Akka.Remote or Akka.Persistence. @@ -29,7 +29,7 @@ To work around this issue, if you're affected by it (most users are not:) * Upgrade all of the nodes to v1.4.20 or later at once; * Or upgrade directly to Akka.NET v1.4.26 or later, which resolves this issue and prevents the regression from occurring. -## Upgrading to Akka.NET v1.4.26 from Older Versions +## Upgrading to Akka.NET v1.4.26 From Older Versions > [!NOTE] > This is an edge-case issue that only affects users who are sending primitive data types (i.e. `int`, `long`, `string`) directly over Akka.Remote or Akka.Persistence. @@ -52,7 +52,7 @@ This setting is set of `on` by default and it resolves the backwards compatibili If you are running a mixed .NET Core and .NET Framework cluster, see the process below. -### Deploying v1.4.26 into Mixed .NET Core and .NET Framework Environments +### Deploying v1.4.26 Into Mixed .NET Core and .NET Framework Environments *However*, if you are attempting to run a mixed-mode cluster - i.e. some services running on .NET Framework and some running on .NET Core, you will eventually want to turn this setting to `off` in order to facilitate smooth operation between both platforms. diff --git a/docs/community/whats-new/akkadotnet-v1.4.md b/docs/community/whats-new/akkadotnet-v1.4.md index fd48d049fc0..eae4cefdd71 100644 --- a/docs/community/whats-new/akkadotnet-v1.4.md +++ b/docs/community/whats-new/akkadotnet-v1.4.md @@ -3,7 +3,7 @@ uid: akkadotnet-v14-migration-guide title: What's new in Akka.NET v1.4.0? --- -# What's new in Akka.NET v1.4.0? +# What's New in Akka.NET v1.4.0? Akka.NET v1.4.0 is the culmination of many major architectural changes, improvements, bugfixes, and updates to the core Akka.NET runtime and its associated modules. @@ -33,7 +33,7 @@ In Akka.NET v1.4.1-RC2 we rolled this change back in order to: More details in the next section below. -#### Post Mortem: Stand-alone HOCON +#### Post Mortem: Stand-Alone HOCON In the previous releases of HOCON, we let the OSS project do its own thing without any real top-down plan for integrating it into Akka.NET and replacing the stand-alone HOCON engine built into the `Akka.Configuration.Config` class. @@ -45,7 +45,7 @@ The specific problems we had with stand-alone HOCON were: 2. Performance - appending a new fallback to a `HOCON.Config` object kicked off a processes of recursive deep-copying, and this was quickly found to be non-performant. 3. Inadequacies in the Akka.NET test suite - the Akka.NET test suite is very extensive, but as we discovered during the Akka.NET v1.4.1-RC1 process: our test configurations are not nearly as complex as real-world test cases are. -#### Stand-alone HOCON Future +#### Stand-Alone HOCON Future Over the course of the Akka.NET v1.4 development cycle, where we will begin introducing lots of the usual bug fixes, feature additions, and performance improvements we will begin the process of gradually introducing abstractions to make it desirable, safe, and backwards-compatible to introduce stand-alone HOCON. From 9760fd4cd7f34a4fedc5b49996b40be52165c0c1 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 29 Dec 2021 21:21:33 -0600 Subject: [PATCH 05/30] Updated Contribution Guidelines (#5461) * reorganized contribution documentation into its own area * more clean up * API change docs * Fleshing out API change policy * fixed typos and added instructions for running the API Approval Tests * finished API changes article * begin updating documentation contribution guidelines * added markdown linting instructions * finished documentation guidelines * fixed markdown linter rules * fixed spellcheck issues * fixed titlecase * fixed header * completed release process documentation * finished adding wire compatibility documentation * fixed title casing * markdown linting * fix typos * more linting * more linting * fixed numerous `!code` references * fixed documentation errors on DocFx tags * fleshed out section on expecting messages in fixed order * finished debugging section * fixed spelling errors --- docs/cSpell.json | 3 + docs/community/building-akka-net.html | 10 + .../contributing/api-changes-compatibility.md | 101 ++++++ .../build-process.md} | 40 +-- .../code-guidelines.md} | 4 +- docs/community/contributing/debugging.md | 73 +++++ .../contributing/documentation-guidelines.md | 306 ++++++++++++++++++ .../community/contributing/release-process.md | 86 +++++ docs/community/contributing/toc.yml | 14 + .../contributing/wire-compatibility.md | 96 ++++++ docs/community/contributor-guidelines.html | 10 + docs/community/documentation-guidelines.html | 10 + docs/community/documentation-guidelines.md | 62 ---- docs/community/public-api-changes.html | 10 + docs/community/public-api-changes.md | 56 ---- docs/community/toc.yml | 12 +- .../build-instructions/release-process.png | Bin 0 -> 18505 bytes .../akkadotnet-2022-sitemap.png | Bin 0 -> 66403 bytes docs/toc.yml | 2 +- src/core/Akka.Cluster/ClusterHeartbeat.cs | 2 + .../Serialization/ClusterMessageSerializer.cs | 3 + .../Actors/ReceiveTimeoutSpecs.cs | 4 - src/core/Akka.Docs.Tests/Debugging/README.md | 3 + .../Akka.Docs.Tests/Debugging/RacySpecs.cs | 235 ++++++++++++++ 24 files changed, 975 insertions(+), 167 deletions(-) create mode 100644 docs/community/building-akka-net.html create mode 100644 docs/community/contributing/api-changes-compatibility.md rename docs/community/{building-akka-net.md => contributing/build-process.md} (80%) rename docs/community/{contributor-guidelines.md => contributing/code-guidelines.md} (97%) create mode 100644 docs/community/contributing/debugging.md create mode 100644 docs/community/contributing/documentation-guidelines.md create mode 100644 docs/community/contributing/release-process.md create mode 100644 docs/community/contributing/toc.yml create mode 100644 docs/community/contributing/wire-compatibility.md create mode 100644 docs/community/contributor-guidelines.html create mode 100644 docs/community/documentation-guidelines.html delete mode 100644 docs/community/documentation-guidelines.md create mode 100644 docs/community/public-api-changes.html delete mode 100644 docs/community/public-api-changes.md create mode 100644 docs/images/community/build-instructions/release-process.png create mode 100644 docs/images/community/contribution-standards/akkadotnet-2022-sitemap.png create mode 100644 src/core/Akka.Docs.Tests/Debugging/README.md create mode 100644 src/core/Akka.Docs.Tests/Debugging/RacySpecs.cs diff --git a/docs/cSpell.json b/docs/cSpell.json index 1f17aede949..ff5c111630a 100644 --- a/docs/cSpell.json +++ b/docs/cSpell.json @@ -99,6 +99,9 @@ "Livescan", "mainlogo", "Synchromatics", + "markdownlint", + "mergetool", + "titlecase", "Varghese" ], "ignoreRegExpList": [ diff --git a/docs/community/building-akka-net.html b/docs/community/building-akka-net.html new file mode 100644 index 00000000000..e09fa1bc834 --- /dev/null +++ b/docs/community/building-akka-net.html @@ -0,0 +1,10 @@ + + + + Building and Distributing Akka.NET + + + +

This page has been moved to Akka.NET Build Process.

+ + \ No newline at end of file diff --git a/docs/community/contributing/api-changes-compatibility.md b/docs/community/contributing/api-changes-compatibility.md new file mode 100644 index 00000000000..f9a7f4679e9 --- /dev/null +++ b/docs/community/contributing/api-changes-compatibility.md @@ -0,0 +1,101 @@ +--- +uid: making-public-api-changes +title: Making Public API Changes +--- + +# Making Public API Changes + +Akka.NET follows the [practical semantic versioning methodology](https://aaronstannard.com/oss-semver/), and as such the most important convention we have to be mindful of is accurately communicating to our users whether or not Akka.NET is compatible with previous versions of the API. + +Here is what that entails: + +* Not all `public` types are part of the "public API" - some public types that are marked with the `InternalApi` attribute or live inside an `.Internal` namespace, for instance, might be public for reasons that have to do with extensibility or completeness but they are not part of the supported public API. We may make breaking changes on those APIs even between revision releases because they're explicitly advertised as not for public use. +* Everything else that is `public`, including components that can be loaded via reflection, is generally considered to be part of the public API. + +As such, we have automated procedures designed to ensure that accidental breaking / incompatible changes to the Akka.NET public API can't sail through the pull request process without some human acknowledgement first. + +## Akka.NET API Versioning Policy + +We do our best to follow "practical semantic versioning" - here is what that means in practice for Akka.NET and its plugins: + +1. **No surprise breaking changes under any circumstances** - we don't let things happen to Akka.NET users by accident. This means observing the lesson of [Chesterton's Fence](https://fs.blog/chestertons-fence/): unless an API is explicitly marked as `InternalApi` or is included inside an `.Internal` namespace assume that it's actively used by downstream plugins or applications and therefore can't be broken on a moments' notice. Users _have_ to be given a heads-up that this is going to happen so they can plan accordingly. This is true for revision, minor versions, and major versions. Therefore, [socialize your proposals for changing public APIs](https://petabridge.com/blog/use-github-professionally/) on Github long before you ever submit a pull request so the Akka.NET team can help plan your changes into a future release. +2. **New or extended public APIs can be introduced during any release** - we try to observe extend-only design as best we can throughout Akka.NET's API and wire formats, which means in essence never removing or changing the meaning of an existing API but always being free to add new ones. This can always be done throughout major, minor, or revision releases. +3. **`Obsolete` APIs can be removed between minor or major versions** - if you want to remove an `Obsolete` API it needs to be done between major / minor versions and explicitly documented in the release notes. +4. **Removing deprecated binaries only happens between minor or major versions** - in the Akka.NET v1.4 lifecycle we deprecated the `Akka.DI.*` plugins and replaced them with a consolidated `Akka.DependencyInjection` implementation. We stopped shipping updates to `Akka.DI.Autofac` as part of this effort. However, we had to ensure that all `Akka.DI.*` plugins still worked over the course of the v1.4 lifespan since we hadn't announced a planned change to users yet. Upgrade cycles happen gradually and we need to give users time to adapt to changes in the ecosystem. We can't pull the rug out from under users all at once. Thus, we still ship updates to those core `Akka.DI.*` libraries up until Akka.NET v1.5. + +This document outlines how to comply with said procedures. + +## API Change Procedures + +The goal of this process is to make conscious decisions about API changes and force the discovery of those changes during the pull request review. Here is how the process works: + +* Uses [ApiApprovals](http://jake.ginnivan.net/apiapprover/) and [ApprovalTests](https://github.com/approvals/ApprovalTests.Net) to generate a public API of a given assembly. +* The public API gets approved by a human into a `*.approved.txt` file. +* Every time the API approval test runs the API is generated again into a `*.received.txt` file. If the two files don't match the test fails on the CI server or locally. Locally on the dev's machine the predefined Diff viewer pops up (never happens on CI) and the dev has to approve the API changes (therefore making a conscious decision) +* Each PR making public API changes will contain the `*.approved.txt` file in the DIFF and all reviewers can easily see the breaking changes on the public API. + +In Akka.NET, the API approval tests can be found in the following test assembly: + + src/core/Akka.API.Tests + +The approval file is located at: + + src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt + +To generate a new approval file: + +```shell +PS> cd src/core/Akka.API.Tests +PS> dotnet test -c Release --framework net6.0 +``` + +You'll need to make sure you have an appropriate mergetool installed in order to update the `.approved.txt` files. We recommend [WinMerge](https://winmerge.org/) or [TortoiseMerge](https://tortoisesvn.net/TortoiseMerge.html). + +### Approving a New Change + +After modifying some code in Akka.NET that results in a public API change - this can be any change, such as adding an overload to a public method or adding a new public class, you will immediately see an API change when you attempt to run the `Akka.API.Tests` unit tests: + +![Failed API approval test](~/images/api-diff-fail.png) + +The tests will fail, because the `.approved.txt` file doesn't match the new `.received.txt`, but you will be prompted by [ApprovalTests](https://github.com/approvals/ApprovalTests.Net) to view the diff between the two files in your favorite diff viewer: + +![API difference as seen in a diff viewer like TortoiseMerge or WinMerge](~/images/api-diff-viewer.png) + +After you've merged the changes generated from your code into the `approved.txt` file, the tests will pass: + +![Passed API approval test](~/images/api-diff-approve.png) + +And then once you've merged in those changes, added them to a Git commit, and sent them in a pull request then other Akka.NET contributors will review your pull request and view the differences between the current `approved.txt` file and the one included in your PR: + +![approved.txt differences as reported by Git](~/images/diff-results.png) + +## Unacceptable API Changes + +The following types of API changes will generally not be approved: + +1. Any breaking modification to a commonly used public interface; +2. Modifying existing public API member signatures - extension is fine, modification is not; +3. Renaming public classes or members; and +4. Changing an access modifier from public to private / internal / protected on any member that is or is meant to be used. + +## How to Safely Introduce Public API Changes: Extend-Only Design + +So if we need to expose a new member and / or deprecate an existing member inside the public API, how can this be done safely? + +At the center of it all is [extend-only design](https://aaronstannard.com/extend-only-design/): + +1. **Previous functionality, schema, or behavior is immutable** and not open for modification. Anything you made available as a public release lives on with its current behavior, API, and definitions and isn't able to be changed. +2. **New functionality, schema, or behavior can be introduced through new constructs only** and ideally those should be opt-in. +3. **Old functionality can only be removed after a long period of time** and that's measured in years. + +How do these resolve some of the frustrating problems around versioning? + +1. Old behavior, schema, and APIs are always available and supported even in newer versions of the software; +2. New behavior is introduced as opt-in extensions that may or may not be used by the code; and +3. Both new and old code pathways are supported concurrently. + +What does this look like in practice? + +1. Old, no-longer-recommended methods still function but are marked with an `Obsolete` attribute; +2. New methods are made opt-in if their behavior differs significantly from previous implementations; and +3. We add new overloads when we need to pass in new values or parameters, rather than change existing method signatures. diff --git a/docs/community/building-akka-net.md b/docs/community/contributing/build-process.md similarity index 80% rename from docs/community/building-akka-net.md rename to docs/community/contributing/build-process.md index 829553dcb21..f13b69db3de 100644 --- a/docs/community/building-akka-net.md +++ b/docs/community/contributing/build-process.md @@ -1,12 +1,15 @@ --- uid: building-and-distributing -title: Building and Distributing Akka.NET +title: Building Akka.NET Repositories --- -# Building and Distributing Akka.NET +# Building Akka.NET Repositories Akka.NET's build system is a modified version of [Petabridge's `dotnet new` template](https://github.com/petabridge/petabridge-dotnet-new), in particular [the Petabridge.Library template](https://github.com/petabridge/Petabridge.Library/) - we typically keep our build system in sync with the documentation you can find there. +> [!TIP] +> All repositories in the [Akka.NET Github organization](https://github.com/akkadotnet) use a nearly identical build process. Type `build.cmd help` or `build.sh help` in the root of any repository to see a full list of supported build instructions. + ## Supported Commands This project supports a wide variety of commands. @@ -14,7 +17,7 @@ This project supports a wide variety of commands. To list on Windows: ```console -C:\> build.cmd help +build.cmd help ``` To list on Linux / OS X: @@ -88,27 +91,9 @@ The attached build script will automatically do the following based on the conve * Any project name ending with `.Tests.Performance` will automatically be treated as a [NBench](https://github.com/petabridge/NBench) project and will be included during the test stages of this build script; and * Any project meeting neither of these conventions will be treated as a NuGet packaging target and its `.nupkg` file will automatically be placed in the `bin\nuget` folder upon running the `build.[cmd|sh] all` command. -### DocFx for Documentation - -This solution also supports [DocFx](http://dotnet.github.io/docfx/) for generating both API documentation and articles to describe the behavior, output, and usages of your project. - -All of the relevant articles you wish to write should be added to the `/docs/articles/` folder and any API documentation you might need will also appear there. - -All of the documentation will be statically generated and the output will be placed in the `/docs/_site/` folder. - -#### Previewing Documentation - -To preview the documentation for this project, execute the following command at the root of this folder: - -```console -C:\> serve-docs.cmd -``` - -This will use the built-in `docfx.console` binary that is installed as part of the NuGet restore process from executing any of the usual `build.cmd` or `build.sh` steps to preview the fully-rendered documentation. For best results, do this immediately after calling `build.cmd buildRelease`. - ## Triggering Builds and Updates on Akka.NET Github Repositories -### Routine Updates and Pull Requests +## Routine Updates and Pull Requests Akka.NET uses Azure DevOps to run its builds and the conventions it uses are rather sample: @@ -118,14 +103,3 @@ Akka.NET uses Azure DevOps to run its builds and the conventions it uses are rat 4. Always `squash` any merges into the `dev` branch in order to preserve a clean commit history. Please read "[How to Use Github Professionally](https://petabridge.com/blog/use-github-professionally/)" for some more general ideas on how to work with a project like Akka.NET on Github. - -### Creating New Akka.NET Releases - -The process for creating new NuGet releases of Akka.NET or any of its projects is standardized: - -1. Update the `RELEASE_NOTES.md` file to include a summary of all relevant changes and the new updated version number; -2. Merge the `dev` branch into the `master` branch _by creating a merge commit_ to the history in `master` matches `dev`; -3. Create a `git tag` that matches the version number in the `RELEASE_NOTES.md` file; and -4. Push the `tag` to the main Github repository. - -This will trigger a new NuGet release to be created, with the release notes from the `RELEASE_NOTES.md` file copied into the body of the NuGet package description. diff --git a/docs/community/contributor-guidelines.md b/docs/community/contributing/code-guidelines.md similarity index 97% rename from docs/community/contributor-guidelines.md rename to docs/community/contributing/code-guidelines.md index 36809a58344..de83af2b349 100644 --- a/docs/community/contributor-guidelines.md +++ b/docs/community/contributing/code-guidelines.md @@ -1,8 +1,8 @@ --- uid: contributor-guidelines -title: Contributor guidelines +title: Code Guidelines --- -# Contributor Guidelines +# Code Guidelines ## To Be Considered While Porting Akka to Akka.NET diff --git a/docs/community/contributing/debugging.md b/docs/community/contributing/debugging.md new file mode 100644 index 00000000000..b49cf8a9b17 --- /dev/null +++ b/docs/community/contributing/debugging.md @@ -0,0 +1,73 @@ +--- +uid: debugging-akkadotnet-core +title: Debugging Akka.NET +--- + +# Debugging Akka.NET + +> [!NOTE] +> This article is intended to provide advice to OSS contributors working on Akka.NET itself, not necessarily end-users of the software. End users might still find this advice helpful, however. + +## Racy Unit Tests + +Akka.NET's test suite is quite large and periodically experiences intermittent "racy" failures as a result of various issues. This is a problem for the project as a whole because it causes us not to carefully investigate periodic and intermittent test failures as thoroughly as we should. + +You can view [the test flip rate report for Akka.NET on Azure DevOps here](https://dev.azure.com/dotnet/Akka.NET/_test/analytics?definitionId=84&contextType=build). + +What are some common reasons that test flip? How can we debug or fix them? + +### Expecting Messages in Fixed Orders + +One common reason for tests to experience high flip rates is that they expect events to happen in a fixed order, whereas due to arbitrary scheduling that's not always the case. + +For example: + +[!code-csharp[PoorOrderingSpec](../../../src/core/Akka.Docs.Tests/Debugging/RacySpecs.cs?name=PoorMsgOrdering)] + +The fundamental mistake this spec author made was using simple ordering assumptions: messages are processed in the order in which they're called. This is true *per actor*, not true for *all actors* in the given process. Once we split the traffic between more than one actor's mailbox all of our ordering assumptions go out the window. + +How do we fix this? Two possible ways. + +[!code-csharp[FixedMsgOrdering](../../../src/core/Akka.Docs.Tests/Debugging/RacySpecs.cs?name=FixedMsgOrdering)] + +The simplest way in this case is to just change the assertion to an `ExpectMsgAllOf` call, which expects an array of messages back _but doesn't care about the order in which they arrive_. This approach may not work in all cases, so the second approach we recommend to fixing these types of buggy tests will usually do the trick. + +[!code-csharp[SplitMsgOrdering](../../../src/core/Akka.Docs.Tests/Debugging/RacySpecs.cs?name=SplitMsgOrdering)] + +In this approach we split the assertions up across multiple `TestProbe` instances - that way we're not coupling each input activity to the same output mailbox. This is a more generalized approach for solving these ordering problems. + +### Not Accounting for System Message Processing Order + +An important caveat when working with Akka.NET actors: system messages always get processed ahead of user-defined messages. `Context.Watch` or `Context.Stop` are examples of methods frequently called from user code which produce system messages. + +Thus, we can get into trouble if we aren't careful about how we write our tests. + +An example of a buggy test: + +[!code-csharp[BuggySysMsgSpec](../../../src/core/Akka.Docs.Tests/Debugging/RacySpecs.cs?name=PoorSysMsgOrdering)] + +Because system messages jump the line there is no guarantee that this actor will ever successfully process their system message - it depends on the whims on the `ThreadPool` and how long it takes this actor to get activated, hence why it's racy. + +There are various ways to rewrite this test to function correctly without any raciness, but the easiest way to do this is to re-arrange the assertions: + +[!code-csharp[CorrectSysMsgOrdering](../../../src/core/Akka.Docs.Tests/Debugging/RacySpecs.cs?name=CorrectSysMsgOrdering)] + +In the case of `Context.Watch` and `ExpectTerminated`, there's a second way we can rewrite this test which doesn't require us to alter the fundamental structure of the original buggy test: + +[!code-csharp[PoisonPillSysMsgOrdering](../../../src/core/Akka.Docs.Tests/Debugging/RacySpecs.cs?name=PoisonPillSysMsgOrdering)] + +The bottom line in this case is that specs can be racy because system messages don't follow the ordering guarantees of the other 99.99999% of user messages. This particular issue is most likely to occur when you're writing specs that look for `Terminated` messages or ones that test supervision strategies, both of which necessitate system messages behind the scenes. + +### Timed Assertions + +Time-delimited assertions are the biggest source of racy unit tests generally, not just inside the Akka.NET project. These types of issues tend to come up most often inside our Akka.Streams.Tests project with tests that look like this: + +[!code-csharp[TooTightTimingSpec](../../../src/core/Akka.Docs.Tests/Debugging/RacySpecs.cs?name=TooTightTimingSpec)] + +This spec is a real test from Akka.Streams.Tests at the time this document was written. It designed to test the backpressure mechanics of the `GroupedWithin` stage, hence the usage of the `Throttle` flow. Unfortunately this stage depends on the scheduler running behind the scenes at a fixed interval and sometimes, especially on a busy Azure DevOps agent, that scheduler will not be able to hit its intervals precisely. Thus, this test will fail periodically. + +Thus there are a few ways we can fix this spec: + +1. Use `await` instead of `Task.Wait` - generally we should be doing this everywhere when possible; +2. Relax the timing constraints either by increasing the wait period or by wrapping the assertion block inside an `AwaitAssert`; or +3. Use the `TestScheduler` and manually advance the clock. That might cause other problems but it takes the non-determinism of the business of the CPU out of the picture. diff --git a/docs/community/contributing/documentation-guidelines.md b/docs/community/contributing/documentation-guidelines.md new file mode 100644 index 00000000000..d0ac4256122 --- /dev/null +++ b/docs/community/contributing/documentation-guidelines.md @@ -0,0 +1,306 @@ +--- +uid: documentation-guidelines +title: Documentation Contribution Guidelines +--- +# Documentation Contribution Guidelines + +Contributions don't need to be limited just to source code - contributions to documentation are also extremely helpful and assist users in understanding the Akka.NET project. + +## Website + +This project uses [DocFX](https://dotnet.github.io/docfx/) to generate our website. This tool uses its own version of the [Markdown](http://daringfireball.net/projects/markdown/syntax) language named [DocFX Flavored Markdown](https://dotnet.github.io/docfx/spec/docfx_flavored_markdown.html) for crafting the documents for the website. Any editor with a valid Markdown plugin based will give you the best preview/edit experience, such as [Atom](https://atom.io/) or [StackEdit](https://stackedit.io/). + +To contribute to the website's documentation, fork the main GitHub repository [Akka.NET](https://github.com/akkadotnet/akka.net). The documentation is under the [docs](https://github.com/akkadotnet/akka.net/tree/dev/docs) directory. Please be sure to read the [`CONTRIBUTING.md`](https://github.com/akkadotnet/akka.net/blob/dev/CONTRIBUTING.md) before getting started to get acquainted with the project's workflow. + +### Organization of Documentation + +In order to keep the documentation discoverable for users who are unfamiliar with the Akka.NET project, we have to enforce a degree of top-down organization to achieve this. + +Our general sitemap looks like this: + +![Akka.NET Documentation sitemap](/images/community/contribution-standards/akkadotnet-2022-sitemap.png) + +If you want to contribute a new page or documentation area, this should help you generally figure out where to categorize it. If you aren't sure where you should add a new piece of documentation, ask in [project chat](https://gitter.im/akkadotnet/akka.net) or [Akka.NET GitHub Discussions](https://github.com/akkadotnet/akka.net/discussions). + +#### Moving Documentation Pages + +One thing we absolutely don't tolerate is breaking existing links in our documentation as lots of external resources depend upon it. Thus, there's a procedure for moving a page from one directory to another that helps us preserve prior links. + +**Step 1 - Remove the old page from `toc.yml`**. +We need to do this in order to prevent the old page from showing up in the navigation under its previous location - we'll add the new destination page back to the `toc.yml` of the appropriate directory. + +**Step 2 - Move the `{filename}.md` file to its new location**. +Move the content to where it's going to live going forward. + +**Step 3 - Add the moved `{filename}.md` file to the `toc.yml` of the new folder location**. +This will update the internal navigation and search to discover the new document. + +**Step 4 - Add a `{filename}.html` in the old location of the previous `{filename}.md` file**. +This file is going to contain content that looks like this: + +[!code[Building Akka.NET old documentation location](../building-akka-net.html)] + +The HTML file uses a `meta http-equiv = "refresh"` tag to send the user, via an HTTP 301 redirect, to the new file location where the content has been moved. Yes, this is a pain but this is done in order to make sure that third party content and search engines can still find what they're looking for even after the content has been moved. + +> [!NOTE] +> In the future this won't be necessary. Once DocFx3 ships native support for folder and file-level redirects will be supported: [https://github.com/dotnet/docfx/issues/3686](https://github.com/dotnet/docfx/issues/3686) + +### DocFx Hygiene + +This section of the documentation explains the DocFx hygiene the Akka.NET project employs in order to ensure that: + +1. It's easy to correctly link between documents; +2. To reference code samples directly from the source code of the project, so those code samples are updated automatically when they're modified in-source; and +3. To make it easier to extend the documentation over a long period of time. + +#### Code Samples Must Use `!code` References + +One of the biggest sources of byte rot, when it comes to documentation, is that the samples embedded in it are gradually deprecated within the code it documents and are subsequently never updated. As a result of this end-users end up newly adopted already-obsolete practices, anti-patterns, and have a bad experience trying to adopt Akka.NET or any other software library. + +Thus [DocFx Flavored Markdown](https://dotnet.github.io/docfx/spec/docfx_flavored_markdown.html) has a great solution for us: [`!code` snippets](https://dotnet.github.io/docfx/spec/docfx_flavored_markdown.html#code-snippet). + +```markdown +[!code-[]( "")] +``` + +These allow us to embed code directly from Akka.NET's own source code, tests, and example projects into documentation articles. This is extremely useful as it helps us ensure that when the underlying code sample gets updated the documentation articles that reference that code are subsequently updated as well. + +##### Targeting Referenced Code With `#region` + +So what does a real-world example of this look like? From the [`Akka.Cluster.Tools.ClusterClient` documentation](xref:cluster-client): + +```markdown +[!code-csharp[Main](../../../src/core/Akka.Docs.Tests/Networking/ClusterClient/ClientListener.cs?name=ClusterClient)] +``` + +This references the following code in the `Akka.Docs.Tests` project: + +```csharp +#region ClusterClient +public class ClientListener : UntypedActor +{ + private readonly IActorRef _targetClient; + + public ClientListener(IActorRef targetClient) + { + _targetClient = targetClient; + } + + protected override void OnReceive(object message) + { + Context.Become(ReceiveWithContactPoints(ImmutableHashSet<ActorPath>.Empty)); + } + + protected override void PreStart() + { + _targetClient.Tell(SubscribeContactPoints.Instance); + } + + public UntypedReceive ReceiveWithContactPoints(IImmutableSet<ActorPath> contactPoints) + { + return (message) => + { + switch (message) + { + // Now do something with the up-to-date "cps" + case ContactPoints cp: + Context.Become(ReceiveWithContactPoints(cp.ContactPointsList)); + break; + // Now do something with an up-to-date "contactPoints + cp" + case ContactPointAdded cpa: + Context.Become(ReceiveWithContactPoints(contactPoints.Add(cpa.ContactPoint))); + break; + // Now do something with an up-to-date "contactPoints - cp" + case ContactPointRemoved cpr: + Context.Become(ReceiveWithContactPoints(contactPoints.Remove(cpr.ContactPoint))); + break; + } + }; + } +} +#endregion +``` + +In this case we're telling DocFx to include all of the code between the `#region` and `#endregion` tags for a region named `ClusterClient` within the `ClientListener.cs` file in this directory. + +This is preferable to referencing entire files or using specific line numbers because it's concise and can still be refactored in the future without having to update the documentation. + +##### Targeting Referenced Code With `//<{name}>` + +If you don't want to use `#region`s to target referenced code inside Akka.NET's documentation, we can also use DocFx's tag syntax to accomplish the same objective: + +```markdown +[!code-csharp[Main](../../../src/core/Akka.Docs.Tests/Networking/ClusterClient/ClientListener.cs?name=ClusterClient)] +``` + +Would also work if we used [DocFx's "tag" syntax](https://dotnet.github.io/docfx/spec/docfx_flavored_markdown.html#tag-name-representation-in-code-snippet-source-file): + +```csharp +// <ClusterClient> +public class ClientListener : UntypedActor +{ + private readonly IActorRef _targetClient; + + public ClientListener(IActorRef targetClient) + { + _targetClient = targetClient; + } + + protected override void OnReceive(object message) + { + Context.Become(ReceiveWithContactPoints(ImmutableHashSet<ActorPath>.Empty)); + } + + protected override void PreStart() + { + _targetClient.Tell(SubscribeContactPoints.Instance); + } + + public UntypedReceive ReceiveWithContactPoints(IImmutableSet<ActorPath> contactPoints) + { + return (message) => + { + switch (message) + { + // Now do something with the up-to-date "cps" + case ContactPoints cp: + Context.Become(ReceiveWithContactPoints(cp.ContactPointsList)); + break; + // Now do something with an up-to-date "contactPoints + cp" + case ContactPointAdded cpa: + Context.Become(ReceiveWithContactPoints(contactPoints.Add(cpa.ContactPoint))); + break; + // Now do something with an up-to-date "contactPoints - cp" + case ContactPointRemoved cpr: + Context.Become(ReceiveWithContactPoints(contactPoints.Remove(cpr.ContactPoint))); + break; + } + }; + } +} +// </ClusterClient> +``` + +These `// <ClusterClient>` and `// </ClusterClient>` tags would accomplish the exact same result as using `#region` and `#endregion`. + +##### Finding Appropriate Code Samples + +You are free to reference samples from any part of the Akka.NET code inside the documentation, but it's often best to have a dedicated code sample for each concept we want demonstrated in the documentation. + +In that case it might be best to do one of the following: + +1. Contribute a new code sample to the `Akka.Docs.Tests` project - these are all unit tests that are referenced somewhere in our DocFx documentation but those tests must still pass or fail or +2. Add a dedicated code sample to the `src/samples` directory, in the event that it's sufficiently complex. + +#### All Pages Must Have a `uid` Defined + +If you look closely at the header of each DocFx article in this repository you'll notice the following at the top of each page: + +```yml +--- +uid: documentation-guidelines +title: Documentation Contribution Guidelines +--- +``` + +The `title` defines the page's `<title>` tag, but the `uid` is the canonical unique identity of a given DocFx document. It allows us to build linking systems within DocFx that don't rely on fixed directory structures. + +#### All Links Between Documents Must Use `xref` + +So per the previous point above about all pages needing to have their own `uid`s defined, all links between documents within the Akka.NET documentation should all be done using the `xref` format: + +```markdown +[`Akka.Cluster.Tools.ClusterClient` documentation](xref:cluster-client) +``` + +This allows us to link to a document regardless of where it is in our file structure, which means that in the event that documentation content is refactored or re-organized the links will still work. + +### Building Documentation Locally + +Akka.NET's DocFx documentation can be built locally via a clone of the main [Akka.NET GitHub repository](https://github.com/akkadotnet/akka.net) + +To preview the documentation for this project, execute the following commands at the root of your local clone of the repository: + +#### Windows + +```console +build.cmd docfx +``` + +#### Linux / OS X + +```console +build.sh docfx +``` + +This will generate all of the static HTML / CSS / JS files needed to render the website into the `~/docs/_site` folder in your local repository. + +In order for all of the JavaScript components to work correctly in your browser, you'll need to serve the documents via a local webserver rather than the file system. You can launch DocFx's build in server via the following script in the root of this repository: + +```console +serve-docs.cmd +``` + +This will use the built-in `docfx.console` binary that is installed as part of the NuGet restore process from executing any of the usual `build.cmd` or `build.sh` steps to preview the fully-rendered documentation. + +### Markdown Linting + +[Akka.NET's build system](xref:building-and-distributing) leverages [`markdownlint`](https://github.com/DavidAnson/markdownlint) (via [`markdown-cli`](https://github.com/igorshubovych/markdownlint-cli)) to validate formatting of the articles, headline capitalization, and lots of other details. + +To run `markdownlint` locally you'll want to have [Node.JS](https://nodejs.org/en/) installed along with Node Package Manager (`npm`). + +**Installation** +To install `markdownlint-cli` execute this command to add it globally to your `npm` command line: + + npm install -g markdownlint-cli markdownlint-rule-titlecase + +**Run** +To run the markdown linting rules for Akka.NET's documentation, in the root directory of the Akka.NET GitHub repository: + + markdownlint "docs/**/*.md" --rules "markdownlint-rule-titlecase" + +If there are any linting errors the exact filename, line number, and rule infraction will be listed there. This is the exact same command we run in Akka.NET's pull request validation system. + +## Code + +When documenting code, please use the standard .NET convention of [XML triple-slash documentation comments](https://msdn.microsoft.com/en-us/library/vstudio/b2s063f7). This allows the project to use tools like Sandcastle to generate the API documentation for the project. The latest stable API documentation can be found [here](https://getakka.net/api/index.html). + +Please be mindful to including *useful* comments when documenting a class or method. *Useful* comments means including full English sentences when summarizing the code and not relying on pre-generated comments from a tool like GhostDoc. Tools like these are great in what they do *if* supplemented with well-reasoned grammar. + +**BAD** obviously auto-generated comment + +```csharp +/// <summary> +/// Class Serializer. +/// </summary> +public abstract class Serializer +{ + /// <summary> + /// Froms the binary. + /// </summary> + /// <param name="bytes">The bytes.</param> + /// <param name="type">The type.</param> + /// <returns>System.Object.</returns> + public abstract object FromBinary(byte[] bytes, Type type); +} +``` + +**GOOD** clear succinct comment + +```csharp +/// <summary> +/// A Serializer represents a bimap between an object and an array of bytes representing that object. +/// </summary> +public abstract class Serializer +{ + /// <summary> + /// Deserializes a byte array into an object of type <paramref name="type"/> + /// </summary> + /// <param name="bytes">The array containing the serialized object</param> + /// <param name="type">The type of object contained in the array</param> + /// <returns>The object contained in the array</returns> + public abstract object FromBinary(byte[] bytes, Type type); +} +``` + +We've all seen the bad examples at one time or another, but rarely do we see the good examples. A nice rule of thumb is to write the comments you would want to read while perusing the API documentation. diff --git a/docs/community/contributing/release-process.md b/docs/community/contributing/release-process.md new file mode 100644 index 00000000000..0c72e876d82 --- /dev/null +++ b/docs/community/contributing/release-process.md @@ -0,0 +1,86 @@ +--- +uid: releasing-packages +title: Creating New Releases of Akka.NET Packages +--- + +# Creating New Releases of Akka.NET Packages + +The process for creating new NuGet releases of Akka.NET or any of its projects is standardized across all repositories in the [Akka.NET GitHub organization](https://github.com/akkadotnet/). + +![Akka.NET NuGet package release process](/images/community/build-instructions/release-process.png) + +## Update `RELEASE_NOTES.md` + +If the current `RELEASE_NOTES.md` file looks like this: + +```yml +#### 1.4.30 December 20 2021 #### +Akka.NET v1.4.30 is a minor release that contains some enhancements for Akka.Streams and some bug fixes. + +New features: +* [Akka: Added StringBuilder pooling in NewtonsoftJsonSerializer](https://github.com/akkadotnet/akka.net/pull/4929) +* [Akka.TestKit: Added InverseFishForMessage](https://github.com/akkadotnet/akka.net/pull/5430) +* [Akka.Streams: Added custom frame sized Flow to Framing](https://github.com/akkadotnet/akka.net/pull/5444) +* [Akka.Streams: Allow Stream to be consumed as IAsyncEnumerable](https://github.com/akkadotnet/akka.net/pull/4742) + +Bug fixes: +* [Akka.Cluster: Reverted startup sequence change](https://github.com/akkadotnet/akka.net/pull/5437) + +If you want to see the [full set of changes made in Akka.NET v1.4.30, click here](https://github.com/akkadotnet/akka.net/milestone/61). + +| COMMITS | LOC+ | LOC- | AUTHOR | +| --- | --- | --- | --- | +| 6 | 75 | 101 | Aaron Stannard | +| 2 | 53 | 5 | Brah McDude | +| 2 | 493 | 12 | Drew | +| 1 | 289 | 383 | Andreas Dirnberger | +| 1 | 220 | 188 | Gregorius Soedharmo | +| 1 | 173 | 28 | Ismael Hamed | +``` + +And we want to release a new 1.4.31 revision of Akka.NET, we'd append this to the _top_ of the `RELEASE_NOTES.md` file: + +```yml +#### 1.4.31 December 20 2021 #### +Akka.NET v1.4.30 is a minor release that contains some bug fixes. + +Akka.NET v1.4.30 contained a breaking change that broke binary compatibility with all Akka.DI plugins. +Even though those plugins are deprecated that change is not compatible with our SemVer standards +and needed to be reverted. We regret the error. + +Bug fixes: +* [Akka: Reverted Props code refactor](https://github.com/akkadotnet/akka.net/pull/5454) + +| COMMITS | LOC+ | LOC- | AUTHOR | +| --- | --- | --- | --- | +| 1 | 9 | 2 | Gregorius Soedharmo | +``` + +For each release of Akka.NET or any of its plugins we include the following in the notes for each release: + +1. The new version number; +2. The publication date; +3. A human-written explanation for the changes; +4. A link to each of the major changes introduced in the release; +5. If necessary, a link to the GitHub Issue Milestone for this release; and +6. For the Akka.NET main project only, we run the `/tools/contributors.sh` script to generate the list of contributions by username. + +This data will be embedded into the `<ReleaseNotes>` NuGet metadata tag (via `common.props` or `Directory.Build.props`) and also in the GitHub Release artifacts listed on the repository. + +<!-- markdownlint-disable titlecase-rule --> +## Update `dev` and `master` Branches +<!-- markdownlint-enable titlecase-rule --> + +The `dev` branch contains the most recent "unshipped" changes - the `master` branch contains the most recent "shipped" changes. To do a new release we need to: + +1. Merge the updated `RELEASE_NOTES.md` into `dev` via a "squash and merge" pull request and +2. Merge the entire `dev` branch into `master` via a "merge commit" pull request. + +## Add a Version-Specific Tag + +Next we need to add an appropriate version tag to the repository: + + git tag -a {version} -m "{project} {version}" + git push upstream --tags + +Once this tag is pushed to the central repository it will kick off an Azure DevOps job that will run the [build system](xref:building-and-distributing)'s `build.cmd NuGet` command with the appropriate keys and publish destinations. diff --git a/docs/community/contributing/toc.yml b/docs/community/contributing/toc.yml new file mode 100644 index 00000000000..4f1edf6b59b --- /dev/null +++ b/docs/community/contributing/toc.yml @@ -0,0 +1,14 @@ +- name: Build Process + href: build-process.md +- name: Release Process + href: release-process.md +- name: API Compatibility Guidelines + href: api-changes-compatibility.md +- name: Wire Compatibility Guidelines + href: wire-compatibility.md +- name: Code Guidelines + href: code-guidelines.md +- name: Documentation Guidelines + href: documentation-guidelines.md +- name: Debugging Akka.NET + href: debugging.md \ No newline at end of file diff --git a/docs/community/contributing/wire-compatibility.md b/docs/community/contributing/wire-compatibility.md new file mode 100644 index 00000000000..7418803ff8f --- /dev/null +++ b/docs/community/contributing/wire-compatibility.md @@ -0,0 +1,96 @@ +--- +uid: wire-compatibility +title: Making Wire Format Changes to Akka.NET +--- + +# Making Wire Format Changes to Akka.NET + +Sometimes it's necessary to introduce new elements to the wire format of Akka.Remote, Akka.Cluster, or Akka.Persistence. This document explains how we try to do that safely in a manner that supports rolling upgrades with no downtime inside production Akka.NET clusters. + +## Wire Compatibility + +[Wire compatibility is a distinct problem from API / binary compatibility](https://aaronstannard.com/oss-compatibility-standards/) - and the big problem with wire compatibility is that it runs in two directions: + +1. **Backward compatibility**: old versions of the software must be able to successfully send messages to new versions of the software; +2. **Backward compatibility**: new versions of the software must be able to process previous versions of the wire format; and +3. **Forward compatibility**: old versions of the software must be able to process messages from _new versions of the software_ during the upgrade. + +This can be difficult to do correctly especially the forward compatibility requirement. + +### Akka.NET's Wire Compatibility Requirements + +Here are the requirements that Akka.NET introduces to its wire compatibility: + +1. All messages written using a previous stable release of Akka.NET should always be able to be read in the future; this is true across even major version upgrades_. +2. New changes to the wire format can be introduced on the read-side at any time, but the write-side must always be opt-in (disabled by default). This is designed to give the Akka.NET install base some time to gradually absorb the functioning, but mostly dormant read-side code into their applications so future rolling upgrades can be safely executed in the future. +3. Wire format changes can be made opt-out only after the release of the next minor version of Akka.NET, after which users have had a significant number of versions where the read-side code. +4. Under no circumstances are new wire types to be introduced using any type of polymorphic serialization. Schema-based serialization via Google Protocol Buffers only. + +Again, we [apply the principles of extend-only design](https://aaronstannard.com/extend-only-design/) here. Once you incorporate a change into the wire format it's there for good. + +#### Safely Enhancing Existing Wire Types and Introducing New Ones + +Akka.NET largely relies on Google Protocol Buffers for all of its internal messaging, and although we support polymorphic serialization by default for user-objects we strongly encourage those users to adopt schema-based serialization as well. + +One of the reasons why schema-based serialization is a preferred choice over polymorphic serialization is its inherent support for sane, manageable versioning. [Google's advice on how to update existing message types](https://developers.google.com/protocol-buffers/docs/proto3#updating) is excellent on this subject. + +##### Case Study: `Heartbeat` Messages in Akka.Cluster + +Consider the `Heartbeat` message in Akka.Cluster: + +[!code-csharp[Heartbeat](../../../src/core/Akka.Cluster/ClusterHeartbeat.cs?name=Heartbeat)] + +Prior to Akka.NET 1.4.19, we represented `Heartbeat` messages over the wire simply by piggy-backing off of the `Address` data type: + +<!-- not using a `code-protobuf` block here because tags aren't supported for `.proto` files --> +```proto +// Defines a remote address. +message AddressData { + string system = 1; + string hostname = 2; + uint32 port = 3; + string protocol = 4; +} +``` + +In Akka.NET v1.4.19 we wanted to add some additional data to `Heartbeat` so we could keep track of inter-node latency and this would require us to introduce an entirely new wire type. How could we accomplish this? + +> [!TIP] +> All Akka.NET serializers should implement the [`SerializerWithStringManifest` base class](xref:Akka.Serialization.SerializerWithStringManifest), which allows for explicit control type identifiers on the wire. + +First, we introduced a new Protobuf to our other message definitions: + +<!-- not using a `code-protobuf` block here because tags aren't supported for `.proto` files --> +```proto +/** + * Prior to version 1.4.19 + * Heartbeat + * Sends an Address + * Version 1.4.19 can deserialize this message but does not send it + */ + message Heartbeat { + Akka.Remote.Serialization.Proto.Msg.AddressData from = 1; + int64 sequenceNr = 2; + sint64 creationTime = 3; +} + +/** + * Prior to version 1.4.19 + * HeartbeatRsp + * Sends an UniqueAddress + * Version 1.4.19 can deserialize this message but does not send it + */ + message HeartBeatResponse { + UniqueAddress from = 1; + int64 sequenceNr = 2; + int64 creationTime = 3; +} +``` + +And we updated the `ClusterMessageSerializer` to consume these new Protobuf types: + +[!code-csharp[Heartbeat](../../../src/core/Akka.Cluster/Serialization/ClusterMessageSerializer.cs?name=MsgRead)] + +But, importantly, **we didn't add the code to begin producing any of these message types immediately** - as we need the read-side code to propagate through the install base first. We will very likely switch over to the new message type for Akka.NET v1.5. This is the crucial step - allowing the read-side code to make its way into users production applications prior to enabling the new message types. + +Another way we could do this is to introduce a configuration setting that is set to `off` by default, but when set to `on` enables the production of these new message types. diff --git a/docs/community/contributor-guidelines.html b/docs/community/contributor-guidelines.html new file mode 100644 index 00000000000..2d85ac8ef85 --- /dev/null +++ b/docs/community/contributor-guidelines.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <title>Akka.NET Contributor Guidelines + + + +

This page has been moved to Akka.NET Code Guidelines.

+ + \ No newline at end of file diff --git a/docs/community/documentation-guidelines.html b/docs/community/documentation-guidelines.html new file mode 100644 index 00000000000..7b1a3384d59 --- /dev/null +++ b/docs/community/documentation-guidelines.html @@ -0,0 +1,10 @@ + + + + Akka.NET Documentation Guidelines + + + +

This page has been moved to Akka.NET Documentation Guidelines.

+ + \ No newline at end of file diff --git a/docs/community/documentation-guidelines.md b/docs/community/documentation-guidelines.md deleted file mode 100644 index 9aef558cbcc..00000000000 --- a/docs/community/documentation-guidelines.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -uid: documentation-guidelines -title: Documentation guidelines ---- -# Documentation Guidelines - -When developers or users have problems with software the usual forum quip is to read the manual. Sometimes in nice tones and others not so nice. It's great when the documentation is succinct and easy to read and comprehend. All too often, though, there are huge swathes of missing, incomplete, or downright wrong bits that leave people more confused than they were before they read the documentation. - -So the call goes out for people to help build up the documentation. Which is great until you have a lot of people with their own ideas how everything should be laid out trying to contribute. To alleviate the confusion, guidelines are setup. This document illustrates the documentation guidelines for this project. - -There is a ton of work that still needs to be done, especially in the API documentation department. Please don't hesitate to join and contribute to the project. We welcome everyone and could use your help. - -## Website - -This project uses [DocFX](https://dotnet.github.io/docfx/) to generate our website. This tool uses its own version of the [Markdown](http://daringfireball.net/projects/markdown/syntax) language named -[DocFX Flavored Markdown](https://dotnet.github.io/docfx/spec/docfx_flavored_markdown.html) for crafting the documents for the website. Any editor with a valid Markdown plugin based will give you the best preview/edit experience, such as [Atom](https://atom.io/) or [StackEdit](https://stackedit.io/). - -To contribute to the website's documentation, fork the main github repository [Akka.Net](https://github.com/akkadotnet/akka.net). The documentation is under the [docs](https://github.com/akkadotnet/akka.net/tree/dev/docs) directory. Please be sure to read the [Contributing.md](https://github.com/akkadotnet/akka.net/blob/dev/CONTRIBUTING.md) before getting started to get acquainted with the project's workflow. - -## Code - -When documenting code, please use the standard .NET convention of [XML documentation comments](https://msdn.microsoft.com/en-us/library/vstudio/b2s063f7). This allows the project to use tools like Sandcastle to generate the API documentation for the project. The latest stable API documentation can be found [here](https://getakka.net/api/index.html). - -Please be mindful to including *useful* comments when documenting a class or method. *Useful* comments means including full English sentences when summarizing the code and not relying on pre-generated comments from a tool like GhostDoc. Tools like these are great in what they do *if* supplemented with well-reasoned grammar. - -**BAD** obviously auto-generated comment - -```csharp -/// -/// Class Serializer. -/// -public abstract class Serializer -{ - /// - /// Froms the binary. - /// - /// The bytes. - /// The type. - /// System.Object. - public abstract object FromBinary(byte[] bytes, Type type); -} -``` - -**GOOD** clear succinct comment - -```csharp -/// -/// A Serializer represents a bimap between an object and an array of bytes representing that object. -/// -public abstract class Serializer -{ - /// - /// Deserializes a byte array into an object of type - /// - /// The array containing the serialized object - /// The type of object contained in the array - /// The object contained in the array - public abstract object FromBinary(byte[] bytes, Type type); -} -``` - -We've all seen the bad examples at one time or another, but rarely do we see the good examples. A nice rule of thumb is to write the comments you would want to read while perusing the API documentation. diff --git a/docs/community/public-api-changes.html b/docs/community/public-api-changes.html new file mode 100644 index 00000000000..a2ae101d974 --- /dev/null +++ b/docs/community/public-api-changes.html @@ -0,0 +1,10 @@ + + + + Making Public API Changes to Akka.NET + + + +

This page has been moved to Making Public API Changes to Akka.NET.

+ + \ No newline at end of file diff --git a/docs/community/public-api-changes.md b/docs/community/public-api-changes.md deleted file mode 100644 index 6909dc5c374..00000000000 --- a/docs/community/public-api-changes.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -uid: making-public-api-changes -title: Making Public API Changes ---- - -# Making Public API Changes - -Akka.NET follows the [semantic versioning methodology](http://semver.org/), and as such the most important convention we have to be mindful of is accurately communicating to our users whether or not Akka.NET is compatible with previous versions of the API. - -As such, we have automated procedures designed to ensure that accidental breaking / incompatible changes to the Akka.NET public API can't sail through the pull request process without some human acknowledgement first. - -This document outlines how to comply with said procedures. - -## API Approvals - -The goal of this process is to make conscious decisions about API changes and force the discovery of those changes during the pull request review. Here is how the process works: - -* Uses [ApiApprovals](http://jake.ginnivan.net/apiapprover/) and [ApprovalTests](https://github.com/approvals/ApprovalTests.Net) to generate a public API of a given assembly. -* The public API gets approved by a human into a `*.approved.txt` file. -* Every time the API approval test runs the API is generated again into a `*.received.txt` file. If the two files don't match the test fails on the CI server or locally. Locally on the dev's machine the predefined Diff viewer pops up (never happens on CI) and the dev has to approve the API changes (therefore making a conscious decision) -* Each PR making public API changes will contain the `*.approved.txt` file in the DIFF and all reviewers can easily see the breaking changes on the public API. - -In Akka.NET, the API approval tests can be found in the following test assembly: - - src/core/Akka.API.Tests - -The approval file is located at: - - src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt - -### Approving a New Change - -After modifying some code in Akka.NET that results in a public API change - this can be any change, such as adding an overload to a public method or adding a new public class, you will immediately see an API change when you attempt to run the `Akka.API.Tests` unit tests: - -![Failed API approval test](../images/api-diff-fail.png) - -The tests will fail, because the `.approved.txt` file doesn't match the new `.received.txt`, but you will be prompted by [ApprovalTests](https://github.com/approvals/ApprovalTests.Net) to view the diff between the two files in your favorite diff viewer: - -![API difference as seen in a diff viewer like TortoiseMerge or WinMerge](../images/api-diff-viewer.png) - -After you've merged the changes generated from your code into the `approved.txt` file, the tests will pass: - -![Passed API approval test](../images/api-diff-approve.png) - -And then once you've merged in those changes, added them to a Git commit, and sent them in a pull request then other Akka.NET contributors will review your pull request and view the differences between the current `approved.txt` file and the one included in your PR: - -![approved.txt differences as reported by Git](../images/diff-results.png) - -## Unacceptable API Changes - -The following types of API changes will generally not be approved: - -1. Any modification to a commonly used public interface; -2. Changing any public method signature or removing any public members; -3. Renaming public classes or members; and -4. Changing an access modifier from public to private / internal / protected on any member that is or is meant to be used. diff --git a/docs/community/toc.yml b/docs/community/toc.yml index 0a91cc3bbb2..15670fc9d60 100644 --- a/docs/community/toc.yml +++ b/docs/community/toc.yml @@ -1,16 +1,10 @@ - name: What's New in Akka.NET? href: whats-new/toc.yml -- name: Building Akka.NET - href: building-akka-net.md +- name: Contributing to Akka.NET + href: contributing/toc.yml - name: Getting Access to Nightly Akka.NET Builds href: getting-access-to-nightly-builds.md -- name: Approving Public API Changes - href: public-api-changes.md -- name: Contributor guideline - href: contributor-guidelines.md -- name: Documentation Guidelines - href: documentation-guidelines.md -- name: Online resources +- name: Online Resources href: online-resources.md - name: Books href: books.md \ No newline at end of file diff --git a/docs/images/community/build-instructions/release-process.png b/docs/images/community/build-instructions/release-process.png new file mode 100644 index 0000000000000000000000000000000000000000..7aa7624ab1bec5f6c51aa553b2193dd398a38790 GIT binary patch literal 18505 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 literal 0 HcmV?d00001 diff --git a/docs/images/community/contribution-standards/akkadotnet-2022-sitemap.png b/docs/images/community/contribution-standards/akkadotnet-2022-sitemap.png new file mode 100644 index 0000000000000000000000000000000000000000..4d0e5720eba960c6af88def99d2df365eaa29701 GIT binary patch literal 66403 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 /// /// Sent at regular intervals for failure detection /// @@ -318,6 +319,7 @@ public override int GetHashCode() } } } + // /// /// Sends replies to messages diff --git a/src/core/Akka.Cluster/Serialization/ClusterMessageSerializer.cs b/src/core/Akka.Cluster/Serialization/ClusterMessageSerializer.cs index f3d7e443262..6b114931623 100644 --- a/src/core/Akka.Cluster/Serialization/ClusterMessageSerializer.cs +++ b/src/core/Akka.Cluster/Serialization/ClusterMessageSerializer.cs @@ -97,6 +97,8 @@ public override byte[] ToBinary(object obj) } } + // DocFx tag for documentation below - MsgRead + // public override object FromBinary(byte[] bytes, string manifest) { switch (manifest) @@ -136,6 +138,7 @@ public override object FromBinary(byte[] bytes, string manifest) throw new ArgumentException($"Unknown manifest [{manifest}] in [{nameof(ClusterMessageSerializer)}]"); } } + // public override string Manifest(object o) { diff --git a/src/core/Akka.Docs.Tests/Actors/ReceiveTimeoutSpecs.cs b/src/core/Akka.Docs.Tests/Actors/ReceiveTimeoutSpecs.cs index 7c3258f430c..9c4717ffe50 100644 --- a/src/core/Akka.Docs.Tests/Actors/ReceiveTimeoutSpecs.cs +++ b/src/core/Akka.Docs.Tests/Actors/ReceiveTimeoutSpecs.cs @@ -6,13 +6,9 @@ //----------------------------------------------------------------------- using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Akka.Actor; using Akka.TestKit.Xunit2; -using FluentAssertions; using Xunit; namespace DocsExamples.Actors diff --git a/src/core/Akka.Docs.Tests/Debugging/README.md b/src/core/Akka.Docs.Tests/Debugging/README.md new file mode 100644 index 00000000000..7accf1b3888 --- /dev/null +++ b/src/core/Akka.Docs.Tests/Debugging/README.md @@ -0,0 +1,3 @@ +# Debugging Examples + +This section of tests includes some non-functional, skipped tests as examples of how to rewrite racy or otherwise buggy unit tests in the Akka.NET test suite. \ No newline at end of file diff --git a/src/core/Akka.Docs.Tests/Debugging/RacySpecs.cs b/src/core/Akka.Docs.Tests/Debugging/RacySpecs.cs new file mode 100644 index 00000000000..b8564ae2294 --- /dev/null +++ b/src/core/Akka.Docs.Tests/Debugging/RacySpecs.cs @@ -0,0 +1,235 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Actor.Dsl; +using Akka.Streams; +using Akka.Streams.Dsl; +using Akka.TestKit; +using Akka.TestKit.Xunit2; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace DocsExamples.Debugging +{ + public class RacySpecs : TestKit + { + public RacySpecs(ITestOutputHelper output) : base(output: output) + { + + } + + [Fact(Skip = "Buggy by design")] + // + public void PoorOrderingSpec() + { + IActorRef CreateForwarder(IActorRef actorRef) + { + return Sys.ActorOf(act => + { + act.ReceiveAny((o, context) => + { + actorRef.Forward(o); + }); + }); + } + + // arrange + IActorRef a1 = Sys.ActorOf(act => + act.Receive((str, context) => + { + context.Sender.Tell(str + "a1"); + }), "a1"); + + IActorRef a2 = CreateForwarder(a1); + IActorRef a3 = CreateForwarder(a1); + + // act + a2.Tell("hit1"); + a3.Tell("hit2"); + + // assert + + /* + * RACY: no guarantee that a2 gets scheduled ahead of a3. + * That depends entirely upon the ThreadPool and the dispatcher. + */ + + ExpectMsg("hit1a1"); + ExpectMsg("hit2a1"); + } + // + + [Fact] + // + public void FixedOrderingSpec() + { + IActorRef CreateForwarder(IActorRef actorRef) + { + return Sys.ActorOf(act => + { + act.ReceiveAny((o, context) => + { + actorRef.Forward(o); + }); + }); + } + + // arrange + IActorRef a1 = Sys.ActorOf(act => + act.Receive((str, context) => + { + context.Sender.Tell(str + "a1"); + }), "a1"); + + IActorRef a2 = CreateForwarder(a1); + IActorRef a3 = CreateForwarder(a1); + + // act + a2.Tell("hit1"); + a3.Tell("hit2"); + + // assert + + // no raciness - ExpectMsgAllOf doesn't care about order + ExpectMsgAllOf("hit1a1", "hit2a1"); + } + // + + [Fact] + // + public void SplitOrderingSpec() + { + IActorRef CreateForwarder(IActorRef actorRef) + { + return Sys.ActorOf(act => + { + act.ReceiveAny((o, context) => + { + actorRef.Forward(o); + }); + }); + } + + // arrange + IActorRef a1 = Sys.ActorOf(act => + act.Receive((str, context) => + { + context.Sender.Tell(str + "a1"); + }), "a1"); + + TestProbe p2 = CreateTestProbe(); + IActorRef a2 = CreateForwarder(a1); + TestProbe p3 = CreateTestProbe(); + IActorRef a3 = CreateForwarder(a1); + + // act + + // manually set the sender - one to each TestProbe + a2.Tell("hit1", p2); + a3.Tell("hit2", p3); + + // assert + + // no raciness - both probes can process their own messages in parallel + p2.ExpectMsg("hit1a1"); + p3.ExpectMsg("hit2a1"); + } + // + + [Fact(Skip = "Buggy by design")] + // + public void PoorSystemMessagingOrderingSpec() + { + // arrange + var myActor = Sys.ActorOf(act => act.ReceiveAny((o, context) => + { + context.Sender.Tell(o); + }), "echo"); + + // act + Watch(myActor); // deathwatch + myActor.Tell("hit"); + Sys.Stop(myActor); + + // assert + ExpectMsg("hit"); + ExpectTerminated(myActor); // RACY + /* + * Sys.Stop sends a system message. If "echo" actor hasn't been scheduled to run yet, + * then the Stop command might get processed first since system messages have priority. + */ + } + // + + [Fact] + // + public void CorrectSystemMessagingOrderingSpec() + { + // arrange + var myActor = Sys.ActorOf(act => act.ReceiveAny((o, context) => + { + context.Sender.Tell(o); + }), "echo"); + + // act + Watch(myActor); // deathwatch + myActor.Tell("hit"); + + // assert + ExpectMsg("hit"); + + Sys.Stop(myActor); // terminate after asserting processing + ExpectTerminated(myActor); + } + // + + [Fact] + // + public void PoisonPillSystemMessagingOrderingSpec() + { + // arrange + var myActor = Sys.ActorOf(act => act.ReceiveAny((o, context) => + { + context.Sender.Tell(o); + }), "echo"); + + // act + Watch(myActor); // deathwatch + myActor.Tell("hit"); + + // use PoisonPill to shut down actor instead; + // eliminates raciness as it passes through /user + // queue instead of /system queue. + myActor.Tell(PoisonPill.Instance); + + // assert + ExpectMsg("hit"); + ExpectTerminated(myActor); // works as expected + } + // + + [Fact(Skip = "Racy by design")] + // + public void TooTightTimingSpec() + { + Task>> t = Source.From(Enumerable.Range(1, 10)) + .GroupedWithin(1, TimeSpan.FromDays(1)) + .Throttle(1, TimeSpan.FromMilliseconds(110), 0, Akka.Streams.ThrottleMode.Shaping) + .RunWith(Sink.Seq>(), Sys.Materializer()); + t.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + t.Result.Should().BeEquivalentTo(Enumerable.Range(1, 10).Select(i => new List {i})); + } + // + } +} \ No newline at end of file From 71289e69f4543c36a995e53ce87459cbd9078f50 Mon Sep 17 00:00:00 2001 From: Ismael Hamed <1279846+ismaelhamed@users.noreply.github.com> Date: Thu, 30 Dec 2021 15:52:22 +0100 Subject: [PATCH 06/30] Avoid nightly jobs in forked projects (#5469) --- .github/workflows/nightly-nuget.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/nightly-nuget.yml b/.github/workflows/nightly-nuget.yml index 2b8cda8b0da..a64e028eaf7 100644 --- a/.github/workflows/nightly-nuget.yml +++ b/.github/workflows/nightly-nuget.yml @@ -12,6 +12,7 @@ on: jobs: build-and-publish: runs-on: windows-latest + if: github.repository == 'akkadotnet/akka.net' steps: - uses: actions/checkout@v2 - name: Setup .NET 5 From a52b91488b32b35f04d8b6642bd68cf3c0055b16 Mon Sep 17 00:00:00 2001 From: Ismael Hamed <1279846+ismaelhamed@users.noreply.github.com> Date: Thu, 30 Dec 2021 15:53:03 +0100 Subject: [PATCH 07/30] Add random factor for CircuitBreaker (#5459) --- .../CoreAPISpec.ApproveCore.approved.txt | 3 ++ .../Akka.Tests/Pattern/CircuitBreakerSpec.cs | 2 +- src/core/Akka/Pattern/CircuitBreaker.cs | 41 +++++++++++++++---- src/core/Akka/Pattern/CircuitBreakerState.cs | 3 +- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index 805ce4d78cb..c06404e477b 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -4023,6 +4023,7 @@ namespace Akka.Pattern { public CircuitBreaker(Akka.Actor.IScheduler scheduler, int maxFailures, System.TimeSpan callTimeout, System.TimeSpan resetTimeout) { } public CircuitBreaker(Akka.Actor.IScheduler scheduler, int maxFailures, System.TimeSpan callTimeout, System.TimeSpan resetTimeout, System.TimeSpan maxResetTimeout, double exponentialBackoffFactor) { } + public CircuitBreaker(Akka.Actor.IScheduler scheduler, int maxFailures, System.TimeSpan callTimeout, System.TimeSpan resetTimeout, System.TimeSpan maxResetTimeout, double exponentialBackoffFactor, double randomFactor) { } public System.TimeSpan CallTimeout { get; } public long CurrentFailureCount { get; } public double ExponentialBackoffFactor { get; } @@ -4032,6 +4033,7 @@ namespace Akka.Pattern public System.Exception LastCaughtException { get; } public int MaxFailures { get; } public System.TimeSpan MaxResetTimeout { get; } + public double RandomFactor { get; } public System.TimeSpan ResetTimeout { get; } public Akka.Actor.IScheduler Scheduler { get; } public static Akka.Pattern.CircuitBreaker Create(Akka.Actor.IScheduler scheduler, int maxFailures, System.TimeSpan callTimeout, System.TimeSpan resetTimeout) { } @@ -4043,6 +4045,7 @@ namespace Akka.Pattern public System.Threading.Tasks.Task WithCircuitBreaker(System.Func> body) { } public System.Threading.Tasks.Task WithCircuitBreaker(System.Func body) { } public Akka.Pattern.CircuitBreaker WithExponentialBackoff(System.TimeSpan maxResetTimeout) { } + public Akka.Pattern.CircuitBreaker WithRandomFactor(double randomFactor) { } public void WithSyncCircuitBreaker(System.Action body) { } public T WithSyncCircuitBreaker(System.Func body) { } } diff --git a/src/core/Akka.Tests/Pattern/CircuitBreakerSpec.cs b/src/core/Akka.Tests/Pattern/CircuitBreakerSpec.cs index 9dd73f13cc9..deafc4511c3 100644 --- a/src/core/Akka.Tests/Pattern/CircuitBreakerSpec.cs +++ b/src/core/Akka.Tests/Pattern/CircuitBreakerSpec.cs @@ -494,7 +494,7 @@ public TestBreaker MultiFailureCb( ) public TestBreaker NonOneFactorCb() { - return new TestBreaker(new CircuitBreaker(Sys.Scheduler, 1, TimeSpan.FromMilliseconds(2000), TimeSpan.FromMilliseconds(1000), TimeSpan.FromDays(1), 5)); + return new TestBreaker(new CircuitBreaker(Sys.Scheduler, 1, TimeSpan.FromMilliseconds(2000), TimeSpan.FromMilliseconds(1000), TimeSpan.FromDays(1), 5, 0)); } } diff --git a/src/core/Akka/Pattern/CircuitBreaker.cs b/src/core/Akka/Pattern/CircuitBreaker.cs index 0f5084b5534..5c3fc735f42 100644 --- a/src/core/Akka/Pattern/CircuitBreaker.cs +++ b/src/core/Akka/Pattern/CircuitBreaker.cs @@ -128,6 +128,11 @@ internal TimeSpan CurrentResetTimeout /// public double ExponentialBackoffFactor { get; } + /// + /// TBD + /// + public double RandomFactor { get; } + //akka.io implementation is to use nested static classes and access parent member variables //.Net static nested classes do not have access to parent member variables -- so we configure the states here and //swap them above @@ -157,7 +162,7 @@ public static CircuitBreaker Create(IScheduler scheduler, int maxFailures, TimeS /// of time after which to attempt to close the circuit /// TBD public CircuitBreaker(IScheduler scheduler, int maxFailures, TimeSpan callTimeout, TimeSpan resetTimeout) - : this(scheduler, maxFailures, callTimeout, resetTimeout, TimeSpan.FromDays(36500), 1.0) + : this(scheduler, maxFailures, callTimeout, resetTimeout, TimeSpan.FromDays(36500), 1.0, 0.0) { } @@ -172,8 +177,25 @@ public CircuitBreaker(IScheduler scheduler, int maxFailures, TimeSpan callTimeou /// /// TBD public CircuitBreaker(IScheduler scheduler, int maxFailures, TimeSpan callTimeout, TimeSpan resetTimeout, TimeSpan maxResetTimeout, double exponentialBackoffFactor) + : this(scheduler, maxFailures, callTimeout, resetTimeout, maxResetTimeout, exponentialBackoffFactor, 0.0) + { + } + + /// + /// Create a new CircuitBreaker + /// + /// Reference to Akka scheduler + /// Maximum number of failures before opening the circuit + /// of time after which to consider a call a failure + /// of time after which to attempt to close the circuit + /// + /// + /// After calculation of the exponential back-off an additional random delay based on this factor is added, e.g. `0.2` adds up to `20%` delay. randomFactor should be in range `0.0` (inclusive) and `1.0` (inclusive). In order to skip this additional delay pass in `0`. + /// TBD + public CircuitBreaker(IScheduler scheduler, int maxFailures, TimeSpan callTimeout, TimeSpan resetTimeout, TimeSpan maxResetTimeout, double exponentialBackoffFactor, double randomFactor) { if (exponentialBackoffFactor < 1.0) throw new ArgumentException("factor must be >= 1.0", nameof(exponentialBackoffFactor)); + if (randomFactor < 0.0 || randomFactor > 1.0) throw new ArgumentException("randomFactor must be between 0.0 and 1.0", nameof(randomFactor)); Scheduler = scheduler; MaxFailures = maxFailures; @@ -181,6 +203,7 @@ public CircuitBreaker(IScheduler scheduler, int maxFailures, TimeSpan callTimeou ResetTimeout = resetTimeout; MaxResetTimeout = maxResetTimeout; ExponentialBackoffFactor = exponentialBackoffFactor; + RandomFactor = randomFactor; Closed = new Closed(this); Open = new Open(this); HalfOpen = new HalfOpen(this); @@ -198,7 +221,6 @@ public long CurrentFailureCount public Exception LastCaughtException { get; private set; } - /// /// Wraps invocation of asynchronous calls that need to be protected /// @@ -331,16 +353,21 @@ public CircuitBreaker OnClose(Action callback) Closed.AddListener(callback); return this; } - + /// /// The will be increased exponentially for each failed attempt to close the circuit. /// The default exponential backoff factor is 2. /// /// The upper bound of - public CircuitBreaker WithExponentialBackoff(TimeSpan maxResetTimeout) - { - return new CircuitBreaker(Scheduler, MaxFailures, CallTimeout, ResetTimeout, maxResetTimeout, 2.0); - } + public CircuitBreaker WithExponentialBackoff(TimeSpan maxResetTimeout) => + new CircuitBreaker(Scheduler, MaxFailures, CallTimeout, ResetTimeout, maxResetTimeout, 2.0, RandomFactor); + + /// + /// Adds jitter to the delay. + /// + /// after calculation of the back-off an additional random delay based on this factor is added, e.g. 0.2 adds up to 20% delay. In order to skip this additional delay pass in 0. + public CircuitBreaker WithRandomFactor(double randomFactor) => + new CircuitBreaker(Scheduler, MaxFailures, CallTimeout, ResetTimeout, MaxResetTimeout, ExponentialBackoffFactor, randomFactor); /// /// Implements consistent transition between states. Throws IllegalStateException if an invalid transition is attempted. diff --git a/src/core/Akka/Pattern/CircuitBreakerState.cs b/src/core/Akka/Pattern/CircuitBreakerState.cs index f3ffb86eedb..f225867e13f 100644 --- a/src/core/Akka/Pattern/CircuitBreakerState.cs +++ b/src/core/Akka/Pattern/CircuitBreakerState.cs @@ -88,7 +88,8 @@ protected override void EnterInternal() GetAndSet(DateTime.UtcNow.Ticks); _breaker.Scheduler.Advanced.ScheduleOnce(_breaker.CurrentResetTimeout, () => _breaker.AttemptReset()); - var nextResetTimeout = TimeSpan.FromTicks(_breaker.CurrentResetTimeout.Ticks * (long)_breaker.ExponentialBackoffFactor); + var rnd = 1.0 + ThreadLocalRandom.Current.NextDouble() * _breaker.RandomFactor; + var nextResetTimeout = TimeSpan.FromTicks(_breaker.CurrentResetTimeout.Ticks * (long)_breaker.ExponentialBackoffFactor * (long)rnd); if (nextResetTimeout < _breaker.MaxResetTimeout) { _breaker.SwapStateResetTimeout(_breaker.CurrentResetTimeout, nextResetTimeout); From d1cb7ec790735dd46d0c35189ab25437c7e1fa28 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 30 Dec 2021 09:46:13 -0600 Subject: [PATCH 08/30] docs: fixed broken links in the build-process area (#5470) * docs: fixed broken links in the build-process area * fixed links --- docs/community/contributing/build-process.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/community/contributing/build-process.md b/docs/community/contributing/build-process.md index f13b69db3de..88beb2f964e 100644 --- a/docs/community/contributing/build-process.md +++ b/docs/community/contributing/build-process.md @@ -43,7 +43,7 @@ However, please see this readme for full details. * `build.[cmd|sh] nuget nugetpublishurl=$(nugetUrl) nugetkey=$(nugetKey)` - compiles the solution in `Release` modem creates Nuget packages from any project that does not have `false` set using the version number from `RELEASE_NOTES.md`and then publishes those packages to the `$(nugetUrl)` using NuGet key `$(nugetKey)`. * `build.[cmd|sh] DocFx` - compiles the solution in `Release` mode and then uses [DocFx](http://dotnet.github.io/docfx/) to generate website documentation inside the `./docs/_site` folder. Use the `./serve-docs.cmd` on Windows to preview the documentation. -This build script is powered by [FAKE](https://fake.build/); please see their API documentation should you need to make any changes to the [`build.fsx`](build.fsx) file. +This build script is powered by [FAKE](https://fake.build/); please see their API documentation should you need to make any changes to the [`build.fsx`](https://github.com/akkadotnet/akka.net/blob/dev/build.fsx) file. ### Incremental Builds @@ -57,7 +57,7 @@ This option will work locally on Linux or Windows. ### Release Notes, Version Numbers, Etc -This project will automatically populate its release notes in all of its modules via the entries written inside [`RELEASE_NOTES.md`](RELEASE_NOTES.md) and will automatically update the versions of all assemblies and NuGet packages via the metadata included inside [`common.props`](src/common.props). +This project will automatically populate its release notes in all of its modules via the entries written inside [`RELEASE_NOTES.md`](https://github.com/akkadotnet/akka.net/blob/dev/RELEASE_NOTES.md) and will automatically update the versions of all assemblies and NuGet packages via the metadata included inside [`common.props`](https://github.com/akkadotnet/akka.net/blob/dev/src/common.props). #### RELEASE_NOTES.md From 95218dc14c2dfae5a744ec672ba1cca8638f9da8 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 30 Dec 2021 09:57:28 -0600 Subject: [PATCH 09/30] Added support for copying .HTML files in DocFx (#5471) Needed to add this to support our manual approach for creating redirects for old content https://getakka.net/community/contributing/documentation-guidelines.html#moving-documentation-pages --- docs/docfx.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/docfx.json b/docs/docfx.json index e11b8775159..c7ed6a09b63 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -27,7 +27,9 @@ }, { "files": [ "articles/**.md", + "articles/**.html", "articles/**/toc.yml", + "community/**.html", "community/**.md", "community/**/toc.yml", "toc.yml", From c9caf7073b81eb5453af22a4d6a8918c0cc28c48 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 30 Dec 2021 13:14:43 -0600 Subject: [PATCH 10/30] docs: "How to Contribute to Akka.NET" (#5472) * adding centralized contributing explainer * finished contributing index * fixed MNTR links from `build-process.md` --- docs/articles/testing/multi-node-testing.md | 5 + docs/cSpell.json | 1 + docs/community/contributing/build-process.md | 8 +- docs/community/contributing/index.md | 154 +++++++++++++++++++ docs/community/contributing/toc.yml | 2 + 5 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 docs/community/contributing/index.md diff --git a/docs/articles/testing/multi-node-testing.md b/docs/articles/testing/multi-node-testing.md index 5f6e3b65b05..eec62912d68 100644 --- a/docs/articles/testing/multi-node-testing.md +++ b/docs/articles/testing/multi-node-testing.md @@ -1,3 +1,8 @@ +--- +uid: multi-node-testing +title: Multi-Node Testing Distributed Akka.NET Applications +--- + # Multi-Node Testing Distributed Akka.NET Applications One of the most powerful testing features of Akka.NET is its ability to create and simulate real-world network conditions such as latency, network partitions, process crashes, and more. Given that any of these can happen in a production environment it's important to be able to write tests which validate your application's ability to correctly recover. diff --git a/docs/cSpell.json b/docs/cSpell.json index ff5c111630a..9198ba51769 100644 --- a/docs/cSpell.json +++ b/docs/cSpell.json @@ -3,6 +3,7 @@ "language": "en", "words": [ "actorref", + "arounds", "Akka", "appender", "asynchronicity", diff --git a/docs/community/contributing/build-process.md b/docs/community/contributing/build-process.md index 88beb2f964e..2270c1e5328 100644 --- a/docs/community/contributing/build-process.md +++ b/docs/community/contributing/build-process.md @@ -34,9 +34,9 @@ However, please see this readme for full details. * `build.[cmd|sh] buildrelease` - compiles the solution in `Release` mode. * `build.[cmd|sh] runtests` - compiles the solution in `Release` mode and runs the unit test suite (all projects that end with the `.Tests.csproj` suffix) but only under the .NET Framework configuration. All of the output will be published to the `./TestResults` folder. * `build.[cmd|sh] runtestsnetcore` - compiles the solution in `Release` mode and runs the unit test suite (all projects that end with the `.Tests.csproj` suffix) but only under the .NET Core configuration. All of the output will be published to the `./TestResults` folder. -* `build.[cmd|sh] MultiNodeTests` - compiles the solution in `Release` mode and runs the [multi-node unit test suite](../articles/testing/multi-node-testing.md) (all projects that end with the `.Tests.csproj` suffix) but only under the .NET Framework configuration. All of the output will be published to the `./TestResults/multinode` folder. -* `build.[cmd|sh] MultiNodeTestsNetCore` - compiles the solution in `Release` mode and runs the [multi-node unit test suite](../articles/testing/multi-node-testing.md) (all projects that end with the `.Tests.csproj` suffix) but only under the .NET Core configuration. All of the output will be published to the `./TestResults/multinode` folder. -* `build.[cmd|sh] MultiNodeTestsNetCore spec={className}` - compiles the solution in `Release` mode and runs the [multi-node unit test suite](../articles/testing/multi-node-testing.md) (all projects that end with the `.Tests.csproj` suffix) but only under the .NET Core configuration. Only tests that match the `{className}` will run. All of the output will be published to the `./TestResults/multinode` folder. This is a very useful setting for running multi-node tests locally. +* `build.[cmd|sh] MultiNodeTests` - compiles the solution in `Release` mode and runs the [multi-node unit test suite](xref:multi-node-testing) (all projects that end with the `.Tests.csproj` suffix) but only under the .NET Framework configuration. All of the output will be published to the `./TestResults/multinode` folder. +* `build.[cmd|sh] MultiNodeTestsNetCore` - compiles the solution in `Release` mode and runs the [multi-node unit test suite](xref:multi-node-testing) (all projects that end with the `.Tests.csproj` suffix) but only under the .NET Core configuration. All of the output will be published to the `./TestResults/multinode` folder. +* `build.[cmd|sh] MultiNodeTestsNetCore spec={className}` - compiles the solution in `Release` mode and runs the [multi-node unit test suite](xref:multi-node-testing) (all projects that end with the `.Tests.csproj` suffix) but only under the .NET Core configuration. Only tests that match the `{className}` will run. All of the output will be published to the `./TestResults/multinode` folder. This is a very useful setting for running multi-node tests locally. * `build.[cmd|sh] nbench` - compiles the solution in `Release` mode and runs the [NBench](https://nbench.io/) performance test suite (all projects that end with the `.Tests.Performance.csproj` suffix). All of the output will be published to the `./PerfResults` folder. * `build.[cmd|sh] nuget` - compiles the solution in `Release` mode and creates Nuget packages from any project that does not have `false` set and uses the version number from `RELEASE_NOTES.md`. * `build.[cmd|sh] nuget nugetprerelease=dev` - compiles the solution in `Release` mode and creates Nuget packages from any project that does not have `false` set - but in this instance all projects will have a `VersionSuffix` of `-beta{DateTime.UtcNow.Ticks}`. It's typically used for publishing nightly releases. @@ -99,7 +99,7 @@ Akka.NET uses Azure DevOps to run its builds and the conventions it uses are rat 1. All pull requests should be created on their own feature branch and should be sent to Akka.NET's `dev` branch; 2. Always review your own pull requests so other developers understand why you made the changes; -3. Any pull request that gets merged into the `dev` branch will appear in the [Akka.NET Nightly Build that evening](../getting-access-to-nightly-builds.md); and +3. Any pull request that gets merged into the `dev` branch will appear in the [Akka.NET Nightly Build that evening](xref:nightly-builds); and 4. Always `squash` any merges into the `dev` branch in order to preserve a clean commit history. Please read "[How to Use Github Professionally](https://petabridge.com/blog/use-github-professionally/)" for some more general ideas on how to work with a project like Akka.NET on Github. diff --git a/docs/community/contributing/index.md b/docs/community/contributing/index.md new file mode 100644 index 00000000000..83b38fa6a56 --- /dev/null +++ b/docs/community/contributing/index.md @@ -0,0 +1,154 @@ +--- +uid: contributing-to-akkadotnet +title: Contributing to Akka.NET +--- + +# Contributing to Akka.NET + +We welcome contributions to all of the Akka.NET organization projects from third party contributors, and this area of the documentation is designed to help explain: + +* How you can contribute to the Akka.NET project and +* How to ensure that your contributions will be accepted. + +## How to Contribute + +What are the ways you can contribute to the Akka.NET project? + +### File a Bug Report + +One of the most valuable ways you can contribute to Akka.NET is to file a high quality bug report on any of our repositories. [Our GitHub issue templates](https://github.com/akkadotnet/.github/tree/master/.github/ISSUE_TEMPLATE) will help you structure this information in a useful, valuable way for us. + +### Fix Bugs and Other Small Problems + +Akka.NET's code base is large and touches on some conceptually difficult areas of computing (concurrency, serialization, distributed systems, performance) but despite that most of the bugs reported in our issue trackers tend to be fairly small changes. _Finding_ the bug is usually the hardest part. + +If you want to help, take a look at any open issues with the "[confirmed bug](https://github.com/akkadotnet/akka.net/issues?q=is%3Aissue+is%3Aopen+label%3A%22confirmed+bug%22)" or "[potential bug](https://github.com/akkadotnet/akka.net/issues?q=is%3Aissue+is%3Aopen+label%3A%22potential+bug%22)" labels and offer to fix it in the comments. + +The Akka.NET project also classifies some easier / more conceptually straight-forward issues with the following two labels: + +* "[up for grabs](https://github.com/akkadotnet/akka.net/issues?q=is%3Aissue+is%3Aopen+label%3A%22up+for+grabs%22)" - it's not currently assigned and anyone should feel free to claim this issue and begin working on it. +* "[good for first time contributors](https://github.com/akkadotnet/akka.net/labels/good%20for%20first-time%20contributors)" - these are issues that someone totally new to working with the central Akka.NET repository could successfully resolve. + +Here are [some tips that might help you debug Akka.NET itself](xref:debugging-akkadotnet-core). + +### Documentation Improvements + +We have a number of open issues for improving the Akka.NET documentation, examples, and tutorials - which you can find via the ["docs" issue label](https://github.com/akkadotnet/akka.net/labels/docs). + +We have a [very detailed guide on how to contribute to the Akka.NET documentation here](xref:documentation-guidelines). + +### Improve Performance + +Akka.NET treats performance as a feature of the framework and therefore we're always looking for ways to improve: + +1. Message processing throughput in-memory, over Akka.Remote, Akka.Persistence, and Akka.Streams; +2. Message processing latency over `Ask`, Akka.Remote, Akka.Persistence, and Akka.Streams; +3. New actor allocation overhead; +4. Actor memory footprint; and +5. Idle CPU consumption in Akka.Cluster. + +You can find various open performance issues that have been reported by Akka.NET users and contributors by looking for the "[perf](https://github.com/akkadotnet/akka.net/labels/perf)" label. + +We use a combination of [BenchmarkDotNet](https://benchmarkdotnet.org/), [NBench](https://nbench.io/), and some custom benchmark programs to measure these facets of Akka.NET's performance. + +You can find all of our benchmarks inside the [`/src/benchmark` directory](https://github.com/akkadotnet/akka.net/tree/dev/src/benchmark). + +We welcome any and all help in working to improve these performance issues so long as those performance fixes don't compromise our [API compatibility](xref:making-public-api-changes) or [wire compatibility](xref:wire-compatibility) guidelines. + +### Port a Missing Feature or Add a New One + +We welcome porting additional features from the [original Akka project](https://akka.io/) or proposing entirely new features, but this is a larger project and you should read the rest of this document before attempting it. + +### Review Someone Else's Pull Request + +Reviewing pull requests takes a lot of time, especially larger ones that touch multiple areas of the Akka.NET code. Anyone can jump in and review anyone else's pull request on Akka.NET's GitHub repositories, not just the core developers and maintainers - thus you can be of great help to the project by reviewing someone else's pull request. + +### What Makes For an Effective Pull Request Review? + +An effective PR review is design to look for _substantial reasons not to merge a pull request_. + +Good reasons not to merge a pull request are: + +1. The code included in the PR doesn't actually work or is currently failing its tests; +2. The code might not be secure; +3. The code might be caused a related set of tests to fail; +4. The code isn't sufficiently covered by tests to check against future regressions; +5. The code really needs to have documentation to go along with it; +6. The PR doesn't comport with our standards for the project; +7. The PR takes a dependency on a third party library that we don't really want to include with Akka.NET; or +8. The code implements a feature or a change that really isn't aligned with the project's goals. + +Those are good reasons not to merge a pull request, but the goal of the reviewer is to explain that to the original author and to _help the author resolve those objections_ through constructive, helpful feedback. + +During the course of your review, you should comment on the individual changes in the file and provide the following types of feedback to the developer: + +* Ask clarifying questions - "why did you choose to do this this way?" +* Request additional comments in the code or documentation - "you need to add an XML-DOC comment for this;" "you need to add some comments explaining why you chose this;" or "you should add some documentation for this to the website." +* Explicitly link to areas in our guidelines this pull request violated - i.e. "[you haven't completed the API approval check](xref:making-public-api-changes#approving-a-new-change) for this pull request." +* Suggest ways in which this code could be better written to comport with best practices or Akka.NET idioms - i.e. "you should really use the `ReceiveActor` API instead of using `PatternMatch`." +* Look for areas that aren't sufficiently well-tested and help the contributor understand what's needed to properly check against regressions. +* Point contributors to our "[Debugging Akka.NET](xref:debugging-akkadotnet-core)" documentation if they run into trouble with getting their tests to pass. + +## Creating Contributions That Will Be Merged + +First, we strongly recommend reading the following two blog posts prior to contributing to Akka.NET - these generally explain how we use GitHub and the workflow we've used for years to manage the project: + +1. "[How to Use Github Professionally](https://petabridge.com/blog/use-github-professionally/)" +2. "[Learning the Github Workflow](https://petabridge.com/blog/github-workflow/)" + +Odds are, someone who is not you is going to end up maintaining the code or documentation you contribute. + +Therefore, you have to convince those long-term maintainers to: + +* Decide that your proposed changes are worth the perpetual maintenance effort; +* Spend hours carefully reviewing your propose changes; +* Spend even more time quality-controlling and testing overhead of your changes; and +* Fleshing out the documentation, examples, and tutorials to accommodate your changes. + +This isn't being mean - it's what is necessary to maintain Akka.NET's professional standards and to conserve the scarce amount of time maintainers and core developers have to produce new releases that satisfy those standards. + +The rest of this guide will provide you with some suggestions on how to create contributions that will be successfully merged. + +### File a GitHub Issue Before You Code + +**This is especially important if you want to contribute a new feature to Akka.NET**. + +The pull requests that will almost certainly never be merged into the Akka.NET repository are for "drive-by" features that were never socialized, discussed, or proposed to maintainers in writing prior to being written. Why would we take the time to carefully review something that no one asked for? + +Save yourself and everyone else a lot of time and trouble by proposing it in the form [a new GitHub issue](https://github.com/akkadotnet/akka.net/issues/new/choose) first. + +The issue templates will force you to: + +* Spell out why you think this change would be valuable; +* What problems not having this change causes; +* What the current alternatives or work-arounds are in Akka.NET; and +* Any other reasons why solving this issue is important, urgent, or otherwise useful. + +Maintainers of Akka.NET _want_ to make Akka.NET as valuable for as many users as possible, so it's in our interest to consider these. Having _other users_ of Akka.NET jump in and support your issue ([which you can socialize in our chatroom](https://gitter.im/akkadotnet/akka.net)) further adds to this "usefulness" or "urgency" signal that maintainers seek. + +But most importantly, starting your work by filing an issue first gives maintainers a chance to give you useful feedback on it. For instance, maybe we've already tried something like this in the past and abandoned it for reasons you're unaware of. Sharing knowledge, experience, and creativity in both directions creates an outcome that is stronger than the sum of the parts - so give that process a chance to happen _first_ before the code exists. + +It's much more difficult to have that creative conversation around a piece of code that's already set in stone, more or less. Increase your odds of success by engaging the other project members first - even for bugs and other small changes. + +### Review Our Contribution Standards and Processes + +If you're on this webpage you're already doing the right thing - please see all of the following documents if they're relevant to your changes: + +* [Building Akka.NET](xref:building-and-distributing) +* [Akka.NET Coding Guidelines](xref:contributor-guidelines) +* [Akka.NET API Compatibility Guidelines](xref:making-public-api-changes) +* [Akka.NET Wire Compatibility Guidelines](xref:wire-compatibility) +* [Akka.NET Documentation Guidelines](xref:documentation-guidelines) + +### Review Your Own Pull Requests + +One thing that will greatly, greatly improve the odds of your pull requests being merged: _review your own PRs first_. + +When you review your own pull request, here is what we want to know about each change: + +1. Why was this necessary? +2. What does it do differently than before? +3. What are some non-obvious things you'd want the next developer to know about this code? +4. How is this change safe? + +These _greatly_ reduce the amount of guesswork and inspection the other maintainers have to make and will greatly improve the speed with which these pull requests can be merged. diff --git a/docs/community/contributing/toc.yml b/docs/community/contributing/toc.yml index 4f1edf6b59b..3d5f415b507 100644 --- a/docs/community/contributing/toc.yml +++ b/docs/community/contributing/toc.yml @@ -1,3 +1,5 @@ +- name: Contributing to Akka.NET + href: index.md - name: Build Process href: build-process.md - name: Release Process From 81f0cb362ad82b9b68fafe459756b052cc022445 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 30 Dec 2021 13:52:24 -0600 Subject: [PATCH 11/30] remove CONTRIBUTING.md (#5473) Replacing with https://github.com/akkadotnet/.github/blob/master/CONTRIBUTING.md --- CONTRIBUTING.md | 176 ------------------------------------------------ 1 file changed, 176 deletions(-) delete mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 8a7a35e497c..00000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,176 +0,0 @@ -# Contributing to Akka.NET -Akka.NET is a large project and contributions are more than welcome, so thank you for wanting to contribute to Akka.NET! - ---- - -### Checklist before creating a Pull Request -Submit only relevant commits. We don't mind many commits in a pull request, but they must be relevant as explained below. - -* __Use a feature branch__ The pull request should be created from a feature branch, and not from _dev_. See below for why. -* __No merge-commits__ -If you have commits that looks like this _"Merge branch 'my-branch' into dev"_ or _"Merge branch 'dev' of github .com/akkadotnet/akka.net into dev"_ you're probaly using merge instead of [rebase](https://help.github.com/articles/about-git-rebase) locally. See below on _Handling updates from upstream_. -* __Squash commits__ Often we create temporary commits like _"Started implementing feature x"_ and then _"Did a bit more on feature x"_. Squash these commits together using [interactive rebase](https://help.github.com/articles/about-git-rebase). Also see [Squashing commits with rebase](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html). -* __Descriptive commit messages__ If a commit's message isn't descriptive, change it using [interactive rebase](https://help.github.com/articles/about-git-rebase). Refer to issues using `#issue`. Example of a bad message ~~"Small cleanup"~~. Example of good message: _"Removed Security.Claims header from FSM, which broke Mono build per #62"_. Don't be afraid to write long messages, if needed. Try to explain _why_ you've done the changes. The Erlang repo has some info on [writing good commit messages](https://github.com/erlang/otp/wiki/Writing-good-commit-messages). -* __No one-commit-to-rule-them-all__ Large commits that changes too many things at the same time are very hard to review. Split large commits into smaller. See this [StackOverflow question](http://stackoverflow.com/questions/6217156/break-a-previous-commit-into-multiple-commits) for information on how to do this. -* __Tests__ Add relevant tests and make sure all existing ones still passes. Tests can be run using the command -* __No Warnings__ Make sure your code do not produce any build warnings. - -After reviewing a Pull request, we might ask you to fix some commits. After you've done that you need to force push to update your branch in your local fork. - -#### Title and Description for the Pull Request -Give the PR a descriptive title and in the description field describe what you have done in general terms and why. This will help the reviewers greatly, and provide a history for the future. - -Especially if you modify something existing, be very clear! Have you changed any algorithms, or did you just intend to reorder the code? Justify why the changes are needed. - - ---- - -### Getting started -Make sure you have a [GitHub](https://github.com/) account. - -* Fork, clone, add upstream to the Akka.NET repository. See [Fork a repo](https://help.github.com/articles/fork-a-repo) for more detailed instructions or follow the instructions below. - -* Fork by clicking _Fork_ on https://github.com/akkadotnet/akka.net -* Clone your fork locally. - -``` -git clone https://github.com/YOUR-USERNAME/akka.net -``` - -* Add an upstream remote. - -``` -git remote add upstream https://github.com/akkadotnet/akka.net -``` -You now have two remotes: _upstream_ points to https://github.com/akkadotnet/akka.net, and _origin_ points to your fork on GitHub. - -* Make changes. See below. - -Unsure where to start? Issues marked with [_up for grabs_](https://github.com/akkadotnet/akka.net/labels/up%20for%20grabs) are things we want help with. - -See also: [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/) - -New to Git? See https://help.github.com/articles/what-are-other-good-resources-for-learning-git-and-github - -### Making changes -__Never__ work directly on _dev_ or _master_ and you should never send a pull request from master - always from a feature branch created by you. - -* Pick an [issue](https://github.com/akkadotnet/akka.net/issues). If no issue exists (search first) create one. -* Get any changes from _upstream_. - -``` -git checkout dev -git fetch upstream -git merge --ff-only upstream/dev -git push origin dev #(optional) this makes sure dev in your own fork on GitHub is up to date -``` - -See https://help.github.com/articles/fetching-a-remote for more info - -* Create a new feature branch. It's important that you do your work on your own branch and that it's created off of _dev_. Tip: Give it a descriptive name and include the issue number, e.g. `implement-testkits-eventfilter-323` or `295-implement-tailchopping-router`, so that others can see what is being worked on. - -``` -git checkout -b my-new-branch-123 -``` - -* Work on your feature. Commit. -* Rebase often, see below. -* Make sure you adhere to _Checklist before creating a Pull Request_ described above. -* Push the branch to your fork on GitHub - -``` -git push origin my-new-branch-123 -``` - -* Send a Pull Request, see https://help.github.com/articles/using-pull-requests to the _dev_ branch. - -See also: [Understanding the GitHub Flow](https://guides.github.com/introduction/flow/) (we're using `dev` as our master branch) - -### Handling updates from upstream - -While you're working away in your branch it's quite possible that your upstream _dev_ may be updated. If this happens you should: - -* [Stash](http://git-scm.com/book/en/Git-Tools-Stashing) any un-committed changes you need to - -``` -git stash - -``` - -* Update your local _dev_ by fetching from _upstream_ - -``` -git checkout dev -git fetch upstream -git merge --ff-only upstream/dev -``` - -* Rebase your feature branch on _dev_. See [Git Branching - Rebasing](http://git-scm.com/book/en/Git-Branching-Rebasing) for more info on rebasing - -``` -git checkout my-new-branch-123 -git rebase dev -git push origin dev #(optional) this makes sure dev in your own fork on GitHub is up to date -``` - -This ensures that your history is "clean" i.e. you have one branch off from _dev_ followed by your changes in a straight line. Failing to do this ends up with several "messy" merges in your history, which we don't want. This is the reason why you should always work in a branch and you should never be working in, or sending pull requests from _dev_. - -If you're working on a long running feature then you may want to do this quite often, rather than run the risk of potential merge issues further down the line. - -### Making changes to a Pull request -If you realize you've missed something after submitting a Pull request, just commit to your local branch and push the branch just like you did the first time. This commit will automatically be included in the Pull request. -If we ask you to change already published commits using interactive rebase (like squashing or splitting commits or rewriting commit messages) you need to force push using `-f`: -``` -git push -f origin my-new-branch-123 -``` - -### The build server isn't picking up a Pull request that I've modified -The build server relies on git commit timestamps to keep track of new builds that it needs to perform. When updating a PR, sometimes the timestamp of the latest commit in the PR isn't updated. This leads the build server to think that the PR has already been built and tested. In order to force the build server to rebuild and test the updated PR, please follow the instructions outlined in this post [How can one change the timestamp of an old commit in Git?](http://stackoverflow.com/questions/454734/how-can-one-change-the-timestamp-of-an-old-commit-in-git/31540373#31540373). - -### All my commits are on dev. How do I get them to a new branch? ### -If all commits are on _dev_ you need to move them to a new feature branch. - -You can rebase your local _dev_ on _upstream/dev_ (to remove any merge commits), rename it, and recreate _dev_ -``` -git checkout dev -git rebase upstream/dev -git branch -m my-new-branch-123 -git branch dev upstream/dev -``` -Or you can create a new branch off of _dev_ and then cherry pick the commits -``` -git checkout -b my-new-branch-123 upstream/dev -git cherry-pick rev #rev is the revisions you want to pick -git cherry-pick rev #repeat until you have picked all commits -git branch -m dev old-dev #rename dev -git branch dev upstream/dev #create a new dev -``` -### What to do with feature branch after the pull request is merged and closed ? ### -After a pull request has been merged and closed you can delete the feature branch. - -Get latest changes from the upstream - -``` -git checkout dev -git fetch upstream -git merge --ff-only upstream/dev -git push origin dev -``` - -Remove the branch locally - -``` -git branch -d my-new-branch-123 -``` -Remove the branch on remote - -``` -git push origin --delete my-new-branch-123 -``` - -## Code guidelines - -See our [Contributor Guidelines](http://getakka.net/community/contributor-guidelines.html) for more information on following the project's conventions. - ---- -Props to [NancyFX](https://github.com/NancyFx/Nancy) from which we've "borrowed" some of this text. From 3e9248c9ac2d697b9d52e8441509481fa1bef75f Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 30 Dec 2021 15:42:10 -0600 Subject: [PATCH 12/30] Added Github Discussions to top of website (#5474) https://github.com/akkadotnet/akka.net/issues/4786 --- docs/toc.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/toc.yml b/docs/toc.yml index 0a148b06518..2d51eedc3ce 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -9,5 +9,7 @@ homepage: community/online-resources.md - name: Project Chat href: https://gitter.im/akkadotnet/akka.net +- name: Discussion Forum + href: https://github.com/akkadotnet/akka.net/discussions - name: Source Code href: https://github.com/akkadotnet/akka.net From 10515184b57aeee0b7fea29640501c452de6926d Mon Sep 17 00:00:00 2001 From: Ismael Hamed <1279846+ismaelhamed@users.noreply.github.com> Date: Mon, 3 Jan 2022 17:40:46 +0100 Subject: [PATCH 13/30] Add Flow.LazyInitAsync, and fix materialized value of Sink.LazyInit must be a Task (#5476) --- docs/articles/streams/builtinstages.md | 28 ++ .../CoreAPISpec.ApproveStreams.approved.txt | 17 ++ .../Akka.Streams.Tests/Dsl/LazyFlowSpec.cs | 227 +++++++++++++++ .../Akka.Streams.Tests/Dsl/LazySinkSpec.cs | 94 ++----- .../Akka.Streams.Tests/IO/FileSinkSpec.cs | 10 +- src/core/Akka.Streams/Dsl/Flow.cs | 21 ++ src/core/Akka.Streams/Dsl/Sink.cs | 38 ++- .../Akka.Streams/Implementation/Fusing/Ops.cs | 265 +++++++++++++++++- src/core/Akka.Streams/Implementation/Sinks.cs | 222 +++++++-------- .../Implementation/Stages/Stages.cs | 4 + 10 files changed, 713 insertions(+), 213 deletions(-) create mode 100644 src/core/Akka.Streams.Tests/Dsl/LazyFlowSpec.cs diff --git a/docs/articles/streams/builtinstages.md b/docs/articles/streams/builtinstages.md index 4276ececfb5..355131b54e7 100644 --- a/docs/articles/streams/builtinstages.md +++ b/docs/articles/streams/builtinstages.md @@ -449,6 +449,18 @@ are emitted from the source The ``Stream`` will no longer be writable when the ``Source`` has been canceled from its downstream, and closing the ``Stream`` will complete the ``Source``. +### LazyInitAsync + +Creates a real ``Sink`` upon receiving the first element. Internal sink will not be created if there are no elements, because of completion or error. + +* If upstream completes before an element was received then the ``Task`` is completed with ``None``. +* If upstream fails before an element was received, ``sinkFactory`` throws an exception, or materialization of the internal sink fails then the ``Task`` is completed with the exception. +* Otherwise the ``Task`` is completed with the materialized value of the internal sink. + +**cancels** never + +**backpressures** when initialized and when created sink backpressures + ## File IO Sinks and Sources Sources and sinks for reading and writing files can be found on ``FileIO``. @@ -757,6 +769,22 @@ If the wire-tap ``Sink`` backpressures, elements that would've been sent to it w **cancels** when downstream cancels +### LazyInitAsync + +Creates a real ``Flow`` upon receiving the first element by calling relevant flowFactory given as an argument. Internal flow will not be created if there are no elements, because of completion or error. The materialized value of the ``Flow`` will be the materialized value of the created internal flow. + +The materialized value of the Flow is a ``Task>`` that is completed with ```TMat``` when the internal flow gets materialized or with ``None`` when there where no elements. If the flow materialization (including the call of the ``flowFactory``) fails then the future is completed with a failure. + +Adheres to the ``ActorAttributes.SupervisionStrategy`` attribute. + +**emits** when the internal flow is successfully created and it emits + +**backpressures** when the internal flow is successfully created and it backpressures + +**completes** when upstream completes and all elements have been emitted from the internal flow + +**completes** when upstream completes and all futures have been completed and all elements have been emitted + ## Asynchronous Processing Stages These stages encapsulate an asynchronous computation, properly handling backpressure while taking care of the asynchronous diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt index c471b93a465..c43ccd9a3da 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt @@ -1316,6 +1316,7 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.Flow FromSinkAndSource(Akka.Streams.IGraph, TMat1> sink, Akka.Streams.IGraph, TMat2> source, System.Func combine) { } public static Akka.Streams.Dsl.Flow Identity() { } public static Akka.Streams.Dsl.Flow Identity() { } + public static Akka.Streams.Dsl.Flow>> LazyInitAsync(System.Func>> flowFactory) { } } public class static FlowOperations { @@ -1910,6 +1911,10 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.Sink Ignore() { } public static Akka.Streams.Dsl.Sink> Last() { } public static Akka.Streams.Dsl.Sink> LastOrDefault() { } + public static Akka.Streams.Dsl.Sink>> LazyInitAsync(System.Func>> sinkFactory) { } + [System.ObsoleteAttribute("Use LazyInitAsync instead. LazyInitAsync no more needs a fallback function and th" + + "e materialized value more clearly indicates if the internal sink was materialize" + + "d or not.")] public static Akka.Streams.Dsl.Sink> LazySink(System.Func>> sinkFactory, System.Func fallback) { } public static Akka.Streams.Dsl.Sink OnComplete(System.Action success, System.Action failure) { } public static Akka.Streams.Dsl.Sink> Publisher() { } @@ -4188,6 +4193,17 @@ namespace Akka.Streams.Implementation.Fusing protected override Akka.Streams.Stage.GraphStageLogic CreateLogic(Akka.Streams.Attributes inheritedAttributes) { } } [Akka.Annotations.InternalApiAttribute()] + public sealed class LazyFlow : Akka.Streams.Stage.GraphStageWithMaterializedValue, System.Threading.Tasks.Task>> + { + public LazyFlow(System.Func>> flowFactory) { } + public Akka.Streams.Inlet In { get; } + protected override Akka.Streams.Attributes InitialAttributes { get; } + public Akka.Streams.Outlet Out { get; } + public override Akka.Streams.FlowShape Shape { get; } + public override Akka.Streams.Stage.ILogicAndMaterializedValue>> CreateLogicAndMaterializedValue(Akka.Streams.Attributes inheritedAttributes) { } + public override string ToString() { } + } + [Akka.Annotations.InternalApiAttribute()] public sealed class LimitWeighted : Akka.Streams.Implementation.Fusing.SimpleLinearGraphStage { public LimitWeighted(long max, System.Func costFunc) { } @@ -4501,6 +4517,7 @@ namespace Akka.Streams.Implementation.Stages public static readonly Akka.Streams.Attributes InputStreamSource; public static readonly Akka.Streams.Attributes LastOrDefaultSink; public static readonly Akka.Streams.Attributes LastSink; + public static readonly Akka.Streams.Attributes LazyFlow; public static readonly Akka.Streams.Attributes LazySink; public static readonly Akka.Streams.Attributes LazySource; public static readonly Akka.Streams.Attributes Limit; diff --git a/src/core/Akka.Streams.Tests/Dsl/LazyFlowSpec.cs b/src/core/Akka.Streams.Tests/Dsl/LazyFlowSpec.cs new file mode 100644 index 00000000000..35724281702 --- /dev/null +++ b/src/core/Akka.Streams.Tests/Dsl/LazyFlowSpec.cs @@ -0,0 +1,227 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Linq; +using System.Threading.Tasks; +using Akka.Streams.Dsl; +using Akka.Streams.TestKit; +using Akka.Streams.TestKit.Tests; +using Akka.TestKit; +using Akka.Util.Internal; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Streams.Tests.Dsl +{ + public class LazyFlowSpec : AkkaSpec + { + public LazyFlowSpec(ITestOutputHelper helper) + : base(helper) + { + var settings = ActorMaterializerSettings.Create(Sys).WithInputBuffer(1, 1); + Materializer = Sys.Materializer(settings); + } + + private ActorMaterializer Materializer { get; } + + private static readonly Func Fallback = () => NotUsed.Instance; + + private static readonly Exception Ex = new TestException(""); + + private static readonly Task> FlowF = Task.FromResult(Flow.Create()); + + [Fact] + public void A_LazyFlow_must_work_in_happy_case() + { + this.AssertAllStagesStopped(() => + { + Func>> MapF(int e) => () => + Task.FromResult(Flow.FromFunction(i => (i * e).ToString())); + + var probe = Source.From(Enumerable.Range(2, 10)) + .Via(Flow.LazyInitAsync(MapF(2))) + .RunWith(this.SinkProbe(), Materializer); + probe.Request(100); + Enumerable.Range(2, 10).Select(i => (i * 2).ToString()).ForEach(i => probe.ExpectNext(i)); + }, Materializer); + } + + [Fact] + public void A_LazyFlow_must_work_with_slow_flow_init() + { + this.AssertAllStagesStopped(() => + { + var p = new TaskCompletionSource>(); + var sourceProbe = this.CreateManualPublisherProbe(); + var flowProbe = Source.FromPublisher(sourceProbe) + .Via(Flow.LazyInitAsync(() => p.Task)) + .RunWith(this.SinkProbe(), Materializer); + + var sourceSub = sourceProbe.ExpectSubscription(); + flowProbe.Request(1); + sourceSub.ExpectRequest(1); + sourceSub.SendNext(0); + sourceSub.ExpectRequest(1); + sourceProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(200)); + + p.SetResult(Flow.Create()); + flowProbe.Request(99); + flowProbe.ExpectNext(0); + Enumerable.Range(1, 10).ForEach(i => + { + sourceSub.SendNext(i); + flowProbe.ExpectNext(i); + }); + sourceSub.SendComplete(); + }, Materializer); + } + + [Fact] + public void A_LazyFlow_must_complete_when_there_was_no_elements_in_stream() + { + this.AssertAllStagesStopped(() => + { + var probe = Source.Empty() + .Via(Flow.LazyInitAsync(() => FlowF)) + .RunWith(this.SinkProbe(), Materializer); + probe.Request(1).ExpectComplete(); + }, Materializer); + } + + [Fact] + public void A_LazyFlow_must_complete_normally_when_upstream_completes_BEFORE_the_stage_has_switched_to_the_inner_flow() + { + this.AssertAllStagesStopped(() => + { + var promise = new TaskCompletionSource>(); + var (pub, sub) = this.SourceProbe() + .ViaMaterialized(Flow.LazyInitAsync(() => promise.Task), Keep.Left) + .ToMaterialized(this.SinkProbe(), Keep.Both) + .Run(Materializer); + + sub.Request(1); + pub.SendNext(1).SendComplete(); + promise.SetResult(Flow.Create()); + sub.ExpectNext(1).ExpectComplete(); + }, Materializer); + } + + [Fact] + public void A_LazyFlow_must_complete_normally_when_upstream_completes_AFTER_the_stage_has_switched_to_the_inner_flow() + { + this.AssertAllStagesStopped(() => + { + var (pub, sub) = this.SourceProbe() + .ViaMaterialized(Flow.LazyInitAsync(() => Task.FromResult(Flow.Create())), Keep.Left) + .ToMaterialized(this.SinkProbe(), Keep.Both) + .Run(Materializer); + + sub.Request(1); + pub.SendNext(1); + sub.ExpectNext(1); + pub.SendComplete(); + sub.ExpectComplete(); + }, Materializer); + } + + [Fact] + public void A_LazyFlow_must_fail_gracefully_when_flow_factory_method_failed() + { + this.AssertAllStagesStopped(() => + { + var sourceProbe = this.CreateManualPublisherProbe(); + var probe = Source.FromPublisher(sourceProbe) + .Via(Flow.LazyInitAsync(() => throw Ex)) + .RunWith(this.SinkProbe(), Materializer); + + var sourceSub = sourceProbe.ExpectSubscription(); + probe.Request(1); + sourceSub.ExpectRequest(1); + sourceSub.SendNext(0); + sourceSub.ExpectCancellation(); + probe.ExpectError().Should().Be(Ex); + }, Materializer); + } + + [Fact] + public void A_LazyFlow_must_fail_gracefully_when_upstream_failed() + { + this.AssertAllStagesStopped(() => + { + var sourceProbe = this.CreateManualPublisherProbe(); + var probe = Source.FromPublisher(sourceProbe) + .Via(Flow.LazyInitAsync(() => FlowF)) + .RunWith(this.SinkProbe(), Materializer); + + var sourceSub = sourceProbe.ExpectSubscription(); + sourceSub.ExpectRequest(1); + sourceSub.SendNext(0); + probe.Request(1).ExpectNext(0); + sourceSub.SendError(Ex); + probe.ExpectError().Should().Be(Ex); + }, Materializer); + } + + [Fact] + public void A_LazyFlow_must_fail_gracefully_when_factory_task_failed() + { + this.AssertAllStagesStopped(() => + { + var sourceProbe = this.CreateManualPublisherProbe(); + var flowprobe = Source.FromPublisher(sourceProbe) + .Via(Flow.LazyInitAsync(() => Task.FromException>(Ex))) + .RunWith(this.SinkProbe(), Materializer); + + var sourceSub = sourceProbe.ExpectSubscription(); + sourceSub.ExpectRequest(1); + sourceSub.SendNext(0); + var error = flowprobe.Request(1).ExpectError().As(); + error.Flatten().InnerException.Should().Be(Ex); + }, Materializer); + } + + [Fact] + public void A_LazyFlow_must_cancel_upstream_when_the_downstream_is_cancelled() + { + this.AssertAllStagesStopped(() => + { + var sourceProbe = this.CreateManualPublisherProbe(); + var probe = Source.FromPublisher(sourceProbe) + .Via(Flow.LazyInitAsync(() => FlowF)) + .RunWith(this.SinkProbe(), Materializer); + + var sourceSub = sourceProbe.ExpectSubscription(); + probe.Request(1); + sourceSub.ExpectRequest(1); + sourceSub.SendNext(0); + sourceSub.ExpectRequest(1); + probe.ExpectNext(0); + probe.Cancel(); + sourceSub.ExpectCancellation(); + }, Materializer); + } + + [Fact] + public void A_LazyFlow_must_fail_correctly_when_factory_throw_error() + { + this.AssertAllStagesStopped(() => + { + const string msg = "fail!"; + var matFail = new TestException(msg); + + var result = Source.Single("whatever") + .ViaMaterialized(Flow.LazyInitAsync(() => throw matFail), Keep.Right) + .ToMaterialized(Sink.Ignore(), Keep.Left) + .Invoking(source => source.Run(Materializer)); + + result.Should().Throw().WithMessage(msg); + }, Materializer); + } + } +} diff --git a/src/core/Akka.Streams.Tests/Dsl/LazySinkSpec.cs b/src/core/Akka.Streams.Tests/Dsl/LazySinkSpec.cs index 5f9f61e19bb..0ae93c23682 100644 --- a/src/core/Akka.Streams.Tests/Dsl/LazySinkSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/LazySinkSpec.cs @@ -14,6 +14,7 @@ using Akka.Streams.TestKit; using Akka.Streams.TestKit.Tests; using Akka.TestKit; +using Akka.Util; using Akka.Util.Internal; using FluentAssertions; using Xunit; @@ -47,10 +48,9 @@ public void A_LazySink_must_work_in_the_happy_case() { this.AssertAllStagesStopped(() => { - var lazySink = Sink.LazySink((int _) => Task.FromResult(this.SinkProbe()), - Fallback>()); + var lazySink = Sink.LazyInitAsync(() => Task.FromResult(this.SinkProbe())); var taskProbe = Source.From(Enumerable.Range(0, 11)).RunWith(lazySink, Materializer); - var probe = taskProbe.AwaitResult(RemainingOrDefault); + var probe = taskProbe.AwaitResult(RemainingOrDefault).Value; probe.Request(100); Enumerable.Range(0, 11).ForEach(i => probe.ExpectNext(i)); }, Materializer); @@ -64,7 +64,7 @@ public void A_LazySink_must_work_with_slow_sink_init() var p = new TaskCompletionSource>>(); var sourceProbe = this.CreateManualPublisherProbe(); var taskProbe = Source.FromPublisher(sourceProbe) - .RunWith(Sink.LazySink((int _) => p.Task, Fallback>()), Materializer); + .RunWith(Sink.LazyInitAsync(() => p.Task), Materializer); var sourceSub = sourceProbe.ExpectSubscription(); sourceSub.ExpectRequest(1); @@ -74,7 +74,7 @@ public void A_LazySink_must_work_with_slow_sink_init() taskProbe.Wait(TimeSpan.FromMilliseconds(200)).ShouldBeFalse(); p.SetResult(this.SinkProbe()); - var probe = taskProbe.AwaitResult(RemainingOrDefault); + var probe = taskProbe.AwaitResult(RemainingOrDefault).Value; probe.Request(100); probe.ExpectNext(0); Enumerable.Range(1,10).ForEach(i => @@ -91,11 +91,9 @@ public void A_LazySink_must_complete_when_there_was_no_elements_in_stream() { this.AssertAllStagesStopped(() => { - var lazySink = Sink.LazySink((int _) => Task.FromResult(Sink.Aggregate(0, (int i, int i2) => i + i2)), - () => Task.FromResult(0)); + var lazySink = Sink.LazyInitAsync(() => Task.FromResult(Sink.Aggregate(0, (int i, int i2) => i + i2))); var taskProbe = Source.Empty().RunWith(lazySink, Materializer); - var taskResult = taskProbe.AwaitResult(RemainingOrDefault); - taskResult.AwaitResult(RemainingOrDefault).ShouldBe(0); + taskProbe.AwaitResult(RemainingOrDefault).ShouldBe(Option>.None); }, Materializer); } @@ -104,10 +102,9 @@ public void A_LazySink_must_complete_normally_when_upstream_is_completed() { this.AssertAllStagesStopped(() => { - var lazySink = Sink.LazySink((int _) => Task.FromResult(this.SinkProbe()), - Fallback>()); + var lazySink = Sink.LazyInitAsync(() => Task.FromResult(this.SinkProbe())); var taskProbe = Source.Single(1).RunWith(lazySink, Materializer); - var taskResult = taskProbe.AwaitResult(RemainingOrDefault); + var taskResult = taskProbe.AwaitResult(RemainingOrDefault).Value; taskResult.Request(1).ExpectNext(1).ExpectComplete(); }, Materializer); } @@ -118,11 +115,7 @@ public void A_LazySink_must_fail_gracefully_when_sink_factory_method_failed() this.AssertAllStagesStopped(() => { var sourceProbe = this.CreateManualPublisherProbe(); - var taskProbe = Source.FromPublisher(sourceProbe).RunWith(Sink.LazySink((int _) => - { - throw Ex; - }, Fallback>()), Materializer); - + var taskProbe = Source.FromPublisher(sourceProbe).RunWith(Sink.LazyInitAsync(() => throw Ex), Materializer); var sourceSub = sourceProbe.ExpectSubscription(); sourceSub.ExpectRequest(1); sourceSub.SendNext(0); @@ -137,14 +130,13 @@ public void A_LazySink_must_fail_gracefully_when_upstream_failed() this.AssertAllStagesStopped(() => { var sourceProbe = this.CreateManualPublisherProbe(); - var lazySink = Sink.LazySink((int _) => Task.FromResult(this.SinkProbe()), - Fallback>()); + var lazySink = Sink.LazyInitAsync(() => Task.FromResult(this.SinkProbe())); var taskProbe = Source.FromPublisher(sourceProbe).RunWith(lazySink, Materializer); var sourceSub = sourceProbe.ExpectSubscription(); sourceSub.ExpectRequest(1); sourceSub.SendNext(0); - var probe = taskProbe.AwaitResult(RemainingOrDefault); + var probe = taskProbe.AwaitResult(RemainingOrDefault).Value; probe.Request(1).ExpectNext(0); sourceSub.SendError(Ex); probe.ExpectError().Should().Be(Ex); @@ -156,12 +148,8 @@ public void A_LazySink_must_fail_gracefully_when_factory_task_failed() { this.AssertAllStagesStopped(() => { - var failedTask = new TaskFactory>>().StartNew(() => - { - throw Ex; - }); var sourceProbe = this.CreateManualPublisherProbe(); - var lazySink = Sink.LazySink((int _) => failedTask, Fallback>()); + var lazySink = Sink.LazyInitAsync(() => Task.FromException>>(Ex)); var taskProbe = Source.FromPublisher(sourceProbe) .ToMaterialized(lazySink, Keep.Right) @@ -181,65 +169,19 @@ public void A_LazySink_must_cancel_upstream_when_internal_sink_is_cancelled() this.AssertAllStagesStopped(() => { var sourceProbe = this.CreateManualPublisherProbe(); - var lazySink = Sink.LazySink((int _) => Task.FromResult(this.SinkProbe()), - Fallback>()); + var lazySink = Sink.LazyInitAsync(() => Task.FromResult(this.SinkProbe())); var taskProbe = Source.FromPublisher(sourceProbe).RunWith(lazySink, Materializer); var sourceSub = sourceProbe.ExpectSubscription(); sourceSub.ExpectRequest(1); sourceSub.SendNext(0); sourceSub.ExpectRequest(1); - var probe = taskProbe.AwaitResult(RemainingOrDefault); + var probe = taskProbe.AwaitResult(RemainingOrDefault).Value; probe.Request(1).ExpectNext(0); probe.Cancel(); sourceSub.ExpectCancellation(); }, Materializer); } - [Fact] - public void A_LazySink_must_continue_if_supervision_is_resume() - { - this.AssertAllStagesStopped(() => - { - var sourceProbe = this.CreateManualPublisherProbe(); - var lazySink = Sink.LazySink((int a) => - { - if (a == 0) - throw Ex; - return Task.FromResult(this.SinkProbe()); - }, - Fallback>()); - var taskProbe = - Source.FromPublisher(sourceProbe) - .ToMaterialized(lazySink, Keep.Right) - .WithAttributes(ActorAttributes.CreateSupervisionStrategy(Deciders.ResumingDecider)) - .Run(Materializer); - var sourceSub = sourceProbe.ExpectSubscription(); - sourceSub.ExpectRequest(1); - sourceSub.SendNext(0); - sourceSub.ExpectRequest(1); - sourceSub.SendNext(1); - var probe = taskProbe.AwaitResult(RemainingOrDefault); - probe.Request(1); - probe.ExpectNext(1); - probe.Cancel(); - }, Materializer); - } - - [Fact] - public void A_LazySink_must_fail_task_when_zero_throws_exception() - { - this.AssertAllStagesStopped(() => - { - var lazySink = Sink.LazySink((int _) => Task.FromResult(Sink.Aggregate(0, (i, i1) => i + i1)), - () => - { - throw Ex; - }); - var taskProbe = Source.Empty().RunWith(lazySink, Materializer); - taskProbe.Invoking(t => t.Wait(TimeSpan.FromMilliseconds(300))).Should().Throw(); - }, Materializer); - } - [Fact] public void A_LazySink_must_fail_correctly_when_materialization_of_inner_sink_fails() { @@ -248,9 +190,7 @@ public void A_LazySink_must_fail_correctly_when_materialization_of_inner_sink_fa var matFail = new TestException("fail!"); var task = Source.Single("whatever") - .RunWith(Sink.LazySink( - str => Task.FromResult(Sink.FromGraph(new FailingInnerMat(matFail))), - () => NotUsed.Instance), Materializer); + .RunWith(Sink.LazyInitAsync(() => Task.FromResult(Sink.FromGraph(new FailingInnerMat(matFail)))), Materializer); try { @@ -260,7 +200,7 @@ public void A_LazySink_must_fail_correctly_when_materialization_of_inner_sink_fa task.IsFaulted.ShouldBe(true); task.Exception.ShouldNotBe(null); - task.Exception.InnerException.Should().BeEquivalentTo(matFail); + task.Exception.Flatten().InnerException.Should().BeEquivalentTo(matFail); }, Materializer); } diff --git a/src/core/Akka.Streams.Tests/IO/FileSinkSpec.cs b/src/core/Akka.Streams.Tests/IO/FileSinkSpec.cs index beff3cbc8af..c91346f1d19 100644 --- a/src/core/Akka.Streams.Tests/IO/FileSinkSpec.cs +++ b/src/core/Akka.Streams.Tests/IO/FileSinkSpec.cs @@ -12,7 +12,6 @@ using System.Threading; using System.Threading.Tasks; using Akka.Actor; -using Akka.Dispatch; using Akka.IO; using Akka.Streams.Dsl; using Akka.Streams.Implementation; @@ -20,7 +19,6 @@ using Akka.Streams.IO; using Akka.Streams.TestKit.Tests; using Akka.TestKit; -using Akka.Tests.Shared.Internals; using Akka.Util.Internal; using FluentAssertions; using Xunit; @@ -312,12 +310,12 @@ public void SynchronousFileSink_should_write_single_line_to_a_file_from_lazy_sin { Within(_expectTimeout, () => { + // LazySink must wait for result of initialization even if got UpstreamComplete TargetFile(f => { - var lazySink = Sink.LazySink( - (ByteString _) => Task.FromResult(FileIO.ToFile(f)), - () => Task.FromResult(IOResult.Success(0))) - .MapMaterializedValue(t => t.AwaitResult()); + var lazySink = Sink.LazyInitAsync(() => Task.FromResult(FileIO.ToFile(f))) + // map a Task>> into a Task + .MapMaterializedValue(t => t.Result.GetOrElse(Task.FromResult(IOResult.Success(0)))); var completion = Source.From(new []{_testByteStrings.Head()}) .RunWith(lazySink, _materializer); diff --git a/src/core/Akka.Streams/Dsl/Flow.cs b/src/core/Akka.Streams/Dsl/Flow.cs index 582b0467406..b4f560a1312 100644 --- a/src/core/Akka.Streams/Dsl/Flow.cs +++ b/src/core/Akka.Streams/Dsl/Flow.cs @@ -9,11 +9,13 @@ using System.Collections.Immutable; using System.Linq; using System.Runtime.ExceptionServices; +using System.Threading.Tasks; using Akka.Actor; using Akka.Streams.Dsl.Internal; using Akka.Streams.Implementation; using Akka.Streams.Implementation.Fusing; using Akka.Streams.Implementation.Stages; +using Akka.Util; using Reactive.Streams; namespace Akka.Streams.Dsl @@ -507,6 +509,25 @@ public static Flow FromSinkAndSource(IGraph /// TBD public static Flow FromSinkAndSource(IGraph, TMat1> sink, IGraph, TMat2> source, Func combine) => FromGraph(GraphDsl.Create(sink, source, combine, (builder, @in, @out) => new FlowShape(@in.Inlet, @out.Outlet))); + + /// + /// Creates a real upon receiving the first element. Internal will not be created + /// if there are no elements, because of completion, cancellation, or error. + /// + /// The materialized value of the is a that is completed with `TMat` when the internal + /// flow gets materialized or with `default` when there where no elements. If the flow materialization (including + /// the call of the `flowFactory`) fails then the future is completed with a failure. + /// + /// Emits when the internal flow is successfully created and it emits + /// Cancels when downstream cancels + /// + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + public static Flow>> LazyInitAsync(Func>> flowFactory) => + FromGraph(new LazyFlow(_ => flowFactory())); } /// diff --git a/src/core/Akka.Streams/Dsl/Sink.cs b/src/core/Akka.Streams/Dsl/Sink.cs index 82faad4d36c..5c61254f457 100644 --- a/src/core/Akka.Streams/Dsl/Sink.cs +++ b/src/core/Akka.Streams/Dsl/Sink.cs @@ -15,6 +15,7 @@ using Akka.Streams.Implementation; using Akka.Streams.Implementation.Fusing; using Akka.Streams.Implementation.Stages; +using Akka.Util; using Reactive.Streams; // ReSharper disable UnusedMember.Global @@ -534,12 +535,14 @@ public static Sink ActorSubscriber(Props props) /// because of completion or error. /// /// - /// If throws an exception and the supervision decision is - /// the will be completed with failure. For all other supervision options it will try to create sink with next element. + /// If upstream completes before an element was received then the is completed with the value created by . /// - /// will be executed when there was no elements and completed is received from upstream. /// - /// Adheres to the attribute. + /// If upstream fails before an element was received, throws an exception, or materialization of the internal + /// sink fails then the is completed with the exception. + /// + /// + /// Otherwise the is completed with the materialized value of the internal sink. /// /// /// TBD @@ -547,8 +550,31 @@ public static Sink ActorSubscriber(Props props) /// TBD /// TBD /// TBD - public static Sink> LazySink(Func>> sinkFactory, - Func fallback) => FromGraph(new LazySink(sinkFactory, fallback)); + [Obsolete("Use LazyInitAsync instead. LazyInitAsync no more needs a fallback function and the materialized value more clearly indicates if the internal sink was materialized or not.")] + public static Sink> LazySink(Func>> sinkFactory, Func fallback) => + FromGraph(new LazySink(sinkFactory)).MapMaterializedValue(t => + t.ContinueWith(t1 => t1.Result.GetOrElse(fallback()), TaskContinuationOptions.ExecuteSynchronously)); + + /// + /// Creates a real upon receiving the first element. Internal + /// will not be created if there are no elements, because of completion or error. + /// + /// If upstream completes before an element was received then the is completed with `default`. + /// + /// + /// If upstream fails before an element was received, throws an exception, or materialization of the internal + /// sink fails then the is completed with the exception. + /// + /// + /// Otherwise the is completed with the materialized value of the internal sink. + /// + /// + /// TBD + /// TBD + /// TBD + /// + public static Sink>> LazyInitAsync(Func>> sinkFactory) => + FromGraph(new LazySink(_ => sinkFactory())); /// /// A graph with the shape of a sink logically is a sink, this method makes diff --git a/src/core/Akka.Streams/Implementation/Fusing/Ops.cs b/src/core/Akka.Streams/Implementation/Fusing/Ops.cs index 833c704bde2..70933b45109 100644 --- a/src/core/Akka.Streams/Implementation/Fusing/Ops.cs +++ b/src/core/Akka.Streams/Implementation/Fusing/Ops.cs @@ -10,7 +10,6 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; -using Akka.Actor; using Akka.Annotations; using Akka.Event; using Akka.Pattern; @@ -526,7 +525,7 @@ protected override void OnResume(Exception ex) private class NotApplied { public static readonly NotApplied Instance = new NotApplied(); - private NotApplied() {} + private NotApplied() { } } } @@ -659,7 +658,7 @@ public Recover(Func> recovery) : base("Recover") /// TBD /// protected override Attributes InitialAttributes { get; } = DefaultAttributes.Recover; - + /// /// TBD /// @@ -1952,7 +1951,7 @@ private sealed class Logic : InAndOutGraphStageLogic private readonly Buffer _stage; private readonly Action _enqueue; private IBuffer _buffer; - + public Logic(Buffer stage) : base(stage.Shape) { @@ -2130,7 +2129,7 @@ public override void OnUpstreamFailure(Exception e) public override void PostStop() { - if(!_completionSignalled) + if (!_completionSignalled) _stage._failure(new AbruptStageTerminationException(this)); } } @@ -2148,7 +2147,7 @@ public OnCompleted(Action success, Action failure) Shape = new FlowShape(In, Out); } - public Inlet In { get; } = new Inlet("OnCompleted.in"); + public Inlet In { get; } = new Inlet("OnCompleted.in"); public Outlet Out { get; } = new Outlet("OnCompleted.out"); @@ -3493,7 +3492,7 @@ private sealed class Logic : InAndOutGraphStageLogic public Logic(Sum stage, Attributes inheritedAttributes) : base(stage.Shape) { _stage = stage; - + var attr = inheritedAttributes.GetAttribute(null); _decider = attr != null ? attr.Decider : Deciders.StoppingDecider; @@ -3833,4 +3832,256 @@ public StatefulSelectMany(Func>> concatFactory) /// public override string ToString() => "StatefulSelectMany"; } + + [InternalApi] + public sealed class LazyFlow : GraphStageWithMaterializedValue, Task>> + { + #region Internal Logic + + private sealed class Logic : InAndOutGraphStageLogic + { + private readonly LazyFlow _stage; + private readonly TaskCompletionSource> _promise; + private bool _switching; + + public Logic(LazyFlow stage, TaskCompletionSource> promise) + : base(stage.Shape) + { + _stage = stage; + _promise = promise; + + SetHandler(stage.In, this); + SetHandler(stage.Out, this); + } + + public override void OnPush() + { + var element = Grab(_stage.In); + _switching = true; + + var cb = GetAsyncCallback>>(result => + { + if (result.IsSuccess && result.Value != null) + { + // check if the stage is still in need for the lazy flow + // (there could have been an OnUpstreamFailure or OnDownstreamFinish in the meantime that has completed the promise) + if (!_promise.Task.IsCompleted) + { + try + { + var mat = SwitchTo(result.Value, element); + _promise.TrySetResult(mat); + } + catch (Exception ex) + { + _promise.TrySetException(ex); + FailStage(ex); + } + } + } + else + { + _promise.TrySetException(result.Exception); + FailStage(result.Exception); + } + }); + + try + { + _stage._flowFactory(element) + .ContinueWith(t => cb(Result.FromTask(t)), TaskContinuationOptions.ExecuteSynchronously); + } + catch (Exception ex) + { + _promise.TrySetException(ex); + FailStage(ex); + } + } + + public override void OnPull() => Pull(_stage.In); + + public override void OnUpstreamFinish() + { + // ignore OnUpstreamFinish while the stage is switching but SetKeepGoing + if (_switching) + SetKeepGoing(true); + else + { + _promise.TrySetResult(Option.None); + base.OnUpstreamFinish(); + } + } + + public override void OnUpstreamFailure(Exception ex) + { + _promise.TrySetException(ex); + base.OnUpstreamFailure(ex); + } + + public override void OnDownstreamFinish() + { + _promise.TrySetResult(Option.None); + base.OnDownstreamFinish(); + } + + private TMat SwitchTo(Flow flow, TIn firstElement) + { + var firstElementPushed = false; + + // + // ports are wired in the following way: + // + // in ~> subOutlet ~> lazyFlow ~> subInlet ~> out + // + + var subInlet = new SubSinkInlet(this, "LazyFlowSubSink"); + var subOutlet = new SubSourceOutlet(this, "LazyFlowSubSource"); + + // The lazily materialized flow may be constructed from a sink and a source. Therefore termination + // signals (completion, cancellation, and errors) are not guaranteed to pass through the flow. This + // means that this stage must not be completed as soon as one side of the flow is finished. + // + // Invariant: IsClosed(Out) == subInlet.IsClosed after each event because termination signals (i.e. + // completion, cancellation, and failure) between these two ports are always forwarded. + // + // However, IsClosed(In) and subOutlet.IsClosed may be different. This happens if upstream completes before + // the cached element was pushed. + void MaybeCompleteStage() + { + if (IsClosed(_stage.In) && subOutlet.IsClosed && IsClosed(_stage.Out)) + CompleteStage(); + } + + var matVal = Source.FromGraph(subOutlet.Source) + .ViaMaterialized(flow, Keep.Right) + .ToMaterialized(subInlet.Sink, Keep.Left) + .Run(Interpreter.SubFusingMaterializer); + + // The stage must not be shut down automatically; it is completed when MaybeCompleteStage decides + SetKeepGoing(true); + + SetHandler(_stage.In, new LambdaInHandler( + () => subOutlet.Push(Grab(_stage.In)), + () => + { + if (firstElementPushed) + { + subOutlet.Complete(); + MaybeCompleteStage(); + } + }, + ex => + { + // propagate exception irrespective if the cached element has been pushed or not + subOutlet.Fail(ex); + MaybeCompleteStage(); + })); + + SetHandler(_stage.Out, new LambdaOutHandler( + () => subInlet.Pull(), + () => + { + subInlet.Cancel(); + MaybeCompleteStage(); + })); + + subOutlet.SetHandler(new LambdaOutHandler( + () => + { + if (firstElementPushed) + Pull(_stage.In); + else + { + // the demand can be satisfied right away by the cached element + firstElementPushed = true; + subOutlet.Push(firstElement); + // In.OnUpstreamFinished was not propagated if it arrived before the cached element was pushed + // -> check if the completion must be propagated now + if (IsClosed(_stage.In)) + { + subOutlet.Complete(); + MaybeCompleteStage(); + } + } + }, + () => + { + if (!IsClosed(_stage.In)) Cancel(_stage.In); + MaybeCompleteStage(); + })); + + subInlet.SetHandler(new LambdaInHandler( + () => Push(_stage.Out, subInlet.Grab()), + () => + { + Complete(_stage.Out); + MaybeCompleteStage(); + }, + ex => + { + Fail(_stage.Out, ex); + MaybeCompleteStage(); + })); + + if (IsClosed(_stage.Out)) + { + // downstream may have been canceled while the stage was switching + subInlet.Cancel(); + } + else + { + subInlet.Pull(); + } + + return matVal; + } + } + + #endregion + + private readonly Func>> _flowFactory; + + public LazyFlow(Func>> flowFactory) + { + _flowFactory = flowFactory; + Shape = new FlowShape(In, Out); + } + + /// + /// TBD + /// + public Inlet In { get; } = new Inlet("lazySink.In"); + + /// + /// TBD + /// + public Outlet Out { get; } = new Outlet("lazySink.Out"); + + /// + /// TBD + /// + protected override Attributes InitialAttributes { get; } = DefaultAttributes.LazyFlow; + + /// + /// TBD + /// + public override FlowShape Shape { get; } + + /// + /// TBD + /// + /// TBD + /// TBD + public override ILogicAndMaterializedValue>> CreateLogicAndMaterializedValue(Attributes inheritedAttributes) + { + var promise = new TaskCompletionSource>(); + var stageLogic = new Logic(this, promise); + return new LogicAndMaterializedValue>>(stageLogic, promise.Task); + } + + /// + /// Returns a that represents this instance. + /// + public override string ToString() => "LazyFlow"; + } } diff --git a/src/core/Akka.Streams/Implementation/Sinks.cs b/src/core/Akka.Streams/Implementation/Sinks.cs index eb6bd06ad51..5dd8de34a0a 100644 --- a/src/core/Akka.Streams/Implementation/Sinks.cs +++ b/src/core/Akka.Streams/Implementation/Sinks.cs @@ -931,179 +931,167 @@ public override ILogicAndMaterializedValue> CreateLogicAndMaterial /// /// TBD /// TBD - internal sealed class LazySink : GraphStageWithMaterializedValue, Task> + internal sealed class LazySink : GraphStageWithMaterializedValue, Task>> { #region Logic private sealed class Logic : InGraphStageLogic { private readonly LazySink _stage; - private readonly TaskCompletionSource _completion; - private readonly Lazy _decider; + private readonly TaskCompletionSource> _completion; + private bool _switching; - private bool _completed; - - public Logic(LazySink stage, Attributes inheritedAttributes, - TaskCompletionSource completion) : base(stage.Shape) + public Logic(LazySink stage, Attributes inheritedAttributes, TaskCompletionSource> completion) + : base(stage.Shape) { _stage = stage; _completion = completion; - _decider = new Lazy(() => - { - var attr = inheritedAttributes.GetAttribute(null); - return attr != null ? attr.Decider : Deciders.StoppingDecider; - }); SetHandler(stage.In, this); } + public override void PreStart() => Pull(_stage.In); + public override void OnPush() { - try + var element = Grab(_stage.In); + _switching = true; + + var callback = GetAsyncCallback>>(result => { - var element = Grab(_stage.In); - var callback = GetAsyncCallback>>(result => + if (result.IsSuccess) { - if (result.IsSuccess) - InitInternalSource(result.Value, element); - else - Failure(result.Exception); - }); + // check if the stage is still in need for the lazy sink + // (there could have been an OnUpstreamFailure in the meantime that has completed the promise) + if (!_completion.Task.IsCompleted) + { + try + { + var mat = SwitchTo(result.Value, element); + _completion.TrySetResult(mat); + SetKeepGoing(true); + } + catch (Exception ex) + { + _completion.TrySetException(ex); + FailStage(ex); + } + } + } + else + { + _completion.TrySetException(result.Exception); + FailStage(result.Exception); + } + }); + + try + { _stage._sinkFactory(element) - .ContinueWith(t => callback(Result.FromTask(t)), - TaskContinuationOptions.ExecuteSynchronously); - SetHandler(_stage.In, new LambdaInHandler( - onPush: () => { }, - onUpstreamFinish: GotCompletionEvent, - onUpstreamFailure: Failure - )); + .ContinueWith(t => callback(Result.FromTask(t)), TaskContinuationOptions.ExecuteSynchronously); } catch (Exception ex) { - if (_decider.Value(ex) == Directive.Stop) - Failure(ex); - else - Pull(_stage.In); + _completion.TrySetException(ex); + FailStage(ex); } } public override void OnUpstreamFinish() { - CompleteStage(); - try + // ignore OnUpstreamFinish while the stage is switching but SetKeepGoing + if (_switching) { - _completion.TrySetResult(_stage._zeroMaterialised()); - } - catch (Exception ex) + // there is a cached element -> the stage must not be shut down automatically because IsClosed(In) is satisfied + SetKeepGoing(true); + } + else { - _completion.SetException(ex); + _completion.TrySetResult(Option.None); + base.OnUpstreamFinish(); } } - public override void OnUpstreamFailure(Exception e) => Failure(e); - - private void GotCompletionEvent() + public override void OnUpstreamFailure(Exception ex) { - SetKeepGoing(true); - _completed = true; + _completion.TrySetException(ex); + base.OnUpstreamFailure(ex); } - public override void PreStart() => Pull(_stage.In); - - private void Failure(Exception ex) + private TMat SwitchTo(Sink sink, TIn firstElement) { - FailStage(ex); - _completion.SetException(ex); - } + var firstElementPushed = false; - private void InitInternalSource(Sink sink, TIn firstElement) - { - var sourceOut = new SubSource(this, firstElement); + var subOutlet = new SubSourceOutlet(this, "LazySink"); - try - { - var matVal = Source.FromGraph(sourceOut.Source) - .RunWith(sink, Interpreter.SubFusingMaterializer); - _completion.TrySetResult(matVal); - } - catch (Exception ex) + var matVal = Source.FromGraph(subOutlet.Source).RunWith(sink, Interpreter.SubFusingMaterializer); + + void MaybeCompleteStage() { - _completion.TrySetException(ex); - FailStage(ex); + if (IsClosed(_stage.In) && subOutlet.IsClosed) + CompleteStage(); } - } - - #region SubSource - - private sealed class SubSource : SubSourceOutlet - { - private readonly Logic _logic; - private readonly LazySink _stage; + // The stage must not be shut down automatically; it is completed when MaybeCompleteStage decides + SetKeepGoing(true); - public SubSource(Logic logic, TIn firstElement) : base(logic, "LazySink") - { - _logic = logic; - _stage = logic._stage; + SetHandler(_stage.In, new LambdaInHandler( + () => subOutlet.Push(Grab(_stage.In)), + () => + { + if (firstElementPushed) + { + subOutlet.Complete(); + MaybeCompleteStage(); + } + }, + ex => + { + // propagate exception irrespective if the cached element has been pushed or not + subOutlet.Fail(ex); + MaybeCompleteStage(); + })); - SetHandler(new LambdaOutHandler(onPull: () => + subOutlet.SetHandler(new LambdaOutHandler( + () => { - Push(firstElement); - if (_logic._completed) - SourceComplete(); + if (firstElementPushed) + Pull(_stage.In); else - SwitchToFinalHandler(); - }, onDownstreamFinish: SourceComplete)); - - logic.SetHandler(_stage.In, new LambdaInHandler( - onPush: () => Push(logic.Grab(_stage.In)), - onUpstreamFinish: logic.GotCompletionEvent, - onUpstreamFailure: SourceFailure)); - } - - private void SourceFailure(Exception ex) - { - Fail(ex); - _logic.FailStage(ex); - } - - private void SwitchToFinalHandler() - { - SetHandler(new LambdaOutHandler( - onPull: () => _logic.Pull(_stage.In), - onDownstreamFinish: SourceComplete)); - - _logic.SetHandler(_stage.In, new LambdaInHandler( - onPush: () => Push(_logic.Grab(_stage.In)), - onUpstreamFinish: SourceComplete, - onUpstreamFailure: SourceFailure)); - } + { + // the demand can be satisfied right away by the cached element + firstElementPushed = true; + subOutlet.Push(firstElement); + // In.OnUpstreamFinished was not propagated if it arrived before the cached element was pushed + // -> check if the completion must be propagated now + if (IsClosed(_stage.In)) + { + subOutlet.Complete(); + MaybeCompleteStage(); + } + } + }, + () => + { + if (!IsClosed(_stage.In)) Cancel(_stage.In); + MaybeCompleteStage(); + })); - private void SourceComplete() - { - Complete(); - _logic.CompleteStage(); - } + return matVal; } - - #endregion } #endregion private readonly Func>> _sinkFactory; - private readonly Func _zeroMaterialised; /// /// TBD /// /// TBD - /// TBD - public LazySink(Func>> sinkFactory, Func zeroMaterialised) + public LazySink(Func>> sinkFactory) { _sinkFactory = sinkFactory; - _zeroMaterialised = zeroMaterialised; - Shape = new SinkShape(In); } @@ -1127,11 +1115,11 @@ public LazySink(Func>> sinkFactory, Func zeroMat /// /// TBD /// TBD - public override ILogicAndMaterializedValue> CreateLogicAndMaterializedValue(Attributes inheritedAttributes) + public override ILogicAndMaterializedValue>> CreateLogicAndMaterializedValue(Attributes inheritedAttributes) { - var completion = new TaskCompletionSource(); + var completion = new TaskCompletionSource>(); var stageLogic = new Logic(this, inheritedAttributes, completion); - return new LogicAndMaterializedValue>(stageLogic, completion.Task); + return new LogicAndMaterializedValue>>(stageLogic, completion.Task); } /// diff --git a/src/core/Akka.Streams/Implementation/Stages/Stages.cs b/src/core/Akka.Streams/Implementation/Stages/Stages.cs index a03b1c75d96..dd8b31915e4 100644 --- a/src/core/Akka.Streams/Implementation/Stages/Stages.cs +++ b/src/core/Akka.Streams/Implementation/Stages/Stages.cs @@ -423,6 +423,10 @@ public static class DefaultAttributes /// /// TBD /// + public static readonly Attributes LazyFlow = Attributes.CreateName("lazyFlow"); + /// + /// TBD + /// public static readonly Attributes LazySource = Attributes.CreateName("lazySource"); /// /// TBD From f2994d727add58011eceb0222fea8973161d67a7 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Tue, 4 Jan 2022 23:45:29 +0700 Subject: [PATCH 14/30] Add scala porting guide to the documentation (#5479) * Add scala porting guide to the documentation * Fix for linter errors * Fix linter errors * Fix linter errors --- .../community/contributing/code-guidelines.md | 2 + docs/community/contributing/porting-guide.md | 591 ++++++++++++++++++ docs/community/contributing/toc.yml | 2 + 3 files changed, 595 insertions(+) create mode 100644 docs/community/contributing/porting-guide.md diff --git a/docs/community/contributing/code-guidelines.md b/docs/community/contributing/code-guidelines.md index de83af2b349..e043bb49990 100644 --- a/docs/community/contributing/code-guidelines.md +++ b/docs/community/contributing/code-guidelines.md @@ -22,6 +22,8 @@ changes to Akka.NET: Akka.NET developers before making a change, you can [create an issue](https://github.com/akkadotnet/akka.net/issues/new) with the `discussion` tag or reach out to [AkkaDotNet on Twitter](https://twitter.com/AkkaDotNet). +For a more complete guide on how to convert Scala code to C#, please read the [Scala To C# Conversion Guide](xref:porting-guide) + ## Coding Conventions * Use the default Resharper guidelines for code diff --git a/docs/community/contributing/porting-guide.md b/docs/community/contributing/porting-guide.md new file mode 100644 index 00000000000..00cfd1b4145 --- /dev/null +++ b/docs/community/contributing/porting-guide.md @@ -0,0 +1,591 @@ +--- +uid: porting-guide +title: Scala To C# Conversion Guide +--- +# Scala To C# Conversion Guide + +* Be .NET idiomatic, e.g. do not port `Duration` instead of `TimeSpan` and `Future` instead of `Task` +* Stay as close as possible to the original JVM implementation, +* Do not add features that don't exist in JVM Akka into the core Akka.NET + +## Case Classes + +From + +```scala +final case class HandingOverData(singleton: ActorRef, name: String) +``` + +All parameters of the case class should become a public property + +Simple implementation + +```c# +public sealed class HandingOverData +{ + public HandingOverData(IActorRef singleton, string name) + { + Singleton = singleton; + Name = name; + } + + public IActorRef Singleton { get; } + + public string Name { get; } +} +``` + +Complex implementation + +```c# +public sealed class HandingOverData +{ + public HandingOverData(IActorRef singleton, string name) + { + Singleton = singleton; + Name = name; + } + + public IActorRef Singleton { get; } + + public string Name { get; } + + private bool Equals(HandingOverData other) + { + return Equals(Singleton, other.Singleton) && string.Equals(Name, other.Name); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is HandingOverData && Equals((HandingOverData)obj); + } + + public override int GetHashCode() + { + unchecked + { + return ((Singleton?.GetHashCode() ?? 0) * 397) ^ (Name?.GetHashCode() ?? 0); + } + } + + public override string ToString() => $"{nameof(HandingOverData)}<{nameof(Singleton)}: {Singleton}, {nameof(Name)}: {Name}>"; +} +``` + +In order to support C# 7 deconstruction you could add `Deconstruct` method + +```c# +public void Deconstruct(out IActorRef singleton, out string name) +{ + singleton = Singleton; + name = Name; +} +``` + +Some messages should implement `With` (from C# 8 records spec) or `Copy` method. + +```c# +public HandingOverData With(IActorRef singleton = null, string name = null) + => new HandingOverData(singleton ?? Singleton, name ?? Name); +``` + +In some cases it would be a good idea (mandatory for value types) to implement `IEquatable` + +```c# +public sealed class HandingOverData : IEquatable +{ + ... + public bool Equals(HandingOverData other) + { + return Equals(Singleton, other.Singleton) && string.Equals(Name, other.Name); + } + ... +} +``` + +## Case Object + +From + +```scala +case object RecoveryCompleted +``` + +Simple implementation + +```c# +public sealed class RecoveryCompleted +{ + public static RecoveryCompleted Instance { get; } = new RecoveryCompleted(); + private RecoveryCompleted() {} +} +``` + +Complex implementation + +```c# +public sealed class RecoveryCompleted +{ + public static RecoveryCompleted Instance { get; } = new RecoveryCompleted(); + + private RecoveryCompleted() {} + public override bool Equals(object obj) => !ReferenceEquals(obj, null) && obj is RecoveryCompleted; + public override int GetHashCode() => nameof(RecoveryCompleted).GetHashCode(); + public override string ToString() => nameof(RecoveryCompleted); +} +``` + +In some cases it would be a good idea (mandatory for value types) to implement `IEquatable` + +```c# +public sealed class RecoveryCompleted : IEquatable +{ + ... + public bool Equals(RecoveryCompleted other) => true; + ... +} +``` + +## Class Constructors + +From + +```scala +class Person(name: String, var surname: string, val age: Int, private val position: String) +{ +} +``` + +To + +```C# +public class Person +{ + private readonly string _name; + private readonly string _position; + + public Person(string name, string surname, int age, string position) + { + _name = name; + Surname = surname; + Age = age; + _position = position; + } + + public string Surname { get; set; } + public int Age { get; } +} +``` + +## LINQ (Collection's Methods) + +| scala | C# | +|--- |--- | +| collectFirst(func) | FirstOrDefault(func) | +| drop(count) | Skip(count) | +| dropRight(count) | Reverse().Skip(count).Reverse() | +| exists(func) | Any(func) | +| filter(func) | Where(func) | +| filterNot(func) | `` | +| flatMap(func) | SelectMany(func) | +| flatten | SelectMany(i => i) | +| fold(initval)(func) | `` | +| foldLeft(initval)(func) | Aggregate(initval, func) | +| foldRight(initval)(func) | Reverse().Aggregate(initval, func) | +| forall(func) | All(func) | +| foreach(func) | `` | +| head | First() | +| headOption | FirstOrDefault() | +| last | Last() | +| lastOption | LastOrDefault() | +| map(func) | Select(func) | +| mkString(separator) | string.Join(separator, array) | +| reduce(func) | `` | +| reduceLeft(func) | Aggregate(func) | +| reduceRight(func) | Reverse().Aggregate(func) | +| sorted(implicit ordering) | OrderBy(i -> i, comparer) | +| tail | Skip(1) | +| take(count) | Take(count) | +| takeRight(count) | Reverse().Take(count).Reverse() | +| zip(seq2, func) | Zip(seq2, func) | +| zipWithIndex | `` | + +## Currying + +From + +```scala +def bufferOr(grouping: String, message: Any, originalSender: ActorRef)(action: ⇒ Unit): Unit = { + buffers.get(grouping) match { + case None ⇒ action + case Some(messages) ⇒ + buffers = buffers.updated(grouping, messages :+ ((message, originalSender))) + totalBufferSize += 1 + } +} +``` + +To + +```C# +public void BufferOr(string grouping, object message, IActorRef originalSender, Action action) +{ + BufferedMessages messages = null; + if (_buffers.TryGetValue(grouping, out messages)) + { + _buffers[grouping].Add(new KeyValuePair(message, originalSender)); + _totalBufferSize += 1; + } + else { + action(); + } +} +``` + +## Exceptions + +| scala | C# | +|--- |--- | +| IllegalArgumentException | ArgumentException | +| IllegalStateException | InvalidOperationException | +| ArithmeticException | ArithmeticException | +| NullPointerException | NullReferenceException | +| NotSerializableException | SerializationException | + +## Pattern Matching + +### Constant Patterns + +```scala +def testPattern(x: Any): String = x match { + case 0 => "zero" + case true => "true" + case "hello" => "you said 'hello'" + case Nil => "an empty List" +} +``` + +C# 7 supports all constant patterns + +```C# +public string TestPattern(object x) +{ + switch(x) + { + case 0: return "zero"; + case true: return "true"; + case "hello": return "you said Hello"; + case null: return "an empty list"; + } + return string.Empty; +} +``` + +C# 8 supports it even closer + +```c# +public string TestPattern(object x) +{ + return x switch + { + 0 => "zero", + true => "true", + "hello" => "you said Hello", + null => "an empty list", + _ => string.Empty + }; +} +``` + +### Sequence Patterns + +```scala +def testPattern(x: Any): String = x match { + case List(0, _, _) => "a three-element list with 0 as the first element" + case List(1, _*) => "a list beginning with 1, having any number of elements" + case Vector(1, _*) => "a vector starting with 1, having any number of elements" +} +``` + +C# does not support sequence patterns + +### Tuples Patterns + +```scala +def testPattern(x: Any): String = x match { + case (a, b) => s"got $a and $b" + case (a, b, c) => s"got $a, $b, and $c" +} +``` + +C# 8 supports tuple patterns by using type patterns + +```c# +public static string TestPattern(object x) => x switch +{ + ValueTuple(var a, var b) => $"got {a} and {b}", + ValueTuple(var a, var b, var c) => $"got {a}, {b}, and {c}", + _ => string.Empty +}; +``` + +C# 9 got it even closer + +```c# +public static string TestPattern(object x) => x switch +{ + (object a, object b) => $"got {a} and {b}", + (object a, object b, object c) => $"got {a}, {b}, and {c}", + _ => string.Empty +}; +``` + +### Constructor Patterns (Case Classes) + +```scala +def testPattern(x: Any): String = x match { + case Person(first, "Alexander") => s"found an Alexander, first name = $first" + case Dog("Suka") => "found a dog named Suka" +} +``` + +C# 7 does not support constructor patterns, but you could use an equivalent using typed pattern + +```C# +public string TestPattern(object x) +{ + switch(x) + { + case Person p when p.LastName == "Alexander": return $"found an Alexander, first name = {p.FirstName}"; + case Dog d when d.Name == "Suka": return "found a dog named Suka"; + } + return string.Empty; +} +``` + +C# 8 supports the property pattern + +```c# +public static string TestPattern(object x) => x switch +{ + Person { FirstName: "Alexander" } p => $"found an Alexander, first name = {p.FirstName}", + Dog { Name: "Suka" } d => "found a dog named Suka", + _ => string.Empty +}; +``` + +### Typed Patterns + +```scala +def testPattern(x: Any): String = x match { + case s: String => s"you gave me this string: $s" + case i: Int => s"thanks for the int: $i" + case f: Float => s"thanks for the float: $f" + case a: Array[Int] => s"an array of int: ${a.mkString(",")}" + case as: Array[String] => s"an array of strings: ${as.mkString(",")}" + case d: Dog => s"dog: ${d.name}" + case list: List[_] => s"thanks for the List: $list" + case m: Map[_, _] => m.toString +} +``` + +You can use typed patterns in C# 7 + +```C# +public string TestPattern(object x) +{ + switch(x) + { + case string s: return $"you gave me this string: {s}"; + case int i: return $"thanks for the int: {i}"; + case float f: return $"thanks for the float: {f}"; + case int[] a: return $"an array of int: {string.Join(",", a)}"; + case Dog d: return $"dog: ${d.Name}"; + case List list: return $"thanks for the List: {list}"; + case Dictionary dict: return $"dictionary: {dict}"; + } + return string.Empty; +} +``` + +### Patterns on Option + +From + +```scala +def matchingRole(member: Member, role: String): Boolean = role match { + case None ⇒ true + case Some(r) ⇒ member.hasRole(r) +} +``` + +To + +```c# +public bool MatchingRole(Member member, string role) +{ + switch (member) + { + case Member m: + return m.HasRole(role); + default: + return true; + } +} +``` + +Or in C# 8 + +```c# +public bool MatchingRole(Member member, string role) + => member switch + { + Member m => m.HasRole(role), + _ => true + } +``` + +### Extractors + +From + +```scala +trait User { + def name: String +} +class FreeUser(val name: String) extends User +class PremiumUser(val name: String) extends User + +object FreeUser { + def unapply(user: FreeUser): Option[String] = Some(user.name) +} +object PremiumUser { + def unapply(user: PremiumUser): Option[String] = Some(user.name) +} + +// using +val user: User = new PremiumUser("Daniel") +user match { + case FreeUser(name) => "Hello " + name + case PremiumUser(name) => "Welcome back, dear " + name +} +``` + +To + +```C# +public interface IUser +{ + string Name { get; } +} + +public class FreeUser : IUser +{ + public FreeUser(string name) + { + Name = name; + } + + public string Name { get; } +} + +public class PremiumUser : IUser +{ + public PremiumUser(string name) + { + Name = name; + } + + public string Name { get; } +} + +public static class UserExtensions +{ + public static bool TryExtractName(this IUser user, out string name) + { + name = user.Name; + return !string.IsNullOrEmpty(user.Name); + } +} + +// using +IUser user = new PremiumUser("Daniel"); +``` + +In C# 7 + +```c# +string message = string.Empty; +switch (user) +{ + case FreeUser p when p.TryExtractName(out var name): + message = $"Hello {name}"; + break; + case PremiumUser p when p.TryExtractName(out var name): + message = $"Welcome back, dear {name}" + break; +} +``` + +In C# 8 + +```c# +var message = user switch +{ + FreeUser p when p.TryExtractName(out var name) => $"Hello {name}", + PremiumUser p when p.TryExtractName(out var name) => $"Welcome back, dear {name}", + _ => string.Empty +}; +``` + +## Require + +```scala +require(cost > 0, "cost must be > 0") +``` + +use `ArgumentException` + +```C# +if (cost <= 0) throw ArgumentException("cost must be > 0", nameof(cost)); +``` + +## Not Covered Topics + +* Traits +* Partial functions +* Local functions/Nested Methods +* Tail call recursion +* For Comprehensions +* Default Parameter Values +* Implicit parameters + +# Tests + +* Prefer to use `FluentAssertions` in tests, instead of `Xunit assertions` and `AkkaSpecExtensions` + +## Intercept[T] + +Akka uses intecept to check that an exception was thrown + +```scala +intercept[IllegalArgumentException] { + val serializer = new MiscMessageSerializer(system.asInstanceOf[ExtendedActorSystem]) + serializer.manifest("INVALID") +} +``` + +in C# you have 2 options + +```c# +var serializer = new MiscMessageSerializer(Sys.AsInstanceOf()); + +// use FluentAssertions +Action comparison = () => serializer.Manifest("INVALID"); +comparison.ShouldThrow(); + +// use Xunit2 asserts +Assert.Throws(() => serializer.Manifest("INVALID")); +``` diff --git a/docs/community/contributing/toc.yml b/docs/community/contributing/toc.yml index 3d5f415b507..94c36eac48d 100644 --- a/docs/community/contributing/toc.yml +++ b/docs/community/contributing/toc.yml @@ -10,6 +10,8 @@ href: wire-compatibility.md - name: Code Guidelines href: code-guidelines.md +- name: Scala To C# Conversion Guide + href: porting-guide.md - name: Documentation Guidelines href: documentation-guidelines.md - name: Debugging Akka.NET From 654be5939e50055b4d8ab07aee4c6d22b5a218c9 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Wed, 5 Jan 2022 04:00:32 +0700 Subject: [PATCH 15/30] Add debugging tips to the community guideline documentation (#5480) * Add debugging tips * Fix linter error --- docs/community/contributing/debugging.md | 42 ++++++++++++++++++ .../debugging/rider-repeat-until-failure.png | Bin 0 -> 186193 bytes 2 files changed, 42 insertions(+) create mode 100644 docs/images/community/debugging/rider-repeat-until-failure.png diff --git a/docs/community/contributing/debugging.md b/docs/community/contributing/debugging.md index b49cf8a9b17..4e972acfa70 100644 --- a/docs/community/contributing/debugging.md +++ b/docs/community/contributing/debugging.md @@ -71,3 +71,45 @@ Thus there are a few ways we can fix this spec: 1. Use `await` instead of `Task.Wait` - generally we should be doing this everywhere when possible; 2. Relax the timing constraints either by increasing the wait period or by wrapping the assertion block inside an `AwaitAssert`; or 3. Use the `TestScheduler` and manually advance the clock. That might cause other problems but it takes the non-determinism of the business of the CPU out of the picture. + +## Testing For Racy Unit Tests Locally + +A racy test under Azure DevOps might run fine locally, and we might need to force run a test until they fail. Here are some techniques you can use to force a test to fail. + +### Running Tests Under Very Limited Computing Resources + +Azure DevOps virtual machines ran under a very tight computing resource budget. It is sometime necessary +for us to emulate that locally to test to see if our code changes would run under a very limited resource +condition. The easiest way to do this is to leverage the Windows 10 WSL 2 Linux virtual machine feature. + +* Install SWL 2 by following these [instructions](https://docs.microsoft.com/en-us/windows/wsl/install) +* Create a `.wslconfig` file in `C:\Users\[User Name]\` +* Copy and paste these configuration. + +```ini +[wsl2] +memory=2GB # Limits VM memory in WSL 2 up to 2GB +processors=2 # Makes the WSL 2 VM use two virtual processors +``` + +### Repeating a Test Until It Fails + +If you're using JetBrains Rider, you can use their unit test feature to run a test until it fails. +On the `Unit Tests` tab, click on the drop down arrow right beside the the play button and click on the +`Run Selected Tests Until Failure` option. + +![JetBrains Rider Unit Test Repeat Until Failure](../../images/community/debugging/rider-repeat-until-failure.png) + +Another option is to leverage the Xunit `TheoryAttribute` to run a test multiple time. We provided a +convenience `RepeatAttribute` to do this. + +```c# +using Akka.Tests.Shared.Internals; + +[Theory] +[Repeat(100)] +public void RepeatedTest(int _) +{ + ... +} +``` diff --git a/docs/images/community/debugging/rider-repeat-until-failure.png b/docs/images/community/debugging/rider-repeat-until-failure.png new file mode 100644 index 0000000000000000000000000000000000000000..622a70fcc433b1485dcb35afd60d1c05647a0288 GIT binary patch literal 186193 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

sAZ&Z3KJ_L2OH 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
Ys5X$|w1DB6 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 literal 0 HcmV?d00001 From 4edb257d96c82f569a22e93e778c3a62ab9f483c Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Wed, 5 Jan 2022 22:08:00 +0700 Subject: [PATCH 16/30] Update Akka.MultiNode.TestAdapter to 1.1.0-beta2 (#5458) * Update Akka.MultiNode.TestAdapter to 1.1.0-beta2 * Mark MultiNodeFactAttribute with the ObsoleteAttribute Co-authored-by: Aaron Stannard --- src/common.props | 2 +- .../ClusterMetricsExtensionSpec.cs | 1 + .../ClustetMetricsRoutingSpec.cs | 1 + .../Properties/AssemblyInfo.cs | 10 ++ .../Sample/StatsSampleSpec.cs | 1 + ...lusterShardingCustomShardAllocationSpec.cs | 1 + .../ClusterShardingFailureSpec.cs | 1 + .../ClusterShardingGetStateSpec.cs | 1 + .../ClusterShardingGetStatsSpec.cs | 1 + .../ClusterShardingGracefulShutdownSpec.cs | 1 + .../ClusterShardingLeavingSpec.cs | 1 + .../ClusterShardingMinMembersSpec.cs | 1 + ...dingRegistrationCoordinatedShutdownSpec.cs | 1 + ...hardingRememberEntitiesNewExtractorSpec.cs | 1 + .../ClusterShardingRememberEntitiesSpec.cs | 1 + .../ClusterShardingSpec.cs | 1 + .../Properties/AssemblyInfo.cs | 5 +- .../RollingUpdateShardAllocationSpec.cs | 1 + .../ShardedDaemonProcessSpec.cs | 1 + .../ClusterClientHandoverSpec.cs | 1 + .../ClusterClient/ClusterClientSpec.cs | 1 + .../ClusterClientStartSpecConfig.cs | 1 + .../ClusterClient/ClusterClientStopSpec.cs | 1 + .../Properties/AssemblyInfo.cs | 3 + .../DistributedPubSubMediatorSpec.cs | 1 + .../DistributedPubSubRestartSpec.cs | 1 + .../ClusterSingletonManagerChaosSpec.cs | 1 + .../ClusterSingletonManagerDownedSpec.cs | 1 + .../ClusterSingletonManagerLeaseSpec.cs | 1 + .../ClusterSingletonManagerLeave2Spec.cs | 1 + .../ClusterSingletonManagerLeaveSpec.cs | 1 + .../Singleton/ClusterSingletonManagerSpec.cs | 1 + .../ClusterSingletonManagerStartupSpec.cs | 1 + .../DurableDataPocoSpec.cs | 1 + .../DurableDataSpec.cs | 1 + .../DurablePruningSpec.cs | 1 + .../JepsenInspiredInsertSpec.cs | 1 + .../Properties/AssemblyInfo.cs | 3 + .../ReplicatorChaosSpec.cs | 1 + .../ReplicatorPruningSpec.cs | 1 + .../ReplicatorSpec.cs | 1 + .../AttemptSysMsgRedeliverySpec.cs | 1 + .../Bugfix4353Specs.cs | 1 + .../ClientDowningNodeThatIsUnreachableSpec.cs | 1 + .../ClientDowningNodeThatIsUpSpec.cs | 1 + .../ClusterAccrualFailureDetectorSpec.cs | 1 + .../ClusterDeathWatchSpec.cs | 1 + .../ConvergenceSpec.cs | 1 + .../DeterministicOldestWhenJoiningSpec.cs | 1 + .../DisallowJoinOfTwoClustersSpec.cs | 1 + .../InitialHeartbeatSpec.cs | 1 + .../JoinInProgressSpec.cs | 1 + .../JoinSeedNodeSpec.cs | 1 + .../JoinWithOfflineSeedNodeSpec.cs | 1 + .../LeaderDowningAllOtherNodesSpec.cs | 1 + .../LeaderDowningNodeThatIsUnreachableSpec.cs | 1 + .../LeaderElectionSpec.cs | 1 + .../LeaderLeavingSpec.cs | 1 + .../MemberWeaklyUpSpec.cs | 1 + .../MembershipChangeListenerExitingSpec.cs | 1 + .../MembershipChangeListenerUpSpec.cs | 1 + .../MinMembersBeforeUpSpec.cs | 1 + .../NodeChurnSpec.cs | 1 + .../NodeDowningAndBeingRemovedSpec.cs | 1 + ...odeLeavingAndExitingAndBeingRemovedSpec.cs | 1 + .../NodeLeavingAndExitingSpec.cs | 1 + .../NodeMembershipSpec.cs | 1 + .../NodeUpSpec.cs | 1 + .../Properties/AssemblyInfo.cs | 3 + .../QuickRestartSpec.cs | 1 + .../RestartFirstSeedNodeSpec.cs | 1 + .../RestartNode2Spec.cs | 1 + .../RestartNode3Spec.cs | 1 + .../RestartNodeSpec.cs | 1 + .../ClusterBroadcastRouter2266BugfixSpec.cs | 1 + .../ClusterConsistentHashingGroupSpec.cs | 1 + .../ClusterConsistentHashingRouterSpec.cs | 1 + .../ClusterPoolRouter3916BugfixSpec.cs | 1 + .../Routing/ClusterRoundRobinSpec.cs | 1 + .../Routing/UseRoleIgnoredSpec.cs | 1 + .../DownAllIndirectlyConnected5NodeSpec.cs | 1 + .../SBR/DownAllUnstable5NodeSpec.cs | 1 + .../SBR/IndirectlyConnected3NodeSpec.cs | 1 + .../SBR/IndirectlyConnected5NodeSpec.cs | 1 + .../SBR/LeaseMajority5NodeSpec.cs | 1 + .../SingletonClusterSpec.cs | 1 + .../SplitBrainResolverDowningSpec.cs | 1 + .../SplitBrainSpec.cs | 1 + .../StressSpec.cs | 122 +++++++++--------- .../SunnyWeatherSpec.cs | 1 + .../SurviveNetworkInstabilitySpec.cs | 1 + .../TransitionSpec.cs | 1 + .../UnreachableNodeJoinsAgainSpec.cs | 1 + src/core/Akka.Remote.TestKit/MultiNodeFact.cs | 4 + .../AttemptSysMsgRedeliverySpec.cs | 1 + .../LookupRemoteActorMultiNetSpec.cs | 1 + .../NewRemoteActorSpec.cs | 1 + .../PiercingShouldKeepQuarantineSpec.cs | 1 + .../Properties/AssemblyInfo.cs | 3 + .../RemoteDeliverySpec.cs | 1 + .../RemoteDeploymentDeathWatchSpec.cs | 1 + .../RemoteGatePiercingSpec.cs | 1 + .../RemoteNodeDeathWatchSpec.cs | 1 + .../RemoteNodeRestartDeathWatchSpec.cs | 1 + .../RemoteNodeRestartGateSpec.cs | 1 + .../RemoteNodeShutdownAndComesBackSpec.cs | 1 + .../RemoteQuarantinePiercingSpec.cs | 1 + .../RemoteReDeploymentSpec.cs | 1 + .../RemoteRestartedQuarantinedSpec.cs | 1 + .../Router/RemoteRandomSpec.cs | 2 + .../Router/RemoteRoundRobinSpec.cs | 1 + .../Router/RemoteScatterGatherSpec.cs | 1 + .../TestConductor/TestConductorSpec.cs | 2 + .../TransportFailSpec.cs | 2 + 114 files changed, 202 insertions(+), 61 deletions(-) create mode 100644 src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Properties/AssemblyInfo.cs diff --git a/src/common.props b/src/common.props index 5f9e8ba4663..df4128a8b1b 100644 --- a/src/common.props +++ b/src/common.props @@ -25,7 +25,7 @@ 2.16.3 2.0.3 4.7.0 - 1.0.0 + 1.1.0-beta2 akka;actors;actor model;Akka;concurrency diff --git a/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/ClusterMetricsExtensionSpec.cs b/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/ClusterMetricsExtensionSpec.cs index 8082248bc7e..8b3b8168b7a 100644 --- a/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/ClusterMetricsExtensionSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/ClusterMetricsExtensionSpec.cs @@ -17,6 +17,7 @@ using Akka.Configuration; using FluentAssertions.Extensions; using ConfigurationFactory = Akka.Configuration.ConfigurationFactory; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Metrics.Tests.MultiNode { diff --git a/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/ClustetMetricsRoutingSpec.cs b/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/ClustetMetricsRoutingSpec.cs index 9782f4ab0c2..9f4889c7408 100644 --- a/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/ClustetMetricsRoutingSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/ClustetMetricsRoutingSpec.cs @@ -25,6 +25,7 @@ using FluentAssertions.Extensions; using Address = Akka.Actor.Address; using ConfigurationFactory = Akka.Configuration.ConfigurationFactory; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Metrics.Tests.MultiNode { diff --git a/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Properties/AssemblyInfo.cs b/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..f166cc8d5f7 --- /dev/null +++ b/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Properties/AssemblyInfo.cs @@ -0,0 +1,10 @@ +// //----------------------------------------------------------------------- +// // +// // Copyright (C) 2009-2021 Lightbend Inc. +// // Copyright (C) 2013-2021 .NET Foundation +// // +// //----------------------------------------------------------------------- + +using Xunit; + +[assembly: TestFramework("Akka.MultiNode.TestAdapter.MultiNodeTestFramework", "Akka.MultiNode.TestAdapter")] diff --git a/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Sample/StatsSampleSpec.cs b/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Sample/StatsSampleSpec.cs index 724c1843bb5..142e3b6cfaf 100644 --- a/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Sample/StatsSampleSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Sample/StatsSampleSpec.cs @@ -16,6 +16,7 @@ using Akka.Configuration; using FluentAssertions.Extensions; using ConfigurationFactory = Akka.Configuration.ConfigurationFactory; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Metrics.Tests.MultiNode { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingCustomShardAllocationSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingCustomShardAllocationSpec.cs index 525d0f3fcb8..35e52aaf58c 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingCustomShardAllocationSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingCustomShardAllocationSpec.cs @@ -14,6 +14,7 @@ using System.Threading.Tasks; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingFailureSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingFailureSpec.cs index e1283af5cab..c7754add610 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingFailureSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingFailureSpec.cs @@ -16,6 +16,7 @@ using Akka.Remote.Transport; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingGetStateSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingGetStateSpec.cs index 218e7f8b82d..1448f349e11 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingGetStateSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingGetStateSpec.cs @@ -14,6 +14,7 @@ using System.Collections.Immutable; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingGetStatsSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingGetStatsSpec.cs index b9404a974fb..36207a68971 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingGetStatsSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingGetStatsSpec.cs @@ -13,6 +13,7 @@ using Akka.Remote.TestKit; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingGracefulShutdownSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingGracefulShutdownSpec.cs index cd69ebaa385..1f093956212 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingGracefulShutdownSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingGracefulShutdownSpec.cs @@ -16,6 +16,7 @@ using System.IO; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingLeavingSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingLeavingSpec.cs index 17560102de1..f1718a6981c 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingLeavingSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingLeavingSpec.cs @@ -16,6 +16,7 @@ using System.IO; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingMinMembersSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingMinMembersSpec.cs index 1a4e64c6f2c..a567caed6da 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingMinMembersSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingMinMembersSpec.cs @@ -15,6 +15,7 @@ using Akka.Remote.TestKit; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingRegistrationCoordinatedShutdownSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingRegistrationCoordinatedShutdownSpec.cs index d96462cf291..a6a4b4a86c2 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingRegistrationCoordinatedShutdownSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingRegistrationCoordinatedShutdownSpec.cs @@ -19,6 +19,7 @@ using System.Threading.Tasks; using System.Threading; using Akka.Event; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingRememberEntitiesNewExtractorSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingRememberEntitiesNewExtractorSpec.cs index e70bd5c1ea3..155e49207b3 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingRememberEntitiesNewExtractorSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingRememberEntitiesNewExtractorSpec.cs @@ -15,6 +15,7 @@ using Akka.Remote.TestKit; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingRememberEntitiesSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingRememberEntitiesSpec.cs index b5a9be629f9..bb9b51913ef 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingRememberEntitiesSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingRememberEntitiesSpec.cs @@ -16,6 +16,7 @@ using Akka.TestKit; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingSpec.cs index db81afa105c..9860877dbc4 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingSpec.cs @@ -22,6 +22,7 @@ using Akka.TestKit.TestEvent; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/Properties/AssemblyInfo.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/Properties/AssemblyInfo.cs index 024284a823c..8212c5f4237 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/Properties/AssemblyInfo.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/Properties/AssemblyInfo.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Xunit; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -19,4 +20,6 @@ [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("666698ab-85bc-4c91-894c-cbb4316dc1a4")] \ No newline at end of file +[assembly: Guid("666698ab-85bc-4c91-894c-cbb4316dc1a4")] + +[assembly: TestFramework("Akka.MultiNode.TestAdapter.MultiNodeTestFramework", "Akka.MultiNode.TestAdapter")] diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/RollingUpdateShardAllocationSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/RollingUpdateShardAllocationSpec.cs index 1c739415e33..4b0edfd230b 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/RollingUpdateShardAllocationSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/RollingUpdateShardAllocationSpec.cs @@ -15,6 +15,7 @@ using Akka.Remote.TestKit; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests { diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ShardedDaemonProcessSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ShardedDaemonProcessSpec.cs index aef341389f8..51554116344 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ShardedDaemonProcessSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ShardedDaemonProcessSpec.cs @@ -13,6 +13,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Sharding.Tests.MultiNode { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientHandoverSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientHandoverSpec.cs index e59defb7b20..376659e0b81 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientHandoverSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientHandoverSpec.cs @@ -19,6 +19,7 @@ using Akka.Remote.TestKit; using Akka.TestKit.TestActors; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.Client { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientSpec.cs index 96d9912a75e..9a5a4a53705 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientSpec.cs @@ -22,6 +22,7 @@ using Akka.Util.Internal; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.Client { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientStartSpecConfig.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientStartSpecConfig.cs index 76a30f2d0e6..c5e64c3a37b 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientStartSpecConfig.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientStartSpecConfig.cs @@ -15,6 +15,7 @@ using Akka.Remote.TestKit; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.Client { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientStopSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientStopSpec.cs index 6874365d362..a5c9b14d035 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientStopSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/ClusterClient/ClusterClientStopSpec.cs @@ -16,6 +16,7 @@ using Akka.Remote.TestKit; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.Client { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Properties/AssemblyInfo.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Properties/AssemblyInfo.cs index 848e3764721..e0ab865dfec 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Properties/AssemblyInfo.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Properties/AssemblyInfo.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Xunit; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -31,3 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] + +[assembly: TestFramework("Akka.MultiNode.TestAdapter.MultiNodeTestFramework", "Akka.MultiNode.TestAdapter")] diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/PublishSubscribe/DistributedPubSubMediatorSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/PublishSubscribe/DistributedPubSubMediatorSpec.cs index 39bd1e8fa3d..cf19d8a81a3 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/PublishSubscribe/DistributedPubSubMediatorSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/PublishSubscribe/DistributedPubSubMediatorSpec.cs @@ -20,6 +20,7 @@ using Xunit; using FluentAssertions; using System.Collections.Immutable; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.PublishSubscribe { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/PublishSubscribe/DistributedPubSubRestartSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/PublishSubscribe/DistributedPubSubRestartSpec.cs index 43698b7d3f7..1d5f49f1045 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/PublishSubscribe/DistributedPubSubRestartSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/PublishSubscribe/DistributedPubSubRestartSpec.cs @@ -13,6 +13,7 @@ using Akka.Remote.TestKit; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.PublishSubscribe { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerChaosSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerChaosSpec.cs index ae3199696ea..1fc059946ab 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerChaosSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerChaosSpec.cs @@ -16,6 +16,7 @@ using FluentAssertions; using Akka.TestKit.TestEvent; using Akka.TestKit.Internal; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.Singleton { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerDownedSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerDownedSpec.cs index 275e0681474..8e550e1461f 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerDownedSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerDownedSpec.cs @@ -15,6 +15,7 @@ using Akka.Remote.Transport; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.Singleton { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerLeaseSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerLeaseSpec.cs index 84a5c43182d..3bdc493d46e 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerLeaseSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerLeaseSpec.cs @@ -18,6 +18,7 @@ using Akka.TestKit; using Akka.Util.Internal; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.Singleton { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerLeave2Spec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerLeave2Spec.cs index 1a3f439d8b9..79792e94d4a 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerLeave2Spec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerLeave2Spec.cs @@ -18,6 +18,7 @@ using Akka.Util.Internal; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.Singleton { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerLeaveSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerLeaveSpec.cs index 6c75afb0c18..7334d7613aa 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerLeaveSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerLeaveSpec.cs @@ -15,6 +15,7 @@ using Akka.TestKit; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.Singleton { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerSpec.cs index 7e46806a8d1..6409edc89c4 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerSpec.cs @@ -17,6 +17,7 @@ using Akka.TestKit.Internal.StringMatcher; using Akka.TestKit.TestEvent; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.Singleton { diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerStartupSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerStartupSpec.cs index 6a95307e9c0..ea7c532b0fb 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerStartupSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Singleton/ClusterSingletonManagerStartupSpec.cs @@ -13,6 +13,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tools.Tests.MultiNode.Singleton { diff --git a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/DurableDataPocoSpec.cs b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/DurableDataPocoSpec.cs index 9c24d4818f9..840aad71c53 100644 --- a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/DurableDataPocoSpec.cs +++ b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/DurableDataPocoSpec.cs @@ -17,6 +17,7 @@ using Akka.TestKit; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.DistributedData.Tests.MultiNode { diff --git a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/DurableDataSpec.cs b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/DurableDataSpec.cs index b151c877532..4ad0ad9f220 100644 --- a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/DurableDataSpec.cs +++ b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/DurableDataSpec.cs @@ -17,6 +17,7 @@ using Akka.TestKit; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.DistributedData.Tests.MultiNode { diff --git a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/DurablePruningSpec.cs b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/DurablePruningSpec.cs index be36d749aad..eb0071bd36c 100644 --- a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/DurablePruningSpec.cs +++ b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/DurablePruningSpec.cs @@ -15,6 +15,7 @@ using Akka.Remote.TestKit; using Akka.TestKit; using Akka.TestKit.Xunit2; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.DistributedData.Tests.MultiNode { diff --git a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/JepsenInspiredInsertSpec.cs b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/JepsenInspiredInsertSpec.cs index ea66f7cbc36..44cb57202b0 100644 --- a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/JepsenInspiredInsertSpec.cs +++ b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/JepsenInspiredInsertSpec.cs @@ -18,6 +18,7 @@ using Akka.TestKit; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.DistributedData.Tests.MultiNode { diff --git a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/Properties/AssemblyInfo.cs b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/Properties/AssemblyInfo.cs index c48550d2b95..9c1d3aff20a 100644 --- a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/Properties/AssemblyInfo.cs +++ b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/Properties/AssemblyInfo.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Xunit; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -31,3 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] + +[assembly: TestFramework("Akka.MultiNode.TestAdapter.MultiNodeTestFramework", "Akka.MultiNode.TestAdapter")] diff --git a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/ReplicatorChaosSpec.cs b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/ReplicatorChaosSpec.cs index 293d298468d..b8bf5bed33c 100644 --- a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/ReplicatorChaosSpec.cs +++ b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/ReplicatorChaosSpec.cs @@ -16,6 +16,7 @@ using Akka.Remote.Transport; using Akka.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.DistributedData.Tests.MultiNode { diff --git a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/ReplicatorPruningSpec.cs b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/ReplicatorPruningSpec.cs index a338710070b..883bb66b2c7 100644 --- a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/ReplicatorPruningSpec.cs +++ b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/ReplicatorPruningSpec.cs @@ -14,6 +14,7 @@ using Akka.TestKit; using Akka.Util.Internal; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.DistributedData.Tests.MultiNode { diff --git a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/ReplicatorSpec.cs b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/ReplicatorSpec.cs index fd23d9026b5..6f4bbc485eb 100644 --- a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/ReplicatorSpec.cs +++ b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/ReplicatorSpec.cs @@ -17,6 +17,7 @@ using Akka.Remote.Transport; using Akka.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.DistributedData.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/AttemptSysMsgRedeliverySpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/AttemptSysMsgRedeliverySpec.cs index cc3dd1354ea..46ef2634a58 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/AttemptSysMsgRedeliverySpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/AttemptSysMsgRedeliverySpec.cs @@ -11,6 +11,7 @@ using Akka.Remote.Transport; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Bugfix4353Specs.cs b/src/core/Akka.Cluster.Tests.MultiNode/Bugfix4353Specs.cs index 347fd141ac1..524b906db4d 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Bugfix4353Specs.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Bugfix4353Specs.cs @@ -15,6 +15,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/ClientDowningNodeThatIsUnreachableSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/ClientDowningNodeThatIsUnreachableSpec.cs index ee878212a79..852c9c6ae5d 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/ClientDowningNodeThatIsUnreachableSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/ClientDowningNodeThatIsUnreachableSpec.cs @@ -12,6 +12,7 @@ using Akka.Cluster.Tests.MultiNode; using Akka.Remote.TestKit; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/ClientDowningNodeThatIsUpSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/ClientDowningNodeThatIsUpSpec.cs index 77e3801411d..4b623490d33 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/ClientDowningNodeThatIsUpSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/ClientDowningNodeThatIsUpSpec.cs @@ -11,6 +11,7 @@ using Akka.Cluster.Tests.MultiNode; using Akka.Remote.TestKit; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.TestKit { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/ClusterAccrualFailureDetectorSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/ClusterAccrualFailureDetectorSpec.cs index a731edc26d8..70c451f82cb 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/ClusterAccrualFailureDetectorSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/ClusterAccrualFailureDetectorSpec.cs @@ -13,6 +13,7 @@ using Akka.Remote.Transport; using Akka.TestKit; using Akka.Cluster.Tests.MultiNode; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/ClusterDeathWatchSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/ClusterDeathWatchSpec.cs index a4076553310..ef75627eed0 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/ClusterDeathWatchSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/ClusterDeathWatchSpec.cs @@ -15,6 +15,7 @@ using Akka.TestKit; using Akka.TestKit.TestActors; using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/ConvergenceSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/ConvergenceSpec.cs index 342e8bc569a..5bc56ccdfe2 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/ConvergenceSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/ConvergenceSpec.cs @@ -14,6 +14,7 @@ using Akka.Remote.TestKit; using Akka.TestKit; using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/DeterministicOldestWhenJoiningSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/DeterministicOldestWhenJoiningSpec.cs index 1979ed206b4..46bb0266254 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/DeterministicOldestWhenJoiningSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/DeterministicOldestWhenJoiningSpec.cs @@ -14,6 +14,7 @@ using Akka.Remote.TestKit; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/DisallowJoinOfTwoClustersSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/DisallowJoinOfTwoClustersSpec.cs index a07acd0edd5..c141a1e361b 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/DisallowJoinOfTwoClustersSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/DisallowJoinOfTwoClustersSpec.cs @@ -10,6 +10,7 @@ using Akka.Cluster.TestKit; using Akka.Remote.TestKit; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/InitialHeartbeatSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/InitialHeartbeatSpec.cs index 8bd3cd42830..198a75625e0 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/InitialHeartbeatSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/InitialHeartbeatSpec.cs @@ -12,6 +12,7 @@ using Akka.Remote.TestKit; using Akka.Remote.Transport; using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/JoinInProgressSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/JoinInProgressSpec.cs index 1e8562c5c68..801df7e753a 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/JoinInProgressSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/JoinInProgressSpec.cs @@ -12,6 +12,7 @@ using Akka.Remote; using Akka.Remote.TestKit; using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/JoinSeedNodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/JoinSeedNodeSpec.cs index 5d07a9a5399..5db78593e08 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/JoinSeedNodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/JoinSeedNodeSpec.cs @@ -11,6 +11,7 @@ using Akka.Cluster.TestKit; using Akka.Configuration; using Akka.Remote.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/JoinWithOfflineSeedNodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/JoinWithOfflineSeedNodeSpec.cs index 3774154c0b4..642c2d476c9 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/JoinWithOfflineSeedNodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/JoinWithOfflineSeedNodeSpec.cs @@ -13,6 +13,7 @@ using Akka.Event; using Akka.Remote.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/LeaderDowningAllOtherNodesSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/LeaderDowningAllOtherNodesSpec.cs index 1d30092641f..41f390a14cc 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/LeaderDowningAllOtherNodesSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/LeaderDowningAllOtherNodesSpec.cs @@ -13,6 +13,7 @@ using Akka.Util.Internal; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/LeaderDowningNodeThatIsUnreachableSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/LeaderDowningNodeThatIsUnreachableSpec.cs index a4ce3760ba2..49d6357d2c3 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/LeaderDowningNodeThatIsUnreachableSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/LeaderDowningNodeThatIsUnreachableSpec.cs @@ -16,6 +16,7 @@ using Akka.Remote.TestKit; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/LeaderElectionSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/LeaderElectionSpec.cs index 39f59d92f92..0458bb4680c 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/LeaderElectionSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/LeaderElectionSpec.cs @@ -11,6 +11,7 @@ using Akka.Cluster.TestKit; using Akka.Remote.TestKit; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/LeaderLeavingSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/LeaderLeavingSpec.cs index 25695b3abd6..69ab05de10a 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/LeaderLeavingSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/LeaderLeavingSpec.cs @@ -11,6 +11,7 @@ using Akka.Cluster.TestKit; using Akka.Remote.TestKit; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/MemberWeaklyUpSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/MemberWeaklyUpSpec.cs index 1d3a143a7a1..e85d0786c86 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/MemberWeaklyUpSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/MemberWeaklyUpSpec.cs @@ -13,6 +13,7 @@ using Akka.Remote.TestKit; using Akka.Remote.Transport; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/MembershipChangeListenerExitingSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/MembershipChangeListenerExitingSpec.cs index 11bb4646e44..f0076d5b454 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/MembershipChangeListenerExitingSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/MembershipChangeListenerExitingSpec.cs @@ -10,6 +10,7 @@ using Akka.Cluster.TestKit; using Akka.Remote.TestKit; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/MembershipChangeListenerUpSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/MembershipChangeListenerUpSpec.cs index 80c60ec4dc5..6c69e468e9f 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/MembershipChangeListenerUpSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/MembershipChangeListenerUpSpec.cs @@ -11,6 +11,7 @@ using Akka.Cluster.TestKit; using Akka.Remote.TestKit; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/MinMembersBeforeUpSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/MinMembersBeforeUpSpec.cs index f7381a18cc8..6be7421a6f2 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/MinMembersBeforeUpSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/MinMembersBeforeUpSpec.cs @@ -14,6 +14,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/NodeChurnSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/NodeChurnSpec.cs index 810655597f4..f21118d67e2 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/NodeChurnSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/NodeChurnSpec.cs @@ -14,6 +14,7 @@ using Akka.Remote.TestKit; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/NodeDowningAndBeingRemovedSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/NodeDowningAndBeingRemovedSpec.cs index a5b31976a52..bc6fd2c6e2c 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/NodeDowningAndBeingRemovedSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/NodeDowningAndBeingRemovedSpec.cs @@ -11,6 +11,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/NodeLeavingAndExitingAndBeingRemovedSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/NodeLeavingAndExitingAndBeingRemovedSpec.cs index 9606343a621..e21d5de7dbc 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/NodeLeavingAndExitingAndBeingRemovedSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/NodeLeavingAndExitingAndBeingRemovedSpec.cs @@ -11,6 +11,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/NodeLeavingAndExitingSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/NodeLeavingAndExitingSpec.cs index 70fa71cd430..915e8c9587d 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/NodeLeavingAndExitingSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/NodeLeavingAndExitingSpec.cs @@ -10,6 +10,7 @@ using Akka.Cluster.TestKit; using Akka.Remote.TestKit; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/NodeMembershipSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/NodeMembershipSpec.cs index c4a016a63ab..29b8937fb0d 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/NodeMembershipSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/NodeMembershipSpec.cs @@ -9,6 +9,7 @@ using Akka.Cluster.TestKit; using Akka.Remote.TestKit; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/NodeUpSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/NodeUpSpec.cs index a865a0c7760..4df006997af 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/NodeUpSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/NodeUpSpec.cs @@ -14,6 +14,7 @@ using Akka.Remote.TestKit; using Akka.TestKit; using Akka.Util; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Properties/AssemblyInfo.cs b/src/core/Akka.Cluster.Tests.MultiNode/Properties/AssemblyInfo.cs index 7e705a88fd1..b0a7c517d84 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Properties/AssemblyInfo.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Properties/AssemblyInfo.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Xunit; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -31,3 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] + +[assembly: TestFramework("Akka.MultiNode.TestAdapter.MultiNodeTestFramework", "Akka.MultiNode.TestAdapter")] diff --git a/src/core/Akka.Cluster.Tests.MultiNode/QuickRestartSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/QuickRestartSpec.cs index e74ee7431d2..6a0b73818d7 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/QuickRestartSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/QuickRestartSpec.cs @@ -15,6 +15,7 @@ using Akka.Remote.TestKit; using Akka.TestKit; using Akka.Util; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/RestartFirstSeedNodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/RestartFirstSeedNodeSpec.cs index 440b85b1449..e27033951c2 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/RestartFirstSeedNodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/RestartFirstSeedNodeSpec.cs @@ -14,6 +14,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs b/src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs index d3d7e951413..c111d34a384 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs @@ -13,6 +13,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/RestartNode3Spec.cs b/src/core/Akka.Cluster.Tests.MultiNode/RestartNode3Spec.cs index ff732b7b599..ec6a1175387 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/RestartNode3Spec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/RestartNode3Spec.cs @@ -15,6 +15,7 @@ using Akka.Remote.TestKit; using Akka.Remote.Transport; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/RestartNodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/RestartNodeSpec.cs index 567da92c30c..0412c3ad7de 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/RestartNodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/RestartNodeSpec.cs @@ -17,6 +17,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterBroadcastRouter2266BugfixSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterBroadcastRouter2266BugfixSpec.cs index 6837d44ec5c..2fa06dcedc7 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterBroadcastRouter2266BugfixSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterBroadcastRouter2266BugfixSpec.cs @@ -15,6 +15,7 @@ using Akka.Routing; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode.Routing { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingGroupSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingGroupSpec.cs index 05826245892..feaac4a33ee 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingGroupSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingGroupSpec.cs @@ -15,6 +15,7 @@ using Akka.Remote.TestKit; using Akka.Routing; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode.Routing { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingRouterSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingRouterSpec.cs index 72ed78cbc98..67bf06d1ae1 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingRouterSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingRouterSpec.cs @@ -18,6 +18,7 @@ using Akka.TestKit; using Xunit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode.Routing { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterPoolRouter3916BugfixSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterPoolRouter3916BugfixSpec.cs index 2f644b6de05..de1fa9ed8f7 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterPoolRouter3916BugfixSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterPoolRouter3916BugfixSpec.cs @@ -13,6 +13,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using Akka.Routing; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode.Routing { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterRoundRobinSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterRoundRobinSpec.cs index 643b1026169..47e692b42fc 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterRoundRobinSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterRoundRobinSpec.cs @@ -18,6 +18,7 @@ using Akka.Util.Internal; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode.Routing { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Routing/UseRoleIgnoredSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/Routing/UseRoleIgnoredSpec.cs index e2c465bfb56..2ded1da3dbf 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Routing/UseRoleIgnoredSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Routing/UseRoleIgnoredSpec.cs @@ -17,6 +17,7 @@ using Akka.Routing; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode.Routing { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllIndirectlyConnected5NodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllIndirectlyConnected5NodeSpec.cs index 9847d8b0de0..7c0ae9a6860 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllIndirectlyConnected5NodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllIndirectlyConnected5NodeSpec.cs @@ -13,6 +13,7 @@ using Akka.Remote.Transport; using Akka.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode.SBR { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllUnstable5NodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllUnstable5NodeSpec.cs index e4babe4eefa..2d98d24b26f 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllUnstable5NodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllUnstable5NodeSpec.cs @@ -14,6 +14,7 @@ using Akka.Remote.Transport; using Akka.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode.SBR { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected3NodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected3NodeSpec.cs index 05005fbec41..90a544da369 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected3NodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected3NodeSpec.cs @@ -13,6 +13,7 @@ using Akka.Remote.Transport; using Akka.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode.SBR { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected5NodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected5NodeSpec.cs index 03e1de074a1..23f96ad4ecf 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected5NodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected5NodeSpec.cs @@ -13,6 +13,7 @@ using Akka.Remote.Transport; using Akka.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode.SBR { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SBR/LeaseMajority5NodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SBR/LeaseMajority5NodeSpec.cs index 11a4113d960..6eb49201764 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SBR/LeaseMajority5NodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SBR/LeaseMajority5NodeSpec.cs @@ -17,6 +17,7 @@ using Akka.Remote.Transport; using Akka.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode.SBR { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SingletonClusterSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SingletonClusterSpec.cs index d80e6a6323b..51e02241a93 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SingletonClusterSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SingletonClusterSpec.cs @@ -12,6 +12,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SplitBrainResolverDowningSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SplitBrainResolverDowningSpec.cs index 93489582034..5fe7f23e17c 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SplitBrainResolverDowningSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SplitBrainResolverDowningSpec.cs @@ -13,6 +13,7 @@ using Akka.Remote.TestKit; using Akka.Remote.Transport; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SplitBrainSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SplitBrainSpec.cs index 1e598287ae6..c1b535b59c3 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SplitBrainSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SplitBrainSpec.cs @@ -13,6 +13,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using Akka.Remote.Transport; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/StressSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/StressSpec.cs index 444dfbf8987..18de69d4585 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/StressSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/StressSpec.cs @@ -28,6 +28,7 @@ using FluentAssertions; using Google.Protobuf.WellKnownTypes; using Environment = System.Environment; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { @@ -46,71 +47,74 @@ public StressSpecConfig() Role("node-" + i); CommonConfig = ConfigurationFactory.ParseString(@" - akka.test.cluster-stress-spec { - infolog = on - # scale the nr-of-nodes* settings with this factor - nr-of-nodes-factor = 1 - # not scaled - nr-of-seed-nodes = 3 - nr-of-nodes-joining-to-seed-initially = 2 - nr-of-nodes-joining-one-by-one-small = 2 - nr-of-nodes-joining-one-by-one-large = 2 - nr-of-nodes-joining-to-one = 2 - nr-of-nodes-leaving-one-by-one-small = 1 - nr-of-nodes-leaving-one-by-one-large = 1 - nr-of-nodes-leaving = 2 - nr-of-nodes-shutdown-one-by-one-small = 1 - nr-of-nodes-shutdown-one-by-one-large = 1 - nr-of-nodes-partition = 2 - nr-of-nodes-shutdown = 2 - nr-of-nodes-join-remove = 2 - # not scaled - # scale the *-duration settings with this factor - duration-factor = 1 - join-remove-duration = 90s - idle-gossip-duration = 10s - expected-test-duration = 600s - # scale convergence within timeouts with this factor - convergence-within-factor = 1.0 - } - akka.actor.provider = cluster +akka.test.cluster-stress-spec { + infolog = on + # scale the nr-of-nodes* settings with this factor + nr-of-nodes-factor = 1 + # not scaled + nr-of-seed-nodes = 3 + nr-of-nodes-joining-to-seed-initially = 2 + nr-of-nodes-joining-one-by-one-small = 2 + nr-of-nodes-joining-one-by-one-large = 2 + nr-of-nodes-joining-to-one = 2 + nr-of-nodes-leaving-one-by-one-small = 1 + nr-of-nodes-leaving-one-by-one-large = 1 + nr-of-nodes-leaving = 2 + nr-of-nodes-shutdown-one-by-one-small = 1 + nr-of-nodes-shutdown-one-by-one-large = 1 + nr-of-nodes-partition = 2 + nr-of-nodes-shutdown = 2 + nr-of-nodes-join-remove = 2 + # not scaled + # scale the *-duration settings with this factor + duration-factor = 1 + join-remove-duration = 90s + idle-gossip-duration = 10s + expected-test-duration = 600s + # scale convergence within timeouts with this factor + convergence-within-factor = 1.0 +} +akka.actor.provider = cluster - akka.cluster { - failure-detector.acceptable-heartbeat-pause = 3s - downing-provider-class = ""Akka.Cluster.SplitBrainResolver, Akka.Cluster"" - split-brain-resolver { - active-strategy = keep-majority #TODO: remove this once it's been made default - stable-after = 10s - } - publish-stats-interval = 1s +akka.cluster { + failure-detector.acceptable-heartbeat-pause = 3s + downing-provider-class = ""Akka.Cluster.SplitBrainResolver, Akka.Cluster"" + split-brain-resolver { + active-strategy = keep-majority #TODO: remove this once it's been made default + stable-after = 10s } - akka.loggers = [""Akka.TestKit.TestEventListener, Akka.TestKit""] - akka.loglevel = INFO - akka.remote.log-remote-lifecycle-events = off - akka.actor.default-dispatcher = { - executor = channel-executor - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 1 - parallelism-max = 64 - } - } - akka.actor.internal-dispatcher = { - executor = channel-executor - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 1 - parallelism-max = 64 - } - } + publish-stats-interval = 1s +} + +akka.loggers = [""Akka.TestKit.TestEventListener, Akka.TestKit""] +akka.loglevel = INFO +akka.remote.log-remote-lifecycle-events = off +akka.actor.default-dispatcher = { + executor = fork-join-executor + fork-join-executor { + parallelism-min = 2 + parallelism-factor = 1 + parallelism-max = 64 + } +} + +akka.actor.internal-dispatcher = { + executor = fork-join-executor + fork-join-executor { + parallelism-min = 2 + parallelism-factor = 1 + parallelism-max = 64 + } +} + akka.remote.default-remote-dispatcher { - executor = channel-executor - fork-join-executor { + executor = fork-join-executor + fork-join-executor { parallelism-min = 2 parallelism-factor = 0.5 parallelism-max = 16 - } - "); + } +}"); TestTransport = true; } diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SunnyWeatherSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SunnyWeatherSpec.cs index a608e794d07..49bb318d18d 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SunnyWeatherSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SunnyWeatherSpec.cs @@ -14,6 +14,7 @@ using Akka.Remote.TestKit; using Akka.Util; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SurviveNetworkInstabilitySpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SurviveNetworkInstabilitySpec.cs index 31cf70a3803..560f2450620 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SurviveNetworkInstabilitySpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SurviveNetworkInstabilitySpec.cs @@ -19,6 +19,7 @@ using Akka.Util.Internal; using FluentAssertions; using FluentAssertions.Extensions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/TransitionSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/TransitionSpec.cs index e4e20ffa445..9afbc5a713a 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/TransitionSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/TransitionSpec.cs @@ -13,6 +13,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Cluster.Tests.MultiNode/UnreachableNodeJoinsAgainSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/UnreachableNodeJoinsAgainSpec.cs index 4165d98f0d2..3200ac8f8a2 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/UnreachableNodeJoinsAgainSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/UnreachableNodeJoinsAgainSpec.cs @@ -17,6 +17,7 @@ using Akka.Remote.Transport; using Akka.Util.Internal; using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Cluster.Tests.MultiNode { diff --git a/src/core/Akka.Remote.TestKit/MultiNodeFact.cs b/src/core/Akka.Remote.TestKit/MultiNodeFact.cs index 6d24a774fa3..bdb05ea1019 100644 --- a/src/core/Akka.Remote.TestKit/MultiNodeFact.cs +++ b/src/core/Akka.Remote.TestKit/MultiNodeFact.cs @@ -10,6 +10,10 @@ namespace Akka.Remote.TestKit { + [Obsolete( + "This attribute and the Akka.MultiNodeTestRunner NuGet package is being deprecated in favor of the new " + + "Akka.MultiNode.TestAdapter NuGet package. To migrate your multi node test to the new package, please read " + + "https://github.com/akkadotnet/akka.net/discussions/5482")] public class MultiNodeFactAttribute : FactAttribute { ///

diff --git a/src/core/Akka.Remote.Tests.MultiNode/AttemptSysMsgRedeliverySpec.cs b/src/core/Akka.Remote.Tests.MultiNode/AttemptSysMsgRedeliverySpec.cs index 395df7e3ea9..5f4dc6d00c1 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/AttemptSysMsgRedeliverySpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/AttemptSysMsgRedeliverySpec.cs @@ -10,6 +10,7 @@ using Akka.Actor; using Akka.Remote.TestKit; using Akka.Remote.Transport; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/LookupRemoteActorMultiNetSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/LookupRemoteActorMultiNetSpec.cs index 7a891200bbc..9909afda193 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/LookupRemoteActorMultiNetSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/LookupRemoteActorMultiNetSpec.cs @@ -9,6 +9,7 @@ using Akka.Actor; using Akka.Remote.TestKit; using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/NewRemoteActorSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/NewRemoteActorSpec.cs index a02729bc460..49b396413ef 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/NewRemoteActorSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/NewRemoteActorSpec.cs @@ -11,6 +11,7 @@ using Akka.Remote.TestKit; using Akka.TestKit; using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/PiercingShouldKeepQuarantineSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/PiercingShouldKeepQuarantineSpec.cs index 3c261727cc6..e5a611413b7 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/PiercingShouldKeepQuarantineSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/PiercingShouldKeepQuarantineSpec.cs @@ -14,6 +14,7 @@ using Akka.Actor; using Akka.Configuration; using Akka.Remote.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/Properties/AssemblyInfo.cs b/src/core/Akka.Remote.Tests.MultiNode/Properties/AssemblyInfo.cs index f854623a878..6589d5bca9d 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/Properties/AssemblyInfo.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/Properties/AssemblyInfo.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Xunit; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -31,3 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] + +[assembly: TestFramework("Akka.MultiNode.TestAdapter.MultiNodeTestFramework", "Akka.MultiNode.TestAdapter")] diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteDeliverySpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteDeliverySpec.cs index 4648ca081c6..3fca108fb96 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/RemoteDeliverySpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteDeliverySpec.cs @@ -11,6 +11,7 @@ using Akka.Actor; using Akka.Remote.TestKit; using Akka.Configuration; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteDeploymentDeathWatchSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteDeploymentDeathWatchSpec.cs index aa016967a0d..d6124ccd376 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/RemoteDeploymentDeathWatchSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteDeploymentDeathWatchSpec.cs @@ -13,6 +13,7 @@ using Akka.TestKit; using Akka.TestKit.Xunit2; using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteGatePiercingSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteGatePiercingSpec.cs index 8128f5dd88a..7eab27ab82a 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/RemoteGatePiercingSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteGatePiercingSpec.cs @@ -10,6 +10,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using Akka.Remote.Transport; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeDeathWatchSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeDeathWatchSpec.cs index 45ff0d43f26..437497c1275 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeDeathWatchSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeDeathWatchSpec.cs @@ -14,6 +14,7 @@ using Akka.Remote.Transport; using Akka.TestKit; using static Akka.Remote.Tests.MultiNode.RemoteNodeDeathWatchMultiNetSpec; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeRestartDeathWatchSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeRestartDeathWatchSpec.cs index 28307a87640..c9b1decfa03 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeRestartDeathWatchSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeRestartDeathWatchSpec.cs @@ -15,6 +15,7 @@ using Akka.TestKit.Xunit2; using Akka.Util; using Akka.Util.Internal; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeRestartGateSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeRestartGateSpec.cs index 2b231d6fe06..d58899f583f 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeRestartGateSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeRestartGateSpec.cs @@ -14,6 +14,7 @@ using Akka.Remote.Transport; using Akka.TestKit; using Akka.Util.Internal; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeShutdownAndComesBackSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeShutdownAndComesBackSpec.cs index 183b87ecee6..6e6fafb5611 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeShutdownAndComesBackSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeShutdownAndComesBackSpec.cs @@ -13,6 +13,7 @@ using Akka.Remote.TestKit; using Akka.Remote.Transport; using Akka.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteQuarantinePiercingSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteQuarantinePiercingSpec.cs index 4b4045144c0..69f0323e77f 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/RemoteQuarantinePiercingSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteQuarantinePiercingSpec.cs @@ -11,6 +11,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteReDeploymentSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteReDeploymentSpec.cs index 8a9d95e4bde..f7a7b2d5733 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/RemoteReDeploymentSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteReDeploymentSpec.cs @@ -11,6 +11,7 @@ using Akka.Event; using Akka.Remote.TestKit; using Akka.Remote.Transport; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteRestartedQuarantinedSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteRestartedQuarantinedSpec.cs index 49962d1d47c..5ff2f7ad7df 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/RemoteRestartedQuarantinedSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteRestartedQuarantinedSpec.cs @@ -11,6 +11,7 @@ using Akka.Configuration; using Akka.Remote.TestKit; using FluentAssertions; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { diff --git a/src/core/Akka.Remote.Tests.MultiNode/Router/RemoteRandomSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/Router/RemoteRandomSpec.cs index c86e3dd73d0..9abccaf29fe 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/Router/RemoteRandomSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/Router/RemoteRandomSpec.cs @@ -13,6 +13,8 @@ using Akka.Routing; using Akka.Util.Internal; using FluentAssertions; +using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode.Router { diff --git a/src/core/Akka.Remote.Tests.MultiNode/Router/RemoteRoundRobinSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/Router/RemoteRoundRobinSpec.cs index d1b1d8313d9..d44d1f7670f 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/Router/RemoteRoundRobinSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/Router/RemoteRoundRobinSpec.cs @@ -16,6 +16,7 @@ using Akka.Util.Internal; using FluentAssertions; using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode.Router { diff --git a/src/core/Akka.Remote.Tests.MultiNode/Router/RemoteScatterGatherSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/Router/RemoteScatterGatherSpec.cs index 88873d32c36..089f1061c86 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/Router/RemoteScatterGatherSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/Router/RemoteScatterGatherSpec.cs @@ -19,6 +19,7 @@ using Akka.Util.Internal; using FluentAssertions; using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode.Router { diff --git a/src/core/Akka.Remote.Tests.MultiNode/TestConductor/TestConductorSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/TestConductor/TestConductorSpec.cs index 91555726d11..75f19442093 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/TestConductor/TestConductorSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/TestConductor/TestConductorSpec.cs @@ -17,6 +17,8 @@ using Akka.Remote.TestKit; using Akka.Remote.Transport; using Akka.TestKit; +using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode.TestConductor { diff --git a/src/core/Akka.Remote.Tests.MultiNode/TransportFailSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/TransportFailSpec.cs index 881eb956a6f..359eaf23fb0 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/TransportFailSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/TransportFailSpec.cs @@ -12,6 +12,8 @@ using Akka.Event; using Akka.Remote.TestKit; using Akka.Util; +using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; namespace Akka.Remote.Tests.MultiNode { From 64ba6383f0856632bdc2525113a091df5927cf2e Mon Sep 17 00:00:00 2001 From: TangkasOka <78479042+TangkasOka@users.noreply.github.com> Date: Fri, 7 Jan 2022 15:10:56 +0100 Subject: [PATCH 17/30] Update terminology.md (#5491) Remove backquote before "Wait-Freedom" header so it's properly formatted. --- docs/articles/concepts/terminology.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/concepts/terminology.md b/docs/articles/concepts/terminology.md index 06cb101041b..71dbcf33594 100644 --- a/docs/articles/concepts/terminology.md +++ b/docs/articles/concepts/terminology.md @@ -50,7 +50,7 @@ A Race condition is when an assumption about the ordering of a set of events mig As discussed in the previous sections, blocking is undesirable for several reasons, including the dangers of deadlocks and reduced throughput in the system. In the following sections we discuss various non-blocking properties with different strength. -`### Wait-Freedom +### Wait-Freedom A method is wait-free if every call is guaranteed to finish in a finite number of steps. If a method is bounded wait-free, then the number of steps has a finite upper bound. From f58a07a7a17c2e0d713ede416f61cb03997f895f Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Fri, 7 Jan 2022 22:12:38 +0700 Subject: [PATCH 18/30] Fix build script, MNTR should output to TestResults/multinode (#5492) --- build.fsx | 65 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/build.fsx b/build.fsx index 2bd3588f184..9b54c46cf20 100644 --- a/build.fsx +++ b/build.fsx @@ -325,30 +325,36 @@ Target "MultiNodeTestsNetCore" (fun _ -> let projects = let rawProjects = match (isWindows) with | true -> !! "./src/**/*.Tests.MultiNode.csproj" - | _ -> !! "./src/**/*.Tests.MulitNode.csproj" // if you need to filter specs for Linux vs. Windows, do it here + | _ -> !! "./src/**/*.Tests.MultiNode.csproj" // if you need to filter specs for Linux vs. Windows, do it here rawProjects |> Seq.choose filterProjects - let runSingleProject project = + let projectDlls = projects |> Seq.map ( fun project -> + let assemblyName = fileNameWithoutExt project + (directory project) @@ "bin" @@ "Release" @@ testNetCoreVersion @@ assemblyName + ".dll" + ) + + let runSingleProject projectDll = let arguments = match (hasTeamCity) with - | true -> (sprintf "test -c Release --no-build --logger:trx --logger:\"console;verbosity=normal\" --framework %s --results-directory \"%s\" -- -parallel none -teamcity" testNetCoreVersion outputMultiNode) - | false -> (sprintf "test -c Release --no-build --logger:trx --logger:\"console;verbosity=normal\" --framework %s --results-directory \"%s\" -- -parallel none" testNetCoreVersion outputMultiNode) - + | true -> (sprintf "test \"%s\" -l:trx -l:\"console;verbosity=detailed\" --framework %s --results-directory \"%s\" -- -teamcity" projectDll testNetCoreVersion outputMultiNode) + | false -> (sprintf "test \"%s\" -l:trx -l:\"console;verbosity=detailed\" --framework %s --results-directory \"%s\"" projectDll testNetCoreVersion outputMultiNode) + + let resultPath = (directory projectDll) + File.WriteAllText( + (resultPath @@ "xunit.multinode.runner.json"), + (sprintf "{\"outputDirectory\":\"%s\"}" outputMultiNode).Replace("\\", "\\\\")) + let result = ExecProcess(fun info -> info.FileName <- "dotnet" - info.WorkingDirectory <- (Directory.GetParent project).FullName + info.WorkingDirectory <- outputMultiNode info.Arguments <- arguments) (TimeSpan.FromMinutes 90.0) ResultHandling.failBuildIfXUnitReportedError TestRunnerErrorLevel.Error result CreateDir outputMultiNode - projects |> Seq.iter ( fun project -> - try - runSingleProject project - with - ex -> - raise (Exception(sprintf "Exception thrown while testing %s" project, ex)) - ) + projectDlls |> Seq.iter ( fun projectDll -> + runSingleProject projectDll + ) ) Target "MultiNodeTestsNet" (fun _ -> @@ -358,31 +364,38 @@ Target "MultiNodeTestsNet" (fun _ -> let projects = let rawProjects = match (isWindows) with | true -> !! "./src/**/*.Tests.MultiNode.csproj" - | _ -> !! "./src/**/*.Tests.MulitNode.csproj" // if you need to filter specs for Linux vs. Windows, do it here + | _ -> !! "./src/**/*.Tests.MultiNode.csproj" // if you need to filter specs for Linux vs. Windows, do it here rawProjects |> Seq.choose filterProjects - let runSingleProject project = + let projectDlls = projects |> Seq.map ( fun project -> + let assemblyName = fileNameWithoutExt project + (directory project) @@ "bin" @@ "Release" @@ testNetVersion @@ assemblyName + ".dll" + ) + + let runSingleProject projectDll = let arguments = match (hasTeamCity) with - | true -> (sprintf "test -c Release --no-build --logger:trx --logger:\"console;verbosity=normal\" --framework %s --results-directory \"%s\" -- -parallel none -teamcity" testNetVersion outputMultiNode) - | false -> (sprintf "test -c Release --no-build --logger:trx --logger:\"console;verbosity=normal\" --framework %s --results-directory \"%s\" -- -parallel none" testNetVersion outputMultiNode) - + | true -> (sprintf "test \"%s\" -l:trx -l:\"console;verbosity=detailed\" --framework %s --results-directory \"%s\" -- -teamcity" projectDll testNetVersion outputMultiNode) + | false -> (sprintf "test \"%s\" -l:trx -l:\"console;verbosity=detailed\" --framework %s --results-directory \"%s\"" projectDll testNetVersion outputMultiNode) + + let resultPath = (directory projectDll) + File.WriteAllText( + (resultPath @@ "xunit.multinode.runner.json"), + (sprintf "{\"outputDirectory\":\"%s\"}" outputMultiNode).Replace("\\", "\\\\")) + let result = ExecProcess(fun info -> info.FileName <- "dotnet" - info.WorkingDirectory <- (Directory.GetParent project).FullName + info.WorkingDirectory <- outputMultiNode info.Arguments <- arguments) (TimeSpan.FromMinutes 90.0) ResultHandling.failBuildIfXUnitReportedError TestRunnerErrorLevel.Error result CreateDir outputMultiNode - projects |> Seq.iter ( fun project -> - try - runSingleProject project - with - ex -> - raise (Exception(sprintf "Exception thrown while testing %s" project, ex)) - ) + projectDlls |> Seq.iter ( fun projectDll -> + runSingleProject projectDll + ) ) + Target "NBench" (fun _ -> ensureDirectory outputPerfTests let projects = From c219bcc52f85c1eda9623f6299bdd2fa2b100880 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Sat, 8 Jan 2022 00:46:40 +0700 Subject: [PATCH 19/30] Bump Akka.MultiNode.TestAdapter from 1.1.0-beta2 to 1.1.0 (#5494) --- src/common.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.props b/src/common.props index df4128a8b1b..1518b6e3f22 100644 --- a/src/common.props +++ b/src/common.props @@ -25,7 +25,7 @@ 2.16.3 2.0.3 4.7.0 - 1.1.0-beta2 + 1.1.0 akka;actors;actor model;Akka;concurrency From 4ba2443f870d4d45a257eb4de075950bc08140e8 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Sat, 8 Jan 2022 01:20:57 +0700 Subject: [PATCH 20/30] Add documentation on the new Akka.MultiNode.TestAdapter package (#5490) * Add documentation on the new Akka.MultiNode.TestAdapter package * Fix docfx.json, remove removed page from toc.yml * Fix linter errors * Modernize the old documentation to use the [!code] tag Co-authored-by: Aaron Stannard --- .../networking/multi-node-test-kit.html | 10 + .../networking/multi-node-test-kit.md | 298 ------------------ .../testing/multi-node-testing-old.md | 241 ++++++++++++++ docs/articles/testing/multi-node-testing.md | 181 +++++------ docs/articles/toc.yml | 2 - docs/docfx.json | 8 +- .../RestartNode2Spec.cs | 20 +- 7 files changed, 352 insertions(+), 408 deletions(-) create mode 100644 docs/articles/networking/multi-node-test-kit.html delete mode 100644 docs/articles/networking/multi-node-test-kit.md create mode 100644 docs/articles/testing/multi-node-testing-old.md diff --git a/docs/articles/networking/multi-node-test-kit.html b/docs/articles/networking/multi-node-test-kit.html new file mode 100644 index 00000000000..219332a8cf5 --- /dev/null +++ b/docs/articles/networking/multi-node-test-kit.html @@ -0,0 +1,10 @@ + + + + Multi-Node TestKit + + + + + + diff --git a/docs/articles/networking/multi-node-test-kit.md b/docs/articles/networking/multi-node-test-kit.md deleted file mode 100644 index 71bfdcd5556..00000000000 --- a/docs/articles/networking/multi-node-test-kit.md +++ /dev/null @@ -1,298 +0,0 @@ ---- -uid: multi-node-test-kit -title: Multi-Node TestKit ---- - -# Using the MultiNode TestKit - -If you intend to contribute to any of the high availability modules in Akka.NET, such as Akka.Remote and Akka.Cluster, you will need to familiarize yourself with the MultiNode Testkit and the test runner. - -The MultiNodeTestkit consists of three binaries within Akka.NET: - -* [`Akka.MultiNodeTestRunner`](https://github.com/akkadotnet/akka.net/tree/dev/src/core/Akka.MultiNodeTestRunner) - custom Xunit2 test runner for executing the specs. -* [`Akka.NodeTestRunner`](https://github.com/akkadotnet/akka.net/tree/dev/src/core/Akka.NodeTestRunner) - test runner for an individual node process launched by `Akka.MultiNodeTestRunner`. -* [`Akka.Remote.TestKit`](https://github.com/akkadotnet/akka.net/tree/dev/src/core/Akka.Remote.TestKit) - the MultiNode TestKit itself. - -## MultiNode Specs - -The multi node specs are different from traditional specs in that they are intended to run across multiple machines in parallel, to simulate multiple logical nodes participating in a network or cluster. - -Here's an example of a multi node spec from the Akka.Cluster.Tests project: - -```csharp -public class JoinInProgressMultiNodeConfig : MultiNodeConfig -{ - public RoleName First { get; } - public RoleName Second { get; } - - public JoinInProgressMultiNodeConfig() - { - First = Role("first"); - Second = Role("second"); - - CommonConfig = MultiNodeLoggingConfig.LoggingConfig.WithFallback(DebugConfig(true)) - .WithFallback(ConfigurationFactory.ParseString(@" - akka.stdout-loglevel = DEBUG - akka.cluster { - # simulate delay in gossip by turning it off - gossip-interval = 300 s - failure-detector { - threshold = 4 - acceptable-heartbeat-pause = 1 second - } - }").WithFallback(MultiNodeClusterSpec.ClusterConfig())); - - NodeConfig(new List { First }, new List - { - ConfigurationFactory.ParseString("akka.cluster.roles =[frontend]") - }); - NodeConfig(new List { Second }, new List - { - ConfigurationFactory.ParseString("akka.cluster.roles =[backend]") - }); - } -} - -public class JoinInProgressSpec : MultiNodeClusterSpec -{ - readonly JoinInProgressMultiNodeConfig _config; - - public JoinInProgressSpec() : this(new JoinInProgressMultiNodeConfig()) - { - } - - private JoinInProgressSpec(JoinInProgressMultiNodeConfig config) : base(config) - { - _config = config; - } - - [MultiNodeFact] - public void AClusterNodeMustSendHeartbeatsImmediatelyWhenJoiningToAvoidFalseFailureDetectionDueToDelayedGossip() - { - RunOn(StartClusterNode, _config.First); - - EnterBarrier("first-started"); - - RunOn(() => Cluster.Join(GetAddress(_config.First)), _config.Second); - - RunOn(() => - { - var until = Deadline.Now + TimeSpan.FromSeconds(5); - while (!until.IsOverdue) - { - Thread.Sleep(200); - Assert.True(Cluster.FailureDetector.IsAvailable(GetAddress(_config.Second))); - } - }, _config.First); - - EnterBarrier("after"); - } -} -``` - -The `MultiNodeFact` attribute is what's used to distinguish a multi-node spec from a typical spec, so you'll need to decorate your multi-node specs with this attribute. - -### Designing a MultiNode Spec - -A multi-node spec gives us the ability to do the following: - -1. Launch multiple independent processes each running their own `ActorSystem`; -2. Define individual configurations for each node; -3. Run specific commands on individual nodes or groups of nodes; -4. Create barriers that are used to synchronize nodes at specific points within a test; and -5. Test assertions across one or more nodes. - -> [!NOTE] -> Everything that's available in the default `Akka.TestKit` is also available inside the `Akka.Remote.TestKit`, but it's worth bearing in mind that `Akka.Remote.TestKit` only works with the `Akka.MultiNodeTestRunner` and uses Xunit 2.0 internally. - -#### Step 1 - Subclass `MultiNodeConfig` - -The first thing to do is define a configuration for each node you want to include in the test, so in order to do that we have to create a test-specific implementation of `MultiNodeConfig`. - -```csharp -public class JoinInProgressMultiNodeConfig : MultiNodeConfig -{ - public RoleName First { get; } - public RoleName Second { get; } - - public JoinInProgressMultiNodeConfig() - { - First = Role("first"); - Second = Role("second"); - - CommonConfig = MultiNodeLoggingConfig.LoggingConfig.WithFallback(DebugConfig(true)) - .WithFallback(ConfigurationFactory.ParseString(@" - akka.stdout-loglevel = DEBUG - akka.cluster { - # simulate delay in gossip by turning it off - gossip-interval = 300 s - failure-detector { - threshold = 4 - acceptable-heartbeat-pause = 1 second - } - }").WithFallback(MultiNodeClusterSpec.ClusterConfig())); - - - NodeConfig(new List { First }, new List - { - ConfigurationFactory.ParseString("akka.cluster.roles =[frontend]") - }); - NodeConfig(new List { Second }, new List - { - ConfigurationFactory.ParseString("akka.cluster.roles =[backend]") - }); - } -} -``` - -In the `JoinInProgressMultiNodeConfig`, we define two `RoleName`s for the two nodes who will be participating in this multi node spec, and then we define a `Config` object and have it set to the `CommonConfig` property, which is shared across all nodes. - -Also we configured each node to represent specific role `[frontend,backend]` in the cluster. You can attach arbitrary config instance(s) to individual node or group of nodes by calling `NodeConfig(IEnumerable roles, IEnumerable configs)`. - -#### Step 2 - Define a Class for Your Spec, Inherit From `MultiNodeSpec` - -The next step is to subclass `MultiNodeSpec` and create a class that each of your individual nodes will run. - -```csharp -public class JoinInProgressSpec : MultiNodeClusterSpec -{ - readonly JoinInProgressMultiNodeConfig _config; - - public JoinInProgressSpec() : this(new JoinInProgressMultiNodeConfig()) - { - } - - private JoinInProgressSpec(JoinInProgressMultiNodeConfig config) : base(config) - { - _config = config; - } -} -``` - -Decorate each of the independent tests with the `MultiNodeFact` attribute - the `MultiNodeTestRunner` will pick these up once it runs. - -You'll need to pass in a copy of your `MultiNodeConfig` object into the constructor of your base class, like this: - -```csharp -protected JoinInProgressSpec() : this(new JoinInProgressMultiNodeConfig()) -{ -} - -private JoinInProgressSpec(JoinInProgressMultiNodeConfig config) : base(config) -{ - _config = config; -} -``` - -The second constructor overload can be used for allowing individual nodes to run with non-shared configurations. - -#### Step 3 - Write the Actual Test Methods - -Decorate each of the independent tests with the `MultiNodeFact` attribute - the `MultiNodeTestRunner` will pick these up once it runs. - -```csharp -public class JoinInProgressSpec : MultiNodeClusterSpec -{ - readonly JoinInProgressMultiNodeConfig _config; - - public JoinInProgressSpec() : this(new JoinInProgressMultiNodeConfig()) - { - } - - private JoinInProgressSpec(JoinInProgressMultiNodeConfig config) : base(config) - { - _config = config; - } - - [MultiNodeFact] - public void AClusterNodeMustSendHeartbeatsImmediatelyWhenJoiningToAvoidFalseFailureDetectionDueToDelayedGossip() - { - RunOn(StartClusterNode, _config.First); - - EnterBarrier("first-started"); - - RunOn(() => Cluster.Join(GetAddress(_config.First)), _config.Second); - - RunOn(() => - { - var until = Deadline.Now + TimeSpan.FromSeconds(5); - while (!until.IsOverdue) - { - Thread.Sleep(200); - Assert.True(Cluster.FailureDetector.IsAvailable(GetAddress(_config.Second))); - } - }, _config.First); - - EnterBarrier("after"); - } -} -``` - -So a couple of special methods to pay attention to.... - -* `RunOn(Action thunk, params RoleName[] roles)` - this will run a method ONLY on the specified `roles`. -* `EnterBarrier(string barrierName)` - this creates a named barrier and waits for all nodes to synchronize on this barrier before moving onto the next portion of the spec. - -There's also the `TestConductor` property, which you can use for doing things like disconnecting a node from the spec: - -```csharp - public void AClusterOf3MembersMustNotReachConvergenceWhileAnyNodesAreUnreachable() -{ - var thirdAddress = GetAddress(_config.Third); - EnterBarrier("before-shutdown"); - - RunOn(() => - { - //kill 'third' node - TestConductor.Exit(_config.Third, 0).Wait(); - MarkNodeAsUnavailable(thirdAddress); - }, _config.First); - - RunOn(() => Within(TimeSpan.FromSeconds(28), () => - { - //third becomes unreachable - AwaitAssert(() => ClusterView.UnreachableMembers.Count.ShouldBe(1)); - AwaitSeenSameState(GetAddress(_config.First), GetAddress(_config.Second)); - // still one unreachable - ClusterView.UnreachableMembers.Count.ShouldBe(1); - ClusterView.UnreachableMembers.First().Address.ShouldBe(thirdAddress); - ClusterView.Members.Count.ShouldBe(3); - }), _config.First, _config.Second); - - EnterBarrier("after-2"); -} -``` - -If you have multiple phases that need to be executed as part of a test, you can write them like this: - -```csharp -[MultiNodeFact] -public void ConvergenceSpecTests() -{ - AClusterOf3MembersMustReachInitialConvergence(); - AClusterOf3MembersMustNotReachConvergenceWhileAnyNodesAreUnreachable(); - AClusterOf3MembersMustNotMoveANewJoiningNodeToUpWhileThereIsNoConvergence(); -} -``` - -This unfortunate design is a byproduct of Xunit and how it recreates the entire test class on each method. - -### Running MultiNode Specs - -To actually run this specification, we have to execute the `Akka.MultiNodeTestRunner.exe` against the .DLL that contains our specs. - -Here's the set of arguments that the MultiNodeTestRunner takes: - -* `Akka.MultiNodeTestRunner.exe path-to-dll` with path to DLL containing tests -* `-Dmultinode.enable-filesink=(on|off)` writes test output to disk -* `-Dmultinode.spec=("fully qualified spec method name)` executes a specific test method instead of all of them - -Here's an example of what invoking the test runner might look like if all of our multi-node tests were packaged into `Akka.MultiNodeTests.dll`. - -```cmd -Akka.MultiNodeTestRunner.exe "Akka.MultiNodetests.dll" -Dmultinode.enable-filesink=on -``` - -The output of a multi node test run will include the results for each specification for every node participating in the test. Here's a sample of what the final output at the end of a full test run looks like: - -![Akka.MultiNodeTestRunner.exe final output](/images/multinode-testkit-output.png) diff --git a/docs/articles/testing/multi-node-testing-old.md b/docs/articles/testing/multi-node-testing-old.md new file mode 100644 index 00000000000..6d2cb0d2ced --- /dev/null +++ b/docs/articles/testing/multi-node-testing-old.md @@ -0,0 +1,241 @@ +--- +uid: multi-node-testing-old +title: Multi-Node Testing Distributed Akka.NET Applications +--- + +# Multi-Node Testing Distributed Akka.NET Applications + +One of the most powerful testing features of Akka.NET is its ability to create and simulate real-world network conditions such as latency, network partitions, process crashes, and more. Given that any of these can happen in a production environment it's important to be able to write tests which validate your application's ability to correctly recover. + +This is precisely what the Multi-Node TestKit and TestRunner (MNTR) does in Akka.NET. + +## MNTR Components + +The Akka.NET Multi-Node TestKit consists of the following publicly available NuGet packages: + +* [Akka.MultiNodeTestRunner](https://www.nuget.org/packages/Akka.MultiNodeTestRunner) - the test runner that can launch a multi-node testing environment; +* [Akka.Remote.TestKit](https://www.nuget.org/packages/Akka.Remote.TestKit) - the base package used to create multi-node tests; and +* [Akka.Cluster.TestKit](https://www.nuget.org/packages/Akka.Cluster.TestKit) - a set of test helper methods for [Akka.Cluster](xref:cluster-overview) applications built on top of the Akka.Remote.TestKit. + +## How the MNTR Works + +The MultiNodeTestRunner works via the following process: + +1. Consumes a .DLL that has Akka.Remote.TestKit or Akka.Cluster.TestKit classes contained inside it; +2. For each detected multi-node test class, read that tests' configuration and build a corresponding network; +3. Run the test, including assertions, process barriers, and logging; +4. Provide a PASS/FAIL signal for each node participating in the test; +5. If any of the nodes failed, mark the entire test as failed; and +6. Write all of the output for each test and for each individual node in that test into its own output folder for review. + +![Akka.NET MultiNodeTestRunner Execution](/images/testing/mntr-execution.png) + +Given this architecture, let's wade into how to actually write and run a test using the MultiNodeTestRunner. + +## How to Write a Multi-Node Test + +The first step in writing a multi-node test is to install either the Akka.Remote.TestKit or the Akka.Cluster.TestKit NuGet package into your application. If you're working with Akka.Cluster, always just use the Akka.Cluster.TestKit package. + +Next, you need to plan out your test scenario for your application. Here's a simple one from the Akka.NET codebase itself we can use as an example: + +1. Form a two-node cluster, "Seed1" and "Seed2" as the role names, using both nodes as seed nodes; +2. Restart the first seed node ("Seed1") process; and +3. Verify that the restarted "Seed1" node is able to rejoin the same cluster as "Seed2." + +So this sounds like a rather complicated procedure, but in actuality the MNTR makes it easy for us to test scenarios just like these. + +### Step 1 - Create a Test Configuration + +The first step in creating an effective multi-node test is to define the configuration class for this test - this is going to tell the MNTR how many nodes there will need to be, how each node should be configured, and what features should be enabled for this unit test. + +[!code-csharp[RestartNode2Spec.cs](../../../src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs?name=MultiNodeSpecConfig)] + +The declaration of the `RoleName` properties is what the MNTR uses to determine how many nodes will be participating in this test. In this example, the test will create exactly two test processes. + +The `CommonConfig` element of the [`MultiNodeConfig` implementation class](../../api/Akka.Remote.TestKit.MultiNodeConfig.html) is the common config that will be used throughout all of the nodes inside the multi-node test. So, for instance, if you want all of the nodes in your test to run [Akka.Cluster.Sharding](xref:cluster-sharding) you'd want to include those configuration elements inside the `CommonConfig` property. + +#### Configuring Individual Nodes Differently + +In addition to passing a `CommonConfig` object throughout all nodes in your multi-node test, you can also provide configurations for individual nodes during each test. + +For example: if you're taking advantage of the `akka.cluster.roles` property to have some nodes execute different workloads than others, this might be something you'd want to specify for nodes individually. + +The `NodeConfig` method allows you to do just that: + +```c# +NodeConfig( + new List { First }, + new List { ConfigurationFactory.ParseString("akka.cluster.roles =[""a"", ""c""]") }); +NodeConfig( + new List { Second, Third }, + new List { ConfigurationFactory.ParseString("akka.cluster.roles =[""b"", ""c""]") }); +``` + +Right after setting `CommonConfig` inside the constructor of your `MultiNodeConfig` class you can call `NodeConfig` for the specified `RoleName`s and each of them will have their `Config`s added to their `ActorSystem` configurations at startup. + +> [!NOTE] +> `NodeConfig` takes precedent over `CommonConfig` + +#### Enabling TestTransport to Simulate Network Errors + +One final but important thing you might want to during the design of a multi-node test is to enable the `TestTransport`, which exposes a capability inside your tests that allows for you to create network partitions, disconnects, and latency on the fly. + +[!code-csharp[SurviveNetworkInstabilitySpec.cs](../../../src/core/Akka.Cluster.Tests.MultiNode/SurviveNetworkInstabilitySpec.cs?name=MultiNodeSpecConfig)] + +To enable the `TestTransport`, all you have to do is set `TestTransport = true` inside the `MultiNodeConfig` constructor. + +Once that's done, you'll be able to use the `TestConductor` inside your multi-node tests to enable all kinds of simulated network partitions. + +### Step 2 - Create Your MultiNodeClusterSpec or MultiNodeSpec + +Once you've created your `MultiNodeConfig`, you'll want to create a `MultiNodeClusterSpec` if you're using Akka.Cluster or a `MultiNodeSpec` if you just want to use Akka.Remote. + +We're going to show you a full code sample first and walk through how it works in detail below. + +[!code-csharp[RestartNode2Spec.cs](../../../src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs?name=MultiNodeSpec)] + +First, take note of the default public constructor: + +```c# +public RestartNode2Spec() : this(new RestartNode2SpecConfig()) { } +``` + +This is an XUnit restriction - there can only be one public constructor per class, and you need to pass in your `MultiNodeConfig` to the base class constructor, which is exactly what we do in the `protected` constructor. + +[!code-csharp[RestartNode2Spec.cs](../../../src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs?name=ProtectedConstructor)] + +We're going to hang onto a copy of our `RestartNode2SpecConfig` class in a field called `_config`, which will be helpful when we need to look up `RoleName`s later. + +Finally, we need to create our test method and decorate it with the `MultiNodeFact` attribute: + +[!code-csharp[RestartNode2Spec.cs](../../../src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs?name=MultiNodeFact)] + +This method is what will be executed by the multi-node test runner. + +#### Addressing Nodes + +All nodes in the multi-node test runner are going to be given randomized addresses and ports - thus we can never predict those addresses at the time we design our tests. Therefore, the way we always refer to nodes is by their `RoleName`s. + +If we want to resolve the Akka.NET `Address` of a specific node, we can do this via the `GetAddress` method: + +[!code-csharp[RestartNode2Spec.cs](../../../src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs?name=SeedNodesProperty)] + +The `GetAddress` method accepts a `RoleName` and returns the `Address` that was assigned to the node by the multi-node test runner. + +#### Running Code on Specific Nodes + +The most important tool in the `Akka.Remote.TestKit`, the base library where all multi-node testing tools are defined, is the `RunOn` method: + +[!code-csharp[RestartNode2Spec.cs](../../../src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs?name=RunOnSample)] + +Notice that the first `RunOn` call takes an argument of `_config.Seed2`, whereas the second `RunOn` call takes an argument of `_config.Seed1`. The code in the first `RunOn` block will only execute on the node with `RoleName` "Seed2" and the code in the second block will only run on `RoleName` "Seed1." + +Given that each node runs inside its own process, it's likely that both of these blocks of code will be executing simultaneously. + +#### Synchronizing Test Progression on Different Nodes + +In order to make multi-node tests effective, we must have some means of synchronizing all of the nodes in each test - such that they all reach the same assertions at the same time. This is precisely what the `EnterBarrier` method helps us do, as you can see in the code sample above. + +`EnterBarrier` creates a synchronization barrier between processes - no processes can advance past it until all processes have reached it. If one process fails to reach the barrier within 30 seconds (this is configurable via the [Akka.Remote.TestKit reference configuration](https://github.com/akkadotnet/akka.net/blob/dev/src/core/Akka.Remote.TestKit/Internals/Reference.conf)), the test will throw an assertion error and fail. + +#### Terminating, Aborting, and Disconnecting Nodes + +One of the most useful features of the multi-node testkit is its ability to simulate real-world networking issues, and this can be accomplished using some of the APIs found in the Akka.Remote.TestKit. + +**Creating Network Partitions** +In order to create a network partition between two or more nodes, the `TestTransport` must be enabled inside the `MultiNodeConfig` class constructor. This allows access to the `TestConductor`, which can be used to render two or more nodes unreachable: + +```c# +RunOn(() => +{ + TestConductor.Blackhole(_config.First, _config.Second, + ThrottleTransportAdapter.Direction.Both).Wait(); +}, _config.First); +EnterBarrier("blackhole-2"); +``` + +In this example, the `TestConductor.Blackhole` method is used to create 100% packet loss between the `RoleName` "First" and "Second". Those two nodes will still be running as part of the test, but they won't be able to communicate with each other over Akka.Remote. + +The `Task` returned by `TestConductor.Blackhole` will complete once the Akka.Remote transport has enabled "blackhole" mode for that connection, which usually doesn't take longer than a few milliseconds. + +To stop black-holing these nodes, we'd need to call the `TestConductor.PassThrough` method on these same two `RoleName` instances: + +```c# +RunOn(() => +{ + TestConductor.PassThrough(_config.First, _config.Second, + ThrottleTransportAdapter.Direction.Both).Wait(); +}, _config.First); +EnterBarrier("repair-2"); +``` + +This will allow Akka.Remote to resume normal execution over the network. + +**Killing Nodes** +There are two ways to kill a node in a running multi-node test. + +The first is to call the `Shutdown` method on the `ActorSystem` of the node you wish to have exit the test. This will cause the `ActorSystem` to terminate gracefully - this simulates the planned shutdown of a node. + +```c# +// shutdown seed1System +RunOn(() => +{ + Shutdown(seed1System.Value, RemainingOrDefault); +}, _config.Seed1); +EnterBarrier("seed1-shutdown"); +``` + +The other way to shutdown a node is to use the `TestConductor.Exit` command - this is intended to simulate the _unplanned_ shutdown of a node, i.e. a process crash. + +```c# +RunOn(() => { + TestConductor.Exit(_config.Third, 0).Wait(); +}, _config.First); +``` + +Once a node has exited the test, it will no longer be able to wait on `EnterBarrier` calls and the multi-node test runner will not try to collect any data from that node from that point onward. + +## Running Multi-Node Tests + +Once you've coded your multi-node tests and compiled them, it's now time to run them. Akka.NET ships a custom XUnit2 runner that it uses to create the simulated networks and clusters and you will need to install that via NuGet in order to run your tests: + +```console +PS> nuget.exe Install-Package Akka.MultiNodeTestRunner -NoVersion +``` + +This will install the [Akka.MultiNodeTestRunner NuGet package](https://www.nuget.org/packages/Akka.MultiNodeTestRunner) with the following directory and file structure: + +```text +root/akka.multinodetestrunner +root/akka.multinodetestrunner/lib/net452/Akka.MultiNodeTestRunner.exe +root/akka.multinodetestrunner/lib/netcoreapp1.1/Akka.MultiNodeTestRunner.dll +``` + +Depending on what framework you're building your application against, you'll want to pick the appropriate tool (.NET Framework or .NET Core.) + +Next, we have to pass in our command-line arguments to the MNTR: + +```console +Akka.MultiNodeTestRunner.exe [path to assembly] [-Dmultinode.enable-filesink=on] [-Dmultinode.output-directory={dir path}] [-Dmultinode.spec={spec name}] +``` + +We strongly recommend setting the `-Dmultinode.output-directory={dir path}` directory to some local folder you can access, as the multi-node test runner will emit: + +1. An output file for the entire test run of the DLL and +2. For each individual spec, a subfolder that contains logs pertaining to the original node. + +_Hint:_ Each test run will append new log entries to output files. +If this is not desired, you can pass `-Dmultinode.clear-output=1` option to delete output folder before MNTR will run tests. + +If you're lost and need more examples, please explore the Akka.NET source code and take a look at some of the MNTR output produced by our CI system on any open pull request. + +## Debugging Failed Tests + +As already mentioned, after each spec is finished, test runner wil emit log files for it to output directory subfolder with the full name of the spec. +In this folder you will find individual logs for each node (named according to the roles they were assigned), and `aggregated.txt` file, +which contains all nodes logs aggregated into single timeline. + +Also, `FAILED_SPECS_LOGS` subdirectory will be generated. If any of your specs failed, this folder will contain aggregated logs for each spec - +basically, the same `aggregated.txt` files but with their spec's names. This is a good place to get a full picture of what has failed and why. + +Also, `-Dmultinode.failed-specs-directory={failed spec dir}` option could be used to override `FAILED_SPECS_LOGS` name. diff --git a/docs/articles/testing/multi-node-testing.md b/docs/articles/testing/multi-node-testing.md index eec62912d68..9ec3a555152 100644 --- a/docs/articles/testing/multi-node-testing.md +++ b/docs/articles/testing/multi-node-testing.md @@ -5,6 +5,11 @@ title: Multi-Node Testing Distributed Akka.NET Applications # Multi-Node Testing Distributed Akka.NET Applications +> [!NOTE] +> This documentation applies to the new `Akka.MultiNode.TestAdapter` multi-node test NuGet package. +> +> For the old `Akka.MultiNodeTestRunner` package, please check the [old documentation](xref:multi-node-testing-old) + One of the most powerful testing features of Akka.NET is its ability to create and simulate real-world network conditions such as latency, network partitions, process crashes, and more. Given that any of these can happen in a production environment it's important to be able to write tests which validate your application's ability to correctly recover. This is precisely what the Multi-Node TestKit and TestRunner (MNTR) does in Akka.NET. @@ -13,28 +18,28 @@ This is precisely what the Multi-Node TestKit and TestRunner (MNTR) does in Akka The Akka.NET Multi-Node TestKit consists of the following publicly available NuGet packages: -* [Akka.MultiNodeTestRunner](https://www.nuget.org/packages/Akka.MultiNodeTestRunner) - the test runner that can launch a multi-node testing environment; +* [Akka.MultiNode.TestAdapter](https://www.nuget.org/packages/Akka.MultiNode.TestAdapter) - the Xunit test framework adapter that can launch a multi-node testing environment; * [Akka.Remote.TestKit](https://www.nuget.org/packages/Akka.Remote.TestKit) - the base package used to create multi-node tests; and * [Akka.Cluster.TestKit](https://www.nuget.org/packages/Akka.Cluster.TestKit) - a set of test helper methods for [Akka.Cluster](xref:cluster-overview) applications built on top of the Akka.Remote.TestKit. ## How the MNTR Works -The MultiNodeTestRunner works via the following process: +The multi node test runner inside the TestAdapter works via the following process: -1. Consumes a .DLL that has Akka.Remote.TestKit or Akka.Cluster.TestKit classes contained inside it; +1. Consumes a .DLL that references the `Akka.MultiNode.TestAdapter` NuGet package; 2. For each detected multi-node test class, read that tests' configuration and build a corresponding network; 3. Run the test, including assertions, process barriers, and logging; 4. Provide a PASS/FAIL signal for each node participating in the test; 5. If any of the nodes failed, mark the entire test as failed; and 6. Write all of the output for each test and for each individual node in that test into its own output folder for review. -![Akka.NET MultiNodeTestRunner Execution](/images/testing/mntr-execution.png) +![Akka.NET MultiNodeTestRunner Execution](../../images/testing/mntr-execution.png) -Given this architecture, let's wade into how to actually write and run a test using the MultiNodeTestRunner. +Given this architecture, let's wade into how to actually write and run a test using the multi node test adapter. ## How to Write a Multi-Node Test -The first step in writing a multi-node test is to install either the Akka.Remote.TestKit or the Akka.Cluster.TestKit NuGet package into your application. If you're working with Akka.Cluster, always just use the Akka.Cluster.TestKit package. +The first step in writing a multi-node test is to install either the `Akka.Remote.TestKit` or the `Akka.Cluster.TestKit` NuGet package into your application. If you're working with `Akka.Cluster`, you only need to use the `Akka.Cluster.TestKit` package. Next, you need to plan out your test scenario for your application. Here's a simple one from the Akka.NET codebase itself we can use as an example: @@ -42,7 +47,7 @@ Next, you need to plan out your test scenario for your application. Here's a sim 2. Restart the first seed node ("Seed1") process; and 3. Verify that the restarted "Seed1" node is able to rejoin the same cluster as "Seed2." -So this sounds like a rather complicated procedure, but in actuality the MNTR makes it easy for us to test scenarios just like these. +So this sounds like a rather complicated procedure, but in actuality the TestKit makes it easy for us to test scenarios just like these. ### Step 1 - Create a Test Configuration @@ -62,13 +67,13 @@ For example: if you're taking advantage of the `akka.cluster.roles` property to The `NodeConfig` method allows you to do just that: -```csharp -NodeConfig(new List { First }, - new List { ConfigurationFactory.ParseString( - @"akka.cluster.roles =[""a"", ""c""]") }); -NodeConfig(new List { Second, Third }, - new List { ConfigurationFactory.ParseString( - @"akka.cluster.roles =[""b"", ""c""]") }); +```c# +NodeConfig( + new List { First }, + new List { ConfigurationFactory.ParseString("akka.cluster.roles =[""a"", ""c""]") }); +NodeConfig( + new List { Second, Third }, + new List { ConfigurationFactory.ParseString("akka.cluster.roles =[""b"", ""c""]") }); ``` Right after setting `CommonConfig` inside the constructor of your `MultiNodeConfig` class you can call `NodeConfig` for the specified `RoleName`s and each of them will have their `Config`s added to their `ActorSystem` configurations at startup. @@ -96,36 +101,17 @@ We're going to show you a full code sample first and walk through how it works i First, take note of the default public constructor: -```csharp -public RestartNode2Spec() : this(new RestartNode2SpecConfig()) { } -``` +[!code-csharp[RestartNode2Spec.cs](../../../src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs?name=PublicConstructor)] This is an XUnit restriction - there can only be one public constructor per class, and you need to pass in your `MultiNodeConfig` to the base class constructor, which is exactly what we do in the `protected` constructor. -```csharp -protected RestartNode2Spec(RestartNode2SpecConfig config) : base(config, typeof(RestartNode2Spec)) -{ - _config = config; - seed1System = new Lazy(() => ActorSystem.Create(Sys.Name, - Sys.Settings.Config)); - restartedSeed1System = new Lazy( - () => ActorSystem.Create(Sys.Name, ConfigurationFactory - .ParseString("akka.remote.netty.tcp.port = " + SeedNodes.First().Port) - .WithFallback(Sys.Settings.Config))); -} -``` +[!code-csharp[RestartNode2Spec.cs](../../../src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs?name=ProtectedConstructor)] We're going to hang onto a copy of our `RestartNode2SpecConfig` class in a field called `_config`, which will be helpful when we need to look up `RoleName`s later. Finally, we need to create our test method and decorate it with the `MultiNodeFact` attribute: -```csharp -[MultiNodeFact] -public void RestartNode2Specs() -{ - Cluster_seed_nodes_must_be_able_to_restart_first_seed_node_and_join_other_seed_nodes(); -} -``` +[!code-csharp[RestartNode2Spec.cs](../../../src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs?name=MultiNodeFact)] This method is what will be executed by the multi-node test runner. @@ -135,15 +121,7 @@ All nodes in the multi-node test runner are going to be given randomized address If we want to resolve the Akka.NET `Address` of a specific node, we can do this via the `GetAddress` method: -```csharp -private ImmutableList
SeedNodes -{ - get - { - return ImmutableList.Create(seedNode1Address, GetAddress(_config.Seed2)); - } -} -``` +[!code-csharp[RestartNode2Spec.cs](../../../src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs?name=SeedNodesProperty)] The `GetAddress` method accepts a `RoleName` and returns the `Address` that was assigned to the node by the multi-node test runner. @@ -151,27 +129,7 @@ The `GetAddress` method accepts a `RoleName` and returns the `Address` that was The most important tool in the `Akka.Remote.TestKit`, the base library where all multi-node testing tools are defined, is the `RunOn` method: -```csharp -RunOn(() => -{ - // seed1System is a separate ActorSystem, to be able to simulate restart - // we must transfer its address to seed2 - Sys.ActorOf(Props.Create().WithDeploy(Deploy.Local), "address-receiver"); - EnterBarrier("seed1-address-receiver-ready"); -}, _config.Seed2); - - -RunOn(() => -{ - EnterBarrier("seed1-address-receiver-ready"); - seedNode1Address = Cluster.Get(seed1System.Value).SelfAddress; - foreach (var r in ImmutableList.Create(_config.Seed2)) - { - Sys.ActorSelection(new RootActorPath(GetAddress(r)) / "user" / "address-receiver").Tell(seedNode1Address); - ExpectMsg("ok", TimeSpan.FromSeconds(5)); - } -}, _config.Seed1); -``` +[!code-csharp[RestartNode2Spec.cs](../../../src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs?name=RunOnSample)] Notice that the first `RunOn` call takes an argument of `_config.Seed2`, whereas the second `RunOn` call takes an argument of `_config.Seed1`. The code in the first `RunOn` block will only execute on the node with `RoleName` "Seed2" and the code in the second block will only run on `RoleName` "Seed1." @@ -187,10 +145,11 @@ In order to make multi-node tests effective, we must have some means of synchron One of the most useful features of the multi-node testkit is its ability to simulate real-world networking issues, and this can be accomplished using some of the APIs found in the Akka.Remote.TestKit. -**Creating Network Partitions** +##### Creating Network Partitions + In order to create a network partition between two or more nodes, the `TestTransport` must be enabled inside the `MultiNodeConfig` class constructor. This allows access to the `TestConductor`, which can be used to render two or more nodes unreachable: -```csharp +```c# RunOn(() => { TestConductor.Blackhole(_config.First, _config.Second, @@ -205,7 +164,7 @@ The `Task` returned by `TestConductor.Blackhole` will complete once the Akka.Rem To stop black-holing these nodes, we'd need to call the `TestConductor.PassThrough` method on these same two `RoleName` instances: -```csharp +```c# RunOn(() => { TestConductor.PassThrough(_config.First, _config.Second, @@ -216,12 +175,13 @@ EnterBarrier("repair-2"); This will allow Akka.Remote to resume normal execution over the network. -**Killing Nodes** +##### Killing Nodes + There are two ways to kill a node in a running multi-node test. The first is to call the `Shutdown` method on the `ActorSystem` of the node you wish to have exit the test. This will cause the `ActorSystem` to terminate gracefully - this simulates the planned shutdown of a node. -```csharp +```c# // shutdown seed1System RunOn(() => { @@ -232,7 +192,7 @@ EnterBarrier("seed1-shutdown"); The other way to shutdown a node is to use the `TestConductor.Exit` command - this is intended to simulate the _unplanned_ shutdown of a node, i.e. a process crash. -```csharp +```c# RunOn(() => { TestConductor.Exit(_config.Third, 0).Wait(); }, _config.First); @@ -240,47 +200,74 @@ RunOn(() => { Once a node has exited the test, it will no longer be able to wait on `EnterBarrier` calls and the multi-node test runner will not try to collect any data from that node from that point onward. -## Running Multi-Node Tests +## Setting Up The Multi-Node Test Project + +The `Akka.MultiNode.TestAdapter` NuGet package is a test adapter based on the popular XUnit that is compatible with `dotnet test`, Microsoft Visual Studio, and JetBrains Rider. + +### Turn Off Xunit Parallelization -Once you've coded your multi-node tests and compiled them, it's now time to run them. Akka.NET ships a custom XUnit2 runner that it uses to create the simulated networks and clusters and you will need to install that via NuGet in order to run your tests: +It is advised that you don't use any test parallelization when running multi node tests. To do this, you can do either of these methods. -```console -PS> nuget.exe Install-Package Akka.MultiNodeTestRunner -NoVersion +#### Using The Convenience Custom Test Framework + +`Akka.MultiNode.TestAdapter` came with a convenience test framework that automatically ignores Xunit collection parallelization feature. To use it, you would need to declare a single assembly level attribute inside your project: + +```c# +using Xunit; + +[assembly: TestFramework("Akka.MultiNode.TestAdapter.MultiNodeTestFramework", "Akka.MultiNode.TestAdapter")] ``` -This will install the [Akka.MultiNodeTestRunner NuGet package](https://www.nuget.org/packages/Akka.MultiNodeTestRunner) with the following directory and file structure: +#### Using Xunit Configuration File + +You can manually set Xunit to turn off collection parallelization by including a special json configuration file named `xunit.runner.json` inside your project. Make sure that the file will be copied to the output directory during build. -```text -root/akka.multinodetestrunner -root/akka.multinodetestrunner/lib/net452/Akka.MultiNodeTestRunner.exe -root/akka.multinodetestrunner/lib/netcoreapp1.1/Akka.MultiNodeTestRunner.dll +```json +{ + "parallelizeTestCollections" : false, + "parallelizeAssembly" : false +} ``` -Depending on what framework you're building your application against, you'll want to pick the appropriate tool (.NET Framework or .NET Core.) +> [!NOTE] +> Be careful that this method might not be honored in some IDE. + +### Configuring MNTR Behavior -Next, we have to pass in our command-line arguments to the MNTR: +By default, MNTR will create a folder named `TestResults` inside the test assembly folder and write all test log output in there. To change this and other default behavior, you can include a json configuration named `xunit.multinode.runner.json` inside your project: -```console -Akka.MultiNodeTestRunner.exe [path to assembly] [-Dmultinode.enable-filesink=on] [-Dmultinode.output-directory={dir path}] [-Dmultinode.spec={spec name}] +```json +{ + "outputDirectory": "TestResults", + "failedSpecsDirectory": "FAILED_SPECS_LOGS", + "listenAddress": "127.0.0.1", + "listenPort": 0, + "clearOutputDirectory": false +} ``` -We strongly recommend setting the `-Dmultinode.output-directory={dir path}` directory to some local folder you can access, as the multi-node test runner will emit: +* **outputDirectory**: The directory path for all log output files. This can be a relative or an absolute folder path; if set as a relative path, it is relative to the test assembly folder. +* **failedSpecsDirectory**: Determines output directory name for aggregated failed test logs. This will be the name of a folder inside the output directory, not a path to a directory. +* **listenAddress**: Determines the address that this multi-node test runner will use to listen for log messages from individual spec. +* **listenPort**: Determines the port number that this multi-node test runner will use to listen for log messages from individual spec. +* **ClearOutputDirectory**: Clear the output directory before running the test session. If set to false, all test logs are appended to the out file. -1. An output file for the entire test run of the DLL and -2. For each individual spec, a subfolder that contains logs pertaining to the original node. +## Running Multi-Node Tests -_Hint:_ Each test run will append new log entries to output files. -If this is not desired, you can pass `-Dmultinode.clear-output=1` option to delete output folder before MNTR will run tests. +Once you've code your multi-node tests and compiled them, it's now time to run them. There are currently three officially supported way to run a multi-node test: + +1. .NET CLI `dotnet test` +2. Microsoft Visual Studio test runner +3. JetBrains Rider test runner + +A multi-node test can be treated just like any other unit tests with the exception that it could not be run in parallel. + +_Hint:_ Each test run will append new log entries to output files. If this is not desired, you can set the `clearOutputDirectory` option in the JSON setting file to true to delete previous output before writing new ones. If you're lost and need more examples, please explore the Akka.NET source code and take a look at some of the MNTR output produced by our CI system on any open pull request. ## Debugging Failed Tests -As already mentioned, after each spec is finished, test runner wil emit log files for it to output directory subfolder with the full name of the spec. -In this folder you will find individual logs for each node (named according to the roles they were assigned), and `aggregated.txt` file, -which contains all nodes logs aggregated into single timeline. - -Also, `FAILED_SPECS_LOGS` subdirectory will be generated. If any of your specs failed, this folder will contain aggregated logs for each spec - -basically, the same `aggregated.txt` files but with their spec's names. This is a good place to get a full picture of what has failed and why. +As already mentioned, after each spec is finished, test runner will emit log files for it to output directory subfolder with the full name of the spec. In this folder you will find individual logs for each node (named according to the roles they were assigned), and `aggregated.txt` file, which contains all nodes logs aggregated into single timeline. -Also, `-Dmultinode.failed-specs-directory={failed spec dir}` option could be used to override `FAILED_SPECS_LOGS` name. +Also, `FAILED_SPECS_LOGS` subdirectory will be generated. If any of your specs failed, this folder will contain aggregated logs for each spec - basically, the same `aggregated.txt` files but with their spec's names. This is a good place to get a full picture of what has failed and why. diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index e1af8a3c0e6..7f2a6e24a98 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -138,8 +138,6 @@ href: networking/io.md - name: Serialization href: networking/serialization.md - - name: Multi-Node TestKit - href: networking/multi-node-test-kit.md - name: Remoting items: - name: Overview diff --git a/docs/docfx.json b/docs/docfx.json index c7ed6a09b63..88cb66fd359 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -16,7 +16,7 @@ "src": "../src" }], "dest": "api", - "filter": "filterConfig.yml", + "filter": "filterConfig.yml" }], "build": { "content": [{ @@ -27,9 +27,7 @@ }, { "files": [ "articles/**.md", - "articles/**.html", "articles/**/toc.yml", - "community/**.html", "community/**.md", "community/**/toc.yml", "toc.yml", @@ -44,7 +42,9 @@ "files": [ "images/**", "web.config", - "xrefmap.yml" + "xrefmap.yml", + "articles/**.html", + "community/**.html" ], "exclude": [ "obj/**", diff --git a/src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs b/src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs index c111d34a384..99537c255be 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/RestartNode2Spec.cs @@ -65,16 +65,17 @@ public Watcher() private Lazy seed1System; private Lazy restartedSeed1System; private static Address seedNode1Address; - private ImmutableList
SeedNodes - { - get - { - return ImmutableList.Create(seedNode1Address, GetAddress(_config.Seed2)); - } - } + #region SeedNodesProperty + private ImmutableList
SeedNodes => + ImmutableList.Create(seedNode1Address, GetAddress(_config.Seed2)); + #endregion + + #region PublicConstructor public RestartNode2Spec() : this(new RestartNode2SpecConfig()) { } + #endregion + #region ProtectedConstructor protected RestartNode2Spec(RestartNode2SpecConfig config) : base(config, typeof(RestartNode2Spec)) { _config = config; @@ -84,6 +85,7 @@ protected RestartNode2Spec(RestartNode2SpecConfig config) : base(config, typeof( .ParseString("akka.remote.netty.tcp.port = " + SeedNodes.First().Port) .WithFallback(Sys.Settings.Config))); } + #endregion protected override void AfterTermination() { @@ -93,16 +95,19 @@ protected override void AfterTermination() }, _config.Seed1); } + #region MultiNodeFact [MultiNodeFact] public void RestartNode2Specs() { Cluster_seed_nodes_must_be_able_to_restart_first_seed_node_and_join_other_seed_nodes(); } + #endregion public void Cluster_seed_nodes_must_be_able_to_restart_first_seed_node_and_join_other_seed_nodes() { Within(TimeSpan.FromSeconds(60), () => { + #region RunOnSample RunOn(() => { // seed1System is a separate ActorSystem, to be able to simulate restart @@ -122,6 +127,7 @@ public void Cluster_seed_nodes_must_be_able_to_restart_first_seed_node_and_join_ ExpectMsg("ok", TimeSpan.FromSeconds(5)); } }, _config.Seed1); + #endregion EnterBarrier("seed1-address-transfered"); // now we can join seed1System, seed2 together From 2896372f8573b5b8844652e650e88741496500fc Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 11 Jan 2022 06:45:50 -0600 Subject: [PATCH 21/30] code cleanup on Akka.Persistence (#5497) --- .../Eventsourced.Lifecycle.cs | 70 ++++++++++++------- src/core/Akka.Persistence/Eventsourced.cs | 4 +- .../Journal/AsyncWriteJournal.cs | 18 +++-- 3 files changed, 53 insertions(+), 39 deletions(-) diff --git a/src/core/Akka.Persistence/Eventsourced.Lifecycle.cs b/src/core/Akka.Persistence/Eventsourced.Lifecycle.cs index 4cd10649223..1cefff8650f 100644 --- a/src/core/Akka.Persistence/Eventsourced.Lifecycle.cs +++ b/src/core/Akka.Persistence/Eventsourced.Lifecycle.cs @@ -63,10 +63,21 @@ public override void AroundPreRestart(Exception cause, object message) finally { object inner; - if (message is WriteMessageSuccess) inner = (message as WriteMessageSuccess).Persistent; - else if (message is LoopMessageSuccess) inner = (message as LoopMessageSuccess).Message; - else if (message is ReplayedMessage) inner = (message as ReplayedMessage).Persistent; - else inner = message; + switch (message) + { + case WriteMessageSuccess success: + inner = success.Persistent; + break; + case LoopMessageSuccess success: + inner = success.Message; + break; + case ReplayedMessage replayedMessage: + inner = replayedMessage.Persistent; + break; + default: + inner = message; + break; + } FlushJournalBatch(); base.AroundPreRestart(cause, inner); @@ -97,31 +108,36 @@ public override void AroundPostStop() /// protected override void Unhandled(object message) { - if (message is RecoveryCompleted) return; // ignore - if (message is SaveSnapshotFailure) + switch (message) { - var m = (SaveSnapshotFailure) message; - if (Log.IsWarningEnabled) - Log.Warning("Failed to SaveSnapshot given metadata [{0}] due to: [{1}: {2}]", m.Metadata, m.Cause, m.Cause.Message); - } - if (message is DeleteSnapshotFailure) - { - var m = (DeleteSnapshotFailure) message; - if (Log.IsWarningEnabled) - Log.Warning("Failed to DeleteSnapshot given metadata [{0}] due to: [{1}: {2}]", m.Metadata, m.Cause, m.Cause.Message); - } - if (message is DeleteSnapshotsFailure) - { - var m = (DeleteSnapshotsFailure) message; - if (Log.IsWarningEnabled) - Log.Warning("Failed to DeleteSnapshots given criteria [{0}] due to: [{1}: {2}]", m.Criteria, m.Cause, m.Cause.Message); - } - if (message is DeleteMessagesFailure) - { - var m = (DeleteMessagesFailure) message; - if (Log.IsWarningEnabled) - Log.Warning("Failed to DeleteMessages ToSequenceNr [{0}] for PersistenceId [{1}] due to: [{2}: {3}]", m.ToSequenceNr, PersistenceId, m.Cause, m.Cause.Message); + case RecoveryCompleted _: + return; // ignore + case SaveSnapshotFailure failure: + { + if (Log.IsWarningEnabled) + Log.Warning("Failed to SaveSnapshot given metadata [{0}] due to: [{1}: {2}]", failure.Metadata, failure.Cause, failure.Cause.Message); + break; + } + case DeleteSnapshotFailure failure: + { + if (Log.IsWarningEnabled) + Log.Warning("Failed to DeleteSnapshot given metadata [{0}] due to: [{1}: {2}]", failure.Metadata, failure.Cause, failure.Cause.Message); + break; + } + case DeleteSnapshotsFailure failure: + { + if (Log.IsWarningEnabled) + Log.Warning("Failed to DeleteSnapshots given criteria [{0}] due to: [{1}: {2}]", failure.Criteria, failure.Cause, failure.Cause.Message); + break; + } + case DeleteMessagesFailure failure: + { + if (Log.IsWarningEnabled) + Log.Warning("Failed to DeleteMessages ToSequenceNr [{0}] for PersistenceId [{1}] due to: [{2}: {3}]", failure.ToSequenceNr, PersistenceId, failure.Cause, failure.Cause.Message); + break; + } } + base.Unhandled(message); } } diff --git a/src/core/Akka.Persistence/Eventsourced.cs b/src/core/Akka.Persistence/Eventsourced.cs index 0e92a7fc666..ce79d622b39 100644 --- a/src/core/Akka.Persistence/Eventsourced.cs +++ b/src/core/Akka.Persistence/Eventsourced.cs @@ -628,9 +628,9 @@ private void StashInternally(object currentMessage) var sender = Sender; Context.System.DeadLetters.Tell(new DeadLetter(currentMessage, sender, Self), Sender); } - else if (strategy is ReplyToStrategy) + else if (strategy is ReplyToStrategy toStrategy) { - Sender.Tell(((ReplyToStrategy)strategy).Response); + Sender.Tell(toStrategy.Response); } else if (strategy is ThrowOverflowExceptionStrategy) { diff --git a/src/core/Akka.Persistence/Journal/AsyncWriteJournal.cs b/src/core/Akka.Persistence/Journal/AsyncWriteJournal.cs index 1cf19e7c8cf..c6e4d0a0dcc 100644 --- a/src/core/Akka.Persistence/Journal/AsyncWriteJournal.cs +++ b/src/core/Akka.Persistence/Journal/AsyncWriteJournal.cs @@ -309,8 +309,7 @@ private void HandleReplayMessages(ReplayMessages message) /// TBD protected Exception TryUnwrapException(Exception e) { - var aggregateException = e as AggregateException; - if (aggregateException != null) + if (e is AggregateException aggregateException) { aggregateException = aggregateException.Flatten(); if (aggregateException.InnerExceptions.Count == 1) @@ -354,21 +353,21 @@ private void HandleWriteMessages(WriteMessages message) writeResult = Task.FromResult((IImmutableList)Enumerable.Repeat(e, atomicWriteCount).ToImmutableList()); } - Action, IImmutableList> resequence = (mapper, results) => + void Resequence(Func mapper, IImmutableList results) { var i = 0; var enumerator = results != null ? results.GetEnumerator() : null; foreach (var resequencable in message.Messages) { - if (resequencable is AtomicWrite) + if (resequencable is AtomicWrite aw) { - var aw = resequencable as AtomicWrite; Exception exception = null; if (enumerator != null) { enumerator.MoveNext(); exception = enumerator.Current; } + foreach (var p in (IEnumerable)aw.Payload) { _resequencer.Tell(new Desequenced(mapper(p, exception), counter + i + 1, message.PersistentActor, p.Sender)); @@ -378,12 +377,11 @@ private void HandleWriteMessages(WriteMessages message) else { var loopMsg = new LoopMessageSuccess(resequencable.Payload, message.ActorInstanceId); - _resequencer.Tell(new Desequenced(loopMsg, counter + i + 1, message.PersistentActor, - resequencable.Sender)); + _resequencer.Tell(new Desequenced(loopMsg, counter + i + 1, message.PersistentActor, resequencable.Sender)); i++; } } - }; + } writeResult .ContinueWith(t => @@ -394,7 +392,7 @@ private void HandleWriteMessages(WriteMessages message) throw new IllegalStateException($"AsyncWriteMessages return invalid number or results. Expected [{atomicWriteCount}], but got [{t.Result.Count}]."); _resequencer.Tell(new Desequenced(WriteMessagesSuccessful.Instance, counter, message.PersistentActor, self)); - resequence((x, exception) => exception == null + Resequence((x, exception) => exception == null ? (object)new WriteMessageSuccess(x, message.ActorInstanceId) : new WriteMessageRejected(x, exception, message.ActorInstanceId), t.Result); } @@ -407,7 +405,7 @@ private void HandleWriteMessages(WriteMessages message) : new OperationCanceledException( "WriteMessagesAsync canceled, possibly due to timing out.")); _resequencer.Tell(new Desequenced(new WriteMessagesFailed(exception, atomicWriteCount), counter, message.PersistentActor, self)); - resequence((x, _) => new WriteMessageFailure(x, exception, message.ActorInstanceId), null); + Resequence((x, _) => new WriteMessageFailure(x, exception, message.ActorInstanceId), null); } }, _continuationOptions); } From 3706c6f2a96ee2de83313e1fc7bb642f9c4a6ec1 Mon Sep 17 00:00:00 2001 From: Ebere Abanonu Date: Wed, 12 Jan 2022 20:03:58 +0100 Subject: [PATCH 22/30] Added getting started to documentation (#5500) * * Created getting-started sub-directory * Created Tutorial Overview * Moved all the Tutorials into getting-started sub-directory of intro * Fix linting issues * Fix linting issues * Resolves: 1. https://github.com/akkadotnet/akka.net/pull/5500#discussion_r783132239 2. https://github.com/akkadotnet/akka.net/pull/5500#discussion_r783132239 --- docs/articles/intro/getting-started/toc.yml | 10 ++++ .../intro/{ => getting-started}/tutorial-1.md | 58 ++----------------- .../intro/{ => getting-started}/tutorial-2.md | 0 .../intro/{ => getting-started}/tutorial-3.md | 0 .../intro/{ => getting-started}/tutorial-4.md | 0 .../getting-started/tutorial-overview.md | 57 ++++++++++++++++++ docs/articles/intro/toc.yml | 12 ++++ docs/articles/intro/tutorial-1.html | 10 ++++ docs/articles/intro/tutorial-2.html | 10 ++++ docs/articles/intro/tutorial-3.html | 10 ++++ docs/articles/intro/tutorial-4.html | 10 ++++ docs/articles/toc.yml | 20 +------ src/common.props | 26 +-------- 13 files changed, 128 insertions(+), 95 deletions(-) create mode 100644 docs/articles/intro/getting-started/toc.yml rename docs/articles/intro/{ => getting-started}/tutorial-1.md (82%) rename docs/articles/intro/{ => getting-started}/tutorial-2.md (100%) rename docs/articles/intro/{ => getting-started}/tutorial-3.md (100%) rename docs/articles/intro/{ => getting-started}/tutorial-4.md (100%) create mode 100644 docs/articles/intro/getting-started/tutorial-overview.md create mode 100644 docs/articles/intro/toc.yml create mode 100644 docs/articles/intro/tutorial-1.html create mode 100644 docs/articles/intro/tutorial-2.html create mode 100644 docs/articles/intro/tutorial-3.html create mode 100644 docs/articles/intro/tutorial-4.html diff --git a/docs/articles/intro/getting-started/toc.yml b/docs/articles/intro/getting-started/toc.yml new file mode 100644 index 00000000000..de9b0aea4fa --- /dev/null +++ b/docs/articles/intro/getting-started/toc.yml @@ -0,0 +1,10 @@ +- name: Tutorial Overview + href: tutorial-overview.md +- name: Part 1. Top-level Architecture + href: tutorial-1.md +- name: Part 2. The Device Actor + href: tutorial-2.md +- name: Part 3. Device Groups and Manager + href: tutorial-3.md +- name: Part 4. Querying a Group of Devices + href: tutorial-4.md \ No newline at end of file diff --git a/docs/articles/intro/tutorial-1.md b/docs/articles/intro/getting-started/tutorial-1.md similarity index 82% rename from docs/articles/intro/tutorial-1.md rename to docs/articles/intro/getting-started/tutorial-1.md index 4e367d4bc4b..dc5ab0d4232 100644 --- a/docs/articles/intro/tutorial-1.md +++ b/docs/articles/intro/getting-started/tutorial-1.md @@ -3,53 +3,7 @@ uid: tutorial-1 title: Part 1. Top-level Architecture --- -# Part 1: Top-Level Architecture - -In this and the following chapters, we will build a sample Akka.NET application -to introduce you to the language of actors and how solutions can be formulated -with them. It is a common hurdle for beginners to translate their project into -actors even though they don't understand what they do on the high-level. We will -build the core logic of a small application and this will serve as a guide for -common patterns that will help to kickstart Akka.NET projects. - -The application we aim to write will be a simplified IoT system where devices, -installed at the home of users, can report temperature data from sensors. Users -will be able to query the current state of these sensors. To keep things simple, -we will not actually expose the application via HTTP or any other external API, -we will, instead, concentrate only on the core logic. However, we will write -tests for the pieces of the application to get comfortable and proficient with -testing actors early on. - -## Our Goals for the IoT System - -We will build a simple IoT application with the bare essentials to demonstrate -designing an Akka.NET-based system. The application will consist of two main -components: - -* **Device data collection:** This component has the responsibility to maintain - a local representation of the otherwise remote devices. The devices will be - organized into device groups, grouping together sensors belonging to a home. -* **User dashboards:** This component has the responsibility to periodically - collect data from the devices for a logged in user and present the results as - a report. - -For simplicity, we will only collect temperature data for the devices, but in a -real application our local representations for a remote device, which we will -model as an actor, would have many more responsibilities. Among others; reading -the configuration of the device, changing the configuration, checking if the -devices are unresponsive, etc. We leave these complexities for now as they can -be easily added as an exercise. - -We will also not address the means by which the remote devices communicate with -the local representations (actors). Instead, we just build an actor based API -that such a network protocol could use. We will use tests for our API everywhere -though. - -The architecture of the application will look like this: - -![box diagram of the architecture](/images/arch_boxes_diagram.png) - -## Top Level Architecture +# Part 1: Top Level Architecture When writing prose, the hardest part is usually to write the first couple of sentences. There is a similar feeling when trying to build an Akka.NET system: @@ -98,7 +52,7 @@ from user, or system, actors end up. > has. This special entity is called the "Bubble-Walker". This special entity is > invisible for the user and only has uses internally. -### Structure of an IActorRef and Paths of Actors +## Structure of an IActorRef and Paths of Actors The easiest way to see this in action is to simply print `IActorRef` instances. In this small experiment, we print the reference of the first actor we create @@ -141,7 +95,7 @@ The last part of the actor reference, like `#1053618476` is a unique identifier of the actor living under the path. This is usually not something the user needs to be concerned with, and we leave the discussion of this field for later. -### Hierarchy and Lifecycle of Actors +## Hierarchy and Lifecycle of Actors We have so far seen that actors are organized into a **strict hierarchy**. This hierarchy consists of a predefined upper layer of three actors (the root @@ -196,7 +150,7 @@ The family of these lifecycle hooks is rich, and we recommend reading [the actor lifecycle](xref:untyped-actor-api#actor-lifecycle) section of the reference for all details. -### Hierarchy and Failure Handling (Supervision) +## Hierarchy and Failure Handling (Supervision) Parents and children are not only connected by their lifecycles. Whenever an actor fails (throws an exception or an unhandled exception bubbles out from @@ -240,7 +194,7 @@ see how the output changes. For the impatient, we also recommend looking into the [supervision reference page](xref:supervision) for more in-depth details. -### The First Actor +## The First Actor Actors are organized into a strict tree, where the lifecycle of every child is tied to the parent and where parents are responsible for deciding the fate of @@ -280,7 +234,7 @@ All we need now is to tie this up with a class with the `main` entry point: This application does very little for now, but we have the first actor in place and we are ready to extend it further. -## What Is Next? +# What Is Next? In the following chapters we will grow the application step-by-step: diff --git a/docs/articles/intro/tutorial-2.md b/docs/articles/intro/getting-started/tutorial-2.md similarity index 100% rename from docs/articles/intro/tutorial-2.md rename to docs/articles/intro/getting-started/tutorial-2.md diff --git a/docs/articles/intro/tutorial-3.md b/docs/articles/intro/getting-started/tutorial-3.md similarity index 100% rename from docs/articles/intro/tutorial-3.md rename to docs/articles/intro/getting-started/tutorial-3.md diff --git a/docs/articles/intro/tutorial-4.md b/docs/articles/intro/getting-started/tutorial-4.md similarity index 100% rename from docs/articles/intro/tutorial-4.md rename to docs/articles/intro/getting-started/tutorial-4.md diff --git a/docs/articles/intro/getting-started/tutorial-overview.md b/docs/articles/intro/getting-started/tutorial-overview.md new file mode 100644 index 00000000000..7616a8a8419 --- /dev/null +++ b/docs/articles/intro/getting-started/tutorial-overview.md @@ -0,0 +1,57 @@ +--- +uid: tutorial-overview +title: Tutorial Overview +--- + +# Tutorial Overview + +In this and the following chapters, we will build a sample Akka.NET application +to introduce you to the language of actors and how solutions can be formulated +with them. It is a common hurdle for beginners to translate their project into +actors even though they don't understand what they do on the high-level. We will +build the core logic of a small application and this will serve as a guide for +common patterns that will help to kickstart Akka.NET projects. + +The application we aim to write will be a simplified IoT system where devices, +installed at the home of users, can report temperature data from sensors. Users +will be able to query the current state of these sensors. To keep things simple, +we will not actually expose the application via HTTP or any other external API, +we will, instead, concentrate only on the core logic. However, we will write +tests for the pieces of the application to get comfortable and proficient with +testing actors early on. + +## Our Goals for the IoT System + +We will build a simple IoT application with the bare essentials to demonstrate +designing an Akka.NET-based system. The application will consist of two main +components: + +* **Device data collection:** This component has the responsibility to maintain + a local representation of the otherwise remote devices. The devices will be + organized into device groups, grouping together sensors belonging to a home. +* **User dashboards:** This component has the responsibility to periodically + collect data from the devices for a logged in user and present the results as + a report. + +For simplicity, we will only collect temperature data for the devices, but in a +real application our local representations for a remote device, which we will +model as an actor, would have many more responsibilities. Among others; reading +the configuration of the device, changing the configuration, checking if the +devices are unresponsive, etc. We leave these complexities for now as they can +be easily added as an exercise. + +We will also not address the means by which the remote devices communicate with +the local representations (actors). Instead, we just build an actor based API +that such a network protocol could use. We will use tests for our API everywhere +though. + +The architecture of the application will look like this: + +![box diagram of the architecture](/images/arch_boxes_diagram.png) + +This tutorial is divided into four parts: + +* [Part 1: Top-level Architecture](../getting-started/tutorial-1.html) +* [Part 2. The Device Actor](../getting-started/tutorial-2.html) +* [Part 3. Device Groups and Manager](../getting-started/tutorial-3.html) +* [Part 4. Querying a Group of Devices](../getting-started/tutorial-4.html) diff --git a/docs/articles/intro/toc.yml b/docs/articles/intro/toc.yml new file mode 100644 index 00000000000..3119a76f9f9 --- /dev/null +++ b/docs/articles/intro/toc.yml @@ -0,0 +1,12 @@ +- name: What is Akka.NET? + href: what-is-akka.md +- name: What problems does the actor model solve? + href: what-problems-does-actor-model-solve.md +- name: Akka.NET Libraries and Modules + href: modules.md +- name: Use-case and Deployment Scenarios + href: use-case-and-deployment-scenarios.md +- name: Production Users and Use Cases for Akka.NET + href: akka-users.md +- name: Getting Started + href: getting-started/toc.yml \ No newline at end of file diff --git a/docs/articles/intro/tutorial-1.html b/docs/articles/intro/tutorial-1.html new file mode 100644 index 00000000000..586702c9f7d --- /dev/null +++ b/docs/articles/intro/tutorial-1.html @@ -0,0 +1,10 @@ + + + + Part 1. Top-level Architecture + + + +

This page has been moved to Part 1. Top-level Architecture.

+ + \ No newline at end of file diff --git a/docs/articles/intro/tutorial-2.html b/docs/articles/intro/tutorial-2.html new file mode 100644 index 00000000000..ad7e1d757b2 --- /dev/null +++ b/docs/articles/intro/tutorial-2.html @@ -0,0 +1,10 @@ + + + + Part 2. The Device Actor + + + +

This page has been moved to Part 2. The Device Actor.

+ + \ No newline at end of file diff --git a/docs/articles/intro/tutorial-3.html b/docs/articles/intro/tutorial-3.html new file mode 100644 index 00000000000..112aea29976 --- /dev/null +++ b/docs/articles/intro/tutorial-3.html @@ -0,0 +1,10 @@ + + + + Part 3. Device Groups and Manager + + + +

This page has been moved to Part 3. Device Groups and Manager.

+ + \ No newline at end of file diff --git a/docs/articles/intro/tutorial-4.html b/docs/articles/intro/tutorial-4.html new file mode 100644 index 00000000000..dbec65eb985 --- /dev/null +++ b/docs/articles/intro/tutorial-4.html @@ -0,0 +1,10 @@ + + + + Part 4. Querying a Group of Devices + + + +

This page has been moved to Part 4. Querying a Group of Devices.

+ + \ No newline at end of file diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index 7f2a6e24a98..df5ca121fa6 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -1,23 +1,5 @@ - name: Introduction - items: - - name: What is Akka.NET? - href: intro/what-is-akka.md - - name: What problems does the actor model solve? - href: intro/what-problems-does-actor-model-solve.md - - name: Akka.NET Libraries and Modules - href: intro/modules.md - - name: Part 1. Top-level Architecture - href: intro/tutorial-1.md - - name: Part 2. The Device Actor - href: intro/tutorial-2.md - - name: Part 3. Device Groups and Manager - href: intro/tutorial-3.md - - name: Part 4. Querying a Group of Devices - href: intro/tutorial-4.md - - name: Use-case and Deployment Scenarios - href: intro/use-case-and-deployment-scenarios.md - - name: Production Users and Use Cases for Akka.NET - href: intro/akka-users.md + href: intro/toc.yml - name: Concepts items: - name: Terminology, Concepts diff --git a/src/common.props b/src/common.props index 1518b6e3f22..4f4f16066c1 100644 --- a/src/common.props +++ b/src/common.props @@ -2,7 +2,7 @@ Copyright © 2013-2021 Akka.NET Team Akka.NET Team - 1.4.28 + 1.4.32 akkalogo.png https://github.com/akkadotnet/akka.net https://github.com/akkadotnet/akka.net/blob/master/LICENSE @@ -32,29 +32,7 @@ true - Maintenance Release for Akka.NET 1.4** -Akka.NET v1.4.28 is a minor release that contains some enhancements for Akka.Streams and some bug fixes. -New Akka.Streams Stages** -Akka.NET v1.4.28 includes two new Akka.Streams stages: -[`Source.Never`](https://getakka.net/articles/streams/builtinstages.html#never) - a utility stage that never emits any elements, never completes, and never fails. Designed primarily for unit testing. -[`Flow.WireTap`](https://getakka.net/articles/streams/builtinstages.html#wiretap) - the `WireTap` stage attaches a given `Sink` to a `Flow` without affecting any of the upstream or downstream elements. This stage is designed for performance monitoring and instrumentation of Akka.Streams graphs. -In addition to these, here are some other changes introduced Akka.NET v1.4.28: -[Akka.Streams: `Source` that flattens a `Task` source and keeps the materialized value](https://github.com/akkadotnet/akka.net/pull/5338) -[Akka.Streams: made `GraphStageLogic.LogSource` virtual and change default `StageLogic` `LogSource`](https://github.com/akkadotnet/akka.net/pull/5360) -[Akka.IO: `UdpListener` Responds IPv6 Bound message with IPv4 Bind message](https://github.com/akkadotnet/akka.net/issues/5344) -[Akka.MultiNodeTestRunner: now runs on Linux and as a `dotnet test` package](https://github.com/akkadotnet/Akka.MultiNodeTestRunner/releases/tag/1.0.0) - we will keep you posted on this, as we're still working on getting Rider / VS Code / Visual Studio debugger-attached support to work correctly. -[Akka.Persistence.Sql.Common: Cancel `DBCommand` after finish reading events by PersistenceId ](https://github.com/akkadotnet/akka.net/pull/5311) - *massive* performance fix for Akka.Persistence with many log entries on SQL-based journals. -[Akka.Actor: `DefaultResizer` does not reisize when `ReceiveAsync` is used](https://github.com/akkadotnet/akka.net/issues/5327) -If you want to see the [full set of changes made in Akka.NET v1.4.28, click here](https://github.com/akkadotnet/akka.net/milestone/59). -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 16 | 2707 | 1911 | Sean Killeen | -| 8 | 1088 | 28 | Ismael Hamed | -| 6 | 501 | 261 | Gregorius Soedharmo | -| 5 | 8 | 8 | dependabot[bot] | -| 4 | 36 | 86 | Aaron Stannard | -| 1 | 1 | 0 | Jarl Sveinung Flø Rasmussen | -Special thanks for @SeanKilleen for contributing extensive Markdown linting and automated CI checks for that to our documentation! https://github.com/akkadotnet/akka.net/issues/5312 + Placeholder for nightlies** From 7cce1a62fc64c35b4d12559d0478c2873e98e5c9 Mon Sep 17 00:00:00 2001 From: Ebere Abanonu Date: Thu, 13 Jan 2022 14:55:17 +0100 Subject: [PATCH 23/30] Re-organize Configuration documentation (#5501) * * Moved ~/articles/hocon/index.md --> ~/articles/configuration/hocon.md * Moved ~/articles/concepts/configuration.md --> ~/articles/configuration/index.md * Moved ~/articles/configuration/* (all of the module-specific configurations) --> ~/articles/configuration/modules/* Co-authored-by: Aaron Stannard --- docs/articles/concepts/configuration.html | 10 ++++++++++ docs/articles/configuration/akka.cluster.html | 10 ++++++++++ .../articles/configuration/akka.persistence.html | 10 ++++++++++ docs/articles/configuration/akka.remote.html | 10 ++++++++++ docs/articles/configuration/akka.streams.html | 10 ++++++++++ docs/articles/configuration/akka.testkit.html | 10 ++++++++++ docs/articles/configuration/akka.testkit.md | 9 --------- .../configuration.md => configuration/config.md} | 0 .../{hocon/index.md => configuration/hocon.md} | 0 .../configuration/{ => modules}/akka.cluster.md | 2 +- .../{ => modules}/akka.persistence.md | 2 +- .../configuration/{ => modules}/akka.remote.md | 2 +- .../configuration/{ => modules}/akka.streams.md | 2 +- .../configuration/modules/akka.testkit.md | 9 +++++++++ docs/articles/configuration/modules/toc.yml | 10 ++++++++++ docs/articles/configuration/toc.yml | 16 ++++++---------- docs/articles/hocon/index.html | 10 ++++++++++ docs/articles/toc.yml | 6 ------ 18 files changed, 99 insertions(+), 29 deletions(-) create mode 100644 docs/articles/concepts/configuration.html create mode 100644 docs/articles/configuration/akka.cluster.html create mode 100644 docs/articles/configuration/akka.persistence.html create mode 100644 docs/articles/configuration/akka.remote.html create mode 100644 docs/articles/configuration/akka.streams.html create mode 100644 docs/articles/configuration/akka.testkit.html delete mode 100644 docs/articles/configuration/akka.testkit.md rename docs/articles/{concepts/configuration.md => configuration/config.md} (100%) rename docs/articles/{hocon/index.md => configuration/hocon.md} (100%) rename docs/articles/configuration/{ => modules}/akka.cluster.md (63%) rename docs/articles/configuration/{ => modules}/akka.persistence.md (65%) rename docs/articles/configuration/{ => modules}/akka.remote.md (63%) rename docs/articles/configuration/{ => modules}/akka.streams.md (65%) create mode 100644 docs/articles/configuration/modules/akka.testkit.md create mode 100644 docs/articles/configuration/modules/toc.yml create mode 100644 docs/articles/hocon/index.html diff --git a/docs/articles/concepts/configuration.html b/docs/articles/concepts/configuration.html new file mode 100644 index 00000000000..a31323fcf8a --- /dev/null +++ b/docs/articles/concepts/configuration.html @@ -0,0 +1,10 @@ + + + + Akka.NET Configuration + + + +

This page has been moved to Akka.NET Configuration.

+ + \ No newline at end of file diff --git a/docs/articles/configuration/akka.cluster.html b/docs/articles/configuration/akka.cluster.html new file mode 100644 index 00000000000..4e232b0de59 --- /dev/null +++ b/docs/articles/configuration/akka.cluster.html @@ -0,0 +1,10 @@ + + + + Akka.Cluster Configuration + + + +

This page has been moved to Akka.Cluster Configuration.

+ + \ No newline at end of file diff --git a/docs/articles/configuration/akka.persistence.html b/docs/articles/configuration/akka.persistence.html new file mode 100644 index 00000000000..af62ecd547a --- /dev/null +++ b/docs/articles/configuration/akka.persistence.html @@ -0,0 +1,10 @@ + + + + Akka.Persistence Configuration + + + +

This page has been moved to Akka.Persistence Configuration.

+ + \ No newline at end of file diff --git a/docs/articles/configuration/akka.remote.html b/docs/articles/configuration/akka.remote.html new file mode 100644 index 00000000000..eb0b78b97b7 --- /dev/null +++ b/docs/articles/configuration/akka.remote.html @@ -0,0 +1,10 @@ + + + + Akka.Remote Configuration + + + +

This page has been moved to Akka.Remote Configuration.

+ + \ No newline at end of file diff --git a/docs/articles/configuration/akka.streams.html b/docs/articles/configuration/akka.streams.html new file mode 100644 index 00000000000..5cfce19dc27 --- /dev/null +++ b/docs/articles/configuration/akka.streams.html @@ -0,0 +1,10 @@ + + + + Akka.Streams Configuration + + + +

This page has been moved to Akka.Streams Configuration.

+ + \ No newline at end of file diff --git a/docs/articles/configuration/akka.testkit.html b/docs/articles/configuration/akka.testkit.html new file mode 100644 index 00000000000..026b27f857b --- /dev/null +++ b/docs/articles/configuration/akka.testkit.html @@ -0,0 +1,10 @@ + + + + Akka.Testkit Configuration + + + +

This page has been moved to Akka.Testkit Configuration.

+ + \ No newline at end of file diff --git a/docs/articles/configuration/akka.testkit.md b/docs/articles/configuration/akka.testkit.md deleted file mode 100644 index f4352c5942f..00000000000 --- a/docs/articles/configuration/akka.testkit.md +++ /dev/null @@ -1,9 +0,0 @@ -# Akka.TestKit Configuration - -Below is the default HOCON configuration for the base `Akka.TestKit` package. - -[!code[Akka.TestKit.dll HOCON Configuration](../../../src/core/Akka.TestKit/Internal/Reference.conf)] - -Additionally, it's also possible to change the default [`IScheduler` implementation](../../api/Akka.Actor.IScheduler.yml) in the `Akka.TestKit` to use [a virtualized `TestScheduler` implementation](../../api/Akka.TestKit.TestScheduler.yml) that Akka.NET developers can use to artificially advance time forward. To swap in the `TestScheduler`, developers will want to include the HOCON below: - -[!code[Akka.TestKit.dll TestScheduler HOCON Configuration](../../../src/core/Akka.TestKit/Configs/TestScheduler.conf)] diff --git a/docs/articles/concepts/configuration.md b/docs/articles/configuration/config.md similarity index 100% rename from docs/articles/concepts/configuration.md rename to docs/articles/configuration/config.md diff --git a/docs/articles/hocon/index.md b/docs/articles/configuration/hocon.md similarity index 100% rename from docs/articles/hocon/index.md rename to docs/articles/configuration/hocon.md diff --git a/docs/articles/configuration/akka.cluster.md b/docs/articles/configuration/modules/akka.cluster.md similarity index 63% rename from docs/articles/configuration/akka.cluster.md rename to docs/articles/configuration/modules/akka.cluster.md index 4aa6da82d12..d528c54ef2c 100644 --- a/docs/articles/configuration/akka.cluster.md +++ b/docs/articles/configuration/modules/akka.cluster.md @@ -7,4 +7,4 @@ title: Akka.Cluster Configuration Below is the default HOCON configuration for the base `Akka.Cluster` package. -[!code[Akka.Cluster.dll HOCON Configuration](../../../src/core/Akka.Cluster/Configuration/Cluster.conf)] +[!code[Akka.Cluster.dll HOCON Configuration](../../../../src/core/Akka.Cluster/Configuration/Cluster.conf)] diff --git a/docs/articles/configuration/akka.persistence.md b/docs/articles/configuration/modules/akka.persistence.md similarity index 65% rename from docs/articles/configuration/akka.persistence.md rename to docs/articles/configuration/modules/akka.persistence.md index ac240b1ad46..c8e2a5b0d96 100644 --- a/docs/articles/configuration/akka.persistence.md +++ b/docs/articles/configuration/modules/akka.persistence.md @@ -7,4 +7,4 @@ title: Akka.Persistence Configuration Below is the default HOCON configuration for the base `Akka.Persistence` package. -[!code[Akka.Persistence.dll HOCON Configuration](../../../src/core/Akka.Persistence/persistence.conf)] +[!code[Akka.Persistence.dll HOCON Configuration](../../../../src/core/Akka.Persistence/persistence.conf)] diff --git a/docs/articles/configuration/akka.remote.md b/docs/articles/configuration/modules/akka.remote.md similarity index 63% rename from docs/articles/configuration/akka.remote.md rename to docs/articles/configuration/modules/akka.remote.md index c8cf0974f85..2598e8d0d1e 100644 --- a/docs/articles/configuration/akka.remote.md +++ b/docs/articles/configuration/modules/akka.remote.md @@ -7,4 +7,4 @@ title: Akka.Remote Configuration Below is the default HOCON configuration for the base `Akka.Remote` package. -[!code[Akka.Remote.dll HOCON Configuration](../../../src/core/Akka.Remote/Configuration/Remote.conf)] +[!code[Akka.Remote.dll HOCON Configuration](../../../../src/core/Akka.Remote/Configuration/Remote.conf)] diff --git a/docs/articles/configuration/akka.streams.md b/docs/articles/configuration/modules/akka.streams.md similarity index 65% rename from docs/articles/configuration/akka.streams.md rename to docs/articles/configuration/modules/akka.streams.md index 057b25dbf55..db0f68d2ec5 100644 --- a/docs/articles/configuration/akka.streams.md +++ b/docs/articles/configuration/modules/akka.streams.md @@ -7,4 +7,4 @@ title: Akka.Streams Configuration Below is the default HOCON configuration for the base `Akka.Streams` package. -[!code[Akka.Streams.dll HOCON Configuration](../../../src/core/Akka.Streams/reference.conf)] +[!code[Akka.Streams.dll HOCON Configuration](../../../../src/core/Akka.Streams/reference.conf)] diff --git a/docs/articles/configuration/modules/akka.testkit.md b/docs/articles/configuration/modules/akka.testkit.md new file mode 100644 index 00000000000..55981fae247 --- /dev/null +++ b/docs/articles/configuration/modules/akka.testkit.md @@ -0,0 +1,9 @@ +# Akka.TestKit Configuration + +Below is the default HOCON configuration for the base `Akka.TestKit` package. + +[!code[Akka.TestKit.dll HOCON Configuration](../../../../src/core/Akka.TestKit/Internal/Reference.conf)] + +Additionally, it's also possible to change the default [`IScheduler` implementation](../../../api/Akka.Actor.IScheduler.yml) in the `Akka.TestKit` to use [a virtualized `TestScheduler` implementation](../../api/Akka.TestKit.TestScheduler.yml) that Akka.NET developers can use to artificially advance time forward. To swap in the `TestScheduler`, developers will want to include the HOCON below: + +[!code[Akka.TestKit.dll TestScheduler HOCON Configuration](../../../../src/core/Akka.TestKit/Configs/TestScheduler.conf)] diff --git a/docs/articles/configuration/modules/toc.yml b/docs/articles/configuration/modules/toc.yml new file mode 100644 index 00000000000..807da952c5d --- /dev/null +++ b/docs/articles/configuration/modules/toc.yml @@ -0,0 +1,10 @@ +- name: Akka.Cluster + href: akka.cluster.md +- name: Akka.Remote + href: akka.remote.md +- name: Akka.Persistence + href: akka.persistence.md +- name: Akka.Streams + href: akka.streams.md +- name: Akka.TestKit + href: akka.testkit.md \ No newline at end of file diff --git a/docs/articles/configuration/toc.yml b/docs/articles/configuration/toc.yml index b00f0b29b60..3a43e97667b 100644 --- a/docs/articles/configuration/toc.yml +++ b/docs/articles/configuration/toc.yml @@ -1,12 +1,8 @@ +- name: Configurations + href: config.md +- name: HOCON Syntax and Practices in Akka.NET + href: hocon.md - name: Akka href: akka.md -- name: Akka.Remote - href: akka.remote.md -- name: Akka.Cluster - href: akka.cluster.md -- name: Akka.Persistence - href: akka.persistence.md -- name: Akka.Streams - href: akka.streams.md -- name: Akka.TestKit - href: akka.testkit.md \ No newline at end of file +- name: Modules + href: modules/toc.yml \ No newline at end of file diff --git a/docs/articles/hocon/index.html b/docs/articles/hocon/index.html new file mode 100644 index 00000000000..11f5da470ce --- /dev/null +++ b/docs/articles/hocon/index.html @@ -0,0 +1,10 @@ + + + + HOCON Syntax and Practices in Akka.NET + + + +

This page has been moved to HOCON Syntax and Practices in Akka.NET.

+ + \ No newline at end of file diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index df5ca121fa6..88ab9507165 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -16,8 +16,6 @@ href: concepts/location-transparency.md - name: Message Delivery Reliability href: concepts/message-delivery-reliability.md - - name: Configuration - href: concepts/configuration.md - name: Actors items: - name: ReceiveActor API @@ -46,10 +44,6 @@ href: actors/testing-actor-systems.md - name: Coordinated Shutdown href: actors/coordinated-shutdown.md -- name: HOCON - items: - - name: HOCON Syntax and Practices in Akka.NET - href: hocon/index.md - name: Persistence items: - name: Architecture From 968b0ddb653bbc9cca3a94cd74bad072df0885b6 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 13 Jan 2022 08:22:25 -0600 Subject: [PATCH 24/30] Added `wsl` reboot instructions to debugging documentation (#5508) --- docs/community/contributing/debugging.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/community/contributing/debugging.md b/docs/community/contributing/debugging.md index 4e972acfa70..1ec75876f36 100644 --- a/docs/community/contributing/debugging.md +++ b/docs/community/contributing/debugging.md @@ -92,6 +92,27 @@ memory=2GB # Limits VM memory in WSL 2 up to 2GB processors=2 # Makes the WSL 2 VM use two virtual processors ``` +#### Rebooting `wsl` With Updated Settings + +Once you've made your changes to `.wslconfig` you'll need to reboot your instance for them to take effect. + +You can list all of your `wsl` distributions via `wsl -l` or `wsl --list`: + +```shell +Windows Subsystem for Linux Distributions: +Ubuntu (Default) +docker-desktop-data +docker-desktop +``` + +In this case we need to terminate our default `Ubuntu` `wsl` instance: + +```shell +wsl --terminate Ubuntu +``` + +The next time we try to launch `wsl` our `.wslconfig` settings will be active inside the environment. + ### Repeating a Test Until It Fails If you're using JetBrains Rider, you can use their unit test feature to run a test until it fails. From e1c89ccb9881a32768c13bc8c178c64c821a434c Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 13 Jan 2022 14:46:09 -0600 Subject: [PATCH 25/30] Benchmark.NET Akka.Persistence Benchmarks (#5509) --- .gitignore | 1 + .../Persistence/JournalWriteBenchmarks.cs | 82 +++++++ .../Persistence/PersistenceInfrastructure.cs | 203 ++++++++++++++++++ .../Persistence/RecoveryBenchmark.cs | 99 +++++++++ .../Akka.Cluster.Benchmarks/Program.cs | 6 +- .../Sharding/ShardingInfrastructure.cs | 12 +- 6 files changed, 395 insertions(+), 8 deletions(-) create mode 100644 src/benchmark/Akka.Cluster.Benchmarks/Persistence/JournalWriteBenchmarks.cs create mode 100644 src/benchmark/Akka.Cluster.Benchmarks/Persistence/PersistenceInfrastructure.cs create mode 100644 src/benchmark/Akka.Cluster.Benchmarks/Persistence/RecoveryBenchmark.cs diff --git a/.gitignore b/.gitignore index 425c808c969..e371f412542 100644 --- a/.gitignore +++ b/.gitignore @@ -224,3 +224,4 @@ launchSettings.json # NDepend *.ndproj /[Nn][Dd]epend[Oo]ut +.ionide/symbolCache.db diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Persistence/JournalWriteBenchmarks.cs b/src/benchmark/Akka.Cluster.Benchmarks/Persistence/JournalWriteBenchmarks.cs new file mode 100644 index 00000000000..5730bf4abec --- /dev/null +++ b/src/benchmark/Akka.Cluster.Benchmarks/Persistence/JournalWriteBenchmarks.cs @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Benchmarks.Configurations; +using BenchmarkDotNet.Attributes; +using static Akka.Cluster.Benchmarks.Persistence.PersistenceInfrastructure; + +namespace Akka.Cluster.Benchmarks.Persistence +{ + [Config(typeof(MicroBenchmarkConfig))] + public class JournalWriteBenchmarks + { + private static readonly Store Message = new Store(1); + + [Params(1, 10, 100)] public int PersistentActors; + + [Params(100)] public int WriteMsgCount; + + private ActorSystem _sys1; + + private IActorRef _doneActor; + private HashSet _persistentActors; + + /* + * Don't need to worry about cleaning up in-memory SQLite databases: https://www.sqlite.org/inmemorydb.html + * Database is automatically deleted once the last connection to it is closed. + */ + + [IterationSetup] + public void Setup() + { + var (connectionStr, config) = GenerateJournalConfig(); + _sys1 = ActorSystem.Create("MySys", config); + _doneActor = _sys1.ActorOf(Props.Create(() => new BenchmarkDoneActor(PersistentActors)), "done"); + _persistentActors = new HashSet(); + + var tasks = new List>(); + var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); + foreach (var i in Enumerable.Range(0, PersistentActors)) + { + var myRef = _sys1.ActorOf(Props.Create(() => new PerformanceTestActor(i.ToString(), _doneActor, WriteMsgCount)), i.ToString()); + _persistentActors.Add(myRef); + tasks.Add(myRef.Ask(Init.Instance, startupCts.Token)); + } + + // all persistence actors have started and successfully communicated with journal + Task.WhenAll(tasks).Wait(startupCts.Token); + } + + [IterationCleanup] + public void Cleanup() + { + _sys1.Terminate().Wait(); + } + + [Benchmark] + public async Task WriteToPersistence() + { + var startupCts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var completionTask = _doneActor.Ask(IsFinished.Instance, startupCts.Token); + + foreach(var _ in Enumerable.Range(0, WriteMsgCount)) + foreach (var a in _persistentActors) + { + a.Tell(Message); + } + + await completionTask; + } + } +} \ No newline at end of file diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Persistence/PersistenceInfrastructure.cs b/src/benchmark/Akka.Cluster.Benchmarks/Persistence/PersistenceInfrastructure.cs new file mode 100644 index 00000000000..f8cc025365e --- /dev/null +++ b/src/benchmark/Akka.Cluster.Benchmarks/Persistence/PersistenceInfrastructure.cs @@ -0,0 +1,203 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Util; +using Akka.Configuration; +using Akka.Util.Internal; +using Akka.Persistence; + +namespace Akka.Cluster.Benchmarks.Persistence +{ + public sealed class Init + { + public static readonly Init Instance = new Init(); + private Init() { } + } + + public sealed class Finish + { + public static readonly Finish Instance = new Finish(); + private Finish() { } + } + + public sealed class Done + { + public static readonly Done Instance = new Done(); + private Done() { } + } + + public sealed class Finished + { + public readonly long State; + + public Finished(long state) + { + State = state; + } + } + + public sealed class RecoveryFinished + { + public static readonly RecoveryFinished Instance = new RecoveryFinished(); + + private RecoveryFinished() { } + } + + public sealed class Store + { + public readonly int Value; + + public Store(int value) + { + Value = value; + } + } + + public sealed class Stored + { + public readonly int Value; + + public Stored(int value) + { + Value = value; + } + } + + /// + /// Query to that will be used to signal termination + /// + public sealed class IsFinished + { + public static readonly IsFinished Instance = new IsFinished(); + private IsFinished(){} + } + + public sealed class BenchmarkDoneActor : ReceiveActor + { + private int _expected; + private IActorRef _asker; + + public BenchmarkDoneActor(int expected) + { + Receive(_ => + { + _expected = expected; + _asker = Sender; + }); + + Receive(f => + { + // this will terminate the benchmark + if(--_expected <= 0) + _asker.Tell(Done.Instance); + }); + + Receive(f => + { + // this will terminate the benchmark + if (--_expected <= 0) + _asker?.Tell(RecoveryFinished.Instance); + }); + } + } + + public sealed class PerformanceTestActor : PersistentActor + { + private long _state = 0L; + private readonly long _target; + private readonly IActorRef _doneActor; + public PerformanceTestActor(string persistenceId, IActorRef doneActor, long target) + { + _doneActor = doneActor; + PersistenceId = persistenceId; + _target = target; + } + + public sealed override string PersistenceId { get; } + + protected override bool ReceiveRecover(object message) { + switch(message){ + case Stored s: + _state += s.Value; + break; + default: + return false; + } + + return true; + } + + protected override void OnReplaySuccess() + { + _doneActor.Tell(RecoveryFinished.Instance); + } + + protected override bool ReceiveCommand(object message){ + switch(message){ + case Store store: + PersistAsync(new Stored(store.Value), s => + { + _state += s.Value; + if(_state >= _target) + _doneActor.Tell(new Finished(_state)); + }); + break; + case Init _: + var sender = Sender; + PersistAsync(new Stored(0), s => + { + _state += s.Value; + sender.Tell(Done.Instance); + }); + break; + case Finish _: + Sender.Tell(new Finished(_state)); + break; + default: + return false; + } + + return true; + } + } + + + public static class PersistenceInfrastructure{ + public static readonly AtomicCounter DbCounter = new AtomicCounter(0); + + public static (string connectionString, Config hoconConfig) GenerateJournalConfig(){ + return GenerateJournalConfig(DbCounter.GetAndIncrement().ToString()); + } + + public static (string connectionString, Config hoconConfig) GenerateJournalConfig(string databaseId){ + // need to create a unique database instance each time benchmark is run so we don't pollute + // might need to disable shared cache + var connectionString = $"Datasource=memdb-journal-{databaseId}.db;Mode=Memory;Cache=Shared"; + + var config = ConfigurationFactory.ParseString(@" + akka { + persistence.journal { + plugin = ""akka.persistence.journal.sqlite"" + sqlite { + class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistence.Sqlite"" + plugin-dispatcher = ""akka.actor.default-dispatcher"" + table-name = event_journal + metadata-table-name = journal_metadata + auto-initialize = on + connection-string = """+ connectionString +@""" + } + } + }"); + + return (connectionString, config); + } + } + +} \ No newline at end of file diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Persistence/RecoveryBenchmark.cs b/src/benchmark/Akka.Cluster.Benchmarks/Persistence/RecoveryBenchmark.cs new file mode 100644 index 00000000000..18a9368f46b --- /dev/null +++ b/src/benchmark/Akka.Cluster.Benchmarks/Persistence/RecoveryBenchmark.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Benchmarks.Configurations; +using BenchmarkDotNet.Attributes; +using static Akka.Cluster.Benchmarks.Persistence.PersistenceInfrastructure; + +namespace Akka.Cluster.Benchmarks.Persistence +{ + [Config(typeof(MicroBenchmarkConfig))] + public class RecoveryBenchmark + { + private static readonly Store Message = new Store(1); + + [Params(1, 10, 100)] public int PersistentActors; + + [Params(100)] public int WriteMsgCount; + + private ActorSystem _sys1; + + private IActorRef _doneActor; + private HashSet _persistentActors; + + /* + * Don't need to worry about cleaning up in-memory SQLite databases: https://www.sqlite.org/inmemorydb.html + * Database is automatically deleted once the last connection to it is closed. + */ + + [GlobalSetup] + public async Task GlobalSetup() + { + var (connectionStr, config) = GenerateJournalConfig(); + _sys1 = ActorSystem.Create("MySys", config); + _doneActor = _sys1.ActorOf(Props.Create(() => new BenchmarkDoneActor(PersistentActors)), "done"); + _persistentActors = new HashSet(); + + var tasks = new List>(); + var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); + foreach (var i in Enumerable.Range(0, PersistentActors)) + { + var myRef = _sys1.ActorOf(Props.Create(() => new PerformanceTestActor(i.ToString(), _doneActor, WriteMsgCount)), i.ToString()); + _persistentActors.Add(myRef); + tasks.Add(myRef.Ask(Init.Instance, startupCts.Token)); + } + + // all persistence actors have started and successfully communicated with journal + await Task.WhenAll(tasks); + + var completionTask = _doneActor.Ask(IsFinished.Instance, startupCts.Token); + + foreach (var _ in Enumerable.Range(0, WriteMsgCount)) + foreach (var a in _persistentActors) + { + a.Tell(Message); + } + + await completionTask; + + Cleanup(); + } + + [GlobalCleanup] + public async Task GlobalCleanup() + { + await _sys1.Terminate(); + } + + [IterationCleanup] + public void Cleanup() + { + foreach (var a in _persistentActors) + { + _sys1.Stop(a); + } + _persistentActors.Clear(); + } + + + [Benchmark] + public async Task RecoverPersistedActors() + { + var startupCts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var completionTask = _doneActor.Ask(IsFinished.Instance, startupCts.Token); + + foreach (var i in Enumerable.Range(0, PersistentActors)) + { + var myRef = _sys1.ActorOf(Props.Create(() => new PerformanceTestActor(i.ToString(), _doneActor, WriteMsgCount)), i.ToString()); + _persistentActors.Add(myRef); + } + + await completionTask; + } + } +} diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Program.cs b/src/benchmark/Akka.Cluster.Benchmarks/Program.cs index df08f62bf68..8c8be2a0f6e 100644 --- a/src/benchmark/Akka.Cluster.Benchmarks/Program.cs +++ b/src/benchmark/Akka.Cluster.Benchmarks/Program.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using Akka.Cluster.Benchmarks.Persistence; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; @@ -10,9 +11,10 @@ class Program static void Main(string[] args) { #if (DEBUG) - BenchmarkSwitcher.FromAssembly(Assembly.GetExecutingAssembly()).Run(args, new DebugInProcessConfig()); + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly) + .Run(args, new DebugInProcessConfig()); #else - BenchmarkSwitcher.FromAssembly(Assembly.GetExecutingAssembly()).Run(args); + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); #endif } diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardingInfrastructure.cs b/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardingInfrastructure.cs index e016ea70052..1f6e08a68ae 100644 --- a/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardingInfrastructure.cs +++ b/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardingInfrastructure.cs @@ -1,9 +1,9 @@ -// //----------------------------------------------------------------------- -// // -// // Copyright (C) 2009-2021 Lightbend Inc. -// // Copyright (C) 2013-2021 .NET Foundation -// // -// //----------------------------------------------------------------------- +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- using System.Threading.Tasks; using Akka.Actor; From 5223ed4f8e1ca9baacc901319e013da1a0465c0d Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Fri, 14 Jan 2022 07:48:48 +0700 Subject: [PATCH 26/30] Bump Hyperion to 0.12.0 (#5510) * Bump Hyperion from 0.11.2 to 0.12.0 * Implement Hyperion TypeFilter feature. Add HOCON config and HyperionSerializerSetup support. --- src/common.props | 2 +- .../HyperionConfigTests.cs | 59 +++++++++++++++++ .../HyperionSerializerSetupSpec.cs | 43 +++++++++++++ .../HyperionSerializer.cs | 63 ++++++++++++++++--- .../HyperionSerializerSetup.cs | 38 ++++++++--- .../reference.conf | 4 ++ 6 files changed, 191 insertions(+), 18 deletions(-) diff --git a/src/common.props b/src/common.props index 4f4f16066c1..5f69676a5e7 100644 --- a/src/common.props +++ b/src/common.props @@ -12,7 +12,7 @@ 2.4.1 17.0.0 - 0.11.2 + 0.12.0 [12.0.3,) 2.0.1 3.17.3 diff --git a/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionConfigTests.cs b/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionConfigTests.cs index 2d2ed8a0727..33f8ccd1f84 100644 --- a/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionConfigTests.cs +++ b/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionConfigTests.cs @@ -8,10 +8,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; using Akka.Actor; using Akka.Configuration; using FluentAssertions; using Hyperion; +using Hyperion.Internal; using Xunit; namespace Akka.Serialization.Hyperion.Tests @@ -36,6 +38,7 @@ public void Hyperion_serializer_should_have_correct_defaults() Assert.True(serializer.Settings.PreserveObjectReferences); Assert.Equal("NoKnownTypes", serializer.Settings.KnownTypesProvider.Name); Assert.True(serializer.Settings.DisallowUnsafeType); + Assert.Equal(serializer.Settings.TypeFilter, DisabledTypeFilter.Instance); } } @@ -52,6 +55,7 @@ public void Hyperion_serializer_should_allow_to_setup_custom_flags() preserve-object-references = false version-tolerance = false disallow-unsafe-type = false + allowed-types = [""Akka.Serialization.Hyperion.Tests.HyperionConfigTests+ClassA, Akka.Serialization.Hyperion.Tests""] } } "); @@ -62,9 +66,52 @@ public void Hyperion_serializer_should_allow_to_setup_custom_flags() Assert.False(serializer.Settings.PreserveObjectReferences); Assert.Equal("NoKnownTypes", serializer.Settings.KnownTypesProvider.Name); Assert.False(serializer.Settings.DisallowUnsafeType); + Assert.Equal("Akka.Serialization.Hyperion.Tests.HyperionConfigTests+ClassA, Akka.Serialization.Hyperion.Tests", ((TypeFilter) serializer.Settings.TypeFilter).FilteredTypes.First()); } } + [Theory] + [MemberData(nameof(TypeFilterObjectFactory))] + public void TypeFilter_defined_in_config_should_filter_serializer_properly(object sampleObject, bool shouldSucceed) + { + var config = ConfigurationFactory.ParseString(@" + akka.actor { + serializers.hyperion = ""Akka.Serialization.HyperionSerializer, Akka.Serialization.Hyperion"" + serialization-bindings { + ""System.Object"" = hyperion + } + serialization-settings.hyperion { + preserve-object-references = false + version-tolerance = false + disallow-unsafe-type = true + allowed-types = [ + ""Akka.Serialization.Hyperion.Tests.HyperionConfigTests+ClassA, Akka.Serialization.Hyperion.Tests"" + ""Akka.Serialization.Hyperion.Tests.HyperionConfigTests+ClassB, Akka.Serialization.Hyperion.Tests"" + ] + } + } + "); + using (var system = ActorSystem.Create(nameof(HyperionConfigTests), config)) + { + var serializer = (HyperionSerializer)system.Serialization.FindSerializerForType(typeof(object)); + + ((TypeFilter)serializer.Settings.TypeFilter).FilteredTypes.Count.Should().Be(2); + var serialized = serializer.ToBinary(sampleObject); + object deserialized = null; + Action act = () => deserialized = serializer.FromBinary(serialized); + if (shouldSucceed) + { + act.Should().NotThrow(); + deserialized.GetType().Should().Be(sampleObject.GetType()); + } + else + { + act.Should().Throw() + .WithInnerException(); + } + } + } + [Fact] public void Hyperion_serializer_should_allow_to_setup_custom_types_provider_with_default_constructor() { @@ -198,6 +245,18 @@ public void Hyperion_serializer_should_allow_to_setup_surrogates() } } + public static IEnumerable TypeFilterObjectFactory() + { + yield return new object[] { new ClassA(), true }; + yield return new object[] { new ClassB(), true }; + yield return new object[] { new ClassC(), false }; + } + + public class ClassA { } + + public class ClassB { } + + public class ClassC { } } class DummyTypesProvider : IKnownTypesProvider diff --git a/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionSerializerSetupSpec.cs b/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionSerializerSetupSpec.cs index 6c203dc7e46..a5e0cc8be2e 100644 --- a/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionSerializerSetupSpec.cs +++ b/src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionSerializerSetupSpec.cs @@ -16,6 +16,7 @@ using Xunit.Abstractions; using FluentAssertions; using Hyperion; +using Hyperion.Internal; namespace Akka.Serialization.Hyperion.Tests { @@ -134,6 +135,35 @@ public void Setup_disallow_unsafe_type_should_work(object dangerousObject, Type serializer.Invoking(s => s.FromBinary(serialized, type)).Should().Throw(); } + [Theory] + [MemberData(nameof(TypeFilterObjectFactory))] + public void Setup_TypeFilter_should_filter_types_properly(object sampleObject, bool shouldSucceed) + { + var setup = HyperionSerializerSetup.Empty + .WithTypeFilter(TypeFilterBuilder.Create() + .Include() + .Include() + .Build()); + + var settings = setup.ApplySettings(HyperionSerializerSettings.Default); + var serializer = new HyperionSerializer((ExtendedActorSystem)Sys, settings); + + ((TypeFilter)serializer.Settings.TypeFilter).FilteredTypes.Count.Should().Be(2); + var serialized = serializer.ToBinary(sampleObject); + object deserialized = null; + Action act = () => deserialized = serializer.FromBinary(serialized); + if (shouldSucceed) + { + act.Should().NotThrow(); + deserialized.GetType().Should().Be(sampleObject.GetType()); + } + else + { + act.Should().Throw() + .WithInnerException(); + } + } + public static IEnumerable DangerousObjectFactory() { var isWindow = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); @@ -150,5 +180,18 @@ public static IEnumerable DangerousObjectFactory() #endif yield return new object[]{ new ClaimsIdentity(), typeof(ClaimsIdentity)}; } + + public static IEnumerable TypeFilterObjectFactory() + { + yield return new object[] { new ClassA(), true }; + yield return new object[] { new ClassB(), true }; + yield return new object[] { new ClassC(), false }; + } + + public class ClassA { } + + public class ClassB { } + + public class ClassC { } } } diff --git a/src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializer.cs b/src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializer.cs index d7f85caa4f1..bd8c0962700 100644 --- a/src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializer.cs +++ b/src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializer.cs @@ -87,7 +87,8 @@ public HyperionSerializer(ExtendedActorSystem system, HyperionSerializerSettings knownTypes: provider.GetKnownTypes(), ignoreISerializable:true, packageNameOverrides: settings.PackageNameOverrides, - disallowUnsafeTypes: settings.DisallowUnsafeType)); + disallowUnsafeTypes: settings.DisallowUnsafeType, + typeFilter: settings.TypeFilter)); } /// @@ -166,7 +167,8 @@ public sealed class HyperionSerializerSettings knownTypesProvider: typeof(NoKnownTypes), packageNameOverrides: new List>(), surrogates: new Surrogate[0], - disallowUnsafeType: true); + disallowUnsafeType: true, + typeFilter: DisabledTypeFilter.Instance); /// /// Creates a new instance of using provided HOCON config. @@ -227,7 +229,22 @@ public static HyperionSerializerSettings Create(Config config) surrogates.Add((Surrogate)Activator.CreateInstance(surrogateType)); } - + + var typeFilter = (ITypeFilter) DisabledTypeFilter.Instance; + var allowedList = config.GetStringList("allowed-types"); + if (allowedList.Count > 0) + { + var filterBuilder = TypeFilterBuilder.Create(); + foreach (var allowedFqcn in allowedList) + { + var allowedType = Type.GetType(allowedFqcn); + if (allowedType is null) + throw new ConfigurationException($"Could not load type [{allowedFqcn}] from allowed-type."); + filterBuilder.Include(allowedType); + } + + typeFilter = filterBuilder.Build(); + } return new HyperionSerializerSettings( preserveObjectReferences: config.GetBoolean("preserve-object-references", true), @@ -235,7 +252,8 @@ public static HyperionSerializerSettings Create(Config config) knownTypesProvider: type, packageNameOverrides: packageNameOverrides, surrogates: surrogates, - disallowUnsafeType: config.GetBoolean("disallow-unsafe-type", true)); + disallowUnsafeType: config.GetBoolean("disallow-unsafe-type", true), + typeFilter: typeFilter); } /// @@ -278,6 +296,12 @@ public static HyperionSerializerSettings Create(Config config) /// public readonly bool DisallowUnsafeType; + /// + /// If set, Hyperion serializer will use the to filter the types that are + /// being deserialized during run-time + /// + public readonly ITypeFilter TypeFilter; + /// /// Creates a new instance of a . /// @@ -287,7 +311,7 @@ public static HyperionSerializerSettings Create(Config config) /// Raised when `known-types-provider` type doesn't implement interface. [Obsolete] public HyperionSerializerSettings(bool preserveObjectReferences, bool versionTolerance, Type knownTypesProvider) - : this(preserveObjectReferences, versionTolerance, knownTypesProvider, new List>(), new Surrogate[0], true) + : this(preserveObjectReferences, versionTolerance, knownTypesProvider, new List>(), new Surrogate[0], true, DisabledTypeFilter.Instance) { } /// @@ -304,7 +328,7 @@ public HyperionSerializerSettings( bool versionTolerance, Type knownTypesProvider, IEnumerable> packageNameOverrides) - : this(preserveObjectReferences, versionTolerance, knownTypesProvider, packageNameOverrides, new Surrogate[0], true) + : this(preserveObjectReferences, versionTolerance, knownTypesProvider, packageNameOverrides, new Surrogate[0], true, DisabledTypeFilter.Instance) { } /// @@ -322,7 +346,27 @@ public HyperionSerializerSettings( Type knownTypesProvider, IEnumerable> packageNameOverrides, IEnumerable surrogates) - : this(preserveObjectReferences, versionTolerance, knownTypesProvider, packageNameOverrides, surrogates, true) + : this(preserveObjectReferences, versionTolerance, knownTypesProvider, packageNameOverrides, surrogates, true, DisabledTypeFilter.Instance) + { } + + /// + /// Creates a new instance of a . + /// + /// Flag which determines if serializer should keep track of references in serialized object graph. + /// Flag which determines if field data should be serialized as part of type manifest. + /// Type implementing to be used to determine a list of types implicitly known by all cooperating serializer. + /// An array of package name overrides for cross platform compatibility + /// A list of instances that are used to de/serialize complex objects into a much simpler serialized objects. + /// Block unsafe types from being deserialized. + /// Raised when `known-types-provider` type doesn't implement interface. + public HyperionSerializerSettings( + bool preserveObjectReferences, + bool versionTolerance, + Type knownTypesProvider, + IEnumerable> packageNameOverrides, + IEnumerable surrogates, + bool disallowUnsafeType) + : this(preserveObjectReferences, versionTolerance, knownTypesProvider, packageNameOverrides, surrogates, disallowUnsafeType, DisabledTypeFilter.Instance) { } /// @@ -334,6 +378,7 @@ public HyperionSerializerSettings( /// An array of package name overrides for cross platform compatibility /// A list of instances that are used to de/serialize complex objects into a much simpler serialized objects. /// Block unsafe types from being deserialized. + /// A instance that will filter types from being deserialized. /// Raised when `known-types-provider` type doesn't implement interface. public HyperionSerializerSettings( bool preserveObjectReferences, @@ -341,7 +386,8 @@ public HyperionSerializerSettings( Type knownTypesProvider, IEnumerable> packageNameOverrides, IEnumerable surrogates, - bool disallowUnsafeType) + bool disallowUnsafeType, + ITypeFilter typeFilter) { knownTypesProvider = knownTypesProvider ?? typeof(NoKnownTypes); if (!typeof(IKnownTypesProvider).IsAssignableFrom(knownTypesProvider)) @@ -353,6 +399,7 @@ public HyperionSerializerSettings( PackageNameOverrides = packageNameOverrides; Surrogates = surrogates; DisallowUnsafeType = disallowUnsafeType; + TypeFilter = typeFilter; } } } diff --git a/src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializerSetup.cs b/src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializerSetup.cs index d7c781b7731..d5319238fd3 100644 --- a/src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializerSetup.cs +++ b/src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializerSetup.cs @@ -17,20 +17,20 @@ namespace Akka.Serialization.Hyperion public class HyperionSerializerSetup : Setup { public static readonly HyperionSerializerSetup Empty = - new HyperionSerializerSetup(Option.None, Option.None, null, null, null, Option.None); + new HyperionSerializerSetup(Option.None, Option.None, null, null, null, Option.None, DisabledTypeFilter.Instance); public static HyperionSerializerSetup Create( bool preserveObjectReferences, bool versionTolerance, Type knownTypesProvider) - => new HyperionSerializerSetup(preserveObjectReferences, versionTolerance, knownTypesProvider, null, null, Option.None); + => new HyperionSerializerSetup(preserveObjectReferences, versionTolerance, knownTypesProvider, null, null, Option.None, DisabledTypeFilter.Instance); public static HyperionSerializerSetup Create( bool preserveObjectReferences, bool versionTolerance, Type knownTypesProvider, IEnumerable> packageNameOverrides) - => new HyperionSerializerSetup(preserveObjectReferences, versionTolerance, knownTypesProvider, packageNameOverrides, null, Option.None); + => new HyperionSerializerSetup(preserveObjectReferences, versionTolerance, knownTypesProvider, packageNameOverrides, null, Option.None, DisabledTypeFilter.Instance); public static HyperionSerializerSetup Create( bool preserveObjectReferences, @@ -38,7 +38,7 @@ public static HyperionSerializerSetup Create( Type knownTypesProvider, IEnumerable> packageNameOverrides, IEnumerable surrogates) - => new HyperionSerializerSetup(preserveObjectReferences, versionTolerance, knownTypesProvider, packageNameOverrides, surrogates, Option.None); + => new HyperionSerializerSetup(preserveObjectReferences, versionTolerance, knownTypesProvider, packageNameOverrides, surrogates, Option.None, DisabledTypeFilter.Instance); public static HyperionSerializerSetup Create( bool preserveObjectReferences, @@ -47,7 +47,17 @@ public static HyperionSerializerSetup Create( IEnumerable> packageNameOverrides, IEnumerable surrogates, bool disallowUnsafeType) - => new HyperionSerializerSetup(preserveObjectReferences, versionTolerance, knownTypesProvider, packageNameOverrides, surrogates, disallowUnsafeType); + => new HyperionSerializerSetup(preserveObjectReferences, versionTolerance, knownTypesProvider, packageNameOverrides, surrogates, disallowUnsafeType, DisabledTypeFilter.Instance); + + public static HyperionSerializerSetup Create( + bool preserveObjectReferences, + bool versionTolerance, + Type knownTypesProvider, + IEnumerable> packageNameOverrides, + IEnumerable surrogates, + bool disallowUnsafeType, + ITypeFilter typeFilter) + => new HyperionSerializerSetup(preserveObjectReferences, versionTolerance, knownTypesProvider, packageNameOverrides, surrogates, disallowUnsafeType, typeFilter); private HyperionSerializerSetup( Option preserveObjectReferences, @@ -55,7 +65,8 @@ private HyperionSerializerSetup( Type knownTypesProvider, IEnumerable> packageNameOverrides, IEnumerable surrogates, - Option disallowUnsafeType) + Option disallowUnsafeType, + ITypeFilter typeFilter) { PreserveObjectReferences = preserveObjectReferences; VersionTolerance = versionTolerance; @@ -63,6 +74,7 @@ private HyperionSerializerSetup( PackageNameOverrides = packageNameOverrides; Surrogates = surrogates; DisallowUnsafeType = disallowUnsafeType; + TypeFilter = typeFilter; } /// @@ -105,6 +117,8 @@ private HyperionSerializerSetup( /// from being deserialized during run-time. Defaults to true. /// public Option DisallowUnsafeType { get; } + + public ITypeFilter TypeFilter { get; } internal HyperionSerializerSettings ApplySettings(HyperionSerializerSettings settings) => new HyperionSerializerSettings( @@ -113,7 +127,8 @@ internal HyperionSerializerSettings ApplySettings(HyperionSerializerSettings set KnownTypesProvider ?? settings.KnownTypesProvider, PackageNameOverrides ?? settings.PackageNameOverrides, Surrogates ?? settings.Surrogates, - DisallowUnsafeType.HasValue ? DisallowUnsafeType.Value : settings.DisallowUnsafeType + DisallowUnsafeType.HasValue ? DisallowUnsafeType.Value : settings.DisallowUnsafeType, + TypeFilter ?? settings.TypeFilter ); public HyperionSerializerSetup WithPreserveObjectReference(bool preserveObjectReference) @@ -137,13 +152,17 @@ public HyperionSerializerSetup WithSurrogates(IEnumerable surrogates) public HyperionSerializerSetup WithDisallowUnsafeType(bool disallowUnsafeType) => Copy(disallowUnsafeType: disallowUnsafeType); + public HyperionSerializerSetup WithTypeFilter(ITypeFilter typeFilter) + => Copy(typeFilter: typeFilter); + private HyperionSerializerSetup Copy( bool? preserveObjectReferences = null, bool? versionTolerance = null, Type knownTypesProvider = null, IEnumerable> packageNameOverrides = null, IEnumerable surrogates = null, - bool? disallowUnsafeType = null + bool? disallowUnsafeType = null, + ITypeFilter typeFilter = null ) => new HyperionSerializerSetup( preserveObjectReferences ?? PreserveObjectReferences, @@ -151,6 +170,7 @@ private HyperionSerializerSetup Copy( knownTypesProvider ?? KnownTypesProvider, packageNameOverrides ?? PackageNameOverrides, surrogates ?? Surrogates, - disallowUnsafeType ?? DisallowUnsafeType); + disallowUnsafeType ?? DisallowUnsafeType, + typeFilter ?? TypeFilter); } } diff --git a/src/contrib/serializers/Akka.Serialization.Hyperion/reference.conf b/src/contrib/serializers/Akka.Serialization.Hyperion/reference.conf index 6817aca475f..eccf4724575 100644 --- a/src/contrib/serializers/Akka.Serialization.Hyperion/reference.conf +++ b/src/contrib/serializers/Akka.Serialization.Hyperion/reference.conf @@ -30,6 +30,10 @@ # constructor or constructor accepting an `ExtendedActorSystem` as its only parameter. known-types-provider = "Akka.Serialization.NoKnownTypes, Akka.Serialization.Hyperion" + # An array of fully qualified class names of types that are allowed to be deserialized + # during run-time. When left as an empty array, type filtering will be disabled. + allowed-types = [] + # A list of incompatible dll package name for deserializing types # between NetFx, .NET Core, and the new .NET # Used to map and rename/correct different dll names between different platforms From 811ab748a4fa682cecdc2d7a35e13f9d1d583f96 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Fri, 14 Jan 2022 22:38:49 +0700 Subject: [PATCH 27/30] Switch Akka.Cluster.Benchmark persistence benchmark from using SQLite to MemoryJournal to minimize external variable (#5514) --- .../Persistence/JournalWriteBenchmarks.cs | 2 +- .../Persistence/PersistenceInfrastructure.cs | 25 ++++++------------- .../Persistence/RecoveryBenchmark.cs | 4 +-- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Persistence/JournalWriteBenchmarks.cs b/src/benchmark/Akka.Cluster.Benchmarks/Persistence/JournalWriteBenchmarks.cs index 5730bf4abec..c62d2ba4737 100644 --- a/src/benchmark/Akka.Cluster.Benchmarks/Persistence/JournalWriteBenchmarks.cs +++ b/src/benchmark/Akka.Cluster.Benchmarks/Persistence/JournalWriteBenchmarks.cs @@ -39,7 +39,7 @@ public class JournalWriteBenchmarks [IterationSetup] public void Setup() { - var (connectionStr, config) = GenerateJournalConfig(); + var config = GenerateJournalConfig(); _sys1 = ActorSystem.Create("MySys", config); _doneActor = _sys1.ActorOf(Props.Create(() => new BenchmarkDoneActor(PersistentActors)), "done"); _persistentActors = new HashSet(); diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Persistence/PersistenceInfrastructure.cs b/src/benchmark/Akka.Cluster.Benchmarks/Persistence/PersistenceInfrastructure.cs index f8cc025365e..197f34be3d9 100644 --- a/src/benchmark/Akka.Cluster.Benchmarks/Persistence/PersistenceInfrastructure.cs +++ b/src/benchmark/Akka.Cluster.Benchmarks/Persistence/PersistenceInfrastructure.cs @@ -172,31 +172,22 @@ protected override bool ReceiveCommand(object message){ public static class PersistenceInfrastructure{ public static readonly AtomicCounter DbCounter = new AtomicCounter(0); - public static (string connectionString, Config hoconConfig) GenerateJournalConfig(){ - return GenerateJournalConfig(DbCounter.GetAndIncrement().ToString()); - } - - public static (string connectionString, Config hoconConfig) GenerateJournalConfig(string databaseId){ - // need to create a unique database instance each time benchmark is run so we don't pollute - // might need to disable shared cache - var connectionString = $"Datasource=memdb-journal-{databaseId}.db;Mode=Memory;Cache=Shared"; - + public static Config GenerateJournalConfig(){ var config = ConfigurationFactory.ParseString(@" akka { persistence.journal { - plugin = ""akka.persistence.journal.sqlite"" - sqlite { - class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistence.Sqlite"" + plugin = ""akka.persistence.journal.inmem"" + # In-memory journal plugin. + akka.persistence.journal.inmem { + # Class name of the plugin. + class = ""Akka.Persistence.Journal.MemoryJournal, Akka.Persistence"" + # Dispatcher for the plugin actor. plugin-dispatcher = ""akka.actor.default-dispatcher"" - table-name = event_journal - metadata-table-name = journal_metadata - auto-initialize = on - connection-string = """+ connectionString +@""" } } }"); - return (connectionString, config); + return config; } } diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Persistence/RecoveryBenchmark.cs b/src/benchmark/Akka.Cluster.Benchmarks/Persistence/RecoveryBenchmark.cs index 18a9368f46b..5c9de4b0d80 100644 --- a/src/benchmark/Akka.Cluster.Benchmarks/Persistence/RecoveryBenchmark.cs +++ b/src/benchmark/Akka.Cluster.Benchmarks/Persistence/RecoveryBenchmark.cs @@ -18,7 +18,7 @@ public class RecoveryBenchmark [Params(1, 10, 100)] public int PersistentActors; - [Params(100)] public int WriteMsgCount; + [Params(100, 1000)] public int WriteMsgCount; private ActorSystem _sys1; @@ -33,7 +33,7 @@ public class RecoveryBenchmark [GlobalSetup] public async Task GlobalSetup() { - var (connectionStr, config) = GenerateJournalConfig(); + var config = GenerateJournalConfig(); _sys1 = ActorSystem.Create("MySys", config); _doneActor = _sys1.ActorOf(Props.Create(() => new BenchmarkDoneActor(PersistentActors)), "done"); _persistentActors = new HashSet(); From 93c7031f10c27630b59e17487c6324fcb2d78ead Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Fri, 14 Jan 2022 10:00:28 -0600 Subject: [PATCH 28/30] cleaning up `async Task` internals inside `AsyncWriteJournal` (#5505) * cleaning up `async Task` internals inside `AsyncWriteJournal` --- .../Journal/AsyncWriteJournal.cs | 214 +++++++++--------- .../Journal/AsyncWriteProxy.cs | 34 +-- 2 files changed, 133 insertions(+), 115 deletions(-) diff --git a/src/core/Akka.Persistence/Journal/AsyncWriteJournal.cs b/src/core/Akka.Persistence/Journal/AsyncWriteJournal.cs index c6e4d0a0dcc..82c404b6254 100644 --- a/src/core/Akka.Persistence/Journal/AsyncWriteJournal.cs +++ b/src/core/Akka.Persistence/Journal/AsyncWriteJournal.cs @@ -217,22 +217,33 @@ protected bool ReceiveWriteJournal(object message) private void HandleDeleteMessagesTo(DeleteMessagesTo message) { var eventStream = Context.System.EventStream; - _breaker.WithCircuitBreaker(() => DeleteMessagesToAsync(message.PersistenceId, message.ToSequenceNr)) - .ContinueWith(t => !t.IsFaulted && !t.IsCanceled - ? new DeleteMessagesSuccess(message.ToSequenceNr) as object - : new DeleteMessagesFailure( - t.IsFaulted - ? TryUnwrapException(t.Exception) - : new OperationCanceledException( - "DeleteMessagesToAsync canceled, possibly due to timing out."), - message.ToSequenceNr), - _continuationOptions) - .PipeTo(message.PersistentActor) - .ContinueWith(t => + var self = Context.Self; + + async Task ProcessDelete() + { + try { - if (!t.IsFaulted && !t.IsCanceled && CanPublish) + await _breaker.WithCircuitBreaker(() => + DeleteMessagesToAsync(message.PersistenceId, message.ToSequenceNr)); + + message.PersistentActor.Tell(new DeleteMessagesSuccess(message.ToSequenceNr), self); + + if (CanPublish) + { eventStream.Publish(message); - }, _continuationOptions); + } + } + catch (Exception ex) + { + message.PersistentActor.Tell( + new DeleteMessagesFailure(TryUnwrapException(ex), message.ToSequenceNr), self); + } + } + + // instead of ContinueWith +#pragma warning disable CS4014 + ProcessDelete(); +#pragma warning restore CS4014 } private void HandleReplayMessages(ReplayMessages message) @@ -246,24 +257,35 @@ private void HandleReplayMessages(ReplayMessages message) var eventStream = Context.System.EventStream; var readHighestSequenceNrFrom = Math.Max(0L, message.FromSequenceNr - 1); - var promise = new TaskCompletionSource(); - _breaker.WithCircuitBreaker(() => ReadHighestSequenceNrAsync(message.PersistenceId, readHighestSequenceNrFrom)) - .ContinueWith(t => + + async Task ExecuteHighestSequenceNr() + { + void CompleteHighSeqNo(long highSeqNo) { - if (!t.IsFaulted && !t.IsCanceled) + replyTo.Tell(new RecoverySuccess(highSeqNo)); + + if (CanPublish) { - var highSequenceNr = t.Result; - var toSequenceNr = Math.Min(message.ToSequenceNr, highSequenceNr); - if (toSequenceNr <= 0L || message.FromSequenceNr > toSequenceNr) - { - promise.SetResult(highSequenceNr); - } - else - { - // Send replayed messages and replay result to persistentActor directly. No need - // to resequence replayed messages relative to written and looped messages. - // not possible to use circuit breaker here - ReplayMessagesAsync(context, message.PersistenceId, message.FromSequenceNr, toSequenceNr, message.Max, p => + eventStream.Publish(message); + } + } + + try + { + var highSequenceNr = await _breaker.WithCircuitBreaker(() => + ReadHighestSequenceNrAsync(message.PersistenceId, readHighestSequenceNrFrom)); + var toSequenceNr = Math.Min(message.ToSequenceNr, highSequenceNr); + if (toSequenceNr <= 0L || message.FromSequenceNr > toSequenceNr) + { + CompleteHighSeqNo(highSequenceNr); + } + else + { + // Send replayed messages and replay result to persistentActor directly. No need + // to resequence replayed messages relative to written and looped messages. + // not possible to use circuit breaker here + await ReplayMessagesAsync(context, message.PersistenceId, message.FromSequenceNr, toSequenceNr, + message.Max, p => { if (!p.IsDeleted) // old records from pre 1.0.7 may still have the IsDeleted flag { @@ -272,34 +294,28 @@ private void HandleReplayMessages(ReplayMessages message) replyTo.Tell(new ReplayedMessage(adaptedRepresentation), ActorRefs.NoSender); } } - }) - .ContinueWith(replayTask => - { - if (!replayTask.IsFaulted && !replayTask.IsCanceled) - promise.SetResult(highSequenceNr); - else - promise.SetException(replayTask.IsFaulted - ? TryUnwrapException(replayTask.Exception) - : new OperationCanceledException("ReplayMessagesAsync canceled, possibly due to timing out.")); - }, _continuationOptions); - } - } - else - { - promise.SetException(t.IsFaulted - ? TryUnwrapException(t.Exception) - : new OperationCanceledException("ReadHighestSequenceNrAsync canceled, possibly due to timing out.")); + }); + + CompleteHighSeqNo(highSequenceNr); } - }, _continuationOptions); - promise.Task - .ContinueWith(t => !t.IsFaulted - ? new RecoverySuccess(t.Result) as IJournalResponse - : new ReplayMessagesFailure(TryUnwrapException(t.Exception)), _continuationOptions) - .PipeTo(replyTo) - .ContinueWith(t => + } + catch (OperationCanceledException cx) + { + // operation failed because a CancellationToken was invoked + // wrap the original exception and throw it, with some additional callsite context + var newEx = new OperationCanceledException("ReplayMessagesAsync canceled, possibly due to timing out.", cx); + replyTo.Tell(new ReplayMessagesFailure(newEx)); + } + catch (Exception ex) { - if (!t.IsFaulted && CanPublish) eventStream.Publish(message); - }, _continuationOptions); + replyTo.Tell(new ReplayMessagesFailure(TryUnwrapException(ex))); + } + } + + // instead of ContinueWith +#pragma warning disable CS4014 + ExecuteHighestSequenceNr(); +#pragma warning restore CS4014 } /// @@ -330,33 +346,11 @@ private void HandleWriteMessages(WriteMessages message) var self = Self; _resequencerCounter += message.Messages.Aggregate(1, (acc, m) => acc + m.Size); var atomicWriteCount = message.Messages.OfType().Count(); - AtomicWrite[] prepared; - Task> writeResult; - Exception writeMessagesAsyncException = null; - try - { - prepared = PreparePersistentBatch(message.Messages).ToArray(); - // try in case AsyncWriteMessages throws - try - { - writeResult = _breaker.WithCircuitBreaker(() => WriteMessagesAsync(prepared)); - } - catch (Exception e) - { - writeResult = Task.FromResult((IImmutableList) null); - writeMessagesAsyncException = e; - } - } - catch (Exception e) - { - // exception from PreparePersistentBatch => rejected - writeResult = Task.FromResult((IImmutableList)Enumerable.Repeat(e, atomicWriteCount).ToImmutableList()); - } void Resequence(Func mapper, IImmutableList results) { var i = 0; - var enumerator = results != null ? results.GetEnumerator() : null; + var enumerator = results?.GetEnumerator(); foreach (var resequencable in message.Messages) { if (resequencable is AtomicWrite aw) @@ -370,44 +364,62 @@ void Resequence(Func mapper, IImmu foreach (var p in (IEnumerable)aw.Payload) { - _resequencer.Tell(new Desequenced(mapper(p, exception), counter + i + 1, message.PersistentActor, p.Sender)); + _resequencer.Tell(new Desequenced(mapper(p, exception), counter + i + 1, message.PersistentActor, p.Sender), self); i++; } } else { var loopMsg = new LoopMessageSuccess(resequencable.Payload, message.ActorInstanceId); - _resequencer.Tell(new Desequenced(loopMsg, counter + i + 1, message.PersistentActor, resequencable.Sender)); + _resequencer.Tell(new Desequenced(loopMsg, counter + i + 1, message.PersistentActor, resequencable.Sender), self); i++; } } } - writeResult - .ContinueWith(t => + async Task ExecuteBatch() + { + void ProcessResults(IImmutableList results) + { + // there should be no circumstances under which `writeResult` can be `null` + if (results != null && results.Count != atomicWriteCount) + throw new IllegalStateException($"AsyncWriteMessages return invalid number or results. " + + $"Expected [{atomicWriteCount}], but got [{results.Count}]."); + + _resequencer.Tell(new Desequenced(WriteMessagesSuccessful.Instance, counter, message.PersistentActor, self), self); + Resequence((x, exception) => exception == null + ? (object)new WriteMessageSuccess(x, message.ActorInstanceId) + : new WriteMessageRejected(x, exception, message.ActorInstanceId), results); + } + + try { - if (!t.IsFaulted && !t.IsCanceled && writeMessagesAsyncException == null) + var prepared = PreparePersistentBatch(message.Messages).ToArray(); + // try in case AsyncWriteMessages throws + try { - if (t.Result != null && t.Result.Count != atomicWriteCount) - throw new IllegalStateException($"AsyncWriteMessages return invalid number or results. Expected [{atomicWriteCount}], but got [{t.Result.Count}]."); + var writeResult = + await _breaker.WithCircuitBreaker(() => WriteMessagesAsync(prepared)).ConfigureAwait(false); - _resequencer.Tell(new Desequenced(WriteMessagesSuccessful.Instance, counter, message.PersistentActor, self)); - Resequence((x, exception) => exception == null - ? (object)new WriteMessageSuccess(x, message.ActorInstanceId) - : new WriteMessageRejected(x, exception, message.ActorInstanceId), t.Result); + ProcessResults(writeResult); } - else + catch (Exception e) // this is the old writeMessagesAsyncException { - var exception = writeMessagesAsyncException != null - ? writeMessagesAsyncException - : (t.IsFaulted - ? TryUnwrapException(t.Exception) - : new OperationCanceledException( - "WriteMessagesAsync canceled, possibly due to timing out.")); - _resequencer.Tell(new Desequenced(new WriteMessagesFailed(exception, atomicWriteCount), counter, message.PersistentActor, self)); - Resequence((x, _) => new WriteMessageFailure(x, exception, message.ActorInstanceId), null); + _resequencer.Tell(new Desequenced(new WriteMessagesFailed(e, atomicWriteCount), counter, message.PersistentActor, self), self); + Resequence((x, _) => new WriteMessageFailure(x, e, message.ActorInstanceId), null); } - }, _continuationOptions); + } + catch (Exception ex) + { + // exception from PreparePersistentBatch => rejected + ProcessResults(Enumerable.Repeat(ex, atomicWriteCount).ToImmutableList()); + } + } + + // Using an async local function instead of ContinueWith +#pragma warning disable CS4014 + ExecuteBatch(); +#pragma warning restore CS4014 } internal sealed class Desequenced @@ -461,7 +473,7 @@ private Desequenced Resequence(Desequenced desequenced) } var delivered = _delivered + 1; - if (_delayed.TryGetValue(delivered, out Desequenced d)) + if (_delayed.TryGetValue(delivered, out var d)) { _delayed.Remove(delivered); return d; diff --git a/src/core/Akka.Persistence/Journal/AsyncWriteProxy.cs b/src/core/Akka.Persistence/Journal/AsyncWriteProxy.cs index ba4bea1f8d5..15b08928b7f 100644 --- a/src/core/Akka.Persistence/Journal/AsyncWriteProxy.cs +++ b/src/core/Akka.Persistence/Journal/AsyncWriteProxy.cs @@ -301,22 +301,28 @@ protected internal override bool AroundReceive(Receive receive, object message) if (!(message is InitTimeout)) return base.AroundReceive(receive, message); } - else if (message is SetStore) + else switch (message) { - _store = ((SetStore) message).Store; - Stash.UnstashAll(); - _isInitialized = true; - } - else if (message is InitTimeout) - { - _isInitTimedOut = true; - Stash.UnstashAll(); // will trigger appropriate failures - } - else if (_isInitTimedOut) - { - return base.AroundReceive(receive, message); + case SetStore store: + _store = store.Store; + Stash.UnstashAll(); + _isInitialized = true; + break; + case InitTimeout _: + _isInitTimedOut = true; + Stash.UnstashAll(); // will trigger appropriate failures + break; + default: + { + if (_isInitTimedOut) + { + return base.AroundReceive(receive, message); + } + else Stash.Stash(); + + break; + } } - else Stash.Stash(); return true; } From 54163d64e5e50f3621e4e1074a5d36425bcc9301 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Wed, 19 Jan 2022 01:59:20 +0700 Subject: [PATCH 29/30] Update RELEASE_NOTES.md for 1.4.32 release (#5518) --- RELEASE_NOTES.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b5b4c028093..d23e896f1a3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,8 +1,38 @@ -#### 1.4.32 December 20 2021 #### -**Placeholder for nightlies** +#### 1.4.32 January 19 2022 #### +Akka.NET v1.4.32 is a minor release that contains some API improvements. Most of the changes have been aimed at improving our web documentation and code cleanup to modernize some of our code. One big improvement in this version release is the Hyperion serialization update. + +Hyperion 0.12.0 introduces a new deserialization security mechanism to allow users to selectively filter allowed types during deserialization to prevent deserialization of untrusted data described [here](https://cwe.mitre.org/data/definitions/502.html). This new feature is exposed in Akka.NET in HOCON through the new [`akka.actor.serialization-settings.hyperion.allowed-types`](https://github.com/akkadotnet/akka.net/blob/dev/src/contrib/serializers/Akka.Serialization.Hyperion/reference.conf#L33-L35) settings or programmatically through the new `WithTypeFilter` method in the `HyperionSerializerSetup` class. + +The simplest way to programmatically describe the type filter is to use the convenience class `TypeFilterBuilder`: + +```c# +var typeFilter = TypeFilterBuilder.Create() + .Include() + .Include() + .Build(); +var setup = HyperionSerializerSetup.Default + .WithTypeFilter(typeFilter); +``` + +You can also create your own implementation of `ITypeFilter` and pass an instance of it into the `WithTypeFilter` method. + +For complete documentation, please read the Hyperion [readme on filtering types for secure deserialization.](https://github.com/akkadotnet/Hyperion#whitelisting-types-on-deserialization) + +* [Akka.Streams: Added Flow.LazyInitAsync and Sink.LazyInitSink to replace Sink.LazyInit](https://github.com/akkadotnet/akka.net/pull/5476) +* [Akka.Serialization.Hyperion: Implement the new ITypeFilter security feature](https://github.com/akkadotnet/akka.net/pull/5510) + +| COMMITS | LOC+ | LOC- | AUTHOR | +| --- | --- | --- | --- | +| 11 | 1752 | 511 | Aaron Stannard | +| 8 | 1433 | 534 | Gregorius Soedharmo | +| 3 | 754 | 222 | Ismael Hamed | +| 2 | 3 | 6 | Brah McDude | +| 2 | 227 | 124 | Ebere Abanonu | +| 1 | 331 | 331 | Sean Killeen | +| 1 | 1 | 1 | TangkasOka | #### 1.4.31 December 20 2021 #### -Akka.NET v1.4.30 is a minor release that contains some bug fixes. +Akka.NET v1.4.31 is a minor release that contains some bug fixes. Akka.NET v1.4.30 contained a breaking change that broke binary compatibility with all Akka.DI plugins. Even though those plugins are deprecated that change is not compatible with our SemVer standards From 73f7eb206c8bd8b8e2526c760d111df0fb5eb34c Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 18 Jan 2022 13:01:02 -0600 Subject: [PATCH 30/30] Added link to v1.4.32 milestone to `RELEASE_NOTES.md` --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d23e896f1a3..f809f6c392f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -21,6 +21,8 @@ For complete documentation, please read the Hyperion [readme on filtering types * [Akka.Streams: Added Flow.LazyInitAsync and Sink.LazyInitSink to replace Sink.LazyInit](https://github.com/akkadotnet/akka.net/pull/5476) * [Akka.Serialization.Hyperion: Implement the new ITypeFilter security feature](https://github.com/akkadotnet/akka.net/pull/5510) +If you want to see the [full set of changes made in Akka.NET v1.4.32, click here](https://github.com/akkadotnet/akka.net/milestone/63). + | COMMITS | LOC+ | LOC- | AUTHOR | | --- | --- | --- | --- | | 11 | 1752 | 511 | Aaron Stannard |

This page has been moved to Multi-Node Testing Distributed Akka.NET Applications.