diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2d0668833d8..b65fd085b90 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/nightly-nuget.yml b/.github/workflows/nightly-nuget.yml index d8513ab884c..513c842b858 100644 --- a/.github/workflows/nightly-nuget.yml +++ b/.github/workflows/nightly-nuget.yml @@ -14,7 +14,7 @@ jobs: runs-on: windows-latest if: github.repository == 'akkadotnet/akka.net' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup .NET 5 uses: actions/setup-dotnet@v4 with: diff --git a/.gitignore b/.gitignore index 31a8b545649..38ed7e158f9 100644 --- a/.gitignore +++ b/.gitignore @@ -234,3 +234,6 @@ launchSettings.json .ionide/symbolCache.db *.mdc + +# Claude worktree management +.claude-wt/worktrees diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..e864b0ab9e7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,136 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build and Test Commands + +### Building the Solution +```bash +# Standard build +dotnet build +dotnet build -c Release + +# Build with warnings as errors (CI validation) +dotnet build -warnaserror +``` + +### Running Tests +```bash +# Run all tests +dotnet test -c Release + +# Run tests for specific framework +dotnet test -c Release --framework net8.0 +dotnet test -c Release --framework net48 + +# Run specific test by name +dotnet test -c Release --filter DisplayName="TestName" + +# Run tests in a specific project +dotnet test path/to/project.csproj -c Release +``` + +### Incremental Testing (for changed code only) +```bash +# Run only unit tests for changed projects +dotnet incrementalist run --config .incrementalist/testsOnly.json -- test -c Release --no-build --framework net8.0 + +# Run only multi-node tests for changed projects +dotnet incrementalist run --config .incrementalist/mutliNodeOnly.json -- test -c Release --no-build --framework net8.0 +``` + +### Code Quality +```bash +# Format check +dotnet format --verify-no-changes + +# API compatibility check +dotnet test -c Release src/core/Akka.API.Tests +``` + +### Documentation +```bash +# Generate API documentation +dotnet docfx metadata ./docs/docfx.json --warningsAsErrors +dotnet docfx build ./docs/docfx.json --warningsAsErrors +``` + +## High-Level Architecture + +### Project Structure +- **`/src/core/`** - Core actor framework components + - `Akka/` - Base actor system, routing, dispatchers, configuration + - `Akka.Remote/` - Distributed actor communication and serialization + - `Akka.Cluster/` - Clustering, gossip protocols, distributed coordination + - `Akka.Persistence/` - Event sourcing, snapshots, journals + - `Akka.Streams/` - Reactive streams with backpressure + - `Akka.TestKit/` - Testing utilities for actor systems + +- **`/src/contrib/`** - Contributed modules (DI integrations, serializers, cluster extensions) +- **`/src/benchmark/`** - Performance benchmarks using BenchmarkDotNet +- **`/src/examples/`** - Sample applications demonstrating patterns + +### Key Architectural Concepts +- **Actor Model**: Message-driven, hierarchical supervision, location transparency +- **Fault Tolerance**: Supervision strategies, let-it-crash philosophy +- **Distribution**: Remote actors, clustering, sharding +- **Reactive Streams**: Backpressure-aware stream processing +- **Event Sourcing**: Persistence with journals and snapshots + +### Testing Patterns +- Inherit from `AkkaSpec` or use `TestKit` for actor tests +- Use `TestProbe` for creating lightweight test actors +- Use `EventFilter` for asserting log messages +- Pass `ITestOutputHelper output` to test constructors for debugging +- Multi-node tests use separate projects (*.Tests.MultiNode.csproj) + +## Code Style and Conventions + +### C# Style +- Allman style braces (opening brace on new line) +- 4 spaces indentation, no tabs +- Private fields prefixed with underscore `_fieldName` +- Use `var` when type is apparent +- Default to `sealed` classes and records +- Enable `#nullable enable` in new/modified files +- Never use `async void`, `.Result`, or `.Wait()` +- Always pass `CancellationToken` through async call chains + +### API Design +- Maintain compatibility with JVM Akka while being .NET idiomatic +- Use `Task` instead of Future, `TimeSpan` instead of Duration +- Extend-only design - don't modify existing public APIs +- Preserve wire format compatibility for serialization +- Include unit tests with all changes + +### Test Naming +- Use `DisplayName` attribute for descriptive test names +- Follow pattern: `Should_ExpectedBehavior_When_Condition` + +## Development Workflow + +### Git Branches +- **`dev`** - Main development branch (default for PRs) +- **`v1.4`**, **`v1.3`**, etc. - Version maintenance branches for older releases +- Feature branches: `feature/description` +- Bugfix branches: `fix/description` + +### Making Changes +1. Always read existing code patterns in the module you're modifying +2. Follow existing conventions for that specific module +3. Add/update tests for your changes +4. Run incremental tests before committing +5. Ensure API compatibility tests pass for core changes + +### Target Frameworks +- **.NET 8.0** - Primary target +- **.NET 6.0** - Library compatibility +- **.NET Framework 4.8** - Legacy support +- **.NET Standard 2.0** - Library compatibility + +## Important Files +- `Directory.Build.props` - MSBuild properties, package versions +- `global.json` - .NET SDK version (8.0.403) +- `xunit.runner.json` - Test configuration (60s timeout, no parallelization) +- `.incrementalist/*.json` - Incremental build configurations +- `RELEASE_NOTES.md` - Version history and changelog \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index cdf65f123b5..745654af745 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -42,10 +42,11 @@ 1.5.25 [6.0.*,) [6.0.*,) - 0.3.2 + 0.3.3 akka;actors;actor model;Akka;concurrency + true true diff --git a/MULTINODE_TEST_ASYNC_MIGRATION.md b/MULTINODE_TEST_ASYNC_MIGRATION.md new file mode 100644 index 00000000000..7d03b8e4eaf --- /dev/null +++ b/MULTINODE_TEST_ASYNC_MIGRATION.md @@ -0,0 +1,254 @@ +# Multi-Node Test Async Migration Guide + +## Overview +This guide helps migrate multi-node tests from blocking synchronous calls to async/await patterns to prevent thread pool starvation and test timeouts in CI environments. + +## Why This Migration Is Necessary +- **Root Cause**: Blocking `.Wait()` calls on TestConductor operations cause thread pool starvation +- **Symptoms**: 20+ second timeout failures in CI environments +- **Solution**: Replace all blocking calls with proper async/await patterns + +## Migration Patterns to Look For + +### 1. TestConductor Blocking Calls +**Look for these patterns:** +```csharp +// OLD - Blocking +TestConductor.Exit(role, 0).Wait(); +TestConductor.Blackhole(node1, node2, direction).Wait(); +TestConductor.PassThrough(node1, node2, direction).Wait(); +TestConductor.Throttle(node1, node2, direction, rate).Wait(); +TestConductor.Disconnect(node1, node2).Wait(); +TestConductor.Shutdown(node, abort).Wait(); +TestConductor.RemoveNode(node).Wait(); + +// NEW - Async +await TestConductor.ExitAsync(role, 0); +await TestConductor.BlackholeAsync(node1, node2, direction); +await TestConductor.PassThroughAsync(node1, node2, direction); +await TestConductor.ThrottleAsync(node1, node2, direction, rate); +await TestConductor.DisconnectAsync(node1, node2); +await TestConductor.ShutdownAsync(node, abort); +await TestConductor.RemoveNodeAsync(node); +``` + +### 2. Barrier Synchronization +**Look for:** +```csharp +// OLD +EnterBarrier("barrier-name"); +EnterBarrier("barrier-1", "barrier-2"); + +// NEW +await EnterBarrierAsync("barrier-name"); +await EnterBarrierAsync("barrier-1", "barrier-2"); +``` + +### 3. RunOn with Async Operations +**Look for:** +```csharp +// OLD +RunOn(() => { + TestConductor.Exit(role, 0).Wait(); +}, roles); + +// NEW +await RunOnAsync(async () => { + await TestConductor.ExitAsync(role, 0); +}, roles); +``` + +### 4. Within Blocks +**Look for:** +```csharp +// OLD +Within(TimeSpan.FromSeconds(30), () => { + // operations + EnterBarrier("done"); +}); + +// NEW +await WithinAsync(TimeSpan.FromSeconds(30), async () => { + // operations + await EnterBarrierAsync("done"); +}); +``` + +### 5. Test Method Signatures +**Change:** +```csharp +// OLD +[MultiNodeFact] +public void TestName() + +// NEW +[MultiNodeFact] +public async Task TestName() +``` + +### 6. Helper Method Signatures +**Change:** +```csharp +// OLD +public void HelperMethod() + +// NEW +public async Task HelperMethod() +``` + +## Required Imports +Add if missing: +```csharp +using System.Threading.Tasks; +``` + +## Migration Checklist + +### ✅ Completed Tests +- [x] StressSpec +- [x] LeaderElectionSpec +- [x] ClusterAccrualFailureDetectorSpec +- [x] TestConductorSpec (in Remote.Tests.MultiNode) +- [x] RemoteNodeDeathWatchSpec (in Remote.Tests.MultiNode) + +### Core Tests - Akka.Cluster.Tests.MultiNode +- [ ] AttemptSysMsgRedeliverySpec +- [ ] ClientDowningNodeThatIsUnreachableSpec +- [ ] ClusterDeathWatchSpec +- [ ] ConvergenceSpec +- [ ] LeaderDowningAllOtherNodesSpec +- [ ] LeaderDowningNodeThatIsUnreachableSpec +- [ ] SingletonClusterSpec +- [ ] SplitBrainResolverDowningSpec +- [ ] SplitBrainSpec +- [ ] SurviveNetworkInstabilitySpec +- [ ] UnreachableNodeJoinsAgainSpec + +### Core Tests - Akka.Cluster.Tests.MultiNode/Routing +- [ ] ClusterRoundRobinSpec + +### Core Tests - Akka.Cluster.Tests.MultiNode/SBR (Split Brain Resolver) +- [ ] DownAllIndirectlyConnected5NodeSpec +- [ ] DownAllUnstable5NodeSpec +- [ ] IndirectlyConnected3NodeSpec +- [ ] IndirectlyConnected5NodeSpec +- [ ] LeaseMajority5NodeSpec + +### Core Tests - Akka.Remote.Tests.MultiNode +- [ ] RemoteNodeRestartGateSpec +- [ ] RemoteNodeShutdownAndComesBackSpec +- [ ] RemoteReDeploymentSpec +- [ ] RemoteRestartedQuarantinedSpec + +### Contrib Tests - Akka.Cluster.Sharding.Tests.MultiNode +- [ ] ClusterShardCoordinatorDowning2Spec +- [ ] ClusterShardCoordinatorDowningSpec +- [ ] ClusterShardingFailureSpec +- [ ] ClusterShardingRememberEntitiesNewExtractorSpec +- [ ] ClusterShardingRememberEntitiesSpec +- [ ] ClusterShardingSingleShardPerEntitySpec +- [ ] ClusterShardingSpec + +### Contrib Tests - Akka.Cluster.Tools.Tests.MultiNode +- [ ] ClusterClient/ClusterClientDiscoverySpec +- [ ] ClusterClient/ClusterClientSpec +- [ ] PublishSubscribe/DistributedPubSubMediatorSpec +- [x] PublishSubscribe/DistributedPubSubRestartSpec +- [ ] Singleton/ClusterSingletonManagerDownedSpec +- [ ] Singleton/ClusterSingletonManagerSpec + +### Tests That May Need EnterBarrier -> EnterBarrierAsync Migration +Additional tests that use EnterBarrier but may not have TestConductor blocking calls still need to be converted for consistency. Run this to find them: +```bash +find src -name "*.cs" -path "*Tests.MultiNode*" -exec grep -l "EnterBarrier(" {} \; +``` + +## Migration Steps + +1. **Add async Task import** + ```csharp + using System.Threading.Tasks; + ``` + +2. **Convert test method signature** + - Change `public void` to `public async Task` + +3. **Find and replace blocking patterns** + - Search for `.Wait()` calls + - Search for `EnterBarrier(` + - Search for `Within(` + - Search for `RunOn(` with async operations inside + +4. **Update method calls** + - Add `await` keyword before async calls + - Change method names to async versions (add `Async` suffix) + - Update lambdas to `async` when needed + +5. **Update helper methods** + - Convert any helper methods that now contain async calls + - Propagate async/await up the call chain + +6. **Build and verify** + ```bash + dotnet build src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj -c Release + ``` + +7. **Run tests (example)** + ```bash + dotnet test src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj \ + -c Release --filter "FullyQualifiedName~YourTestName" --framework net8.0 + ``` + +## Common Pitfalls to Avoid + +1. **Don't use ConfigureAwait(false) in tests** + - Tests should maintain their synchronization context + +2. **Don't use GetAwaiter().GetResult()** + - This is just as bad as .Wait() for blocking + +3. **Ensure all async operations are awaited** + - Missing awaits can cause race conditions + +4. **Watch for nested RunOn calls** + - Inner RunOn may need to become RunOnAsync if it contains async operations + +5. **Don't forget lambda async modifiers** + ```csharp + // Wrong + ReportResult(() => { await SomeAsync(); }); + + // Right + ReportResult(async () => { await SomeAsync(); }); + ``` + +## Verification Commands + +Check for remaining blocking calls: +```bash +# Find .Wait() calls +grep -r "\.Wait()" src --include="*.cs" | grep -i multinode + +# Find EnterBarrier calls +grep -r "EnterBarrier(" src --include="*.cs" | grep -i multinode + +# Find TestConductor blocking calls +grep -r "TestConductor\.[A-Z].*\.Wait()" src --include="*.cs" +``` + +## Git Commit Message Template +``` +Convert [TestName] to async + +- Convert main test method to async Task +- Replace TestConductor.[Method]().Wait() with await TestConductor.[Method]Async() +- Replace EnterBarrier with EnterBarrierAsync +- Use RunOnAsync for async operations +- Use WithinAsync for async timing constraints +- Add using System.Threading.Tasks +``` + +## Notes +- This migration improves test reliability by preventing thread pool starvation +- Tests should run faster and more reliably in CI environments +- The async APIs provide better cancellation support via CancellationToken \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ebb9d383c0c..83db8a256e8 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,2014 +1,110 @@ -#### 1.5.42 May 21st, 2025 #### +#### 1.5.47 August 12th, 2025 #### -Akka.NET v1.5.42 contains several bug fixes and also adds new quality of life features to `Akka.IO.Tcp`, `Akka.Persistence`, `Akka.Pattern`, and `Akka.Cluster.Tools.DistributedPubSub`. +Akka.NET v1.5.47 is a minor patch containing several stability improvements to Akka.TestKit. -* [IO.Tcp: Cleanup API](https://github.com/akkadotnet/akka.net/pull/7621) -* [IO.Tcp: Fix `TcpListener` connection queue problem](https://github.com/akkadotnet/akka.net/pull/7621) -* [Persistence: Allow user to use supervision strategy on journal and snapshot store](https://github.com/akkadotnet/akka.net/pull/7595) -* [Core: Add cancellation token to `CircuitBreaker` API to signal timed out operations](https://github.com/akkadotnet/akka.net/pull/7624) -* [Persistence: Change all `CircuitBreaker` protected method API to use cancellation token](https://github.com/akkadotnet/akka.net/pull/7624) -* [Core: Leverage Exception...when pattern](https://github.com/akkadotnet/akka.net/pull/7614) -* [IO.Tcp: Add `TcpListenerStatistics` and subscription methods](https://github.com/akkadotnet/akka.net/pull/7633) -* [Cluster.Tools: Modernize `DistributedPubSub` code](https://github.com/akkadotnet/akka.net/pull/7640) -* [Cluster.Tools: Optimize `DistributedPubSub` memory allocation](https://github.com/akkadotnet/akka.net/pull/7642) -* [Cluster.Tools: Improve `DistributedPubSub` `DeadLetter` log message](https://github.com/akkadotnet/akka.net/pull/7646) -* [Core: Refactor immutable collection builders to use simpler `CreateBuilder` pattern](https://github.com/akkadotnet/akka.net/pull/7656) -* [IO.Tcp: Redesign `TcpConnection`](https://github.com/akkadotnet/akka.net/pull/7637) -* [Cluster.Tools: Add `PublishWithAck` feature to `DistributedPubSub`](https://github.com/akkadotnet/akka.net/pull/7652) +* [TestKit: Replace Thread.Sleep with SpinWait](https://github.com/akkadotnet/akka.net/pull/7745) +* [TestKit: Fix excessive AggregateException nesting when cancelling ExpectMessageAsync](https://github.com/akkadotnet/akka.net/pull/7747) +* [TestKit: Add async overload to multi-node TestConductor API](https://github.com/akkadotnet/akka.net/pull/7750) +* [Core: Move ByteBuffer alias to global using](https://github.com/akkadotnet/akka.net/pull/7681) -> [!WARNING] -> -> This release contains several public API breaking changes to Akka.IO.Tcp and Akka.Persistence - -**Akka.Pattern.CircuitBreaker** - -Backward compatible API changes: -* New `.WithCircuitBreaker()` APIs were added that changes the protected function delegate to accept a new `CancellationToken` argument. -* Old `.WithCircuitBreaker()` APIs were marked as obsolete. - -**Akka.Cluster.Tools.DistributedPubSub** - -The documentation for the new `PublishWithAck` feature can be read [here](https://getakka.net/articles/clustering/distributed-publish-subscribe.html#publishwithack) - -Backward compatible API changes: -* There is a new `DistributedPubSubSettings` constructor that leverages the new `PublishWithAck` feature. The old constructor is marked as obsolete. - -**Akka.Persistence** - -The documentation for the new supervision strategy for journal and snapshot-store feature can be read [here](https://getakka.net/articles/persistence/storage-plugins.html#controlling-journal-or-snapshot-crash-behavior) - -Breaking API changes: - -Due to changes in `CircuitBreaker.WithCircuitBreaker()` APIs, several `Akka.Persistence` journal and snapshot-store were changed in a breaking manner. You will need to consider these changes if you have your own specific `Akka.Persistence` plugin implementation and needed to upgrade to this version of Akka. - -* AsyncWriteJournal - * `DeleteMessagesToAsync()` - * `ReadHighestSequenceNrAsync()` - * `WriteMessagesAsync` -* SnapshotStore - * Both `DeleteAsync()` methods - * `LoadAsync()` - * `SaveAsync()` - -**Akka.IO.Tcp** - -The Akka.IO.Tcp has been redesigned to improve its reliability, visibility, and performance. This, unfortunately, requires some breaking changes to be introduced into the code base. - -New features: -* The `TcpListener` actor now accepts a new `SubscribeToTcpListenerStats` message. Subscribers will receive regular `TcpListenerStatistics` metrics report from the `TcpListener`. -* The new `UnsubscribeFromTcpListenerStats` message can be used to unsubscribe from the `TcpListener` - -Backward compatible changes: -* The `Akka.IO.Tcp.Bind` command now contain a new settable `TcpSettings` property. -* The `Akka.IO.Tcp.Connect` command now contain a new settable `TcpSettings` property. -* The `Akka.IO.TcpSettings` class have several of its unused properties deprecated and/or changed: - * The `ReceivedMessageSizeLimit` property has been deprecated, replaced with the new `MaxFrameSizeBytes` property. - * New `ReceiveBufferSize` property added. - * New `SendBufferSize` property added. - -Breaking API changes: -* `Akka.IO.Tcp.Instance` static field has been removed. -* `Akka.IO.TcpExt.BufferPool` static property has been removed. - -To [see the full set of changes in Akka.NET v1.5.42, click here](https://github.com/akkadotnet/akka.net/milestone/125?closed=1). - -5 contributors since release 1.5.41 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 15 | 3480 | 2864 | Aaron Stannard | -| 5 | 1711 | 713 | Gregorius Soedharmo | -| 4 | 72 | 100 | Simon Cropp | -| 1 | 172 | 51 | Arjen Smits | -| 1 | 16 | 60 | JuYoung Kim | - -#### 1.5.41 May 6th, 2025 #### - -Akka.NET v1.5.41 contains several bug fixes and also adds new quality of life features to Akka.TestKit and Akka.Streams. - -* [TestKit: Ensure that `EventFilter` respects `WithinAsync` timeout blocks](https://github.com/akkadotnet/akka.net/pull/7541) -* [TestKit: Add support to XUnit 3 to Akka.TestKit.Xunit](https://github.com/akkadotnet/akka.net/issues/7603) -* [TestKit: Improve `ExpectNextNAsync()` error message clarity](https://github.com/akkadotnet/akka.net/pull/7616) -* [Remoting: Mark `IDaemonMsg` with public interface marker](https://github.com/akkadotnet/akka.net/pull/7596) -* [Streams: Fix cancelled sinks are blocking other `BroadcastHub` consumers](https://github.com/akkadotnet/akka.net/pull/7615) -* [Streams: Allow `GroupBy` to use infinite output sub-streams](https://github.com/akkadotnet/akka.net/pull/7607) -* [Analyzers: Bump Akka.Analyzers from 0.3.1 to 0.3.2](https://github.com/akkadotnet/akka.net/pull/7609) - -**XUnit V3 Support** - -We've added XUnit v3 support to Akka.TestKit.Xunit, please use this package if you're planning on using and/or migrating to the latest XUnit 3 platform. - -Note that due to XUnit v3 limitation, please make sure that you're following these minimum requirements: -* Use net472 and above if you're targeting .NET Framework. -* Use net8.0 and above if you're targeting .NET Core. -* Reference the `xunit.v3.*` packages v2.0.2 and above. -* Reference the `xunit.runner.visualstudio` package v3.1.0 and above. - -**Akka.Streams `GroupBy` API improvement** - -`Akka.Streams` `GroupBy` stage can now, and now by default, create an unlimited number of sub-streams. Simply omit the `maxSubstreams` parameter or change the `maxSubstreams` parameter to a negative value to enable this feature. - -> [!NOTE] -> -> This can cause memory leak issue if you design your stream to be long-running, and it is designed to generate/process a very large number of sub-streams. - -4 contributors since release 1.5.40 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 9 | 1757 | 1690 | Aaron Stannard | -| 6 | 1089 | 542 | Gregorius Soedharmo | -| 1 | 3 | 16 | Simon Cropp | -| 1 | 10 | 2 | JuYoung Kim | - -To [see the full set of changes in Akka.NET v1.5.40, click here](https://github.com/akkadotnet/akka.net/milestone/124?closed=1). - -#### 1.5.40 March 24th, 2025 #### - -* [Remote: `Endpoint` actor cleanup](https://github.com/akkadotnet/akka.net/issues/7524) -* [Streams: Implement nullability in `Buffer`](https://github.com/akkadotnet/akka.net/issues/7496) -* [Streams: Refactor `SelectAsyncUnordered` `ContinueWith` to local function](https://github.com/akkadotnet/akka.net/issues/7531) -* [Core: Cleanup build warnings](https://github.com/akkadotnet/akka.net/issues/7522) -* [Streams: Make `SelectAsync` check equality by reference instead of by struct value](https://github.com/akkadotnet/akka.net/issues/7543) -* [Query.InMemory: Properly unwrap tagged messages in all queries](https://github.com/akkadotnet/akka.net/issues/7548) -* [Resolve Akka.Delivery and Akka.Cluster.Sharding.Delivery issues](https://github.com/akkadotnet/akka.net/issues/7538) -* [Persistence: Remove Akka.Persistence.Sql.Common and Akka.Persistence.Query.Sql packages](https://github.com/akkadotnet/akka.net/issues/7551) -* [Persistence: Remove Akka.Persistence.Sqlite](https://github.com/akkadotnet/akka.net/issues/7559) - -2 contributors since release 1.5.39 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 28 | 1177 | 2458 | Aaron Stannard | -| 7 | 788 | 9534 | Gregorius Soedharmo | - -To [see the full set of changes in Akka.NET v1.5.40, click here](https://github.com/akkadotnet/akka.net/milestone/123?closed=1). - -#### 1.5.39 March 14th, 2025 #### - -Akka.NET v1.5.39 contains a mission-critical bugfix for most Akka.Streams users. - -* [Akka.Cluster.Sharding: recursively unpack `ShardingEnvelope` contents inside `IMessageExtract.EntityMessage`](https://github.com/akkadotnet/akka.net/issues/7470) - fixed a small edge case bug that could cause the `ShardingEnvelope` to be delivered to actors rather than the content inside the envelope. -* [Akka.Util: improve `Result`](https://github.com/akkadotnet/akka.net/pull/7520) - small set of API changes here aimed at making Akka.Streams easier to reason about. -* [Akka.Streams: Fixed race conditions + unsafe struct assignment in `SelectAsync`](https://github.com/akkadotnet/akka.net/pull/7521) - this is a bug that's popped up in [Akka.Persistence.Sql](https://github.com/akkadotnet/Akka.Persistence.Sql), [Akka.Streams.Kafka](https://github.com/akkadotnet/Akka.Streams.Kafka), and many other places where `SelectAsync` is used: https://github.com/akkadotnet/akka.net/issues/7518 - -3 contributors since release 1.5.38 - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 6 | 242 | 251 | Aaron Stannard | -| 1 | 42 | 2 | Arjen Smits | -| 1 | 107 | 1 | Gregorius Soedharmo | - -To [see the full set of changes in Akka.NET v1.5.39, click here](https://github.com/akkadotnet/akka.net/milestone/122). - -#### 1.5.38 February 17th 2025 #### - -Akka.NET v1.5.38 is a maintenance release with several bug fixes and minor quality of life API additions - -* [Core: Add `ByteString.ToReadOnlySpan()`](https://github.com/akkadotnet/akka.net/pull/7487) -* [TestKit: Add `IntentionalRestart` auto received message to easily test actor restart behavior](https://github.com/akkadotnet/akka.net/pull/7493) -* [Streams: Fix null exceptions being propagated upstream by downstream completion](https://github.com/akkadotnet/akka.net/pull/7497) -* [Core: Add death watch support to `Ask()` `FutureActorRef` temporary actors to prevent memory leaks](https://github.com/akkadotnet/akka.net/pull/7502) -* [Documentation: Add new AK1008 Akka.Analyzers rule](https://github.com/akkadotnet/akka.net/pull/7504) -* [Core: Bump Akka.Analyzers version from 0.3.0 to 0.3.1](https://github.com/akkadotnet/akka.net/pull/7506) - -To [see the full set of changes in Akka.NET v1.5.38, click here](https://github.com/akkadotnet/akka.net/milestone/121?closed=1). - -4 contributors since release 1.5.37 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 5 | 110 | 9 | Gregorius Soedharmo | -| 3 | 231 | 28 | Aaron Stannard | -| 1 | 81 | 0 | Lydon Chandra | -| 1 | 24 | 20 | Dmitriy Barbul | - -#### 1.5.37 January 23rd 2025 #### - -Akka.NET v1.5.37 is a maintenance release that rolls back earlier changes made in Akka.NET v1.5.35 that have caused problems in some downstream Akka.NET plugins. - -* [Rollback to using 6.0 MSFT libraries](https://github.com/akkadotnet/akka.net/pull/7482) <- moving all of our BCL dependencies to 8.0 created issues for our .NET 6-9 users when adopting Akka.NET packages that only targeted .NET Standard, so for the time being we're normalizing everything back to 6.0 -* [Akka.Persistence: `Akka.Persistence.Journal.AsyncWriteJournal+Resequencer` is created as a top-level `/user` actor instead of a child of the journal](https://github.com/akkadotnet/akka.net/issues/7480) - -To [see the full set of changes in Akka.NET v1.5.37, click here](https://github.com/akkadotnet/akka.net/milestone/120?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 4 | 9 | 7 | Aaron Stannard | - -#### 1.5.36 January 22nd 2025 #### - -Akka.NET v1.5.36 is a maintenance release that addresses several bugs and added several improvements. - -* [Core: Implement nullability for `ActorCell`](https://github.com/akkadotnet/akka.net/pull/7475) -* [Core: Add filtering to `ActorCell` lifecycle metrics](https://github.com/akkadotnet/akka.net/pull/7478) -* [Streams: Complete MergeHub Sink gracefully on graceful stop](https://github.com/akkadotnet/akka.net/pull/7468) - -To [see the full set of changes in Akka.NET v1.5.36, click here](https://github.com/akkadotnet/akka.net/milestone/119?closed=1). - -2 contributors since release 1.5.35 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|----------------| -| 4 | 371 | 251 | Aaron Stannard | -| 1 | 37 | 2 | Pavel Anpin | - -#### 1.5.35 January 13th 2025 #### - -Akka.NET v1.5.35 is a maintenance release that addresses several bugs and added several improvements. - -* [Persistence: Add per-plugin recovery permiter actor](https://github.com/akkadotnet/akka.net/pull/7448) -* [Persistence: Add support for optional snapshots](https://github.com/akkadotnet/akka.net/pull/7444) -* [TestKit: Improve XUnit assertion message formatting](https://github.com/akkadotnet/akka.net/pull/7446) -* [Sharding: Add `Broadcast` message support to sharded daemon process](https://github.com/akkadotnet/akka.net/pull/7451) -* [Core: Bump Microsoft.Extensions and BCL library version to 8.0.*](https://github.com/akkadotnet/akka.net/pull/7460) -* [DData: Fix 8.0 BCL library causing DeltaPropagationSelector to throw IndexOutOfBoundException](https://github.com/akkadotnet/akka.net/pull/7462) -* [Sharding: Fix Shard fails to unwrap buffered messages](https://github.com/akkadotnet/akka.net/pull/7452) -* [Core: Deprecate AddOrSet utility method](https://github.com/akkadotnet/akka.net/pull/7408) - -To [see the full set of changes in Akka.NET v1.5.35, click here](https://github.com/akkadotnet/akka.net/milestone/118?closed=1). - -5 contributors since release 1.5.34 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 10 | 431 | 95 | Gregorius Soedharmo | -| 7 | 4535 | 4515 | Aaron Stannard | -| 1 | 90 | 6 | Chris Hoare | -| 1 | 5 | 5 | Simon Cropp | -| 1 | 173 | 34 | Milan Gardian | - -#### 1.5.34 January 7th 2025 #### - -* [TestKit: Fix DelegatingSupervisorStrategy KeyNotFoundException](https://github.com/akkadotnet/akka.net/pull/7438) -* [Core: Improve actor telemetry type name override](https://github.com/akkadotnet/akka.net/pull/7439) -* [Sharding: Add `IShardingBufferMessageAdapter` to support tracing over sharding](https://github.com/akkadotnet/akka.net/pull/7441) - -To [see the full set of changes in Akka.NET v1.5.34, click here](https://github.com/akkadotnet/akka.net/milestone/117?closed=1). - -3 contributors since release 1.5.33 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 2 | 311 | 16 | Gregorius Soedharmo | -| 2 | 17 | 9 | Aaron Stannard | -| 1 | 1 | 1 | jasonmar | - -#### 1.5.33 December 23rd 2024 #### - -* [Bump Akka.Analyzers from 0.2.5 to 0.3.0](https://github.com/akkadotnet/akka.net/pull/7415) -* [Core: Throw better error message when `Stash()` stashes null message](https://github.com/akkadotnet/akka.net/pull/7425) -* [Core: Fix `IWrappedMessage` and `IDeadLetterSuppression` handling](https://github.com/akkadotnet/akka.net/pull/7414) -* [Core: Make actor start/stop telemetry descriptors overridable](https://github.com/akkadotnet/akka.net/pull/7434) -* [Core: Fix `Result.FromTask` edge case handling](https://github.com/akkadotnet/akka.net/pull/7433) -* [Remote: HandleStashedInbound performance improvement](https://github.com/akkadotnet/akka.net/pull/7409) -* [TestKit: Make startup timeout configurable](https://github.com/akkadotnet/akka.net/pull/7423) -* [TestKit: Make InternalTestActor override its SupervisionStrategy](https://github.com/akkadotnet/akka.net/pull/7221) -* [Streams: Add custom log level argument to `Log` stage](https://github.com/akkadotnet/akka.net/pull/7424) - -To [see the full set of changes in Akka.NET v1.5.33, click here](https://github.com/akkadotnet/akka.net/milestone/116?closed=1). - -4 contributors since release 1.5.32 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 9 | 513 | 117 | Aaron Stannard | -| 3 | 299 | 49 | Gregorius Soedharmo | -| 1 | 32 | 1 | Yan Pitangui | -| 1 | 3 | 4 | Simon Cropp | - -#### 1.5.32 December 4th 2024 #### - -Akka.NET v1.5.32 is a maintenance release that addresses several bugs. - -* [Cluster.Tools: Deprecate ClusterSingleton.Init() method](https://github.com/akkadotnet/akka.net/pull/7387) -* [Remote: Ensure RemoteActorRef are serialized correctly when using multiple transports](https://github.com/akkadotnet/akka.net/pull/7393) -* [Sharding: Harden event-sourced RememberEntities infrastructure against transient persistence failures](https://github.com/akkadotnet/akka.net/pull/7401) - -To [see the full set of changes in Akka.NET v1.5.32, click here](https://github.com/akkadotnet/akka.net/milestone/115?closed=1). - -3 contributors since release 1.5.31 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 8 | 750 | 350 | Aaron Stannard | -| 5 | 505 | 15 | Gregorius Soedharmo | -| 1 | 2 | 2 | Ran Trifon | - -#### 1.5.31 November 11th 2024 #### - -Akka.NET v1.5.31 is a maintenance release that addresses several bugs and added new features. - -* [Persistence: Add logging for failed DeleteAsync() that was caused by failed SaveSnapshot()](https://github.com/akkadotnet/akka.net/pull/7360) -* [Persistence: Fix RecoveryTick timer leak](https://github.com/akkadotnet/akka.net/pull/7343) -* [Serialization.Hyperion: Fix serializer config bug](https://github.com/akkadotnet/akka.net/pull/7364) -* [Sharding: Fix potential `ArgumentException` during shard re-balancing](https://github.com/akkadotnet/akka.net/pull/7367) -* [Cluster: Fix multiple `Member` with the same `Address` crashing `ClusterDaemon`](https://github.com/akkadotnet/akka.net/pull/7371) -* [Core: Fix `Stash` filtering out identical `Envelope`s](https://github.com/akkadotnet/akka.net/pull/7375) -* [Streams: Fix `ShellRegistered` message deadletter log](https://github.com/akkadotnet/akka.net/pull/7376) -* [Sharding: Make lease release in `Shard.PostStop` be blocking instead of using detached async task](https://github.com/akkadotnet/akka.net/pull/7383) -* [Cluster.Tools: Add missing singleton detection feature for easier infrastructure debugging](https://github.com/akkadotnet/akka.net/pull/7363) - -**Upgrade Advisory** - -There is a slight change in how actor `Stash` behavior. In previous behavior, `Stash` will filter out any messages that are identical (see explanation below) when it is prepended with another. It will not do so now, which is the actual intended behavior. - -This change will affect `Akka.Persistence` users or users who use the `Stash.Prepend()` method in their code. You will need to add a de-duplication code if your code depends on sending identical messages multiple times to a persistence actor while it is recovering. - -Messages are considered as identical if they are sent from the same sender actor and have a payload message that `Equals()` to true against another message. Example payload types would be an object pointing to the same memory address (`ReferenceEquals()` returns true), value types (enum, primitives, structs), and classes that implements the `IEquatable` interface. - -To [see the full set of changes in Akka.NET v1.5.31, click here](https://github.com/akkadotnet/akka.net/milestone/114?closed=1). - -2 contributors since release 1.5.30 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 9 | 627 | 154 | Gregorius Soedharmo | -| 4 | 133 | 40 | Aaron Stannard | - -#### 1.5.30 October 1st 2024 #### - -Akka.NET v1.5.29 introduced an interface change on the `IScheduler` that unfortunately caused a lot of other plugins to break due to API compatibility issues. v1.5.30 rolls back that change but still fixes the underlying bug in Akka.Persistence's handling and serialziation of timestamps without any interface changes. v1.5.29 will be deprecated from NuGet. - -* [DData: Remove Hyperion dependency](https://github.com/akkadotnet/akka.net/pull/7337) -* [Streams: Fix SelectAsync race condition bug](https://github.com/akkadotnet/akka.net/pull/7338) -* [Core: Add new IWithTimers API to allow sender override](https://github.com/akkadotnet/akka.net/pull/7341) -* [Persistence: Fix SnapshotMetadata default timestamp value (DateTimeKind.Utc bug)](https://github.com/akkadotnet/akka.net/pull/7354) -* [Core: Fix AskTimeoutException message formatting bug](https://github.com/akkadotnet/akka.net/pull/7350) - -To [see the full set of changes in Akka.NET v1.5.30, click here](https://github.com/akkadotnet/akka.net/milestone/113?closed=1). - -3 contributors since release 1.5.28 - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 8 | 259 | 104 | Gregorius Soedharmo | -| 1 | 10 | 10 | Simon Cropp | -| 1 | 0 | 1 | Aaron Stannard | - -#### 1.5.29 October 1st 2024 #### - -> [!NOTE] -> -> **Deprecated** -> -> This version introduced breaking changes that needs to be reverted. Please use 1.5.30 instead. - -Akka.NET v1.5.29 is an emergency patch release that addresses a severe bug for persistence users whom also use protobuf serializer. - -* [DData: Remove Hyperion dependency](https://github.com/akkadotnet/akka.net/pull/7337) -* [Streams: Fix SelectAsync race condition bug](https://github.com/akkadotnet/akka.net/pull/7338) -* [Core: Add new IWithTimers API to allow sender override](https://github.com/akkadotnet/akka.net/pull/7341) -* [Persistence: Fix SnapshotMetadata default timestamp value (DateTimeKind.Utc bug)](https://github.com/akkadotnet/akka.net/pull/7349) -* [Core: Fix AskTimeoutException message formatting bug](https://github.com/akkadotnet/akka.net/pull/7350) - -To [see the full set of changes in Akka.NET v1.5.29, click here](https://github.com/akkadotnet/akka.net/milestone/112?closed=1). - -3 contributors since release 1.5.28 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 4 | 177 | 14 | Gregorius Soedharmo | -| 1 | 10 | 10 | Simon Cropp | -| 1 | 0 | 1 | Aaron Stannard | - -#### 1.5.28 September 5th 2024 #### - -Akka.NET v1.5.28 is a release with several bug fixes and improvements. - -* [Singleton: Fix oldest member transition log message](https://github.com/akkadotnet/akka.net/pull/7309) -* [Core: Make ITimeProvider injectable into consuming classes](https://github.com/akkadotnet/akka.net/pull/7314) -* [Singleton: Fix ClusterSingletonProxy failed to re-acquire singleton actor](https://github.com/akkadotnet/akka.net/pull/7315) -* [Persistence: Make DateTime.UtcNow the default SnapshotMetadata timestamp](https://github.com/akkadotnet/akka.net/pull/7313) -* [Remote.TestKit: Improve diagnostics and code modernization](https://github.com/akkadotnet/akka.net/pull/7321) -* [Persistence.TestKit: Add new ConnectionInterceptor and improve usability](https://github.com/akkadotnet/akka.net/pull/7324) -* [Sharding: Disable durable DData if RememberEntity does not use DData storage](https://github.com/akkadotnet/akka.net/pull/7327) -* [Persistence.Sql.Common: Harden journal and snapshot store initialization](https://github.com/akkadotnet/akka.net/pull/7325) -* [Streams: Fix missing AlsoTo public API in Flow, SubFlow, and Source](https://github.com/akkadotnet/akka.net/pull/7325) -* [Streams: Fix StreamRefSerializer NRE bug](https://github.com/akkadotnet/akka.net/pull/7333) -* [Persistence: Fix SnapshotStore.SaveSnapshot metadata timestamp bug](https://github.com/akkadotnet/akka.net/pull/7334) -* [Persistence.TCK: Add new optional SnapshotStore save data integrity spec](https://github.com/akkadotnet/akka.net/pull/7335) - -To [see the full set of changes in Akka.NET v1.5.28, click here](https://github.com/akkadotnet/akka.net/milestone/110?closed=1). - -2 contributors since release 1.5.27.1 +4 contributors since release 1.5.46 | COMMITS | LOC+ | LOC- | AUTHOR | |---------|------|------|---------------------| -| 10 | 5318 | 5153 | Aaron Stannard | -| 8 | 1568 | 158 | Gregorius Soedharmo | - -#### 1.5.28-beta1 August 23rd 2024 #### - -Akka.NET v1.5.28-beta1 is a patch beta release with several bug fixes and improvements. - -* [Singleton: Fix oldest member transition log message](https://github.com/akkadotnet/akka.net/pull/7309) -* [Core: Make ITimeProvider injectable into consuming classes](https://github.com/akkadotnet/akka.net/pull/7314) -* [Singleton: Fix ClusterSingletonProxy failed to re-acquire singleton actor](https://github.com/akkadotnet/akka.net/pull/7315) -* [Persistence: Make DateTime.UtcNow the default SnapshotMetadata timestamp](https://github.com/akkadotnet/akka.net/pull/7313) -* [Remote.TestKit: Improve diagnostics and code modernization](https://github.com/akkadotnet/akka.net/pull/7321) -* [Persistence.TestKit: Add new ConnectionInterceptor and improve usability](https://github.com/akkadotnet/akka.net/pull/7324) -* [Sharding: Disable durable DData if RememberEntity does not use DData storage](https://github.com/akkadotnet/akka.net/pull/7327) -* [Persistence.Sql.Common: Harden journal and snapshot store initialization](https://github.com/akkadotnet/akka.net/pull/7325) -* [Streams: Fix missing AlsoTo public API in Flow, SubFlow, and Source](https://github.com/akkadotnet/akka.net/pull/7325) - -To [see the full set of changes in Akka.NET v1.5.28-beta1, click here](https://github.com/akkadotnet/akka.net/milestone/110?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 10 | 5318 | 5153 | Aaron Stannard | -| 4 | 1440 | 115 | Gregorius Soedharmo | - -#### 1.5.27.1 July 25th 2024 #### - -Akka.NET v1.5.27.1 is a minor patch to fix a race condition between the logging and remoting system. - -* [Akka: Fix Remoting-Logging DefaultAddress race condition](https://github.com/akkadotnet/akka.net/pull/7305) - -To [see the full set of changes in Akka.NET v1.5.27.1, click here](https://github.com/akkadotnet/akka.net/milestone/110). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 1 | 4 | 0 | Aaron Stannard | -| 1 | 10 | 3 | Gregorius Soedharmo | - -#### 1.5.27 July 25th 2024 #### - -Akka.NET v1.5.27 is a significant release that includes new features, mission-critical bug fixes, and some performance improvements. - -**Major Akka.Cluster.Sharding and Akka.Cluster.Tools.Singleton Bug Fixes** - -In _all prior versions_ of Akka.NET, there are two high impact distributed systems bugs: - -1. [Akka.Cluster.Tools.Singleton: singleton moves earlier than expected - as soon as new node joins](https://github.com/akkadotnet/akka.net/issues/7196) -2. [Akka.Cluster.Sharding: duplicate shards / entities](https://github.com/akkadotnet/akka.net/issues/6973) - -As we discovered during the course of our pains-taking bug investigation, these were, in fact, the same issue: - -1. The `ClusterSingletonManager` is supposed to _always_ belong on the oldest node of a given role type, but an original design error from the time Akka.Cluster.Tools was first introduced to Akka.NET meant that nodes were always sorted in _descending_ order of `UpNumber`. This is backwards: nodes should always be sorted in _ascending_ order of `UpNumber` - this means that the oldest possible node is always at the front of the "who is oldest?" list held by the `ClusterSingletonManager`. This explains why the singleton could appear to move early during deployments and restarts. -2. The `ClusterSingletonManager` was suspectible to a race condition where if nodes were shutdown and restarted with the same address in under 20 seconds, the default "down removal margin" used by the `ClusterSingletonManager` to tolerate dirty exits, it would be possible after _multiple_ successive, fast, restarts for multiple instances of the singleton to be alive at the same time (for a short period.) - -Both of these varieties of problem, duplicate singletons, is what lead to duplicate shards. - -As a result we've made the following fixes: - -* [Akka.Cluster.Tools: deprecate ClustersSingletonManagerSettings.ConsiderAppVersion](https://github.com/akkadotnet/akka.net/pull/7302) - `AppVersion` is no longer considered for singleton placement as it could easily result in split brains. -* [Akka.Cluster.Tools: fix mutability and oldest state bugs with `ClusterSingletonManager`](https://github.com/akkadotnet/akka.net/pull/7298) - resolves the issue with rapid rolling restarts creating duplicates. We've tested this fix in our test lab across thousands of coordinator restarts and haven't been able to reproduce the issue since (we could easily do it before.) -* [Akka.Cluster.Tools.Singleton / Akka.Cluster.Sharding: fix duplicate shards caused by incorrect `ClusterSingletonManager` `HandOver`](https://github.com/akkadotnet/akka.net/pull/7297) - we fixed the member age problem here, which could cause a second singleton to start at inappropriate times. - -**Akka.Discovery and `ClusterClient` Discovery Support** - -In Akka.NET v1.5.27 we've added support for using Akka.Cluster.Tools.ClusterClient alongside with [Akka.Discovery plugins](https://getakka.net/articles/discovery/index.html) to automatically discover the initial contacts you need for `ClusterClientReceptionist` instances in your environment. - -You can read the documentation for how this works here: https://getakka.net/articles/clustering/cluster-client.html#contact-auto-discovery-using-akkadiscovery - -Related PRs and issues: - -* [Akka.Discovery: Add multi-config support to config-based discovery](https://github.com/akkadotnet/akka.net/issues/7271) -* [Cluster.Tools: Fix missing VerboseLogging in ClusterClientSettings.Copy method](https://github.com/akkadotnet/akka.net/issues/7272) -* [Cluster.Tools: Improve ClusterClientDiscovery to avoid thundering herd problem](https://github.com/akkadotnet/akka.net/issues/7270) -* [Cluster.Tools: Change ClusterClientDiscovery to use the new Akka.Management "/cluster-client/receptionist" endpoint](https://github.com/akkadotnet/akka.net/issues/7274) - -**Other Bug Fixes and Improvements** - -* [Akka.Cluster: improve gossip serialization performance](https://github.com/akkadotnet/akka.net/pull/7281) -* [Akka.Streams: Fix `ActorMaterializerImpl` `null` `LogSource`](https://github.com/akkadotnet/akka.net/pull/7300) -* [Akka.Streams: `AlsoTo` may not be failing graph when its sink throws exception](https://github.com/akkadotnet/akka.net/issues/7269) -* [Akka.DistributedData: if `lmdb.dir` is null or empty, log a warning and set to default](https://github.com/akkadotnet/akka.net/pull/7292) - -To [see the full set of changes in Akka.NET v1.5.27, click here](https://github.com/akkadotnet/akka.net/milestone/109). - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 15 | 835 | 1001 | Aaron Stannard | -| 12 | 1123 | 207 | Gregorius Soedharmo | - -#### 1.5.27-beta2 July 3rd 2024 #### +| 7 | 4185 | 3156 | Aaron Stannard | +| 5 | 352 | 142 | Gregorius Soedharmo | +| 1 | 2 | 2 | dependabot[bot] | +| 1 | 13 | 22 | Simon Cropp | -* [Cluster.Tools: Fix missing port name argument in ClusterClientDiscovery](https://github.com/akkadotnet/akka.net/issues/7276) +To [see the full set of changes in Akka.NET v1.5.47, click here](https://github.com/akkadotnet/akka.net/milestone/130?closed=1) -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 1 | 1 | 1 | Gregorius Soedharmo | +#### 1.5.46 July 17th, 2025 #### -#### 1.5.27-beta1 July 3rd 2024 #### +Akka.NET v1.5.46 is a minor patch containing a fix for the Akka.IO.Dns extension. -Akka.NET v1.5.27-beta1 improves upon the new ClusterClient initial contact auto-discovery feature to make it more robust in implementation. +* [Core: Resolve ManagerClass type from IDnsProvider](https://github.com/akkadotnet/akka.net/pull/7727) -* [Akka.Discovery: Add multi-config support to config-based discovery](https://github.com/akkadotnet/akka.net/issues/7271) -* [Cluster.Tools: Fix missing VerboseLogging in ClusterClientSettings.Copy method](https://github.com/akkadotnet/akka.net/issues/7272) -* [Cluster.Tools: Improve ClusterClientDiscovery to avoid thundering herd problem](https://github.com/akkadotnet/akka.net/issues/7270) -* [Cluster.Tools: Change ClusterClientDiscovery to use the new Akka.Management "/cluster-client/receptionist" endpoint](https://github.com/akkadotnet/akka.net/issues/7274) +3 contributors since release 1.5.45 | COMMITS | LOC+ | LOC- | AUTHOR | |---------|------|------|---------------------| -| 5 | 422 | 183 | Gregorius Soedharmo | | 1 | 4 | 0 | Aaron Stannard | -| 1 | 1 | 1 | Sean Killeen | - -#### 1.5.26 June 27th 2024 #### - -Akka.NET v1.5.26 introduces a new Akka.Cluster.Tools feature and a logging improvement. - -* [Add ClusterClient initial contact auto-discovery feature](https://github.com/akkadotnet/akka.net/issues/7261) -* [Improve traceability of `ITimerMsg`](https://github.com/akkadotnet/akka.net/issues/7262) - -**Preliminary ClusterClient Initial Contact Auto-Discovery Feature** - -> To use this feature, you will need to use Akka.Discovery implementation (Kubernetes or Azure) version 1.5.26-beta1 or higher - -This feature allows ClusterClient to use Akka.Discovery to automatically query for cluster client receptionists inside a dynamic environment such as Kubernetes. - -The preliminary documentation for this feature can be read [here](https://getakka.net/articles/clustering/cluster-client.html#contact-auto-discovery-using-akkadiscovery) - -You can [see the full set of changes for Akka.NET v1.5.26 here](https://github.com/akkadotnet/akka.net/milestones/1.5.26). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 3 | 45 | 11 | Aaron Stannard | -| 2 | 945 | 15 | Gregorius Soedharmo | - -#### 1.5.25 June 14th 2024 #### - -Akka.NET v1.5.25 includes a critical bug fix for logging and some other minor fixes. - -**Logging Errors Introduced in v1.5.21** - -Versions [v1.5.21,v1.5.24] are all affected by [Akka.Logging: v1.5.21 appears to have truncated log source, timestamps, etc from all log messages](https://github.com/akkadotnet/akka.net/issues/7255) - this was a bug introduced when we added [the log-filtering feature we shipped in Akka.NET v1.5.21](https://getakka.net/articles/utilities/logging.html#filtering-log-messages). - -This issue has been resolved in v1.5.25 and we've [added regression tests to ensure that the log format gets version-checked just like our APIs going forward](https://github.com/akkadotnet/akka.net/pull/7256). - -Other fixes: - -* [Akka.Router: sending a message to a remote actor via `IScheduledTellMsg` results in serialization error](https://github.com/akkadotnet/akka.net/issues/7247) -* [Akka.Discovery: Make Akka.Discovery less coupled with Akka.Management](https://github.com/akkadotnet/akka.net/issues/7242) - -You can [see the full set of changes for Akka.NET v1.5.25 here](https://github.com/akkadotnet/akka.net/milestones/1.5.25). - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 6 | 347 | 44 | Aaron Stannard | -| 2 | 1197 | 1015 | Gregorius Soedharmo | - -#### 1.5.24 June 7th 2024 #### - -Akka.NET v1.5.24 is a patch release for Akka.NET that addresses CVE-2018-8292 and also adds a quality of life improvement to IActorRef serialization. - -* [Fix invalid serializer was being used when serialize-message is set to true](https://github.com/akkadotnet/akka.net/pull/7236) -* [Add Serialization.DeserializeActorRef() QoL method](https://github.com/akkadotnet/akka.net/pull/7237) -* Resolve CVE-2018-8292 in [this PR](https://github.com/akkadotnet/akka.net/pull/7235) and [this PR](https://github.com/akkadotnet/akka.net/pull/7238) - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 3 | 35 | 22 | Gregorius Soedharmo | -| 1 | 26 | 51 | Mike Perrin | -| 1 | 15 | 2 | Aaron Stannard | +| 1 | 1 | 1 | Pavel Anpin | +| 1 | 1 | 0 | Gregorius Soedharmo | -You can [see the full set of changes for Akka.NET v1.5.24 here](https://github.com/akkadotnet/akka.net/milestones/1.5.24). +To [see the full set of changes in Akka.NET v1.5.46, click here](https://github.com/akkadotnet/akka.net/milestone/129?closed=1) -#### 1.5.23 June 4th 2024 #### +#### 1.5.45 July 7th, 2025 #### -* [Fix missing `HandOverDone` handler in ClusterSingletonManager](https://github.com/akkadotnet/akka.net/pull/7230) -* [Add push mode to `ShardedDaemonProcess`](https://github.com/akkadotnet/akka.net/pull/7229) +Akka.NET v1.5.45 is a minor patch containing bug fixes for Core Akka and Akka.Cluster.Sharding plugin. -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 2 | 299 | 44 | Aaron Stannard | -| 1 | 47 | 49 | Gregorius Soedharmo | -| 1 | 1 | 1 | Hassan Abu Bakar | - -You can [see the full set of changes for Akka.NET v1.5.23 here](https://github.com/akkadotnet/akka.net/milestones/1.5.23). - -#### 1.5.22 June 4th 2024 #### - -Akka.NET v1.5.22 is a patch release for Akka.NET with a few bug fixes and logging improvement. - -* [Streams: Bump Reactive.Streams to 1.0.4](https://github.com/akkadotnet/akka.net/pull/7213) -* [Remote: Bump DotNetty.Handlers to 0.7.6](https://github.com/akkadotnet/akka.net/pull/7198) -* [Core: Resolve CVE-2018-8292 for Akka.Streams and Akka.Remote](https://github.com/akkadotnet/akka.net/issues/7191) -* [Core: Expose `BusLogging` `EventStream` as public API](https://github.com/akkadotnet/akka.net/pull/7210) -* [Remote: Add cross-platform support to the exception serializer](https://github.com/akkadotnet/akka.net/pull/7222) - -**On Resolving CVE-2018-8292** - -In order to resolve this CVE, we had to update `DotNetty.Handlers` to the latest version and unfortunately, this comes with about 10% network throughput performance hit. We are looking into possible replacement for `DotNetty` to improve this performance lost in the future (see [`#7225`](https://github.com/akkadotnet/akka.net/issues/7225) for updates). - -**Before** - -``` -Num clients, Total [msg], Msgs/sec, Total [ms], Start Threads, End Threads - 1, 200000, 125000, 1600.62, 46, 76 - 5, 1000000, 494072, 2024.04, 84, 95 - 10, 2000000, 713013, 2805.73, 103, 107 - 15, 3000000, 724463, 4141.38, 115, 115 - 20, 4000000, 714669, 5597.66, 123, 123 - 25, 5000000, 684932, 7300.37, 131, 107 - 30, 6000000, 694525, 8639.88, 115, 93 -``` - -**After** - -``` -Num clients, Total [msg], Msgs/sec, Total [ms], Start Threads, End Threads - 1, 200000, 123763, 1616.32, 46, 73 - 5, 1000000, 386101, 2590.66, 81, 90 - 10, 2000000, 662691, 3018.54, 98, 104 - 15, 3000000, 666223, 4503.86, 112, 113 - 20, 4000000, 669681, 5973.89, 121, 113 - 25, 5000000, 669255, 7471.86, 121, 105 - 30, 6000000, 669121, 8967.61, 113, 92 -``` - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 6 | 167 | 188 | Aaron Stannard | -| 3 | 93 | 10 | Gregorius Soedharmo | - -You can [see the full set of changes for Akka.NET v1.5.22 here](https://github.com/akkadotnet/akka.net/milestones/1.5.22). - -#### 1.5.21 May 28th 2024 #### - -Akka.NET v1.5.21 is a significant release for Akka.NET with a major feature additions and bug fixes. - -* [Core: Fix error logging bug](https://github.com/akkadotnet/akka.net/pull/7186) -* [Core: Add log filtering feature](https://github.com/akkadotnet/akka.net/pull/7179) -* [Pub-Sub: Fix missing SendOneMessageToEachGroup property](https://github.com/akkadotnet/akka.net/pull/7202) -* [Core: Fix incorrect IWrappedMessage deserialization when serialize-messages setting is on](https://github.com/akkadotnet/akka.net/pull/7200) -* [Core: Bump Akka.Analyzers to 0.2.5](https://github.com/akkadotnet/akka.net/pull/7206) - -**Log Message Filtering** - -You can now filter out unwanted log messages based on either its source or message content. Documentation can be read in the [logging documentation](https://getakka.net/articles/utilities/logging.html#filtering-log-messages). - -**New Akka.Analyzers Rule** - -Added AK1006 rule to suggest user to use `PersistAll()` and `PersistAllAsync()` when applicable. Documentation can be read in the [documentation](https://getakka.net/articles/debugging/rules/AK1006.html) - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 7 | 900 | 53 | Aaron Stannard | -| 5 | 497 | 1187 | Gregorius Soedharmo | -| 1 | 1 | 1 | Åsmund | - -You can [see the full set of changes for Akka.NET v1.5.21 here](https://github.com/akkadotnet/akka.net/milestones/1.5.21). - -#### 1.5.20 April 29th 2024 #### - -Akka.NET v1.5.20 is a patch release for Akka.NET with a few bug fixes and Akka.Streams quality of life improvement. - -* [Cluster: Fix split brain resolver downing all nodes when failure detector records are unclean/poisoned](https://github.com/akkadotnet/akka.net/pull/7141) -* [TestKit: Fix `AkkaEqualException` message formatting](https://github.com/akkadotnet/akka.net/pull/7164) -* [Core: Generate Protobuf code automatically during build](https://github.com/akkadotnet/akka.net/pull/7063) -* [Streams: `LogSource` quality of life improvement](https://github.com/akkadotnet/akka.net/pull/7168) -* [Core: Fix `HashedWheelTimer` startup crash](https://github.com/akkadotnet/akka.net/pull/7174) - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|-------|---------------------| -| 5 | 360 | 93 | Aaron Stannard | -| 3 | 187 | 20 | Gregorius Soedharmo | -| 1 | 81 | 41827 | Yan Pitangui | - -You can [see the full set of changes for Akka.NET v1.5.20 here](https://github.com/akkadotnet/akka.net/milestones/1.5.20). +* [Core: Code modernization, use deconstructor for variable swapping](https://github.com/akkadotnet/akka.net/pull/7658) +* [Sharding: Fix unclean `ShardingConsumerControllerImpl` shutdown](https://github.com/akkadotnet/akka.net/pull/7714) +* [Core: Convert `Failure` to `Exception` for `Ask`](https://github.com/akkadotnet/akka.net/pull/7286) +* [Core: Fix `Settings.InjectTopLevelFallback` race condition](https://github.com/akkadotnet/akka.net/pull/7721) +* [Sharding: Make remembered entities honor supervision strategy decisions](https://github.com/akkadotnet/akka.net/pull/7720) -#### 1.5.19 April 15th 2024 #### +**Supervision Strategy For Sharding Remembered Entities** -Akka.NET v1.5.19 is a patch release for Akka.NET with a few bug fixes. +* We've added a `SupervisorStrategy` property to `ClusterShardingSettings`. You can use any type of `SupervisionStrategy`, but it is recommended that you inherit `ShardSupervisionStrategy` if you're making your own custom supervision strategy. +* Remembered shard entities will now honor `SupervisionStrategy` decisions and stops remembered entities if the `SupervisionStrategy.Decider` returned a `Directive.Stop` or if there is a maximum restart retry limitation. -* [Persistence.SQLite: Bump Microsoft.Data.SQLite to 8.0.4](https://github.com/akkadotnet/akka.net/pull/7148) -* [Core: Bump Google.Protobuf to 3.26.1](https://github.com/akkadotnet/akka.net/pull/7138) -* [Core: Bump Akka.Analyzer to 0.2.4](https://github.com/akkadotnet/akka.net/pull/7143) -* [Remote: Improve logging](https://github.com/akkadotnet/akka.net/pull/7149) -* [Cluster: Improve logging](https://github.com/akkadotnet/akka.net/pull/7149) -* [Core: Fix resource contention problem with HashedWheelTimerScheduler during start-up](https://github.com/akkadotnet/akka.net/pull/7144) -* [TestKit: Fix async deadlock by replacing IAsyncQueue with System.Threading.Channel](https://github.com/akkadotnet/akka.net/pull/7157) - -**Akka.Analyzers** - -We've added 3 new analyzer rules to `Akka.Analyzers`: - -* **AK1004** - - AK1004 warns users to replace any `ScheduleTellOnce()` and `ScheduleTellRepeatedly()` invocation inside an actor to implement `IWithTimers` interface instead. Documentation can be read [here](https://getakka.net/articles/debugging/rules/AK1004.html) - -* **AK1005** - - AK1005 warns users about improper `Sender` and `Self` access from inside an async lambda callbacks inside actor implementation. Documentation can be read [here](https://getakka.net/articles/debugging/rules/AK1005.html) - -* **AK1007** - - AK1007 is an error message for any `Timers.StartSingleTimer()` and `Timers.StartPeriodicTimer()` invocation from inside the actor `PreRestart()` and `AroundPreRestart()` lifecycle callback methods. Documentation can be read [here](https://getakka.net/articles/debugging/rules/AK1007.html) +4 contributors since release 1.5.44 | COMMITS | LOC+ | LOC- | AUTHOR | |---------|------|------|---------------------| -| 9 | 366 | 1951 | Aaron Stannard | -| 9 | 14 | 14 | dependabot[bot] | -| 2 | 516 | 30 | Gregorius Soedharmo | +| 10 | 823 | 108 | Gregorius Soedharmo | +| 1 | 7 | 13 | Simon Cropp | +| 1 | 60 | 18 | ondravondra | +| 1 | 1 | 0 | Aaron Stannard | -You can [see the full set of changes for Akka.NET v1.5.19 here](https://github.com/akkadotnet/akka.net/milestones/1.5.19). +To [see the full set of changes in Akka.NET v1.5.45, click here](https://github.com/akkadotnet/akka.net/milestone/128?closed=1) -#### 1.5.18 March 13th 2024 #### +#### 1.5.44 June 19th, 2025 #### -Akka.NET v1.5.18 is a patch release for Akka.NET with a feature addition. +Akka.NET v1.5.44 is a minor patch that contains a bug fix to the Akka.Persistence plugin. -* [Migrate all internal dispatchers to default thread pool dispatcher](https://github.com/akkadotnet/akka.net/pull/7117) +* [Persistence: Make sure that EventSourced timer is canceled when persistent actor is stopped](https://github.com/akkadotnet/akka.net/pull/7693) -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|-----------------| -| 1 | 9 | 9 | Aaron Stannard | -| 1 | 1 | 1 | dependabot[bot] | - -You can [see the full set of changes for Akka.NET v1.5.18 here](https://github.com/akkadotnet/akka.net/milestones/1.5.18). - -#### 1.5.17.1 March 1st 2024 #### - -Akka.NET v1.5.17.1 is a patch release for Akka.NET with a bug fix. - -* [Core: Bump Akka.Analyzers to 0.2.3.1](https://github.com/akkadotnet/akka.analyzers/releases/tag/0.2.3.1) +3 contributors since release 1.5.43 | COMMITS | LOC+ | LOC- | AUTHOR | |---------|------|------|---------------------| -| 1 | 1 | 1 | Gregorius Soedharmo | -| 1 | 1 | 1 | Aaron Stannard | - -#### 1.5.17 February 29th 2024 #### - -Akka.NET v1.5.17 is a patch release for Akka.NET with some feature additions and bug fixes. - -* [Core: Fix null ref for `LogSource`](https://github.com/akkadotnet/akka.net/pull/7078) -* [Persistence.TCK: Make all snapshot tests virtual](https://github.com/akkadotnet/akka.net/pull/7093) -* [Core: Suppress extremely verbose `TimerScheduler` debug message](https://github.com/akkadotnet/akka.net/pull/7102) -* [Sharding: Implement reliable delivery custom message bypass feature](https://github.com/akkadotnet/akka.net/pull/7106) -* Update dependencies - * [Persistence: Bump Microsoft.Data.SQLite to 8.0.2](https://github.com/akkadotnet/akka.net/pull/7096) - * [Core: Bump Google.Protobuf to 3.25.3](https://github.com/akkadotnet/akka.net/pull/7100) - * [Core: Bump Akka.Analyzers to 0.2.3](https://github.com/akkadotnet/akka.analyzers/releases/tag/0.2.3) -* Documentations - * [Actors: Fix typo](https://github.com/akkadotnet/akka.net/pull/7085) - * [Remoting: Fix remote deployment typo](https://github.com/akkadotnet/akka.net/pull/7095) - * [Analyzers: Add missing TOC links](https://github.com/akkadotnet/akka.net/pull/7098) - * [Sharding: Add missing custom sharding handoff stop documentation](https://github.com/akkadotnet/akka.net/pull/7101) - -**Akka.Analyzers** +| 10 | 438 | 323 | Gregorius Soedharmo | +| 2 | 4 | 2015 | Aaron Stannard | +| 1 | 47 | 43 | Simon Cropp | -* The AK1001 rule has been removed due to the discussion [here](https://github.com/akkadotnet/akka.analyzers/issues/65). -* AK1002 has been enhanced with better problem detection. +To [see the full set of changes in Akka.NET v1.5.44, click here](https://github.com/akkadotnet/akka.net/milestone/127?closed=1). -You can [see the full set of changes for Akka.NET v1.5.17 here](https://github.com/akkadotnet/akka.net/milestones/1.5.17). +#### 1.5.43 June 10th, 2025 #### -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 7 | 1342 | 732 | Gregorius Soedharmo | -| 4 | 5 | 5 | dependabot[bot] | -| 3 | 158 | 4 | Aaron Stannard | -| 2 | 3 | 3 | hassan-me | -| 1 | 2 | 8 | Massimiliano Donini | -| 1 | 12 | 12 | Mattias Jakobsson | - -#### 1.5.16 January 29th 2024 #### - -Akka.NET v1.5.16 is a patch release for Akka.NET with some feature additions and changes. +Akka.NET v1.5.43 contains several bug fixes and also adds new quality of life features. -* [Core: Bump Google.Protobuf to 3.25.2](https://github.com/akkadotnet/akka.net/pull/7056) -* [Core: Remove redundant assembly titles](https://github.com/akkadotnet/akka.net/pull/6796) -* [Akka.Cluster.Sharding: Fix sharding entity ID extractor nullability](https://github.com/akkadotnet/akka.net/pull/7059) -* [Akka.Cluster.Sharding: Fix cluster sharding benchmark](https://github.com/akkadotnet/akka.net/pull/7061) -* [Akka.TestKit: Fix Watch and Unwatch bug](https://github.com/akkadotnet/akka.net/pull/7037) -* [Akka.Cluster.Metrics: Separate business models and wire format models](https://github.com/akkadotnet/akka.net/pull/7067) -* [Akka.Analyzer: Bump Akka.Analyzer to 0.2.2](https://github.com/akkadotnet/akka.net/pull/7073) +* [Cluster.Tools: Fix PublishWithAck response message type](https://github.com/akkadotnet/akka.net/pull/7673) +* [Sharding: Allows sharding delivery consumer to passivate self](https://github.com/akkadotnet/akka.net/pull/7670) +* [TestKit: Fix CallingThreadDispatcher async context switching](https://github.com/akkadotnet/akka.net/pull/7674) +* [Persistence.Query: Add non-generic `ReadJournalFor` API method](https://github.com/akkadotnet/akka.net/pull/7679) +* [Core: Simplify null checks](https://github.com/akkadotnet/akka.net/pull/7659) +* [Core: Propagate CoordinatedShutdown reason to application exit code](https://github.com/akkadotnet/akka.net/pull/7684) +* [Core: Bump AkkaAnalyzerVersion to 0.3.3](https://github.com/akkadotnet/akka.net/pull/7685) +* [Core: Improve IScheduledTellMsg DeadLetter log message](https://github.com/akkadotnet/akka.net/pull/7686) -**Akka.Analyzers** +**New Akka.Analyzer Rules** -We have expanded Akka.Analyzer and introduced 2 new rules to the Roslyn analyzer: -* `AK1002`: Error: Must not await `Self.GracefulStop()` inside `ReceiveAsync()` or `ReceiveAnyAsync` -* `AK1003`: Warning: `ReceiveAsync()` or `ReceiveAnyAsync()` message handler without async lambda body +We've added three new Akka.Analyzer rules, AK2003, AK2004, and AK2005. All of them addresses the same Akka anti-pattern where a `void async` delegate is being passed into the `ReceiveActor.Receive()` (AK2003), `IDslActor.Receive()` (AK2004), and `ReceivePersistentActor.Command()` (AK2005) message handlers. -[See the full set of supported Akka.Analyzers rules here](https://getakka.net/articles/debugging/akka-analyzers.html) +Here are the documentation for each new rules: +* [AK2003 documentation](https://getakka.net/articles/debugging/rules/AK2003.html) +* [AK2004 documentation](https://getakka.net/articles/debugging/rules/AK2004.html) +* [AK2005 documentation](https://getakka.net/articles/debugging/rules/AK2005.html) -You can [see the full set of changes for Akka.NET v1.5.16 here](https://github.com/akkadotnet/akka.net/milestones/1.5.16). +4 contributors since release 1.5.42 | COMMITS | LOC+ | LOC- | AUTHOR | |---------|------|------|---------------------| -| 6 | 1268 | 628 | Gregorius Soedharmo | -| 5 | 6 | 6 | dependabot[bot] | -| 2 | 286 | 224 | Aaron Stannard | -| 1 | 2120 | 0 | Yan Pitangui | -| 1 | 2 | 2 | Mattias Jakobsson | -| 1 | 2 | 0 | Ebere Abanonu | -| 1 | 0 | 65 | Simon Cropp | - -#### 1.5.15 January 9th 2024 #### - -Akka.NET v1.5.15 is a significant release for Akka.NET with some major feature additions and changes. - -* [Introducing `Akka.Analyzers` - Roslyn Analysis for Akka.NET](https://getakka.net/articles/debugging/akka-analyzers.html) -* [Akka.Cluster.Sharding: perf optimize message extraction, automate `StartEntity` and `ShardEnvelope` handling](https://github.com/akkadotnet/akka.net/pull/6863) -* [Akka.Cluster.Tools: Make `ClusterClient` messages be serialized using `ClusterClientMessageSerializer`](https://github.com/akkadotnet/akka.net/pull/7032) -* [Akka.Persistence: Fix `LocalSnapshotStore` Metadata Fetch to ensure persistenceid match.](https://github.com/akkadotnet/akka.net/pull/7040) -* [Akka.Delivery: Fix `ProducerControllerImpl` state bug](https://github.com/akkadotnet/akka.net/pull/7034) -* [Change MS.EXT and System package versioning to range](https://github.com/akkadotnet/akka.net/pull/7029) - we now support all Microsoft.Extensions packages from `(6.0,]`. -* [Akka.Serialization: `INoSerializationVerificationNeeded` does not handle `IWrappedMessage` correctly](https://github.com/akkadotnet/akka.net/pull/7010) - -**Akka.Analyzers** - -The core Akka NuGet package now references [Akka.Analyzers](https://github.com/akkadotnet/akka.analyzers), a new set of Roslyn Code Analysis and Code Fix Providers that we distribute via NuGet. You can [see the full set of supported Akka.Analyzers rules here](https://getakka.net/articles/debugging/akka-analyzers.html). - -**Akka.Cluster.Sharding Changes** - -In [#6863](https://github.com/akkadotnet/akka.net/pull/6863) we made some major changes to the Akka.Cluster.Sharding API aimed at helping improve Cluster.Sharding's performance _and_ ease of use. However, these changes _may require some effort on the part of the end user_ in order to take full advantage: - -* [`ExtractEntityId`](https://getakka.net/api/Akka.Cluster.Sharding.ExtractEntityId.html) and [`ExtractShardId`](https://getakka.net/api/Akka.Cluster.Sharding.ExtractShardId.html) have been deprecated as they _fundamentally can't be extended and can't benefit from the performance improvements introduced into Akka.NET v1.5.15_. It is **imperative** that you migrate to using the [`HashCodeMessageExtractor`](https://getakka.net/api/Akka.Cluster.Sharding.HashCodeMessageExtractor.html) instead. -* You no longer need to handle [`ShardRegion.StartEntity`](https://getakka.net/api/Akka.Cluster.Sharding.ShardRegion.StartEntity.html) or [`ShardingEnvelope`](https://getakka.net/api/Akka.Cluster.Sharding.ShardingEnvelope.html) inside your `IMessageExtractor` implementations, and in fact [`AK2001`](https://getakka.net/articles/debugging/rules/AK2001.html) (part of Akka.Analyzers) will automatically detect this and remove those handlers for you. Akka.NET automatically handles these two message types internally now. - -**ClusterClient Serialization Changes** - -In [#7032](https://github.com/akkadotnet/akka.net/pull/7032) we solved a long-standing serialization problem with the [`ClusterClient`](https://getakka.net/api/Akka.Cluster.Tools.Client.ClusterClient.html) where `Send`, `SendToAll`, and `Publish` were not handled by the correct internal serializer. This has been fixed by default in Akka.NET v1.5.15, but this can potentially cause wire compatibility problems during upgrades - therefore we have introduced a configuration setting to toggle this: - -```hocon -# re-enable legacy serialization -akka.cluster.client.use-legacy-serialization = on -``` - -That setting is currently set to `on` by default, so v1.5.15 will still behave like previous versions of Akka.NET. However, if you have been affected by serialization issues with the `ClusterClient` (such as [#6803](https://github.com/akkadotnet/akka.net/issues/6803)) you should toggle this setting to `off`. - -See "[Akka.NET v1.5.15 Upgrade Advisories](https://getakka.net/community/whats-new/akkadotnet-v1.5-upgrade-advisories.html)" for full details on some of the things you might need to do while upgrading to this version of Akka.NET. - -You can [see the full set of changes for Akka.NET v1.5.15 here](https://github.com/akkadotnet/akka.net/milestones/1.5.15). - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 16 | 2228 | 1490 | Aaron Stannard | -| 9 | 9 | 9 | dependabot[bot] | -| 2 | 610 | 173 | Gregorius Soedharmo | -| 2 | 337 | 0 | Drew | -| 2 | 124 | 118 | Lehonti Ramos | -| 1 | 2 | 2 | Sergey Popov | -| 1 | 108 | 25 | Yaroslav Paslavskiy | -| 1 | 1 | 1 | Bert Lamb | - -#### 1.5.14 September 24th 2023 #### - -Akka.NET v1.5.14 is a maintenance release with several bug fixes. - -* [Streams: Ensure stream are closed on shutdown](https://github.com/akkadotnet/akka.net/pull/6935) -* [Akka: Fix PeriodicTimer HashWheelTimerScheduler deadlock during start](https://github.com/akkadotnet/akka.net/pull/6949) -* [Cluster: Old version of LeastShardAllocationStrategy is now deprecated](https://github.com/akkadotnet/akka.net/pull/6975) -* [Query: Add a more descriptive ToString() values to Offset types](https://github.com/akkadotnet/akka.net/pull/6978) -* Package dependency upgrades - * [MNTR: Bump Akka.Multinode.TestAdapter to 1.5.13](https://github.com/akkadotnet/akka.net/pull/6926) - * [Akka: Bump Polyfill to 1.28](https://github.com/akkadotnet/akka.net/pull/6936) - * [Akka: Bump Google.Protobuf to 3.24.4](https://github.com/akkadotnet/akka.net/pull/6951) - * [DData: Bump LightningDB to 0.16.0](https://github.com/akkadotnet/akka.net/pull/6960) - * [Persistence: Bump Microsoft.Data.SQLite to 7.0.13](https://github.com/akkadotnet/akka.net/pull/6969) - -If you want to see the [full set of changes made in Akka.NET v1.5.14, click here](https://github.com/akkadotnet/akka.net/milestone/96?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 11 | 25 | 21 | dependabot[bot] | -| 3 | 14 | 2 | Aaron Stannard | -| 3 | 114 | 369 | Simon Cropp | -| 2 | 36 | 31 | Gregorius Soedharmo | -| 1 | 41 | 43 | Lehonti Ramos | -| 1 | 38 | 0 | Yaroslav Paslavskiy | -| 1 | 3 | 0 | Sean Killeen | -| 1 | 227 | 25 | Drew | -| 1 | 1 | 1 | szaliszali | - -#### 1.5.13 August 26th 2023 #### - -Akka.NET v1.5.13 is a maintenance release with several bug fixes and also performance and QOL improvements. - -* [Akka: Clean up and optimize actor name validation](https://github.com/akkadotnet/akka.net/pull/6919) -* [Akka: Wrap all scheduler Tell messages in `IScheduledMessage` envelope](https://github.com/akkadotnet/akka.net/pull/6461) -* [Akka: Fix possible NRE bug in `Dispatchers`](https://github.com/akkadotnet/akka.net/pull/6906) -* [Akka.Cluster.Sharding: Log shard coordinator remember entities timeout](https://github.com/akkadotnet/akka.net/pull/6885) -* [Akka.Cluster.Sharding: Fix shard coordinator throwing NullReferenceException](https://github.com/akkadotnet/akka.net/pull/6892) -* [Akka.Streams: Log errors inside SelectAsync stage](https://github.com/akkadotnet/akka.net/pull/6884) -* [Akka.Streams: Add supervisor strategy support for Throttle stage](https://github.com/akkadotnet/akka.net/pull/6886) -* [Akka: Change HashedWheelTimerScheduler implementation to use `PeriodicTimer` for net6.0+ builds](https://github.com/akkadotnet/akka.net/pull/6435) -* Package dependency upgrades - * [Bump Polyfill to 1.27.1](https://github.com/akkadotnet/akka.net/pull/6899) - * [Bump Microsoft.Data.SQLite to 7.0.11](https://github.com/akkadotnet/akka.net/pull/6917) - * [Bump Google.Protobuf tp 3.24.3](https://github.com/akkadotnet/akka.net/pull/6909) - -If you want to see the [full set of changes made in Akka.NET v1.5.13, click here](https://github.com/akkadotnet/akka.net/milestone/95?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 18 | 25 | 25 | dependabot[bot] | -| 6 | 435 | 200 | Gregorius Soedharmo | -| 4 | 512 | 293 | Aaron Stannard | -| 2 | 3 | 7 | Simon Cropp | -| 1 | 7 | 0 | Sergey Popov | -| 1 | 66 | 17 | Ismael Hamed | -| 1 | 1 | 1 | HamzaAmjad-RG | - -#### 1.5.13-beta1 August 26th 2023 #### - -Akka.NET v1.5.13-beta1 is a maintenance release with several performance and QOL improvements. - -* [Akka.Cluster.Sharding: Log shard coordinator remember entities timeout](https://github.com/akkadotnet/akka.net/pull/6885) -* [Akka.Cluster.Sharding: Fix shard coordinator throwing NullReferenceException](https://github.com/akkadotnet/akka.net/pull/6892) -* [Akka.Streams: Log errors inside SelectAsync stage](https://github.com/akkadotnet/akka.net/pull/6884) -* [Akka.Streams: Add supervisor strategy support for Throttle stage](https://github.com/akkadotnet/akka.net/pull/6886) -* [Akka: Change HashedWheelTimerScheduler implementation to use `PeriodicTimer` for net6.0+ builds](https://github.com/akkadotnet/akka.net/pull/6435) -* Package dependency upgrades - * [Bump Microsoft.Data.SQLite to 7.0.10](https://github.com/akkadotnet/akka.net/pull/6876) - * [Bump Google.Protobuf tp 3.24.1](https://github.com/akkadotnet/akka.net/pull/6891) - -If you want to see the [full set of changes made in Akka.NET v1.5.13-beta1, click here](https://github.com/akkadotnet/akka.net/milestone/95?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 11 | 15 | 15 | dependabot[bot] | -| 3 | 302 | 143 | Aaron Stannard | -| 2 | 384 | 195 | Gregorius Soedharmo | -| 1 | 7 | 0 | Sergey Popov | -| 1 | 66 | 17 | Ismael Hamed | -| 1 | 3 | 5 | Simon Cropp | -| 1 | 1 | 1 | HamzaAmjad-RG | - -#### 1.5.12 August 2nd 2023 #### - -Akka.NET v1.5.12 is a maintenance release with a minor API change and a minor bug fix. - -* [Persistence.Query: Fix `ReadJournalFor()` thread safety](https://github.com/akkadotnet/akka.net/pull/6859) -* [Persistence.Query: Expose new `Tags` property in `EventEnvelope`](https://github.com/akkadotnet/akka.net/pull/6862) -* [Documentation: Fix typo in member-roles.md](https://github.com/akkadotnet/akka.net/pull/6784) - -If you want to see the [full set of changes made in Akka.NET v1.5.12, click here](https://github.com/akkadotnet/akka.net/milestone/94?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 5 | 34 | 18 | Aaron Stannard | -| 2 | 150 | 51 | Gregorius Soedharmo | -| 1 | 1 | 1 | dependabot[bot] | -| 1 | 1 | 1 | Jim Aho | - -#### 1.5.11 July 27th 2023 #### - -Akka.NET v1.5.11 is a maintenance release with a minor API change and internal code modernization/cleanup. - -* [Remote: Modernize DotNettyTransportSettings class and add support for a SSL Setup class](https://github.com/akkadotnet/akka.net/pull/6854) -* [PubSub: Make CountSubscriber query command public](https://github.com/akkadotnet/akka.net/pull/6856) - -If you want to see the [full set of changes made in Akka.NET v1.5.11, click here](https://github.com/akkadotnet/akka.net/milestone/93?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 1 | 465 | 321 | Gregorius Soedharmo | -| 1 | 22 | 2 | Aaron Stannard | - -#### 1.5.10 July 26th 2023 #### - -Akka.NET v1.5.10 is a maintenance release with a minor API change. - -* [Persistence.TCK: Add constructor overload that takes ActorSystemSetup argument](https://github.com/akkadotnet/akka.net/pull/6850) - -If you want to see the [full set of changes made in Akka.NET v1.5.10, click here](https://github.com/akkadotnet/akka.net/milestone/92?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 1 | 70 | 12 | Gregorius Soedharmo | - -#### 1.5.9 July 18th 2023 #### - -Akka.NET v1.5.9 is a maintenance release that introduces some performance improvements and internal code cleanup/modernization. - -__Changes:__ -* [Remoting: Make transport adapter component public](https://github.com/akkadotnet/akka.net/pull/6838) - -__Improvements:__ -* [Memory optimization, use `Array.Empty` instead of creating empty arrays](https://github.com/akkadotnet/akka.net/pull/6801) -* [Remoting: Log all wrapped message layers during errors](https://github.com/akkadotnet/akka.net/pull/6818) -* [Port #6805 and #6807, Improve Stream and Pattern.Backoff instance creation time performance](https://github.com/akkadotnet/akka.net/pull/6821) -* [DData: Harden LWWDictionary serialization null check](https://github.com/akkadotnet/akka.net/pull/6837) - -__Code modernization:__ -* [Use C# 9.0 target-typed new()](https://github.com/akkadotnet/akka.net/pull/6798) -* [Use C# 8.0 null-coalescing operator](https://github.com/akkadotnet/akka.net/pull/6814) - -__Update dependency versions:__ -* [Bump Google.Protobuf to 3.23.4](https://github.com/akkadotnet/akka.net/pull/6826) -* [Bump Akka.MultiNode.TestAdapter to 1.5.8](https://github.com/akkadotnet/akka.net/pull/6802) -* [Bump Microsoft.Data.SQLite to 7.0.9](https://github.com/akkadotnet/akka.net/pull/6835) -* [Bump Microsoft.Extensions.ObjectPool to 7.0.8](https://github.com/akkadotnet/akka.net/pull/6813) -* [Bump Xunit to 2.5.0](https://github.com/akkadotnet/akka.net/pull/6825) - -__Akka.TestKit.Xunit Changes__ - -Due to breaking API change in Xunit 2.5.0, updating to Akka.NET 1.5.9 might break your unit tests. Some of the breaking change that we've noticed are: - -* `AkkaEqualException` constructor has been changed due to changes in Xunit API. If you're using this class, please use the `AkkaEqualException.ForMismatchedValues()` static method instead of using the constructor. -* Testing for exception types by calling async code inside a sync delegate will not unwrap the `AggregateException` thrown. Either use async all the way or manually unwrap the exception. -* Xunit `Asset.Equal()` does not automatically check for collection item equality anymore, that means doing `Assert.Equal()` between two dictionary or list would not work anymore. -* Some Xunit classes have been changed from public to private. If you're using these classes, you will need to refactor your code. -* __FsCheck.Xunit:__ Xunit Roslyn analyzer has become a bit too overzealous and insists that all unit test method can only return either void or Task and will raise a compilation error if you tried to return anything else. If you're using `FsCheck.Xunit`, you will need to use a pragma to disable this check: `#pragma warning disable xUnit1028`. - -If you want to see the [full set of changes made in Akka.NET v1.5.9, click here](https://github.com/akkadotnet/akka.net/milestone/91?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 12 | 171 | 155 | dependabot[bot] | -| 7 | 466 | 165 | Aaron Stannard | -| 4 | 1648 | 1725 | Simon Cropp | -| 1 | 9 | 4 | Gregorius Soedharmo | -| 1 | 7 | 1 | Michael Buck | - -#### 1.5.8 June 15th 2023 #### - -Akka.NET v1.5.8 is a maintenance release that introduces some new features and fixes some bugs with Akka.NET v1.5.7 and earlier. - -* [Akka.Streams: Added `Source`/`Flow` `Setup` operator](https://github.com/akkadotnet/akka.net/pull/6788) -* [Akka.Cluster.Sharding: fixed potential wire format problem when upgrading from v1.4 to v1.5 with `state-store-mode=ddata` and `remember-entities=on`](https://github.com/akkadotnet/akka.net/issues/6704) -* [Akka.Remote.TestKit: Fix MNTR crashing because it is using PolyFill extension method](https://github.com/akkadotnet/akka.net/pull/6768) - -If you want to see the [full set of changes made in Akka.NET v1.5.8, click here](https://github.com/akkadotnet/akka.net/milestone/90?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 9 | 11 | 11 | dependabot[bot] | -| 2 | 8 | 0 | Aaron Stannard | -| 2 | 75 | 4 | Gregorius Soedharmo | -| 2 | 132 | 158 | Simon Cropp | -| 1 | 431 | 1 | Ismael Hamed | -| 1 | 1 | 1 | Andrea Di Stefano | - -#### 1.5.7 May 17th 2023 #### - -Akka.NET v1.5.7 is a significant release that introduces a [major new reliable message delivery feature to Akka.NET and Akka.Cluster.Sharding](https://getakka.net/articles/actors/reliable-delivery.html): `Akka.Delivery`. - -**`Akka.Delivery`** - -Akka.Delivery is a reliable delivery system that leverages built-in actors, serialization, and persistence to help guarantee that all messages sent from one producer to one consumer will be delivered, in-order, even across process restarts / actor restarts / network outages. - -Akka.Delivery's functionality is divded across four libraries: - -* Akka - defines the base definitions for all messages, the `ProducerController` type, and the `ConsumerController` type; -* Akka.Cluster - contains the serialization definitions for Akka.Delivery; -* Akka.Persistence - contains the `EventSourcedProducerQueue` implementation, an optional feature that can be used to make the `ProducerController`'s outbound delivery queue persisted to the Akka.Persistence Journal and SnapshotStore; and -* Akka.Cluster.Sharding - contains the definitions for the `ShardingProducerController` and `ShardingConsumerController`. - -We've documented how these features work in the following two detailed articles official website: - -* "[Reliable Message Delivery with Akka.Delivery](https://getakka.net/articles/actors/reliable-delivery.html)" -* "[Reliable Delivery over Akka.Cluster.Sharding](https://getakka.net/articles/clustering/cluster-sharding-delivery.html)" - -If you want to see the [full set of changes made in Akka.NET v1.5.7, click here](https://github.com/akkadotnet/akka.net/milestone/86?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 9 | 13972 | 135 | Aaron Stannard | -| 6 | 92 | 88 | Ebere Abanonu | -| 4 | 803 | 807 | Simon Cropp | -| 3 | 70 | 53 | Gregorius Soedharmo | -| 3 | 3 | 3 | dependabot[bot] | - -#### 1.5.6 May 8th 2023 #### - -Version 1.5.6 is a patch with a few minor bug fix - -* [TestKit: Remove duplicate info log for unhandled messages](https://github.com/akkadotnet/akka.net/pull/6730) -* [Core: Change logging DateTime formatter from 12 hour to 24 hour format](https://github.com/akkadotnet/akka.net/pull/6734) - -If you want to see the [full set of changes made in Akka.NET v1.5.6, click here](https://github.com/akkadotnet/akka.net/milestone/88?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 2 | 4 | 4 | Aaron Stannard | -| 2 | 33 | 84 | Simon Cropp | -| 2 | 2 | 2 | dependabot[bot] | -| 2 | 2 | 2 | Richard Smith | -| 1 | 2 | 2 | Gregorius Soedharmo | -| 1 | 2 | 12 | Sergey Popov | - -#### 1.5.5 May 4th 2023 #### - -* [TestKit: Add new variant of `ExpectAll` that accepts predicates](https://github.com/akkadotnet/akka.net/pull/6668) -* [FSharp: Downgrade FSharp to v6.0.5](https://github.com/akkadotnet/akka.net/pull/6688) -* [Core: Bump Google.Protobuf from 3.22.1 to 3.22.3](https://github.com/akkadotnet/akka.net/pull/6648) -* [Core: Fix ByteString to check for index bounds](https://github.com/akkadotnet/akka.net/pull/6709) -* [Core: Fix ReceiveActor ReceiveAsync ReceiveTimeout bug](https://github.com/akkadotnet/akka.net/pull/6718) -* [Core: Fix race condition inside FastLazy](https://github.com/akkadotnet/akka.net/pull/6707) - -If you want to see the [full set of changes made in Akka.NET v1.5.5, click here](https://github.com/akkadotnet/akka.net/milestone/87?closed=1). - -7 contributors since release 1.5.4 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 16 | 68 | 34 | Ebere Abanonu | -| 9 | 598 | 1053 | Simon Cropp | -| 4 | 4 | 4 | dependabot[bot] | -| 2 | 229 | 5 | Gregorius Soedharmo | -| 1 | 33 | 28 | Aaron Stannard | -| 1 | 256 | 3 | Malcolm Learner | -| 1 | 148 | 140 | Sergey Popov | - -#### 1.5.4 April 25th 2023 #### - -* [Akka: Enhance IStash API and configuration](https://github.com/akkadotnet/akka.net/pull/6660) -* [Akka: Add bounded IStash configuration](https://github.com/akkadotnet/akka.net/pull/6661) - -**IStash Enhancements** - -`IStash` API have been improved with metrics API and its bound/capacity can be programatically set. Documentation can be read [here](https://getakka.net/articles/actors/receive-actor-api.html#bounded-stashes) - -If you want to see the [full set of changes made in Akka.NET v1.5.4, click here](https://github.com/akkadotnet/akka.net/milestone/86?closed=1). - -5 contributors since release 1.5.3 - -| COMMITS | LOC+ | LOC- | AUTHOR | -|-----------|-------|-------|---------------------| -| 7 | 477 | 486 | Ebere Abanonu | -| 4 | 627 | 143 | Aaron Stannard | -| 2 | 2 | 2 | dependabot[bot] | -| 1 | 87 | 0 | Sergey Popov | -| 1 | 0 | 1 | Gregorius Soedharmo | - -#### 1.5.3 April 20th 2023 #### - -* [Persistence.Sqlite: Bump Microsoft.Data.SQLite to 7.0.5](https://github.com/akkadotnet/akka.net/pull/6643) -* [Serialization.Hyperion: Fix bug: surrogate and known type provider not applied correctly by Setup](https://github.com/akkadotnet/akka.net/pull/6655) -* [Akka: Bump Microsoft.Extensions.ObjectPool to 7.0.5](https://github.com/akkadotnet/akka.net/pull/6644) -* [Persistence.Sql.Common: Add transaction isolation level to SQL queries](https://github.com/akkadotnet/akka.net/pull/6654) - -**SQL Transaction Isolation Level Setting** - -In 1.5.3, we're introducing fine-grained control over transaction isolation level inside the `Akka.Persistence.Sql.Common` common library. This setting will be propagated to the rest of the SQL persistence plugin ecosystem and the `Akka.Hosting` package in their next release version. - -Four new HOCON settings are introduced: -* `akka.persistence.journal.{plugin-name}.read-isolation-level` -* `akka.persistence.journal.{plugin-name}.write-isolation-level` -* `akka.persistence.snapshot-store.{plugin-name}.read-isolation-level` -* `akka.persistence.snapshot-store.{plugin-name}.write-isolation-level` - -you can go to the [official Microsoft documentation](https://learn.microsoft.com/en-us/dotnet/api/system.data.isolationlevel?#fields) to read more about these isolation level settings. - -If you want to see the [full set of changes made in Akka.NET v1.5.3, click here](https://github.com/akkadotnet/akka.net/milestone/85?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 23 | 1284 | 1248 | Ebere Abanonu | -| 4 | 7 | 7 | dependabot[bot] | -| 3 | 933 | 267 | Gregorius Soedharmo | -| 2 | 4498 | 4407 | Aaron Stannard | - -#### 1.5.2 April 5th 2023 #### - -There are some major behavioral changes introduced to Akka.Cluster and Akka.Persistence in Akka.NET v1.5.2 - to learn how these changes might affect your Akka.NET applications, please [see our Akka.NET v1.5.2 Upgrade Advisories on the Akka.NET website](https://getakka.net/community/whats-new/akkadotnet-v1.5-upgrade-advisories.html#upgrading-to-akkanet-v152). - -* [Akka.Remote: Remove secure cookie from configuration](https://github.com/akkadotnet/akka.net/pull/6515) -* [DData: Remove unused _pruningPerformed and _tombstonedNodes variables](https://github.com/akkadotnet/akka.net/pull/6526) -* [Akka.Persistence: Remove default object serializer in Sql.Common](https://github.com/akkadotnet/akka.net/pull/6528) -* [Akka.Cluster: Log send time in verbose heartbeat message](https://github.com/akkadotnet/akka.net/pull/6548) -* [Akka.Streams: Optimize ForEachAsync](https://github.com/akkadotnet/akka.net/pull/6538) -* [Akka: Implement alternative AtomicState leveraging WaitAsync](https://github.com/akkadotnet/akka.net/pull/6109) -* [Akka.Streams: Use correct capacity when creating DynamicQueue when FixedQueue is full](https://github.com/akkadotnet/akka.net/pull/6632) -* [Akka.Cluster: Enable keep majority split brain resolver as default](https://github.com/akkadotnet/akka.net/pull/6628) - -If you want to see the [full set of changes made in Akka.NET v1.5.2, click here](https://github.com/akkadotnet/akka.net/milestone/83?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 56 | 2580 | 2913 | Ebere Abanonu | -| 5 | 201 | 82 | Aaron Stannard | -| 4 | 754 | 558 | Ismael Hamed | -| 3 | 4 | 4 | dependabot[bot] | -| 2 | 33 | 12 | Sergey Popov | -| 1 | 511 | 53 | Gregorius Soedharmo | -| 1 | 1 | 1 | ondravondra | -| 1 | 0 | 2 | Simon Cropp | - -#### 1.5.1 March 15th 2023 #### - -* [Akka.Persistence: Improve memory allocation](https://github.com/akkadotnet/akka.net/pull/6487) -* [Akka.Persistence: Implement persistence query in InMemory journal](https://github.com/akkadotnet/akka.net/pull/6409) -* [Akka: Fix bugs reported by PVS-Studio static analysis](https://github.com/akkadotnet/akka.net/pull/6497) -* [Akka: Bump Google.Protobuf to 3.22.1](https://github.com/akkadotnet/akka.net/pull/6500) -* [Akka.Persistence.Sqlite: Bump Microsoft.Data.SQLite to 7.0.4](https://github.com/akkadotnet/akka.net/pull/6516) -* [Akka: Fix StackOverflow exception in NewtonSoftSerializer](https://github.com/akkadotnet/akka.net/pull/6503) - -If you want to see the [full set of changes made in Akka.NET v1.5.1, click here](https://github.com/akkadotnet/akka.net/milestone/82?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 9 | 425 | 331 | Ebere Abanonu | -| 5 | 6 | 6 | dependabot[bot] | -| 3 | 2399 | 109 | Sergey Popov | -| 1 | 97 | 4 | Gregorius Soedharmo | -| 1 | 2 | 2 | Aaron Stannard | - -#### 1.5.0 March 2nd 2023 #### -Version 1.5.0 is a major new release of Akka.NET that is now marked as stable and ready for production use. - -You can read the [full notes about what's changed in Akka.NET v1.5 here](https://getakka.net/community/whats-new/akkadotnet-v1.5.html). We also encourage you to watch our video: "[Akka NET v1.5 New Features and Upgrade Guide](https://www.youtube.com/watch?v=-UPestlIw4k)" - -If you want to see the [full set of changes made in Akka.NET v1.5.0 so far, click here](https://github.com/akkadotnet/akka.net/milestone/7). - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 95 | 25041 | 24976 | Gregorius Soedharmo | -| 85 | 89784 | 18362 | Aaron Stannard | -| 76 | 95 | 95 | dependabot[bot] | -| 18 | 3201 | 908 | Ismael Hamed | -| 5 | 230 | 251 | Sergey Popov | -| 2 | 77 | 7 | Vagif Abilov | -| 2 | 38 | 8 | Brah McDude | -| 1 | 92 | 92 | nabond251 | -| 1 | 843 | 0 | Drew | -| 1 | 7 | 6 | Tjaart Blignaut | -| 1 | 5 | 4 | Sean Killeen | -| 1 | 32 | 1 | JonnyII | -| 1 | 26 | 4 | Thomas Stegemann | -| 1 | 203 | 5 | Ebere Abanonu | -| 1 | 2 | 2 | Popov Sergey | -| 1 | 2 | 2 | Denis | -| 1 | 16 | 0 | Damian | -| 1 | 11 | 2 | Nicolai Davies | -| 1 | 101 | 3 | aminchenkov | -| 1 | 1 | 1 | zbynek001 | -| 1 | 1 | 1 | Michel van Os | -| 1 | 1 | 1 | Adrian D. Alvarez | - -#### 1.5.0-beta5 February 28th 2023 #### -Version 1.5.0-beta5 contains **breaking API changes** and new API changes for Akka.NET. - -* [Akka.Cluster: Remove `JoinAsync` and `JoinSeedNodesAsync` default timeout values](https://github.com/akkadotnet/akka.net/pull/6473) -* [Akka.Event: expose `Args()` on `LogMessage`](https://github.com/akkadotnet/akka.net/pull/6472) - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 2 | 50 | 28 | Aaron Stannard | -| 1 | 22 | 32 | Gregorius Soedharmo | - -#### 1.5.0-beta4 February 28th 2023 #### -Version 1.5.0-beta4 contains **breaking API changes** and new API changes for Akka.NET. - -* [Akka.Persistence.TCK: remove `IDisposable` from Akka.Persistence.TCK](https://github.com/akkadotnet/akka.net/pull/6465) - this hid methods from the `TestKit` base classes. -* [Akka.Remote: Make transport adapter messages public](https://github.com/akkadotnet/akka.net/pull/6469) - adds back public APIs from v1.4. -* [Akka.TestKit: fix accidental breaking changes in v1.5.0-beta3](https://github.com/akkadotnet/akka.net/issues/6466) - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 2 | 110 | 37 | Aaron Stannard | -| 1 | 253 | 7 | Gregorius Soedharmo | - -#### 1.5.0-beta3 February 27th 2023 #### -Version 1.5.0-beta3 contains **breaking API changes** and new API changes for Akka.NET. - -* Removed a number of `Obsolete` APIs that were generally not used much. -* [Akka.Actor: `ActorSystem.Create` throws `PlatformNotSupportedException` on net7.0-android](https://github.com/akkadotnet/akka.net/issues/6459) -* [Akka.Actor: Append message content to `DeadLetter` log messages](https://github.com/akkadotnet/akka.net/pull/6448) -* [Akka.Streams: Use `ActorSystem` for `Materializer`](https://github.com/akkadotnet/akka.net/pull/6453) - *massive* memory improvements for applications that materialize a large number of streams. -* [Akka.Persistence.Query.Sql: backpressure control for queries](https://github.com/akkadotnet/akka.net/pull/6436) - full details on this here: https://petabridge.com/blog/largescale-cqrs-akkadotnet-v1.5/ - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 14 | 68 | 794 | Gregorius Soedharmo | -| 5 | 997 | 498 | Aaron Stannard | -| 3 | 6 | 6 | dependabot[bot] | - -#### 1.5.0-beta2 February 20th 2023 #### -Version 1.5.0-beta2 contains **breaking API changes** and new API changes for Akka.NET. - -* [Akka.Event: Add K to the DateTime format string to include TZ information](https://github.com/akkadotnet/akka.net/pull/6419) -* [Akka.TestKit: Reintroduce old code and mark them obsolete](https://github.com/akkadotnet/akka.net/pull/6420) - fixes major regression in Akka.TestKit.Xunit2 where we removed `IDipsoable` before. This PR reintroduces it for backwards compat. -* [Akka.Cluster.Sharding: clean its internal cache if region/proxy died](https://github.com/akkadotnet/akka.net/pull/6424) -* [Akka.Util: Harden `Option` by disallowing null value](https://github.com/akkadotnet/akka.net/pull/6426) -* [Akka.Util: move `DateTime` / `TimeSpan` extension APIs out of Akka.Util and into Akka.Cluster.Metrics](https://github.com/akkadotnet/akka.net/pull/6427) -* [Akka.Util: Remove unsafe `implicit` conversion operators in `AtomicBoolean` and `AtomicReference`](https://github.com/akkadotnet/akka.net/pull/6429) -* [Akka: Standardize on C# 11.0](https://github.com/akkadotnet/akka.net/pull/6431) -* [Akka.Persistence: improve `AsyncWriteJournal` and `PersistentActor` performance](https://github.com/akkadotnet/akka.net/pull/6432) - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 8 | 260 | 942 | Aaron Stannard | -| 5 | 169 | 60 | Gregorius Soedharmo | - -#### 1.5.0-beta1 February 20th 2023 #### -Version 1.5.0-beta1 contains **breaking API changes** and new API changes for Akka.NET. - -**Breaking Changes: Logging** - -In [https://github.com/akkadotnet/akka.net/pull/6408](https://github.com/akkadotnet/akka.net/pull/6408) the entire `ILoggingAdapter` interface was rewritten in order to improve extensibility and performance (logging is now 30-40% faster in all cases and allocates ~50% fewer objects for large format strings). - -All of the changes made here are _source compatible_, but not _binary compatible_ - meaning that users and package authors will need to do the following: - -* Add `using Akka.Event` in all files that used the `ILoggingAdapter` and -* Recompile. - -> NOTE: you can use a [`global using Akka.Event` directive](https://devblogs.microsoft.com/dotnet/welcome-to-csharp-10/#global-using-directives) to do this solution / project-wide if your project supports C# 10 and / or .NET 6. - -In addition to improving the performance of the `ILoggingAdapter` system, we've also made it more extensible - for instance, you can now globally configure the `ILogMessageFormatter` via the following HOCON: - -``` -akka { - loglevel=INFO, - loggers=["Akka.Logger.Serilog.SerilogLogger, Akka.Logger.Serilog"] - logger-formatter="Akka.Logger.Serilog.SerilogLogMessageFormatter, Akka.Logger.Serilog" -} -``` - -That will allow users to use the `SerilogLogMessageFormatter` globally throughout their applications - no more annoying calls like this inside individual actors that want to use semantic logging: - -```csharp -private readonly ILoggingAdapter _logger = Context.GetLogger(); -``` - -**Breaking Changes: Akka.Persistence.Sql.Common** - -This is a breaking change that should effect almost no users, but [we deleted some old, bad ideas from the API surface](https://github.com/akkadotnet/akka.net/pull/6412) and it might require all Akka.Persistence.Sql* plugins to be recompiled. - -For what it's worth, [Akka.Persistence.Sql.Common's performance has been improved significantly](https://github.com/akkadotnet/akka.net/pull/6384) and we'll continue working on that with some additional API changes this week. - -**Other Changes and Additions** - -* [Akka.Actor: New API - `IActorRef.WatchAsync`](https://github.com/akkadotnet/akka.net/pull/6102) - adds a new extension method to `IActorRef` which allows users to subscribe to actor lifecycle notifications outside of the `ActorSystem`. -* [Akka.Actor: Suppress `System.Object` warning for serializer configuration changes](https://github.com/akkadotnet/akka.net/issues/6377) - -If you want to see the [full set of changes made in Akka.NET v1.5.0 so far, click here](https://github.com/akkadotnet/akka.net/milestone/7?closed=1). - - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 12 | 15 | 15 | dependabot[bot] | -| 11 | 1930 | 1278 | Aaron Stannard | -| 2 | 143 | 73 | Sergey Popov | -| 1 | 26 | 4 | Thomas Stegemann | -| 1 | 1 | 1 | Michel van Os | - -#### 1.5.0-alpha4 February 1st 2023 #### -Version 1.5.0-alpha3 contains several bug fixes and new features to Akka.NET - -* [Akka.TestKit: Remove Akka.Tests.Shared.Internal dependency](https://github.com/akkadotnet/akka.net/pull/6258) -* [Akka.TestKit: Added ReceiveAsync feature to TestActorRef](https://github.com/akkadotnet/akka.net/pull/6281) -* [Akka.Stream: Fix `IAsyncEnumerator.DisposeAsync` bug](https://github.com/akkadotnet/akka.net/pull/6296) -* [Akka: Add `Exception` serialization support for built-in messages](https://github.com/akkadotnet/akka.net/pull/6300) -* [Akka: Add simple actor telemetry feature](https://github.com/akkadotnet/akka.net/pull/6299) -* [Akka.Streams: Move Channel stages from Alpakka to Akka.NET repo](https://github.com/akkadotnet/akka.net/pull/6268) -* [Akka: Set actor stash capacity to actor mailbox or dispatcher size](https://github.com/akkadotnet/akka.net/pull/6323) -* [Akka: Add `ByteString` support to copy to/from `Memory` and `Span`](https://github.com/akkadotnet/akka.net/pull/6026) -* [Akka: Add support for `UnrestrictedStash`](https://github.com/akkadotnet/akka.net/pull/6325) -* [Akka: Add API for `UntypedActorWithStash`](https://github.com/akkadotnet/akka.net/pull/6327) -* [Akka.Persistence.Sql.Common: Fix unhandled `DbExceptions` that are wrapped inside `AggregateException`](https://github.com/akkadotnet/akka.net/pull/6361) -* [Akka.Persistence.Sql: Fix persistence id publisher actor hung on failure messages](https://github.com/akkadotnet/akka.net/pull/6374) -* [Akka: Change default pool router supervisor strategy to `Restart`](https://github.com/akkadotnet/akka.net/pull/6370) -* NuGet package upgrades: - * [Bump Microsoft.Data.SQLite from 6.0.10 to 7.0.2](https://github.com/akkadotnet/akka.net/pull/6339) - * [Bump Google.Protobuf from 3.21.9 to 3.21.12](https://github.com/akkadotnet/akka.net/pull/6311) - * [Bump Newtonsoft.Json from 9.0.1 to 13.0.1](https://github.com/akkadotnet/akka.net/pull/6303) - * [Bump Microsoft.Extensions.ObjectPool from 6.0.10 to 7.0.2](https://github.com/akkadotnet/akka.net/pull/6340) - * [Bump Microsoft.Extensions.DependencyInjection from 6.0.1 to 7.0.0](https://github.com/akkadotnet/akka.net/pull/6234) - -If you want to see the [full set of changes made in Akka.NET v1.5.0 so far, click here](https://github.com/akkadotnet/akka.net/milestone/7?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 27 | 30 | 30 | dependabot[bot] | -| 11 | 2212 | 165 | Gregorius Soedharmo | -| 4 | 741 | 208 | Ismael Hamed | -| 4 | 680 | 112 | Aaron Stannard | -| 3 | 87 | 178 | Sergey Popov | -| 1 | 843 | 0 | Drew | -| 1 | 2 | 2 | Popov Sergey | - -#### 1.5.0-alpha3 November 15th 2022 #### -Akka.NET v1.5.0-alpha3 is a security patch for Akka.NET v1.5.0-alpha2 but also includes some other fixes. - -**Security Advisory**: Akka.NET v1.5.0-alpha2 and earlier depend on an old System.Configuration.ConfigurationManager version 4.7.0 which transitively depends on System.Common.Drawing v4.7.0. The System.Common.Drawing v4.7.0 is affected by a remote code execution vulnerability [GHSA-ghhp-997w-qr28](https://github.com/advisories/GHSA-ghhp-997w-qr28). - -We have separately created a security advisory for [Akka.NET Versions < 1.4.46 and < 1.5.0-alpha3 to track this issue](https://github.com/akkadotnet/akka.net/security/advisories/GHSA-gpv5-rp6w-58r8). - -**Fixes and Updates** - -* [Akka: Revert ConfigurationException due to binary incompatibility](https://github.com/akkadotnet/akka.net/pull/6204) -* [Akka: Upgrade to Newtonsoft.Json 13.0.1 as minimum version](https://github.com/akkadotnet/akka.net/pull/6230) - resolves security issue. -* [Akka: Upgrade to System.Configuration.ConfigurationManager 6.0.1](https://github.com/akkadotnet/akka.net/pull/6229) - resolves security issue. -* [Akka: Upgrade to Google.Protobuf 3.21.9](https://github.com/akkadotnet/akka.net/pull/6217) -* [Akka.Cluster.Tools: Make sure that `DeadLetter`s published by `DistributedPubSubMediator` contain full context of topic](https://github.com/akkadotnet/akka.net/pull/6212) -* [Akka.Streams: Remove suspicious code fragment in ActorMaterializer](https://github.com/akkadotnet/akka.net/pull/6216) -* [Akka.IO: Report cause for Akka/IO TCP `CommandFailed` events](https://github.com/akkadotnet/akka.net/pull/6221) -* [Akka.Cluster.Metrics: Improve CPU/Memory metrics collection at Akka.Cluster.Metrics](https://github.com/akkadotnet/akka.net/pull/6225) - built-in metrics are now much more accurate. - -You can see the [full set of tracked issues for Akka.NET v1.5.0 here](https://github.com/akkadotnet/akka.net/milestone/7). - -#### 1.5.0-alpha2 October 17th 2022 #### -Akka.NET v1.5.0-alpha2 is a maintenance release for Akka.NET v1.5 that contains numerous performance improvements in critical areas, including core actor message processing and Akka.Remote. - -**Performance Fixes** - -* [remove delegate allocation from `ForkJoinDispatcher` and `DedicatedThreadPool`](https://github.com/akkadotnet/akka.net/pull/6162) -* [eliminate `Mailbox` delegate allocations](https://github.com/akkadotnet/akka.net/pull/6162) -* [Reduce `FSM` allocations](https://github.com/akkadotnet/akka.net/pull/6162) -* [removed boxing allocations inside `FSM.State.Equals`](https://github.com/akkadotnet/akka.net/pull/6196) -* [Eliminate `DefaultLogMessageFormatter` allocations](https://github.com/akkadotnet/akka.net/pull/6166) - -In sum you should expect to see total memory consumption, garbage collection, and throughput improve when you upgrade to Akka.NET v1.5.0-alpha2. - -**Other Features and Improvements** - -* [DData: Suppress gossip message from showing up in debug log unless verbose debug logging is turned on](https://github.com/akkadotnet/akka.net/pull/6089) -* [TestKit: TestKit automatically injects the default TestKit default configuration if an ActorSystem is passed into its constructor](https://github.com/akkadotnet/akka.net/pull/6092) -* [Sharding: Added a new `GetEntityLocation` query message to retrieve an entity address location in the shard region](https://github.com/akkadotnet/akka.net/pull/6107) -* [Sharding: Fixed `GetEntityLocation` uses wrong actor path](https://github.com/akkadotnet/akka.net/pull/6121) -* [Akka.Cluster and Akka.Cluster.Sharding: should throw human-friendly exception when accessing cluster / sharding plugins when clustering is not running](https://github.com/akkadotnet/akka.net/pull/6169) -* [Akka.Cluster.Sharding: Add `HashCodeMessageExtractor` factory](https://github.com/akkadotnet/akka.net/pull/6173) -* [Akka.Persistence.Sql.Common: Fix `DbCommand.CommandTimeout` in `BatchingSqlJournal`](https://github.com/akkadotnet/akka.net/pull/6175) - - -#### 1.5.0-alpha1 August 22 2022 #### -Akka.NET v1.5.0-alpha1 is a major release that contains a lot of code improvement and rewrites/refactors. **Major upgrades to Akka.Cluster.Sharding in particular**. - -__Deprecation__ - -Some codes and packages are being deprecated in v1.5 -* [Deprecated/removed Akka.DI package](https://github.com/akkadotnet/akka.net/pull/6003) - Please use the new `Akka.DependencyInjection` NuGet package as a replacement. Documentation can be read [here](https://getakka.net/articles/actors/dependency-injection.html) -* [Deprecated/removed Akka.MultiNodeTestRunner package](https://github.com/akkadotnet/akka.net/pull/6002) - Please use the new `Akka.MultiNode.TestAdapter` NuGet package as a replacement. Documentation can be read [here](https://getakka.net/articles/testing/multi-node-testing.html). -* [Streams] [Refactor `SetHandler(Inlet, Outlet, IanAndOutGraphStageLogic)` to `SetHandlers()`](https://github.com/akkadotnet/akka.net/pull/5931) - -__Changes__ - -__Akka__ - -* [Add dual targetting to support .NET 6.0](https://github.com/akkadotnet/akka.net/pull/5926) - All `Akka.NET` packages are now dual targetting netstandard2.0 and net6.0 platforms, we will be integrating .NET 6.0 better performing API and SDK in the future. -* [Add `IThreadPoolWorkItem` support to `ThreadPoolDispatcher`](https://github.com/akkadotnet/akka.net/pull/5943) -* [Add `ValueTask` support to `PipeTo` extensions](https://github.com/akkadotnet/akka.net/pull/6025) -* [Add `CancellationToken` support to `Cancelable`](https://github.com/akkadotnet/akka.net/pull/6032) -* [Fix long starting loggers crashing `ActorSystem` startup](https://github.com/akkadotnet/akka.net/pull/6053) - All loggers are asynchronously started during `ActorSystem` startup. A warning will be logged if a logger does not respond within the prescribed `akka.logger-startup-timeout` period and will be awaited upon in a detached task until the `ActorSystem` is shut down. This have a side effect in that slow starting loggers might not be able to capture all log events emmited by the `EventBus` until it is ready. - -__Akka.Cluster__ - -* [Fix `ChannelTaskScheduler` to work with Akka.Cluster, ported from 1.4](https://github.com/akkadotnet/akka.net/pull/5920) -* [Harden `Cluster.JoinAsync()` and `Cluster.JoinSeedNodesAsync()` methods](https://github.com/akkadotnet/akka.net/pull/6033) -* [Fix `ShardedDaemonProcess` should use lease, if configured](https://github.com/akkadotnet/akka.net/pull/6058) -* [Make `SplitBrainResolver` more tolerant to invalid node records](https://github.com/akkadotnet/akka.net/pull/6064) -* [Enable `Heartbeat` and `HearbeatRsp` message serialization and deserialization](https://github.com/akkadotnet/akka.net/pull/6063) - By default, `Akka.Cluster` will now use the new `Heartbeat` and `HartbeatRsp` message serialization/deserialization that was introduced in version 1.4.19. If you're doing a rolling upgrade from a version older than 1.4.19, you will need to set `akka.cluster.use-legacy-heartbeat-message` to true. - -__Akka.Cluster.Sharding__ - -* [Make Cluster.Sharding recovery more tolerant against corrupted persistence data](https://github.com/akkadotnet/akka.net/pull/5978) -* [Major reorganization to Akka.Cluster.Sharding](https://github.com/akkadotnet/akka.net/pull/5857) - -The Akka.Cluster.Sharding changes in Akka.NET v1.5 are significant, but backwards compatible with v1.4 and upgrades should happen seamlessly. - -Akka.Cluster.Sharding's `state-store-mode` has been split into two parts: - -* CoordinatorStore -* ShardStore - -Which can use different persistent mode configured via `akka.cluster.sharding.state-store-mode` & `akka.cluster.sharding.remember-entities-store`. - -Possible combinations: - -state-store-mode | remember-entities-store | CoordinatorStore mode | ShardStore mode ------------------- | ------------------------- | ------------------------ | ------------------ -persistence (default) | - (ignored) | persistence | persistence -ddata | ddata | ddata | ddata -ddata | eventsourced (new) | ddata | persistence - -There should be no breaking changes from user perspective. Only some internal messages/objects were moved. -There should be no change in the `PersistentId` behavior and default persistent configuration (`akka.cluster.sharding.state-store-mode`) - -This change is designed to speed up the performance of Akka.Cluster.Sharding coordinator recovery by moving `remember-entities` recovery into separate actors - this also solves major performance problems with the `ddata` recovery mode overall. - -The recommended settings for maximum ease-of-use for Akka.Cluster.Sharding going forward will be: - -``` -akka.cluster.sharding{ - state-store-mode = ddata - remember-entities-store = eventsourced -} -``` - -However, for the sake of backwards compatibility the Akka.Cluster.Sharding defaults have been left as-is: - -``` -akka.cluster.sharding{ - state-store-mode = persistence - # remember-entities-store (not set - also uses legacy Akka.Persistence) -} -``` - -Switching over to using `remember-entities-store = eventsourced` will cause an initial migration of data from the `ShardCoordinator`'s journal into separate event journals going forward - __this migration is irreversible__ without taking the cluster offline and deleting all Akka.Cluster.Sharding-related data from Akka.Persistence, so plan accordingly. - -__Akka.Cluster.Tools__ - -* [Add typed `ClusterSingleton` support](https://github.com/akkadotnet/akka.net/pull/6050) -* [Singleton can use `Member.AppVersion` metadata to decide its host node during hand-over](https://github.com/akkadotnet/akka.net/pull/6065) - `Akka.Cluster.Singleton` can use `Member.AppVersion` metadata when it is relocating the singleton instance. When turned on, new singleton instance will be created on the oldest node in the cluster with the highest `AppVersion` number. You can opt-in to this behavior by setting `akka.cluster.singleton.consider-app-version` to true. - -__Akka.Persistence.Query__ - -* [Add `TimeBasedUuid` offset property](https://github.com/akkadotnet/akka.net/pull/5995) - -__Akka.Remote__ - -* [Fix typo in HOCON SSL settings. Backward compatible with the old setting names](https://github.com/akkadotnet/akka.net/pull/5895) -* [Treat all exceptions thrown inside `EndpointReader` message dispatch as transient, Ported from 1.4](https://github.com/akkadotnet/akka.net/pull/5972) -* [Fix SSL enable HOCON setting](https://github.com/akkadotnet/akka.net/pull/6038) - -__Akka.Streams__ - -* [Allow GroupBy sub-flow to re-create closed sub-streams, backported to 1.4](https://github.com/akkadotnet/akka.net/pull/5874) -* [Fix ActorRef source not completing properly, backported to 1.4](https://github.com/akkadotnet/akka.net/pull/5875) -* [Rewrite `ActorRefSink` as a `GraphStage`](https://github.com/akkadotnet/akka.net/pull/5920) -* [Add stream cancellation cause upstream propagation, ported from 1.4](https://github.com/akkadotnet/akka.net/pull/5949) -* [Fix `VirtualProcessor` subscription bug, ported from 1.4](https://github.com/akkadotnet/akka.net/pull/5950) -* [Refactor `Sink.Ignore` signature from `Task` to `Task`](https://github.com/akkadotnet/akka.net/pull/5973) -* [Add `SourceWithContext.FromTuples()` operator`](https://github.com/akkadotnet/akka.net/pull/5987) -* [Add `GroupedWeightedWithin` operator](https://github.com/akkadotnet/akka.net/pull/6000) -* [Add `IAsyncEnumerable` source](https://github.com/akkadotnet/akka.net/pull/6044) - -__Akka.TestKit__ - -* [Rewrite Akka.TestKit to work asynchronously from the ground up](https://github.com/akkadotnet/akka.net/pull/5953) - - - -#### 1.4.37 April 14 2022 #### -Akka.NET v1.4.37 is a minor release that contains some minor bug fixes. - -* [Persistence.Query: Change AllEvents query failure log severity from Debug to Error](https://github.com/akkadotnet/akka.net/pull/5835) -* [Coordination: Harden LeaseProvider instance Activator exception handling](https://github.com/akkadotnet/akka.net/pull/5838) -* [Akka: Make ActorSystemImpl.Abort skip the CoordinatedShutdown check](https://github.com/akkadotnet/akka.net/pull/5839) - -If you want to see the [full set of changes made in Akka.NET v1.4.37, click here](https://github.com/akkadotnet/akka.net/milestone/68?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 3 | 15 | 4 | Gregorius Soedharmo | -| 1 | 2 | 2 | dependabot[bot] | - -#### 1.4.36 April 4 2022 #### -Akka.NET v1.4.36 is a minor release that contains some bug fixes. Most of the changes have been aimed at improving our web documentation and code cleanup to modernize some of our code. - -* [Akka: Bump Hyperion to 0.12.2](https://github.com/akkadotnet/akka.net/pull/5805) - -__Bug fixes__: -* [Akka: Fix CoordinatedShutdown memory leak](https://github.com/akkadotnet/akka.net/pull/5816) -* [Akka: Fix TcpConnection error handling and death pact de-registration](https://github.com/akkadotnet/akka.net/pull/5817) - -If you want to see the [full set of changes made in Akka.NET v1.4.36, click here](https://github.com/akkadotnet/akka.net/milestone/67?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 5 | 274 | 33 | Gregorius Soedharmo | -| 4 | 371 | 6 | Ebere Abanonu | -| 3 | 9 | 3 | Aaron Stannard | -| 1 | 34 | 38 | Ismael Hamed | -| 1 | 2 | 3 | Adrian Leonhard | - -#### 1.4.35 March 18 2022 #### -Akka.NET v1.4.35 is a minor release that contains some bug fixes. Most of the changes have been aimed at improving our web documentation and code cleanup to modernize some of our code. - -__Bug fixes__: -* [Akka: Fixed IActorRef leak inside EventStream](https://github.com/akkadotnet/akka.net/pull/5720) -* [Akka: Fixed ActorSystemSetup.And forgetting registered types](https://github.com/akkadotnet/akka.net/issues/5728) -* [Akka.Persistence.Query.Sql: Fixed Query PersistenceIds query bug](https://github.com/akkadotnet/akka.net/pull/5715) -* [Akka.Streams: Add MapMaterializedValue for SourceWithContext and FlowWithContext](https://github.com/akkadotnet/akka.net/pull/5711) - -If you want to see the [full set of changes made in Akka.NET v1.4.35, click here](https://github.com/akkadotnet/akka.net/milestone/66?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|------|------|---------------------| -| 6 | 2178 | 174 | Aaron Stannard | -| 2 | 43 | 33 | Gregorius Soedharmo | -| 1 | 71 | 19 | Ismael Hamed | -| 1 | 1 | 1 | dependabot[bot] | - -#### 1.4.34 March 7 2022 #### -Akka.NET v1.4.34 is a minor release that contains some bug fixes. Most of the changes have been aimed at improving our web documentation and code cleanup to modernize some of our code. - -__Bug fixes__: -* [Akka: Added support to pass a state object into CircuitBreaker to reduce allocations](https://github.com/akkadotnet/akka.net/pull/5650) -* [Akka.DistributedData: ORSet merge operation performance improvement](https://github.com/akkadotnet/akka.net/pull/5686) -* [Akka.Streams: FlowWithContext generic type parameters have been reordered to make them easier to read](https://github.com/akkadotnet/akka.net/pull/5648) - -__Improvements__: -* [Akka: PipeTo can be configured to retain async threading context](https://github.com/akkadotnet/akka.net/pull/5684) - -If you want to see the [full set of changes made in Akka.NET v1.4.34, click here](https://github.com/akkadotnet/akka.net/milestone/65?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -|---------|--------|-------|---------------------| -| 12 | 1177 | 718 | Ebere Abanonu | -| 6 | 192 | 47 | Gregorius Soedharmo | -| 3 | 255 | 167 | Ismael Hamed | -| 1 | 3 | 0 | Aaron Stannard | -| 1 | 126 | 10 | Drew | - -#### 1.4.33 February 14 2022 #### -Akka.NET v1.4.33 is a minor release that contains some bug fixes. Most of the changes have been aimed at improving our web documentation and code cleanup to modernize some of our code. The most important bug fix is the actor Props memory leak when actors are cached inside Akka.Remote. - -* [Akka: Fix memory leak bug within actor Props](https://github.com/akkadotnet/akka.net/pull/5556) -* [Akka: Fix ChannelExecutor configuration backward compatibility bug](https://github.com/akkadotnet/akka.net/pull/5568) -* [Akka.TestKit: Fix ExpectAsync detached Task bug](https://github.com/akkadotnet/akka.net/pull/5538) -* [DistributedPubSub: Fix DeadLetter suppression for topics with no subscribers](https://github.com/akkadotnet/akka.net/pull/5561) - -If you want to see the [full set of changes made in Akka.NET v1.4.33, click here](https://github.com/akkadotnet/akka.net/milestone/64?closed=1). - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 63 | 1264 | 1052 | Ebere Abanonu | -| 9 | 221 | 27 | Brah McDude | -| 8 | 2537 | 24 | Gregorius Soedharmo | -| 2 | 4 | 1 | Aaron Stannard | -| 1 | 2 | 2 | ignobilis | - -#### 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) - -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 | -| 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.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 -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 | - -#### 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 | - -#### 1.4.29 December 13 2021 #### -**Maintenance Release for Akka.NET 1.4** -Akka.NET v1.4.29 is a minor release that contains some enhancements for Akka.Streams and some bug fixes. - -New features: -* [Akka: Added a channel based task scheduler](https://github.com/akkadotnet/akka.net/pull/5403) -* [Akka.Discovery: Moved Akka.Discovery out of beta](https://github.com/akkadotnet/akka.net/pull/5380) - -Documentation: -* [Akka: Added a serializer ID troubleshooting table](https://github.com/akkadotnet/akka.net/pull/5418) -* [Akka.Cluster.Sharding: Added a tutorial section](https://github.com/akkadotnet/akka.net/pull/5421) - -Bug fixes: -* [Akka.Cluster: Changed Akka.Cluster startup sequence](https://github.com/akkadotnet/akka.net/pull/5398) -* [Akka.DistributedData: Fix LightningDB throws MDB_NOTFOUND when data directory already exist](https://github.com/akkadotnet/akka.net/pull/5424) -* [Akka.IO: Fix memory leak on UDP connector](https://github.com/akkadotnet/akka.net/pull/5404) -* [Akka.Persistence.Sql: Fix performance issue with highest sequence number query](https://github.com/akkadotnet/akka.net/pull/5420) - -If you want to see the [full set of changes made in Akka.NET v1.4.29, click here](https://github.com/akkadotnet/akka.net/milestone/60). - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 7 | 82 | 51 | Aaron Stannard | -| 6 | 1381 | 483 | Gregorius Soedharmo | -| 4 | 618 | 85 | Andreas Dirnberger | -| 1 | 4 | 4 | Luca V | -| 1 | 1 | 1 | dependabot[bot] | - -#### 1.4.28 November 10 2021 #### -**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 - -#### 1.4.27 October 11 2021 #### -**Maintenance Release for Akka.NET 1.4** -Akka.NET v1.4.27 is a small release that contains some _major_ performance improvements for Akka.Remote. - -**Performance Fixes** -In [RemoteActorRefProvider address paring, caching and resolving improvements](https://github.com/akkadotnet/akka.net/pull/5273) Akka.NET contributor @Zetanova introduced some major changes that make the entire `ActorPath` class much more reusable and more parse-efficient. - -Our last major round of Akka.NET performance improvements in Akka.NET v1.4.25 produced the following: - -``` -OSVersion: Microsoft Windows NT 6.2.9200.0 -ProcessorCount: 16 -ClockSpeed: 0 MHZ -Actor Count: 32 -Messages sent/received per client: 200000 (2e5) -Is Server GC: True -Thread count: 111 - -Num clients, Total [msg], Msgs/sec, Total [ms] - 1, 200000, 130634, 1531.54 - 5, 1000000, 246975, 4049.20 - 10, 2000000, 244499, 8180.16 - 15, 3000000, 244978, 12246.39 - 20, 4000000, 245159, 16316.37 - 25, 5000000, 243333, 20548.09 - 30, 6000000, 241644, 24830.55 -``` - -In Akka.NET v1.4.27 those numbers now look like: - -``` -OSVersion: Microsoft Windows NT 6.2.9200. -ProcessorCount: 16 -ClockSpeed: 0 MHZ -Actor Count: 32 -Messages sent/received per client: 200000 (2e5) -Is Server GC: True -Thread count: 111 - -Num clients, Total [msg], Msgs/sec, Total [ms] - 1, 200000, 105043, 1904.29 - 5, 1000000, 255494, 3914.73 - 10, 2000000, 291843, 6853.30 - 15, 3000000, 291291, 10299.75 - 20, 4000000, 286513, 13961.68 - 25, 5000000, 292569, 17090.64 - 30, 6000000, 281492, 21315.35 -``` - -To put these numbers in comparison, here's what Akka.NET's performance looked like as of v1.4.0: - -``` -Num clients (actors) Total [msg] Msgs/sec Total [ms] -1 200000 69736 2868.60 -5 1000000 141243 7080.98 -10 2000000 136771 14623.27 -15 3000000 38190 78556.49 -20 4000000 32401 123454.60 -25 5000000 33341 149967.08 -30 6000000 126093 47584.92 -``` - - -We've made Akka.Remote consistently faster, more predictable, and reduced total memory consumption significantly in the process. - - -You can [see the full set of changes introduced in Akka.NET v1.4.27 here](https://github.com/akkadotnet/akka.net/milestone/57?closed=1) - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 3 | 89 | 8 | Aaron Stannard | -| 1 | 856 | 519 | Andreas Dirnberger | -| 1 | 3 | 4 | Vadym Artemchuk | -| 1 | 261 | 233 | Gregorius Soedharmo | -| 1 | 1 | 1 | dependabot[bot] | - -#### 1.4.26 September 28 2021 #### -**Maintenance Release for Akka.NET 1.4** -Akka.NET v1.4.26 is a very small release that addresses one wire format regression introduced in Akka.NET v1.4.20. - -**Bug Fixes and Improvements** -* [Akka.Remote / Akka.Persistence: PrimitiveSerializers manifest backwards compatibility problem](https://github.com/akkadotnet/akka.net/issues/5279) - this could cause regressions when upgrading to Akka.NET v1.4.20 and later. We have resolved this issue in Akka.NET v1.4.26. [Please see our Akka.NET v1.4.26 upgrade advisory for details](https://getakka.net/community/whats-new/akkadotnet-v1.4-upgrade-advisories.html#upgrading-to-akkanet-v1426-from-older-versions). -* [Akka.DistributedData.LightningDb: Revert #5180, switching back to original LightningDB packages](https://github.com/akkadotnet/akka.net/pull/5286) - -You can [see the full set of changes introduced in Akka.NET v1.4.26 here](https://github.com/akkadotnet/akka.net/milestone/57?closed=1) - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 4 | 99 | 96 | Gregorius Soedharmo | -| 3 | 79 | 5 | Aaron Stannard | -| 1 | 1 | 1 | dependabot[bot] | - -#### 1.4.25 September 08 2021 #### -**Maintenance Release for Akka.NET 1.4** -Akka.NET v1.4.25 includes some _significant_ performance improvements for Akka.Remote and a number of important bug fixes and improvements. - -**Bug Fixes and Improvements** -* [Akka.IO.Tcp: connecting to an unreachable DnsEndpoint never times out](https://github.com/akkadotnet/akka.net/issues/5154) -* [Akka.Actor: need to enforce `stdout-loglevel = off` all the way through ActorSystem lifecycle](https://github.com/akkadotnet/akka.net/issues/5246) -* [Akka.Actor: `Ask` should push unhandled answers into deadletter](https://github.com/akkadotnet/akka.net/pull/5259) -* [Akka.Routing: Make Router.Route` virtual](https://github.com/akkadotnet/akka.net/pull/5238) -* [Akka.Actor: Improve performance on `IActorRef.Child` API](https://github.com/akkadotnet/akka.net/pull/5242) - _signficantly_ improves performance of many Akka.NET functions, but includes a public API change on `IActorRef` that is source compatible but not necessarily binary-compatible. `IActorRef GetChild(System.Collections.Generic.IEnumerable name)` is now `IActorRef GetChild(System.Collections.Generic.IReadOnlyList name)`. This API is almost never called directly by user code (it's almost always called via the internals of the `ActorSystem` when resolving `ActorSelection`s or remote messages) so this change should be safe. -* [Akka.Actor: `IsNobody` throws NRE](https://github.com/akkadotnet/akka.net/issues/5213) -* [Akka.Cluster.Tools: singleton fix cleanup of overdue _removed members](https://github.com/akkadotnet/akka.net/pull/5229) -* [Akka.DistributedData: ddata exclude `Exiting` members in Read/Write `MajorityPlus`](https://github.com/akkadotnet/akka.net/pull/5227) - -**Performance Improvements** -Using our standard `RemotePingPong` benchmark, the difference between v1.4.24 and v1.4.24 is significant: - -_v1.4.24_ - -``` -OSVersion: Microsoft Windows NT 6.2.9200.0 -ProcessorCount: 16 -ClockSpeed: 0 MHZ -Actor Count: 32 -Messages sent/received per client: 200000 (2e5) -Is Server GC: True -Thread count: 111 - -Num clients, Total [msg], Msgs/sec, Total [ms] - 1, 200000, 96994, 2062.08 - 5, 1000000, 194818, 5133.93 - 10, 2000000, 198966, 10052.93 - 15, 3000000, 199455, 15041.56 - 20, 4000000, 198177, 20184.53 - 25, 5000000, 197613, 25302.80 - 30, 6000000, 197349, 30403.82 -``` - -_v1.4.25_ - -``` -OSVersion: Microsoft Windows NT 6.2.9200.0 -ProcessorCount: 16 -ClockSpeed: 0 MHZ -Actor Count: 32 -Messages sent/received per client: 200000 (2e5) -Is Server GC: True -Thread count: 111 - -Num clients, Total [msg], Msgs/sec, Total [ms] - 1, 200000, 130634, 1531.54 - 5, 1000000, 246975, 4049.20 - 10, 2000000, 244499, 8180.16 - 15, 3000000, 244978, 12246.39 - 20, 4000000, 245159, 16316.37 - 25, 5000000, 243333, 20548.09 - 30, 6000000, 241644, 24830.55 -``` - -This represents a 24% overall throughput improvement in Akka.Remote across the board. We have additional PRs staged that should get aggregate performance improvements above 40% for Akka.Remote over v1.4.24 but they didn't make it into the Akka.NET v1.4.25 release. - -You can [see the full set of changes introduced in Akka.NET v1.4.25 here](https://github.com/akkadotnet/akka.net/milestone/56?closed=1) - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 32 | 1301 | 400 | Aaron Stannard | -| 4 | 358 | 184 | Andreas Dirnberger | -| 3 | 414 | 149 | Gregorius Soedharmo | -| 3 | 3 | 3 | dependabot[bot] | -| 2 | 43 | 10 | zbynek001 | -| 1 | 14 | 13 | tometchy | -| 1 | 139 | 3 | carlcamilleri | - -#### 1.4.24 August 17 2021 #### -**Maintenance Release for Akka.NET 1.4** - -**Bug Fixes and Improvements** - -* [Akka: Make `Router` open to extensions](https://github.com/akkadotnet/akka.net/pull/5201) -* [Akka: Allow null response to `Ask`](https://github.com/akkadotnet/akka.net/pull/5205) -* [Akka.Cluster: Fix cluster startup race condition](https://github.com/akkadotnet/akka.net/pull/5185) -* [Akka.Streams: Fix RestartFlow bug](https://github.com/akkadotnet/akka.net/pull/5181) -* [Akka.Persistence.Sql: Implement TimestampProvider in BatchingSqlJournal](https://github.com/akkadotnet/akka.net/pull/5192) -* [Akka.Serialization.Hyperion: Bump Hyperion version from 0.11.0 to 0.11.1](https://github.com/akkadotnet/akka.net/pull/5206) -* [Akka.Serialization.Hyperion: Add Hyperion unsafe type filtering security feature](https://github.com/akkadotnet/akka.net/pull/5208) - -You can [see the full set of changes introduced in Akka.NET v1.4.24 here](https://github.com/akkadotnet/akka.net/milestone/55?closed=1) - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 5 | 360 | 200 | Aaron Stannard | -| 3 | 4 | 4 | dependabot[bot] | -| 1 | 548 | 333 | Arjen Smits | -| 1 | 42 | 19 | Martijn Schoemaker | -| 1 | 26 | 27 | Andreas Dirnberger | -| 1 | 171 | 27 | Gregorius Soedharmo | - -#### 1.4.23 August 09 2021 #### -**Maintenance Release for Akka.NET 1.4** - -Akka.NET v1.4.23 is designed to patch an issue that occurs on Linux machines using Akka.Cluster.Sharding with `akka.cluster.sharding.state-store-mode=ddata` and `akka.cluster.sharding.remember-entities=on`: "[System.DllNotFoundException: Unable to load shared library 'lmdb' or one of its dependencies](https://github.com/akkadotnet/akka.net/issues/5174)" - -In [Akka.NET v1.4.21 we added built-in support for Akka.DistributedData.LightningDb](https://github.com/akkadotnet/akka.net/releases/tag/1.4.21) for use with the `remember-entities` setting, but we never received any reports about this issue until shortly after v1.4.22 was released. Fundamentally, the problem was that our downstream dependency, Lightning.NET, doesn't include any of the necessary Linux native binaries in their distributions currently. So in the meantime, we've published our own "vendored" distribution of Lightning.NET to NuGet until a new official one is released that includes these binaries. - -There are some other small [fixes included in Akka.NET v1.4.23 and you can read about them here](https://github.com/akkadotnet/akka.net/milestone/54). - -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 8 | 136 | 2803 | Aaron Stannard | -| 2 | 61 | 3 | Gregorius Soedharmo | - -#### 1.4.22 August 05 2021 #### -**Maintenance Release for Akka.NET 1.4** - -Akka.NET v1.4.22 is a fairly large release that includes an assortment of performance and bug fixes. - -**Performance Fixes** -Akka.NET v1.4.22 includes a _significant_ performance improvement for `Ask`, which now requires 1 internal `await` operation instead of 3: - -*Before* - -| Method | Iterations | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | -|------------------------------ |----------- |----------:|----------:|----------:|----------:|------:|------:|----------:| -| RequestResponseActorSelection | 10000 | 83.313 ms | 0.7553 ms | 0.7065 ms | 4666.6667 | - | - | 19 MB | -| CreateActorSelection | 10000 | 5.572 ms | 0.1066 ms | 0.1140 ms | 953.1250 | - | - | 4 MB | - - -*After* - -| Method | Iterations | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | -|------------------------------ |----------- |----------:|----------:|----------:|----------:|------:|------:|----------:| -| RequestResponseActorSelection | 10000 | 71.216 ms | 0.9885 ms | 0.9246 ms | 4285.7143 | - | - | 17 MB | -| CreateActorSelection | 10000 | 5.462 ms | 0.0495 ms | 0.0439 ms | 953.1250 | - | - | 4 MB | - -**Bug Fixes and Improvements** - -* [Akka: Use ranged nuget versioning for Newtonsoft.Json](https://github.com/akkadotnet/akka.net/pull/5099) -* [Akka: Pipe of Canceled Tasks](https://github.com/akkadotnet/akka.net/pull/5123) -* [Akka: CircuitBreaker's Open state should return a faulted Task instead of throwing](https://github.com/akkadotnet/akka.net/issues/5117) -* [Akka.Remote: Can DotNetty socket exception include information about the address?](https://github.com/akkadotnet/akka.net/issues/5130) -* [Akka.Remote: log full exception upon deserialization failure](https://github.com/akkadotnet/akka.net/pull/5121) -* [Akka.Cluster: SBR fix & update](https://github.com/akkadotnet/akka.net/pull/5147) -* [Akka.Streams: Restart Source|Flow|Sink: Configurable stream restart deadline](https://github.com/akkadotnet/akka.net/pull/5122) -* [Akka.DistributedData: ddata replicator stops but doesn't look like it can be restarted easily](https://github.com/akkadotnet/akka.net/pull/5145) -* [Akka.DistributedData: ddata ReadMajorityPlus and WriteMajorityPlus](https://github.com/akkadotnet/akka.net/pull/5146) -* [Akka.DistributedData: DData Max-Delta-Elements may not be fully honoured](https://github.com/akkadotnet/akka.net/issues/5157) - -You can [see the full set of changes introduced in Akka.NET v1.4.22 here](https://github.com/akkadotnet/akka.net/milestone/52) - -**Akka.Cluster.Sharding.RepairTool** -In addition to the work done on Akka.NET itself, we've also created a separate tool for cleaning up any left-over data in the event of an Akka.Cluster.Sharding cluster running with `akka.cluster.sharding.state-store-mode=persistence` was terminated abruptly before it had a chance to cleanup. - -We've added documentation to the Akka.NET website that explains how to use this tool here: https://getakka.net/articles/clustering/cluster-sharding.html#cleaning-up-akkapersistence-shard-state - -And the tool itself has documentation here: https://github.com/petabridge/Akka.Cluster.Sharding.RepairTool +| 7 | 435 | 19 | Gregorius Soedharmo | +| 2 | 26 | 23 | Mark Dinh | +| 1 | 49 | 136 | Simon Cropp | +| 1 | 4 | 0 | Aaron Stannard | -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 16 | 1254 | 160 | Gregorius Soedharmo | -| 7 | 104 | 83 | Aaron Stannard | -| 5 | 8 | 8 | dependabot[bot] | -| 4 | 876 | 302 | Ismael Hamed | -| 2 | 3942 | 716 | zbynek001 | -| 2 | 17 | 3 | Andreas Dirnberger | -| 1 | 187 | 2 | andyfurnival | -| 1 | 110 | 5 | Igor Fedchenko | +To [see the full set of changes in Akka.NET v1.5.43, click here](https://github.com/akkadotnet/akka.net/milestone/126?closed=1). diff --git a/check-multinode-migration.sh b/check-multinode-migration.sh new file mode 100755 index 00000000000..f9b9866982e --- /dev/null +++ b/check-multinode-migration.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +# Multi-Node Test Async Migration Status Checker +# This script helps identify which multi-node tests still need async migration + +echo "=========================================" +echo "Multi-Node Test Async Migration Status" +echo "=========================================" +echo "" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to check a directory +check_directory() { + local dir=$1 + local name=$2 + + echo -e "${YELLOW}Checking $name:${NC}" + echo "----------------------------------------" + + # Find files with blocking TestConductor calls + local blocking_files=$(find "$dir" -name "*.cs" -exec grep -l "TestConductor.*\.Wait()" {} \; 2>/dev/null | sort) + + if [ -z "$blocking_files" ]; then + echo -e "${GREEN}✓ No TestConductor blocking calls found${NC}" + else + echo -e "${RED}✗ Files with TestConductor.*.Wait() calls:${NC}" + for file in $blocking_files; do + basename_file=$(basename "$file") + count=$(grep -c "\.Wait()" "$file") + echo " - $basename_file ($count .Wait() calls)" + done + fi + + # Check for EnterBarrier (non-async) + local barrier_count=$(find "$dir" -name "*.cs" -exec grep -l "EnterBarrier(" {} \; 2>/dev/null | wc -l) + if [ "$barrier_count" -gt 0 ]; then + echo -e "${YELLOW}⚠ $barrier_count files still use EnterBarrier (should be EnterBarrierAsync)${NC}" + fi + + # Check for Within (non-async) + local within_count=$(find "$dir" -name "*.cs" -exec grep -l "Within(" {} \; 2>/dev/null | wc -l) + if [ "$within_count" -gt 0 ]; then + echo -e "${YELLOW}⚠ $within_count files use Within (may need WithinAsync)${NC}" + fi + + echo "" +} + +# Check core tests +check_directory "src/core/Akka.Cluster.Tests.MultiNode" "Akka.Cluster.Tests.MultiNode" +check_directory "src/core/Akka.Remote.Tests.MultiNode" "Akka.Remote.Tests.MultiNode" + +# Check contrib tests +if [ -d "src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode" ]; then + check_directory "src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode" "Akka.Cluster.Sharding.Tests.MultiNode" +fi + +if [ -d "src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode" ]; then + check_directory "src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode" "Akka.Cluster.Tools.Tests.MultiNode" +fi + +if [ -d "src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode" ]; then + check_directory "src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode" "Akka.Cluster.Metrics.Tests.MultiNode" +fi + +if [ -d "src/contrib/cluster/Akka.DistributedData.Tests.MultiNode" ]; then + check_directory "src/contrib/cluster/Akka.DistributedData.Tests.MultiNode" "Akka.DistributedData.Tests.MultiNode" +fi + +echo "=========================================" +echo "Summary" +echo "=========================================" + +# Count total blocking files +total_blocking=$(find src -name "*.cs" -path "*Tests.MultiNode*" -exec grep -l "TestConductor.*\.Wait()" {} \; 2>/dev/null | wc -l) +total_files=$(find src -name "*.cs" -path "*Tests.MultiNode*" 2>/dev/null | wc -l) + +echo "Total multi-node test files: $total_files" +echo -e "${RED}Files with blocking TestConductor calls: $total_blocking${NC}" + +if [ "$total_blocking" -eq 0 ]; then + echo -e "${GREEN}🎉 All TestConductor blocking calls have been migrated!${NC}" +else + echo -e "${YELLOW}⚠ Migration still needed for $total_blocking files${NC}" +fi + +echo "" +echo "Run this script periodically to track migration progress." +echo "See MULTINODE_TEST_ASYNC_MIGRATION.md for migration guide." \ No newline at end of file diff --git a/docs/articles/clustering/cluster-sharding.md b/docs/articles/clustering/cluster-sharding.md index 57a98859b26..b4368bce915 100644 --- a/docs/articles/clustering/cluster-sharding.md +++ b/docs/articles/clustering/cluster-sharding.md @@ -324,15 +324,10 @@ You can inspect current sharding stats by using following messages: It's possible to query a `ShardRegion` or a `ShardRegionProxy` using a `GetEntityLocation` query: -[!code-csharp[ShardedDaemonProcessSpec.cs](../../../src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardRegionQueriesSpecs.cs?name=GetEntityLocationQuery)] +[!code-csharp[ShardRegionQueriesSpecs.cs](../../../src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardRegionQueriesSpecs.cs?name=GetEntityLocationQuery)] A `GetEntityLocation` query will always return an `EntityLocation` response - even if the query could not be executed. -> [!IMPORTANT] -> One major caveat is that in order for the `GetEntityLocation` to execute your `IMessageExtractor` or `ShardExtractor` delegate will need to support the `ShardRegion.StartEntity` message - just like you'd have to use in order to support `remember-entities=on`: - -[!code-csharp[ShardedDaemonProcessSpec.cs](../../../src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardRegionQueriesSpecs.cs?name=GetEntityLocationExtractor)] - ## Integrating Cluster Sharding with Persistent Actors One of the most common scenarios, where cluster sharding is used, is to combine them with event-sourced persistent actors from [Akka.Persistence](xref:persistence-architecture) module. diff --git a/docs/articles/clustering/split-brain-resolver.md b/docs/articles/clustering/split-brain-resolver.md index 5a8166297a6..e29a3634510 100644 --- a/docs/articles/clustering/split-brain-resolver.md +++ b/docs/articles/clustering/split-brain-resolver.md @@ -6,6 +6,10 @@ title: Split Brain Resolver When working with an Akka.NET cluster, you must consider how to handle [network partitions](https://en.wikipedia.org/wiki/Network_partition) (a.k.a. split brain scenarios) and machine crashes (including .NET CLR/Core and hardware failures). This is crucial for correct behavior of your cluster, especially if you use Cluster Singleton or Cluster Sharding. + + + + ## The Problem One of the common problems present in distributed systems are potential hardware failures. Things like garbage collection pauses, machine crashes or network partitions happen all the time. Moreover it is impossible to distinguish between them. Different cause can have different result on our cluster. A careful balance here is highly desired: diff --git a/docs/articles/debugging/akka-analyzers.md b/docs/articles/debugging/akka-analyzers.md index d88bc01444a..cd88f67dd47 100644 --- a/docs/articles/debugging/akka-analyzers.md +++ b/docs/articles/debugging/akka-analyzers.md @@ -24,6 +24,9 @@ Akka.Analyzer is a [Roslyn Analysis and Code Fix](https://learn.microsoft.com/en | [AK2000](xref:AK2000) | Do not use `Ask` with `TimeSpan.Zero` for timeout. | Error | API Usage | | [AK2001](xref:AK2001) | Do not use automatically handled messages in inside `Akka.Cluster.Sharding.IMessageExtractor`s. | Warning | API Usage | | [AK2002](xref:AK2002) | `Context.Materializer()` should not be invoked multiple times, use a cached value instead. | Warning | API Usage | +| [AK2003](xref:AK2003) | `ReceiveActor.Receive` message handler must not be a void async delegate. | Error | API Usage | +| [AK2004](xref:AK2004) | `IDslActor.Receive` message handler delegate must not be a void async delegate. Use `ReceiveAsync` instead. | Error | API Usage | +| [AK2005](xref:AK2005) | `ReceivePersistentActor.Command` message handler delegate must not be a void async delegate. Use `ReceiveAsync` instead. | Error | API Usage | ## Deprecated Rules diff --git a/docs/articles/debugging/rules/AK2003.md b/docs/articles/debugging/rules/AK2003.md new file mode 100644 index 00000000000..05b2a7d1f98 --- /dev/null +++ b/docs/articles/debugging/rules/AK2003.md @@ -0,0 +1,51 @@ +--- +uid: AK2003 +title: Akka.Analyzers Rule AK2003 - "`ReceiveActor.Receive` message handler must not be a void async delegate." +--- + +# AK2003 - Error + +`ReceiveActor.Receive` message handler delegate must not be a void async delegate. Use `ReceiveAsync` instead. + +## Cause + +`ReceiveActor.Receive` accepts an `Action` as a delegate, any `void async` delegate passed as an argument will be invoked as a detached asynchronous function that can cause erroneous message processing behavior. + +An example: + +```csharp +using Akka.Actor; +using System.Threading.Tasks; + +public sealed class MyActor : ReceiveActor +{ + public MyActor() + { + Receive(async msg => + { + await Task.Yield(); + Sender.Tell(msg); + }); + } +} +``` + +## Resolution + +```csharp +using Akka.Actor; +using System.Threading.Tasks; + +public sealed class MyActor : ReceiveActor +{ + public MyActor() + { + // Use ReceiveAsync() if you're passing an asynchronous delegate + ReceiveAsync(async msg => + { + await Task.Yield(); + Sender.Tell(msg); + }); + } +} +``` diff --git a/docs/articles/debugging/rules/AK2004.md b/docs/articles/debugging/rules/AK2004.md new file mode 100644 index 00000000000..99fc1848b8c --- /dev/null +++ b/docs/articles/debugging/rules/AK2004.md @@ -0,0 +1,57 @@ +--- +uid: AK2004 +title: Akka.Analyzers Rule AK2004 - "`IDslActor.Receive` message handler must not be a void async delegate." +--- + +# AK2004 - Error + +`IDslActor.Receive` message handler delegate must not be a void async delegate. Use `ReceiveAsync` instead. + +## Cause + +`IDslActor.Receive` accepts an `Action` as a delegate, any `void async` delegate passed as an argument will be invoked as a detached asynchronous function that can cause erroneous message processing behavior. + +An example: + +```csharp +using Akka.Actor; +using System.Threading.Tasks; + +public sealed class MyClass +{ + public MyClass(ActorSystem sys) + { + sys.ActorOf(act => + { + act.Receive(async (msg, context) => + { + await Task.Yield(); + context.Sender.Tell(msg); + }); + }, "dslActor"); + } +} +``` + +## Resolution + +```csharp +using Akka.Actor; +using System.Threading.Tasks; + +public sealed class MyClass +{ + public MyClass(ActorSystem sys) + { + sys.ActorOf(act => + { + // Use ReceiveAsync() if you're passing an asynchronous delegate + act.ReceiveAsync(async (msg, context) => + { + await Task.Yield(); + context.Sender.Tell(msg); + }); + }, "dslActor"); + } +} +``` diff --git a/docs/articles/debugging/rules/AK2005.md b/docs/articles/debugging/rules/AK2005.md new file mode 100644 index 00000000000..10035dbdff7 --- /dev/null +++ b/docs/articles/debugging/rules/AK2005.md @@ -0,0 +1,59 @@ +--- +uid: AK2005 +title: Akka.Analyzers Rule AK2005 - "`ReceivePersistentActor.Command` message handler must not be a void async delegate." +--- + +# AK2005 - Error + +`ReceivePersistentActor.Command` message handler delegate must not be a void async delegate. Use `ReceiveAsync` instead. + +## Cause + +`ReceivePersistentActor.Command` accepts an `Action` as a delegate, any `void async` delegate passed as an argument will be invoked as a detached asynchronous function that can cause erroneous message processing behavior. + +An example: + +```csharp +using Akka.Actor; +using Akka.Persistence; +using System.Threading.Tasks; + +public class MyActor: ReceivePersistentActor +{ + public MyActor(string persistenceId) + { + PersistenceId = persistenceId; + + Command(async msg => + { + await Task.Yield(); + }); + } + + public override string PersistenceId { get; } +} +``` + +## Resolution + +```csharp +using Akka.Actor; +using Akka.Persistence; +using System.Threading.Tasks; + +public class MyActor: ReceivePersistentActor +{ + public MyActor(string persistenceId) + { + PersistenceId = persistenceId; + + // Use CommandAsync() if you're passing an asynchronous delegate + CommandAsync(async msg => + { + await Task.Yield(); + }); + } + + public override string PersistenceId { get; } +} +``` diff --git a/docs/articles/debugging/rules/toc.yml b/docs/articles/debugging/rules/toc.yml index a7d4d97e48a..573939bc009 100644 --- a/docs/articles/debugging/rules/toc.yml +++ b/docs/articles/debugging/rules/toc.yml @@ -21,4 +21,10 @@ - name: AK2001 href: AK2001.md - name: AK2002 - href: AK2002.md \ No newline at end of file + href: AK2002.md +- name: AK2003 + href: AK2003.md +- name: AK2004 + href: AK2004.md +- name: AK2005 + href: AK2005.md \ No newline at end of file diff --git a/docs/articles/discovery/akka-management.md b/docs/articles/discovery/akka-management.md index fed636cf7c5..2b7b565ec83 100644 --- a/docs/articles/discovery/akka-management.md +++ b/docs/articles/discovery/akka-management.md @@ -96,6 +96,32 @@ akkaBuilder.WithAzureDiscovery(options => }); ``` +## Running Clustered Nodes on a Single Machine + +When developing or testing, you may need to run multiple Akka.NET nodes on the same host. Manually assigning distinct remote port for each process can work for a fixed seed-node strategy, but doing so for management ports would not work with `Akka.Discovery` and `Akka.Management.Cluster.Bootstrap`. + +`Cluster.Bootstrap` expects that all hosts have the same management port. When `Akka.Discovery` returns a list of open ports on a host, the bootstrap coordinator will filter out any ports that are different from the locally configured management port, ignoring the rest. + +To allow each host to advertise its own management port without filtering each other out, disable fallback-port filtering: + +```csharp +builder.WithClusterBootstrap(setup => +{ + setup.ContactPoint.FilterOnFallbackPort = false; +}); +``` + +```hocon +akka.management { + cluster.bootstrap { + contact-point { + # allow discovery to return multiple ports per host + filter-on-fallback-port = false + } + } +} +``` + ## Further Reading * [Akka.Discovery Overview](index.md) diff --git a/docs/community/contributing/build-process.md b/docs/community/contributing/build-process.md index 2270c1e5328..1b030ac220a 100644 --- a/docs/community/contributing/build-process.md +++ b/docs/community/contributing/build-process.md @@ -5,101 +5,196 @@ title: Building Akka.NET Repositories # 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. +Akka.NET has migrated from using FAKE build scripts to using the .NET CLI for building and testing. This approach provides better integration with modern .NET tooling and improved performance. -> [!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. +> [!NOTE] +> We have dropped the F# FAKE build script (`build.fsx`) in favor of using the .NET CLI directly. The build system now uses standard .NET commands and tools. -## Supported Commands +## Prerequisites -This project supports a wide variety of commands. +* .NET SDK (as specified in `global.json`) +* PowerShell -To list on Windows: +## Building the Solution -```console -build.cmd help +### Basic Build Commands + +To build the entire solution: + +```bash +# Build in Debug mode (default) +dotnet build + +# Build in Release mode +dotnet build -c Release + +# Build with warnings as errors +dotnet build -warnaserror +``` + +For more information, see the [dotnet build documentation](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-build). + +### Building Specific Projects + +```bash +# Build a specific project +dotnet build src/core/Akka/Akka.csproj + +# Build with specific configuration +dotnet build src/core/Akka/Akka.csproj -c Release +``` + +## Running Tests + +### All Tests + +```bash +# Run all tests in Release mode +dotnet test -c Release + +# Run tests with specific framework +dotnet test -c Release --framework net8.0 +dotnet test -c Release --framework net48 +``` + +For more information, see the [dotnet test documentation](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-test). + +### Specific Test Projects + +```bash +# Run tests for a specific project +dotnet test src/core/Akka.Tests/Akka.Tests.csproj -c Release + +# Run tests with filtering +dotnet test -c Release --filter DisplayName="TestName" +dotnet test -c Release --filter "FullyQualifiedName~TestClass" ``` -To list on Linux / OS X: +### Multi-Node Tests + +```bash +# Run multi-node tests +dotnet test -c Release --framework net8.0 --filter "Category=MultiNodeTest" -```console -build.sh help +# Run specific multi-node test class +dotnet test -c Release --filter "FullyQualifiedName~ClusterSpec" ``` -However, please see this readme for full details. +### Performance Tests -### Summary +```bash +# Run performance tests (NBench) +dotnet test -c Release --filter "Category=Performance" +``` -* `build.[cmd|sh] all` - runs the entire build system minus documentation: `NBench`, `Tests`, and `Nuget`. -* `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](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. -* `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. +## Incremental Builds with Incrementalist -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. +Akka.NET is a large project, so it's often necessary to run tests incrementally to reduce build time during development. The project uses [Incrementalist](https://github.com/petabridge/Incrementalist) for optimized builds. -### Incremental Builds +### Using Incrementalist -Akka.NET is a large project, so it's often necessary to run tests incrementally in order to reduce the total end-to-end build time during development. In Akka.NET this is accomplished using [the Incrementalist project](https://github.com/petabridge/Incrementalist) - which can be invoked by adding the `incremental` option to any `build.sh` or `build.cmd` command: +```bash +# Run only tests for changed projects +dotnet incrementalist run --config .incrementalist/testsOnly.json -- test -c Release --no-build --framework net8.0 -```console -PS> build.cmd MultiNodeTestsNetCore spec={className} incremental +# Run multi-node tests incrementally +dotnet incrementalist run --config .incrementalist/mutliNodeOnly.json -- test -c Release --no-build --framework net8.0 + +# Run all tests incrementally +dotnet incrementalist run --config .incrementalist/incrementalist.json -- test -c Release --no-build ``` -This option will work locally on Linux or Windows. +### Incrementalist Configuration + +The project includes several Incrementalist configuration files: -### Release Notes, Version Numbers, Etc +* `.incrementalist/incrementalist.json` - General incremental build configuration +* `.incrementalist/testsOnly.json` - Configuration for running only tests +* `.incrementalist/mutliNodeOnly.json` - Configuration for multi-node tests only -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). +For more information about Incrementalist, visit the [GitHub repository](https://github.com/petabridge/Incrementalist). -#### RELEASE_NOTES.md +## Creating NuGet Packages -```text -#### 0.1.0 October 05 2019 #### -First release +```bash +# Create packages in Release mode +dotnet pack -c Release + +# Create packages with version suffix (for nightly builds) +dotnet pack -c Release -p:VersionSuffix=beta$(Get-Date -Format "yyyyMMddHHmmss") + +# Create packages for specific project +dotnet pack src/core/Akka/Akka.csproj -c Release ``` -In this instance, the NuGet and assembly version will be `0.1.0` based on what's available at the top of the `RELEASE_NOTES.md` file. +For more information, see the [dotnet pack documentation](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-pack). + +## Documentation Generation + +```bash +# Generate documentation using DocFX +dotnet docfx metadata ./docs/docfx.json --warningsAsErrors +dotnet docfx build ./docs/docfx.json --warningsAsErrors -#### RELEASE_NOTES.md +# Serve documentation locally (Windows) +./serve-docs.cmd -```text -#### 0.1.0-beta1 October 05 2019 #### -First release +# Serve documentation locally (PowerShell - works on Windows, macOS, and Linux) +./serve-docs.ps1 ``` -But in this case the NuGet and assembly version will be `0.1.0-beta1`. +For more information about DocFX, see the [DocFX documentation](https://dotnet.github.io/docfx/). -If you add any new projects to the solution created with this template, be sure to add the following line to each one of them in order to ensure that you can take advantage of `common.props` for standardization purposes: +## Release Notes and Version Management -```xml - +The project uses a PowerShell script (`build.ps1`) to handle release notes and version updates: + +```powershell +# Update release notes and version information +./build.ps1 ``` -### Conventions +This script: + +* Reads release notes from `RELEASE_NOTES.md` +* Updates version information in `Directory.Build.props` +* Prepares the project for building with the correct version + +> [!NOTE] +> PowerShell is now available on Linux and macOS, so the `build.ps1` script can be run on all supported platforms. + +## CI/CD Integration + +The project uses Azure DevOps for continuous integration. The build pipelines use the same .NET CLI commands shown above, ensuring consistency between local development and CI/CD environments. -The attached build script will automatically do the following based on the conventions of the project names added to this project: +### Common CI Commands -* Any project name ending with `.Tests` will automatically be treated as a [XUnit2](https://xunit.github.io/) project and will be included during the test stages of this build script; -* 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. +```bash +# Restore tools +dotnet tool restore + +# Build solution +dotnet build -c Release + +# Run tests with results output +dotnet test -c Release --framework net8.0 --logger:trx --results-directory TestResults + +# Create packages +dotnet pack -c Release -o $(Build.ArtifactStagingDirectory)/nuget +``` -## Triggering Builds and Updates on Akka.NET Github Repositories +For more information about .NET tools, see the [dotnet tool documentation](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-tool). -## Routine Updates and Pull Requests +## Troubleshooting -Akka.NET uses Azure DevOps to run its builds and the conventions it uses are rather sample: +If you were previously using the FAKE build scripts, here are the equivalent .NET CLI commands: -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](xref:nightly-builds); and -4. Always `squash` any merges into the `dev` branch in order to preserve a clean commit history. +| FAKE Command | .NET CLI Equivalent | +|--------------|---------------------| +| `build.cmd buildrelease` | `dotnet build -c Release` | +| `build.cmd runtests` | `dotnet test -c Release --framework net48` | +| `build.cmd runtestsnetcore` | `dotnet test -c Release --framework net8.0` | +| `build.cmd nuget` | `dotnet pack -c Release` | +| `build.cmd DocFx` | `dotnet docfx build ./docs/docfx.json` | -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. +The new approach provides better integration with modern .NET tooling and improved performance through incremental builds. diff --git a/src/benchmark/Akka.Cluster.Cpu.Benchmark/Akka.Cluster.Cpu.Benchmark.csproj b/src/benchmark/Akka.Cluster.Cpu.Benchmark/Akka.Cluster.Cpu.Benchmark.csproj index d6c649c3eaa..dcbc433468a 100644 --- a/src/benchmark/Akka.Cluster.Cpu.Benchmark/Akka.Cluster.Cpu.Benchmark.csproj +++ b/src/benchmark/Akka.Cluster.Cpu.Benchmark/Akka.Cluster.Cpu.Benchmark.csproj @@ -3,6 +3,7 @@ Exe net8.0 + false diff --git a/src/contrib/cluster/Akka.Cluster.Metrics.Tests/Base/AkkaSpecWithCollector.cs b/src/contrib/cluster/Akka.Cluster.Metrics.Tests/Base/AkkaSpecWithCollector.cs index 16fbf1b2a15..ccf91f04c86 100644 --- a/src/contrib/cluster/Akka.Cluster.Metrics.Tests/Base/AkkaSpecWithCollector.cs +++ b/src/contrib/cluster/Akka.Cluster.Metrics.Tests/Base/AkkaSpecWithCollector.cs @@ -5,9 +5,17 @@ // //----------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Akka.Actor; using Akka.Cluster.Metrics.Collectors; +using Akka.Cluster.Metrics.Serialization; using Akka.TestKit; +using FluentAssertions.Extensions; using Xunit.Abstractions; namespace Akka.Cluster.Metrics.Tests.Base @@ -25,7 +33,72 @@ public abstract class AkkaSpecWithCollector : AkkaSpec protected AkkaSpecWithCollector(string config, ITestOutputHelper output = null) : base(config, output) { - Collector = new DefaultCollector((Sys as ExtendedActorSystem).Provider.RootPath.Address); + Collector = new DefaultCollector(((ExtendedActorSystem)Sys).Provider.RootPath.Address); + } + + // We need this because metrics can be missing from samples + protected Queue CreateTestData(int count, TimeSpan timeout, string[] requiredMetrics) + { + using var cts = new CancellationTokenSource(timeout); + var queue = new Queue(); + + foreach (var _ in Enumerable.Range(0, count)) + { + queue.Enqueue(CreateTestData(requiredMetrics, cts.Token)); + } + + return queue; + } + + protected NodeMetrics CreateTestData(TimeSpan timeout, string[] requiredMetrics) + { + using var cts = new CancellationTokenSource(timeout); + return CreateTestData(requiredMetrics, cts.Token); + } + + protected NodeMetrics CreateTestData(string[] requiredMetrics, CancellationToken token) + { + NodeMetrics metrics; + do + { + token.ThrowIfCancellationRequested(); + metrics = Collector.Sample(); + } while (!HasRequiredMetrics(metrics.Metrics, requiredMetrics)); + return metrics; + } + + protected async Task CreateTestDataAsync(TimeSpan timeout, string[] requiredMetrics) + { + using var cts = new CancellationTokenSource(timeout); + NodeMetrics metrics; + + do + { + cts.Token.ThrowIfCancellationRequested(); + metrics = Collector.Sample(); + + if (HasRequiredMetrics(metrics.Metrics, requiredMetrics)) + { + return metrics; + } + + // Small delay between attempts to avoid tight loop + await Task.Delay(100, cts.Token); + + } while (!cts.Token.IsCancellationRequested); + + throw new OperationCanceledException($"Could not collect required metrics {string.Join(", ", requiredMetrics)} within {timeout}"); + } + + private static bool HasRequiredMetrics(ImmutableHashSet metrics, string[] requiredMetrics) + { + foreach (var requiredMetric in requiredMetrics) + { + if (metrics.All(m => m.Name != requiredMetric)) + return false; + } + + return true; } } } diff --git a/src/contrib/cluster/Akka.Cluster.Metrics.Tests/MetricSpec.cs b/src/contrib/cluster/Akka.Cluster.Metrics.Tests/MetricSpec.cs index 3c0dca9b26f..36ddb86d7c6 100644 --- a/src/contrib/cluster/Akka.Cluster.Metrics.Tests/MetricSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Metrics.Tests/MetricSpec.cs @@ -13,11 +13,10 @@ using Akka.Cluster.Metrics.Serialization; using Akka.Cluster.Metrics.Tests.Base; using Akka.Cluster.Metrics.Tests.Helpers; -using Akka.TestKit; using Akka.Util; -using Akka.Util.Extensions; using Akka.Util.Internal; using FluentAssertions; +using FluentAssertions.Extensions; using Xunit; using Address = Akka.Actor.Address; @@ -303,20 +302,39 @@ public class MetricValuesSpec : AkkaSpecWithCollector public MetricValuesSpec() : base(ClusterMetricsTestConfig.DefaultEnabled) { - _node1 = new NodeMetrics(new Address("akka", "sys", "a", 2554), 1, Collector.Sample().Metrics); - _node2 = new NodeMetrics(new Address("akka", "sys", "a", 2555), 1, Collector.Sample().Metrics); - _nodes = Enumerable.Range(1, 100).Aggregate(ImmutableList.Create(_node1, _node2), (nodes, _) => + Queue testData; + try { - return nodes.Select(n => + testData = CreateTestData(202, 10.Seconds(), [ + StandardMetrics.MemoryUsed, + StandardMetrics.MemoryAvailable, + StandardMetrics.Processors, + StandardMetrics.CpuProcessUsage, + StandardMetrics.CpuTotalUsage ]); + } + catch (Exception e) + { + throw new Exception("Failed to initialize test data", e); + } + + _node1 = new NodeMetrics(new Address("akka", "sys", "a", 2554), 1, testData.Dequeue().Metrics); + _node2 = new NodeMetrics(new Address("akka", "sys", "a", 2555), 1, testData.Dequeue().Metrics); + _nodes = Enumerable.Range(1, 100).Aggregate(ImmutableList.Create(_node1, _node2), + (nodes, _) => { - return new NodeMetrics(n.Address, n.Timestamp, metrics: Collector.Sample().Metrics.SelectMany(latest => + return nodes.Select(n => { - return n.Metrics.Where(latest.SameAs).Select(streaming => streaming + latest); - })); - }).ToImmutableList(); - }); + return new NodeMetrics(n.Address, n.Timestamp, + metrics: testData.Dequeue().Metrics.SelectMany(latest => + { + return n.Metrics.Where(latest.SameAs) + .Select(streaming => streaming + latest); + })); + }).ToImmutableList(); + }); } + [Fact] public void NodeMetrics_MetricValues_should_extract_expected_metrics_for_load_balancing() { diff --git a/src/contrib/cluster/Akka.Cluster.Metrics.Tests/MetricsCollectorSpec.cs b/src/contrib/cluster/Akka.Cluster.Metrics.Tests/MetricsCollectorSpec.cs index 832e6dddee1..374432b5c27 100644 --- a/src/contrib/cluster/Akka.Cluster.Metrics.Tests/MetricsCollectorSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Metrics.Tests/MetricsCollectorSpec.cs @@ -7,16 +7,13 @@ using System; using System.Linq; -using System.Threading; using System.Threading.Tasks; +using Akka.Cluster.Metrics.Serialization; using Akka.Cluster.Metrics.Tests.Base; using Akka.Cluster.Metrics.Tests.Helpers; -using Akka.TestKit; using Akka.TestKit.Xunit2.Attributes; -using Akka.Util.Extensions; -using Akka.Util.Internal; using FluentAssertions; -using Google.Protobuf.WellKnownTypes; +using FluentAssertions.Extensions; using Xunit; using Xunit.Abstractions; @@ -52,45 +49,51 @@ public void Metric_should_merge_2_metrics_that_are_tracking_the_same_metric() [Fact] public async Task MetricsCollector_should_collector_accurate_metrics_for_node() { - // await assert here in case there's no metrics available on the very first sample - await AwaitAssertAsync(() => + NodeMetrics sample; + try { - var sample = Collector.Sample(); - var metrics = sample.Metrics.Select(m => (Name: m.Name, Value: m.Value)).ToList(); - var used = metrics.First(m => m.Name == StandardMetrics.MemoryUsed); - var available = metrics.First(m => m.Name == StandardMetrics.MemoryAvailable); - metrics.ForEach(m => + sample = await CreateTestDataAsync(30.Seconds(), [ + StandardMetrics.MemoryAvailable, + StandardMetrics.MemoryUsed + ]); + } + catch (Exception e) + { + throw new Exception("Failed to initialize test data", e); + } + + var metrics = sample.Metrics.Select(m => (Name: m.Name, Value: m.Value)).ToList(); + metrics.ForEach(m => + { + switch (m.Name) { - switch (m.Name) - { - case StandardMetrics.Processors: - m.Value.DoubleValue.Should().BeGreaterOrEqualTo(0); - break; - case StandardMetrics.MemoryAvailable: - m.Value.LongValue.Should().BeGreaterThan(0); - break; - case StandardMetrics.MemoryUsed: - m.Value.LongValue.Should().BeGreaterOrEqualTo(0); - break; - case StandardMetrics.MaxMemoryRecommended: - m.Value.LongValue.Should().BeGreaterThan(0); - // Since setting is only a recommendation, we can ignore it - // See: https://stackoverflow.com/a/7729022/3094849 + case StandardMetrics.Processors: + m.Value.DoubleValue.Should().BeGreaterOrEqualTo(0); + break; + case StandardMetrics.MemoryAvailable: + m.Value.LongValue.Should().BeGreaterThan(0); + break; + case StandardMetrics.MemoryUsed: + m.Value.LongValue.Should().BeGreaterOrEqualTo(0); + break; + case StandardMetrics.MaxMemoryRecommended: + m.Value.LongValue.Should().BeGreaterThan(0); + // Since setting is only a recommendation, we can ignore it + // See: https://stackoverflow.com/a/7729022/3094849 - // used.Value.LongValue.Should().BeLessThan(m.Value.LongValue); - // available.Value.LongValue.Should().BeLessThan(m.Value.LongValue); - break; - case StandardMetrics.CpuProcessUsage: - m.Value.DoubleValue.Should().BeInRange(0, 1); - break; - case StandardMetrics.CpuTotalUsage: - m.Value.DoubleValue.Should().BeInRange(0, 1); - break; - default: - throw new ArgumentOutOfRangeException($"Unexpected metric type {m.Name}"); - } - }); - }, interval:TimeSpan.FromMilliseconds(250)); + // used.Value.LongValue.Should().BeLessThan(m.Value.LongValue); + // available.Value.LongValue.Should().BeLessThan(m.Value.LongValue); + break; + case StandardMetrics.CpuProcessUsage: + m.Value.DoubleValue.Should().BeInRange(0, 1); + break; + case StandardMetrics.CpuTotalUsage: + m.Value.DoubleValue.Should().BeInRange(0, 1); + break; + default: + throw new ArgumentOutOfRangeException($"Unexpected metric type {m.Name}"); + } + }); } [LocalFact(SkipLocal = "This performance really depends on current load - so while should work well with " + diff --git a/src/contrib/cluster/Akka.Cluster.Metrics/Collectors/DefaultCollector.cs b/src/contrib/cluster/Akka.Cluster.Metrics/Collectors/DefaultCollector.cs index 94b7d515736..990e8c2b8d7 100644 --- a/src/contrib/cluster/Akka.Cluster.Metrics/Collectors/DefaultCollector.cs +++ b/src/contrib/cluster/Akka.Cluster.Metrics/Collectors/DefaultCollector.cs @@ -70,11 +70,19 @@ public NodeMetrics Sample() if(processorCount.HasValue) metrics.Add(processorCount.Value); - if (process.MaxWorkingSet != IntPtr.Zero) + try { - var workingSet = NodeMetrics.Types.Metric.Create(StandardMetrics.MaxMemoryRecommended, process.MaxWorkingSet.ToInt64()); - if(workingSet.HasValue) - metrics.Add(workingSet.Value); + if (process.MaxWorkingSet != IntPtr.Zero) + { + var workingSet = NodeMetrics.Types.Metric.Create(StandardMetrics.MaxMemoryRecommended, process.MaxWorkingSet.ToInt64()); + if(workingSet.HasValue) + metrics.Add(workingSet.Value); + } + } + catch (Exception) + { + // MaxWorkingSet may throw on some platforms (e.g., Linux/Mono) + // Ignore and continue without this metric } var (processCpuUsage, totalCpuUsage) = GetCpuUsages(process.Id); diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/AsyncWriteProxyEx.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/AsyncWriteProxyEx.cs index c6a285b5bd3..2583296f5c0 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/AsyncWriteProxyEx.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/AsyncWriteProxyEx.cs @@ -65,13 +65,8 @@ public sealed class SetStore /// /// This exception is thrown when the specified is undefined. /// - public SetStore(IActorRef store) - { - if (store == null) - throw new ArgumentNullException(nameof(store), "SetStore requires non-null reference to store actor"); - - Store = store; - } + public SetStore(IActorRef store) => + Store = store ?? throw new ArgumentNullException(nameof(store), "SetStore requires non-null reference to store actor"); /// /// TBD @@ -82,8 +77,10 @@ public SetStore(IActorRef store) /// /// A journal that delegates actual storage to a target actor. For testing only. /// - public abstract class AsyncWriteProxyEx : AsyncWriteJournal, IWithUnboundedStash + public abstract class AsyncWriteProxyEx : AsyncWriteJournal, IWithUnboundedStash, IWithTimers { + private const string InitTimeoutTimerKey = nameof(InitTimeoutTimerKey); + private class InitTimeout { public static readonly InitTimeout Instance = new(); @@ -114,7 +111,7 @@ protected AsyncWriteProxyEx() /// public override void AroundPreStart() { - Context.System.Scheduler.ScheduleTellOnce(Timeout, Self, InitTimeout.Instance, Self); + Timers.StartSingleTimer(InitTimeoutTimerKey, InitTimeout.Instance, Timeout, Self); base.AroundPreStart(); } @@ -259,6 +256,8 @@ private Task StoreNotInitialized() /// TBD /// public IStash Stash { get; set; } + + public ITimerScheduler Timers { get; set; } } /// diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/SnapshotStoreProxy.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/SnapshotStoreProxy.cs index 0973d5fc755..a7964da8924 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/SnapshotStoreProxy.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/SnapshotStoreProxy.cs @@ -16,8 +16,10 @@ namespace Akka.Cluster.Sharding.Tests { - public abstract class SnapshotStoreProxy : SnapshotStore, IWithUnboundedStash + public abstract class SnapshotStoreProxy : SnapshotStore, IWithUnboundedStash, IWithTimers { + private const string TimeoutTimerKey = nameof(TimeoutTimerKey); + private class InitTimeout { public static readonly InitTimeout Instance = new(); @@ -49,12 +51,14 @@ protected SnapshotStoreProxy() /// public IStash Stash { get; set; } + public ITimerScheduler Timers { get; set; } + /// /// TBD /// public override void AroundPreStart() { - Context.System.Scheduler.ScheduleTellOnce(Timeout, Self, InitTimeout.Instance, Self); + Timers.StartSingleTimer(TimeoutTimerKey, InitTimeout.Instance, Timeout, Self); base.AroundPreStart(); } diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/Bugfix7399Specs.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/Bugfix7399Specs.cs index 5c62adf13db..4d3254d1429 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/Bugfix7399Specs.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/Bugfix7399Specs.cs @@ -196,6 +196,7 @@ protected override async Task LoadAsync( SnapshotSelectionCriteria criteria, CancellationToken cancellationToken) { + await Task.Yield(); if (!Working) { throw new ApplicationException("Failed"); diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingInternalsSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingInternalsSpec.cs index cd1202d8989..5bb82fea05a 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingInternalsSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingInternalsSpec.cs @@ -20,24 +20,27 @@ namespace Akka.Cluster.Sharding.Tests { public class ClusterShardingInternalsSpec : AkkaSpec { - private Option<(string, object)> ExtractEntityId(object message) + private class MessageExtractor: IMessageExtractor { - switch (message) - { - case int i: - return (i.ToString(), message); - } - throw new NotSupportedException(); - } - - private string ExtractShardId(object message) - { - switch (message) - { - case int i: - return (i % 10).ToString(); - } - throw new NotSupportedException(); + public string EntityId(object message) + => message switch + { + int i => i.ToString(), + _ => null + }; + + public object EntityMessage(object message) + => message; + + public string ShardId(object message) + => message switch + { + int i => (i % 10).ToString(), + _ => null + }; + + public string ShardId(string entityId, object messageHint = null) + => (int.Parse(entityId) % 10).ToString(); } private static Config SpecConfig => @@ -50,7 +53,8 @@ private string ExtractShardId(object message) .WithFallback(DistributedData.DistributedData.DefaultConfig()) .WithFallback(ClusterSingleton.DefaultConfig()); - ClusterSharding clusterSharding; + private ClusterSharding clusterSharding; + private readonly MessageExtractor _messageExtractor = new(); public ClusterShardingInternalsSpec(ITestOutputHelper helper) : base(SpecConfig, helper) { @@ -67,16 +71,14 @@ public void ClusterSharding_must_start_a_region_in_proxy_mode_in_case_of_node_ro typeName: typeName, entityProps: Props.Empty, settings: settingsWithRole, - extractEntityId: ExtractEntityId, - extractShardId: ExtractShardId, + messageExtractor: _messageExtractor, allocationStrategy: ShardAllocationStrategy.LeastShardAllocationStrategy(3, 0.1), handOffStopMessage: PoisonPill.Instance); var proxy = clusterSharding.StartProxy( typeName: typeName, role: settingsWithRole.Role, - extractEntityId: ExtractEntityId, - extractShardId: ExtractShardId + messageExtractor: _messageExtractor ); region.Should().BeSameAs(proxy); diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingLeaseSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingLeaseSpec.cs index d3597c2fb4d..8377a0556ae 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingLeaseSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingLeaseSpec.cs @@ -55,11 +55,6 @@ public LeaseFailed(string message, Exception innerEx) : base(message, innerEx) { } - - protected LeaseFailed(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } private static Config SpecConfig => diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/Delivery/DurableRememberEntitiesShardingSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/Delivery/DurableRememberEntitiesShardingSpec.cs new file mode 100644 index 00000000000..19ca2fbdf35 --- /dev/null +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/Delivery/DurableRememberEntitiesShardingSpec.cs @@ -0,0 +1,140 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Text; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Actor.Dsl; +using Akka.Cluster.Sharding.Delivery; +using Akka.Configuration; +using Akka.Delivery; +using Akka.Event; +using Akka.Persistence.Delivery; +using Akka.TestKit; +using Xunit; +using Xunit.Abstractions; +using FluentAssertions; +using static Akka.Tests.Delivery.TestConsumer; + +namespace Akka.Cluster.Sharding.Tests.Delivery; + +public class DurableRememberEntitiesShardingSpec : AkkaSpec +{ + private static readonly Config Config = + """ + akka.loglevel = DEBUG + akka.actor.provider = cluster + akka.persistence.journal.plugin = "akka.persistence.journal.inmem" + akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.inmem" + akka.remote.dot-netty.tcp.port = 0 + + akka.cluster.sharding.remember-entities = on + akka.cluster.sharding.state-store-mode = ddata + # no leaks between test runs thank you + akka.cluster.sharding.distributed-data.durable.keys = [] + akka.cluster.sharding.verbose-debug-logging = on + akka.cluster.sharding.fail-on-invalid-entity-state-transition = on + akka.cluster.sharding.entity-restart-backoff = 250ms + """; + + public DurableRememberEntitiesShardingSpec(ITestOutputHelper output) : base(Config, output) + { + // TODO: add journal operations subscriptions, once that's properly supported in Akka.Persistence + } + + private int _idCount; + + private string ProducerId => $"p-{_idCount}"; + + private int NextId() + { + return _idCount++; + } + + private async Task JoinCluster() + { + var cluster = Cluster.Get(Sys); + await cluster.JoinAsync(cluster.SelfAddress); + await AwaitAssertAsync(() => Assert.True(cluster.IsUp)); + } + + [Fact] + public async Task ReliableDelivery_with_remember_entity_sharding_must_allow_consumer_to_passivate_self_using_Passivate() + { + await JoinCluster(); + NextId(); + + var consumerProbe = CreateTestProbe(); + var sharding = await ClusterSharding.Get(system: Sys).StartAsync( + typeName: $"TestConsumer-{_idCount}", + entityPropsFactory: _ => ShardingConsumerController.Create( + c => Props.Create(() => new Consumer(c, consumerProbe)), + ShardingConsumerController.Settings.Create(Sys)), settings: ClusterShardingSettings.Create(Sys), + messageExtractor: HashCodeMessageExtractor.Create(10, o => string.Empty, o => o)); + + var durableQueueProps = EventSourcedProducerQueue.Create(ProducerId, Sys); + var shardingProducerController = Sys.ActorOf( + props: ShardingProducerController.Create( + ProducerId, sharding, durableQueueProps, ShardingProducerController.Settings.Create(Sys)), + name: $"shardingProducerController-{_idCount}"); + var producerProbe = CreateTestProbe(); + shardingProducerController.Tell(new ShardingProducerController.Start(producerProbe.Ref)); + + var replyProbe = CreateTestProbe(); + var next = await producerProbe.ExpectMsgAsync>(); + next.AskNextTo( + msgWithConfirmation: new ShardingProducerController.MessageWithConfirmation(EntityId: "entity-1", Message: new Job("ping"), + ReplyTo: replyProbe.Ref)); + await replyProbe.ExpectMsgAsync(); + + consumerProbe.ExpectMsg("pong"); + var entity = consumerProbe.LastSender; + await consumerProbe.WatchAsync(entity); + + next = await producerProbe.ExpectMsgAsync>(); + next.AskNextTo( + msgWithConfirmation: new ShardingProducerController.MessageWithConfirmation(EntityId: "entity-1", Message: new Job("passivate"), + ReplyTo: replyProbe.Ref)); + await replyProbe.ExpectMsgAsync(); + + consumerProbe.ExpectMsg("passivate"); + await consumerProbe.ExpectTerminatedAsync(entity); + } + + private class Consumer : ReceiveActor + { + private readonly IActorRef _consumerController; + public Consumer(IActorRef consumerController, IActorRef consumerProbe) + { + _consumerController = consumerController; + + Receive>(delivery => + { + Sender.Tell(ConsumerController.Confirmed.Instance); + switch (delivery.Message.Payload) + { + case "stop": + Context.Stop(Self); + break; + case "ping": + consumerProbe.Tell("pong"); + break; + case "passivate": + consumerProbe.Tell("passivate"); + Context.Parent.Tell(new Passivate(PoisonPill.Instance)); + break; + } + }); + } + + protected override void PreStart() + { + _consumerController.Tell(new ConsumerController.Start(Self)); + } + } +} diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/DeprecatedLeastShardAllocationStrategySpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/DeprecatedLeastShardAllocationStrategySpec.cs index 175cd5e8911..44c705dcb84 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/DeprecatedLeastShardAllocationStrategySpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/DeprecatedLeastShardAllocationStrategySpec.cs @@ -20,7 +20,9 @@ namespace Akka.Cluster.Sharding.Tests { public class DeprecatedLeastShardAllocationStrategySpec : AkkaSpec { +#pragma warning disable CS0618 // Type or member is obsolete. This is fine, we're actually testing backward compatibility with the deprecated class internal class TestLeastShardAllocationStrategy : LeastShardAllocationStrategy +#pragma warning restore CS0618 // Type or member is obsolete { private readonly Func clusterState; private readonly Func selfMember; diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/GetShardTypeNamesSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/GetShardTypeNamesSpec.cs index 4c49f616f46..8d2d0c479be 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/GetShardTypeNamesSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/GetShardTypeNamesSpec.cs @@ -19,6 +19,29 @@ namespace Akka.Cluster.Sharding.Tests { public class GetShardTypeNamesSpec : AkkaSpec { + private class MessageExtractor: IMessageExtractor + { + public string EntityId(object message) + => message switch + { + int i => i.ToString(), + _ => null + }; + + public object EntityMessage(object message) + => message; + + public string ShardId(object message) + => message switch + { + int i => (i % 10).ToString(), + _ => null + }; + + public string ShardId(string entityId, object messageHint = null) + => (int.Parse(entityId) % 10).ToString(); + } + private static Config SpecConfig => ConfigurationFactory.ParseString(@" akka.actor.provider = cluster @@ -29,6 +52,8 @@ public class GetShardTypeNamesSpec : AkkaSpec .WithFallback(DistributedData.DistributedData.DefaultConfig()) .WithFallback(ClusterSingleton.DefaultConfig()); + private readonly MessageExtractor _messageExtractor = new(); + public GetShardTypeNamesSpec(ITestOutputHelper helper) : base(SpecConfig, helper) { } @@ -44,30 +69,10 @@ public void GetShardTypeNames_must_contain_started_shards_when_started_2_shards( { Cluster.Get(Sys).Join(Cluster.Get(Sys).SelfAddress); var settings = ClusterShardingSettings.Create(Sys); - ClusterSharding.Get(Sys).Start("type1", SimpleEchoActor.Props(), settings, ExtractEntityId, ExtractShardId); - ClusterSharding.Get(Sys).Start("type2", SimpleEchoActor.Props(), settings, ExtractEntityId, ExtractShardId); + ClusterSharding.Get(Sys).Start("type1", SimpleEchoActor.Props(), settings, _messageExtractor); + ClusterSharding.Get(Sys).Start("type2", SimpleEchoActor.Props(), settings, _messageExtractor); ClusterSharding.Get(Sys).ShardTypeNames.Should().BeEquivalentTo("type1", "type2"); } - - private Option<(string, object)> ExtractEntityId(object message) - { - switch (message) - { - case int i: - return (i.ToString(), message); - } - throw new NotSupportedException(); - } - - private string ExtractShardId(object message) - { - switch (message) - { - case int i: - return (i % 10).ToString(); - } - throw new NotSupportedException(); - } } } diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/LeastShardAllocationStrategySpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/LeastShardAllocationStrategySpec.cs index e424b7cba79..8b5ed313a35 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/LeastShardAllocationStrategySpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/LeastShardAllocationStrategySpec.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading.Tasks; using Akka.Actor; using Akka.TestKit; using Akka.Util; @@ -311,76 +312,76 @@ public void LeastShardAllocationStrategy_must_rebalance_shards_3_0_0() } [Fact] - public void LeastShardAllocationStrategy_must_rebalance_shards_4_4_0() + public async Task LeastShardAllocationStrategy_must_rebalance_shards_4_4_0() { var allocationStrategy = strategyWithoutLimits; var allocations = CreateAllocations(aCount: 4, bCount: 4); - var result = allocationStrategy.Rebalance(allocations, ImmutableHashSet.Empty).Result; + var result = await allocationStrategy.Rebalance(allocations, ImmutableHashSet.Empty); result.Should().BeEquivalentTo("001", "005"); AllocationCountsAfterRebalance(allocationStrategy, allocations, result).Should().Equal(3, 3, 2); } [Fact] - public void LeastShardAllocationStrategy_must_rebalance_shards_4_4_2() + public async Task LeastShardAllocationStrategy_must_rebalance_shards_4_4_2() { // this is handled by phase 2, to find diff of 2 var allocationStrategy = strategyWithoutLimits; var allocations = CreateAllocations(aCount: 4, bCount: 4, cCount: 2); - var result = allocationStrategy.Rebalance(allocations, ImmutableHashSet.Empty).Result; + var result = await allocationStrategy.Rebalance(allocations, ImmutableHashSet.Empty); result.Should().BeEquivalentTo("001"); AllocationCountsAfterRebalance(allocationStrategy, allocations, result).OrderBy(i => i).Should().Equal(new[] { 3, 4, 3 }.OrderBy(i => i)); } [Fact] - public void LeastShardAllocationStrategy_must_rebalance_shards_5_5_0() + public async Task LeastShardAllocationStrategy_must_rebalance_shards_5_5_0() { var allocationStrategy = strategyWithoutLimits; var allocations = CreateAllocations(aCount: 5, bCount: 5); - var result1 = allocationStrategy.Rebalance(allocations, ImmutableHashSet.Empty).Result; + var result1 = await allocationStrategy.Rebalance(allocations, ImmutableHashSet.Empty); result1.Should().BeEquivalentTo("001", "006"); // so far [4, 4, 2] AllocationCountsAfterRebalance(allocationStrategy, allocations, result1).Should().Equal(4, 4, 2); var allocations2 = AfterRebalance(allocationStrategy, allocations, result1); // second phase will find the diff of 2, resulting in [3, 4, 3] - var result2 = allocationStrategy.Rebalance(allocations2, ImmutableHashSet.Empty).Result; + var result2 = await allocationStrategy.Rebalance(allocations2, ImmutableHashSet.Empty); result2.Should().BeEquivalentTo("002"); AllocationCountsAfterRebalance(allocationStrategy, allocations2, result2).OrderBy(i => i).Should().Equal(new[] { 3, 4, 3 }.OrderBy(i => i)); } [Fact] - public void LeastShardAllocationStrategy_must_rebalance_shards_50_50_0() + public async Task LeastShardAllocationStrategy_must_rebalance_shards_50_50_0() { var allocationStrategy = strategyWithoutLimits; var allocations = CreateAllocations(aCount: 50, cCount: 50); - var result1 = allocationStrategy.Rebalance(allocations, ImmutableHashSet.Empty).Result; + var result1 = await allocationStrategy.Rebalance(allocations, ImmutableHashSet.Empty); result1.Should().BeEquivalentTo(shards.Take(50 - 34).Union(shards.Skip(50).Take(50 - 34))); // so far [34, 34, 32] AllocationCountsAfterRebalance(allocationStrategy, allocations, result1).OrderBy(i => i).Should().Equal(new[] { 34, 34, 32 }.OrderBy(i => i)); var allocations2 = AfterRebalance(allocationStrategy, allocations, result1); // second phase will find the diff of 2, resulting in [33, 34, 33] - var result2 = allocationStrategy.Rebalance(allocations2, ImmutableHashSet.Empty).Result; + var result2 = await allocationStrategy.Rebalance(allocations2, ImmutableHashSet.Empty); result2.Should().BeEquivalentTo("017"); AllocationCountsAfterRebalance(allocationStrategy, allocations2, result2).OrderBy(i => i).Should().Equal(new[] { 33, 34, 33 }.OrderBy(i => i)); } [Fact] - public void LeastShardAllocationStrategy_must_respect_absolute_limit_of_number_shards() + public async Task LeastShardAllocationStrategy_must_respect_absolute_limit_of_number_shards() { var allocationStrategy = StrategyWithFakeCluster(absoluteLimit: 3, relativeLimit: 1.0); var allocations = CreateAllocations(aCount: 1, bCount: 9); - var result = allocationStrategy.Rebalance(allocations, ImmutableHashSet.Empty).Result; + var result = await allocationStrategy.Rebalance(allocations, ImmutableHashSet.Empty); result.Should().BeEquivalentTo("002", "003", "004"); AllocationCountsAfterRebalance(allocationStrategy, allocations, result).Should().Equal(2, 6, 2); } [Fact] - public void LeastShardAllocationStrategy_must_respect_relative_limit_of_number_shards() + public async Task LeastShardAllocationStrategy_must_respect_relative_limit_of_number_shards() { var allocationStrategy = StrategyWithFakeCluster(absoluteLimit: 5, relativeLimit: 0.3); var allocations = CreateAllocations(aCount: 1, bCount: 9); - var result = allocationStrategy.Rebalance(allocations, ImmutableHashSet.Empty).Result; + var result = await allocationStrategy.Rebalance(allocations, ImmutableHashSet.Empty); result.Should().BeEquivalentTo("002", "003", "004"); AllocationCountsAfterRebalance(allocationStrategy, allocations, result).Should().Equal(2, 6, 2); } diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ProxyShardingSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ProxyShardingSpec.cs index 8023885dc6c..e056ac561e7 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ProxyShardingSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ProxyShardingSpec.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Threading.Tasks; using Akka.Actor; using Akka.Cluster.Tools.Singleton; using Akka.Configuration; @@ -19,9 +20,10 @@ namespace Akka.Cluster.Sharding.Tests { public class ProxyShardingSpec : Akka.TestKit.Xunit2.TestKit { - ClusterSharding clusterSharding; - ClusterShardingSettings shardingSettings; + private ClusterSharding clusterSharding; + private ClusterShardingSettings shardingSettings; private MessageExtractor messageExtractor = new(10); + private readonly ProxyMessageExtractor _proxyMessageExtractor = new(); private class MessageExtractor : HashCodeMessageExtractor { @@ -35,24 +37,27 @@ public override string EntityId(object message) } } - private Option<(string, object)> IdExtractor(object message) + private class ProxyMessageExtractor: IMessageExtractor { - switch (message) - { - case int i: - return (i.ToString(), message); - } - throw new NotSupportedException(); - } - - private string ShardResolver(object message) - { - switch (message) - { - case int i: - return i.ToString(); - } - throw new NotSupportedException(); + public string EntityId(object message) + => message switch + { + int i => i.ToString(), + _ => null + }; + + public object EntityMessage(object message) + => message; + + public string ShardId(object message) + => message switch + { + int i => i.ToString(), + _ => null + }; + + public string ShardId(string entityId, object messageHint = null) + => entityId; } @@ -72,7 +77,7 @@ public ProxyShardingSpec() : base(SpecConfig) var role = "Shard"; clusterSharding = ClusterSharding.Get(Sys); shardingSettings = ClusterShardingSettings.Create(Sys); - clusterSharding.StartProxy("myType", role, IdExtractor, ShardResolver); + clusterSharding.StartProxy("myType", role, _proxyMessageExtractor); } [Fact] @@ -95,12 +100,12 @@ public void ProxyShardingSpec_Shard_region_should_be_found() } [Fact] - public void ProxyShardingSpec_Shard_coordinator_should_be_found() + public async Task ProxyShardingSpec_Shard_coordinator_should_be_found() { var shardRegion = clusterSharding.Start("myType", SimpleEchoActor.Props(), shardingSettings, messageExtractor); - IActorRef shardCoordinator = Sys.ActorSelection("akka://test/system/sharding/myTypeCoordinator") - .ResolveOne(TimeSpan.FromSeconds(5)).Result; + IActorRef shardCoordinator = await Sys.ActorSelection("akka://test/system/sharding/myTypeCoordinator") + .ResolveOne(TimeSpan.FromSeconds(5)); shardCoordinator.Path.Should().NotBeNull(); shardCoordinator.Path.ToString().Should().EndWith("Coordinator"); diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/RememberEntitiesSupervisionStrategyDecisionSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/RememberEntitiesSupervisionStrategyDecisionSpec.cs new file mode 100644 index 00000000000..c9381f14782 --- /dev/null +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/RememberEntitiesSupervisionStrategyDecisionSpec.cs @@ -0,0 +1,348 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Actor.Internal; +using Akka.Cluster.Sharding.Internal; +using Akka.Cluster.Tools.Singleton; +using Akka.Configuration; +using Akka.Event; +using Akka.TestKit; +using Akka.Util; +using Akka.Util.Internal; +using FluentAssertions; +using FluentAssertions.Extensions; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Cluster.Sharding.Tests; + +public class RememberEntitiesSupervisionStrategyDecisionSpec : AkkaSpec +{ + private sealed record EntityEnvelope(long Id, object Payload); + + private class ConstructorFailActor : ActorBase + { + private readonly ILoggingAdapter _log = Context.GetLogger(); + + public ConstructorFailActor() + { + throw new Exception("EXPLODING CONSTRUCTOR!"); + } + + protected override bool Receive(object message) + { + _log.Info("Msg {0}", message); + Sender.Tell($"ack {message}"); + return true; + } + } + + private class PreStartFailActor : ActorBase + { + private readonly ILoggingAdapter _log = Context.GetLogger(); + + protected override void PreStart() + { + base.PreStart(); + throw new Exception("EXPLODING PRE-START!"); + } + + protected override bool Receive(object message) + { + _log.Info("Msg {0}", message); + Sender.Tell($"ack {message}"); + return true; + } + } + + private sealed class TestMessageExtractor: IMessageExtractor + { + public string EntityId(object message) + => message switch + { + EntityEnvelope env => env.Id.ToString(), + _ => null + }; + + public object EntityMessage(object message) + => message switch + { + EntityEnvelope env => env.Payload, + _ => message + }; + + public string ShardId(object message) + => message switch + { + EntityEnvelope msg => msg.Id.ToString(), + _ => null + }; + + public string ShardId(string entityId, object messageHint = null) + => entityId; + } + + private class FakeShardRegion : ReceiveActor + { + private readonly ClusterShardingSettings _settings; + private readonly Props _entityProps; + private IActorRef? _shard; + + public FakeShardRegion(ClusterShardingSettings settings, Props entityProps) + { + _settings = settings; + _entityProps = entityProps; + + Receive(_ => + { + // no-op + }); + Receive(msg => + { + _shard.Forward(msg); + }); + } + + protected override void PreStart() + { + base.PreStart(); + var provider = new FakeStore(_settings, "cats"); + + var props = Props.Create(() => new Shard( + "cats", + "shard-1", + _ => _entityProps, + _settings, + new TestMessageExtractor(), + PoisonPill.Instance, + provider, + null + )); + _shard = Context.ActorOf(props); + } + } + + private class ShardStoreCreated + { + public ShardStoreCreated(IActorRef store, string shardId) + { + Store = store; + ShardId = shardId; + } + + public IActorRef Store { get; } + public string ShardId { get; } + } + + private class CoordinatorStoreCreated + { + public CoordinatorStoreCreated(IActorRef store) + { + Store = store; + } + + public IActorRef Store { get; } + } + + private class FakeStore : IRememberEntitiesProvider + { + public FakeStore(ClusterShardingSettings settings, string typeName) + { + } + + public Props ShardStoreProps(string shardId) + { + return FakeShardStoreActor.Props(shardId); + } + + public Props CoordinatorStoreProps() + { + return FakeCoordinatorStoreActor.Props(); + } + } + + private class FakeShardStoreActor : ActorBase, IWithTimers + { + public static Props Props(string shardId) => Actor.Props.Create(() => new FakeShardStoreActor(shardId)); + + private readonly string _shardId; + private readonly ILoggingAdapter _log = Context.GetLogger(); + + public FakeShardStoreActor(string shardId) + { + _shardId = shardId; + Context.System.EventStream.Publish(new ShardStoreCreated(Self, shardId)); + } + + public ITimerScheduler Timers { get; set; } + + protected override bool Receive(object message) + { + switch (message) + { + case RememberEntitiesShardStore.GetEntities _: + Sender.Tell(new RememberEntitiesShardStore.RememberedEntities(ImmutableHashSet.Empty.Add("1"))); + return true; + case RememberEntitiesShardStore.Update m: + Sender.Tell(new RememberEntitiesShardStore.UpdateDone(m.Started, m.Stopped)); + return true; + } + return false; + } + } + + private class FakeCoordinatorStoreActor : ActorBase, IWithTimers + { + public static Props Props() => Actor.Props.Create(() => new FakeCoordinatorStoreActor()); + + private readonly ILoggingAdapter _log = Context.GetLogger(); + + public ITimerScheduler Timers { get; set; } + + public FakeCoordinatorStoreActor() + { + Context.System.EventStream.Publish(new CoordinatorStoreCreated(Context.Self)); + } + + protected override bool Receive(object message) + { + switch (message) + { + case RememberEntitiesCoordinatorStore.GetShards _: + Sender.Tell(new RememberEntitiesCoordinatorStore.RememberedShards(ImmutableHashSet.Empty.Add("1"))); + return true; + case RememberEntitiesCoordinatorStore.AddShard m: + Sender.Tell(new RememberEntitiesCoordinatorStore.UpdateDone(m.ShardId)); + return true; + } + return false; + } + } + + private class TestSupervisionStrategy: ShardSupervisionStrategy + { + private readonly AtomicCounter _counter; + + public TestSupervisionStrategy(AtomicCounter counter, int maxRetry, int window, Func localOnlyDecider) + : base(maxRetry, window, localOnlyDecider) + { + _counter = counter; + } + + public override void ProcessFailure(IActorContext context, bool restart, IActorRef child, Exception cause, ChildRestartStats stats, + IReadOnlyCollection children) + { + _counter.GetAndIncrement(); + base.ProcessFailure(context, restart, child, cause, stats, children); + } + } + + private static Config SpecConfig => + ConfigurationFactory.ParseString( + """ + akka { + loglevel = DEBUG + actor.provider = cluster + remote.dot-netty.tcp.port = 0 + + cluster.sharding { + distributed-data.durable.keys = [] + state-store-mode = ddata + remember-entities = on + remember-entities-store = custom + remember-entities-custom-store = "Akka.Cluster.Sharding.Tests.RememberEntitiesSupervisionStrategyDecisionSpec+FakeStore, Akka.Cluster.Sharding.Tests" + verbose-debug-logging = on + } + } + """) + .WithFallback(ClusterSingleton.DefaultConfig()) + .WithFallback(ClusterSharding.DefaultConfig()); + + public RememberEntitiesSupervisionStrategyDecisionSpec(ITestOutputHelper helper) : base(SpecConfig, helper) + { + } + + protected override void AtStartup() + { + // Form a one node cluster + var cluster = Cluster.Get(Sys); + cluster.Join(cluster.SelfAddress); + AwaitAssert(() => + { + cluster.ReadView.Members.Count(m => m.Status == MemberStatus.Up).Should().Be(1); + }); + } + + public Directive TestDecider(Exception cause) + { + return Directive.Restart; + } + + [Fact(DisplayName = "Persistent shard must stop remembered entity with excessive failures")] + public async Task Persistent_Shard_must_stop_remembered_entity_with_excessive_restart_attempt() + { + var strategyCounter = new AtomicCounter(0); + + var settings = ClusterShardingSettings.Create(Sys); + settings = settings + .WithTuningParameters(settings.TuningParameters.WithEntityRestartBackoff(0.1.Seconds())) + .WithRememberEntities(true) + .WithSupervisorStrategy(new TestSupervisionStrategy(strategyCounter, 3, 1000, TestDecider)); + + var storeProbe = CreateTestProbe(); + Sys.EventStream.Subscribe(storeProbe); + Sys.EventStream.Subscribe(TestActor); + + var entityProps = Props.Create(() => new PreStartFailActor()); + await EventFilter.Error(contains: "cats: Remembered entity 1 was stopped: entity failed repeatedly") + .ExpectOneAsync(async () => + { + _ = Sys.ActorOf(Props.Create(() => new FakeShardRegion(settings, entityProps))); + storeProbe.ExpectMsg(); + await Task.Yield(); + }); + + // Failed on the 4th call + strategyCounter.Current.Should().Be(4); + } + + [Fact(DisplayName = "Persistent shard must stop remembered entity when stopped using Directive.Stop decision")] + public async Task Persistent_Shard_must_stop_remembered_entity_with_stop_directive_on_constructor_failure() + { + var strategyCounter = new AtomicCounter(0); + + var settings = ClusterShardingSettings.Create(Sys); + settings = settings + .WithTuningParameters(settings.TuningParameters.WithEntityRestartBackoff(0.1.Seconds())) + .WithRememberEntities(true) + .WithSupervisorStrategy(new TestSupervisionStrategy(strategyCounter, 3, 1000, SupervisorStrategy.DefaultDecider.Decide)); + + var storeProbe = CreateTestProbe(); + Sys.EventStream.Subscribe(storeProbe); + Sys.EventStream.Subscribe(TestActor); + + var entityProps = Props.Create(() => new ConstructorFailActor()); + await EventFilter.Error(contains: "cats: Remembered entity 1 was stopped: entity stopped by Directive.Stop decision") + .ExpectOneAsync(async () => + { + _ = Sys.ActorOf(Props.Create(() => new FakeShardRegion(settings, entityProps))); + storeProbe.ExpectMsg(); + await Task.Yield(); + }); + + // Failed on the 1st call + strategyCounter.Current.Should().Be(1); + } + +} \ No newline at end of file diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardEntityFailureSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardEntityFailureSpec.cs index 1fc92a9a341..0c545e23477 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardEntityFailureSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardEntityFailureSpec.cs @@ -139,14 +139,15 @@ public async Task Persistent_Shard_must_recover_from_failing_entity(Props entity null )); - Sys.EventStream.Subscribe(TestActor); + var errorProbe = CreateTestProbe(); + Sys.EventStream.Subscribe(errorProbe); var persistentShard = Sys.ActorOf(props); persistentShard.Tell(new EntityEnvelope(1, "Start")); // entity died here - var err = ExpectMsg(); + var err = errorProbe.ExpectMsg(); err.Cause.Should().BeOfType(); // Need to wait for the internal state to reset, else everything we sent will go to dead letter diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardRegionQueriesSpecs.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardRegionQueriesSpecs.cs index f0a1dba7f9d..74a1560adb7 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardRegionQueriesSpecs.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardRegionQueriesSpecs.cs @@ -23,6 +23,7 @@ public class ShardRegionQueriesSpecs : AkkaSpec { private readonly Cluster _cluster; private readonly ClusterSharding _clusterSharding; + private readonly MessageExtractor _messageExtractor = new(); private readonly IActorRef _shardRegion; private readonly ActorSystem _proxySys; @@ -32,7 +33,7 @@ public ShardRegionQueriesSpecs(ITestOutputHelper outputHelper) : base(GetConfig( _clusterSharding = ClusterSharding.Get(Sys); _cluster = Cluster.Get(Sys); _shardRegion = _clusterSharding.Start("entity", _ => EchoActor.Props(this, true), - ClusterShardingSettings.Create(Sys).WithRole("shard"), ExtractEntityId, ExtractShardId); + ClusterShardingSettings.Create(Sys).WithRole("shard"), _messageExtractor); var proxySysConfig = ConfigurationFactory.ParseString("akka.cluster.roles = [proxy]") .WithFallback(Sys.Settings.Config); @@ -53,33 +54,28 @@ protected override void AfterAll() base.AfterAll(); } - private Option<(string, object)> ExtractEntityId(object message) + private class MessageExtractor: IMessageExtractor { - switch (message) - { - case int i: - return (i.ToString(), message); - } - - throw new NotSupportedException(); - } - - // - private string ExtractShardId(object message) - { - switch (message) - { - case int i: - return (i % 10 + 1).ToString(); - // must support ShardRegion.StartEntity in order for - // GetEntityLocation to work properly - case ShardRegion.StartEntity se: - return (int.Parse(se.EntityId) % 10 + 1).ToString(); - } - - throw new NotSupportedException(); + public string EntityId(object message) + => message switch + { + int i => i.ToString(), + _ => null + }; + + public object EntityMessage(object message) + => message; + + public string ShardId(object message) + => message switch + { + int i => (i % 10).ToString(), + _ => null + }; + + public string ShardId(string entityId, object messageHint = null) + => (int.Parse(entityId) % 10).ToString(); } - // private static Config GetConfig() { @@ -153,7 +149,7 @@ public async Task ShardRegion_should_support_GetEntityLocation_query_remotely() { // arrange var sharding2 = ClusterSharding.Get(_proxySys); - var shardRegionProxy = await sharding2.StartProxyAsync("entity", "shard", ExtractEntityId, ExtractShardId); + var shardRegionProxy = await sharding2.StartProxyAsync("entity", "shard", _messageExtractor); await shardRegionProxy.Ask(1, TimeSpan.FromSeconds(3)); await shardRegionProxy.Ask(2, TimeSpan.FromSeconds(3)); diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardWithLeaseSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardWithLeaseSpec.cs index d171162eddc..1ff49bd082c 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardWithLeaseSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ShardWithLeaseSpec.cs @@ -88,11 +88,6 @@ public BadLease(string message, Exception innerEx) : base(message, innerEx) { } - - protected BadLease(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } private class Setup diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterSharding.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterSharding.cs index 8e6323bcb8c..ed58db8b50a 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterSharding.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterSharding.cs @@ -229,7 +229,7 @@ public virtual ShardId ShardId(string entityId, Msg? messageHint = null) /// /// Typical usage of this extension: /// 1. At system startup on each cluster node by registering the supported entity types with - /// the method + /// the method /// 1. Retrieve the actor for a named entity type with /// Settings can be configured as described in the `akka.cluster.sharding` section of the `reference.conf`. /// @@ -291,9 +291,9 @@ public virtual ShardId ShardId(string entityId, Msg? messageHint = null) /// /// '''Delivery Semantics''': /// As long as a sender uses the same actor to deliver messages to an entity - /// actor the order of the messages is preserved. As long as the buffer limit is not reached - /// messages are delivered on a best effort basis, with at-most once delivery semantics, - /// in the same way as ordinary message sending. Reliable end-to-end messaging, with + /// actor the order of the messages is preserved. As long as the buffer limit is not reached, + /// messages will be delivered on a best effort basis, with at-most once delivery semantics, + /// in the same way as an ordinary message sending. Reliable end-to-end messaging, with /// at-least-once semantics can be added by using `AtLeastOnceDelivery` in `akka-persistence`. /// /// @@ -1417,7 +1417,9 @@ public async Task StartProxyAsync(string typeName, string role, IMess #pragma warning disable CS0419 // Ambiguous reference in cref attribute /// /// Retrieve the actor reference of the actor responsible for the named entity type. - /// The entity type must be registered with the or method before it + /// The entity type must be registered with the + /// or + /// method before it /// can be used here. Messages to the entity is always sent via the . /// /// TBD diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs index 99ba4921eca..0a160af4471 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs @@ -95,10 +95,9 @@ public Start( object handOffStopMessage) { if (string.IsNullOrEmpty(typeName)) throw new ArgumentNullException(nameof(typeName), "ClusterSharding start requires type name to be provided"); - if (entityProps == null) throw new ArgumentNullException(nameof(entityProps), $"ClusterSharding start requires Props for [{typeName}] to be provided"); TypeName = typeName; - EntityProps = entityProps; + EntityProps = entityProps ?? throw new ArgumentNullException(nameof(entityProps), $"ClusterSharding start requires Props for [{typeName}] to be provided"); Settings = settings; MessageExtractor = extractor; AllocationStrategy = allocationStrategy; diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingSettings.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingSettings.cs index 206ed1ef05c..edcbd5fd380 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingSettings.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingSettings.cs @@ -338,6 +338,8 @@ public sealed class ClusterShardingSettings : INoSerializationVerificationNeeded /// public readonly LeaseUsageSettings LeaseSettings; + public SupervisorStrategy? SupervisorStrategy { get; } + /// /// Create settings from the default configuration `akka.cluster.sharding`. /// @@ -517,6 +519,34 @@ public ClusterShardingSettings( LeaseSettings = leaseSettings; } + private ClusterShardingSettings( + string role, + bool rememberEntities, + string journalPluginId, + string snapshotPluginId, + TimeSpan passivateIdleEntityAfter, + StateStoreMode stateStoreMode, + RememberEntitiesStore rememberEntitiesStore, + TimeSpan shardRegionQueryTimeout, + TuningParameters tuningParameters, + ClusterSingletonManagerSettings coordinatorSingletonSettings, + LeaseUsageSettings leaseSettings, + SupervisorStrategy supervisorStrategy) + { + Role = role; + RememberEntities = rememberEntities; + JournalPluginId = journalPluginId; + SnapshotPluginId = snapshotPluginId; + PassivateIdleEntityAfter = passivateIdleEntityAfter; + StateStoreMode = stateStoreMode; + RememberEntitiesStore = rememberEntitiesStore; + ShardRegionQueryTimeout = shardRegionQueryTimeout; + TuningParameters = tuningParameters; + CoordinatorSingletonSettings = coordinatorSingletonSettings; + LeaseSettings = leaseSettings; + SupervisorStrategy = supervisorStrategy; + } + /// /// If true, this node should run the shard region, otherwise just a shard proxy should started on this node. /// @@ -603,6 +633,11 @@ public ClusterShardingSettings WithLeaseSettings(LeaseUsageSettings leaseSetting return Copy(leaseSettings: leaseSettings); } + public ClusterShardingSettings WithSupervisorStrategy(SupervisorStrategy supervisorStrategy) + { + return Copy(supervisorStrategy: supervisorStrategy); + } + /// /// TBD /// @@ -630,7 +665,8 @@ private ClusterShardingSettings Copy( TimeSpan? shardRegionQueryTimeout = null, TuningParameters tuningParameters = null, ClusterSingletonManagerSettings coordinatorSingletonSettings = null, - Option leaseSettings = default) + Option leaseSettings = default, + SupervisorStrategy? supervisorStrategy = null) { return new ClusterShardingSettings( role: role.HasValue ? role.Value : Role, @@ -643,7 +679,8 @@ private ClusterShardingSettings Copy( shardRegionQueryTimeout: shardRegionQueryTimeout ?? ShardRegionQueryTimeout, tuningParameters: tuningParameters ?? TuningParameters, coordinatorSingletonSettings: coordinatorSingletonSettings ?? CoordinatorSingletonSettings, - leaseSettings: leaseSettings.HasValue ? leaseSettings.Value : LeaseSettings); + leaseSettings: leaseSettings.HasValue ? leaseSettings.Value : LeaseSettings, + supervisorStrategy: supervisorStrategy ?? SupervisorStrategy); } } } diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/Delivery/Internal/ShardingConsumerControllerImpl.cs b/src/contrib/cluster/Akka.Cluster.Sharding/Delivery/Internal/ShardingConsumerControllerImpl.cs index 1eafef1c32d..57ab5ca612d 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/Delivery/Internal/ShardingConsumerControllerImpl.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/Delivery/Internal/ShardingConsumerControllerImpl.cs @@ -20,8 +20,16 @@ namespace Akka.Cluster.Sharding.Delivery.Internal; /// INTERNAL API /// /// The types of messages handled by the ConsumerController -internal class ShardingConsumerController : ReceiveActor, IWithStash +internal class ShardingConsumerController : ReceiveActor, IWithStash, IWithTimers { + private const string ShutdownTimeoutTimerKey = nameof(ShutdownTimeoutTimerKey); + + private sealed class ShutdownTimeout + { + public static readonly ShutdownTimeout Instance = new (); + private ShutdownTimeout() { } + } + public ShardingConsumerController(Func consumerProps, ShardingConsumerController.Settings settings) { @@ -69,6 +77,11 @@ private void WaitForStart() _log.Debug("Consumer terminated before initialized."); Context.Stop(Self); }); + + Receive(_ => Sender.Equals(_consumer), p => + { + Context.Parent.Tell(p); + }); ReceiveAny(msg => { @@ -110,7 +123,17 @@ private void Active() Receive(t => t.ActorRef.Equals(_consumer), _ => { _log.Debug("Consumer terminated."); - Context.Stop(Self); + + // Short-circuit shutdown process, just shut down immediately if there's nothing to clean. + if (ProducerControllers.Count == 0 && ConsumerControllers.Count == 0) + { + _log.Debug("ShardingConsumerController terminated."); + Context.Stop(Self); + } + else + { + Become(ShuttingDown()); + } }); Receive(t => @@ -141,6 +164,11 @@ private void Active() } } }); + + Receive(_ => Sender.Equals(_consumer), p => + { + Context.Parent.Tell(p); + }); ReceiveAny(msg => { @@ -156,6 +184,62 @@ private void Active() }); } + // Shutdown state after `_consumer` actor is downed. + private Action ShuttingDown() + { + // start a 3-seconds shutdown timeout timer + Timers.StartSingleTimer(ShutdownTimeoutTimerKey, ShutdownTimeout.Instance, TimeSpan.FromSeconds(3), Self); + + _log.Debug("Shutting down child controllers"); + + foreach (var p in ProducerControllers.Keys) + Context.Unwatch(p); + ProducerControllers = ImmutableDictionary.Empty; + + foreach (var c in ConsumerControllers.Values.Distinct()) + Context.Stop(c); + + return () => + { + Receive>(seqMsg => + { + var messageType = seqMsg.Message.Chunk.HasValue + ? $"Manifest: {seqMsg.Message.Chunk.Value.Manifest}, SerializerId: {seqMsg.Message.Chunk.Value.SerializerId}" + : seqMsg.Message.Message?.GetType().FullName ?? "Unknown type"; + _log.Warning("Message [{0}] from [{1}] is being ignored because ShardingConsumerController is shutting down.", messageType, seqMsg.ProducerId); + }); + + Receive(_ => + { + // We somehow could not terminate cleanly within 3 seconds, shutdown immediately + _log.Warning("ShardingConsumerController cleanup timed out, force terminating."); + Context.Stop(Self); + }); + + Receive(t => + { + var removeList = ConsumerControllers + .Where(kv => kv.Value.Equals(t.ActorRef)) + .Select(kv => kv.Key) + .ToArray(); + + if(removeList.Length > 0) + { + foreach (var key in removeList) + _log.Debug("ConsumerController for producerId [{0}] terminated.", key); + + ConsumerControllers = ConsumerControllers.RemoveRange(removeList); + } + + if (ProducerControllers.Count > 0 || ConsumerControllers.Count > 0) + return; + + _log.Debug("ShardingConsumerController terminated."); + Context.Stop(Self); + }); + }; + } + private ImmutableDictionary UpdatedProducerControllers(IActorRef producerController, string producer) { @@ -173,4 +257,5 @@ protected override void PreStart() } public IStash Stash { get; set; } = null!; + public ITimerScheduler Timers { get; set; } = null!; } diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs b/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs index 8afedddb406..6d9d3b2de5f 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs @@ -945,6 +945,7 @@ public override string ToString() } } + private static readonly SupervisorStrategy DefaultShardSupervisionStrategy = new ShardSupervisionStrategy(); private readonly string _typeName; private readonly string _shardId; @@ -966,6 +967,7 @@ public override string ToString() private readonly TimeSpan _leaseRetryInterval = TimeSpan.FromSeconds(5); // won't be used private readonly IShardingBufferMessageAdapter _bufferMessageAdapter; + private readonly SupervisorStrategy? _supervisorStrategy; public ILoggingAdapter Log { get; } = Context.GetLogger(); public IStash Stash { get; set; } = null!; @@ -1024,11 +1026,12 @@ public Shard( } _bufferMessageAdapter = bufferMessageAdapter ?? EmptyBufferMessageAdapter.Instance; + _supervisorStrategy = settings.SupervisorStrategy; } protected override SupervisorStrategy SupervisorStrategy() { - return base.SupervisorStrategy(); + return _supervisorStrategy ?? DefaultShardSupervisionStrategy; } protected override bool Receive(object message) @@ -1258,6 +1261,9 @@ private bool Idle(object message) case Passivate p: Passivate(Sender, p.StopMessage); return true; + case SupervisorStopDirectivePassivation ex: + HandleSupervisorStop(ex); + return true; case IShardQuery msg: ReceiveShardQuery(msg); return true; @@ -1375,6 +1381,9 @@ bool WaitingForRememberEntitiesStore(object message) _entities.EntityId(Sender) ?? $"Unknown actor {Sender}"); Passivate(Sender, p.StopMessage); return true; + case SupervisorStopDirectivePassivation ex: + HandleSupervisorStop(ex); + return true; case IShardQuery msg: ReceiveShardQuery(msg); return true; @@ -1842,6 +1851,29 @@ private void PassivateCompleted(EntityId entityId) } } + private void HandleSupervisorStop(SupervisorStopDirectivePassivation msg) + { + // We only have to do this if we have R-E enabled + if (!_rememberEntities) + return; + + var id = _entities.EntityId(msg.Child); + // Just return if the child actor is not a recorded shard entity + if (id is null) + return; + + // Remove the child actor from the entity list + _entities.RemoveEntity(id); + + // Force stop the child actor, it might have been restarted + Context.Stop(msg.Child); + + Log.Error( + msg.LastCause, + "{0}: Remembered entity {1} was stopped: {2}", + _typeName, id, msg.Reason); + } + private void DeliverMessage(string entityId, object msg, IActorRef snd) { var payload = _extractor.EntityMessage(msg); // payload can't be null unless dev really screwed up diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ShardSupervisionStrategy.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ShardSupervisionStrategy.cs new file mode 100644 index 00000000000..0315f7836d1 --- /dev/null +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ShardSupervisionStrategy.cs @@ -0,0 +1,69 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Akka.Actor; +using Akka.Actor.Internal; + +namespace Akka.Cluster.Sharding; + +public class ShardSupervisionStrategy: OneForOneStrategy +{ + public ShardSupervisionStrategy(int? maxNrOfRetries, TimeSpan? withinTimeRange, Func localOnlyDecider) + : base(maxNrOfRetries, withinTimeRange, localOnlyDecider) + { + } + + public ShardSupervisionStrategy(int? maxNrOfRetries, TimeSpan? withinTimeRange, IDecider decider) + : base(maxNrOfRetries, withinTimeRange, decider) + { + } + + public ShardSupervisionStrategy(int maxNrOfRetries, int withinTimeMilliseconds, Func localOnlyDecider, bool loggingEnabled = true) + : base(maxNrOfRetries, withinTimeMilliseconds, localOnlyDecider, loggingEnabled) + { + } + + public ShardSupervisionStrategy(int maxNrOfRetries, int withinTimeMilliseconds, IDecider decider, bool loggingEnabled = true) + : base(maxNrOfRetries, withinTimeMilliseconds, decider, loggingEnabled) + { + } + + public ShardSupervisionStrategy(Func localOnlyDecider) + : base(localOnlyDecider) + { + } + + public ShardSupervisionStrategy(Func localOnlyDecider, bool loggingEnabled = true) + : base(localOnlyDecider, loggingEnabled) + { + } + + public ShardSupervisionStrategy(IDecider decider) + : base(decider) + { + } + + public ShardSupervisionStrategy() + { + } + + public override void ProcessFailure(IActorContext context, bool restart, IActorRef child, Exception cause, ChildRestartStats stats, IReadOnlyCollection children) + { + if (restart && stats.RequestRestartPermission(MaxNumberOfRetries, WithinTimeRangeMilliseconds)) + RestartChild(child, cause, suspendFirst: false); + else + { + var reason = restart + ? $"entity failed repeatedly within {WithinTimeRangeMilliseconds} ms, exceeding the supervisor strategy maximum restart count of {MaxNumberOfRetries}" + : "entity stopped by Directive.Stop decision"; + context.Self.Tell(new SupervisorStopDirectivePassivation(child, reason, cause)); + context.Stop(child); + } + } +} \ No newline at end of file diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ShardingMessages.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ShardingMessages.cs index b6a4d66b735..8302fcde08d 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ShardingMessages.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ShardingMessages.cs @@ -61,6 +61,8 @@ public Passivate(object stopMessage) public object StopMessage { get; } } + internal sealed record SupervisorStopDirectivePassivation(IActorRef Child, string Reason, Exception LastCause) : IShardRegionCommand; + /// /// Send this message to the actor to handoff all shards that are hosted by /// the and then the actor will be stopped. You can 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 0a6868227c3..fd755c0fa9b 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 @@ -15,22 +15,23 @@ using Akka.Remote.TestKit; using FluentAssertions; using FluentAssertions.Extensions; +using System.Threading.Tasks; -namespace Akka.Cluster.Tools.Tests.MultiNode.PublishSubscribe +namespace Akka.Cluster.Tools.Tests.MultiNode.PublishSubscribe; + +public class DistributedPubSubRestartSpecConfig : MultiNodeConfig { - public class DistributedPubSubRestartSpecConfig : MultiNodeConfig - { - public RoleName First { get; } - public RoleName Second { get; } - public RoleName Third { get; } + public RoleName First { get; } + public RoleName Second { get; } + public RoleName Third { get; } - public DistributedPubSubRestartSpecConfig() - { - First = Role("first"); - Second = Role("second"); - Third = Role("third"); + public DistributedPubSubRestartSpecConfig() + { + First = Role("first"); + Second = Role("second"); + Third = Role("third"); - CommonConfig = ConfigurationFactory.ParseString(@" + CommonConfig = ConfigurationFactory.ParseString(@" akka.loglevel = INFO akka.actor.provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster"" akka.cluster.pub-sub.gossip-interval = 500ms @@ -38,177 +39,176 @@ public DistributedPubSubRestartSpecConfig() akka.cluster.auto-down-unreachable-after = off ").WithFallback(DistributedPubSub.DefaultConfig()); - TestTransport = true; - } + TestTransport = true; + } - internal class Shutdown : ReceiveActor + internal class Shutdown : ReceiveActor + { + public Shutdown() { - public Shutdown() + Context.GetLogger().Info("Shutdown actor started on {0}", Context.System.Name); + Receive(str => str.Equals("shutdown"), _ => { - Context.GetLogger().Info("Shutdown actor started on {0}", Context.System.Name); - Receive(str => str.Equals("shutdown"), _ => - { - Context.System.Terminate(); - }); - } + Context.System.Terminate(); + }); } } +} + +public class DistributedPubSubRestartSpec : MultiNodeClusterSpec +{ + private readonly DistributedPubSubRestartSpecConfig _config; - public class DistributedPubSubRestartSpec : MultiNodeClusterSpec + public DistributedPubSubRestartSpec() : this(new DistributedPubSubRestartSpecConfig()) { - private readonly DistributedPubSubRestartSpecConfig _config; + } - public DistributedPubSubRestartSpec() : this(new DistributedPubSubRestartSpecConfig()) - { - } + protected DistributedPubSubRestartSpec(DistributedPubSubRestartSpecConfig config) : base(config, typeof(DistributedPubSubRestartSpec)) + { + _config = config; + } - protected DistributedPubSubRestartSpec(DistributedPubSubRestartSpecConfig config) : base(config, typeof(DistributedPubSubRestartSpec)) - { - _config = config; - } + [MultiNodeFact] + public async Task DistributedPubSubRestartSpecs() + { + await A_Cluster_with_DistributedPubSub_must_startup_3_node_cluster(); + await A_Cluster_with_DistributedPubSub_must_handle_restart_of_nodes_with_same_address(); + } - [MultiNodeFact] - public void DistributedPubSubRestartSpecs() + public async Task A_Cluster_with_DistributedPubSub_must_startup_3_node_cluster() + { + await WithinAsync(15.Seconds(), async () => { - A_Cluster_with_DistributedPubSub_must_startup_3_node_cluster(); - A_Cluster_with_DistributedPubSub_must_handle_restart_of_nodes_with_same_address(); - } + await JoinAsync(_config.First, _config.First); + await JoinAsync(_config.Second, _config.First); + await JoinAsync(_config.Third, _config.First); + await EnterBarrierAsync("after-1"); + }); + } - public void A_Cluster_with_DistributedPubSub_must_startup_3_node_cluster() + public async Task A_Cluster_with_DistributedPubSub_must_handle_restart_of_nodes_with_same_address() + { + await WithinAsync(30.Seconds(), async () => { - Within(15.Seconds(), () => - { - Join(_config.First, _config.First); - Join(_config.Second, _config.First); - Join(_config.Third, _config.First); - EnterBarrier("after-1"); - }); - } + Mediator.Tell(new Subscribe("topic1", TestActor)); + ExpectMsg(); + await CountAsync(3); - public void A_Cluster_with_DistributedPubSub_must_handle_restart_of_nodes_with_same_address() - { - Within(30.Seconds(), () => + RunOn(() => { - Mediator.Tell(new Subscribe("topic1", TestActor)); - ExpectMsg(); - AwaitCount(3); - - RunOn(() => - { - Mediator.Tell(new Publish("topic1", "msg1")); - }, _config.First); - EnterBarrier("pub-msg1"); + Mediator.Tell(new Publish("topic1", "msg1")); + }, _config.First); + await EnterBarrierAsync("pub-msg1"); - ExpectMsg("msg1"); - EnterBarrier("got-msg1"); + await ExpectMsgAsync("msg1"); + await EnterBarrierAsync("got-msg1"); - RunOn(() => - { - Mediator.Tell(DeltaCount.Instance); - var oldDeltaCount = ExpectMsg(); + await RunOnAsync(async () => + { + Mediator.Tell(DeltaCount.Instance); + var oldDeltaCount = await ExpectMsgAsync(); - EnterBarrier("end"); + await EnterBarrierAsync("end"); - Mediator.Tell(DeltaCount.Instance); - var deltaCount = ExpectMsg(); - deltaCount.Should().Be(oldDeltaCount); - }, _config.Second); + Mediator.Tell(DeltaCount.Instance); + var deltaCount = await ExpectMsgAsync(); + deltaCount.Should().Be(oldDeltaCount); + }, _config.Second); - RunOn(() => - { - Mediator.Tell(DeltaCount.Instance); - var oldDeltaCount = ExpectMsg(); + await RunOnAsync(async () => + { + Mediator.Tell(DeltaCount.Instance); + var oldDeltaCount = await ExpectMsgAsync(); - var thirdAddress = Node(_config.Third).Address; - TestConductor.Shutdown(_config.Third).Wait(); + var thirdAddress = (await NodeAsync(_config.Third)).Address; + await TestConductor.Shutdown(_config.Third).WaitAsync(30.Seconds()); - Within(20.Seconds(), () => + await WithinAsync(20.Seconds(), async () => + { + await AwaitAssertAsync(async () => { - AwaitAssert(() => - { - Sys.ActorSelection(new RootActorPath(thirdAddress) / "user" / "shutdown").Tell(new Identify(null)); - ExpectMsg(1.Seconds()).Subject.Should().NotBeNull(); - }); + Sys.ActorSelection(new RootActorPath(thirdAddress) / "user" / "shutdown").Tell(new Identify(null)); + (await ExpectMsgAsync(1.Seconds())).Subject.Should().NotBeNull(); }); + }); - Sys.ActorSelection(new RootActorPath(thirdAddress) / "user" / "shutdown").Tell("shutdown"); + Sys.ActorSelection(new RootActorPath(thirdAddress) / "user" / "shutdown").Tell("shutdown"); - EnterBarrier("end"); + await EnterBarrierAsync("end"); - Mediator.Tell(DeltaCount.Instance); - var deltaCount = ExpectMsg(); - deltaCount.Should().Be(oldDeltaCount); - }, _config.First); + Mediator.Tell(DeltaCount.Instance); + var deltaCount = await ExpectMsgAsync(); + deltaCount.Should().Be(oldDeltaCount); + }, _config.First); - RunOn(() => + await RunOnAsync(async () => + { + var node3Address = Cluster.Get(Sys).SelfAddress; + await Sys.WhenTerminated.WaitAsync(30.Seconds()); + var newSystem = ActorSystem.Create( + Sys.Name, + ConfigurationFactory + .ParseString($"akka.remote.dot-netty.tcp.port={node3Address.Port}") + .WithFallback(Sys.Settings.Config)); + + try { - var node3Address = Cluster.Get(Sys).SelfAddress; - Sys.WhenTerminated.Wait(10.Seconds()); - var newSystem = ActorSystem.Create( - Sys.Name, - ConfigurationFactory - .ParseString($"akka.remote.dot-netty.tcp.port={node3Address.Port}") - .WithFallback(Sys.Settings.Config)); - - try - { - // don't join the old cluster - Cluster.Get(newSystem).Join(Cluster.Get(newSystem).SelfAddress); - var newMediator = DistributedPubSub.Get(newSystem).Mediator; - var probe = CreateTestProbe(newSystem); - newMediator.Tell(new Subscribe("topic2", probe.Ref), probe.Ref); - probe.ExpectMsg(); - - // let them gossip, but Delta should not be exchanged - probe.ExpectNoMsg(5.Seconds()); - newMediator.Tell(DeltaCount.Instance, probe.Ref); - probe.ExpectMsg(0L); - - newSystem.Log.Info("Shutdown actor started on {0}",node3Address); - newSystem.ActorOf("shutdown"); - newSystem.WhenTerminated.Wait(10.Seconds()); - } - finally - { - newSystem.Terminate(); - } - }, _config.Third); - }); - } + // don't join the old cluster + await Cluster.Get(newSystem).JoinAsync(Cluster.Get(newSystem).SelfAddress); + var newMediator = DistributedPubSub.Get(newSystem).Mediator; + var probe = CreateTestProbe(newSystem); + newMediator.Tell(new Subscribe("topic2", probe.Ref), probe.Ref); + await probe.ExpectMsgAsync(); + + // let them gossip, but Delta should not be exchanged + await probe.ExpectNoMsgAsync(5.Seconds()); + newMediator.Tell(DeltaCount.Instance, probe.Ref); + await probe.ExpectMsgAsync(0L); + + newSystem.Log.Info("Shutdown actor started on {0}",node3Address); + newSystem.ActorOf("shutdown"); + await newSystem.WhenTerminated.WaitAsync(30.Seconds()); + } + finally + { + await newSystem.Terminate().WaitAsync(30.Seconds()); + } + }, _config.Third); + }); + } - protected override int InitialParticipantsValueFactory => Roles.Count; + protected override int InitialParticipantsValueFactory => Roles.Count; - private IActorRef CreateMediator() - { - return DistributedPubSub.Get(Sys).Mediator; - } + private IActorRef CreateMediator() + { + return DistributedPubSub.Get(Sys).Mediator; + } - private IActorRef Mediator + private IActorRef Mediator + { + get { - get - { - return DistributedPubSub.Get(Sys).Mediator; - } + return DistributedPubSub.Get(Sys).Mediator; } + } - private void Join(RoleName from, RoleName to) + private async Task JoinAsync(RoleName from, RoleName to) + { + RunOn(() => { - RunOn(() => - { - Cluster.Get(Sys).Join(Node(to).Address); - CreateMediator(); - }, from); - EnterBarrier(from.Name + "-joined"); - } + Cluster.Get(Sys).Join(Node(to).Address); + CreateMediator(); + }, from); + await EnterBarrierAsync(from.Name + "-joined"); + } - private void AwaitCount(int expected) + private async Task CountAsync(int expected) + { + var probe = CreateTestProbe(); + await AwaitAssertAsync(async () => { - var probe = CreateTestProbe(); - AwaitAssert(() => - { - Mediator.Tell(Count.Instance, probe.Ref); - probe.ExpectMsg().Should().Be(expected); - }); - } + Mediator.Tell(Count.Instance, probe.Ref); + (await probe.ExpectMsgAsync()).Should().Be(expected); + }); } -} +} \ No newline at end of file diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests/Singleton/ClusterSingletonApiSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests/Singleton/ClusterSingletonApiSpec.cs index 2e9abb97ea2..b8609cb4126 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests/Singleton/ClusterSingletonApiSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests/Singleton/ClusterSingletonApiSpec.cs @@ -71,7 +71,9 @@ public static Config GetConfig() => ConfigurationFactory.ParseString(@" hostname = ""127.0.0.1"" port = 0 } - }").WithFallback(TestConfigs.DefaultConfig); + }") + .WithFallback(TestConfigs.DefaultConfig) + .WithFallback(ClusterSingleton.DefaultConfig()); public ClusterSingletonApiSpec(ITestOutputHelper testOutput) : base(GetConfig(), testOutput) @@ -81,6 +83,7 @@ public ClusterSingletonApiSpec(ITestOutputHelper testOutput) _system2 = ActorSystem.Create( Sys.Name, ConfigurationFactory.ParseString("akka.cluster.roles = [\"singleton\"]").WithFallback(Sys.Settings.Config)); + InitializeLogger(_system2, "Sys2: "); _clusterNode2 = Cluster.Get(_system2); } @@ -97,29 +100,40 @@ public void A_cluster_singleton_must_be_accessible_from_two_nodes_in_a_cluster() _clusterNode2.Join(_clusterNode2.SelfAddress); node2UpProbe.AwaitAssert(() => _clusterNode2.SelfMember.Status.ShouldBe(MemberStatus.Up), TimeSpan.FromSeconds(3)); - var cs1 = ClusterSingleton.Get(Sys); - var cs2 = ClusterSingleton.Get(_system2); - - var settings = ClusterSingletonSettings.Create(Sys).WithRole("singleton"); - var node1ref = cs1.Init(SingletonActor.Create(Props.Create(), "ping-pong").WithStopMessage(Perish.Instance).WithSettings(settings)); - var node2ref = cs2.Init(SingletonActor.Create(Props.Create(), "ping-pong").WithStopMessage(Perish.Instance).WithSettings(settings)); - - // subsequent spawning returns the same refs - cs1.Init(SingletonActor.Create(Props.Create(), "ping-pong").WithStopMessage(Perish.Instance).WithSettings(settings)).ShouldBe(node1ref); - cs2.Init(SingletonActor.Create(Props.Create(), "ping-pong").WithStopMessage(Perish.Instance).WithSettings(settings)).ShouldBe(node2ref); + var settings = ClusterSingletonManagerSettings.Create(Sys) + .WithRole("singleton").WithSingletonName("ping-pong"); + + Sys.ActorOf(ClusterSingletonManager.Props( + singletonProps: Props.Create(), + terminationMessage: Perish.Instance, + settings: settings), "singletonManager-ping-pong"); + _system2.ActorOf(ClusterSingletonManager.Props( + singletonProps: Props.Create(), + terminationMessage: Perish.Instance, + settings: settings), "singletonManager-ping-pong"); + + var proxySettings = ClusterSingletonProxySettings.Create(Sys) + .WithRole("singleton").WithSingletonName("ping-pong"); + + var node1Ref = Sys.ActorOf(ClusterSingletonProxy.Props( + singletonManagerPath: "/user/singletonManager-ping-pong", + settings: proxySettings), "singletonProxy-ping-pong"); + var node2Ref = _system2.ActorOf(ClusterSingletonProxy.Props( + singletonManagerPath: "/user/singletonManager-ping-pong", + settings: proxySettings), "singletonProxy-ping-pong"); var node1PongProbe = CreateTestProbe(Sys); var node2PongProbe = CreateTestProbe(_system2); node1PongProbe.AwaitAssert(() => { - node1ref.Tell(new Ping(node1PongProbe.Ref)); + node1Ref.Tell(new Ping(node1PongProbe.Ref)); node1PongProbe.ExpectMsg(); }, TimeSpan.FromSeconds(3)); node2PongProbe.AwaitAssert(() => { - node2ref.Tell(new Ping(node2PongProbe.Ref)); + node2Ref.Tell(new Ping(node2PongProbe.Ref)); node2PongProbe.ExpectMsg(); }, TimeSpan.FromSeconds(3)); } diff --git a/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedMessages.cs b/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedMessages.cs index 4154cecc804..8d51a0a2911 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedMessages.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedMessages.cs @@ -441,9 +441,11 @@ public enum PublishFailReason MediatorShuttingDown } - public sealed record PublishFailed(PublishWithAck Message, PublishFailReason Reason): IDeadLetterSuppression; + public interface IPublishResponse; - public sealed record PublishSucceeded(PublishWithAck Message): IDeadLetterSuppression; + public sealed record PublishFailed(PublishWithAck Message, PublishFailReason Reason): IPublishResponse, IDeadLetterSuppression; + + public sealed record PublishSucceeded(PublishWithAck Message): IPublishResponse, IDeadLetterSuppression; /// /// TBD diff --git a/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedPubSubMediator.cs b/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedPubSubMediator.cs index ccb07eefc00..8bba912fc30 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedPubSubMediator.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedPubSubMediator.cs @@ -106,10 +106,10 @@ public class DistributedPubSubMediator : ReceiveActor, IWithTimers private const string PruneBufferTimerKey = "PruneBufferTimer"; /// - /// TBD + /// Creates a new instance to create this actor /// - /// TBD - /// TBD + /// The settings to use to create the actor + /// A new instance to create this actor public static Props Props(DistributedPubSubSettings settings) { return Actor.Props.Create(() => new DistributedPubSubMediator(settings)).WithDeploy(Deploy.Local); @@ -137,7 +137,7 @@ public static Props Props(DistributedPubSubSettings settings) public ITimerScheduler Timers { get; set; } /// - /// TBD + /// Transforms the local bucket registry dictionary into a dictionary of topic key and version number pairs /// public IImmutableDictionary OwnVersions => _registry @@ -145,11 +145,15 @@ public IImmutableDictionary OwnVersions .ToImmutableDictionary(kv => kv.Key, kv => kv.Value); /// - /// TBD + /// Creates a new instance of actor. + /// Use instead. /// - /// TBD - /// TBD - /// TBD + /// The settings to use to create the actor + /// + /// Thrown when: + /// * Settings uses as routing logic + /// * Settings uses a role not listed inside the local cluster settings. + /// public DistributedPubSubMediator(DistributedPubSubSettings settings) { if (settings.RoutingLogic is ConsistentHashingRoutingLogic) @@ -729,9 +733,6 @@ private static Address SelectRandomNode(IList
addresses) return addresses[ThreadLocalRandom.Current.Next(addresses.Count)]; } - /// - /// TBD - /// protected override void PreStart() { base.PreStart(); @@ -744,9 +745,6 @@ protected override void PreStart() Timers.StartPeriodicTimer(PruneBufferTimerKey, PruneBufferTick.Instance, _bufferedMessageTimeoutCheckInterval, _bufferedMessageTimeoutCheckInterval, Self); } - /// - /// TBD - /// protected override void PostStop() { Timers.CancelAll(); diff --git a/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/Internal/TopicMessages.cs b/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/Internal/TopicMessages.cs index 7de8496244f..00b33402c36 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/Internal/TopicMessages.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/Internal/TopicMessages.cs @@ -328,6 +328,7 @@ internal sealed record NewBucketKeysAdded(IReadOnlyList Topics): IDeadLe ///
/// The original message being buffered /// The deadline where this buffered message should be timed out + /// The original sender of the message internal readonly record struct BufferedMessage(IWrappedMessage Message, Deadline Deadline, IActorRef Sender); internal sealed class PruneBufferTick: IDeadLetterSuppression diff --git a/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/Internal/Topics.cs b/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/Internal/Topics.cs index 49984efdfea..cf1cc3b1843 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/Internal/Topics.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/Internal/Topics.cs @@ -46,22 +46,27 @@ internal abstract class TopicLike : ActorBase, IWithTimers private const string PruneTimerKey = "PruneTimer"; /// - /// TBD + /// Timer interval to check to see if this actor needs to notify the + /// that this topic needs to be pruned. /// protected readonly TimeSpan PruneInterval; /// - /// TBD + /// Hash set of all local that subscribed to this topic /// protected readonly ISet Subscribers; /// - /// TBD + /// Delay before this actor notify the that the topic is empty and + /// it needs to be pruned. /// protected readonly TimeSpan EmptyTimeToLive; /// - /// TBD + /// The current prune deadline. + /// * Set when the last subscriber is downed or unsubscribed from this topic. + /// * Reset to null when a new subscriber arrived. + /// * Deadline checked regularly every interval. /// protected Deadline PruneDeadline = null; @@ -166,7 +171,11 @@ private bool DefaultReceive(object message) } } - /// + /// + /// Default message handler for both and + /// + /// The message we're going to process. + /// true if we handled it, false otherwise. protected abstract bool Business(object message); /// diff --git a/src/contrib/cluster/Akka.DistributedData/Durable/Messages.cs b/src/contrib/cluster/Akka.DistributedData/Durable/Messages.cs index e9c85bffa7e..4cae00df8f8 100644 --- a/src/contrib/cluster/Akka.DistributedData/Durable/Messages.cs +++ b/src/contrib/cluster/Akka.DistributedData/Durable/Messages.cs @@ -61,6 +61,7 @@ public sealed class LoadAll : IEquatable private LoadAll() { } public bool Equals(LoadAll other) => true; public override bool Equals(object obj) => obj is LoadAll; + public override int GetHashCode() => 761; } public sealed class LoadData @@ -79,6 +80,7 @@ public sealed class LoadAllCompleted : IEquatable private LoadAllCompleted() { } public bool Equals(LoadAllCompleted other) => true; public override bool Equals(object obj) => obj is LoadAllCompleted; + public override int GetHashCode() => 769; } public sealed class LoadFailedException : AkkaException diff --git a/src/contrib/cluster/Akka.DistributedData/ORDictionary.cs b/src/contrib/cluster/Akka.DistributedData/ORDictionary.cs index 781ac9f57ee..c213a9d66f2 100644 --- a/src/contrib/cluster/Akka.DistributedData/ORDictionary.cs +++ b/src/contrib/cluster/Akka.DistributedData/ORDictionary.cs @@ -478,6 +478,14 @@ public override bool Equals(object obj) public Type KeyType { get; } = typeof(TKey); public Type ValueType { get; } = typeof(TValue); + + public override int GetHashCode() + { + var hash = KeyType.GetHashCode(); + hash = (hash * 397) ^ ValueType.GetHashCode(); + hash = (hash * 397) ^ Underlying?.GetHashCode() ?? 0; + return hash; + } } internal sealed class PutDeltaOperation : AtomicDeltaOperation, ORDictionary.IPutDeltaOp @@ -488,9 +496,7 @@ internal sealed class PutDeltaOperation : AtomicDeltaOperation, ORDictionary.IPu public PutDeltaOperation(ORSet.IDeltaOperation underlying, TKey key, TValue value) { - if (underlying == null) throw new ArgumentNullException(nameof(underlying)); - - Underlying = underlying; + Underlying = underlying ?? throw new ArgumentNullException(nameof(underlying)); Key = key; Value = value; } diff --git a/src/contrib/cluster/Akka.DistributedData/ReplicatedData.cs b/src/contrib/cluster/Akka.DistributedData/ReplicatedData.cs index c989a232bdb..08cd92ed6c6 100644 --- a/src/contrib/cluster/Akka.DistributedData/ReplicatedData.cs +++ b/src/contrib/cluster/Akka.DistributedData/ReplicatedData.cs @@ -75,13 +75,13 @@ public interface IRemovedNodePruning : IRemovedNodePruning, IReplicatedData - T Prune(UniqueAddress removedNode, UniqueAddress collapseInto); + new T Prune(UniqueAddress removedNode, UniqueAddress collapseInto); /// /// Remove data entries from a node that has been removed from the cluster /// and already been pruned. /// - T PruningCleanup(UniqueAddress removedNode); + new T PruningCleanup(UniqueAddress removedNode); } /// @@ -130,7 +130,7 @@ public interface IDeltaReplicatedData : IDeltaReplicatedData, IReplic /// `modify` function shall still return the full state in the same way as /// without support for deltas. /// - TDelta Delta { get; } + new TDelta Delta { get; } /// /// When delta is merged into the full state this method is used. @@ -149,7 +149,7 @@ public interface IDeltaReplicatedData : IDeltaReplicatedData, IReplic /// has grabbed the it will invoke this method /// to get a clean data instance without the delta. /// - T ResetDelta(); + new T ResetDelta(); } /// diff --git a/src/contrib/cluster/Akka.DistributedData/Replicator.Messages.cs b/src/contrib/cluster/Akka.DistributedData/Replicator.Messages.cs index 26bb8ce9fa3..ed8dfc7dc6f 100644 --- a/src/contrib/cluster/Akka.DistributedData/Replicator.Messages.cs +++ b/src/contrib/cluster/Akka.DistributedData/Replicator.Messages.cs @@ -992,6 +992,8 @@ public override bool Equals(object obj) { return obj is FlushChanges; } + + public override int GetHashCode() => 809; } /// diff --git a/src/contrib/cluster/Akka.DistributedData/Replicator.cs b/src/contrib/cluster/Akka.DistributedData/Replicator.cs index 78c0720d61c..1fc40c852a6 100644 --- a/src/contrib/cluster/Akka.DistributedData/Replicator.cs +++ b/src/contrib/cluster/Akka.DistributedData/Replicator.cs @@ -220,7 +220,6 @@ namespace Akka.DistributedData /// A deleted key cannot be reused again, but it is still recommended to delete unused /// data entries because that reduces the replication overhead when new nodes join the cluster. /// Subsequent , and requests will be replied with . - /// Subscribers will receive . /// /// In the message you can pass an optional request context in the same way as for the /// message, described above. For example the original sender can be passed and replied diff --git a/src/contrib/cluster/Akka.DistributedData/VersionVector.cs b/src/contrib/cluster/Akka.DistributedData/VersionVector.cs index 7031ff36282..58e2552fe3f 100644 --- a/src/contrib/cluster/Akka.DistributedData/VersionVector.cs +++ b/src/contrib/cluster/Akka.DistributedData/VersionVector.cs @@ -11,6 +11,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Runtime.CompilerServices; using Akka.Cluster; using Akka.Util.Internal; @@ -105,6 +106,17 @@ public bool Equals(VersionVector other) public override bool Equals(object obj) => obj is VersionVector vector && Equals(vector); + public override int GetHashCode() + { + var hash = 373; + foreach (var (addr, ver) in InternalVersions) + { + hash = hash * 31 + addr.GetHashCode(); + hash = hash * 31 + ver.GetHashCode(); + } + return hash; + } + /// /// Returns true if this VersionVector has the same history /// as the VersionVector else false. @@ -162,14 +174,17 @@ private Ordering CompareOnlyTo(VersionVector other, Ordering order) { if (ReferenceEquals(this, other)) return Ordering.Same; - return Compare(InternalVersionEnumerator, other.InternalVersionEnumerator, - order == Ordering.Concurrent ? Ordering.FullOrder : order); + using var ie1 = InternalVersionEnumerator; + using var ie2 = other.InternalVersionEnumerator; + return Compare(ie1, ie2, order == Ordering.Concurrent ? Ordering.FullOrder : order); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static T NextOrElse(IEnumerator enumerator, T defaultValue) => enumerator.MoveNext() ? enumerator.Current : defaultValue; - private Ordering Compare(IEnumerator<(UniqueAddress addr, long version)> i1, + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Ordering Compare(IEnumerator<(UniqueAddress addr, long version)> i1, IEnumerator<(UniqueAddress addr, long version)> i2, Ordering requestedOrder) { var nt1 = NextOrElse(i1, EndMarker); diff --git a/src/contrib/cluster/Akka.DistributedData/WriteAggregator.cs b/src/contrib/cluster/Akka.DistributedData/WriteAggregator.cs index 14a2dacea22..15fee8dbbe3 100644 --- a/src/contrib/cluster/Akka.DistributedData/WriteAggregator.cs +++ b/src/contrib/cluster/Akka.DistributedData/WriteAggregator.cs @@ -193,6 +193,8 @@ public override bool Equals(object obj) return obj != null && obj is WriteLocal; } + public override int GetHashCode() => 811; + private WriteLocal() { } public override string ToString() => "WriteLocal"; diff --git a/src/contrib/persistence/Akka.Persistence.Query.InMemory/EventsByPersistenceIdPublisher.cs b/src/contrib/persistence/Akka.Persistence.Query.InMemory/EventsByPersistenceIdPublisher.cs index 5eaaaf8dfc6..cc4dc03b30d 100644 --- a/src/contrib/persistence/Akka.Persistence.Query.InMemory/EventsByPersistenceIdPublisher.cs +++ b/src/contrib/persistence/Akka.Persistence.Query.InMemory/EventsByPersistenceIdPublisher.cs @@ -204,7 +204,7 @@ public CurrentEventsByPersistenceIdPublisher(string persistenceId, long fromSequ { } - protected void ReceiveInitialRequest() + public void ReceiveInitialRequest() { Replay(); } diff --git a/src/contrib/testkits/Akka.TestKit.Xunit2/TestKit.cs b/src/contrib/testkits/Akka.TestKit.Xunit2/TestKit.cs index 6f1596e6710..4f73708548f 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit2/TestKit.cs +++ b/src/contrib/testkits/Akka.TestKit.Xunit2/TestKit.cs @@ -142,8 +142,12 @@ protected void InitializeLogger(ActorSystem system) { var extSystem = (ExtendedActorSystem)system; var logger = extSystem.SystemActorOf(Props.Create(() => new TestOutputLogger(Output)), "log-test"); - logger.Ask(new InitializeLogger(system.EventStream), TestKitSettings.TestKitStartupTimeout) - .ConfigureAwait(false).GetAwaiter().GetResult(); + // Start the logger initialization task but don't wait for it yet + var loggerTask = logger.Ask(new InitializeLogger(system.EventStream), TestKitSettings.TestKitStartupTimeout); + + // By the time TestActor is ready (which happens in base constructor), + // the logger is likely ready too. Now we can safely wait. + loggerTask.ConfigureAwait(false).GetAwaiter().GetResult(); } } @@ -154,8 +158,12 @@ protected void InitializeLogger(ActorSystem system, string prefix) var extSystem = (ExtendedActorSystem)system; var logger = extSystem.SystemActorOf(Props.Create(() => new TestOutputLogger( string.IsNullOrEmpty(prefix) ? Output : new PrefixedOutput(Output, prefix))), "log-test"); - logger.Ask(new InitializeLogger(system.EventStream), TestKitSettings.TestKitStartupTimeout) - .ConfigureAwait(false).GetAwaiter().GetResult(); + // Start the logger initialization task but don't wait for it yet + var loggerTask = logger.Ask(new InitializeLogger(system.EventStream), TestKitSettings.TestKitStartupTimeout); + + // By the time TestActor is ready (which happens in base constructor), + // the logger is likely ready too. Now we can safely wait. + loggerTask.ConfigureAwait(false).GetAwaiter().GetResult(); } } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterSharding.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterSharding.DotNet.verified.txt index 49bc3bd4627..0236d017068 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterSharding.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterSharding.DotNet.verified.txt @@ -78,6 +78,8 @@ namespace Akka.Cluster.Sharding public ClusterShardingSettings(string role, bool rememberEntities, string journalPluginId, string snapshotPluginId, System.TimeSpan passivateIdleEntityAfter, Akka.Cluster.Sharding.StateStoreMode stateStoreMode, Akka.Cluster.Sharding.TuningParameters tuningParameters, Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings coordinatorSingletonSettings) { } public ClusterShardingSettings(string role, bool rememberEntities, string journalPluginId, string snapshotPluginId, System.TimeSpan passivateIdleEntityAfter, Akka.Cluster.Sharding.StateStoreMode stateStoreMode, Akka.Cluster.Sharding.TuningParameters tuningParameters, Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings coordinatorSingletonSettings, Akka.Coordination.LeaseUsageSettings leaseSettings) { } public ClusterShardingSettings(string role, bool rememberEntities, string journalPluginId, string snapshotPluginId, System.TimeSpan passivateIdleEntityAfter, Akka.Cluster.Sharding.StateStoreMode stateStoreMode, Akka.Cluster.Sharding.RememberEntitiesStore rememberEntitiesStore, System.TimeSpan shardRegionQueryTimeout, Akka.Cluster.Sharding.TuningParameters tuningParameters, Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings coordinatorSingletonSettings, Akka.Coordination.LeaseUsageSettings leaseSettings) { } + [System.Runtime.CompilerServices.NullableAttribute(2)] + public Akka.Actor.SupervisorStrategy SupervisorStrategy { get; } public static Akka.Cluster.Sharding.ClusterShardingSettings Create(Akka.Actor.ActorSystem system) { } public static Akka.Cluster.Sharding.ClusterShardingSettings Create(Akka.Configuration.Config config, Akka.Configuration.Config singletonConfig) { } public Akka.Cluster.Sharding.ClusterShardingSettings WithCoordinatorSingletonSettings(Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings coordinatorSingletonSettings) { } @@ -88,6 +90,7 @@ namespace Akka.Cluster.Sharding public Akka.Cluster.Sharding.ClusterShardingSettings WithRole(string role) { } public Akka.Cluster.Sharding.ClusterShardingSettings WithSnapshotPluginId(string snapshotPluginId) { } public Akka.Cluster.Sharding.ClusterShardingSettings WithStateStoreMode(Akka.Cluster.Sharding.StateStoreMode mode) { } + public Akka.Cluster.Sharding.ClusterShardingSettings WithSupervisorStrategy(Akka.Actor.SupervisorStrategy supervisorStrategy) { } public Akka.Cluster.Sharding.ClusterShardingSettings WithTuningParameters(Akka.Cluster.Sharding.TuningParameters tuningParameters) { } } public sealed class ClusterShardingStats : Akka.Cluster.Sharding.IClusterShardingSerializable, System.IEquatable @@ -296,6 +299,18 @@ namespace Akka.Cluster.Sharding public override int GetHashCode() { } public override string ToString() { } } + public class ShardSupervisionStrategy : Akka.Actor.OneForOneStrategy + { + public ShardSupervisionStrategy(System.Nullable maxNrOfRetries, System.Nullable withinTimeRange, System.Func localOnlyDecider) { } + public ShardSupervisionStrategy(System.Nullable maxNrOfRetries, System.Nullable withinTimeRange, Akka.Actor.IDecider decider) { } + public ShardSupervisionStrategy(int maxNrOfRetries, int withinTimeMilliseconds, System.Func localOnlyDecider, bool loggingEnabled = True) { } + public ShardSupervisionStrategy(int maxNrOfRetries, int withinTimeMilliseconds, Akka.Actor.IDecider decider, bool loggingEnabled = True) { } + public ShardSupervisionStrategy(System.Func localOnlyDecider) { } + public ShardSupervisionStrategy(System.Func localOnlyDecider, bool loggingEnabled = True) { } + public ShardSupervisionStrategy(Akka.Actor.IDecider decider) { } + public ShardSupervisionStrategy() { } + public override void ProcessFailure(Akka.Actor.IActorContext context, bool restart, Akka.Actor.IActorRef child, System.Exception cause, Akka.Actor.Internal.ChildRestartStats stats, System.Collections.Generic.IReadOnlyCollection children) { } + } [Akka.Annotations.ApiMayChangeAttribute()] [Akka.Annotations.DoNotInheritAttribute()] [System.Runtime.CompilerServices.NullableAttribute(0)] diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterSharding.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterSharding.Net.verified.txt index 5570b269431..38f5d852dcc 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterSharding.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterSharding.Net.verified.txt @@ -78,6 +78,8 @@ namespace Akka.Cluster.Sharding public ClusterShardingSettings(string role, bool rememberEntities, string journalPluginId, string snapshotPluginId, System.TimeSpan passivateIdleEntityAfter, Akka.Cluster.Sharding.StateStoreMode stateStoreMode, Akka.Cluster.Sharding.TuningParameters tuningParameters, Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings coordinatorSingletonSettings) { } public ClusterShardingSettings(string role, bool rememberEntities, string journalPluginId, string snapshotPluginId, System.TimeSpan passivateIdleEntityAfter, Akka.Cluster.Sharding.StateStoreMode stateStoreMode, Akka.Cluster.Sharding.TuningParameters tuningParameters, Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings coordinatorSingletonSettings, Akka.Coordination.LeaseUsageSettings leaseSettings) { } public ClusterShardingSettings(string role, bool rememberEntities, string journalPluginId, string snapshotPluginId, System.TimeSpan passivateIdleEntityAfter, Akka.Cluster.Sharding.StateStoreMode stateStoreMode, Akka.Cluster.Sharding.RememberEntitiesStore rememberEntitiesStore, System.TimeSpan shardRegionQueryTimeout, Akka.Cluster.Sharding.TuningParameters tuningParameters, Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings coordinatorSingletonSettings, Akka.Coordination.LeaseUsageSettings leaseSettings) { } + [System.Runtime.CompilerServices.NullableAttribute(2)] + public Akka.Actor.SupervisorStrategy SupervisorStrategy { get; } public static Akka.Cluster.Sharding.ClusterShardingSettings Create(Akka.Actor.ActorSystem system) { } public static Akka.Cluster.Sharding.ClusterShardingSettings Create(Akka.Configuration.Config config, Akka.Configuration.Config singletonConfig) { } public Akka.Cluster.Sharding.ClusterShardingSettings WithCoordinatorSingletonSettings(Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings coordinatorSingletonSettings) { } @@ -88,6 +90,7 @@ namespace Akka.Cluster.Sharding public Akka.Cluster.Sharding.ClusterShardingSettings WithRole(string role) { } public Akka.Cluster.Sharding.ClusterShardingSettings WithSnapshotPluginId(string snapshotPluginId) { } public Akka.Cluster.Sharding.ClusterShardingSettings WithStateStoreMode(Akka.Cluster.Sharding.StateStoreMode mode) { } + public Akka.Cluster.Sharding.ClusterShardingSettings WithSupervisorStrategy(Akka.Actor.SupervisorStrategy supervisorStrategy) { } public Akka.Cluster.Sharding.ClusterShardingSettings WithTuningParameters(Akka.Cluster.Sharding.TuningParameters tuningParameters) { } } public sealed class ClusterShardingStats : Akka.Cluster.Sharding.IClusterShardingSerializable, System.IEquatable @@ -296,6 +299,18 @@ namespace Akka.Cluster.Sharding public override int GetHashCode() { } public override string ToString() { } } + public class ShardSupervisionStrategy : Akka.Actor.OneForOneStrategy + { + public ShardSupervisionStrategy(System.Nullable maxNrOfRetries, System.Nullable withinTimeRange, System.Func localOnlyDecider) { } + public ShardSupervisionStrategy(System.Nullable maxNrOfRetries, System.Nullable withinTimeRange, Akka.Actor.IDecider decider) { } + public ShardSupervisionStrategy(int maxNrOfRetries, int withinTimeMilliseconds, System.Func localOnlyDecider, bool loggingEnabled = True) { } + public ShardSupervisionStrategy(int maxNrOfRetries, int withinTimeMilliseconds, Akka.Actor.IDecider decider, bool loggingEnabled = True) { } + public ShardSupervisionStrategy(System.Func localOnlyDecider) { } + public ShardSupervisionStrategy(System.Func localOnlyDecider, bool loggingEnabled = True) { } + public ShardSupervisionStrategy(Akka.Actor.IDecider decider) { } + public ShardSupervisionStrategy() { } + public override void ProcessFailure(Akka.Actor.IActorContext context, bool restart, Akka.Actor.IActorRef child, System.Exception cause, Akka.Actor.Internal.ChildRestartStats stats, System.Collections.Generic.IReadOnlyCollection children) { } + } [Akka.Annotations.ApiMayChangeAttribute()] [Akka.Annotations.DoNotInheritAttribute()] [System.Runtime.CompilerServices.NullableAttribute(0)] diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.DotNet.verified.txt index 8c8ff8a9041..e87ea8710a2 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.DotNet.verified.txt @@ -281,6 +281,7 @@ namespace Akka.Cluster.Tools.PublishSubscribe public static Akka.Cluster.Tools.PublishSubscribe.GetTopics Instance { get; } } public interface IDistributedPubSubMessage { } + public interface IPublishResponse { } public sealed class Publish : Akka.Actor.IWrappedMessage, Akka.Cluster.Tools.PublishSubscribe.IDistributedPubSubMessage, System.IEquatable { public Publish(string topic, object message, bool sendOneMessageToEachGroup = False) { } @@ -297,13 +298,13 @@ namespace Akka.Cluster.Tools.PublishSubscribe Timeout = 0, MediatorShuttingDown = 1, } - public sealed class PublishFailed : Akka.Event.IDeadLetterSuppression, System.IEquatable + public sealed class PublishFailed : Akka.Cluster.Tools.PublishSubscribe.IPublishResponse, Akka.Event.IDeadLetterSuppression, System.IEquatable { public PublishFailed(Akka.Cluster.Tools.PublishSubscribe.PublishWithAck Message, Akka.Cluster.Tools.PublishSubscribe.PublishFailReason Reason) { } public Akka.Cluster.Tools.PublishSubscribe.PublishWithAck Message { get; set; } public Akka.Cluster.Tools.PublishSubscribe.PublishFailReason Reason { get; set; } } - public sealed class PublishSucceeded : Akka.Event.IDeadLetterSuppression, System.IEquatable + public sealed class PublishSucceeded : Akka.Cluster.Tools.PublishSubscribe.IPublishResponse, Akka.Event.IDeadLetterSuppression, System.IEquatable { public PublishSucceeded(Akka.Cluster.Tools.PublishSubscribe.PublishWithAck Message) { } public Akka.Cluster.Tools.PublishSubscribe.PublishWithAck Message { get; set; } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.Net.verified.txt index 744cb971ab7..d2660fe7688 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.Net.verified.txt @@ -281,6 +281,7 @@ namespace Akka.Cluster.Tools.PublishSubscribe public static Akka.Cluster.Tools.PublishSubscribe.GetTopics Instance { get; } } public interface IDistributedPubSubMessage { } + public interface IPublishResponse { } public sealed class Publish : Akka.Actor.IWrappedMessage, Akka.Cluster.Tools.PublishSubscribe.IDistributedPubSubMessage, System.IEquatable { public Publish(string topic, object message, bool sendOneMessageToEachGroup = False) { } @@ -297,13 +298,13 @@ namespace Akka.Cluster.Tools.PublishSubscribe Timeout = 0, MediatorShuttingDown = 1, } - public sealed class PublishFailed : Akka.Event.IDeadLetterSuppression, System.IEquatable + public sealed class PublishFailed : Akka.Cluster.Tools.PublishSubscribe.IPublishResponse, Akka.Event.IDeadLetterSuppression, System.IEquatable { public PublishFailed(Akka.Cluster.Tools.PublishSubscribe.PublishWithAck Message, Akka.Cluster.Tools.PublishSubscribe.PublishFailReason Reason) { } public Akka.Cluster.Tools.PublishSubscribe.PublishWithAck Message { get; set; } public Akka.Cluster.Tools.PublishSubscribe.PublishFailReason Reason { get; set; } } - public sealed class PublishSucceeded : Akka.Event.IDeadLetterSuppression, System.IEquatable + public sealed class PublishSucceeded : Akka.Cluster.Tools.PublishSubscribe.IPublishResponse, Akka.Event.IDeadLetterSuppression, System.IEquatable { public PublishSucceeded(Akka.Cluster.Tools.PublishSubscribe.PublishWithAck Message) { } public Akka.Cluster.Tools.PublishSubscribe.PublishWithAck Message { get; set; } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt index 36bf7bcfab8..7d8f3de5a78 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt @@ -579,30 +579,37 @@ namespace Akka.Actor public class ActorSystemTerminateReason : Akka.Actor.CoordinatedShutdown.Reason { public static readonly Akka.Actor.CoordinatedShutdown.Reason Instance; + public override int ExitCode { get; } } public class ClrExitReason : Akka.Actor.CoordinatedShutdown.Reason { public static readonly Akka.Actor.CoordinatedShutdown.Reason Instance; + public override int ExitCode { get; } } public class ClusterDowningReason : Akka.Actor.CoordinatedShutdown.Reason { public static readonly Akka.Actor.CoordinatedShutdown.Reason Instance; + public override int ExitCode { get; } } public class ClusterJoinUnsuccessfulReason : Akka.Actor.CoordinatedShutdown.Reason { public static readonly Akka.Actor.CoordinatedShutdown.Reason Instance; + public override int ExitCode { get; } } public class ClusterLeavingReason : Akka.Actor.CoordinatedShutdown.Reason { public static readonly Akka.Actor.CoordinatedShutdown.Reason Instance; + public override int ExitCode { get; } } - public class Reason + public abstract class Reason { protected Reason() { } + public abstract int ExitCode { get; } } public class UnknownReason : Akka.Actor.CoordinatedShutdown.Reason { public static readonly Akka.Actor.CoordinatedShutdown.Reason Instance; + public override int ExitCode { get; } } } public sealed class CoordinatedShutdownExtension : Akka.Actor.ExtensionIdProvider @@ -907,14 +914,14 @@ namespace Akka.Actor } public delegate void TransitionHandler(TState initialState, TState nextState); } - [System.ObsoleteAttribute("Use Akka.Actor.Status.Failure")] + [System.ObsoleteAttribute("Use Akka.Actor.Status.Failure. Will be removed in v1.6")] public class Failure { public Failure() { } public System.Exception Exception { get; set; } public System.DateTime Timestamp { get; set; } } - [System.ObsoleteAttribute("Use List of Akka.Actor.Status.Failure")] + [System.ObsoleteAttribute("Use List of Akka.Actor.Status.Failure. Will be removed in v1.6")] public class Failures { public Failures() { } @@ -2547,6 +2554,10 @@ namespace Akka.Delivery public static Akka.Actor.Props Create<[System.Runtime.CompilerServices.NullableAttribute(2)] T>(Akka.Actor.IActorRefFactory actorRefFactory, [System.Runtime.CompilerServices.NullableAttribute(new byte[] { 0, 1})] Akka.Util.Option producerControllerReference, [System.Runtime.CompilerServices.NullableAttribute(2)] Akka.Delivery.ConsumerController.Settings settings = null) { } + [Akka.Annotations.InternalApiAttribute()] + public static Akka.Actor.Props CreateWithFuzzing<[System.Runtime.CompilerServices.NullableAttribute(2)] T>(Akka.Actor.IActorRefFactory actorRefFactory, [System.Runtime.CompilerServices.NullableAttribute(new byte[] { + 0, + 1})] Akka.Util.Option producerControllerReference, System.Func fuzzing, [System.Runtime.CompilerServices.NullableAttribute(2)] Akka.Delivery.ConsumerController.Settings settings = null) { } public sealed class Confirmed { [System.Runtime.CompilerServices.NullableAttribute(1)] @@ -2721,6 +2732,13 @@ namespace Akka.Delivery 2, 1, 1})] System.Action> sendAdapter = null) { } + [Akka.Annotations.InternalApiAttribute()] + public static Akka.Actor.Props CreateWithFuzzing<[System.Runtime.CompilerServices.NullableAttribute(2)] T>(Akka.Actor.IActorRefFactory actorRefFactory, string producerId, System.Func fuzzing, [System.Runtime.CompilerServices.NullableAttribute(new byte[] { + 0, + 1})] Akka.Util.Option durableProducerQueue, [System.Runtime.CompilerServices.NullableAttribute(2)] Akka.Delivery.ProducerController.Settings settings = null, [System.Runtime.CompilerServices.NullableAttribute(new byte[] { + 2, + 1, + 1})] System.Action> sendAdapter = null) { } public interface IProducerCommand { } [System.Runtime.CompilerServices.NullableAttribute(0)] public sealed class MessageWithConfirmation<[System.Runtime.CompilerServices.NullableAttribute(2)] T> : Akka.Delivery.ProducerController.IProducerCommand diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt index 7c2892fc6ce..02e6be3fb03 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt @@ -579,30 +579,37 @@ namespace Akka.Actor public class ActorSystemTerminateReason : Akka.Actor.CoordinatedShutdown.Reason { public static readonly Akka.Actor.CoordinatedShutdown.Reason Instance; + public override int ExitCode { get; } } public class ClrExitReason : Akka.Actor.CoordinatedShutdown.Reason { public static readonly Akka.Actor.CoordinatedShutdown.Reason Instance; + public override int ExitCode { get; } } public class ClusterDowningReason : Akka.Actor.CoordinatedShutdown.Reason { public static readonly Akka.Actor.CoordinatedShutdown.Reason Instance; + public override int ExitCode { get; } } public class ClusterJoinUnsuccessfulReason : Akka.Actor.CoordinatedShutdown.Reason { public static readonly Akka.Actor.CoordinatedShutdown.Reason Instance; + public override int ExitCode { get; } } public class ClusterLeavingReason : Akka.Actor.CoordinatedShutdown.Reason { public static readonly Akka.Actor.CoordinatedShutdown.Reason Instance; + public override int ExitCode { get; } } - public class Reason + public abstract class Reason { protected Reason() { } + public abstract int ExitCode { get; } } public class UnknownReason : Akka.Actor.CoordinatedShutdown.Reason { public static readonly Akka.Actor.CoordinatedShutdown.Reason Instance; + public override int ExitCode { get; } } } public sealed class CoordinatedShutdownExtension : Akka.Actor.ExtensionIdProvider @@ -905,14 +912,14 @@ namespace Akka.Actor } public delegate void TransitionHandler(TState initialState, TState nextState); } - [System.ObsoleteAttribute("Use Akka.Actor.Status.Failure")] + [System.ObsoleteAttribute("Use Akka.Actor.Status.Failure. Will be removed in v1.6")] public class Failure { public Failure() { } public System.Exception Exception { get; set; } public System.DateTime Timestamp { get; set; } } - [System.ObsoleteAttribute("Use List of Akka.Actor.Status.Failure")] + [System.ObsoleteAttribute("Use List of Akka.Actor.Status.Failure. Will be removed in v1.6")] public class Failures { public Failures() { } @@ -2544,6 +2551,10 @@ namespace Akka.Delivery public static Akka.Actor.Props Create<[System.Runtime.CompilerServices.NullableAttribute(2)] T>(Akka.Actor.IActorRefFactory actorRefFactory, [System.Runtime.CompilerServices.NullableAttribute(new byte[] { 0, 1})] Akka.Util.Option producerControllerReference, [System.Runtime.CompilerServices.NullableAttribute(2)] Akka.Delivery.ConsumerController.Settings settings = null) { } + [Akka.Annotations.InternalApiAttribute()] + public static Akka.Actor.Props CreateWithFuzzing<[System.Runtime.CompilerServices.NullableAttribute(2)] T>(Akka.Actor.IActorRefFactory actorRefFactory, [System.Runtime.CompilerServices.NullableAttribute(new byte[] { + 0, + 1})] Akka.Util.Option producerControllerReference, System.Func fuzzing, [System.Runtime.CompilerServices.NullableAttribute(2)] Akka.Delivery.ConsumerController.Settings settings = null) { } public sealed class Confirmed { [System.Runtime.CompilerServices.NullableAttribute(1)] @@ -2717,6 +2728,13 @@ namespace Akka.Delivery 2, 1, 1})] System.Action> sendAdapter = null) { } + [Akka.Annotations.InternalApiAttribute()] + public static Akka.Actor.Props CreateWithFuzzing<[System.Runtime.CompilerServices.NullableAttribute(2)] T>(Akka.Actor.IActorRefFactory actorRefFactory, string producerId, System.Func fuzzing, [System.Runtime.CompilerServices.NullableAttribute(new byte[] { + 0, + 1})] Akka.Util.Option durableProducerQueue, [System.Runtime.CompilerServices.NullableAttribute(2)] Akka.Delivery.ProducerController.Settings settings = null, [System.Runtime.CompilerServices.NullableAttribute(new byte[] { + 2, + 1, + 1})] System.Action> sendAdapter = null) { } public interface IProducerCommand { } [System.Runtime.CompilerServices.NullableAttribute(0)] public sealed class MessageWithConfirmation<[System.Runtime.CompilerServices.NullableAttribute(2)] T> : Akka.Delivery.ProducerController.IProducerCommand diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveDistributedData.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveDistributedData.DotNet.verified.txt index 8dc6d1da49f..caedb74c375 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveDistributedData.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveDistributedData.DotNet.verified.txt @@ -155,6 +155,7 @@ namespace Akka.DistributedData { public static readonly Akka.DistributedData.FlushChanges Instance; public override bool Equals(object obj) { } + public override int GetHashCode() { } } public sealed class GCounter : Akka.DistributedData.FastMerge, Akka.DistributedData.IDeltaReplicatedData, Akka.DistributedData.IDeltaReplicatedData, Akka.DistributedData.IRemovedNodePruning, Akka.DistributedData.IRemovedNodePruning, Akka.DistributedData.IReplicatedData, Akka.DistributedData.IReplicatedDataSerialization, Akka.DistributedData.IReplicatedData, Akka.DistributedData.IReplicatedDelta, System.IEquatable { @@ -910,6 +911,7 @@ namespace Akka.DistributedData public static Akka.DistributedData.VersionVector Create(System.Collections.Immutable.ImmutableDictionary versions) { } public bool Equals(Akka.DistributedData.VersionVector other) { } public override bool Equals(object obj) { } + public override int GetHashCode() { } public abstract Akka.DistributedData.VersionVector Increment(Akka.Cluster.UniqueAddress node); public bool IsAfter(Akka.DistributedData.VersionVector y) { } public bool IsBefore(Akka.DistributedData.VersionVector y) { } @@ -948,6 +950,7 @@ namespace Akka.DistributedData public static readonly Akka.DistributedData.WriteLocal Instance; public System.TimeSpan Timeout { get; } public override bool Equals(object obj) { } + public override int GetHashCode() { } public override string ToString() { } } public sealed class WriteMajority : Akka.DistributedData.IWriteConsistency, System.IEquatable @@ -998,12 +1001,14 @@ namespace Akka.DistributedData.Durable public static readonly Akka.DistributedData.Durable.LoadAll Instance; public bool Equals(Akka.DistributedData.Durable.LoadAll other) { } public override bool Equals(object obj) { } + public override int GetHashCode() { } } public sealed class LoadAllCompleted : System.IEquatable { public static readonly Akka.DistributedData.Durable.LoadAllCompleted Instance; public bool Equals(Akka.DistributedData.Durable.LoadAllCompleted other) { } public override bool Equals(object obj) { } + public override int GetHashCode() { } } public sealed class LoadData { diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveDistributedData.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveDistributedData.Net.verified.txt index 6e25bdca888..7af1e8e83cb 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveDistributedData.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveDistributedData.Net.verified.txt @@ -155,6 +155,7 @@ namespace Akka.DistributedData { public static readonly Akka.DistributedData.FlushChanges Instance; public override bool Equals(object obj) { } + public override int GetHashCode() { } } public sealed class GCounter : Akka.DistributedData.FastMerge, Akka.DistributedData.IDeltaReplicatedData, Akka.DistributedData.IDeltaReplicatedData, Akka.DistributedData.IRemovedNodePruning, Akka.DistributedData.IRemovedNodePruning, Akka.DistributedData.IReplicatedData, Akka.DistributedData.IReplicatedDataSerialization, Akka.DistributedData.IReplicatedData, Akka.DistributedData.IReplicatedDelta, System.IEquatable { @@ -910,6 +911,7 @@ namespace Akka.DistributedData public static Akka.DistributedData.VersionVector Create(System.Collections.Immutable.ImmutableDictionary versions) { } public bool Equals(Akka.DistributedData.VersionVector other) { } public override bool Equals(object obj) { } + public override int GetHashCode() { } public abstract Akka.DistributedData.VersionVector Increment(Akka.Cluster.UniqueAddress node); public bool IsAfter(Akka.DistributedData.VersionVector y) { } public bool IsBefore(Akka.DistributedData.VersionVector y) { } @@ -948,6 +950,7 @@ namespace Akka.DistributedData public static readonly Akka.DistributedData.WriteLocal Instance; public System.TimeSpan Timeout { get; } public override bool Equals(object obj) { } + public override int GetHashCode() { } public override string ToString() { } } public sealed class WriteMajority : Akka.DistributedData.IWriteConsistency, System.IEquatable @@ -998,12 +1001,14 @@ namespace Akka.DistributedData.Durable public static readonly Akka.DistributedData.Durable.LoadAll Instance; public bool Equals(Akka.DistributedData.Durable.LoadAll other) { } public override bool Equals(object obj) { } + public override int GetHashCode() { } } public sealed class LoadAllCompleted : System.IEquatable { public static readonly Akka.DistributedData.Durable.LoadAllCompleted Instance; public bool Equals(Akka.DistributedData.Durable.LoadAllCompleted other) { } public override bool Equals(object obj) { } + public override int GetHashCode() { } } public sealed class LoadData { diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistence.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistence.DotNet.verified.txt index d56d0a2edd9..b6a437a6d2b 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistence.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistence.DotNet.verified.txt @@ -866,11 +866,12 @@ namespace Akka.Persistence.Journal protected static System.Exception TryUnwrapException(System.Exception e) { } protected abstract System.Threading.Tasks.Task> WriteMessagesAsync(System.Collections.Generic.IEnumerable messages, System.Threading.CancellationToken cancellationToken); } - public abstract class AsyncWriteProxy : Akka.Persistence.Journal.AsyncWriteJournal, Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Actor.IWithUnrestrictedStash, Akka.Dispatch.IRequiresMessageQueue + public abstract class AsyncWriteProxy : Akka.Persistence.Journal.AsyncWriteJournal, Akka.Actor.IActorStash, Akka.Actor.IWithTimers, Akka.Actor.IWithUnboundedStash, Akka.Actor.IWithUnrestrictedStash, Akka.Dispatch.IRequiresMessageQueue { protected AsyncWriteProxy() { } public Akka.Actor.IStash Stash { get; set; } public abstract System.TimeSpan Timeout { get; } + public Akka.Actor.ITimerScheduler Timers { get; set; } public override void AroundPreStart() { } protected override bool AroundReceive(Akka.Actor.Receive receive, object message) { } protected override System.Threading.Tasks.Task DeleteMessagesToAsync(string persistenceId, long toSequenceNr, System.Threading.CancellationToken cancellationToken) { } @@ -1063,10 +1064,11 @@ namespace Akka.Persistence.Journal public Akka.Actor.IActorRef ReplyTo { get; } } } - public class PersistencePluginProxy : Akka.Actor.ActorBase, Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Actor.IWithUnrestrictedStash, Akka.Dispatch.IRequiresMessageQueue + public class PersistencePluginProxy : Akka.Actor.ActorBase, Akka.Actor.IActorStash, Akka.Actor.IWithTimers, Akka.Actor.IWithUnboundedStash, Akka.Actor.IWithUnrestrictedStash, Akka.Dispatch.IRequiresMessageQueue { public PersistencePluginProxy(Akka.Configuration.Config config) { } public Akka.Actor.IStash Stash { get; set; } + public Akka.Actor.ITimerScheduler Timers { get; set; } protected override void PreStart() { } protected override bool Receive(object message) { } public static void SetTargetLocation(Akka.Actor.ActorSystem system, Akka.Actor.Address address) { } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistence.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistence.Net.verified.txt index a136e19d3d9..5b8a811b2bb 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistence.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistence.Net.verified.txt @@ -866,11 +866,12 @@ namespace Akka.Persistence.Journal protected static System.Exception TryUnwrapException(System.Exception e) { } protected abstract System.Threading.Tasks.Task> WriteMessagesAsync(System.Collections.Generic.IEnumerable messages, System.Threading.CancellationToken cancellationToken); } - public abstract class AsyncWriteProxy : Akka.Persistence.Journal.AsyncWriteJournal, Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Actor.IWithUnrestrictedStash, Akka.Dispatch.IRequiresMessageQueue + public abstract class AsyncWriteProxy : Akka.Persistence.Journal.AsyncWriteJournal, Akka.Actor.IActorStash, Akka.Actor.IWithTimers, Akka.Actor.IWithUnboundedStash, Akka.Actor.IWithUnrestrictedStash, Akka.Dispatch.IRequiresMessageQueue { protected AsyncWriteProxy() { } public Akka.Actor.IStash Stash { get; set; } public abstract System.TimeSpan Timeout { get; } + public Akka.Actor.ITimerScheduler Timers { get; set; } public override void AroundPreStart() { } protected override bool AroundReceive(Akka.Actor.Receive receive, object message) { } protected override System.Threading.Tasks.Task DeleteMessagesToAsync(string persistenceId, long toSequenceNr, System.Threading.CancellationToken cancellationToken) { } @@ -1063,10 +1064,11 @@ namespace Akka.Persistence.Journal public Akka.Actor.IActorRef ReplyTo { get; } } } - public class PersistencePluginProxy : Akka.Actor.ActorBase, Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Actor.IWithUnrestrictedStash, Akka.Dispatch.IRequiresMessageQueue + public class PersistencePluginProxy : Akka.Actor.ActorBase, Akka.Actor.IActorStash, Akka.Actor.IWithTimers, Akka.Actor.IWithUnboundedStash, Akka.Actor.IWithUnrestrictedStash, Akka.Dispatch.IRequiresMessageQueue { public PersistencePluginProxy(Akka.Configuration.Config config) { } public Akka.Actor.IStash Stash { get; set; } + public Akka.Actor.ITimerScheduler Timers { get; set; } protected override void PreStart() { } protected override bool Receive(object message) { } public static void SetTargetLocation(Akka.Actor.ActorSystem system, Akka.Actor.Address address) { } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistenceQuery.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistenceQuery.DotNet.verified.txt index fc98e247614..84c81eb1408 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistenceQuery.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistenceQuery.DotNet.verified.txt @@ -81,8 +81,10 @@ namespace Akka.Persistence.Query public Akka.Event.ILoggingAdapter Log { get; } public static Akka.Persistence.Query.PersistenceQuery Get(Akka.Actor.ActorSystem system) { } public static Akka.Configuration.Config GetDefaultConfig() { } + public static Akka.Configuration.Config GetDefaultConfig(System.Type journalType) { } public TJournal ReadJournalFor(string readJournalPluginId) where TJournal : Akka.Persistence.Query.IReadJournal { } + public Akka.Persistence.Query.IReadJournal ReadJournalFor(System.Type readJournalType, string readJournalPluginId) { } } public class static PersistenceQueryExtensions { diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistenceQuery.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistenceQuery.Net.verified.txt index e598732f711..b5df93f1d03 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistenceQuery.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApprovePersistenceQuery.Net.verified.txt @@ -81,8 +81,10 @@ namespace Akka.Persistence.Query public Akka.Event.ILoggingAdapter Log { get; } public static Akka.Persistence.Query.PersistenceQuery Get(Akka.Actor.ActorSystem system) { } public static Akka.Configuration.Config GetDefaultConfig() { } + public static Akka.Configuration.Config GetDefaultConfig(System.Type journalType) { } public TJournal ReadJournalFor(string readJournalPluginId) where TJournal : Akka.Persistence.Query.IReadJournal { } + public Akka.Persistence.Query.IReadJournal ReadJournalFor(System.Type readJournalType, string readJournalPluginId) { } } public class static PersistenceQueryExtensions { diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveStreams.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveStreams.DotNet.verified.txt index e993e240eb2..df631b1ce0c 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveStreams.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveStreams.DotNet.verified.txt @@ -145,6 +145,7 @@ namespace Akka.Streams public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferSize, string dispatcher, Akka.Streams.Supervision.Decider supervisionDecider, Akka.Streams.StreamSubscriptionTimeoutSettings subscriptionTimeoutSettings, Akka.Streams.Dsl.StreamRefSettings streamRefSettings, bool isDebugLogging, int outputBurstLimit, bool isFuzzingMode, bool isAutoFusing, int maxFixedBufferSize, int syncProcessingLimit = 1000) { } public static Akka.Streams.ActorMaterializerSettings Create(Akka.Actor.ActorSystem system) { } public override bool Equals(object obj) { } + public override int GetHashCode() { } public Akka.Streams.ActorMaterializerSettings WithAutoFusing(bool isAutoFusing) { } public Akka.Streams.ActorMaterializerSettings WithDebugLogging(bool isEnabled) { } public Akka.Streams.ActorMaterializerSettings WithDispatcher(string dispatcher) { } @@ -172,7 +173,7 @@ namespace Akka.Streams public System.Collections.Generic.IEnumerable AttributeList { get; } public Akka.Streams.Attributes And(Akka.Streams.Attributes other) { } public Akka.Streams.Attributes And(Akka.Streams.Attributes.IAttribute other) { } - [System.ObsoleteAttribute("Use GetAttribute() instead")] + [System.ObsoleteAttribute("Use Contains() instead")] public bool Contains(TAttr attribute) where TAttr : Akka.Streams.Attributes.IAttribute { } public bool Contains() @@ -206,6 +207,7 @@ namespace Akka.Streams public static readonly Akka.Streams.Attributes.AsyncBoundary Instance; public bool Equals(Akka.Streams.Attributes.AsyncBoundary other) { } public override bool Equals(object obj) { } + public override int GetHashCode() { } public override string ToString() { } } public sealed class CancellationStrategy : Akka.Streams.Attributes.IAttribute, Akka.Streams.Attributes.IMandatoryAttribute @@ -1471,8 +1473,16 @@ namespace Akka.Streams.Dsl } public class static FlowWithContextOperations { + [System.ObsoleteAttribute("Deprecated. Please use Collect(FlowWithContext, Func, Func) instead. Since v1.5.4" + + "4, will be removed in v1.6")] public static Akka.Streams.Dsl.FlowWithContext Collect(this Akka.Streams.Dsl.FlowWithContext fn) where TOut2 : class { } + public static Akka.Streams.Dsl.FlowWithContext Collect(this Akka.Streams.Dsl.FlowWithContext, bool> isDefined, System.Func fn) + where TOut2 : class { } public static Akka.Streams.Dsl.FlowWithContext, System.Collections.Generic.IReadOnlyList, TMat> Grouped(this Akka.Streams.Dsl.FlowWithContext Select(this Akka.Streams.Dsl.FlowWithContext fn) { } public static Akka.Streams.Dsl.FlowWithContext SelectAsync(this Akka.Streams.Dsl.FlowWithContext> fn) { } @@ -2172,8 +2182,16 @@ namespace Akka.Streams.Dsl } public class static SourceWithContextOperations { + [System.ObsoleteAttribute("Deprecated. Please use Collect(SourceWithContext, Func, Func) instead. Since v1.5" + + ".44, will be removed in v1.6")] public static Akka.Streams.Dsl.SourceWithContext Collect(this Akka.Streams.Dsl.SourceWithContext flow, System.Func fn) where TOut2 : class { } + public static Akka.Streams.Dsl.SourceWithContext Collect(this Akka.Streams.Dsl.SourceWithContext flow, [System.Runtime.CompilerServices.NullableAttribute(new byte[] { + 2, + 0, + 0, + 0})] System.Func, bool> isDefined, System.Func fn) + where TOut2 : class { } public static Akka.Streams.Dsl.SourceWithContext, System.Collections.Generic.IReadOnlyList, TMat> Grouped(this Akka.Streams.Dsl.SourceWithContext flow, int n) { } public static Akka.Streams.Dsl.SourceWithContext Select(this Akka.Streams.Dsl.SourceWithContext flow, System.Func fn) { } public static Akka.Streams.Dsl.SourceWithContext SelectAsync(this Akka.Streams.Dsl.SourceWithContext flow, int parallelism, System.Func> fn) { } @@ -2281,7 +2299,12 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.SubFlow Batch(this Akka.Streams.Dsl.SubFlow flow, long max, System.Func seed, System.Func aggregate) { } public static Akka.Streams.Dsl.SubFlow BatchWeighted(this Akka.Streams.Dsl.SubFlow flow, long max, System.Func costFunction, System.Func seed, System.Func aggregate) { } public static Akka.Streams.Dsl.SubFlow Buffer(this Akka.Streams.Dsl.SubFlow flow, int size, Akka.Streams.OverflowStrategy strategy) { } + [System.ObsoleteAttribute("Deprecated. Please use Collect(SubFlow, Func, Func) instead. Since v1.5.44, will " + + "be removed in v1.6")] public static Akka.Streams.Dsl.SubFlow Collect(this Akka.Streams.Dsl.SubFlow flow, System.Func collector) { } + public static Akka.Streams.Dsl.SubFlow Collect(this Akka.Streams.Dsl.SubFlow flow, [System.Runtime.CompilerServices.NullableAttribute(new byte[] { + 2, + 0})] System.Func isDefined, System.Func collector) { } public static Akka.Streams.Dsl.SubFlow CompletionTimeout(this Akka.Streams.Dsl.SubFlow flow, System.TimeSpan timeout) { } public static Akka.Streams.Dsl.SubFlow Concat(this Akka.Streams.Dsl.SubFlow flow, Akka.Streams.IGraph, TMat> other) { } public static Akka.Streams.Dsl.SubFlow ConcatMany(this Akka.Streams.Dsl.SubFlow flow, System.Func, TMat>> flatten) { } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveStreams.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveStreams.Net.verified.txt index badb6f52d94..32083672072 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveStreams.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveStreams.Net.verified.txt @@ -145,6 +145,7 @@ namespace Akka.Streams public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferSize, string dispatcher, Akka.Streams.Supervision.Decider supervisionDecider, Akka.Streams.StreamSubscriptionTimeoutSettings subscriptionTimeoutSettings, Akka.Streams.Dsl.StreamRefSettings streamRefSettings, bool isDebugLogging, int outputBurstLimit, bool isFuzzingMode, bool isAutoFusing, int maxFixedBufferSize, int syncProcessingLimit = 1000) { } public static Akka.Streams.ActorMaterializerSettings Create(Akka.Actor.ActorSystem system) { } public override bool Equals(object obj) { } + public override int GetHashCode() { } public Akka.Streams.ActorMaterializerSettings WithAutoFusing(bool isAutoFusing) { } public Akka.Streams.ActorMaterializerSettings WithDebugLogging(bool isEnabled) { } public Akka.Streams.ActorMaterializerSettings WithDispatcher(string dispatcher) { } @@ -172,7 +173,7 @@ namespace Akka.Streams public System.Collections.Generic.IEnumerable AttributeList { get; } public Akka.Streams.Attributes And(Akka.Streams.Attributes other) { } public Akka.Streams.Attributes And(Akka.Streams.Attributes.IAttribute other) { } - [System.ObsoleteAttribute("Use GetAttribute() instead")] + [System.ObsoleteAttribute("Use Contains() instead")] public bool Contains(TAttr attribute) where TAttr : Akka.Streams.Attributes.IAttribute { } public bool Contains() @@ -205,6 +206,7 @@ namespace Akka.Streams public static readonly Akka.Streams.Attributes.AsyncBoundary Instance; public bool Equals(Akka.Streams.Attributes.AsyncBoundary other) { } public override bool Equals(object obj) { } + public override int GetHashCode() { } public override string ToString() { } } public sealed class CancellationStrategy : Akka.Streams.Attributes.IAttribute, Akka.Streams.Attributes.IMandatoryAttribute @@ -1469,8 +1471,16 @@ namespace Akka.Streams.Dsl } public class static FlowWithContextOperations { + [System.ObsoleteAttribute("Deprecated. Please use Collect(FlowWithContext, Func, Func) instead. Since v1.5.4" + + "4, will be removed in v1.6")] public static Akka.Streams.Dsl.FlowWithContext Collect(this Akka.Streams.Dsl.FlowWithContext fn) where TOut2 : class { } + public static Akka.Streams.Dsl.FlowWithContext Collect(this Akka.Streams.Dsl.FlowWithContext, bool> isDefined, System.Func fn) + where TOut2 : class { } public static Akka.Streams.Dsl.FlowWithContext, System.Collections.Generic.IReadOnlyList, TMat> Grouped(this Akka.Streams.Dsl.FlowWithContext Select(this Akka.Streams.Dsl.FlowWithContext fn) { } public static Akka.Streams.Dsl.FlowWithContext SelectAsync(this Akka.Streams.Dsl.FlowWithContext> fn) { } @@ -2170,8 +2180,16 @@ namespace Akka.Streams.Dsl } public class static SourceWithContextOperations { + [System.ObsoleteAttribute("Deprecated. Please use Collect(SourceWithContext, Func, Func) instead. Since v1.5" + + ".44, will be removed in v1.6")] public static Akka.Streams.Dsl.SourceWithContext Collect(this Akka.Streams.Dsl.SourceWithContext flow, System.Func fn) where TOut2 : class { } + public static Akka.Streams.Dsl.SourceWithContext Collect(this Akka.Streams.Dsl.SourceWithContext flow, [System.Runtime.CompilerServices.NullableAttribute(new byte[] { + 2, + 0, + 0, + 0})] System.Func, bool> isDefined, System.Func fn) + where TOut2 : class { } public static Akka.Streams.Dsl.SourceWithContext, System.Collections.Generic.IReadOnlyList, TMat> Grouped(this Akka.Streams.Dsl.SourceWithContext flow, int n) { } public static Akka.Streams.Dsl.SourceWithContext Select(this Akka.Streams.Dsl.SourceWithContext flow, System.Func fn) { } public static Akka.Streams.Dsl.SourceWithContext SelectAsync(this Akka.Streams.Dsl.SourceWithContext flow, int parallelism, System.Func> fn) { } @@ -2279,7 +2297,12 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.SubFlow Batch(this Akka.Streams.Dsl.SubFlow flow, long max, System.Func seed, System.Func aggregate) { } public static Akka.Streams.Dsl.SubFlow BatchWeighted(this Akka.Streams.Dsl.SubFlow flow, long max, System.Func costFunction, System.Func seed, System.Func aggregate) { } public static Akka.Streams.Dsl.SubFlow Buffer(this Akka.Streams.Dsl.SubFlow flow, int size, Akka.Streams.OverflowStrategy strategy) { } + [System.ObsoleteAttribute("Deprecated. Please use Collect(SubFlow, Func, Func) instead. Since v1.5.44, will " + + "be removed in v1.6")] public static Akka.Streams.Dsl.SubFlow Collect(this Akka.Streams.Dsl.SubFlow flow, System.Func collector) { } + public static Akka.Streams.Dsl.SubFlow Collect(this Akka.Streams.Dsl.SubFlow flow, [System.Runtime.CompilerServices.NullableAttribute(new byte[] { + 2, + 0})] System.Func isDefined, System.Func collector) { } public static Akka.Streams.Dsl.SubFlow CompletionTimeout(this Akka.Streams.Dsl.SubFlow flow, System.TimeSpan timeout) { } public static Akka.Streams.Dsl.SubFlow Concat(this Akka.Streams.Dsl.SubFlow flow, Akka.Streams.IGraph, TMat> other) { } public static Akka.Streams.Dsl.SubFlow ConcatMany(this Akka.Streams.Dsl.SubFlow flow, System.Func, TMat>> flatten) { } diff --git a/src/core/Akka.Cluster.TestKit/MultiNodeClusterSpec.cs b/src/core/Akka.Cluster.TestKit/MultiNodeClusterSpec.cs index cf01e4aea72..86f3f54e116 100644 --- a/src/core/Akka.Cluster.TestKit/MultiNodeClusterSpec.cs +++ b/src/core/Akka.Cluster.TestKit/MultiNodeClusterSpec.cs @@ -11,6 +11,8 @@ using System.Collections.Immutable; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using Akka.Actor; using Akka.Actor.Setup; using Akka.Cluster.Tests.MultiNode; @@ -258,6 +260,20 @@ public void StartClusterNode() } } + /// + /// Use this method for the initial startup of the cluster node + /// + public async Task StartClusterNodeAsync(CancellationToken cancellationToken = default) + { + if (ClusterView.Members.IsEmpty) + { + // !!! NOTE: Do not convert this to JoinAsync() !!! + // ReSharper disable once MethodHasAsyncOverloadWithCancellation + Cluster.Join(GetAddress(Myself)); + await AwaitAssertAsync(() => Assert.Contains(GetAddress(Myself), ClusterView.Members.Select(m => m.Address)), cancellationToken: cancellationToken); + } + } + /// /// Initialize the cluster of the specified member nodes () /// and wait until all joined and . @@ -279,6 +295,28 @@ public void AwaitClusterUp(params RoleName[] roles) EnterBarrier(roles.Select(r => r.Name).Aggregate((a, b) => a + "-" + b) + "-joined"); } + /// + /// Initialize the cluster of the specified member nodes () + /// and wait until all joined and . + /// + /// First node will be started first and others will join the first. + /// + public async Task AwaitClusterUpAsync(CancellationToken cancellationToken, params RoleName[] roles) + { + // make sure that the node-to-join is started before other join + await RunOnAsync(async () => await StartClusterNodeAsync(cancellationToken), roles.First()); + + await EnterBarrierAsync(cancellationToken, roles.First().Name + "-started"); + if (roles.Skip(1).Contains(Myself)) + await Cluster.JoinAsync(GetAddress(roles.First()), cancellationToken); + + if (roles.Contains(Myself)) + { + await AwaitMembersUpAsync(roles.Length, cancellationToken: cancellationToken); + } + await EnterBarrierAsync(cancellationToken, roles.Select(r => r.Name).Aggregate((a, b) => a + "-" + b) + "-joined"); + } + public void JoinWithin(RoleName joinNode, TimeSpan? max = null, TimeSpan? interval = null) { if (max == null) max = RemainingOrDefault; @@ -380,11 +418,48 @@ public void AwaitMembersUp( }); } + public async Task AwaitMembersUpAsync( + int numbersOfMembers, + ImmutableHashSet
canNotBePartOfMemberRing = null, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default) + { + canNotBePartOfMemberRing ??= ImmutableHashSet.Create
(); + timeout ??= TimeSpan.FromSeconds(25); + + await WithinAsync(timeout.Value, async () => + { + if (canNotBePartOfMemberRing.Any()) // don't run this on an empty set + await AwaitAssertAsync(() => + { + foreach (var a in canNotBePartOfMemberRing) + _assertions.AssertFalse(ClusterView.Members.Select(m => m.Address).Contains(a)); + }, cancellationToken: cancellationToken); + await AwaitAssertAsync( + () => _assertions.AssertEqual(numbersOfMembers, ClusterView.Members.Count), + cancellationToken: cancellationToken); + await AwaitAssertAsync( + () => _assertions.AssertTrue(ClusterView.Members.All(m => m.Status == MemberStatus.Up), "All members should be up"), + cancellationToken: cancellationToken); + // clusterView.leader is updated by LeaderChanged, await that to be updated also + var firstMember = ClusterView.Members.FirstOrDefault(); + var expectedLeader = firstMember?.Address; + await AwaitAssertAsync( + () => _assertions.AssertEqual(expectedLeader, ClusterView.Leader), + cancellationToken: cancellationToken); + }, cancellationToken: cancellationToken); + } + public void AwaitAllReachable() { AwaitAssert(() => _assertions.AssertFalse(ClusterView.UnreachableMembers.Any())); } + public async Task AwaitAllReachableAsync() + { + await AwaitAssertAsync(() => _assertions.AssertFalse(ClusterView.UnreachableMembers.Any())); + } + public void AwaitSeenSameState(params Address[] addresses) { AwaitAssert(() => _assertions.AssertFalse(addresses.ToImmutableHashSet().Except(ClusterView.SeenBy).Any())); diff --git a/src/core/Akka.Cluster.Tests.MultiNode/ClusterAccrualFailureDetectorSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/ClusterAccrualFailureDetectorSpec.cs index 9e1a8c8853b..25130063ee1 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/ClusterAccrualFailureDetectorSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/ClusterAccrualFailureDetectorSpec.cs @@ -7,6 +7,7 @@ using System; using System.Threading; +using System.Threading.Tasks; using Akka.Cluster.TestKit; using Akka.Configuration; using Akka.Remote.TestKit; @@ -15,142 +16,135 @@ using Akka.Cluster.Tests.MultiNode; using Akka.MultiNode.TestAdapter; -namespace Akka.Cluster.Tests.MultiNode +namespace Akka.Cluster.Tests.MultiNode; + +public class ClusterAccrualFailureDetectorMultiSpec : MultiNodeConfig { + public RoleName First { get; } + + public RoleName Second { get; } + + public RoleName Third { get; } + + public ClusterAccrualFailureDetectorMultiSpec() + { + First = Role("first"); + Second = Role("second"); + Third = Role("third"); + + CommonConfig= DebugConfig(false) + .WithFallback(ConfigurationFactory.ParseString("akka.cluster.failure-detector.threshold = 4")) + .WithFallback(MultiNodeClusterSpec.ClusterConfig()); + + TestTransport = true; + } +} + +public class ClusterAccrualFailureDetectorSpec : MultiNodeClusterSpec { - public class ClusterAccrualFailureDetectorMultiSpec : MultiNodeConfig { - public RoleName First { get; private set; } + private readonly ClusterAccrualFailureDetectorMultiSpec _config; - public RoleName Second { get; private set; } + public ClusterAccrualFailureDetectorSpec() + : this(new ClusterAccrualFailureDetectorMultiSpec()) + { + MuteMarkingAsUnreachable(); + } - public RoleName Third { get; private set; } + protected ClusterAccrualFailureDetectorSpec(ClusterAccrualFailureDetectorMultiSpec config) + : base(config, typeof(ClusterAccrualFailureDetectorSpec)) + { + _config = config; + } - public ClusterAccrualFailureDetectorMultiSpec() - { - First = Role("first"); - Second = Role("second"); - Third = Role("third"); + [MultiNodeFact] + public async Task ClusterAccrualFailureDetectorSpecs() + { + await A_heartbeat_driven_Failure_Detector_receive_heartbeats_so_that_all_member_nodes_in_the_cluster_are_marked_available(); + await A_heartbeat_driven_Failure_Detector_mark_node_as_unavailable_when_network_partition_and_then_back_to_available_when_partition_is_healed(); + await A_heartbeat_driven_Failure_Detector_mark_node_as_unavailable_if_a_node_in_the_cluster_is_shut_down_and_its_heartbeats_stops(); + } - CommonConfig= DebugConfig(false) - .WithFallback(ConfigurationFactory.ParseString("akka.cluster.failure-detector.threshold = 4")) - .WithFallback(MultiNodeClusterSpec.ClusterConfig()); + public async Task A_heartbeat_driven_Failure_Detector_receive_heartbeats_so_that_all_member_nodes_in_the_cluster_are_marked_available() + { + await AwaitClusterUpAsync(CancellationToken.None, _config.First, _config.Second, _config.Third); + + await Task.Yield(); + + Cluster.FailureDetector.IsAvailable(GetAddress(_config.First)).ShouldBeTrue(); + Cluster.FailureDetector.IsAvailable(GetAddress(_config.Second)).ShouldBeTrue(); + Cluster.FailureDetector.IsAvailable(GetAddress(_config.Third)).ShouldBeTrue(); - TestTransport = true; - } + await EnterBarrierAsync("after-1"); } - public class ClusterAccrualFailureDetectorSpec : MultiNodeClusterSpec + public async Task A_heartbeat_driven_Failure_Detector_mark_node_as_unavailable_when_network_partition_and_then_back_to_available_when_partition_is_healed() { - private readonly ClusterAccrualFailureDetectorMultiSpec _config; + await RunOnAsync(async () => { + await TestConductor.BlackholeAsync(_config.First, _config.Second, ThrottleTransportAdapter.Direction.Both); + }, _config.First); - public ClusterAccrualFailureDetectorSpec() - : this(new ClusterAccrualFailureDetectorMultiSpec()) - { - MuteMarkingAsUnreachable(); - } + await EnterBarrierAsync("broken"); - protected ClusterAccrualFailureDetectorSpec(ClusterAccrualFailureDetectorMultiSpec config) - : base(config, typeof(ClusterAccrualFailureDetectorSpec)) + RunOn(() => { - _config = config; - } + // detect failure... + AwaitCondition(() => !Cluster.FailureDetector.IsAvailable(GetAddress(_config.Second)), + TimeSpan.FromSeconds(15)); + // other connections still ok + Cluster.FailureDetector.IsAvailable(GetAddress(_config.Third)).ShouldBeTrue(); + }, _config.First); - [MultiNodeFact] - public void ClusterAccrualFailureDetectorSpecs() - { - A_heartbeat_driven_Failure_Detector_receive_heartbeats_so_that_all_member_nodes_in_the_cluster_are_marked_available - (); - A_heartbeat_driven_Failure_Detector_mark_node_as_unavailable_when_network_partition_and_then_back_to_available_when_partition_is_healed - (); - A_heartbeat_driven_Failure_Detector_mark_node_as_unavailable_if_a_node_in_the_cluster_is_shut_down_and_its_heartbeats_stops - (); - } - - public void - A_heartbeat_driven_Failure_Detector_receive_heartbeats_so_that_all_member_nodes_in_the_cluster_are_marked_available - () + RunOn(() => { - AwaitClusterUp(_config.First, _config.Second, _config.Third); - - Thread.Sleep(5); - Cluster.FailureDetector.IsAvailable(GetAddress(_config.First)).ShouldBeTrue(); - Cluster.FailureDetector.IsAvailable(GetAddress(_config.Second)).ShouldBeTrue(); + // detect failure... + AwaitCondition(() => !Cluster.FailureDetector.IsAvailable(GetAddress(_config.First)), + TimeSpan.FromSeconds(15)); + // other connections still ok Cluster.FailureDetector.IsAvailable(GetAddress(_config.Third)).ShouldBeTrue(); + }, _config.Second); + + + await EnterBarrierAsync("partitioned"); + + await RunOnAsync(async () => { + await TestConductor.PassThroughAsync(_config.First, _config.Second, ThrottleTransportAdapter.Direction.Both); + }, _config.First); + + await EnterBarrierAsync("repaired"); - EnterBarrier("after-1"); - } + RunOn(() => + { + AwaitCondition(() => Cluster.FailureDetector.IsAvailable(GetAddress(_config.Second)), + TimeSpan.FromSeconds(15)); + }, _config.First, _config.Third); - public void - A_heartbeat_driven_Failure_Detector_mark_node_as_unavailable_when_network_partition_and_then_back_to_available_when_partition_is_healed - () + RunOn(() => { - RunOn(() => { - TestConductor.Blackhole(_config.First, _config.Second, ThrottleTransportAdapter.Direction.Both).Wait(); - }, _config.First); - - EnterBarrier("broken"); - - RunOn(() => - { - // detect failure... - AwaitCondition(() => !Cluster.FailureDetector.IsAvailable(GetAddress(_config.Second)), - TimeSpan.FromSeconds(15)); - // other connections still ok - Cluster.FailureDetector.IsAvailable(GetAddress(_config.Third)).ShouldBeTrue(); - }, _config.First); - - RunOn(() => - { - // detect failure... - AwaitCondition(() => !Cluster.FailureDetector.IsAvailable(GetAddress(_config.First)), - TimeSpan.FromSeconds(15)); - // other connections still ok - Cluster.FailureDetector.IsAvailable(GetAddress(_config.Third)).ShouldBeTrue(); - }, _config.Second); - - - EnterBarrier("partitioned"); - - RunOn(() => { - TestConductor.PassThrough(_config.First, _config.Second, ThrottleTransportAdapter.Direction.Both).Wait(); - }, _config.First); - - EnterBarrier("repaired"); - - RunOn(() => - { - AwaitCondition(() => Cluster.FailureDetector.IsAvailable(GetAddress(_config.Second)), - TimeSpan.FromSeconds(15)); - }, _config.First, _config.Third); - - RunOn(() => - { - AwaitCondition(() => Cluster.FailureDetector.IsAvailable(GetAddress(_config.First)), - TimeSpan.FromSeconds(15)); - }, _config.Second); - - EnterBarrier("after-2"); - } + AwaitCondition(() => Cluster.FailureDetector.IsAvailable(GetAddress(_config.First)), + TimeSpan.FromSeconds(15)); + }, _config.Second); + + await EnterBarrierAsync("after-2"); + } - public void - A_heartbeat_driven_Failure_Detector_mark_node_as_unavailable_if_a_node_in_the_cluster_is_shut_down_and_its_heartbeats_stops - () + public async Task + A_heartbeat_driven_Failure_Detector_mark_node_as_unavailable_if_a_node_in_the_cluster_is_shut_down_and_its_heartbeats_stops + () + { + await RunOnAsync(async () => { + await TestConductor.ExitAsync(_config.Third, 0); + }, _config.First); + + await EnterBarrierAsync("third-shutdown"); + + RunOn(() => { - RunOn(() => { - TestConductor.Exit(_config.Third, 0).Wait(); - }, _config.First); - - EnterBarrier("third-shutdown"); - - RunOn(() => - { - // remaining nodes should detect failure... - AwaitCondition(() => !Cluster.FailureDetector.IsAvailable(GetAddress(_config.Third)), TimeSpan.FromSeconds(15)); - // other connections still ok - Cluster.FailureDetector.IsAvailable(GetAddress(_config.First)).ShouldBeTrue(); - Cluster.FailureDetector.IsAvailable(GetAddress(_config.Second)).ShouldBeTrue(); - }, _config.First, _config.Second); - - EnterBarrier("after-3"); - } + // remaining nodes should detect failure... + AwaitCondition(() => !Cluster.FailureDetector.IsAvailable(GetAddress(_config.Third)), TimeSpan.FromSeconds(15)); + // other connections still ok + Cluster.FailureDetector.IsAvailable(GetAddress(_config.First)).ShouldBeTrue(); + Cluster.FailureDetector.IsAvailable(GetAddress(_config.Second)).ShouldBeTrue(); + }, _config.First, _config.Second); + + await EnterBarrierAsync("after-3"); } -} +} \ No newline at end of file diff --git a/src/core/Akka.Cluster.Tests.MultiNode/LeaderElectionSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/LeaderElectionSpec.cs index 7e19f47c2df..77fea1782ba 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/LeaderElectionSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/LeaderElectionSpec.cs @@ -8,167 +8,168 @@ using System; using System.Collections.Immutable; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Akka.Cluster.TestKit; using Akka.MultiNode.TestAdapter; using Akka.Remote.TestKit; using Akka.TestKit; -namespace Akka.Cluster.Tests.MultiNode +namespace Akka.Cluster.Tests.MultiNode; + +public class LeaderElectionSpecConfig : MultiNodeConfig { - public class LeaderElectionSpecConfig : MultiNodeConfig - { - public RoleName Controller { get; private set; } - public RoleName First { get; private set; } - public RoleName Second { get; private set; } - public RoleName Third { get; private set; } - public RoleName Forth { get; private set; } + public RoleName Controller { get; } + public RoleName First { get; } + public RoleName Second { get; } + public RoleName Third { get; } + public RoleName Forth { get; } - public LeaderElectionSpecConfig(bool failureDetectorPuppet) - { - Controller = Role("controller"); - First = Role("first"); - Second = Role("second"); - Third = Role("third"); - Forth = Role("forth"); - - CommonConfig = DebugConfig(false) - .WithFallback(MultiNodeClusterSpec.ClusterConfig(failureDetectorPuppet)); - } + public LeaderElectionSpecConfig(bool failureDetectorPuppet) + { + Controller = Role("controller"); + First = Role("first"); + Second = Role("second"); + Third = Role("third"); + Forth = Role("forth"); + + CommonConfig = DebugConfig(false) + .WithFallback(MultiNodeClusterSpec.ClusterConfig(failureDetectorPuppet)); } +} - public class LeaderElectionWithFailureDetectorPuppetMultiJvmNode : LeaderElectionSpec +public class LeaderElectionWithFailureDetectorPuppetMultiJvmNode : LeaderElectionSpec +{ + public LeaderElectionWithFailureDetectorPuppetMultiJvmNode() + : base(true, typeof(LeaderElectionWithFailureDetectorPuppetMultiJvmNode)) { - public LeaderElectionWithFailureDetectorPuppetMultiJvmNode() - : base(true, typeof(LeaderElectionWithFailureDetectorPuppetMultiJvmNode)) - { - } } +} - public class LeaderElectionWithAccrualFailureDetectorMultiJvmNode : LeaderElectionSpec +public class LeaderElectionWithAccrualFailureDetectorMultiJvmNode : LeaderElectionSpec +{ + public LeaderElectionWithAccrualFailureDetectorMultiJvmNode() + : base(false, typeof(LeaderElectionWithAccrualFailureDetectorMultiJvmNode)) { - public LeaderElectionWithAccrualFailureDetectorMultiJvmNode() - : base(false, typeof(LeaderElectionWithAccrualFailureDetectorMultiJvmNode)) - { - } } +} - public abstract class LeaderElectionSpec : MultiNodeClusterSpec - { - private readonly LeaderElectionSpecConfig _config; +public abstract class LeaderElectionSpec : MultiNodeClusterSpec +{ + private readonly LeaderElectionSpecConfig _config; - private readonly ImmutableList _sortedRoles; + private readonly ImmutableList _sortedRoles; - protected LeaderElectionSpec(bool failureDetectorPuppet, Type type) - : this(new LeaderElectionSpecConfig(failureDetectorPuppet), type) - { + protected LeaderElectionSpec(bool failureDetectorPuppet, Type type) + : this(new LeaderElectionSpecConfig(failureDetectorPuppet), type) + { - } + } - protected LeaderElectionSpec(LeaderElectionSpecConfig config, Type type) - : base(config, type) - { - _config = config; - _sortedRoles = ImmutableList.Create( + protected LeaderElectionSpec(LeaderElectionSpecConfig config, Type type) + : base(config, type) + { + _config = config; + _sortedRoles = ImmutableList.Create( _config.First, _config.Second, _config.Third, _config.Forth) - .Sort(new RoleNameComparer(this)); - } + .Sort(new RoleNameComparer(this)); + } - [MultiNodeFact] - public void LeaderElectionSpecs() + [MultiNodeFact] + public async Task LeaderElectionSpecs() + { + await Cluster_of_four_nodes_must_be_able_to_elect_single_leaderAsync(); + await Cluster_of_four_nodes_must_be_able_to_reelect_single_leader_after_leader_has_leftAsync(); + await Cluster_of_four_nodes_must_be_able_to_reelect_single_leader_after_leader_has_left_again(); + } + + public async Task Cluster_of_four_nodes_must_be_able_to_elect_single_leaderAsync() + { + await AwaitClusterUpAsync(CancellationToken.None, _config.First, _config.Second, _config.Third, _config.Forth); + + if (Myself != _config.Controller) { - Cluster_of_four_nodes_must_be_able_to_elect_single_leader(); - Cluster_of_four_nodes_must_be_able_to_reelect_single_leader_after_leader_has_left(); - Cluster_of_four_nodes_must_be_able_to_reelect_single_leader_after_leader_has_left_again(); + ClusterView.IsLeader.ShouldBe(Myself == _sortedRoles.First()); + AssertLeaderIn(_sortedRoles); } - public void Cluster_of_four_nodes_must_be_able_to_elect_single_leader() + await EnterBarrierAsync("after-1"); + } + + public async Task ShutdownLeaderAndVerifyNewLeaderAsync(int alreadyShutdown) + { + var currentRoles = _sortedRoles.Skip(alreadyShutdown).ToList(); + currentRoles.Count.ShouldBeGreaterOrEqual(2); + var leader = currentRoles.First(); + var aUser = currentRoles.Last(); + var remainingRoles = currentRoles.Skip(1).ToImmutableList(); + var n = "-" + (alreadyShutdown + 1); + + if (Myself == _config.Controller) + { + await EnterBarrierAsync("before-shutdown" + n); + await TestConductor.ExitAsync(leader, 0); + await EnterBarrierAsync("after-shutdown" + n, "after-unavailable" + n, "after-down" + n, "completed" + n); + } + else if (Myself == leader) + { + await EnterBarrierAsync("before-shutdown" + n, "after-shutdown" + n); + // this node will be shutdown by the controller and doesn't participate in more barriers + } + else if (Myself == aUser) { - AwaitClusterUp(_config.First, _config.Second, _config.Third, _config.Forth); + var leaderAddress = GetAddress(leader); + await EnterBarrierAsync("before-shutdown" + n, "after-shutdown" + n); - if (Myself != _config.Controller) - { - ClusterView.IsLeader.ShouldBe(Myself == _sortedRoles.First()); - AssertLeaderIn(_sortedRoles); - } + // detect failure + MarkNodeAsUnavailable(leaderAddress); + await AwaitAssertAsync(() => ClusterView.UnreachableMembers.Select(x => x.Address).Contains(leaderAddress).ShouldBeTrue()); + await EnterBarrierAsync("after-unavailable" + n); - EnterBarrier("after-1"); - } + // user marks the shutdown leader as DOWN + Cluster.Down(leaderAddress); - public void ShutdownLeaderAndVerifyNewLeader(int alreadyShutdown) + // removed + await AwaitAssertAsync((() => ClusterView.UnreachableMembers.Select(x => x.Address).Contains(leaderAddress).ShouldBeFalse())); + await EnterBarrierAsync("after-down" + n, "completed" + n); + } + else if (remainingRoles.Contains(Myself)) { - var currentRoles = _sortedRoles.Skip(alreadyShutdown).ToList(); - currentRoles.Count.ShouldBeGreaterOrEqual(2); - var leader = currentRoles.First(); - var aUser = currentRoles.Last(); - var remainingRoles = currentRoles.Skip(1).ToImmutableList(); - var n = "-" + (alreadyShutdown + 1); - - if (Myself == _config.Controller) - { - EnterBarrier("before-shutdown" + n); - TestConductor.Exit(leader, 0).Wait(); - EnterBarrier("after-shutdown" + n, "after-unavailable" + n, "after-down" + n, "completed" + n); - } - else if (Myself == leader) - { - EnterBarrier("before-shutdown" + n, "after-shutdown" + n); - // this node will be shutdown by the controller and doesn't participate in more barriers - } - else if (Myself == aUser) - { - var leaderAddress = GetAddress(leader); - EnterBarrier("before-shutdown" + n, "after-shutdown" + n); - - // detect failure - MarkNodeAsUnavailable(leaderAddress); - AwaitAssert(() => ClusterView.UnreachableMembers.Select(x => x.Address).Contains(leaderAddress).ShouldBeTrue()); - EnterBarrier("after-unavailable" + n); - - // user marks the shutdown leader as DOWN - Cluster.Down(leaderAddress); - - // removed - AwaitAssert((() => ClusterView.UnreachableMembers.Select(x => x.Address).Contains(leaderAddress).ShouldBeFalse())); - EnterBarrier("after-down" + n, "completed" + n); - } - else if (remainingRoles.Contains(Myself)) - { - // remaining cluster nodes, not shutdown - var leaderAddress = GetAddress(leader); - EnterBarrier("before-shutdown" + n, "after-shutdown" + n); - - AwaitAssert(() => ClusterView.UnreachableMembers.Select(x => x.Address).Contains(leaderAddress).ShouldBeTrue()); - EnterBarrier("after-unavailable" + n); - - EnterBarrier("after-down" + n); - AwaitMembersUp(currentRoles.Count - 1); - var nextExpectedLeader = remainingRoles.First(); - ClusterView.IsLeader.ShouldBe(Myself == nextExpectedLeader); - AssertLeaderIn(remainingRoles); - - EnterBarrier("completed" + n); - } + // remaining cluster nodes, not shutdown + var leaderAddress = GetAddress(leader); + await EnterBarrierAsync("before-shutdown" + n, "after-shutdown" + n); + + await AwaitAssertAsync(() => ClusterView.UnreachableMembers.Select(x => x.Address).Contains(leaderAddress).ShouldBeTrue()); + await EnterBarrierAsync("after-unavailable" + n); + + await EnterBarrierAsync("after-down" + n); + await AwaitMembersUpAsync(currentRoles.Count - 1); + var nextExpectedLeader = remainingRoles.First(); + ClusterView.IsLeader.ShouldBe(Myself == nextExpectedLeader); + AssertLeaderIn(remainingRoles); + + await EnterBarrierAsync("completed" + n); } + } - public void Cluster_of_four_nodes_must_be_able_to_reelect_single_leader_after_leader_has_left() + public async Task Cluster_of_four_nodes_must_be_able_to_reelect_single_leader_after_leader_has_leftAsync() + { + await WithinAsync(TimeSpan.FromSeconds(30), async () => { - Within(TimeSpan.FromSeconds(30), () => - { - ShutdownLeaderAndVerifyNewLeader(0); - EnterBarrier("after-2"); - }); - } + await ShutdownLeaderAndVerifyNewLeaderAsync(0); + await EnterBarrierAsync("after-2"); + }); + } - public void Cluster_of_four_nodes_must_be_able_to_reelect_single_leader_after_leader_has_left_again() + public async Task Cluster_of_four_nodes_must_be_able_to_reelect_single_leader_after_leader_has_left_again() + { + await WithinAsync(TimeSpan.FromSeconds(30), async () => { - Within(TimeSpan.FromSeconds(30), () => - { - ShutdownLeaderAndVerifyNewLeader(1); - EnterBarrier("after-3"); - }); - } + await ShutdownLeaderAndVerifyNewLeaderAsync(1); + await EnterBarrierAsync("after-3"); + }); } -} +} \ No newline at end of file diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterRoundRobinSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterRoundRobinSpec.cs index 501988b8845..477107b6ffd 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterRoundRobinSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterRoundRobinSpec.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Akka.Actor; using Akka.Cluster.Routing; using Akka.Cluster.TestKit; @@ -177,27 +178,27 @@ private Dictionary ReceiveReplays(ClusterRoundRobinSpecConfig.IRou } [MultiNodeFact] - public void ClusterRoundRobinSpecs() + public async Task ClusterRoundRobinSpecs() { - A_cluster_router_with_a_RoundRobin_router_must_start_cluster_with_2_nodes(); - A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_the_member_nodes_in_the_cluster(); - A_cluster_router_with_a_RoundRobin_router_must_lookup_routees_on_the_member_nodes_in_the_cluster(); - A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_new_nodes_in_the_cluster(); - A_cluster_router_with_a_RoundRobin_router_must_lookup_routees_on_new_nodes_in_the_cluster(); - A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_only_remote_nodes_when_allowlocalrouteesoff(); - A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_specified_node_role(); - A_cluster_router_with_a_RoundRobin_router_must_deploy_programatically_defined_routees_to_the_member_nodes_in_the_cluster(); - A_cluster_router_with_a_RoundRobin_router_must_remove_routees_for_unreachable_nodes_and_add_when_reachable_again(); - A_cluster_router_with_a_RoundRobin_router_must_deploy_programatically_defined_routees_to_other_node_when_a_node_becomes_down(); + await A_cluster_router_with_a_RoundRobin_router_must_start_cluster_with_2_nodes(); + await A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_the_member_nodes_in_the_cluster(); + await A_cluster_router_with_a_RoundRobin_router_must_lookup_routees_on_the_member_nodes_in_the_cluster(); + await A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_new_nodes_in_the_cluster(); + await A_cluster_router_with_a_RoundRobin_router_must_lookup_routees_on_new_nodes_in_the_cluster(); + await A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_only_remote_nodes_when_allowlocalrouteesoff(); + await A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_specified_node_role(); + await A_cluster_router_with_a_RoundRobin_router_must_deploy_programatically_defined_routees_to_the_member_nodes_in_the_cluster(); + await A_cluster_router_with_a_RoundRobin_router_must_remove_routees_for_unreachable_nodes_and_add_when_reachable_again(); + await A_cluster_router_with_a_RoundRobin_router_must_deploy_programatically_defined_routees_to_other_node_when_a_node_becomes_down(); } - private void A_cluster_router_with_a_RoundRobin_router_must_start_cluster_with_2_nodes() + private async Task A_cluster_router_with_a_RoundRobin_router_must_start_cluster_with_2_nodes() { AwaitClusterUp(_config.First, _config.Second); - EnterBarrier("after-1"); + await EnterBarrierAsync("after-1"); } - private void A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_the_member_nodes_in_the_cluster() + private async Task A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_the_member_nodes_in_the_cluster() { RunOn(() => { @@ -221,17 +222,17 @@ private void A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_th replays.Values.Sum().Should().Be(iterationCount); }, _config.First); - EnterBarrier("after-2"); + await EnterBarrierAsync("after-2"); } - private void A_cluster_router_with_a_RoundRobin_router_must_lookup_routees_on_the_member_nodes_in_the_cluster() + private async Task A_cluster_router_with_a_RoundRobin_router_must_lookup_routees_on_the_member_nodes_in_the_cluster() { // cluster consists of first and second Sys.ActorOf(Props.Create(() => new ClusterRoundRobinSpecConfig.SomeActor(new ClusterRoundRobinSpecConfig.GroupRoutee())), "myserviceA"); Sys.ActorOf(Props.Create(() => new ClusterRoundRobinSpecConfig.SomeActor(new ClusterRoundRobinSpecConfig.GroupRoutee())), "myserviceB"); - EnterBarrier("myservice-started"); + await EnterBarrierAsync("myservice-started"); RunOn(() => { @@ -256,10 +257,10 @@ private void A_cluster_router_with_a_RoundRobin_router_must_lookup_routees_on_th replays.Values.Sum().Should().Be(iterationCount); }, _config.First); - EnterBarrier("after-3"); + await EnterBarrierAsync("after-3"); } - private void A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_new_nodes_in_the_cluster() + private async Task A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_new_nodes_in_the_cluster() { // add third and fourth AwaitClusterUp(_config.First, _config.Second, _config.Third, _config.Fourth); @@ -281,10 +282,10 @@ private void A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_ne replays.Values.Sum().Should().Be(iterationCount); }, _config.First); - EnterBarrier("after-4"); + await EnterBarrierAsync("after-4"); } - private void A_cluster_router_with_a_RoundRobin_router_must_lookup_routees_on_new_nodes_in_the_cluster() + private async Task A_cluster_router_with_a_RoundRobin_router_must_lookup_routees_on_new_nodes_in_the_cluster() { // cluster consists of first, second, third and fourth RunOn(() => @@ -304,10 +305,10 @@ private void A_cluster_router_with_a_RoundRobin_router_must_lookup_routees_on_ne replays.Values.Sum().Should().Be(iterationCount); }, _config.First); - EnterBarrier("after-5"); + await EnterBarrierAsync("after-5"); } - private void A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_only_remote_nodes_when_allowlocalrouteesoff() + private async Task A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_only_remote_nodes_when_allowlocalrouteesoff() { RunOn(() => { @@ -329,10 +330,10 @@ private void A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_on replays.Values.Sum().Should().Be(iterationCount); }, _config.First); - EnterBarrier("after-6"); + await EnterBarrierAsync("after-6"); } - private void A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_specified_node_role() + private async Task A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_specified_node_role() { RunOn(() => { @@ -353,10 +354,10 @@ private void A_cluster_router_with_a_RoundRobin_router_must_deploy_routees_to_sp replays.Values.Sum().Should().Be(iterationCount); }, _config.First); - EnterBarrier("after-7"); + await EnterBarrierAsync("after-7"); } - private void A_cluster_router_with_a_RoundRobin_router_must_deploy_programatically_defined_routees_to_the_member_nodes_in_the_cluster() + private async Task A_cluster_router_with_a_RoundRobin_router_must_deploy_programatically_defined_routees_to_the_member_nodes_in_the_cluster() { RunOn(() => { @@ -381,12 +382,12 @@ private void A_cluster_router_with_a_RoundRobin_router_must_deploy_programatical replays.Values.Sum().Should().Be(iterationCount); }, _config.First); - EnterBarrier("after-8"); + await EnterBarrierAsync("after-8"); } - private void A_cluster_router_with_a_RoundRobin_router_must_remove_routees_for_unreachable_nodes_and_add_when_reachable_again() + private async Task A_cluster_router_with_a_RoundRobin_router_must_remove_routees_for_unreachable_nodes_and_add_when_reachable_again() { - Within(30.Seconds(), () => + await WithinAsync(30.Seconds(), async () => { // myservice is already running @@ -396,26 +397,26 @@ private void A_cluster_router_with_a_RoundRobin_router_must_remove_routees_for_u .Select(c => FullAddress(((ActorSelectionRoutee)c).Selection.Anchor)) .ToList(); - RunOn(() => + await RunOnAsync(async () => { // 4 nodes, 2 routees on each node AwaitAssert(() => CurrentRoutees(router4.Value).Count().Should().Be(8)); - TestConductor.Blackhole(_config.First, _config.Second, ThrottleTransportAdapter.Direction.Both).Wait(); + await TestConductor.BlackholeAsync(_config.First, _config.Second, ThrottleTransportAdapter.Direction.Both); AwaitAssert(() => routees().Count.Should().Be(6)); routeeAddresses().Should().NotContain(GetAddress(_config.Second)); - TestConductor.PassThrough(_config.First, _config.Second, ThrottleTransportAdapter.Direction.Both); + await TestConductor.PassThroughAsync(_config.First, _config.Second, ThrottleTransportAdapter.Direction.Both); AwaitAssert(() => routees().Count.Should().Be(8)); routeeAddresses().Should().Contain(GetAddress(_config.Second)); }, _config.First); }); - EnterBarrier("after-9"); + await EnterBarrierAsync("after-9"); } - private void A_cluster_router_with_a_RoundRobin_router_must_deploy_programatically_defined_routees_to_other_node_when_a_node_becomes_down() + private async Task A_cluster_router_with_a_RoundRobin_router_must_deploy_programatically_defined_routees_to_other_node_when_a_node_becomes_down() { MuteMarkingAsUnreachable(); @@ -459,7 +460,7 @@ private void A_cluster_router_with_a_RoundRobin_router_must_deploy_programatical replays.Values.Sum().Should().Be(iterationCount); }, _config.First); - EnterBarrier("after-10"); + await EnterBarrierAsync("after-10"); } } } diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllIndirectlyConnected5NodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllIndirectlyConnected5NodeSpec.cs index 1f317e9a3bb..4547ebaeb4d 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllIndirectlyConnected5NodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllIndirectlyConnected5NodeSpec.cs @@ -7,6 +7,7 @@ using System; using System.Linq; +using System.Threading.Tasks; using Akka.Cluster.TestKit; using Akka.Configuration; using Akka.MultiNode.TestAdapter; @@ -74,12 +75,12 @@ protected DownAllIndirectlyConnected5NodeSpec(DownAllIndirectlyConnected5NodeSpe } [MultiNodeFact] - public void DownAllIndirectlyConnected5NodeSpecTests() + public async Task DownAllIndirectlyConnected5NodeSpecTests() { - A_5_node_cluster_with_keep_one_indirectly_connected_off_should_down_all_when_indirectly_connected_combined_with_clean_partition(); + await A_5_node_cluster_with_keep_one_indirectly_connected_off_should_down_all_when_indirectly_connected_combined_with_clean_partition(); } - public void A_5_node_cluster_with_keep_one_indirectly_connected_off_should_down_all_when_indirectly_connected_combined_with_clean_partition() + public async Task A_5_node_cluster_with_keep_one_indirectly_connected_off_should_down_all_when_indirectly_connected_combined_with_clean_partition() { var cluster = Cluster.Get(Sys); @@ -87,7 +88,7 @@ public void A_5_node_cluster_with_keep_one_indirectly_connected_off_should_down_ { cluster.Join(cluster.SelfAddress); }, _config.Node1); - EnterBarrier("node1 joined"); + await EnterBarrierAsync("node1 joined"); RunOn(() => { cluster.Join(Node(_config.Node1).Address); @@ -103,26 +104,26 @@ public void A_5_node_cluster_with_keep_one_indirectly_connected_off_should_down_ } }); }); - EnterBarrier("Cluster formed"); + await EnterBarrierAsync("Cluster formed"); - RunOn(() => + await RunOnAsync(async () => { foreach (var x in new[] { _config.Node1, _config.Node2, _config.Node3 }) { foreach (var y in new[] { _config.Node4, _config.Node5 }) { - TestConductor.Blackhole(x, y, ThrottleTransportAdapter.Direction.Both).Wait(); + await TestConductor.BlackholeAsync(x, y, ThrottleTransportAdapter.Direction.Both); } } }, _config.Node1); - EnterBarrier("blackholed-clean-partition"); + await EnterBarrierAsync("blackholed-clean-partition"); - RunOn(() => + await RunOnAsync(async () => { - TestConductor.Blackhole(_config.Node2, _config.Node3, ThrottleTransportAdapter.Direction.Both).Wait(); + await TestConductor.BlackholeAsync(_config.Node2, _config.Node3, ThrottleTransportAdapter.Direction.Both); }, _config.Node1); - EnterBarrier("blackholed-indirectly-connected"); + await EnterBarrierAsync("blackholed-indirectly-connected"); Within(TimeSpan.FromSeconds(10), () => { @@ -146,13 +147,13 @@ public void A_5_node_cluster_with_keep_one_indirectly_connected_off_should_down_ }, _config.Node4, _config.Node5); }); }); - EnterBarrier("unreachable"); + await EnterBarrierAsync("unreachable"); - RunOn(() => + await RunOnAsync(async () => { - Within(TimeSpan.FromSeconds(15), () => + await WithinAsync(TimeSpan.FromSeconds(15), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { cluster.State.Members.Select(i => i.Address).Should().BeEquivalentTo(Node(_config.Node1).Address); foreach (var m in cluster.State.Members) @@ -163,13 +164,13 @@ public void A_5_node_cluster_with_keep_one_indirectly_connected_off_should_down_ }); }, _config.Node1); - RunOn(() => + await RunOnAsync(async () => { // downed - AwaitCondition(() => cluster.IsTerminated, max: TimeSpan.FromSeconds(15)); + await AwaitConditionAsync(() => Task.FromResult(cluster.IsTerminated), max: TimeSpan.FromSeconds(15)); }, _config.Node2, _config.Node3, _config.Node4, _config.Node5); - EnterBarrier("done"); + await EnterBarrierAsync("done"); } } } diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllUnstable5NodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllUnstable5NodeSpec.cs index 00043181b76..4f60798863d 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllUnstable5NodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SBR/DownAllUnstable5NodeSpec.cs @@ -8,6 +8,7 @@ using System; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Akka.Cluster.TestKit; using Akka.Configuration; using Akka.MultiNode.TestAdapter; @@ -80,12 +81,12 @@ protected DownAllUnstable5NodeSpec(DownAllUnstable5NodeSpecConfig config) } [MultiNodeFact] - public void DownAllUnstable5NodeSpecTests() + public async Task DownAllUnstable5NodeSpecTests() { - A_5_node_cluster_with_down_all_when_unstable_should_down_all_when_instability_continues(); + await A_5_node_cluster_with_down_all_when_unstable_should_down_all_when_instability_continues(); } - public void A_5_node_cluster_with_down_all_when_unstable_should_down_all_when_instability_continues() + public async Task A_5_node_cluster_with_down_all_when_unstable_should_down_all_when_instability_continues() { var cluster = Cluster.Get(Sys); @@ -93,14 +94,14 @@ public void A_5_node_cluster_with_down_all_when_unstable_should_down_all_when_in { cluster.Join(cluster.SelfAddress); }, _config.Node1); - EnterBarrier("node1 joined"); + await EnterBarrierAsync("node1 joined"); RunOn(() => { cluster.Join(Node(_config.Node1).Address); }, _config.Node2, _config.Node3, _config.Node4, _config.Node5); - Within(TimeSpan.FromSeconds(10), () => + await WithinAsync(TimeSpan.FromSeconds(10), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { cluster.State.Members.Count.Should().Be(5); foreach (var m in cluster.State.Members) @@ -110,27 +111,27 @@ public void A_5_node_cluster_with_down_all_when_unstable_should_down_all_when_in }); }); - EnterBarrier("Cluster formed"); + await EnterBarrierAsync("Cluster formed"); // acceptable-heartbeat-pause = 3s // stable-after = 10s // down-all-when-unstable = 7s - RunOn(() => + await RunOnAsync(async () => { foreach (var x in new[] { _config.Node1, _config.Node2, _config.Node3 }) { foreach (var y in new[] { _config.Node4, _config.Node5 }) { - TestConductor.Blackhole(x, y, ThrottleTransportAdapter.Direction.Both).Wait(); + await TestConductor.BlackholeAsync(x, y, ThrottleTransportAdapter.Direction.Both); } } }, _config.Node1); - EnterBarrier("blackholed-clean-partition"); + await EnterBarrierAsync("blackholed-clean-partition"); - Within(TimeSpan.FromSeconds(10), () => + await WithinAsync(TimeSpan.FromSeconds(10), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { RunOn(() => { @@ -142,36 +143,36 @@ public void A_5_node_cluster_with_down_all_when_unstable_should_down_all_when_in }, _config.Node4, _config.Node5); }); }); - EnterBarrier("unreachable-clean-partition"); + await EnterBarrierAsync("unreachable-clean-partition"); // no decision yet - Thread.Sleep(2000); + await Task.Delay(2000); cluster.State.Members.Count.Should().Be(5); foreach (var m in cluster.State.Members) { m.Status.Should().Be(MemberStatus.Up); } - RunOn(() => + await RunOnAsync(async () => { - TestConductor.Blackhole(_config.Node2, _config.Node3, ThrottleTransportAdapter.Direction.Both).Wait(); + await TestConductor.BlackholeAsync(_config.Node2, _config.Node3, ThrottleTransportAdapter.Direction.Both); }, _config.Node1); - EnterBarrier("blackhole-2"); + await EnterBarrierAsync("blackhole-2"); // then it takes about 5 seconds for failure detector to observe that - Thread.Sleep(7000); + await Task.Delay(7000); - RunOn(() => + await RunOnAsync(async () => { - TestConductor.PassThrough(_config.Node2, _config.Node3, ThrottleTransportAdapter.Direction.Both).Wait(); + await TestConductor.PassThroughAsync(_config.Node2, _config.Node3, ThrottleTransportAdapter.Direction.Both); }, _config.Node1); - EnterBarrier("passThrough-2"); + await EnterBarrierAsync("passThrough-2"); // now it should have been unstable for more than 17 seconds // all downed - AwaitCondition(() => cluster.IsTerminated, max: TimeSpan.FromSeconds(15)); + await AwaitConditionAsync(() => Task.FromResult(cluster.IsTerminated), max: TimeSpan.FromSeconds(15)); - EnterBarrier("done"); + await EnterBarrierAsync("done"); } } } diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected3NodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected3NodeSpec.cs index 93617466782..f098d828c7c 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected3NodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected3NodeSpec.cs @@ -7,6 +7,7 @@ using System; using System.Linq; +using System.Threading.Tasks; using Akka.Cluster.TestKit; using Akka.Configuration; using Akka.MultiNode.TestAdapter; @@ -68,12 +69,12 @@ protected IndirectlyConnected3NodeSpec(IndirectlyConnected3NodeSpecConfig config } [MultiNodeFact] - public void IndirectlyConnected3NodeSpecTests() + public async Task IndirectlyConnected3NodeSpecTests() { - A_3_node_cluster_should_avoid_a_split_brain_when_two_unreachable_but_can_talk_via_third(); + await A_3_node_cluster_should_avoid_a_split_brain_when_two_unreachable_but_can_talk_via_third(); } - public void A_3_node_cluster_should_avoid_a_split_brain_when_two_unreachable_but_can_talk_via_third() + public async Task A_3_node_cluster_should_avoid_a_split_brain_when_two_unreachable_but_can_talk_via_third() { var cluster = Cluster.Get(Sys); @@ -81,14 +82,14 @@ public void A_3_node_cluster_should_avoid_a_split_brain_when_two_unreachable_but { cluster.Join(cluster.SelfAddress); }, _config.Node1); - EnterBarrier("node1 joined"); + await EnterBarrierAsync("node1 joined"); RunOn(() => { cluster.Join(Node(_config.Node1).Address); }, _config.Node2, _config.Node3); - Within(TimeSpan.FromSeconds(10), () => + await WithinAsync(TimeSpan.FromSeconds(10), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { cluster.State.Members.Count.Should().Be(3); foreach (var m in cluster.State.Members) @@ -97,17 +98,17 @@ public void A_3_node_cluster_should_avoid_a_split_brain_when_two_unreachable_but } }); }); - EnterBarrier("Cluster formed"); + await EnterBarrierAsync("Cluster formed"); - RunOn(() => + await RunOnAsync(async () => { - TestConductor.Blackhole(_config.Node2, _config.Node3, ThrottleTransportAdapter.Direction.Both).Wait(); + await TestConductor.BlackholeAsync(_config.Node2, _config.Node3, ThrottleTransportAdapter.Direction.Both); }, _config.Node1); - EnterBarrier("Blackholed"); + await EnterBarrierAsync("Blackholed"); - Within(TimeSpan.FromSeconds(10), () => + await WithinAsync(TimeSpan.FromSeconds(10), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { RunOn(() => { @@ -123,13 +124,13 @@ public void A_3_node_cluster_should_avoid_a_split_brain_when_two_unreachable_but }, _config.Node1); }); }); - EnterBarrier("unreachable"); + await EnterBarrierAsync("unreachable"); - RunOn(() => + await RunOnAsync(async () => { - Within(TimeSpan.FromSeconds(15), () => + await WithinAsync(TimeSpan.FromSeconds(15), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { cluster.State.Members.Select(i => i.Address).Should().BeEquivalentTo(Node(_config.Node1).Address); foreach (var m in cluster.State.Members) @@ -140,13 +141,13 @@ public void A_3_node_cluster_should_avoid_a_split_brain_when_two_unreachable_but }); }, _config.Node1); - RunOn(() => + await RunOnAsync(async () => { // downed - AwaitCondition(() => cluster.IsTerminated, max: TimeSpan.FromSeconds(15)); + await AwaitConditionAsync(() => Task.FromResult(cluster.IsTerminated), max: TimeSpan.FromSeconds(15)); }, _config.Node2, _config.Node3); - EnterBarrier("done"); + await EnterBarrierAsync("done"); } } } diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected5NodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected5NodeSpec.cs index f561598b6a4..1d5f9c13e75 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected5NodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SBR/IndirectlyConnected5NodeSpec.cs @@ -7,6 +7,7 @@ using System; using System.Linq; +using System.Threading.Tasks; using Akka.Cluster.TestKit; using Akka.Configuration; using Akka.MultiNode.TestAdapter; @@ -73,12 +74,12 @@ protected IndirectlyConnected5NodeSpec(IndirectlyConnected5NodeSpecConfig config } [MultiNodeFact] - public void IndirectlyConnected5NodeSpecTests() + public async Task IndirectlyConnected5NodeSpecTests() { - A_5_node_cluster_should_avoid_a_split_brain_when_indirectly_connected_combined_with_clean_partition(); + await A_5_node_cluster_should_avoid_a_split_brain_when_indirectly_connected_combined_with_clean_partition(); } - public void A_5_node_cluster_should_avoid_a_split_brain_when_indirectly_connected_combined_with_clean_partition() + public async Task A_5_node_cluster_should_avoid_a_split_brain_when_indirectly_connected_combined_with_clean_partition() { var cluster = Cluster.Get(Sys); @@ -86,14 +87,14 @@ public void A_5_node_cluster_should_avoid_a_split_brain_when_indirectly_connecte { cluster.Join(cluster.SelfAddress); }, _config.Node1); - EnterBarrier("node1 joined"); + await EnterBarrierAsync("node1 joined"); RunOn(() => { cluster.Join(Node(_config.Node1).Address); }, _config.Node2, _config.Node3, _config.Node4, _config.Node5); - Within(TimeSpan.FromSeconds(10), () => + await WithinAsync(TimeSpan.FromSeconds(10), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { cluster.State.Members.Count.Should().Be(5); foreach (var m in cluster.State.Members) @@ -102,30 +103,30 @@ public void A_5_node_cluster_should_avoid_a_split_brain_when_indirectly_connecte } }); }); - EnterBarrier("Cluster formed"); + await EnterBarrierAsync("Cluster formed"); - RunOn(() => + await RunOnAsync(async () => { foreach (var x in new[] { _config.Node1, _config.Node2, _config.Node3 }) { foreach (var y in new[] { _config.Node4, _config.Node5 }) { - TestConductor.Blackhole(x, y, ThrottleTransportAdapter.Direction.Both).Wait(); + await TestConductor.BlackholeAsync(x, y, ThrottleTransportAdapter.Direction.Both); } } }, _config.Node1); - EnterBarrier("blackholed-clean-partition"); + await EnterBarrierAsync("blackholed-clean-partition"); - RunOn(() => + await RunOnAsync(async () => { - TestConductor.Blackhole(_config.Node2, _config.Node3, ThrottleTransportAdapter.Direction.Both).Wait(); + await TestConductor.BlackholeAsync(_config.Node2, _config.Node3, ThrottleTransportAdapter.Direction.Both); }, _config.Node1); - EnterBarrier("blackholed-indirectly-connected"); + await EnterBarrierAsync("blackholed-indirectly-connected"); - Within(TimeSpan.FromSeconds(10), () => + await WithinAsync(TimeSpan.FromSeconds(10), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { RunOn(() => { @@ -145,13 +146,13 @@ public void A_5_node_cluster_should_avoid_a_split_brain_when_indirectly_connecte }, _config.Node4, _config.Node5); }); }); - EnterBarrier("unreachable"); + await EnterBarrierAsync("unreachable"); - RunOn(() => + await RunOnAsync(async () => { - Within(TimeSpan.FromSeconds(15), () => + await WithinAsync(TimeSpan.FromSeconds(15), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { cluster.State.Members.Select(i => i.Address).Should().BeEquivalentTo(Node(_config.Node1).Address); foreach (var m in cluster.State.Members) @@ -162,13 +163,13 @@ public void A_5_node_cluster_should_avoid_a_split_brain_when_indirectly_connecte }); }, _config.Node1); - RunOn(() => + await RunOnAsync(async () => { // downed - AwaitCondition(() => cluster.IsTerminated, max: TimeSpan.FromSeconds(15)); + await AwaitConditionAsync(() => Task.FromResult(cluster.IsTerminated), max: TimeSpan.FromSeconds(15)); }, _config.Node2, _config.Node3, _config.Node4, _config.Node5); - EnterBarrier("done"); + await EnterBarrierAsync("done"); } } } diff --git a/src/core/Akka.Cluster.Tests.MultiNode/SBR/LeaseMajority5NodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/SBR/LeaseMajority5NodeSpec.cs index af7e6333acd..7c291ad7dbd 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/SBR/LeaseMajority5NodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/SBR/LeaseMajority5NodeSpec.cs @@ -124,27 +124,27 @@ List SortByAddress(RoleName[] roles) [MultiNodeFact] - public void LeaseMajority5NodeSpecTests() + public async Task LeaseMajority5NodeSpecTests() { - LeaseMajority_in_a_5_node_cluster_should_setup_cluster(); - LeaseMajority_in_a_5_node_cluster_should_keep_the_side_that_can_acquire_the_lease(); - LeaseMajority_in_a_5_node_cluster_should_keep_the_side_that_can_acquire_the_lease_round_2(); + await LeaseMajority_in_a_5_node_cluster_should_setup_cluster(); + await LeaseMajority_in_a_5_node_cluster_should_keep_the_side_that_can_acquire_the_lease(); + await LeaseMajority_in_a_5_node_cluster_should_keep_the_side_that_can_acquire_the_lease_round_2(); } - public void LeaseMajority_in_a_5_node_cluster_should_setup_cluster() + public async Task LeaseMajority_in_a_5_node_cluster_should_setup_cluster() { RunOn(() => { Cluster.Join(Cluster.SelfAddress); }, _config.Node1); - EnterBarrier("node1 joined"); + await EnterBarrierAsync("node1 joined"); RunOn(() => { Cluster.Join(Node(_config.Node1).Address); }, _config.Node2, _config.Node3, _config.Node4, _config.Node5); - Within(TimeSpan.FromSeconds(10), () => + await WithinAsync(TimeSpan.FromSeconds(10), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { Cluster.State.Members.Count.Should().Be(5); foreach (var m in Cluster.State.Members) @@ -153,10 +153,10 @@ public void LeaseMajority_in_a_5_node_cluster_should_setup_cluster() } }); }); - EnterBarrier("Cluster formed"); + await EnterBarrierAsync("Cluster formed"); } - public void LeaseMajority_in_a_5_node_cluster_should_keep_the_side_that_can_acquire_the_lease() + public async Task LeaseMajority_in_a_5_node_cluster_should_keep_the_side_that_can_acquire_the_lease() { var lease = TestLeaseExt.Get(Sys).GetTestLease(testLeaseName); var leaseProbe = lease.Probe; @@ -169,24 +169,24 @@ public void LeaseMajority_in_a_5_node_cluster_should_keep_the_side_that_can_acqu { lease.SetNextAcquireResult(Task.FromResult(false)); }, _config.Node4, _config.Node5); - EnterBarrier("lease-in-place"); - RunOn(() => + await EnterBarrierAsync("lease-in-place"); + await RunOnAsync(async () => { foreach (var x in new[] { _config.Node1, _config.Node2, _config.Node3 }) { foreach (var y in new[] { _config.Node4, _config.Node5 }) { - TestConductor.Blackhole(x, y, ThrottleTransportAdapter.Direction.Both).Wait(); + await TestConductor.BlackholeAsync(x, y, ThrottleTransportAdapter.Direction.Both); } } }, _config.Node1); - EnterBarrier("blackholed-clean-partition"); + await EnterBarrierAsync("blackholed-clean-partition"); - RunOn(() => + await RunOnAsync(async () => { - Within(TimeSpan.FromSeconds(20), () => + await WithinAsync(TimeSpan.FromSeconds(20), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { Cluster.State.Members.Count.Should().Be(3); }); @@ -198,11 +198,11 @@ public void LeaseMajority_in_a_5_node_cluster_should_keep_the_side_that_can_acqu leaseProbe.ExpectMsg(TimeSpan.FromSeconds(14)); }, Leader(_config.Node1, _config.Node2, _config.Node3)); }, _config.Node1, _config.Node2, _config.Node3); - RunOn(() => + await RunOnAsync(async () => { - Within(TimeSpan.FromSeconds(20), () => + await WithinAsync(TimeSpan.FromSeconds(20), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { Cluster.IsTerminated.Should().BeTrue(); }); @@ -212,13 +212,13 @@ public void LeaseMajority_in_a_5_node_cluster_should_keep_the_side_that_can_acqu }, Leader(_config.Node4, _config.Node5)); }); }, _config.Node4, _config.Node5); - EnterBarrier("downed-and-removed"); + await EnterBarrierAsync("downed-and-removed"); leaseProbe.ExpectNoMsg(TimeSpan.FromSeconds(1)); - EnterBarrier("done-1"); + await EnterBarrierAsync("done-1"); } - public void LeaseMajority_in_a_5_node_cluster_should_keep_the_side_that_can_acquire_the_lease_round_2() + public async Task LeaseMajority_in_a_5_node_cluster_should_keep_the_side_that_can_acquire_the_lease_round_2() { var lease = TestLeaseExt.Get(Sys).GetTestLease(testLeaseName); @@ -230,41 +230,41 @@ public void LeaseMajority_in_a_5_node_cluster_should_keep_the_side_that_can_acqu { lease.SetNextAcquireResult(Task.FromResult(false)); }, _config.Node2, _config.Node3); - EnterBarrier("lease-in-place-2"); - RunOn(() => + await EnterBarrierAsync("lease-in-place-2"); + await RunOnAsync(async () => { foreach (var x in new[] { _config.Node1 }) { foreach (var y in new[] { _config.Node2, _config.Node3 }) { - TestConductor.Blackhole(x, y, ThrottleTransportAdapter.Direction.Both).Wait(); + await TestConductor.BlackholeAsync(x, y, ThrottleTransportAdapter.Direction.Both); } } }, _config.Node1); - EnterBarrier("blackholed-clean-partition-2"); + await EnterBarrierAsync("blackholed-clean-partition-2"); - RunOn(() => + await RunOnAsync(async () => { - Within(TimeSpan.FromSeconds(20), () => + await WithinAsync(TimeSpan.FromSeconds(20), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { Cluster.State.Members.Count.Should().Be(1); }); }); }, _config.Node1); - RunOn(() => + await RunOnAsync(async () => { - Within(TimeSpan.FromSeconds(20), () => + await WithinAsync(TimeSpan.FromSeconds(20), async () => { - AwaitAssert(() => + await AwaitAssertAsync(() => { Cluster.IsTerminated.Should().BeTrue(); }); }); }, _config.Node2, _config.Node3); - EnterBarrier("done-2"); + await EnterBarrierAsync("done-2"); } } } diff --git a/src/core/Akka.Cluster.Tests.MultiNode/StressSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/StressSpec.cs index 34c7b826c73..e9a95fa8395 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/StressSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/StressSpec.cs @@ -13,6 +13,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading; +using System.Threading.Tasks; using Akka.Actor; using Akka.Cluster.TestKit; using Akka.Configuration; @@ -30,23 +31,23 @@ using Google.Protobuf.WellKnownTypes; using Environment = System.Environment; -namespace Akka.Cluster.Tests.MultiNode +namespace Akka.Cluster.Tests.MultiNode; + +public class StressSpecConfig : MultiNodeConfig { - public class StressSpecConfig : MultiNodeConfig + public int TotalNumberOfNodes => Environment.GetEnvironmentVariable("MNTR_STRESSSPEC_NODECOUNT") switch { - public int TotalNumberOfNodes => Environment.GetEnvironmentVariable("MNTR_STRESSSPEC_NODECOUNT") switch - { - string e when string.IsNullOrEmpty(e) => 13, - string val => int.Parse(val), - _ => 13 - }; + string e when string.IsNullOrEmpty(e) => 13, + string val => int.Parse(val), + _ => 13 + }; - public StressSpecConfig() - { - foreach (var i in Enumerable.Range(1, TotalNumberOfNodes)) - Role("node-" + i); + public StressSpecConfig() + { + foreach (var i in Enumerable.Range(1, TotalNumberOfNodes)) + Role("node-" + i); - CommonConfig = ConfigurationFactory.ParseString(@" + CommonConfig = ConfigurationFactory.ParseString(@" akka.test.cluster-stress-spec { infolog = on # scale the nr-of-nodes* settings with this factor @@ -116,1309 +117,1323 @@ public StressSpecConfig() } }"); - TestTransport = true; - } + TestTransport = true; + } - public class Settings - { - private readonly Config _testConfig; + public class Settings + { + private readonly Config _testConfig; - public Settings(Config config, int totalNumberOfNodes) + public Settings(Config config, int totalNumberOfNodes) + { + TotalNumberOfNodes = totalNumberOfNodes; + _testConfig = config.GetConfig("akka.test.cluster-stress-spec"); + Infolog = _testConfig.GetBoolean("infolog"); + NFactor = _testConfig.GetInt("nr-of-nodes-factor"); + NumberOfSeedNodes = _testConfig.GetInt("nr-of-seed-nodes"); + NumberOfNodesJoiningToSeedNodesInitially = + _testConfig.GetInt("nr-of-nodes-joining-to-seed-initially") * NFactor; + NumberOfNodesJoiningOneByOneSmall = _testConfig.GetInt("nr-of-nodes-joining-one-by-one-small") * NFactor; + NumberOfNodesJoiningOneByOneLarge = _testConfig.GetInt("nr-of-nodes-joining-one-by-one-large") * NFactor; + NumberOfNodesJoiningToOneNode = _testConfig.GetInt("nr-of-nodes-joining-to-one") * NFactor; + // remaining will join to seed nodes + NumberOfNodesJoiningToSeedNodes = (totalNumberOfNodes - NumberOfSeedNodes - + NumberOfNodesJoiningToSeedNodesInitially - + NumberOfNodesJoiningOneByOneSmall - + NumberOfNodesJoiningOneByOneLarge - NumberOfNodesJoiningToOneNode); + if (NumberOfNodesJoiningToSeedNodes < 0) + throw new ArgumentOutOfRangeException("nr-of-nodes-joining-*", + $"too many configured nr-of-nodes-joining-*, total should be <= {totalNumberOfNodes}"); + + NumberOfNodesLeavingOneByOneSmall = _testConfig.GetInt("nr-of-nodes-leaving-one-by-one-small") * NFactor; + NumberOfNodesLeavingOneByOneLarge = _testConfig.GetInt("nr-of-nodes-leaving-one-by-one-large") * NFactor; + NumberOfNodesLeaving = _testConfig.GetInt("nr-of-nodes-leaving") * NFactor; + NumberOfNodesShutdownOneByOneSmall = _testConfig.GetInt("nr-of-nodes-shutdown-one-by-one-small") * NFactor; + NumberOfNodesShutdownOneByOneLarge = _testConfig.GetInt("nr-of-nodes-shutdown-one-by-one-large") * NFactor; + NumberOfNodesShutdown = _testConfig.GetInt("nr-of-nodes-shutdown") * NFactor; + NumberOfNodesPartition = _testConfig.GetInt("nr-of-nodes-partition") * NFactor; + NumberOfNodesJoinRemove = _testConfig.GetInt("nr-of-nodes-join-remove"); // not scaled by nodes factor + + DFactor = _testConfig.GetInt("duration-factor"); + JoinRemoveDuration = TimeSpan.FromMilliseconds(_testConfig.GetTimeSpan("join-remove-duration").TotalMilliseconds * DFactor); + IdleGossipDuration = TimeSpan.FromMilliseconds(_testConfig.GetTimeSpan("idle-gossip-duration").TotalMilliseconds * DFactor); + ExpectedTestDuration = TimeSpan.FromMilliseconds(_testConfig.GetTimeSpan("expected-test-duration").TotalMilliseconds * DFactor); + ConvergenceWithinFactor = _testConfig.GetDouble("convergence-within-factor"); + + if (NumberOfSeedNodes + NumberOfNodesJoiningToSeedNodesInitially + NumberOfNodesJoiningOneByOneSmall + + NumberOfNodesJoiningOneByOneLarge + NumberOfNodesJoiningToOneNode + + NumberOfNodesJoiningToSeedNodes > totalNumberOfNodes) { - TotalNumberOfNodes = totalNumberOfNodes; - _testConfig = config.GetConfig("akka.test.cluster-stress-spec"); - Infolog = _testConfig.GetBoolean("infolog"); - NFactor = _testConfig.GetInt("nr-of-nodes-factor"); - NumberOfSeedNodes = _testConfig.GetInt("nr-of-seed-nodes"); - NumberOfNodesJoiningToSeedNodesInitially = - _testConfig.GetInt("nr-of-nodes-joining-to-seed-initially") * NFactor; - NumberOfNodesJoiningOneByOneSmall = _testConfig.GetInt("nr-of-nodes-joining-one-by-one-small") * NFactor; - NumberOfNodesJoiningOneByOneLarge = _testConfig.GetInt("nr-of-nodes-joining-one-by-one-large") * NFactor; - NumberOfNodesJoiningToOneNode = _testConfig.GetInt("nr-of-nodes-joining-to-one") * NFactor; - // remaining will join to seed nodes - NumberOfNodesJoiningToSeedNodes = (totalNumberOfNodes - NumberOfSeedNodes - - NumberOfNodesJoiningToSeedNodesInitially - - NumberOfNodesJoiningOneByOneSmall - - NumberOfNodesJoiningOneByOneLarge - NumberOfNodesJoiningToOneNode); - if (NumberOfNodesJoiningToSeedNodes < 0) - throw new ArgumentOutOfRangeException("nr-of-nodes-joining-*", - $"too many configured nr-of-nodes-joining-*, total should be <= {totalNumberOfNodes}"); - - NumberOfNodesLeavingOneByOneSmall = _testConfig.GetInt("nr-of-nodes-leaving-one-by-one-small") * NFactor; - NumberOfNodesLeavingOneByOneLarge = _testConfig.GetInt("nr-of-nodes-leaving-one-by-one-large") * NFactor; - NumberOfNodesLeaving = _testConfig.GetInt("nr-of-nodes-leaving") * NFactor; - NumberOfNodesShutdownOneByOneSmall = _testConfig.GetInt("nr-of-nodes-shutdown-one-by-one-small") * NFactor; - NumberOfNodesShutdownOneByOneLarge = _testConfig.GetInt("nr-of-nodes-shutdown-one-by-one-large") * NFactor; - NumberOfNodesShutdown = _testConfig.GetInt("nr-of-nodes-shutdown") * NFactor; - NumberOfNodesPartition = _testConfig.GetInt("nr-of-nodes-partition") * NFactor; - NumberOfNodesJoinRemove = _testConfig.GetInt("nr-of-nodes-join-remove"); // not scaled by nodes factor - - DFactor = _testConfig.GetInt("duration-factor"); - JoinRemoveDuration = TimeSpan.FromMilliseconds(_testConfig.GetTimeSpan("join-remove-duration").TotalMilliseconds * DFactor); - IdleGossipDuration = TimeSpan.FromMilliseconds(_testConfig.GetTimeSpan("idle-gossip-duration").TotalMilliseconds * DFactor); - ExpectedTestDuration = TimeSpan.FromMilliseconds(_testConfig.GetTimeSpan("expected-test-duration").TotalMilliseconds * DFactor); - ConvergenceWithinFactor = _testConfig.GetDouble("convergence-within-factor"); - - if (NumberOfSeedNodes + NumberOfNodesJoiningToSeedNodesInitially + NumberOfNodesJoiningOneByOneSmall + - NumberOfNodesJoiningOneByOneLarge + NumberOfNodesJoiningToOneNode + - NumberOfNodesJoiningToSeedNodes > totalNumberOfNodes) - { - throw new ArgumentOutOfRangeException("nr-of-nodes-joining-*", - $"specified number of joining nodes <= {totalNumberOfNodes}"); - } + throw new ArgumentOutOfRangeException("nr-of-nodes-joining-*", + $"specified number of joining nodes <= {totalNumberOfNodes}"); + } - // don't shutdown the 3 nodes hosting the master actors - if (NumberOfNodesLeavingOneByOneSmall + NumberOfNodesLeavingOneByOneLarge + NumberOfNodesLeaving + - NumberOfNodesShutdownOneByOneSmall + NumberOfNodesShutdownOneByOneLarge + NumberOfNodesShutdown > - totalNumberOfNodes - 3) - { - throw new ArgumentOutOfRangeException("nr-of-nodes-leaving-*", - $"specified number of leaving/shutdown nodes <= {totalNumberOfNodes - 3}"); - } + // don't shutdown the 3 nodes hosting the master actors + if (NumberOfNodesLeavingOneByOneSmall + NumberOfNodesLeavingOneByOneLarge + NumberOfNodesLeaving + + NumberOfNodesShutdownOneByOneSmall + NumberOfNodesShutdownOneByOneLarge + NumberOfNodesShutdown > + totalNumberOfNodes - 3) + { + throw new ArgumentOutOfRangeException("nr-of-nodes-leaving-*", + $"specified number of leaving/shutdown nodes <= {totalNumberOfNodes - 3}"); + } - if (NumberOfNodesJoinRemove > totalNumberOfNodes) - { - throw new ArgumentOutOfRangeException("nr-of-nodes-join-remove*", - $"nr-of-nodes-join-remove should be <= {totalNumberOfNodes}"); - } + if (NumberOfNodesJoinRemove > totalNumberOfNodes) + { + throw new ArgumentOutOfRangeException("nr-of-nodes-join-remove*", + $"nr-of-nodes-join-remove should be <= {totalNumberOfNodes}"); } + } - public int TotalNumberOfNodes { get; } + public int TotalNumberOfNodes { get; } - public bool Infolog { get; } - public int NFactor { get; } + public bool Infolog { get; } + public int NFactor { get; } - public int NumberOfSeedNodes { get; } + public int NumberOfSeedNodes { get; } - public int NumberOfNodesJoiningToSeedNodesInitially { get; } + public int NumberOfNodesJoiningToSeedNodesInitially { get; } - public int NumberOfNodesJoiningOneByOneSmall { get; } + public int NumberOfNodesJoiningOneByOneSmall { get; } - public int NumberOfNodesJoiningOneByOneLarge { get; } + public int NumberOfNodesJoiningOneByOneLarge { get; } - public int NumberOfNodesJoiningToOneNode { get; } + public int NumberOfNodesJoiningToOneNode { get; } - public int NumberOfNodesJoiningToSeedNodes { get; } + public int NumberOfNodesJoiningToSeedNodes { get; } - public int NumberOfNodesLeavingOneByOneSmall { get; } + public int NumberOfNodesLeavingOneByOneSmall { get; } - public int NumberOfNodesLeavingOneByOneLarge { get; } + public int NumberOfNodesLeavingOneByOneLarge { get; } - public int NumberOfNodesLeaving { get; } + public int NumberOfNodesLeaving { get; } - public int NumberOfNodesShutdownOneByOneSmall { get; } + public int NumberOfNodesShutdownOneByOneSmall { get; } - public int NumberOfNodesShutdownOneByOneLarge { get; } + public int NumberOfNodesShutdownOneByOneLarge { get; } - public int NumberOfNodesShutdown { get; } + public int NumberOfNodesShutdown { get; } - public int NumberOfNodesPartition { get; } + public int NumberOfNodesPartition { get; } - public int NumberOfNodesJoinRemove { get; } + public int NumberOfNodesJoinRemove { get; } - public int DFactor { get; } + public int DFactor { get; } - public TimeSpan JoinRemoveDuration { get; } + public TimeSpan JoinRemoveDuration { get; } - public TimeSpan IdleGossipDuration { get; } + public TimeSpan IdleGossipDuration { get; } - public TimeSpan ExpectedTestDuration { get; } + public TimeSpan ExpectedTestDuration { get; } - public double ConvergenceWithinFactor { get; } + public double ConvergenceWithinFactor { get; } - public override string ToString() - { - return _testConfig.WithFallback($"nrOfNodes={TotalNumberOfNodes}").Root.ToString(2); - } + public override string ToString() + { + return _testConfig.WithFallback($"nrOfNodes={TotalNumberOfNodes}").Root.ToString(2); } } +} - internal sealed class ClusterResult +internal sealed class ClusterResult +{ + public ClusterResult(Address address, TimeSpan duration, GossipStats clusterStats) { - public ClusterResult(Address address, TimeSpan duration, GossipStats clusterStats) - { - Address = address; - Duration = duration; - ClusterStats = clusterStats; - } - - public Address Address { get; } - public TimeSpan Duration { get; } - public GossipStats ClusterStats { get; } + Address = address; + Duration = duration; + ClusterStats = clusterStats; } - internal sealed class AggregatedClusterResult + public Address Address { get; } + public TimeSpan Duration { get; } + public GossipStats ClusterStats { get; } +} + +internal sealed class AggregatedClusterResult +{ + public AggregatedClusterResult(string title, TimeSpan duration, GossipStats clusterStats) { - public AggregatedClusterResult(string title, TimeSpan duration, GossipStats clusterStats) - { - Title = title; - Duration = duration; - ClusterStats = clusterStats; - } + Title = title; + Duration = duration; + ClusterStats = clusterStats; + } - public string Title { get; } + public string Title { get; } - public TimeSpan Duration { get; } + public TimeSpan Duration { get; } - public GossipStats ClusterStats { get; } - } + public GossipStats ClusterStats { get; } +} - /// - /// Central aggregator of cluster statistics and metrics. - /// - /// Reports the result via log periodically and when all - /// expected results has been collected. It shuts down - /// itself when expected results has been collected. - /// - internal class ClusterResultAggregator : ReceiveActor - { - private readonly string _title; - private readonly int _expectedResults; - private readonly StressSpecConfig.Settings _settings; +/// +/// Central aggregator of cluster statistics and metrics. +/// +/// Reports the result via log periodically and when all +/// expected results has been collected. It shuts down +/// itself when expected results has been collected. +/// +internal class ClusterResultAggregator : ReceiveActor +{ + private readonly string _title; + private readonly int _expectedResults; + private readonly StressSpecConfig.Settings _settings; - private readonly ILoggingAdapter _log = Context.GetLogger(); + private readonly ILoggingAdapter _log = Context.GetLogger(); - private Option _reportTo = Option.None; - private ImmutableList _results = ImmutableList.Empty; - private ImmutableSortedDictionary> _phiValuesObservedByNode = - ImmutableSortedDictionary>.Empty.WithComparers(Member.AddressOrdering); - private ImmutableSortedDictionary _clusterStatsObservedByNode = - ImmutableSortedDictionary.Empty.WithComparers(Member.AddressOrdering); + private Option _reportTo = Option.None; + private ImmutableList _results = ImmutableList.Empty; + private ImmutableSortedDictionary> _phiValuesObservedByNode = + ImmutableSortedDictionary>.Empty.WithComparers(Member.AddressOrdering); + private ImmutableSortedDictionary _clusterStatsObservedByNode = + ImmutableSortedDictionary.Empty.WithComparers(Member.AddressOrdering); - public static readonly string FormatPhiHeader = "[Monitor]\t[Subject]\t[count]\t[count phi > 1.0]\t[max phi]"; + public static readonly string FormatPhiHeader = "[Monitor]\t[Subject]\t[count]\t[count phi > 1.0]\t[max phi]"; - public string FormatPhiLine(Address monitor, Address subject, PhiValue phi) - { - return $"{monitor}\t{subject}\t{phi.Count}\t{phi.CountAboveOne}\t{phi.Max:F2}"; - } + public string FormatPhiLine(Address monitor, Address subject, PhiValue phi) + { + return $"{monitor}\t{subject}\t{phi.Count}\t{phi.CountAboveOne}\t{phi.Max:F2}"; + } - public string FormatPhi() + public string FormatPhi() + { + if (_phiValuesObservedByNode.IsEmpty) return string.Empty; + else { - if (_phiValuesObservedByNode.IsEmpty) return string.Empty; - else - { - var lines = (from mon in _phiValuesObservedByNode from phi in mon.Value select FormatPhiLine(mon.Key, phi.Address, phi)); - return FormatPhiHeader + Environment.NewLine + string.Join(Environment.NewLine, lines); - } + var lines = (from mon in _phiValuesObservedByNode from phi in mon.Value select FormatPhiLine(mon.Key, phi.Address, phi)); + return FormatPhiHeader + Environment.NewLine + string.Join(Environment.NewLine, lines); } + } - public TimeSpan MaxDuration => _results.Max(x => x.Duration); + public TimeSpan MaxDuration => _results.Max(x => x.Duration); - public GossipStats TotalGossipStats => - _results.Aggregate(new GossipStats(), (stats, result) => stats += result.ClusterStats); + public GossipStats TotalGossipStats => + _results.Aggregate(new GossipStats(), (stats, result) => stats += result.ClusterStats); - public string FormatStats() + public string FormatStats() + { + string F(ClusterEvent.CurrentInternalStats stats) { - string F(ClusterEvent.CurrentInternalStats stats) - { - return - $"CurrentClusterStats({stats.GossipStats?.ReceivedGossipCount}, {stats.GossipStats?.MergeCount}, " + - $"{stats.GossipStats?.SameCount}, {stats.GossipStats?.NewerCount}, {stats.GossipStats?.OlderCount}," + - $"{stats.SeenBy?.VersionSize}, {stats.SeenBy?.SeenLatest})"; - } - - return string.Join(Environment.NewLine, "ClusterStats(gossip, merge, same, newer, older, vclockSize, seenLatest)" + - Environment.NewLine + - string.Join(Environment.NewLine, _clusterStatsObservedByNode.Select(x => $"{x.Key}\t{F(x.Value)}"))); + return + $"CurrentClusterStats({stats.GossipStats?.ReceivedGossipCount}, {stats.GossipStats?.MergeCount}, " + + $"{stats.GossipStats?.SameCount}, {stats.GossipStats?.NewerCount}, {stats.GossipStats?.OlderCount}," + + $"{stats.SeenBy?.VersionSize}, {stats.SeenBy?.SeenLatest})"; } - public ClusterResultAggregator(string title, int expectedResults, StressSpecConfig.Settings settings) + return string.Join(Environment.NewLine, "ClusterStats(gossip, merge, same, newer, older, vclockSize, seenLatest)" + + Environment.NewLine + + string.Join(Environment.NewLine, _clusterStatsObservedByNode.Select(x => $"{x.Key}\t{F(x.Value)}"))); + } + + public ClusterResultAggregator(string title, int expectedResults, StressSpecConfig.Settings settings) + { + _title = title; + _expectedResults = expectedResults; + _settings = settings; + + Receive(phi => { - _title = title; - _expectedResults = expectedResults; - _settings = settings; + _phiValuesObservedByNode = _phiValuesObservedByNode.SetItem(phi.Address, phi.PhiValues); + }); - Receive(phi => - { - _phiValuesObservedByNode = _phiValuesObservedByNode.SetItem(phi.Address, phi.PhiValues); - }); + Receive(stats => + { + _clusterStatsObservedByNode = _clusterStatsObservedByNode.SetItem(stats.Address, stats.Stats); + }); - Receive(stats => + Receive(_ => + { + if (_settings.Infolog) { - _clusterStatsObservedByNode = _clusterStatsObservedByNode.SetItem(stats.Address, stats.Stats); - }); + _log.Info("BEGIN CLUSTER OPERATION: [{0}] in progress" + Environment.NewLine + "{1}" + Environment.NewLine + "{2}", _title, + FormatPhi(), FormatStats()); + } + }); - Receive(_ => + Receive(r => + { + _results = _results.Add(r); + if (_results.Count == _expectedResults) { + var aggregated = new AggregatedClusterResult(_title, MaxDuration, TotalGossipStats); if (_settings.Infolog) { - _log.Info("BEGIN CLUSTER OPERATION: [{0}] in progress" + Environment.NewLine + "{1}" + Environment.NewLine + "{2}", _title, - FormatPhi(), FormatStats()); - } - }); - - Receive(r => - { - _results = _results.Add(r); - if (_results.Count == _expectedResults) - { - var aggregated = new AggregatedClusterResult(_title, MaxDuration, TotalGossipStats); - if (_settings.Infolog) - { - _log.Info("END CLUSTER OPERATION: [{0}] completed in [{1}] ms" + Environment.NewLine + "{2}" + - Environment.NewLine + "{3}" + Environment.NewLine + "{4}", _title, aggregated.Duration.TotalMilliseconds, + _log.Info("END CLUSTER OPERATION: [{0}] completed in [{1}] ms" + Environment.NewLine + "{2}" + + Environment.NewLine + "{3}" + Environment.NewLine + "{4}", _title, aggregated.Duration.TotalMilliseconds, aggregated.ClusterStats, FormatPhi(), FormatStats()); - } - _reportTo.OnSuccess(r => r.Tell(aggregated)); - Context.Stop(Self); } - }); + _reportTo.OnSuccess(r => r.Tell(aggregated)); + Context.Stop(Self); + } + }); - Receive(_ => { }); - Receive(re => - { - _reportTo = re.Ref; - }); - } + Receive(_ => { }); + Receive(re => + { + _reportTo = re.Ref; + }); } +} - /// - /// Keeps cluster statistics and metrics reported by . - /// - /// Logs the list of historical results when a new is received. - /// - internal class ClusterResultHistory : ReceiveActor - { - private ILoggingAdapter _log = Context.GetLogger(); - private ImmutableList _history = ImmutableList.Empty; +/// +/// Keeps cluster statistics and metrics reported by . +/// +/// Logs the list of historical results when a new is received. +/// +internal class ClusterResultHistory : ReceiveActor +{ + private ILoggingAdapter _log = Context.GetLogger(); + private ImmutableList _history = ImmutableList.Empty; - public ClusterResultHistory() + public ClusterResultHistory() + { + Receive(result => { - Receive(result => - { - _history = _history.Add(result); - }); - } - - public static readonly string FormatHistoryHeader = "[Title]\t[Duration (ms)]\t[GossipStats(gossip, merge, same, newer, older)]"; + _history = _history.Add(result); + }); + } - public string FormatHistoryLine(AggregatedClusterResult result) - { - return $"{result.Title}\t{result.Duration.TotalMilliseconds}\t{result.ClusterStats}"; - } + public static readonly string FormatHistoryHeader = "[Title]\t[Duration (ms)]\t[GossipStats(gossip, merge, same, newer, older)]"; - public string FormatHistory => FormatHistoryHeader + Environment.NewLine + - string.Join(Environment.NewLine, _history.Select(x => FormatHistoryLine(x))); + public string FormatHistoryLine(AggregatedClusterResult result) + { + return $"{result.Title}\t{result.Duration.TotalMilliseconds}\t{result.ClusterStats}"; } - /// - /// Collect phi values of the failure detector and report to the central - /// - internal class PhiObserver : ReceiveActor - { - private readonly Cluster _cluster = Cluster.Get(Context.System); - private readonly ILoggingAdapter _log = Context.GetLogger(); - private ImmutableDictionary _phiByNode = ImmutableDictionary.Empty; + public string FormatHistory => FormatHistoryHeader + Environment.NewLine + + string.Join(Environment.NewLine, _history.Select(x => FormatHistoryLine(x))); +} + +/// +/// Collect phi values of the failure detector and report to the central +/// +internal class PhiObserver : ReceiveActor +{ + private readonly Cluster _cluster = Cluster.Get(Context.System); + private readonly ILoggingAdapter _log = Context.GetLogger(); + private ImmutableDictionary _phiByNode = ImmutableDictionary.Empty; - private Option _reportTo = Option.None; - private HashSet
_nodes = new(); + private Option _reportTo = Option.None; + private HashSet
_nodes = new(); - private ICancelable _checkPhiTask = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable( - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(1), Context.Self, PhiTick.Instance, ActorRefs.NoSender); + private ICancelable _checkPhiTask = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable( + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1), Context.Self, PhiTick.Instance, ActorRefs.NoSender); - private double Phi(Address address) + private double Phi(Address address) + { + return _cluster.FailureDetector switch { - return _cluster.FailureDetector switch + DefaultFailureDetectorRegistry
reg => (reg.GetFailureDetector(address)) switch { - DefaultFailureDetectorRegistry
reg => (reg.GetFailureDetector(address)) switch - { - PhiAccrualFailureDetector fd => fd.CurrentPhi, - _ => 0.0d - }, + PhiAccrualFailureDetector fd => fd.CurrentPhi, _ => 0.0d - }; - } + }, + _ => 0.0d + }; + } - private PhiValue PhiByNodeDefault(Address address) + private PhiValue PhiByNodeDefault(Address address) + { + if (!_phiByNode.ContainsKey(address)) { - if (!_phiByNode.ContainsKey(address)) - { - // populate default value - _phiByNode = _phiByNode.Add(address, new PhiValue(address, 0, 0, 0.0d)); - } - - return _phiByNode[address]; + // populate default value + _phiByNode = _phiByNode.Add(address, new PhiValue(address, 0, 0, 0.0d)); } - public PhiObserver() + return _phiByNode[address]; + } + + public PhiObserver() + { + Receive(_ => { - Receive(_ => + foreach (var node in _nodes) { - foreach (var node in _nodes) - { - var previous = PhiByNodeDefault(node); - var p = Phi(node); + var previous = PhiByNodeDefault(node); + var p = Phi(node); - if (p > 0 || _cluster.FailureDetector.IsMonitoring(node)) + if (p > 0 || _cluster.FailureDetector.IsMonitoring(node)) + { + if (double.IsInfinity(p)) { - if (double.IsInfinity(p)) + _log.Warning("Detected phi value of infinity for [{0}] - ", node); + var (history, time) = _cluster.FailureDetector.GetFailureDetector(node) switch { - _log.Warning("Detected phi value of infinity for [{0}] - ", node); - var (history, time) = _cluster.FailureDetector.GetFailureDetector(node) switch - { - PhiAccrualFailureDetector fd => (fd.State.History, fd.State.TimeStamp), - _ => (HeartbeatHistory.Apply(1), null) - }; - _log.Warning("PhiValues: (Timestamp={0}, Mean={1}, Variance={2}, StdDeviation={3}, Intervals=[{4}])",time, - history.Mean, history.Variance, history.StdDeviation, - string.Join(",", history.Intervals)); - } - - var aboveOne = !double.IsInfinity(p) && p > 1.0d ? 1 : 0; - _phiByNode = _phiByNode.SetItem(node, new PhiValue(node, - previous.CountAboveOne + aboveOne, - previous.Count + 1, - Math.Max(previous.Max, p))); + PhiAccrualFailureDetector fd => (fd.State.History, fd.State.TimeStamp), + _ => (HeartbeatHistory.Apply(1), null) + }; + _log.Warning("PhiValues: (Timestamp={0}, Mean={1}, Variance={2}, StdDeviation={3}, Intervals=[{4}])",time, + history.Mean, history.Variance, history.StdDeviation, + string.Join(",", history.Intervals)); } - } - - var phiSet = _phiByNode.Values.ToImmutableSortedSet(); - _reportTo.OnSuccess(r => r.Tell(new PhiResult(_cluster.SelfAddress, phiSet))); - }); - Receive(state => - { - _nodes = new HashSet
(state.Members.Select(x => x.Address)); - }); + var aboveOne = !double.IsInfinity(p) && p > 1.0d ? 1 : 0; + _phiByNode = _phiByNode.SetItem(node, new PhiValue(node, + previous.CountAboveOne + aboveOne, + previous.Count + 1, + Math.Max(previous.Max, p))); + } + } - Receive(m => - { - _nodes.Add(m.Member.Address); - }); + var phiSet = _phiByNode.Values.ToImmutableSortedSet(); + _reportTo.OnSuccess(r => r.Tell(new PhiResult(_cluster.SelfAddress, phiSet))); + }); - Receive(r => - { - _reportTo.OnSuccess(o => Context.Unwatch(o)); - _reportTo = r.Ref; - _reportTo.OnSuccess(n => Context.Watch(n)); - }); + Receive(state => + { + _nodes = new HashSet
(state.Members.Select(x => x.Address)); + }); - Receive(_ => - { - if (_reportTo.HasValue) - _reportTo = Option.None; - }); + Receive(m => + { + _nodes.Add(m.Member.Address); + }); - Receive(_ => - { - _phiByNode = ImmutableDictionary.Empty; - _nodes.Clear(); - _cluster.Unsubscribe(Self); - _cluster.Subscribe(Self, typeof(ClusterEvent.IMemberEvent)); - }); - } + Receive(r => + { + _reportTo.OnSuccess(o => Context.Unwatch(o)); + _reportTo = r.Ref; + _reportTo.OnSuccess(n => Context.Watch(n)); + }); - protected override void PreStart() + Receive(_ => { - _cluster.Subscribe(Self, typeof(ClusterEvent.IMemberEvent)); - } + if (_reportTo.HasValue) + _reportTo = Option.None; + }); - protected override void PostStop() + Receive(_ => { + _phiByNode = ImmutableDictionary.Empty; + _nodes.Clear(); _cluster.Unsubscribe(Self); - _checkPhiTask.Cancel(); - base.PostStop(); - } + _cluster.Subscribe(Self, typeof(ClusterEvent.IMemberEvent)); + }); } - internal readonly struct PhiValue : IComparable + protected override void PreStart() { - public PhiValue(Address address, int countAboveOne, int count, double max) - { - Address = address; - CountAboveOne = countAboveOne; - Count = count; - Max = max; - } - - public Address Address { get; } - public int CountAboveOne { get; } - public int Count { get; } - public double Max { get; } + _cluster.Subscribe(Self, typeof(ClusterEvent.IMemberEvent)); + } - public int CompareTo(PhiValue other) - { - return Member.AddressOrdering.Compare(Address, other.Address); - } + protected override void PostStop() + { + _cluster.Unsubscribe(Self); + _checkPhiTask.Cancel(); + base.PostStop(); } +} - internal readonly struct PhiResult +internal readonly struct PhiValue : IComparable +{ + public PhiValue(Address address, int countAboveOne, int count, double max) { - public PhiResult(Address address, ImmutableSortedSet phiValues) - { - Address = address; - PhiValues = phiValues; - } + Address = address; + CountAboveOne = countAboveOne; + Count = count; + Max = max; + } - public Address Address { get; } + public Address Address { get; } + public int CountAboveOne { get; } + public int Count { get; } + public double Max { get; } - public ImmutableSortedSet PhiValues { get; } + public int CompareTo(PhiValue other) + { + return Member.AddressOrdering.Compare(Address, other.Address); } +} - internal class StatsObserver : ReceiveActor +internal readonly struct PhiResult +{ + public PhiResult(Address address, ImmutableSortedSet phiValues) { - private readonly Cluster _cluster = Cluster.Get(Context.System); - private Option _reportTo = Option.None; - private Option _startStats = Option.None; + Address = address; + PhiValues = phiValues; + } - protected override void PreStart() - { - _cluster.Subscribe(Self, typeof(ClusterEvent.CurrentInternalStats)); - } + public Address Address { get; } - protected override void PostStop() - { - _cluster.Unsubscribe(Self); - } + public ImmutableSortedSet PhiValues { get; } +} - public StatsObserver() - { - Receive(stats => - { - var gossipStats = stats.GossipStats; - var vclockStats = stats.SeenBy; +internal class StatsObserver : ReceiveActor +{ + private readonly Cluster _cluster = Cluster.Get(Context.System); + private Option _reportTo = Option.None; + private Option _startStats = Option.None; - GossipStats MatchStats() - { - if (!_startStats.HasValue) - { - _startStats = gossipStats; - return gossipStats; - } + protected override void PreStart() + { + _cluster.Subscribe(Self, typeof(ClusterEvent.CurrentInternalStats)); + } - return gossipStats -_startStats.Value; - } + protected override void PostStop() + { + _cluster.Unsubscribe(Self); + } - var diff = MatchStats(); - var res = new StatsResult(_cluster.SelfAddress, new ClusterEvent.CurrentInternalStats(diff, vclockStats)); - _reportTo.OnSuccess(a => a.Tell(res)); - }); + public StatsObserver() + { + Receive(stats => + { + var gossipStats = stats.GossipStats; + var vclockStats = stats.SeenBy; - Receive(r => + GossipStats MatchStats() { - _reportTo.OnSuccess(o => Context.Unwatch(o)); - _reportTo = r.Ref; - _reportTo.OnSuccess(n => Context.Watch(n)); - }); + if (!_startStats.HasValue) + { + _startStats = gossipStats; + return gossipStats; + } - Receive(_ => - { - if (_reportTo.HasValue) - _reportTo = Option.None; - }); + return gossipStats -_startStats.Value; + } - Receive(_ => - { - _startStats = Option.None; - }); + var diff = MatchStats(); + var res = new StatsResult(_cluster.SelfAddress, new ClusterEvent.CurrentInternalStats(diff, vclockStats)); + _reportTo.OnSuccess(a => a.Tell(res)); + }); - // nothing interesting here - Receive(_ => { }); - } - } + Receive(r => + { + _reportTo.OnSuccess(o => Context.Unwatch(o)); + _reportTo = r.Ref; + _reportTo.OnSuccess(n => Context.Watch(n)); + }); - /// - /// Used for remote death watch testing - /// - internal class Watchee : ActorBase - { - protected override bool Receive(object message) + Receive(_ => { - return true; - } + if (_reportTo.HasValue) + _reportTo = Option.None; + }); + + Receive(_ => + { + _startStats = Option.None; + }); + + // nothing interesting here + Receive(_ => { }); } +} - internal sealed class Begin +/// +/// Used for remote death watch testing +/// +internal class Watchee : ActorBase +{ + protected override bool Receive(object message) { - public static readonly Begin Instance = new(); - private Begin() { } + return true; } +} + +internal sealed class Begin +{ + public static readonly Begin Instance = new(); + private Begin() { } +} + +internal sealed class End +{ + public static readonly End Instance = new(); + private End() { } +} + +internal sealed class RetryTick +{ + public static readonly RetryTick Instance = new(); + private RetryTick() { } +} + +internal sealed class ReportTick +{ + public static readonly ReportTick Instance = new(); + private ReportTick() { } +} + +internal sealed class PhiTick +{ + public static readonly PhiTick Instance = new(); + private PhiTick() { } +} - internal sealed class End +internal sealed class ReportTo +{ + public ReportTo(Option @ref) { - public static readonly End Instance = new(); - private End() { } + Ref = @ref; } - internal sealed class RetryTick + public Option Ref { get; } +} + +internal sealed class StatsResult +{ + public StatsResult(Address address, ClusterEvent.CurrentInternalStats stats) { - public static readonly RetryTick Instance = new(); - private RetryTick() { } + Address = address; + Stats = stats; } - internal sealed class ReportTick + public Address Address { get; } + + public Akka.Cluster.ClusterEvent.CurrentInternalStats Stats { get; } +} + +internal sealed class Reset +{ + public static readonly Reset Instance = new(); + private Reset() { } +} + +internal class MeasureDurationUntilDown : ReceiveActor +{ + private readonly Cluster _cluster = Cluster.Get(Context.System); + private readonly long _startTime; + private readonly ILoggingAdapter _log = Context.GetLogger(); + public MeasureDurationUntilDown() { - public static readonly ReportTick Instance = new(); - private ReportTick() { } + _startTime = MonotonicClock.GetTicks(); + + Receive(d => + { + var m = d.Member; + if (m.UniqueAddress == _cluster.SelfUniqueAddress) + { + _log.Info("Downed [{0}] after [{1} ms]", _cluster.SelfAddress, TimeSpan.FromTicks(MonotonicClock.GetTicks() - _startTime).TotalMilliseconds); + } + }); + + Receive(_ => { }); } - internal sealed class PhiTick + protected override void PreStart() { - public static readonly PhiTick Instance = new(); - private PhiTick() { } + _cluster.Subscribe(Self, ClusterEvent.SubscriptionInitialStateMode.InitialStateAsSnapshot, typeof(ClusterEvent.MemberDowned)); } +} - internal sealed class ReportTo - { - public ReportTo(Option @ref) - { - Ref = @ref; - } +public class StressSpec : MultiNodeClusterSpec +{ + public StressSpecConfig.Settings Settings { get; } + public TestProbe IdentifyProbe; + + protected override TimeSpan ShutdownTimeout => Dilated(TimeSpan.FromSeconds(30)); - public Option Ref { get; } + public int Step = 0; + public int NbrUsedRoles = 0; + + public override void MuteLog(ActorSystem sys = null) + { + sys ??= Sys; + base.MuteLog(sys); + Sys.EventStream.Publish(new Mute(new ErrorFilter(typeof(ApplicationException), new ContainsString("Simulated exception")))); + MuteDeadLetters(sys, typeof(AggregatedClusterResult), typeof(StatsResult), typeof(PhiResult), typeof(RetryTick)); } - internal sealed class StatsResult + public StressSpec() : this(new StressSpecConfig()){ } + + protected StressSpec(StressSpecConfig config) : base(config, typeof(StressSpec)) { - public StatsResult(Address address, ClusterEvent.CurrentInternalStats stats) + Settings = new StressSpecConfig.Settings(Sys.Settings.Config, config.TotalNumberOfNodes); + ClusterResultHistory = new Lazy(() => { - Address = address; - Stats = stats; - } + if (Settings.Infolog) + return Sys.ActorOf(Props.Create(() => new ClusterResultHistory()), "resultHistory"); + return Sys.DeadLetters; + }); - public Address Address { get; } + PhiObserver = new Lazy(() => + { + return Sys.ActorOf(Props.Create(() => new PhiObserver()), "phiObserver"); + }); - public Akka.Cluster.ClusterEvent.CurrentInternalStats Stats { get; } + StatsObserver = new Lazy(() => + { + return Sys.ActorOf(Props.Create(() => new StatsObserver()), "statsObserver"); + }); } - internal sealed class Reset + protected override void AtStartup() { - public static readonly Reset Instance = new(); - private Reset() { } + IdentifyProbe = CreateTestProbe(); + base.AtStartup(); } - internal class MeasureDurationUntilDown : ReceiveActor + public string ClrInfo() { - private readonly Cluster _cluster = Cluster.Get(Context.System); - private readonly long _startTime; - private readonly ILoggingAdapter _log = Context.GetLogger(); - public MeasureDurationUntilDown() - { - _startTime = MonotonicClock.GetTicks(); + var sb = new StringBuilder(); + sb.Append("Operating System: ") + .Append(Environment.OSVersion.Platform) + .Append(", ") + .Append(RuntimeInformation.ProcessArchitecture.ToString()) + .Append(", ") + .Append(Environment.OSVersion.VersionString) + .AppendLine(); + + sb.Append("CLR: ") + .Append(RuntimeInformation.FrameworkDescription) + .AppendLine(); + + sb.Append("Processors: ").Append(Environment.ProcessorCount) + .AppendLine() + .Append("Load average: ").Append("can't be easily measured on .NET Core") // TODO: fix + .AppendLine() + .Append("Thread count: ") + .Append(Process.GetCurrentProcess().Threads.Count) + .AppendLine(); + + sb.Append("Memory: ") + .Append(" (") + .Append(Process.GetCurrentProcess().WorkingSet64 / 1024 / 1024) + .Append(" - ") + .Append(Process.GetCurrentProcess().PeakWorkingSet64 / 1024 / 1024) + .Append(") MB [working set / peak working set]"); + + sb.AppendLine("Args: ").Append(string.Join(Environment.NewLine, Environment.GetCommandLineArgs())) + .AppendLine(); + + return sb.ToString(); + } - Receive(d => - { - var m = d.Member; - if (m.UniqueAddress == _cluster.SelfUniqueAddress) - { - _log.Info("Downed [{0}] after [{1} ms]", _cluster.SelfAddress, TimeSpan.FromTicks(MonotonicClock.GetTicks() - _startTime).TotalMilliseconds); - } - }); + public ImmutableList SeedNodes => Roles.Take(Settings.NumberOfSeedNodes).ToImmutableList(); - Receive(_ => { }); - } + internal GossipStats LatestGossipStats => Cluster.ReadView.LatestStats.GossipStats; - protected override void PreStart() - { - _cluster.Subscribe(Self, ClusterEvent.SubscriptionInitialStateMode.InitialStateAsSnapshot, typeof(ClusterEvent.MemberDowned)); - } - } + public Lazy ClusterResultHistory { get; } + + public Lazy PhiObserver { get; } - public class StressSpec : MultiNodeClusterSpec + public Lazy StatsObserver { get; } + + public Option ClusterResultAggregator() { - public StressSpecConfig.Settings Settings { get; } - public TestProbe IdentifyProbe; + Sys.ActorSelection(new RootActorPath(GetAddress(Roles.First())) / "user" / ("result" + Step)) + .Tell(new Identify(Step), IdentifyProbe.Ref); + return Option.Create(IdentifyProbe.ExpectMsg().Subject); + } - protected override TimeSpan ShutdownTimeout => Dilated(TimeSpan.FromSeconds(30)); + public async Task CreateResultAggregatorAsync(string title, int expectedResults, bool includeInHistory) + { + RunOn(() => + { + var aggregator = Sys.ActorOf( + Props.Create(() => new ClusterResultAggregator(title, expectedResults, Settings)) + .WithDeploy(Deploy.Local), "result" + Step); - public int Step = 0; - public int NbrUsedRoles = 0; + if (includeInHistory && Settings.Infolog) + { + aggregator.Tell(new ReportTo(Option.Create(ClusterResultHistory.Value))); + } + else + { + aggregator.Tell(new ReportTo(Option.None)); + } + }, + Roles.First()); + await EnterBarrierAsync("result-aggregator-created-" + Step); - public override void MuteLog(ActorSystem sys = null) + RunOn(() => { - sys ??= Sys; - base.MuteLog(sys); - Sys.EventStream.Publish(new Mute(new ErrorFilter(typeof(ApplicationException), new ContainsString("Simulated exception")))); - MuteDeadLetters(sys, typeof(AggregatedClusterResult), typeof(StatsResult), typeof(PhiResult), typeof(RetryTick)); - } + var resultAggregator = ClusterResultAggregator(); + PhiObserver.Value.Tell(new ReportTo(resultAggregator)); + StatsObserver.Value.Tell(Reset.Instance); + StatsObserver.Value.Tell(new ReportTo(resultAggregator)); + }, Roles.Take(NbrUsedRoles).ToArray()); - public StressSpec() : this(new StressSpecConfig()){ } + } - protected StressSpec(StressSpecConfig config) : base(config, typeof(StressSpec)) + public async Task AwaitClusterResultAsync() + { + RunOn(() => { - Settings = new StressSpecConfig.Settings(Sys.Settings.Config, config.TotalNumberOfNodes); - ClusterResultHistory = new Lazy(() => + ClusterResultAggregator().OnSuccess(r => { - if (Settings.Infolog) - return Sys.ActorOf(Props.Create(() => new ClusterResultHistory()), "resultHistory"); - return Sys.DeadLetters; + Watch(r); + ExpectMsg(t => t.ActorRef.Path == r.Path); }); + }, Roles.First()); + await EnterBarrierAsync("cluster-result-done-" + Step); + } - PhiObserver = new Lazy(() => - { - return Sys.ActorOf(Props.Create(() => new PhiObserver()), "phiObserver"); - }); - - StatsObserver = new Lazy(() => - { - return Sys.ActorOf(Props.Create(() => new StatsObserver()), "statsObserver"); - }); - } - - protected override void AtStartup() - { - IdentifyProbe = CreateTestProbe(); - base.AtStartup(); - } - - public string ClrInfo() - { - var sb = new StringBuilder(); - sb.Append("Operating System: ") - .Append(Environment.OSVersion.Platform) - .Append(", ") - .Append(RuntimeInformation.ProcessArchitecture.ToString()) - .Append(", ") - .Append(Environment.OSVersion.VersionString) - .AppendLine(); - - sb.Append("CLR: ") - .Append(RuntimeInformation.FrameworkDescription) - .AppendLine(); - - sb.Append("Processors: ").Append(Environment.ProcessorCount) - .AppendLine() - .Append("Load average: ").Append("can't be easily measured on .NET Core") // TODO: fix - .AppendLine() - .Append("Thread count: ") - .Append(Process.GetCurrentProcess().Threads.Count) - .AppendLine(); - - sb.Append("Memory: ") - .Append(" (") - .Append(Process.GetCurrentProcess().WorkingSet64 / 1024 / 1024) - .Append(" - ") - .Append(Process.GetCurrentProcess().PeakWorkingSet64 / 1024 / 1024) - .Append(") MB [working set / peak working set]"); - - sb.AppendLine("Args: ").Append(string.Join(Environment.NewLine, Environment.GetCommandLineArgs())) - .AppendLine(); - - return sb.ToString(); - } - - public ImmutableList SeedNodes => Roles.Take(Settings.NumberOfSeedNodes).ToImmutableList(); - - internal GossipStats LatestGossipStats => Cluster.ReadView.LatestStats.GossipStats; - - public Lazy ClusterResultHistory { get; } - - public Lazy PhiObserver { get; } - - public Lazy StatsObserver { get; } - - public Option ClusterResultAggregator() + public async Task JoinOneByOneAsync(int numberOfNodes) + { + foreach (var i in Enumerable.Range(0, numberOfNodes)) { - Sys.ActorSelection(new RootActorPath(GetAddress(Roles.First())) / "user" / ("result" + Step)) - .Tell(new Identify(Step), IdentifyProbe.Ref); - return Option.Create(IdentifyProbe.ExpectMsg().Subject); + await JoinOneAsync(); + NbrUsedRoles += 1; + Step += 1; } + } - public void CreateResultAggregator(string title, int expectedResults, bool includeInHistory) - { - RunOn(() => - { - var aggregator = Sys.ActorOf( - Props.Create(() => new ClusterResultAggregator(title, expectedResults, Settings)) - .WithDeploy(Deploy.Local), "result" + Step); - - if (includeInHistory && Settings.Infolog) - { - aggregator.Tell(new ReportTo(Option.Create(ClusterResultHistory.Value))); - } - else - { - aggregator.Tell(new ReportTo(Option.None)); - } - }, - Roles.First()); - EnterBarrier("result-aggregator-created-" + Step); - - RunOn(() => - { - var resultAggregator = ClusterResultAggregator(); - PhiObserver.Value.Tell(new ReportTo(resultAggregator)); - StatsObserver.Value.Tell(Reset.Instance); - StatsObserver.Value.Tell(new ReportTo(resultAggregator)); - }, Roles.Take(NbrUsedRoles).ToArray()); - - } + public TimeSpan ConvergenceWithin(TimeSpan baseDuration, int nodes) + { + return TimeSpan.FromMilliseconds(baseDuration.TotalMilliseconds * Settings.ConvergenceWithinFactor * nodes); + } - public void AwaitClusterResult() + public async Task JoinOneAsync() + { + await WithinAsync(TimeSpan.FromSeconds(5) + ConvergenceWithin(TimeSpan.FromSeconds(2), NbrUsedRoles + 1), async () => { - RunOn(() => + var currentRoles = Roles.Take(NbrUsedRoles + 1).ToArray(); + var title = $"join one to {NbrUsedRoles} nodes cluster"; + await CreateResultAggregatorAsync(title, expectedResults: currentRoles.Length, includeInHistory: true); + await RunOnAsync(async () => { - ClusterResultAggregator().OnSuccess(r => + await ReportResult(async () => { - Watch(r); - ExpectMsg(t => t.ActorRef.Path == r.Path); + await RunOnAsync(async () => + { + await Cluster.JoinAsync(GetAddress(Roles.First())); + }, currentRoles.Last()); + await AwaitMembersUpAsync(currentRoles.Length, timeout: RemainingOrDefault); + return true; }); - }, Roles.First()); - EnterBarrier("cluster-result-done-" + Step); - } - - public void JoinOneByOne(int numberOfNodes) - { - foreach (var i in Enumerable.Range(0, numberOfNodes)) - { - JoinOne(); - NbrUsedRoles += 1; - Step += 1; - } - } + }, currentRoles); + await AwaitClusterResultAsync(); + await EnterBarrierAsync("join-one-" + Step); + }); + } - public TimeSpan ConvergenceWithin(TimeSpan baseDuration, int nodes) + public async Task JoinSeveralAsync(int numberOfNodes, bool toSeedNodes) + { + string FormatSeedJoin() { - return TimeSpan.FromMilliseconds(baseDuration.TotalMilliseconds * Settings.ConvergenceWithinFactor * nodes); + return toSeedNodes ? "seed nodes" : "one node"; } - public void JoinOne() - { - Within(TimeSpan.FromSeconds(5) + ConvergenceWithin(TimeSpan.FromSeconds(2), NbrUsedRoles + 1), () => + await WithinAsync(TimeSpan.FromSeconds(10) + ConvergenceWithin(TimeSpan.FromSeconds(3), NbrUsedRoles + numberOfNodes), + async () => { - var currentRoles = Roles.Take(NbrUsedRoles + 1).ToArray(); - var title = $"join one to {NbrUsedRoles} nodes cluster"; - CreateResultAggregator(title, expectedResults: currentRoles.Length, includeInHistory: true); - RunOn(() => + var currentRoles = Roles.Take(NbrUsedRoles + numberOfNodes).ToArray(); + var joiningRoles = currentRoles.Skip(NbrUsedRoles).ToArray(); + var title = $"join {numberOfNodes} to {FormatSeedJoin()}, in {NbrUsedRoles} nodes cluster"; + await CreateResultAggregatorAsync(title, expectedResults: currentRoles.Length, true); + await RunOnAsync(async () => { - ReportResult(() => + await ReportResult(async () => { RunOn(() => { - Cluster.Join(GetAddress(Roles.First())); - }, currentRoles.Last()); - AwaitMembersUp(currentRoles.Length, timeout: RemainingOrDefault); + if (toSeedNodes) + { + Cluster.JoinSeedNodes(SeedNodes.Select(GetAddress)); + } + else + { + Cluster.Join(GetAddress(Roles.First())); + } + }, joiningRoles); + await AwaitMembersUpAsync(currentRoles.Length, timeout: RemainingOrDefault); return true; }); }, currentRoles); - AwaitClusterResult(); - EnterBarrier("join-one-" + Step); + await AwaitClusterResultAsync(); + await EnterBarrierAsync("join-several-" + Step); }); - } + } - public void JoinSeveral(int numberOfNodes, bool toSeedNodes) + public async Task RemoveOneByOne(int numberOfNodes, bool shutdown) + { + foreach (var i in Enumerable.Range(0, numberOfNodes)) { - string FormatSeedJoin() - { - return toSeedNodes ? "seed nodes" : "one node"; - } - - Within(TimeSpan.FromSeconds(10) + ConvergenceWithin(TimeSpan.FromSeconds(3), NbrUsedRoles + numberOfNodes), - () => - { - var currentRoles = Roles.Take(NbrUsedRoles + numberOfNodes).ToArray(); - var joiningRoles = currentRoles.Skip(NbrUsedRoles).ToArray(); - var title = $"join {numberOfNodes} to {FormatSeedJoin()}, in {NbrUsedRoles} nodes cluster"; - CreateResultAggregator(title, expectedResults: currentRoles.Length, true); - RunOn(() => - { - ReportResult(() => - { - RunOn(() => - { - if (toSeedNodes) - { - Cluster.JoinSeedNodes(SeedNodes.Select(x => GetAddress(x))); - } - else - { - Cluster.Join(GetAddress(Roles.First())); - } - }, joiningRoles); - AwaitMembersUp(currentRoles.Length, timeout: RemainingOrDefault); - return true; - }); - }, currentRoles); - AwaitClusterResult(); - EnterBarrier("join-several-" + Step); - }); + await RemoveOneAsync(shutdown); + NbrUsedRoles -= 1; + Step += 1; } + } - public void RemoveOneByOne(int numberOfNodes, bool shutdown) + public async Task RemoveOneAsync(bool shutdown) + { + string FormatNodeLeave() { - foreach (var i in Enumerable.Range(0, numberOfNodes)) - { - RemoveOne(shutdown); - NbrUsedRoles -= 1; - Step += 1; - } + return shutdown ? "shutdown" : "remove"; } - public void RemoveOne(bool shutdown) + await WithinAsync(TimeSpan.FromSeconds(25) + ConvergenceWithin(TimeSpan.FromSeconds(3), NbrUsedRoles - 1), async () => { - string FormatNodeLeave() + var currentRoles = Roles.Take(NbrUsedRoles - 1).ToArray(); + var title = $"{FormatNodeLeave()} one from {NbrUsedRoles} nodes cluster"; + await CreateResultAggregatorAsync(title, expectedResults:currentRoles.Length, true); + + var removeRole = Roles[NbrUsedRoles - 1]; + var removeAddress = GetAddress(removeRole); + Console.WriteLine($"Preparing to {FormatNodeLeave()}[{removeAddress}] role [{removeRole.Name}] out of [{Roles.Count}]"); + RunOn(() => { - return shutdown ? "shutdown" : "remove"; - } + var watchee = Sys.ActorOf(Props.Create(() => new Watchee()), "watchee"); + Console.WriteLine("Created watchee [{0}]", watchee); + }, removeRole); + + await EnterBarrierAsync("watchee-created-" + Step); - Within(TimeSpan.FromSeconds(25) + ConvergenceWithin(TimeSpan.FromSeconds(3), NbrUsedRoles - 1), () - => + await RunOnAsync(async () => { - var currentRoles = Roles.Take(NbrUsedRoles - 1).ToArray(); - var title = $"{FormatNodeLeave()} one from {NbrUsedRoles} nodes cluster"; - CreateResultAggregator(title, expectedResults:currentRoles.Length, true); - - var removeRole = Roles[NbrUsedRoles - 1]; - var removeAddress = GetAddress(removeRole); - Console.WriteLine($"Preparing to {FormatNodeLeave()}[{removeAddress}] role [{removeRole.Name}] out of [{Roles.Count}]"); - RunOn(() => + await AwaitAssertAsync(async () => { - var watchee = Sys.ActorOf(Props.Create(() => new Watchee()), "watchee"); - Console.WriteLine("Created watchee [{0}]", watchee); - }, removeRole); + Sys.ActorSelection(new RootActorPath(removeAddress) / "user" / "watchee").Tell(new Identify("watchee"), IdentifyProbe.Ref); + var watchee = (await IdentifyProbe.ExpectMsgAsync(TimeSpan.FromSeconds(1))).Subject; + await WatchAsync(watchee); + }, interval:TimeSpan.FromSeconds(1.25d)); + + }, Roles.First()); + await EnterBarrierAsync("watchee-established-" + Step); - EnterBarrier("watchee-created-" + Step); + RunOn(() => + { + if (!shutdown) + Cluster.Leave(GetAddress(Myself)); + }, removeRole); - RunOn(() => + await RunOnAsync(async () => + { + await ReportResult(async () => { - AwaitAssert(() => + await RunOnAsync(async () => { - Sys.ActorSelection(new RootActorPath(removeAddress) / "user" / "watchee").Tell(new Identify("watchee"), IdentifyProbe.Ref); - var watchee = IdentifyProbe.ExpectMsg(TimeSpan.FromSeconds(1)).Subject; - Watch(watchee); - }, interval:TimeSpan.FromSeconds(1.25d)); - - }, Roles.First()); - EnterBarrier("watchee-established-" + Step); + if (shutdown) + { + if (Settings.Infolog) + { + Log.Info("Shutting down [{0}]", removeAddress); + } + + await TestConductor.ExitAsync(removeRole, 0); + } + }, Roles.First()); + + await AwaitMembersUpAsync(currentRoles.Length, timeout: RemainingOrDefault); + await AwaitAllReachableAsync(); + return true; + }); + }, currentRoles); + + await RunOnAsync(async () => + { + var expectedPath = new RootActorPath(removeAddress) / "user" / "watchee"; + await ExpectMsgAsync(t => t.ActorRef.Path == expectedPath); + }, Roles.First()); + + await EnterBarrierAsync("watch-verified-" + Step); + + await AwaitClusterResultAsync(); + await EnterBarrierAsync("remove-one-" + Step); + }); + } + + public async Task RemoveSeveralAsync(int numberOfNodes, bool shutdown) + { + string FormatNodeLeave() + { + return shutdown ? "shutdown" : "remove"; + } + + await WithinAsync(TimeSpan.FromSeconds(25) + ConvergenceWithin(TimeSpan.FromSeconds(5), NbrUsedRoles - numberOfNodes), + async () => + { + var currentRoles = Roles.Take(NbrUsedRoles - numberOfNodes).ToArray(); + var removeRoles = Roles.Skip(currentRoles.Length).Take(numberOfNodes).ToArray(); + var title = $"{FormatNodeLeave()} {numberOfNodes} in {NbrUsedRoles} nodes cluster"; + await CreateResultAggregatorAsync(title, expectedResults: currentRoles.Length, includeInHistory: true); RunOn(() => { if (!shutdown) + { Cluster.Leave(GetAddress(Myself)); - }, removeRole); + } + }, removeRoles); - RunOn(() => + await RunOnAsync(async () => { - ReportResult(() => + await ReportResult(async () => { - RunOn(() => + await RunOnAsync(async () => { if (shutdown) { - if (Settings.Infolog) + foreach (var role in removeRoles) { - Log.Info("Shutting down [{0}]", removeAddress); + if (Settings.Infolog) + Log.Info("Shutting down [{0}]", GetAddress(role)); + await TestConductor.ExitAsync(role, 0); } - - TestConductor.Exit(removeRole, 0).Wait(); } }, Roles.First()); - - AwaitMembersUp(currentRoles.Length, timeout: RemainingOrDefault); - AwaitAllReachable(); + await AwaitMembersUpAsync(currentRoles.Length, timeout: RemainingOrDefault); + await AwaitAllReachableAsync(); return true; }); }, currentRoles); - RunOn(() => - { - var expectedPath = new RootActorPath(removeAddress) / "user" / "watchee"; - ExpectMsg(t => t.ActorRef.Path == expectedPath); - }, Roles.First()); - - EnterBarrier("watch-verified-" + Step); - - AwaitClusterResult(); - EnterBarrier("remove-one-" + Step); + await AwaitClusterResultAsync(); + await EnterBarrierAsync("remove-several-" + Step); }); - } + } - public void RemoveSeveral(int numberOfNodes, bool shutdown) - { - string FormatNodeLeave() + public async Task PartitionSeveral(int numberOfNodes) + { + await WithinAsync(TimeSpan.FromSeconds(25) + ConvergenceWithin(TimeSpan.FromSeconds(5), NbrUsedRoles - numberOfNodes), + async () => { - return shutdown ? "shutdown" : "remove"; - } - - Within(TimeSpan.FromSeconds(25) + ConvergenceWithin(TimeSpan.FromSeconds(5), NbrUsedRoles - numberOfNodes), - () => + var currentRoles = Roles.Take(NbrUsedRoles - numberOfNodes).ToArray(); + var removeRoles = Roles.Skip(currentRoles.Length).Take(numberOfNodes).ToArray(); + var title = $"partition {numberOfNodes} in {NbrUsedRoles} nodes cluster"; + Console.WriteLine(title); + Console.WriteLine("[{0}] are blackholing [{1}]", string.Join(",", currentRoles.Select(x => x.ToString())), string.Join(",", removeRoles.Select(x => x.ToString()))); + await CreateResultAggregatorAsync(title, expectedResults: currentRoles.Length, includeInHistory: true); + + await RunOnAsync(async () => { - var currentRoles = Roles.Take(NbrUsedRoles - numberOfNodes).ToArray(); - var removeRoles = Roles.Skip(currentRoles.Length).Take(numberOfNodes).ToArray(); - var title = $"{FormatNodeLeave()} {numberOfNodes} in {NbrUsedRoles} nodes cluster"; - CreateResultAggregator(title, expectedResults: currentRoles.Length, includeInHistory: true); - - RunOn(() => + foreach (var x in currentRoles) { - if (!shutdown) + foreach (var y in removeRoles) { - Cluster.Leave(GetAddress(Myself)); + await TestConductor.BlackholeAsync(x, y, ThrottleTransportAdapter.Direction.Both); } - }, removeRoles); + } + }, Roles.First()); + await EnterBarrierAsync("partition-several-blackhole"); - RunOn(() => + await RunOnAsync(async () => + { + await ReportResult(async () => { - ReportResult(() => - { - RunOn(() => - { - if (shutdown) - { - foreach (var role in removeRoles) - { - if (Settings.Infolog) - Log.Info("Shutting down [{0}]", GetAddress(role)); - TestConductor.Exit(role, 0).Wait(RemainingOrDefault); - } - } - }, Roles.First()); - AwaitMembersUp(currentRoles.Length, timeout: RemainingOrDefault); - AwaitAllReachable(); - return true; - }); - }, currentRoles); - - AwaitClusterResult(); - EnterBarrier("remove-several-" + Step); - }); - } + var startTime = MonotonicClock.GetTicks(); + await AwaitMembersUpAsync(currentRoles.Length, timeout:RemainingOrDefault); + Sys.Log.Info("Removed [{0}] members after [{0} ms]", + removeRoles.Length, TimeSpan.FromTicks(MonotonicClock.GetTicks() - startTime).TotalMilliseconds); + await AwaitAllReachableAsync(); + return true; + }); + }, currentRoles); - public void PartitionSeveral(int numberOfNodes) - { - Within(TimeSpan.FromSeconds(25) + ConvergenceWithin(TimeSpan.FromSeconds(5), NbrUsedRoles - numberOfNodes), - () => + RunOn(() => { - var currentRoles = Roles.Take(NbrUsedRoles - numberOfNodes).ToArray(); - var removeRoles = Roles.Skip(currentRoles.Length).Take(numberOfNodes).ToArray(); - var title = $"partition {numberOfNodes} in {NbrUsedRoles} nodes cluster"; - Console.WriteLine(title); - Console.WriteLine("[{0}] are blackholing [{1}]", string.Join(",", currentRoles.Select(x => x.ToString())), string.Join(",", removeRoles.Select(x => x.ToString()))); - CreateResultAggregator(title, expectedResults: currentRoles.Length, includeInHistory: true); - - RunOn(() => + Sys.ActorOf(Props.Create()); + AwaitAssert(() => { - foreach (var x in currentRoles) - { - foreach (var y in removeRoles) - { - TestConductor.Blackhole(x, y, ThrottleTransportAdapter.Direction.Both).Wait(); - } - } - }, Roles.First()); - EnterBarrier("partition-several-blackhole"); + Cluster.IsTerminated.Should().BeTrue(); + }); + }, removeRoles); + await AwaitClusterResultAsync(); + await EnterBarrierAsync("partition-several-" + Step); + }); + } - RunOn(() => - { - ReportResult(() => - { - var startTime = MonotonicClock.GetTicks(); - AwaitMembersUp(currentRoles.Length, timeout:RemainingOrDefault); - Sys.Log.Info("Removed [{0}] members after [{0} ms]", - removeRoles.Length, TimeSpan.FromTicks(MonotonicClock.GetTicks() - startTime).TotalMilliseconds); - AwaitAllReachable(); - return true; - }); - }, currentRoles); + public T ReportResult(Func thunk) + { + var startTime = MonotonicClock.GetTicks(); + var startStats = ClusterView.LatestStats.GossipStats; - RunOn(() => - { - Sys.ActorOf(Props.Create()); - AwaitAssert(() => - { - Cluster.IsTerminated.Should().BeTrue(); - }); - }, removeRoles); - AwaitClusterResult(); - EnterBarrier("partition-several-" + Step); - }); - } + var returnValue = thunk(); - public T ReportResult(Func thunk) + ClusterResultAggregator().OnSuccess(r => { - var startTime = MonotonicClock.GetTicks(); - var startStats = ClusterView.LatestStats.GossipStats; + r.Tell(new ClusterResult(Cluster.SelfAddress, TimeSpan.FromTicks(MonotonicClock.GetTicks() - startTime), LatestGossipStats - startStats)); + }); - var returnValue = thunk(); + return returnValue; + } - ClusterResultAggregator().OnSuccess(r => - { - r.Tell(new ClusterResult(Cluster.SelfAddress, TimeSpan.FromTicks(MonotonicClock.GetTicks() - startTime), LatestGossipStats - startStats)); - }); + public async Task ReportResult(Func> thunk) + { + var startTime = MonotonicClock.GetTicks(); + var startStats = ClusterView.LatestStats.GossipStats; - return returnValue; - } + var returnValue = await thunk(); + + ClusterResultAggregator().OnSuccess(r => + { + r.Tell(new ClusterResult(Cluster.SelfAddress, TimeSpan.FromTicks(MonotonicClock.GetTicks() - startTime), LatestGossipStats - startStats)); + }); - public void ExerciseJoinRemove(string title, TimeSpan duration) + return returnValue; + } + + public async Task ExerciseJoinRemoveAsync(string title, TimeSpan duration) + { + var activeRoles = Roles.Take(Settings.NumberOfNodesJoinRemove).ToArray(); + var loopDuration = TimeSpan.FromSeconds(10) + + ConvergenceWithin(TimeSpan.FromSeconds(4), NbrUsedRoles + activeRoles.Length); + var rounds = (int)Math.Max(1.0d, (duration - loopDuration).TotalMilliseconds / loopDuration.TotalMilliseconds); + var usedRoles = Roles.Take(NbrUsedRoles).ToArray(); + var usedAddresses = usedRoles.Select(GetAddress).ToImmutableHashSet(); + + async Task> Loop(int counter, Option previousAs, + ImmutableHashSet
allPreviousAddresses) { - var activeRoles = Roles.Take(Settings.NumberOfNodesJoinRemove).ToArray(); - var loopDuration = TimeSpan.FromSeconds(10) + - ConvergenceWithin(TimeSpan.FromSeconds(4), NbrUsedRoles + activeRoles.Length); - var rounds = (int)Math.Max(1.0d, (duration - loopDuration).TotalMilliseconds / loopDuration.TotalMilliseconds); - var usedRoles = Roles.Take(NbrUsedRoles).ToArray(); - var usedAddresses = usedRoles.Select(x => GetAddress(x)).ToImmutableHashSet(); - - Option Loop(int counter, Option previousAs, - ImmutableHashSet
allPreviousAddresses) + if (counter > rounds) + return previousAs; + + var t = title + " round " + counter; + RunOn(() => { - if (counter > rounds) return previousAs; + PhiObserver.Value.Tell(Reset.Instance); + StatsObserver.Value.Tell(Reset.Instance); + }, usedRoles); + await CreateResultAggregatorAsync(t, expectedResults:NbrUsedRoles, includeInHistory:true); - var t = title + " round " + counter; - RunOn(() => - { - PhiObserver.Value.Tell(Reset.Instance); - StatsObserver.Value.Tell(Reset.Instance); - }, usedRoles); - CreateResultAggregator(t, expectedResults:NbrUsedRoles, includeInHistory:true); - - var nextAs = Option.None; - var nextAddresses = ImmutableHashSet
.Empty; - Within(loopDuration, () => + var nextAs = Option.None; + var nextAddresses = ImmutableHashSet
.Empty; + await WithinAsync(loopDuration, async () => + { + var (nextAsy, nextAddr) = await ReportResult(async () => { - var (nextAsy, nextAddr) = ReportResult(() => - { - Option nextAs; + Option nextAs; - if (activeRoles.Contains(Myself)) - { - previousAs.OnSuccess(s => - { - Shutdown(s); - }); - - var sys = ActorSystem.Create(Sys.Name, Sys.Settings.Config); - MuteLog(sys); - Akka.Cluster.Cluster.Get(sys).JoinSeedNodes(SeedNodes.Select(x => GetAddress(x))); - nextAs = Option.Create(sys); - } - else + if (activeRoles.Contains(Myself)) + { + previousAs.OnSuccess(s => { - nextAs = previousAs; - } + Shutdown(s); + }); - RunOn(() => - { - AwaitMembersUp(NbrUsedRoles + activeRoles.Length, - canNotBePartOfMemberRing: allPreviousAddresses, - timeout: RemainingOrDefault); - AwaitAllReachable(); - }, usedRoles); + var sys = ActorSystem.Create(Sys.Name, Sys.Settings.Config); + MuteLog(sys); + await Cluster.Get(sys).JoinSeedNodesAsync(SeedNodes.Select(GetAddress)); + nextAs = Option.Create(sys); + } + else + { + nextAs = previousAs; + } - nextAddresses = ClusterView.Members.Select(x => x.Address).ToImmutableHashSet() - .Except(usedAddresses); + await RunOnAsync(async () => + { + await AwaitMembersUpAsync(NbrUsedRoles + activeRoles.Length, + canNotBePartOfMemberRing: allPreviousAddresses, + timeout: RemainingOrDefault); + await AwaitAllReachableAsync(); + }, usedRoles); - RunOn(() => - { - nextAddresses.Count.Should().Be(Settings.NumberOfNodesJoinRemove); - }, usedRoles); + nextAddresses = ClusterView.Members.Select(x => x.Address).ToImmutableHashSet() + .Except(usedAddresses); - return (nextAs, nextAddresses); - }); + RunOn(() => + { + nextAddresses.Count.Should().Be(Settings.NumberOfNodesJoinRemove); + }, usedRoles); - nextAs = nextAsy; - nextAddresses = nextAddr; + return (nextAs, nextAddresses); }); - AwaitClusterResult(); - Step += 1; - return Loop(counter + 1, nextAs, nextAddresses); - } - - Loop(1, Option.None, ImmutableHashSet
.Empty).OnSuccess(aSys => - { - Shutdown(aSys); + nextAs = nextAsy; + nextAddresses = nextAddr; }); - Within(loopDuration, () => - { - RunOn(() => - { - AwaitMembersUp(NbrUsedRoles, timeout: RemainingOrDefault); - AwaitAllReachable(); - PhiObserver.Value.Tell(Reset.Instance); - StatsObserver.Value.Tell(Reset.Instance); - }, usedRoles); - }); - EnterBarrier("join-remove-shutdown-" + Step); + await AwaitClusterResultAsync(); + Step += 1; + return await Loop(counter + 1, nextAs, nextAddresses); } - public void IdleGossip(string title) + (await Loop(1, Option.None, ImmutableHashSet
.Empty)).OnSuccess(aSys => { - CreateResultAggregator(title, expectedResults: NbrUsedRoles, includeInHistory: true); - ReportResult(() => - { - ClusterView.Members.Count.Should().Be(NbrUsedRoles); - Thread.Sleep(Settings.IdleGossipDuration); - ClusterView.Members.Count.Should().Be(NbrUsedRoles); - return true; - }); - AwaitClusterResult(); - } + Shutdown(aSys); + }); - public void IncrementStep() + await WithinAsync(loopDuration, async () => { - Step += 1; - } + await RunOnAsync(async () => + { + await AwaitMembersUpAsync(NbrUsedRoles, timeout: RemainingOrDefault); + await AwaitAllReachableAsync(); + PhiObserver.Value.Tell(Reset.Instance); + StatsObserver.Value.Tell(Reset.Instance); + }, usedRoles); + }); + await EnterBarrierAsync("join-remove-shutdown-" + Step); + } - [MultiNodeFact] - public void Cluster_under_stress() + public async Task IdleGossipAsync(string title) + { + await CreateResultAggregatorAsync(title, expectedResults: NbrUsedRoles, includeInHistory: true); + await ReportResult(async () => { - MustLogSettings(); - IncrementStep(); - MustJoinSeedNodes(); - IncrementStep(); - MustJoinSeedNodesOneByOneToSmallCluster(); - IncrementStep(); - MustJoinSeveralNodesToOneNode(); - IncrementStep(); - MustJoinSeveralNodesToSeedNodes(); - IncrementStep(); - MustJoinNodesOneByOneToLargeCluster(); - IncrementStep(); - MustExerciseJoinRemoveJoinRemove(); - IncrementStep(); - MustGossipWhenIdle(); - IncrementStep(); - MustDownPartitionedNodes(); - IncrementStep(); - MustLeaveNodesOneByOneFromLargeCluster(); - IncrementStep(); - MustShutdownNodesOneByOneFromLargeCluster(); - IncrementStep(); - MustLeaveSeveralNodes(); - IncrementStep(); - MustShutdownSeveralNodes(); - IncrementStep(); - MustShutdownNodesOneByOneFromSmallCluster(); - IncrementStep(); - MustLeaveNodesOneByOneFromSmallCluster(); - IncrementStep(); - MustLogClrInfo(); - } + ClusterView.Members.Count.Should().Be(NbrUsedRoles); + await Task.Delay(Settings.IdleGossipDuration); + ClusterView.Members.Count.Should().Be(NbrUsedRoles); + return true; + }); + await AwaitClusterResultAsync(); + } + + public void IncrementStep() + { + Step += 1; + } + + [MultiNodeFact] + public async Task Cluster_under_stress() + { + await MustLogSettings(); + IncrementStep(); + await MustJoinSeedNodesAsync(); + IncrementStep(); + await MustJoinSeedNodesOneByOneToSmallClusterAsync(); + IncrementStep(); + await MustJoinSeveralNodesToOneNodeAsync(); + IncrementStep(); + await MustJoinSeveralNodesToSeedNodesAsync(); + IncrementStep(); + await MustJoinNodesOneByOneToLargeClusterAsync(); + IncrementStep(); + await MustExerciseJoinRemoveJoinRemoveAsync(); + IncrementStep(); + await MustGossipWhenIdleAsync(); + IncrementStep(); + await MustDownPartitionedNodesAsync(); + IncrementStep(); + await MustLeaveNodesOneByOneFromLargeClusterAsync(); + IncrementStep(); + await MustShutdownNodesOneByOneFromLargeClusterAsync(); + IncrementStep(); + await MustLeaveSeveralNodesAsync(); + IncrementStep(); + await MustShutdownSeveralNodesAsync(); + IncrementStep(); + await MustShutdownNodesOneByOneFromSmallClusterAsync(); + IncrementStep(); + await MustLeaveNodesOneByOneFromSmallClusterAsync(); + IncrementStep(); + await MustLogClrInfoAsync(); + } - public void MustLogSettings() + public async Task MustLogSettings() + { + if (Settings.Infolog) { - if (Settings.Infolog) + Log.Info("StressSpec CLR:" + Environment.NewLine + ClrInfo()); + RunOn(() => { - Log.Info("StressSpec CLR:" + Environment.NewLine + ClrInfo()); - RunOn(() => - { - Log.Info("StressSpec settings:" + Environment.NewLine + Settings); - }); - } - EnterBarrier("after-" + Step); + Log.Info("StressSpec settings:" + Environment.NewLine + Settings); + }); } + await EnterBarrierAsync("after-" + Step); + } - public void MustJoinSeedNodes() + public async Task MustJoinSeedNodesAsync() + { + await WithinAsync(TimeSpan.FromSeconds(30), async () => { - Within(TimeSpan.FromSeconds(30), () => - { - var otherNodesJoiningSeedNodes = Roles.Skip(Settings.NumberOfSeedNodes) - .Take(Settings.NumberOfNodesJoiningToSeedNodesInitially).ToArray(); - var size = SeedNodes.Count + otherNodesJoiningSeedNodes.Length; + var otherNodesJoiningSeedNodes = Roles.Skip(Settings.NumberOfSeedNodes) + .Take(Settings.NumberOfNodesJoiningToSeedNodesInitially).ToArray(); + var size = SeedNodes.Count + otherNodesJoiningSeedNodes.Length; - CreateResultAggregator("join seed nodes", expectedResults: size, includeInHistory: true); + await CreateResultAggregatorAsync("join seed nodes", expectedResults: size, includeInHistory: true); - RunOn(() => + await RunOnAsync(async () => + { + await ReportResult(async () => { - ReportResult(() => - { - Cluster.JoinSeedNodes(SeedNodes.Select(x => GetAddress(x))); - AwaitMembersUp(size, timeout: RemainingOrDefault); - return true; - }); - }, SeedNodes.AddRange(otherNodesJoiningSeedNodes).ToArray()); + await Cluster.JoinSeedNodesAsync(SeedNodes.Select(GetAddress)); + await AwaitMembersUpAsync(size, timeout: RemainingOrDefault); + return await Task.FromResult(true); + }); + }, SeedNodes.AddRange(otherNodesJoiningSeedNodes).ToArray()); - AwaitClusterResult(); - NbrUsedRoles += size; - EnterBarrier("after-" + Step); - }); - } + await AwaitClusterResultAsync(); + NbrUsedRoles += size; + await EnterBarrierAsync("after-" + Step); + }); + } - public void MustJoinSeedNodesOneByOneToSmallCluster() - { - JoinOneByOne(Settings.NumberOfNodesJoiningOneByOneSmall); - EnterBarrier("after-" + Step); - } + public async Task MustJoinSeedNodesOneByOneToSmallClusterAsync() + { + await JoinOneByOneAsync(Settings.NumberOfNodesJoiningOneByOneSmall); + await EnterBarrierAsync("after-" + Step); + } - public void MustJoinSeveralNodesToOneNode() - { - JoinSeveral(Settings.NumberOfNodesJoiningToOneNode, false); - NbrUsedRoles += Settings.NumberOfNodesJoiningToOneNode; - EnterBarrier("after-" + Step); - } + public async Task MustJoinSeveralNodesToOneNodeAsync() + { + await JoinSeveralAsync(Settings.NumberOfNodesJoiningToOneNode, false); + NbrUsedRoles += Settings.NumberOfNodesJoiningToOneNode; + await EnterBarrierAsync("after-" + Step); + } - public void MustJoinSeveralNodesToSeedNodes() + public async Task MustJoinSeveralNodesToSeedNodesAsync() + { + if (Settings.NumberOfNodesJoiningToSeedNodes > 0) { - if (Settings.NumberOfNodesJoiningToSeedNodes > 0) - { - JoinSeveral(Settings.NumberOfNodesJoiningToSeedNodes, true); - NbrUsedRoles += Settings.NumberOfNodesJoiningToSeedNodes; - } - EnterBarrier("after-" + Step); + await JoinSeveralAsync(Settings.NumberOfNodesJoiningToSeedNodes, true); + NbrUsedRoles += Settings.NumberOfNodesJoiningToSeedNodes; } + await EnterBarrierAsync("after-" + Step); + } - public void MustJoinNodesOneByOneToLargeCluster() - { - JoinOneByOne(Settings.NumberOfNodesJoiningOneByOneLarge); - EnterBarrier("after-" + Step); - } + public async Task MustJoinNodesOneByOneToLargeClusterAsync() + { + await JoinOneByOneAsync(Settings.NumberOfNodesJoiningOneByOneLarge); + await EnterBarrierAsync("after-" + Step); + } - public void MustExerciseJoinRemoveJoinRemove() - { - ExerciseJoinRemove("exercise join/remove", Settings.JoinRemoveDuration); - EnterBarrier("after-" + Step); - } + public async Task MustExerciseJoinRemoveJoinRemoveAsync() + { + await ExerciseJoinRemoveAsync("exercise join/remove", Settings.JoinRemoveDuration); + await EnterBarrierAsync("after-" + Step); + } - public void MustGossipWhenIdle() - { - IdleGossip("idle gossip"); - EnterBarrier("after-" + Step); - } + public async Task MustGossipWhenIdleAsync() + { + await IdleGossipAsync("idle gossip"); + await EnterBarrierAsync("after-" + Step); + } - public void MustDownPartitionedNodes() - { - PartitionSeveral(Settings.NumberOfNodesPartition); - NbrUsedRoles -= Settings.NumberOfNodesPartition; - EnterBarrier("after-" + Step); - } + public async Task MustDownPartitionedNodesAsync() + { + await PartitionSeveral(Settings.NumberOfNodesPartition); + NbrUsedRoles -= Settings.NumberOfNodesPartition; + await EnterBarrierAsync("after-" + Step); + } - public void MustLeaveNodesOneByOneFromLargeCluster() - { - RemoveOneByOne(Settings.NumberOfNodesLeavingOneByOneLarge, shutdown:false); - EnterBarrier("after-" + Step); - } + public async Task MustLeaveNodesOneByOneFromLargeClusterAsync() + { + await RemoveOneByOne(Settings.NumberOfNodesLeavingOneByOneLarge, shutdown:false); + await EnterBarrierAsync("after-" + Step); + } - public void MustShutdownNodesOneByOneFromLargeCluster() - { - RemoveOneByOne(Settings.NumberOfNodesShutdownOneByOneLarge, shutdown: true); - EnterBarrier("after-" + Step); - } + public async Task MustShutdownNodesOneByOneFromLargeClusterAsync() + { + await RemoveOneByOne(Settings.NumberOfNodesShutdownOneByOneLarge, shutdown: true); + await EnterBarrierAsync("after-" + Step); + } - public void MustLeaveSeveralNodes() - { - RemoveSeveral(Settings.NumberOfNodesLeaving, shutdown: false); - NbrUsedRoles -= Settings.NumberOfNodesLeaving; - EnterBarrier("after-" + Step); - } + public async Task MustLeaveSeveralNodesAsync() + { + await RemoveSeveralAsync(Settings.NumberOfNodesLeaving, shutdown: false); + NbrUsedRoles -= Settings.NumberOfNodesLeaving; + await EnterBarrierAsync("after-" + Step); + } - public void MustShutdownSeveralNodes() - { - RemoveSeveral(Settings.NumberOfNodesShutdown, shutdown: true); - NbrUsedRoles -= Settings.NumberOfNodesShutdown; - EnterBarrier("after-" + Step); - } + public async Task MustShutdownSeveralNodesAsync() + { + await RemoveSeveralAsync(Settings.NumberOfNodesShutdown, shutdown: true); + NbrUsedRoles -= Settings.NumberOfNodesShutdown; + await EnterBarrierAsync("after-" + Step); + } - public void MustShutdownNodesOneByOneFromSmallCluster() - { - RemoveOneByOne(Settings.NumberOfNodesShutdownOneByOneSmall, true); - EnterBarrier("after-" + Step); - } + public async Task MustShutdownNodesOneByOneFromSmallClusterAsync() + { + await RemoveOneByOne(Settings.NumberOfNodesShutdownOneByOneSmall, true); + await EnterBarrierAsync("after-" + Step); + } - public void MustLeaveNodesOneByOneFromSmallCluster() - { - RemoveOneByOne(Settings.NumberOfNodesLeavingOneByOneSmall, false); - EnterBarrier("after-" + Step); - } + public async Task MustLeaveNodesOneByOneFromSmallClusterAsync() + { + await RemoveOneByOne(Settings.NumberOfNodesLeavingOneByOneSmall, false); + await EnterBarrierAsync("after-" + Step); + } - public void MustLogClrInfo() + public async Task MustLogClrInfoAsync() + { + if (Settings.Infolog) { - if (Settings.Infolog) - { - Log.Info("StressSpec CLR: " + Environment.NewLine + "{0}", ClrInfo()); - } - EnterBarrier("after-" + Step); + Log.Info("StressSpec CLR: " + Environment.NewLine + "{0}", ClrInfo()); } + await EnterBarrierAsync("after-" + Step); } -} +} \ No newline at end of file diff --git a/src/core/Akka.Cluster.Tests/MemberOrderingModelBasedTests.cs b/src/core/Akka.Cluster.Tests/MemberOrderingModelBasedTests.cs index 6b3e43b9752..ac0ec8220a1 100644 --- a/src/core/Akka.Cluster.Tests/MemberOrderingModelBasedTests.cs +++ b/src/core/Akka.Cluster.Tests/MemberOrderingModelBasedTests.cs @@ -56,7 +56,7 @@ public Property DistinctMemberAddressesMustCompareDifferently(Address[] addresse a1 = a2; // next member } - return true.Classify(true, "all distinct").Classify(false, "empty set"); ; + return true.Classify(true, "all distinct").Classify(false, "empty set"); } } diff --git a/src/core/Akka.Cluster/SplitBrainResolver.cs b/src/core/Akka.Cluster/SplitBrainResolver.cs index 6c6e4ac80bb..de28808283b 100644 --- a/src/core/Akka.Cluster/SplitBrainResolver.cs +++ b/src/core/Akka.Cluster/SplitBrainResolver.cs @@ -257,10 +257,8 @@ public static Actor.Props Props(TimeSpan stableAfter, ISplitBrainStrategy strate public SplitBrainDecider(TimeSpan stableAfter, ISplitBrainStrategy strategy, Cluster cluster) { - if (strategy == null) throw new ArgumentNullException(nameof(strategy)); - _stabilityTimeout = stableAfter; - _strategy = strategy; + _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); _cluster = cluster; } diff --git a/src/core/Akka.Discovery.Tests/Aggregate/AggregateServiceDiscoverySpec.cs b/src/core/Akka.Discovery.Tests/Aggregate/AggregateServiceDiscoverySpec.cs index 8427d289f8f..c4bfdbace28 100644 --- a/src/core/Akka.Discovery.Tests/Aggregate/AggregateServiceDiscoverySpec.cs +++ b/src/core/Akka.Discovery.Tests/Aggregate/AggregateServiceDiscoverySpec.cs @@ -72,9 +72,9 @@ public AggregateServiceDiscoverySpec(ITestOutputHelper output) } [Fact] - public void Aggregate_service_discovery_must_only_call_first_one_if_returns_results() + public async Task Aggregate_service_discovery_must_only_call_first_one_if_returns_results() { - var result = _discovery.Lookup("stubbed", 100.Milliseconds()).Result; + var result = await _discovery.Lookup("stubbed", 100.Milliseconds()); result.Should().Be(new ServiceDiscovery.Resolved( "stubbed", new List @@ -84,9 +84,9 @@ public void Aggregate_service_discovery_must_only_call_first_one_if_returns_resu } [Fact] - public void Aggregate_service_discovery_must_move_onto_the_next_if_no_resolved_targets() + public async Task Aggregate_service_discovery_must_move_onto_the_next_if_no_resolved_targets() { - var result = _discovery.Lookup("config1", 100.Milliseconds()).Result; + var result = await _discovery.Lookup("config1", 100.Milliseconds()); result.Should().Be(new ServiceDiscovery.Resolved( "config1", new List @@ -95,11 +95,11 @@ public void Aggregate_service_discovery_must_move_onto_the_next_if_no_resolved_t new("dog", 1234) })); } - + [Fact] - public void Aggregate_service_discovery_must_move_onto_next_if_fails() + public async Task Aggregate_service_discovery_must_move_onto_next_if_fails() { - var result = _discovery.Lookup("fail", 100.Milliseconds()).Result; + var result = await _discovery.Lookup("fail", 100.Milliseconds()); // Stub fails then result comes from config result.Should().Be(new ServiceDiscovery.Resolved( "fail", diff --git a/src/core/Akka.Discovery.Tests/Config/ConfigServiceDiscoverySpec.cs b/src/core/Akka.Discovery.Tests/Config/ConfigServiceDiscoverySpec.cs index f45c4369e7d..de5c92ed7ef 100644 --- a/src/core/Akka.Discovery.Tests/Config/ConfigServiceDiscoverySpec.cs +++ b/src/core/Akka.Discovery.Tests/Config/ConfigServiceDiscoverySpec.cs @@ -5,6 +5,7 @@ // //----------------------------------------------------------------------- +using System.Threading.Tasks; using Akka.Configuration; using FluentAssertions; using FluentAssertions.Extensions; @@ -44,9 +45,9 @@ public ConfigServiceDiscoverySpec() } [Fact] - public void Config_discovery_must_load_from_config() + public async Task Config_discovery_must_load_from_config() { - var result = _discovery.Lookup("service1", 100.Milliseconds()).Result; + var result = await _discovery.Lookup("service1", 100.Milliseconds()); result.ServiceName.Should().Be("service1"); result.Addresses.Should().Contain(new[] { @@ -56,9 +57,9 @@ public void Config_discovery_must_load_from_config() } [Fact] - public void Config_discovery_must_return_no_resolved_targets_if_not_in_config() + public async Task Config_discovery_must_return_no_resolved_targets_if_not_in_config() { - var result = _discovery.Lookup("dontexist", 100.Milliseconds()).Result; + var result = await _discovery.Lookup("dontexist", 100.Milliseconds()); result.ServiceName.Should().Be("dontexist"); result.Addresses.Should().BeEmpty(); } diff --git a/src/core/Akka.Docs.Tests/Testkit/ParentSampleTest.cs b/src/core/Akka.Docs.Tests/Testkit/ParentSampleTest.cs index 538358fcafc..30c3ba745ec 100644 --- a/src/core/Akka.Docs.Tests/Testkit/ParentSampleTest.cs +++ b/src/core/Akka.Docs.Tests/Testkit/ParentSampleTest.cs @@ -11,6 +11,7 @@ using Akka.TestKit.Xunit2; using Xunit; +#pragma warning disable CS0414 // Field is assigned but its value is never used. This is for documentation purposes, its fine. namespace DocsExamples.Testkit { public class ParentSampleTest : TestKit diff --git a/src/core/Akka.Docs.Tutorials/Tutorial4/DeviceGroup.cs b/src/core/Akka.Docs.Tutorials/Tutorial4/DeviceGroup.cs index 4dab18f4496..c29e88711f1 100644 --- a/src/core/Akka.Docs.Tutorials/Tutorial4/DeviceGroup.cs +++ b/src/core/Akka.Docs.Tutorials/Tutorial4/DeviceGroup.cs @@ -96,7 +96,6 @@ public class DeviceGroup : UntypedActor { private Dictionary deviceIdToActor = new(); private Dictionary actorToDeviceId = new(); - private long nextCollectionId = 0L; public DeviceGroup(string groupId) { diff --git a/src/core/Akka.Docs.Tutorials/Tutorial4/DeviceGroupQueryInProgress.cs b/src/core/Akka.Docs.Tutorials/Tutorial4/DeviceGroupQueryInProgress.cs index 98429808e6f..70219bd2b5c 100644 --- a/src/core/Akka.Docs.Tutorials/Tutorial4/DeviceGroupQueryInProgress.cs +++ b/src/core/Akka.Docs.Tutorials/Tutorial4/DeviceGroupQueryInProgress.cs @@ -75,9 +75,7 @@ public static class DeviceGroupInProgress2 #region query-added public class DeviceGroup : UntypedActor { - private Dictionary deviceIdToActor = new(); private Dictionary actorToDeviceId = new(); - private long nextCollectionId = 0L; public DeviceGroup(string groupId) { diff --git a/src/core/Akka.FSharp/FsApi.fs b/src/core/Akka.FSharp/FsApi.fs index b19a30e012c..e8a4709db8d 100644 --- a/src/core/Akka.FSharp/FsApi.fs +++ b/src/core/Akka.FSharp/FsApi.fs @@ -29,10 +29,10 @@ module Serialization = type ExprSerializer(system) = inherit Serializer(system) let fsp = FsPickler.CreateBinarySerializer() - override __.Identifier = 99 - override __.IncludeManifest = true - override __.ToBinary(o) = serializeToBinary fsp o - override __.FromBinary(bytes, _) = deserializeFromBinary fsp bytes + override _.Identifier = 99 + override _.IncludeManifest = true + override _.ToBinary(o) = serializeToBinary fsp o + override _.FromBinary(bytes, _) = deserializeFromBinary fsp bytes let exprSerializationSupport (system: ActorSystem) = @@ -176,7 +176,7 @@ module Actors = type ActorBuilder() = /// Binds the next message. - member __.Bind(m : IO<'In>, f : 'In -> _) = Func(fun m -> f m) + member _.Bind(m : IO<'In>, f : 'In -> _) = Func(fun m -> f m) /// Binds the result of another actor computation expression. member this.Bind(x : Cont<'In, 'Out1>, f : 'Out1 -> Cont<'In, 'Out2>) : Cont<'In, 'Out2> = @@ -184,9 +184,9 @@ module Actors = | Func fx -> Func(fun m -> this.Bind(fx m, f)) | Return v -> f v - member __.ReturnFrom(x) = x - member __.Return x = Return x - member __.Zero() = Return() + member _.ReturnFrom(x) = x + member _.Return x = Return x + member _.Zero() = Return() member this.TryWith(f : unit -> Cont<'In, 'Out>, c : exn -> Cont<'In, 'Out>) : Cont<'In, 'Out> = try @@ -222,7 +222,7 @@ module Actors = | v -> this.While(condition, f) else Return() - member __.For(source : 'Iter seq, f : 'Iter -> Cont<'In, unit>) : Cont<'In, unit> = + member _.For(source : 'Iter seq, f : 'Iter -> Cont<'In, unit>) : Cont<'In, unit> = use e = source.GetEnumerator() let rec loop() = @@ -236,9 +236,9 @@ module Actors = else Return() loop() - member __.Delay(f : unit -> Cont<_, _>) = f - member __.Run(f : unit -> Cont<_, _>) = f() - member __.Run(f : Cont<_, _>) = f + member _.Delay(f : unit -> Cont<_, _>) = f + member _.Run(f : unit -> Cont<_, _>) = f() + member _.Run(f : Cont<_, _>) = f member this.Combine(f : unit -> Cont<'In, _>, g : unit -> Cont<'In, 'Out>) : Cont<'In, 'Out> = match f() with @@ -268,26 +268,26 @@ module Actors = let self' = this.Self let context = UntypedActor.Context :> IActorContext actor { new Actor<'Message> with - member __.Receive() = Input - member __.Self = self' - member __.Context = context - member __.Sender() = this.Sender() - member __.Unhandled msg = this.Unhandled msg - member __.ActorOf(props, name) = context.ActorOf(props, name) - member __.ActorSelection(path : string) = context.ActorSelection(path) - member __.ActorSelection(path : ActorPath) = context.ActorSelection(path) - member __.Watch(aref:IActorRef) = context.Watch aref - member __.WatchWith(aref:IActorRef, msg) = context.WatchWith (aref, msg) - member __.Unwatch(aref:IActorRef) = context.Unwatch aref - member __.Log = lazy (Akka.Event.Logging.GetLogger(context)) - member __.Defer fn = deferables <- fn::deferables - member __.Stash() = (this :> IWithUnboundedStash).Stash.Stash() - member __.Unstash() = (this :> IWithUnboundedStash).Stash.Unstash() - member __.UnstashAll() = (this :> IWithUnboundedStash).Stash.UnstashAll() } + member _.Receive() = Input + member _.Self = self' + member _.Context = context + member _.Sender() = this.Sender() + member _.Unhandled msg = this.Unhandled msg + member _.ActorOf(props, name) = context.ActorOf(props, name) + member _.ActorSelection(path : string) = context.ActorSelection(path) + member _.ActorSelection(path : ActorPath) = context.ActorSelection(path) + member _.Watch(aref:IActorRef) = context.Watch aref + member _.WatchWith(aref:IActorRef, msg) = context.WatchWith (aref, msg) + member _.Unwatch(aref:IActorRef) = context.Unwatch aref + member _.Log = lazy (Akka.Event.Logging.GetLogger(context)) + member _.Defer fn = deferables <- fn::deferables + member _.Stash() = (this :> IWithUnboundedStash).Stash.Stash() + member _.Unstash() = (this :> IWithUnboundedStash).Stash.Unstash() + member _.UnstashAll() = (this :> IWithUnboundedStash).Stash.UnstashAll() } new(actor : Expr -> Cont<'Message, 'Returned>>) = FunActor(QuotationEvaluator.Evaluate actor) - member __.Sender() : IActorRef = base.Sender - member __.Unhandled msg = base.Unhandled msg + member _.Sender() : IActorRef = base.Sender + member _.Unhandled msg = base.Unhandled msg override x.OnReceive msg = match state with | Func f -> @@ -440,7 +440,7 @@ module internal OptionHelper = open Akka.Util type ExprDeciderSurrogate(serializedExpr: byte array) = - member __.SerializedExpr = serializedExpr + member _.SerializedExpr = serializedExpr interface ISurrogate with member this.FromSurrogate _ = let fsp = MBrace.FsPickler.FsPickler.CreateBinarySerializer() @@ -448,7 +448,7 @@ type ExprDeciderSurrogate(serializedExpr: byte array) = ExprDecider(expr) :> ISurrogated and ExprDecider (expr: Expr<(exn->Directive)>) = - member __.Expr = expr + member _.Expr = expr member private this.Compiled = lazy (QuotationEvaluator.Evaluate this.Expr) interface IDecider with member this.Decide (e: exn): Directive = this.Compiled.Value (e) diff --git a/src/core/Akka.Persistence.FSharp/FsApi.fs b/src/core/Akka.Persistence.FSharp/FsApi.fs index 40702fde377..57b76080b42 100644 --- a/src/core/Akka.Persistence.FSharp/FsApi.fs +++ b/src/core/Akka.Persistence.FSharp/FsApi.fs @@ -127,7 +127,7 @@ type FunPersistentActor<'Command, 'Event, 'State>(aggregate: Aggregate<'Command, member __.Self = self' member __.Context = context member __.Sender() = this.Sender() - member __.Unhandled msg = this.Unhandled msg + member __.Unhandled msg = this.CallUnhandled msg member __.ActorOf(props, name) = context.ActorOf(props, name) member __.ActorSelection(path : string) = context.ActorSelection(path) member __.ActorSelection(path : ActorPath) = context.ActorSelection(path) @@ -151,7 +151,8 @@ type FunPersistentActor<'Command, 'Event, 'State>(aggregate: Aggregate<'Command, member __.DeleteSnapshots criteria = this.DeleteSnapshots(criteria) } member __.Sender() : IActorRef = base.Sender - member __.Unhandled msg = base.Unhandled msg + override __.Unhandled msg = base.Unhandled msg + member private _.CallUnhandled msg = base.Unhandled msg override x.OnCommand (msg: obj) = match msg with | :? 'Command as cmd -> aggregate.exec mailbox state cmd @@ -273,7 +274,7 @@ type Deliverer<'Command, 'Event, 'State>(aggregate: DeliveryAggregate<'Command, let mutable state: 'State = aggregate.state let mailbox = let self' = this.Self - let context = AtLeastOnceDeliveryActor.Context :> IActorContext + let context = AtLeastOnceDeliveryActor.Context let updateState (updater: 'Event -> 'State) e : unit = state <- updater e () @@ -281,7 +282,7 @@ type Deliverer<'Command, 'Event, 'State>(aggregate: DeliveryAggregate<'Command, member __.Self = self' member __.Context = context member __.Sender() = this.Sender() - member __.Unhandled msg = this.Unhandled msg + member __.Unhandled msg = this.CallUnhandled msg member __.ActorOf(props, name) = context.ActorOf(props, name) member __.ActorSelection(path : string) = context.ActorSelection(path) member __.ActorSelection(path : ActorPath) = context.ActorSelection(path) @@ -310,7 +311,8 @@ type Deliverer<'Command, 'Event, 'State>(aggregate: DeliveryAggregate<'Command, member __.UnconfirmedCount() = this.UnconfirmedCount } member __.Sender() : IActorRef = base.Sender - member __.Unhandled msg = base.Unhandled msg + override __.Unhandled msg = base.Unhandled msg + member private _.CallUnhandled msg = base.Unhandled msg override x.ReceiveCommand (msg: obj) = match msg with | :? 'Command as cmd -> aggregate.exec mailbox state cmd diff --git a/src/core/Akka.Persistence.Query/PersistenceQuery.cs b/src/core/Akka.Persistence.Query/PersistenceQuery.cs index df7f1fe101a..7e17018942f 100644 --- a/src/core/Akka.Persistence.Query/PersistenceQuery.cs +++ b/src/core/Akka.Persistence.Query/PersistenceQuery.cs @@ -16,6 +16,8 @@ namespace Akka.Persistence.Query { public sealed class PersistenceQuery : IExtension { + private static readonly Type ReadJournalType = typeof(IReadJournal); + private readonly ExtendedActorSystem _system; private readonly ConcurrentDictionary _readJournalPluginExtensionIds = new(); private ILoggingAdapter _log; @@ -34,18 +36,24 @@ public PersistenceQuery(ExtendedActorSystem system) } public TJournal ReadJournalFor(string readJournalPluginId) where TJournal : IReadJournal + => (TJournal) ReadJournalFor(typeof(TJournal), readJournalPluginId); + + public IReadJournal ReadJournalFor(Type readJournalType, string readJournalPluginId) { + if(!ReadJournalType.IsAssignableFrom(readJournalType)) + throw new ArgumentException("Must implement IReadJournal interface", nameof(readJournalType)); + if(_readJournalPluginExtensionIds.TryGetValue(readJournalPluginId, out var plugin)) - return (TJournal)plugin; + return plugin; lock (_lock) { if (_readJournalPluginExtensionIds.TryGetValue(readJournalPluginId, out plugin)) - return (TJournal)plugin; + return plugin; - plugin = CreatePlugin(readJournalPluginId, GetDefaultConfig()).GetReadJournal(); + plugin = CreatePlugin(readJournalPluginId, GetDefaultConfig(readJournalType)).GetReadJournal(); _readJournalPluginExtensionIds[readJournalPluginId] = plugin; - return (TJournal)plugin; + return plugin; } } @@ -79,8 +87,11 @@ private IReadJournalProvider CreateType(Type pluginType, object[] parameters) } public static Config GetDefaultConfig() + => GetDefaultConfig(typeof(TJournal)); + + public static Config GetDefaultConfig(Type journalType) { - var defaultConfigMethod = typeof(TJournal).GetMethod("DefaultConfiguration", BindingFlags.Public | BindingFlags.Static); + var defaultConfigMethod = journalType.GetMethod("DefaultConfiguration", BindingFlags.Public | BindingFlags.Static); return defaultConfigMethod?.Invoke(null, null) as Config; } } diff --git a/src/core/Akka.Persistence.TCK/Performance/JournalPerfSpec.cs b/src/core/Akka.Persistence.TCK/Performance/JournalPerfSpec.cs index 56f1e5548cf..f2030d05aec 100644 --- a/src/core/Akka.Persistence.TCK/Performance/JournalPerfSpec.cs +++ b/src/core/Akka.Persistence.TCK/Performance/JournalPerfSpec.cs @@ -66,7 +66,7 @@ protected JournalPerfSpec(Config config, string actorSystem, ITestOutputHelper o internal IActorRef BenchActor(string pid, int replyAfter) { - return Sys.ActorOf(Props.Create(() => new BenchActor(pid, _testProbe, EventsCount, false)));; + return Sys.ActorOf(Props.Create(() => new BenchActor(pid, _testProbe, EventsCount, false))); } internal (IActorRef aut,TestProbe probe) BenchActorNewProbe(string pid, int replyAfter) diff --git a/src/core/Akka.Persistence.TCK/Query/AllEventsSpec.cs b/src/core/Akka.Persistence.TCK/Query/AllEventsSpec.cs index 1fc4d9d04a0..0b96f8f8c8a 100644 --- a/src/core/Akka.Persistence.TCK/Query/AllEventsSpec.cs +++ b/src/core/Akka.Persistence.TCK/Query/AllEventsSpec.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; using Akka.Actor; using Akka.Configuration; using Akka.Persistence.Query; @@ -70,7 +71,7 @@ public virtual void ReadJournal_query_AllEvents_should_find_new_events() } [Fact] - public virtual void ReadJournal_query_AllEvents_should_find_events_from_offset_exclusive() + public virtual async Task ReadJournal_query_AllEvents_should_find_events_from_offset_exclusive() { var queries = ReadJournal as IAllEventsQuery; @@ -79,23 +80,24 @@ public virtual void ReadJournal_query_AllEvents_should_find_events_from_offset_e var c = Sys.ActorOf(Query.TestActor.Props("c")); a.Tell("keep"); - ExpectMsg("keep-done"); + await ExpectMsgAsync("keep-done"); a.Tell("calm"); - ExpectMsg("calm-done"); + await ExpectMsgAsync("calm-done"); b.Tell("and"); - ExpectMsg("and-done"); + await ExpectMsgAsync("and-done"); a.Tell("keep"); - ExpectMsg("keep-done"); + await ExpectMsgAsync("keep-done"); a.Tell("streaming"); - ExpectMsg("streaming-done"); + await ExpectMsgAsync("streaming-done"); var eventSrc1 = queries.AllEvents(NoOffset.Instance); var probe1 = eventSrc1.RunWith(this.SinkProbe(), Materializer); probe1.Request(4); - probe1.ExpectNext(p => p.PersistenceId == "a" && p.SequenceNr == 1L && p.Event.Equals("keep")); - probe1.ExpectNext(p => p.PersistenceId == "a" && p.SequenceNr == 2L && p.Event.Equals("calm")); - probe1.ExpectNext(p => p.PersistenceId == "b" && p.SequenceNr == 1L && p.Event.Equals("and")); - var offs = probe1.ExpectNext(p => p.PersistenceId == "a" && p.SequenceNr == 3L && p.Event.Equals("keep")).Offset; + await probe1.ExpectNextAsync(p => p.PersistenceId == "a" && p.SequenceNr == 1L && p.Event.Equals("keep")); + await probe1.ExpectNextAsync(p => p.PersistenceId == "a" && p.SequenceNr == 2L && p.Event.Equals("calm")); + await probe1.ExpectNextAsync(p => p.PersistenceId == "b" && p.SequenceNr == 1L && p.Event.Equals("and")); + var keepEvent = await probe1.ExpectNextAsync(p => p.PersistenceId == "a" && p.SequenceNr == 3L && p.Event.Equals("keep")); + var offs = keepEvent.Offset; probe1.Cancel(); var eventSrc2 = queries.AllEvents(offs); @@ -103,14 +105,14 @@ public virtual void ReadJournal_query_AllEvents_should_find_events_from_offset_e probe2.Request(10); b.Tell("new"); - ExpectMsg("new-done"); + await ExpectMsgAsync("new-done"); c.Tell("events"); - ExpectMsg("events-done"); + await ExpectMsgAsync("events-done"); // everything before "streaming" are not included, since exclusive offset - probe2.ExpectNext(p => p.PersistenceId == "a" && p.SequenceNr == 4L && p.Event.Equals("streaming")); - probe2.ExpectNext(p => p.PersistenceId == "b" && p.SequenceNr == 2L && p.Event.Equals("new")); - probe2.ExpectNext(p => p.PersistenceId == "c" && p.SequenceNr == 1L && p.Event.Equals("events")); + await probe2.ExpectNextAsync(p => p.PersistenceId == "a" && p.SequenceNr == 4L && p.Event.Equals("streaming")); + await probe2.ExpectNextAsync(p => p.PersistenceId == "b" && p.SequenceNr == 2L && p.Event.Equals("new")); + await probe2.ExpectNextAsync(p => p.PersistenceId == "c" && p.SequenceNr == 1L && p.Event.Equals("events")); probe2.Cancel(); } } diff --git a/src/core/Akka.Persistence.Tests/AtLeastOnceDeliveryCrashSpec.cs b/src/core/Akka.Persistence.Tests/AtLeastOnceDeliveryCrashSpec.cs index 6b9d62761d7..7d5b3f53014 100644 --- a/src/core/Akka.Persistence.Tests/AtLeastOnceDeliveryCrashSpec.cs +++ b/src/core/Akka.Persistence.Tests/AtLeastOnceDeliveryCrashSpec.cs @@ -74,7 +74,7 @@ internal class CrashingActor : AtLeastOnceDeliveryActor private readonly IActorRef _testProbe; private ILoggingAdapter _adapter; - ILoggingAdapter Log { get { return _adapter ??= Context.GetLogger(); } } + protected override ILoggingAdapter Log { get { return _adapter ??= Context.GetLogger(); } } public CrashingActor(IActorRef testProbe) { diff --git a/src/core/Akka.Persistence.Tests/AtLeastOnceDeliveryFailureSpec.cs b/src/core/Akka.Persistence.Tests/AtLeastOnceDeliveryFailureSpec.cs index ed12708d2c7..c6e73ac9943 100644 --- a/src/core/Akka.Persistence.Tests/AtLeastOnceDeliveryFailureSpec.cs +++ b/src/core/Akka.Persistence.Tests/AtLeastOnceDeliveryFailureSpec.cs @@ -134,7 +134,7 @@ internal class ChaosSender : AtLeastOnceDeliveryActor private readonly double _replayProcessingFailureRate; private ILoggingAdapter _log; - public ILoggingAdapter Log { get { return _log ??= Context.GetLogger(); }} + protected override ILoggingAdapter Log { get { return _log ??= Context.GetLogger(); }} public ChaosSender(IActorRef destination, IActorRef probe) : base(x => x.WithRedeliverInterval(TimeSpan.FromMilliseconds(500))) diff --git a/src/core/Akka.Persistence.Tests/AtLeastOnceDeliveryReceiveActorSpec.cs b/src/core/Akka.Persistence.Tests/AtLeastOnceDeliveryReceiveActorSpec.cs index 2acbda69da6..818f7defea9 100644 --- a/src/core/Akka.Persistence.Tests/AtLeastOnceDeliveryReceiveActorSpec.cs +++ b/src/core/Akka.Persistence.Tests/AtLeastOnceDeliveryReceiveActorSpec.cs @@ -128,6 +128,8 @@ public override bool Equals(object obj) { return obj is InvalidReq; } + + public override int GetHashCode() => 19; } internal class Receiver : AtLeastOnceDeliveryReceiveActor @@ -272,6 +274,8 @@ public override bool Equals(object obj) { return obj is ReqAck; } + + public override int GetHashCode() => 31; } [Serializable] diff --git a/src/core/Akka.Persistence.Tests/MemoryEventAdapterSpec.cs b/src/core/Akka.Persistence.Tests/MemoryEventAdapterSpec.cs index 3e7e84e6324..4ddebfb7dde 100644 --- a/src/core/Akka.Persistence.Tests/MemoryEventAdapterSpec.cs +++ b/src/core/Akka.Persistence.Tests/MemoryEventAdapterSpec.cs @@ -29,8 +29,8 @@ public interface IJournalModel [Serializable] public sealed class Tagged : IJournalModel, IEquatable { - public object Payload { get; private set; } - public ISet Tags { get; private set; } + public object Payload { get; } + public ISet Tags { get; } public Tagged(object payload, ISet tags) { @@ -47,6 +47,16 @@ public override bool Equals(object obj) { return Equals(obj as IJournalModel); } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Payload.GetHashCode(); + hashCode = (hashCode * 397) ^ Tags.GetHashCode(); + return hashCode; + } + } } [Serializable] diff --git a/src/core/Akka.Persistence.Tests/ReceivePersistentActorAsyncAwaitSpec.cs b/src/core/Akka.Persistence.Tests/ReceivePersistentActorAsyncAwaitSpec.cs index 688190b195c..23506082131 100644 --- a/src/core/Akka.Persistence.Tests/ReceivePersistentActorAsyncAwaitSpec.cs +++ b/src/core/Akka.Persistence.Tests/ReceivePersistentActorAsyncAwaitSpec.cs @@ -315,7 +315,9 @@ public AsyncTplActor(string persistenceId) RunTask(async () => { await Task.Delay(TimeSpan.FromSeconds(1)) +#pragma warning disable AK1005 // Actor context thread safety is being tested here .ContinueWith(_ => { Sender.Tell("done"); }); +#pragma warning restore AK1005 }); }); } diff --git a/src/core/Akka.Persistence/AtLeastOnceDeliverySemantic.cs b/src/core/Akka.Persistence/AtLeastOnceDeliverySemantic.cs index 76437760534..e25b58b27d4 100644 --- a/src/core/Akka.Persistence/AtLeastOnceDeliverySemantic.cs +++ b/src/core/Akka.Persistence/AtLeastOnceDeliverySemantic.cs @@ -37,12 +37,9 @@ public sealed class AtLeastOnceDeliverySnapshot : IMessage, IEquatable public AtLeastOnceDeliverySnapshot(long currentDeliveryId, UnconfirmedDelivery[] unconfirmedDeliveries) { - if (unconfirmedDeliveries == null) - throw new ArgumentNullException(nameof(unconfirmedDeliveries), - "AtLeastOnceDeliverySnapshot expects not null array of unconfirmed deliveries"); - CurrentDeliveryId = currentDeliveryId; - UnconfirmedDeliveries = unconfirmedDeliveries; + UnconfirmedDeliveries = unconfirmedDeliveries ?? throw new ArgumentNullException(nameof(unconfirmedDeliveries), + "AtLeastOnceDeliverySnapshot expects not null array of unconfirmed deliveries"); } /// @@ -97,14 +94,9 @@ public sealed class UnconfirmedWarning : IEquatable /// /// This exception is thrown when the specified array is undefined. /// - public UnconfirmedWarning(UnconfirmedDelivery[] unconfirmedDeliveries) - { - if (unconfirmedDeliveries == null) - throw new ArgumentNullException(nameof(unconfirmedDeliveries), - "UnconfirmedWarning expects not null array of unconfirmed deliveries"); - - UnconfirmedDeliveries = unconfirmedDeliveries; - } + public UnconfirmedWarning(UnconfirmedDelivery[] unconfirmedDeliveries) => + UnconfirmedDeliveries = unconfirmedDeliveries ?? throw new ArgumentNullException(nameof(unconfirmedDeliveries), + "UnconfirmedWarning expects not null array of unconfirmed deliveries"); /// /// TBD diff --git a/src/core/Akka.Persistence/Eventsourced.Lifecycle.cs b/src/core/Akka.Persistence/Eventsourced.Lifecycle.cs index 801d0bc11c9..85a5e93b6b3 100644 --- a/src/core/Akka.Persistence/Eventsourced.Lifecycle.cs +++ b/src/core/Akka.Persistence/Eventsourced.Lifecycle.cs @@ -93,6 +93,10 @@ public override void AroundPostRestart(Exception reason, object message) /// public override void AroundPostStop() { + // This is declared in Eventsourced.Recovery.cs + _timeoutCancelable?.Cancel(); + _timeoutCancelable = null; + try { _internalStash.UnstashAll(); diff --git a/src/core/Akka.Persistence/Eventsourced.Recovery.cs b/src/core/Akka.Persistence/Eventsourced.Recovery.cs index 71062a844be..4a1a1c59d5a 100644 --- a/src/core/Akka.Persistence/Eventsourced.Recovery.cs +++ b/src/core/Akka.Persistence/Eventsourced.Recovery.cs @@ -32,6 +32,8 @@ public EventsourcedState(string name, Func isRecoveryRunning, StateReceive public abstract partial class Eventsourced { + private ICancelable? _timeoutCancelable; + /// /// Initial state. Before starting the actual recovery it must get a permit from the `RecoveryPermitter`. /// When starting many persistent actors at the same time the journal and its data store is protected from @@ -61,7 +63,8 @@ private EventsourcedState RecoveryStarted(long maxReplays) { // protect against snapshot stalling forever because journal overloaded and such var timeout = Extension.JournalConfigFor(JournalPluginId).GetTimeSpan("recovery-event-timeout", null, false); - var timeoutCancelable = Context.System.Scheduler.ScheduleTellOnceCancelable(timeout, Self, new RecoveryTick(true), Self); + _timeoutCancelable?.Cancel(); + _timeoutCancelable = Context.System.Scheduler.ScheduleTellOnceCancelable(timeout, Self, new RecoveryTick(true), Self); var snapshotIsOptional = Extension.SnapshotStoreConfigFor(SnapshotPluginId).GetBoolean("snapshot-is-optional", false); @@ -89,7 +92,8 @@ bool RecoveryBehavior(object message) { case LoadSnapshotResult res: { - timeoutCancelable.Cancel(); + _timeoutCancelable?.Cancel(); + _timeoutCancelable = null; if (res.Snapshot != null) { var offer = new SnapshotOffer(res.Snapshot.Metadata, res.Snapshot.Snapshot); @@ -122,7 +126,8 @@ bool RecoveryBehavior(object message) break; } case LoadSnapshotFailed failed: - timeoutCancelable.Cancel(); + _timeoutCancelable?.Cancel(); + _timeoutCancelable = null; if (snapshotIsOptional) { Log.Info("Snapshot load error for persistenceId [{0}]. Replaying all events since snapshot-is-optional=true", PersistenceId); @@ -162,7 +167,8 @@ bool RecoveryBehavior(object message) } catch (Exception) { - timeoutCancelable.Cancel(); + _timeoutCancelable?.Cancel(); + _timeoutCancelable = null; ReturnRecoveryPermit(); throw; } @@ -182,7 +188,8 @@ bool RecoveryBehavior(object message) private EventsourcedState Recovering(Receive recoveryBehavior, TimeSpan timeout) { // protect against event replay stalling forever because of journal overloaded and such - var timeoutCancelable = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(timeout, timeout, Self, new RecoveryTick(false), Self); + _timeoutCancelable?.Cancel(); + _timeoutCancelable = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(timeout, timeout, Self, new RecoveryTick(false), Self); var eventSeenInInterval = false; var recoveryRunning = true; @@ -201,7 +208,8 @@ private EventsourcedState Recovering(Receive recoveryBehavior, TimeSpan timeout) } catch (Exception cause) { - timeoutCancelable.Cancel(); + _timeoutCancelable?.Cancel(); + _timeoutCancelable = null; try { OnRecoveryFailure(cause, replayed.Persistent.Payload); @@ -214,7 +222,8 @@ private EventsourcedState Recovering(Receive recoveryBehavior, TimeSpan timeout) } break; case RecoverySuccess success: - timeoutCancelable.Cancel(); + _timeoutCancelable?.Cancel(); + _timeoutCancelable = null; OnReplaySuccess(); var highestSeqNr = Math.Max(success.HighestSequenceNr, LastSequenceNr); _sequenceNr = highestSeqNr; @@ -232,7 +241,8 @@ private EventsourcedState Recovering(Receive recoveryBehavior, TimeSpan timeout) ReturnRecoveryPermit(); break; case ReplayMessagesFailure failure: - timeoutCancelable.Cancel(); + _timeoutCancelable?.Cancel(); + _timeoutCancelable = null; try { OnRecoveryFailure(failure.Cause); @@ -246,7 +256,8 @@ private EventsourcedState Recovering(Receive recoveryBehavior, TimeSpan timeout) case RecoveryTick { Snapshot: false }: if (!eventSeenInInterval) { - timeoutCancelable.Cancel(); + _timeoutCancelable?.Cancel(); + _timeoutCancelable = null; try { OnRecoveryFailure( @@ -274,7 +285,8 @@ private EventsourcedState Recovering(Receive recoveryBehavior, TimeSpan timeout) } catch (Exception) { - timeoutCancelable.Cancel(); + _timeoutCancelable?.Cancel(); + _timeoutCancelable = null; ReturnRecoveryPermit(); throw; } diff --git a/src/core/Akka.Persistence/Eventsourced.cs b/src/core/Akka.Persistence/Eventsourced.cs index 39f5b81c252..252addaf197 100644 --- a/src/core/Akka.Persistence/Eventsourced.cs +++ b/src/core/Akka.Persistence/Eventsourced.cs @@ -623,7 +623,7 @@ private void StashInternally(object currentMessage) { _internalStash.Stash(); } - catch (StashOverflowException e) + catch (StashOverflowException) { var strategy = InternalStashOverflowStrategy; if (strategy is DiscardToDeadLetterStrategy) diff --git a/src/core/Akka.Persistence/Journal/AsyncWriteProxy.cs b/src/core/Akka.Persistence/Journal/AsyncWriteProxy.cs index e4eedc8735e..45e63b827b2 100644 --- a/src/core/Akka.Persistence/Journal/AsyncWriteProxy.cs +++ b/src/core/Akka.Persistence/Journal/AsyncWriteProxy.cs @@ -94,13 +94,8 @@ public sealed class ReplayFailure /// /// This exception is thrown when the specified is undefined. /// - public ReplayFailure(Exception cause) - { - if (cause == null) - throw new ArgumentNullException(nameof(cause), "AsyncWriteTarget.ReplayFailure cause exception cannot be null"); - - Cause = cause; - } + public ReplayFailure(Exception cause) => + Cause = cause ?? throw new ArgumentNullException(nameof(cause), "AsyncWriteTarget.ReplayFailure cause exception cannot be null"); /// /// The cause of the failure @@ -257,8 +252,10 @@ public bool Equals(DeleteMessagesTo other) /// /// A journal that delegates actual storage to a target actor. For testing only. /// - public abstract class AsyncWriteProxy : AsyncWriteJournal, IWithUnboundedStash + public abstract class AsyncWriteProxy : AsyncWriteJournal, IWithUnboundedStash, IWithTimers { + private const string InitTimeoutTimerKey = nameof(InitTimeoutTimerKey); + private bool _isInitialized; private bool _isInitTimedOut; private IActorRef _store; @@ -280,7 +277,7 @@ protected AsyncWriteProxy() /// public override void AroundPreStart() { - Context.System.Scheduler.ScheduleTellOnce(Timeout, Self, InitTimeout.Instance, Self); + Timers.StartSingleTimer(InitTimeoutTimerKey, InitTimeout.Instance, Timeout, Self); base.AroundPreStart(); } @@ -420,7 +417,9 @@ private static Task StoreNotInitialized() /// /// TBD /// - public IStash Stash { get; set; } + public IStash Stash { get; set; } = null!; + + public ITimerScheduler Timers { get; set; } = null!; // sent to self only /// diff --git a/src/core/Akka.Persistence/Journal/PersistencePluginProxy.cs b/src/core/Akka.Persistence/Journal/PersistencePluginProxy.cs index 5c52004485f..1df9825cc4d 100644 --- a/src/core/Akka.Persistence/Journal/PersistencePluginProxy.cs +++ b/src/core/Akka.Persistence/Journal/PersistencePluginProxy.cs @@ -18,8 +18,10 @@ namespace Akka.Persistence.Journal /// /// TBD /// - public class PersistencePluginProxy : ActorBase, IWithUnboundedStash + public class PersistencePluginProxy : ActorBase, IWithUnboundedStash, IWithTimers { + private const string InitTimeoutTimerKey = nameof(InitTimeoutTimerKey); + /// /// TBD /// @@ -125,7 +127,9 @@ public PersistencePluginProxy(Config config) /// /// TBD /// - public IStash Stash { get; set; } + public IStash Stash { get; set; } = null!; + + public ITimerScheduler Timers { get; set; } = null!; /// /// TBD @@ -168,7 +172,7 @@ protected override void PreStart() targetAddress); } } - Context.System.Scheduler.ScheduleTellOnce(_initTimeout, Self, InitTimeout.Instance, Self); + Timers.StartSingleTimer(InitTimeoutTimerKey, InitTimeout.Instance, _initTimeout, Self); } base.PreStart(); } diff --git a/src/core/Akka.Persistence/JournalProtocol.cs b/src/core/Akka.Persistence/JournalProtocol.cs index dfb5856224a..d97bfb3f208 100644 --- a/src/core/Akka.Persistence/JournalProtocol.cs +++ b/src/core/Akka.Persistence/JournalProtocol.cs @@ -78,10 +78,7 @@ public sealed class DeleteMessagesFailure : IEquatable, I /// public DeleteMessagesFailure(Exception cause, long toSequenceNr) { - if (cause == null) - throw new ArgumentNullException(nameof(cause), "DeleteMessagesFailure cause exception cannot be null"); - - Cause = cause; + Cause = cause ?? throw new ArgumentNullException(nameof(cause), "DeleteMessagesFailure cause exception cannot be null"); ToSequenceNr = toSequenceNr; } @@ -373,11 +370,8 @@ public sealed class WriteMessageRejected : IJournalResponse, INoSerializationVer /// public WriteMessageRejected(IPersistentRepresentation persistent, Exception cause, int actorInstanceId) { - if (cause == null) - throw new ArgumentNullException(nameof(cause), "WriteMessageRejected cause exception cannot be null"); - Persistent = persistent; - Cause = cause; + Cause = cause ?? throw new ArgumentNullException(nameof(cause), "WriteMessageRejected cause exception cannot be null"); ActorInstanceId = actorInstanceId; } @@ -440,11 +434,8 @@ public sealed class WriteMessageFailure : IJournalResponse, INoSerializationVeri /// public WriteMessageFailure(IPersistentRepresentation persistent, Exception cause, int actorInstanceId) { - if (cause == null) - throw new ArgumentNullException(nameof(cause), "WriteMessageFailure cause exception cannot be null"); - Persistent = persistent; - Cause = cause; + Cause = cause ?? throw new ArgumentNullException(nameof(cause), "WriteMessageFailure cause exception cannot be null"); ActorInstanceId = actorInstanceId; } @@ -705,20 +696,14 @@ public sealed class ReplayMessagesFailure : IJournalResponse, IDeadLetterSuppres /// /// This exception is thrown when the specified is undefined. /// - public ReplayMessagesFailure(Exception cause) - { - if (cause == null) - throw new ArgumentNullException(nameof(cause), "ReplayMessagesFailure cause exception cannot be null"); - - Cause = cause; - } + public ReplayMessagesFailure(Exception cause) => + Cause = cause ?? throw new ArgumentNullException(nameof(cause), "ReplayMessagesFailure cause exception cannot be null"); /// /// The cause of the failure /// public Exception Cause { get; } - public bool Equals(ReplayMessagesFailure other) { if (ReferenceEquals(other, null)) return false; @@ -727,13 +712,10 @@ public bool Equals(ReplayMessagesFailure other) return Equals(Cause, other.Cause); } - public override bool Equals(object obj) => Equals(obj as ReplayMessagesFailure); - public override int GetHashCode() => Cause.GetHashCode(); - public override string ToString() => $"ReplayMessagesFailure"; } } diff --git a/src/core/Akka.Persistence/SnapshotProtocol.cs b/src/core/Akka.Persistence/SnapshotProtocol.cs index da1ae402bf6..915d09887f5 100644 --- a/src/core/Akka.Persistence/SnapshotProtocol.cs +++ b/src/core/Akka.Persistence/SnapshotProtocol.cs @@ -793,10 +793,7 @@ public sealed class SaveSnapshot : ISnapshotRequest, IEquatable /// public SaveSnapshot(SnapshotMetadata metadata, object snapshot) { - if (metadata == null) - throw new ArgumentNullException(nameof(metadata), "SaveSnapshot requires SnapshotMetadata to be provided"); - - Metadata = metadata; + Metadata = metadata ?? throw new ArgumentNullException(nameof(metadata), "SaveSnapshot requires SnapshotMetadata to be provided"); Snapshot = snapshot; } @@ -848,13 +845,8 @@ public sealed class DeleteSnapshot : ISnapshotRequest, IEquatable /// This exception is thrown when the specified is undefined. /// - public DeleteSnapshot(SnapshotMetadata metadata) - { - if (metadata == null) - throw new ArgumentNullException(nameof(metadata), "DeleteSnapshot requires SnapshotMetadata to be provided"); - - Metadata = metadata; - } + public DeleteSnapshot(SnapshotMetadata metadata) => + Metadata = metadata ?? throw new ArgumentNullException(nameof(metadata), "DeleteSnapshot requires SnapshotMetadata to be provided"); /// /// Snapshot metadata. diff --git a/src/core/Akka.Remote.TestKit/BarrierCoordinator.cs b/src/core/Akka.Remote.TestKit/BarrierCoordinator.cs index e02bfcef230..0233563d4e2 100644 --- a/src/core/Akka.Remote.TestKit/BarrierCoordinator.cs +++ b/src/core/Akka.Remote.TestKit/BarrierCoordinator.cs @@ -483,7 +483,7 @@ protected override void PostRestart(Exception reason) _failed = true; } - protected void InitFSM() + private void InitFSM() { StartWith(State.Idle, new Data(ImmutableHashSet.Create(), "", ImmutableHashSet.Create(), null)); diff --git a/src/core/Akka.Remote.TestKit/Conductor.cs b/src/core/Akka.Remote.TestKit/Conductor.cs index 78abecb6569..abdf2c33ae6 100644 --- a/src/core/Akka.Remote.TestKit/Conductor.cs +++ b/src/core/Akka.Remote.TestKit/Conductor.cs @@ -9,6 +9,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; +using System.Threading; using System.Threading.Tasks; using Akka.Actor; using Akka.Event; @@ -61,14 +62,40 @@ public IActorRef Controller /// /// /// - public async Task StartController(int participants, RoleName name, IPEndPoint controllerPort) + public Task StartController(int participants, RoleName name, IPEndPoint controllerPort) + { + return StartControllerAsync(participants, name, controllerPort, CancellationToken.None); + } + + /// + /// Start the , which in turn will + /// bind to a TCP port as specified in the `akka.testconductor.port` config + /// property, where 0 denotes automatic allocation. Since the latter is + /// actually preferred, a `Future[Int]` is returned which will be completed + /// with the port number actually chosen, so that this can then be communicated + /// to the players for their proper start-up. + /// + /// This method also invokes Player.startClient, + /// since it is expected that the conductor participates in barriers for + /// overall coordination. The returned Future will only be completed once the + /// client’s start-up finishes, which in fact waits for all other players to + /// connect. + /// + /// participants gives the number of participants which shall connect + /// before any of their startClient() operations complete + /// + /// + /// + /// + /// + public async Task StartControllerAsync(int participants, RoleName name, IPEndPoint controllerPort, CancellationToken cancellationToken = default) { if(_controller != null) throw new IllegalStateException("TestConductorServer was already started"); _controller = _system.ActorOf(Props.Create(() => new Controller(participants, controllerPort)), - "controller"); + "controller"); - var node = await _controller.Ask(TestKit.Controller.GetSockAddr.Instance, Settings.QueryTimeout).ConfigureAwait(false); - await StartClient(name, node).ConfigureAwait(false); + var node = await _controller.Ask(TestKit.Controller.GetSockAddr.Instance, Settings.QueryTimeout, cancellationToken); + await StartClient(name, node); return node; } @@ -96,9 +123,18 @@ public async Task StartController(int participants, RoleName name, I /// public Task Throttle(RoleName node, RoleName target, ThrottleTransportAdapter.Direction direction, float rateMBit) + { + return ThrottleAsync(node, target, direction, rateMBit, CancellationToken.None); + } + + /// + /// Async version of Throttle with cancellation token support. + /// + public Task ThrottleAsync(RoleName node, RoleName target, ThrottleTransportAdapter.Direction direction, + float rateMBit, CancellationToken cancellationToken = default) { RequireTestConductorTransport(); - return Controller.Ask(new Throttle(node, target, direction, rateMBit), Settings.QueryTimeout); + return Controller.Ask(new Throttle(node, target, direction, rateMBit), Settings.QueryTimeout, cancellationToken); } /// @@ -117,7 +153,28 @@ public Task Throttle(RoleName node, RoleName target, ThrottleTransportAdap /// public Task Blackhole(RoleName node, RoleName target, ThrottleTransportAdapter.Direction direction) { - return Throttle(node, target, direction, 0f); + return BlackholeAsync(node, target, direction, CancellationToken.None); + } + + /// + /// Async version of Blackhole with cancellation token support. + /// Switch the helios pipeline of the remote support into blackhole mode for + /// sending and/or receiving: it will just drop all messages right before + /// submitting them to the Socket or right after receiving them from the + /// Socket. + /// + /// ====Note==== + /// To use this feature you must activate the failure injector and throttler + /// transport adapters by specifying `testTransport(on = true)` in your MultiNodeConfig. + /// + /// is the symbolic name of the node which is to be affected + /// is the symbolic name of the other node to which connectivity shall be impeded + /// can be either `Direction.Send`, `Direction.Receive` or `Direction.Both` + /// Cancellation token + /// Task indicating completion + public Task BlackholeAsync(RoleName node, RoleName target, ThrottleTransportAdapter.Direction direction, CancellationToken cancellationToken = default) + { + return ThrottleAsync(node, target, direction, 0f, cancellationToken); } private void RequireTestConductorTransport() @@ -142,7 +199,26 @@ private void RequireTestConductorTransport() /// public Task PassThrough(RoleName node, RoleName target, ThrottleTransportAdapter.Direction direction) { - return Throttle(node, target, direction, -1f); + return PassThroughAsync(node, target, direction, CancellationToken.None); + } + + /// + /// Async version of PassThrough with cancellation token support. + /// Switch the Helios pipeline of the remote support into pass through mode for + /// sending and/or receiving. + /// + /// ====Note==== + /// To use this feature you must activate the failure injector and throttler + /// transport adapters by specifying `testTransport(on = true)` in your MultiNodeConfig. + /// + /// is the symbolic name of the node which is to be affected + /// is the symbolic name of the other node to which connectivity shall be impeded + /// can be either `Direction.Send`, `Direction.Receive` or `Direction.Both` + /// Cancellation token + /// Task indicating completion + public Task PassThroughAsync(RoleName node, RoleName target, ThrottleTransportAdapter.Direction direction, CancellationToken cancellationToken = default) + { + return ThrottleAsync(node, target, direction, -1f, cancellationToken); } /// @@ -155,7 +231,21 @@ public Task PassThrough(RoleName node, RoleName target, ThrottleTransportA /// public Task Disconnect(RoleName node, RoleName target) { - return Controller.Ask(new Disconnect(node, target, false), Settings.QueryTimeout); + return DisconnectAsync(node, target, CancellationToken.None); + } + + /// + /// Tell the remote support to TCP_RESET the connection to the given remote + /// peer. It works regardless of whether the recipient was initiator or + /// responder. + /// + /// is the symbolic name of the node which is to be affected + /// is the symbolic name of the other node to which connectivity shall be impeded + /// Cancellation token + /// + public Task DisconnectAsync(RoleName node, RoleName target, CancellationToken cancellationToken = default) + { + return Controller.Ask(new Disconnect(node, target, false), Settings.QueryTimeout, cancellationToken); } /// @@ -168,7 +258,21 @@ public Task Disconnect(RoleName node, RoleName target) /// public Task Abort(RoleName node, RoleName target) { - return Controller.Ask(new Disconnect(node, target, true), Settings.QueryTimeout); + return AbortAsync(node, target, CancellationToken.None); + } + + /// + /// Tell the remote support to TCP_RESET the connection to the given remote + /// peer. It works regardless of whether the recipient was initiator or + /// responder. + /// + /// is the symbolic name of the node which is to be affected + /// is the symbolic name of the other node to which connectivity shall be impeded + /// Cancellation token + /// + public Task AbortAsync(RoleName node, RoleName target, CancellationToken cancellationToken = default) + { + return Controller.Ask(new Disconnect(node, target, true), Settings.QueryTimeout, cancellationToken); } /// @@ -181,16 +285,33 @@ public Task Abort(RoleName node, RoleName target) /// TBD public Task Exit(RoleName node, int exitValue) { - // the recover is needed to handle ClientDisconnectedException exception, - // which is normal during shutdown - return Controller.Ask(new Terminate(node, new Right(exitValue)), Settings.QueryTimeout).ContinueWith(t => - { - if(t.Result is Done) return Done.Instance; - var failure = t.Result as FSMBase.Failure; - if (failure != null && failure.Cause is Controller.ClientDisconnectedException) return Done.Instance; + // Use the async version with no cancellation token for consistency + return ExitAsync(node, exitValue, CancellationToken.None); + } - throw new InvalidOperationException($"Expected Done but received {t.Result}"); - }); + /// + /// Async version of Exit with cancellation token support. + /// Tell the actor system at the remote node to shut itself down. The node will also be + /// removed, so that the remaining nodes may still pass subsequent barriers. + /// + /// is the symbolic name of the node which is to be affected + /// is the return code which shall be given to System.exit + /// Cancellation token + /// Task indicating completion + public async Task ExitAsync(RoleName node, int exitValue, CancellationToken cancellationToken = default) + { + try + { + var result = await Controller.Ask(new Terminate(node, new Right(exitValue)), Settings.QueryTimeout, cancellationToken); + if (result is Done) return Done.Instance; + if (result is FSMBase.Failure failure && failure.Cause is Controller.ClientDisconnectedException) + return Done.Instance; + throw new InvalidOperationException($"Expected Done but received {result}"); + } + catch (TaskCanceledException) + { + throw new TimeoutException($"ExitAsync operation was cancelled for node {node}"); + } } /// @@ -201,19 +322,32 @@ public Task Exit(RoleName node, int exitValue) /// is the symbolic name of the node which is to be affected /// TBD /// TBD - /// TBD + /// Task indicating completion public Task Shutdown(RoleName node, bool abort = false) + { + // Use the async version with no cancellation token for consistency + return ShutdownAsync(node, abort, CancellationToken.None); + } + + /// + /// Tell the actor system at the remote node to shut itself down without + /// awaiting termination of remote-deployed children. The node will also be + /// removed, so that the remaining nodes may still pass subsequent barriers. + /// + /// is the symbolic name of the node which is to be affected + /// TBD + /// Cancellation token + /// Task indicating completion + public async Task ShutdownAsync(RoleName node, bool abort = false, CancellationToken cancellationToken = default) { // the recover is needed to handle ClientDisconnectedException exception, // which is normal during shutdown - return Controller.Ask(new Terminate(node, new Left(abort)), Settings.QueryTimeout).ContinueWith(t => + var result = await Controller.Ask(new Terminate(node, new Left(abort)), Settings.QueryTimeout, cancellationToken); + return result switch { - if (t.Result is Done) return Done.Instance; - var failure = t.Result as FSMBase.Failure; - if (failure != null && failure.Cause is Controller.ClientDisconnectedException) return Done.Instance; - - throw new InvalidOperationException($"Expected Done but received {t.Result}"); - }); + Done or FSMBase.Failure { Cause: TestKit.Controller.ClientDisconnectedException } => Done.Instance, + _ => throw new InvalidOperationException($"Expected Done but received {result}") + }; } /// @@ -221,7 +355,17 @@ public Task Shutdown(RoleName node, bool abort = false) /// public Task> GetNodes() { - return Controller.Ask>(TestKit.Controller.GetNodes.Instance, Settings.QueryTimeout); + // Use the async version with no cancellation token for consistency + return GetNodesAsync(CancellationToken.None); + } + + /// + /// Async version of GetNodes with cancellation token support. + /// Obtain the list of remote host names currently registered. + /// + public Task> GetNodesAsync(CancellationToken cancellationToken = default) + { + return Controller.Ask>(TestKit.Controller.GetNodes.Instance, Settings.QueryTimeout, cancellationToken); } /// @@ -234,7 +378,23 @@ public Task> GetNodes() /// public Task RemoveNode(RoleName node) { - return Controller.Ask(new Remove(node), Settings.QueryTimeout); + // Use the async version with no cancellation token for consistency + return RemoveNodeAsync(node, CancellationToken.None); + } + + /// + /// Async version of RemoveNode with cancellation token support. + /// Remove a remote host from the list, so that the remaining nodes may still + /// pass subsequent barriers. This must be done before the client connection + /// breaks down in order to affect an "orderly" removal (i.e. without failing + /// present and future barriers). + /// + /// is the symbolic name of the node which is to be removed + /// Cancellation token + /// Task indicating completion + public Task RemoveNodeAsync(RoleName node, CancellationToken cancellationToken = default) + { + return Controller.Ask(new Remove(node), Settings.QueryTimeout, cancellationToken); } } diff --git a/src/core/Akka.Remote.TestKit/MultiNodeSpec.cs b/src/core/Akka.Remote.TestKit/MultiNodeSpec.cs index 4fcefc77149..fe4adebc3b5 100644 --- a/src/core/Akka.Remote.TestKit/MultiNodeSpec.cs +++ b/src/core/Akka.Remote.TestKit/MultiNodeSpec.cs @@ -15,6 +15,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Text; +using System.Threading; using System.Threading.Tasks; using Akka.Actor; using Akka.Actor.Setup; @@ -25,49 +26,49 @@ using Akka.TestKit.Xunit2; using Akka.Util.Internal; -namespace Akka.Remote.TestKit +namespace Akka.Remote.TestKit; + +/// +/// Configure the role names and participants of the test, including configuration settings +/// +public abstract class MultiNodeConfig { + // allows us to avoid NullReferenceExceptions if we make this empty rather than null + // so that way if a MultiNodeConfig doesn't explicitly set CommonConfig to some value + // it will remain safe by defaut + private Config _commonConf = ConfigurationFactory.Empty; + + private ImmutableDictionary _nodeConf = ImmutableDictionary.Create(); + private ImmutableList _roles = ImmutableList.Create(); + private ImmutableDictionary> _deployments = ImmutableDictionary.Create>(); + private ImmutableList _allDeploy = ImmutableList.Create(); + private bool _testTransport = false; + /// - /// Configure the role names and participants of the test, including configuration settings + /// Register a common base config for all test participants, if so desired. /// - public abstract class MultiNodeConfig - { - // allows us to avoid NullReferenceExceptions if we make this empty rather than null - // so that way if a MultiNodeConfig doesn't explicitly set CommonConfig to some value - // it will remain safe by defaut - Config _commonConf = ConfigurationFactory.Empty; - - ImmutableDictionary _nodeConf = ImmutableDictionary.Create(); - ImmutableList _roles = ImmutableList.Create(); - ImmutableDictionary> _deployments = ImmutableDictionary.Create>(); - ImmutableList _allDeploy = ImmutableList.Create(); - bool _testTransport = false; - - /// - /// Register a common base config for all test participants, if so desired. - /// - public Config CommonConfig - { - set { _commonConf = value; } - } + public Config CommonConfig + { + set { _commonConf = value; } + } - /// - /// Register a config override for a specific participant. - /// - public void NodeConfig(IEnumerable roles, IEnumerable configs) - { - var c = configs.Aggregate((a, b) => a.WithFallback(b)); - _nodeConf = _nodeConf.AddRange(roles.Select(r => new KeyValuePair(r, c))); - } + /// + /// Register a config override for a specific participant. + /// + public void NodeConfig(IEnumerable roles, IEnumerable configs) + { + var c = configs.Aggregate((a, b) => a.WithFallback(b)); + _nodeConf = _nodeConf.AddRange(roles.Select(r => new KeyValuePair(r, c))); + } - /// - /// Include for verbose debug logging - /// - /// when `true` debug Config is returned, otherwise config with info logging - public Config DebugConfig(bool on) - { - if (on) - return ConfigurationFactory.ParseString(@" + /// + /// Include for verbose debug logging + /// + /// when `true` debug Config is returned, otherwise config with info logging + public Config DebugConfig(bool on) + { + if (on) + return ConfigurationFactory.ParseString(@" akka.loglevel = DEBUG akka.remote { log-received-messages = on @@ -80,288 +81,282 @@ public Config DebugConfig(bool on) akka.remote.log-remote-lifecycle-events = on akka.log-dead-letters = on "); - return ConfigurationFactory.Empty; - } - - public RoleName Role(string name) - { - if (_roles.Exists(r => r.Name == name)) throw new ArgumentException("non-unique role name " + name); - var roleName = new RoleName(name); - _roles = _roles.Add(roleName); - return roleName; - } + return ConfigurationFactory.Empty; + } - public void DeployOn(RoleName role, string deployment) - { - _deployments.TryGetValue(role, out var roleDeployments); - _deployments = _deployments.SetItem(role, - roleDeployments == null ? ImmutableList.Create(deployment) : roleDeployments.Add(deployment)); - } + public RoleName Role(string name) + { + if (_roles.Exists(r => r.Name == name)) throw new ArgumentException("non-unique role name " + name); + var roleName = new RoleName(name); + _roles = _roles.Add(roleName); + return roleName; + } - public void DeployOnAll(string deployment) - { - _allDeploy = _allDeploy.Add(deployment); - } + public void DeployOn(RoleName role, string deployment) + { + _deployments.TryGetValue(role, out var roleDeployments); + _deployments = _deployments.SetItem(role, + roleDeployments == null ? ImmutableList.Create(deployment) : roleDeployments.Add(deployment)); + } - /// - /// To be able to use `blackhole`, `passThrough`, and `throttle` you must - /// activate the failure injector and throttler transport adapters by - /// specifying `testTransport(on = true)` in your MultiNodeConfig. - /// - public bool TestTransport - { - set { _testTransport = value; } - } + public void DeployOnAll(string deployment) + { + _allDeploy = _allDeploy.Add(deployment); + } - readonly Lazy _myself; + /// + /// To be able to use `blackhole`, `passThrough`, and `throttle` you must + /// activate the failure injector and throttler transport adapters by + /// specifying `testTransport(on = true)` in your MultiNodeConfig. + /// + public bool TestTransport + { + set { _testTransport = value; } + } - protected MultiNodeConfig() - { - var roleName = CommandLine.GetPropertyOrDefault("multinode.role", null); + private readonly Lazy _myself; - if (String.IsNullOrEmpty(roleName)) - { - _myself = new Lazy(() => - { - if (MultiNodeSpec.SelfIndex > _roles.Count) throw new ArgumentException("not enough roles declared for this test"); - return _roles[MultiNodeSpec.SelfIndex]; - }); - } - else - { - _myself = new Lazy(() => - { - var myself = _roles.FirstOrDefault(r => r.Name.Equals(roleName, StringComparison.OrdinalIgnoreCase)); - if (myself == default(RoleName)) throw new ArgumentException($"cannot find {roleName} among configured roles"); - return myself; - }); - } - } + protected MultiNodeConfig() + { + var roleName = CommandLine.GetPropertyOrDefault("multinode.role", null); - public RoleName Myself + if (string.IsNullOrEmpty(roleName)) { - get { return _myself.Value; } + _myself = new Lazy(() => + { + if (MultiNodeSpec.SelfIndex > _roles.Count) throw new ArgumentException("not enough roles declared for this test"); + return _roles[MultiNodeSpec.SelfIndex]; + }); } - - internal Config Config + else { - get + _myself = new Lazy(() => { - var transportConfig = _testTransport ? - ConfigurationFactory.ParseString("akka.remote.dot-netty.tcp.applied-adapters = [trttl, gremlin]") - : ConfigurationFactory.Empty; - - var builder = ImmutableList.CreateBuilder(); - if (_nodeConf.TryGetValue(Myself, out var nodeConfig)) - builder.Add(nodeConfig); - builder.Add(_commonConf); - builder.Add(transportConfig); - builder.Add(MultiNodeSpec.NodeConfig); - builder.Add(MultiNodeSpec.BaseConfig); - - return builder.ToImmutable().Aggregate((a, b) => a.WithFallback(b)); - } + var myself = _roles.FirstOrDefault(r => r.Name.Equals(roleName, StringComparison.OrdinalIgnoreCase)); + if (myself is null) throw new ArgumentException($"cannot find {roleName} among configured roles"); + return myself; + }); } + } - internal ImmutableList Deployments(RoleName node) - { - _deployments.TryGetValue(node, out var deployments); - return deployments == null ? _allDeploy : deployments.AddRange(_allDeploy); - } + public RoleName Myself => _myself.Value; - public ImmutableList Roles + internal Config Config + { + get { - get { return _roles; } + var transportConfig = _testTransport ? + ConfigurationFactory.ParseString("akka.remote.dot-netty.tcp.applied-adapters = [trttl, gremlin]") + : ConfigurationFactory.Empty; + + var builder = ImmutableList.CreateBuilder(); + if (_nodeConf.TryGetValue(Myself, out var nodeConfig)) + builder.Add(nodeConfig); + builder.Add(_commonConf); + builder.Add(transportConfig); + builder.Add(MultiNodeSpec.NodeConfig); + builder.Add(MultiNodeSpec.BaseConfig); + + return builder.ToImmutable().Aggregate((a, b) => a.WithFallback(b)); } } - //TODO: Applicable? - /// - /// Note: To be able to run tests with everything ignored or excluded by tags - /// you must not use `testconductor`, or helper methods that use `testconductor`, - /// from the constructor of your test class. Otherwise the controller node might - /// be shutdown before other nodes have completed and you will see errors like: - /// `AskTimeoutException: sending to terminated ref breaks promises`. Using lazy - /// val is fine. - /// - public abstract class MultiNodeSpec : TestKitBase, IMultiNodeSpecCallbacks, IDisposable + internal ImmutableList Deployments(RoleName node) { - //TODO: Sort out references to Java classes in + _deployments.TryGetValue(node, out var deployments); + return deployments == null ? _allDeploy : deployments.AddRange(_allDeploy); + } + + public ImmutableList Roles => _roles; +} + +//TODO: Applicable? +/// +/// Note: To be able to run tests with everything ignored or excluded by tags +/// you must not use `testconductor`, or helper methods that use `testconductor`, +/// from the constructor of your test class. Otherwise the controller node might +/// be shutdown before other nodes have completed and you will see errors like: +/// `AskTimeoutException: sending to terminated ref breaks promises`. Using lazy +/// val is fine. +/// +public abstract class MultiNodeSpec : TestKitBase, IMultiNodeSpecCallbacks, IDisposable +{ + //TODO: Sort out references to Java classes in - /// - /// Marker used to indicate that has not been set yet. - /// - private const int MaxNodesUnset = -1; - private static int _maxNodes = MaxNodesUnset; + /// + /// Marker used to indicate that has not been set yet. + /// + private const int MaxNodesUnset = -1; + private static int _maxNodes = MaxNodesUnset; - /// - /// Number of nodes node taking part in this test. - /// -Dmultinode.max-nodes=4 - /// - public static int MaxNodes + /// + /// Number of nodes node taking part in this test. + /// -Dmultinode.max-nodes=4 + /// + public static int MaxNodes + { + get { - get + if (_maxNodes == MaxNodesUnset) { - if (_maxNodes == MaxNodesUnset) - { - _maxNodes = CommandLine.GetInt32("multinode.max-nodes"); - } - - if (_maxNodes <= 0) throw new InvalidOperationException("multinode.max-nodes must be greater than 0"); - return _maxNodes; + _maxNodes = CommandLine.GetInt32("multinode.max-nodes"); } + + if (_maxNodes <= 0) throw new InvalidOperationException("multinode.max-nodes must be greater than 0"); + return _maxNodes; } + } + + private static string _multiNodeHost; - private static string _multiNodeHost; - - /// - /// Name (or IP address; must be resolvable) - /// of the host this node is running on - /// - /// -Dmultinode.host=host.example.com - /// - /// InetAddress.getLocalHost.getHostAddress is used if empty or "localhost" - /// is defined as system property "multinode.host". - /// - public static string SelfName + /// + /// Name (or IP address; must be resolvable) + /// of the host this node is running on + /// + /// -Dmultinode.host=host.example.com + /// + /// InetAddress.getLocalHost.getHostAddress is used if empty or "localhost" + /// is defined as system property "multinode.host". + /// + public static string SelfName + { + get { - get + if (string.IsNullOrEmpty(_multiNodeHost)) { - if (string.IsNullOrEmpty(_multiNodeHost)) - { - _multiNodeHost = CommandLine.GetProperty("multinode.host"); - } - - //Run this assertion every time. Consistency is more important than performance. - if (string.IsNullOrEmpty(_multiNodeHost)) throw new InvalidOperationException("multinode.host must not be empty"); - return _multiNodeHost; + _multiNodeHost = CommandLine.GetProperty("multinode.host"); } + + //Run this assertion every time. Consistency is more important than performance. + if (string.IsNullOrEmpty(_multiNodeHost)) throw new InvalidOperationException("multinode.host must not be empty"); + return _multiNodeHost; } + } - /// - /// Marker used to indicate what the "not been set" value of is. - /// - private const int SelfPortUnsetValue = -1; - private static int _selfPort = SelfPortUnsetValue; + /// + /// Marker used to indicate what the "not been set" value of is. + /// + private const int SelfPortUnsetValue = -1; + private static int _selfPort = SelfPortUnsetValue; - /// - /// Port number of this node. Defaults to 0 which means a random port. - /// - /// -Dmultinode.port=0 - /// - public static int SelfPort + /// + /// Port number of this node. Defaults to 0 which means a random port. + /// + /// -Dmultinode.port=0 + /// + public static int SelfPort + { + get { - get + if (_selfPort == SelfPortUnsetValue) //unset { - if (_selfPort == SelfPortUnsetValue) //unset - { - var selfPortStr = CommandLine.GetProperty("multinode.port"); - _selfPort = string.IsNullOrEmpty(selfPortStr) ? 0 : Int32.Parse(selfPortStr); - } - - if (!(_selfPort >= 0 && _selfPort < 65535)) throw new InvalidOperationException("multinode.port is out of bounds: " + _selfPort); - return _selfPort; + var selfPortStr = CommandLine.GetProperty("multinode.port"); + _selfPort = string.IsNullOrEmpty(selfPortStr) ? 0 : Int32.Parse(selfPortStr); } + + if (!(_selfPort >= 0 && _selfPort < 65535)) throw new InvalidOperationException("multinode.port is out of bounds: " + _selfPort); + return _selfPort; } + } - private static string _serverName; - /// - /// Name (or IP address; must be resolvable using InetAddress.getByName) - /// of the host that the server node is running on. - /// - /// -Dmultinode.server-host=server.example.com - /// - public static string ServerName + private static string _serverName; + /// + /// Name (or IP address; must be resolvable using InetAddress.getByName) + /// of the host that the server node is running on. + /// + /// -Dmultinode.server-host=server.example.com + /// + public static string ServerName + { + get { - get + if (string.IsNullOrEmpty(_serverName)) { - if (string.IsNullOrEmpty(_serverName)) - { - _serverName = CommandLine.GetProperty("multinode.server-host"); - } - if (string.IsNullOrEmpty(_serverName)) throw new InvalidOperationException("multinode.server-host must not be empty"); - return _serverName; + _serverName = CommandLine.GetProperty("multinode.server-host"); } + if (string.IsNullOrEmpty(_serverName)) throw new InvalidOperationException("multinode.server-host must not be empty"); + return _serverName; } + } - /// - /// Marker used to indicate what the "not been set" value of is. - /// - private const int ServerPortUnsetValue = -1; + /// + /// Marker used to indicate what the "not been set" value of is. + /// + private const int ServerPortUnsetValue = -1; - /// - /// Default value for - /// - private const int ServerPortDefault = 47110; + /// + /// Default value for + /// + private const int ServerPortDefault = 47110; - private static int _serverPort = ServerPortUnsetValue; + private static int _serverPort = ServerPortUnsetValue; - /// - /// Port number of the node that's running the server system. Defaults to 4711. - /// - /// -Dmultinode.server-port=4711 - /// - public static int ServerPort + /// + /// Port number of the node that's running the server system. Defaults to 4711. + /// + /// -Dmultinode.server-port=4711 + /// + public static int ServerPort + { + get { - get + if (_serverPort == ServerPortUnsetValue) { - if (_serverPort == ServerPortUnsetValue) - { - var serverPortStr = CommandLine.GetProperty("multinode.server-port"); - _serverPort = string.IsNullOrEmpty(serverPortStr) ? ServerPortDefault : Int32.Parse(serverPortStr); - } - - if (!(_serverPort > 0 && _serverPort < 65535)) throw new InvalidOperationException("multinode.server-port is out of bounds: " + _serverPort); - return _serverPort; + var serverPortStr = CommandLine.GetProperty("multinode.server-port"); + _serverPort = string.IsNullOrEmpty(serverPortStr) ? ServerPortDefault : Int32.Parse(serverPortStr); } + + if (!(_serverPort > 0 && _serverPort < 65535)) throw new InvalidOperationException("multinode.server-port is out of bounds: " + _serverPort); + return _serverPort; } + } - /// - /// Marker value used to indicate that has not been set yet. - /// - private const int SelfIndexUnset = -1; + /// + /// Marker value used to indicate that has not been set yet. + /// + private const int SelfIndexUnset = -1; - private static int _selfIndex = SelfIndexUnset; + private static int _selfIndex = SelfIndexUnset; - /// - /// Index of this node in the roles sequence. The TestConductor - /// is started in "controller" mode on selfIndex 0, i.e. there you can inject - /// failures and shutdown other nodes etc. - /// - public static int SelfIndex + /// + /// Index of this node in the roles sequence. The TestConductor + /// is started in "controller" mode on selfIndex 0, i.e. there you can inject + /// failures and shutdown other nodes etc. + /// + public static int SelfIndex + { + get { - get + if (_selfIndex == SelfIndexUnset) { - if (_selfIndex == SelfIndexUnset) - { - _selfIndex = CommandLine.GetInt32("multinode.index"); - } - - if (!(_selfIndex >= 0 && _selfIndex < MaxNodes)) throw new InvalidOperationException("multinode.index is out of bounds: " + _selfIndex); - return _selfIndex; + _selfIndex = CommandLine.GetInt32("multinode.index"); } + + if (!(_selfIndex >= 0 && _selfIndex < MaxNodes)) throw new InvalidOperationException("multinode.index is out of bounds: " + _selfIndex); + return _selfIndex; } + } - public static Config NodeConfig + public static Config NodeConfig + { + get { - get - { - const string config = @" + const string config = @" akka.actor.provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote"" akka.remote.dot-netty.tcp.hostname = ""{0}"" akka.remote.dot-netty.tcp.port = {1}"; - return ConfigurationFactory.ParseString(String.Format(config, SelfName, SelfPort)); - } + return ConfigurationFactory.ParseString(String.Format(config, SelfName, SelfPort)); } + } - public static Config BaseConfig + public static Config BaseConfig + { + get { - get - { - return ConfigurationFactory.ParseString( - @"akka { + return ConfigurationFactory.ParseString( + @"akka { loglevel = ""WARNING"" stdout-loglevel = ""WARNING"" coordinated-shutdown.terminate-actor-system = off @@ -381,351 +376,384 @@ public static Config BaseConfig } cluster.downing-provider-class = """" #disable SBR by default }").WithFallback(TestKitBase.DefaultConfig); - } } + } - private readonly RoleName _myself; - public RoleName Myself { get { return _myself; } } - private readonly ILoggingAdapter _log; - private bool _isDisposed; //Automatically initialized to false; - private readonly ImmutableList _roles; - private readonly Func> _deployments; - private readonly ImmutableDictionary _replacements; - private readonly Address _myAddress; - - protected MultiNodeSpec(MultiNodeConfig config, Type type) : - this(config.Myself, ActorSystem.Create(type.Name, config.Config), config.Roles, config.Deployments) - { - } + public RoleName Myself { get; } - protected MultiNodeSpec( - RoleName myself, - ActorSystem system, - ImmutableList roles, - Func> deployments) - : this(myself, system, null, roles, deployments) - { - } + private readonly ILoggingAdapter _log; + private bool _isDisposed; //Automatically initialized to false; + private readonly Func> _deployments; + private readonly ImmutableDictionary _replacements; + private readonly Address _myAddress; - protected MultiNodeSpec( - RoleName myself, - ActorSystemSetup setup, - ImmutableList roles, - Func> deployments) - : this(myself, null, setup, roles, deployments) - { - } + protected MultiNodeSpec(MultiNodeConfig config, Type type) : + this(config.Myself, ActorSystem.Create(type.Name, config.Config), config.Roles, config.Deployments) + { + } - private MultiNodeSpec( - RoleName myself, - ActorSystem system, - ActorSystemSetup setup, - ImmutableList roles, - Func> deployments) - : base(new XunitAssertions(), system, setup, null, null) - { - _myself = myself; - _log = Logging.GetLogger(Sys, this); - _roles = roles; - _deployments = deployments; + protected MultiNodeSpec( + RoleName myself, + ActorSystem system, + ImmutableList roles, + Func> deployments) + : this(myself, system, null, roles, deployments) + { + } - var node = new IPEndPoint(Dns.GetHostAddresses(ServerName)[0], ServerPort); - _controllerAddr = node; + protected MultiNodeSpec( + RoleName myself, + ActorSystemSetup setup, + ImmutableList roles, + Func> deployments) + : this(myself, null, setup, roles, deployments) + { + } - AttachConductor(new TestConductor(Sys)); + private MultiNodeSpec( + RoleName myself, + ActorSystem system, + ActorSystemSetup setup, + ImmutableList roles, + Func> deployments) + : base(new XunitAssertions(), system, setup, null, null) + { + Myself = myself; + _log = Logging.GetLogger(Sys, this); + Roles = roles; + _deployments = deployments; - _replacements = _roles.ToImmutableDictionary(r => r, r => new Replacement("@" + r.Name + "@", r, this)); + var node = new IPEndPoint(Dns.GetHostAddresses(ServerName)[0], ServerPort); + _controllerAddr = node; - InjectDeployments(Sys, myself); + AttachConductor(new TestConductor(Sys)); - _myAddress = Sys.AsInstanceOf().Provider.DefaultAddress; + _replacements = Roles.ToImmutableDictionary(r => r, r => new Replacement("@" + r.Name + "@", r, this)); - Log.Info("Role [{0}] started with address [{1}]", myself.Name, _myAddress); - MultiNodeSpecBeforeAll(); - } + InjectDeployments(Sys, myself); - public void MultiNodeSpecBeforeAll() - { - AtStartup(); - } + _myAddress = Sys.AsInstanceOf().Provider.DefaultAddress; - public void MultiNodeSpecAfterAll() + Log.Info("Role [{0}] started with address [{1}]", myself.Name, _myAddress); + MultiNodeSpecBeforeAll(); + } + + public void MultiNodeSpecBeforeAll() + { + AtStartup(); + } + + public void MultiNodeSpecAfterAll() + { + // wait for all nodes to remove themselves before we shut the conductor down + if (SelfIndex == 0) { - // wait for all nodes to remove themselves before we shut the conductor down - if (SelfIndex == 0) - { - TestConductor.RemoveNode(_myself); - Within(TestConductor.Settings.BarrierTimeout, () => - AwaitCondition(() => TestConductor.GetNodes().Result.All(n => n.Equals(_myself)))); + TestConductor.RemoveNode(Myself); + Within(TestConductor.Settings.BarrierTimeout, () => + AwaitCondition(() => TestConductor.GetNodes().Result.All(n => n.Equals(Myself)))); - } - Shutdown(Sys); - AfterTermination(); } + Shutdown(Sys); + AfterTermination(); + } - protected virtual TimeSpan ShutdownTimeout { get { return TimeSpan.FromSeconds(5); } } + protected virtual TimeSpan ShutdownTimeout { get { return TimeSpan.FromSeconds(5); } } - /// - /// Override this and return `true` to assert that the - /// shutdown of the `ActorSystem` was done properly. - /// - protected virtual bool VerifySystemShutdown { get { return false; } } + /// + /// Override this and return `true` to assert that the + /// shutdown of the `ActorSystem` was done properly. + /// + protected virtual bool VerifySystemShutdown { get { return false; } } - //Test Class Interface + //Test Class Interface - /// - /// Override this method to do something when the whole test is starting up. - /// - protected virtual void AtStartup() - { - } + /// + /// Override this method to do something when the whole test is starting up. + /// + protected virtual void AtStartup() + { + } + + /// + /// Override this method to do something when the whole test is terminating. + /// + protected virtual void AfterTermination() + { + } + + /// + /// All registered roles + /// + public ImmutableList Roles { get; } - /// - /// Override this method to do something when the whole test is terminating. - /// - protected virtual void AfterTermination() + /// + /// MUST BE DEFINED BY USER. + /// + /// Defines the number of participants required for starting the test. This + /// might not be equals to the number of nodes available to the test. + /// + public int InitialParticipants + { + get { + var initialParticipants = InitialParticipantsValueFactory; + if (initialParticipants <= 0) throw new InvalidOperationException("InitialParticipantsValueFactory must be populated early on, and it must be greater zero"); + if (initialParticipants > MaxNodes) throw new InvalidOperationException("not enough nodes to run this test"); + return initialParticipants; } - /// - /// All registered roles - /// - public ImmutableList Roles { get { return _roles; } } - - /// - /// MUST BE DEFINED BY USER. - /// - /// Defines the number of participants required for starting the test. This - /// might not be equals to the number of nodes available to the test. - /// - public int InitialParticipants - { - get - { - var initialParticipants = InitialParticipantsValueFactory; - if (initialParticipants <= 0) throw new InvalidOperationException("InitialParticipantsValueFactory must be populated early on, and it must be greater zero"); - if (initialParticipants > MaxNodes) throw new InvalidOperationException("not enough nodes to run this test"); - return initialParticipants; - } + } - } + /// + /// Must be defined by user. Creates the values used by + /// + protected abstract int InitialParticipantsValueFactory { get; } - /// - /// Must be defined by user. Creates the values used by - /// - protected abstract int InitialParticipantsValueFactory { get; } + protected TestConductor TestConductor; - protected TestConductor TestConductor; + /// + /// Execute the given block of code only on the given nodes (names according + /// to the `roleMap`). + /// + public void RunOn(Action thunk, params RoleName[] nodes) + { + if (IsNode(nodes)) thunk(); + } - /// - /// Execute the given block of code only on the given nodes (names according - /// to the `roleMap`). - /// - public void RunOn(Action thunk, params RoleName[] nodes) - { - if (IsNode(nodes)) thunk(); - } + /// + /// Execute the given block of code only on the given nodes (names according + /// to the `roleMap`). + /// + public async Task RunOnAsync(Func thunkAsync, params RoleName[] nodes) + { + if (IsNode(nodes)) await thunkAsync(); + } - /// - /// Execute the given block of code only on the given nodes (names according - /// to the `roleMap`). - /// - public async Task RunOnAsync(Func thunkAsync, params RoleName[] nodes) - { - if (IsNode(nodes)) await thunkAsync(); - } + /// + /// Verify that the running node matches one of the given nodes + /// + public bool IsNode(params RoleName[] nodes) + { + return nodes.Contains(Myself); + } - /// - /// Verify that the running node matches one of the given nodes - /// - public bool IsNode(params RoleName[] nodes) - { - return nodes.Contains(_myself); - } + /// + /// Enter the named barriers in the order given. Use the remaining duration from + /// the innermost enclosing `within` block or the default `BarrierTimeout` + /// + public void EnterBarrier(params string[] name) + { + TestConductor.Enter(RemainingOr(TestConductor.Settings.BarrierTimeout), Myself, name.ToImmutableList()); + } - /// - /// Enter the named barriers in the order given. Use the remaining duration from - /// the innermost enclosing `within` block or the default `BarrierTimeout` - /// - public void EnterBarrier(params string[] name) - { - TestConductor.Enter(RemainingOr(TestConductor.Settings.BarrierTimeout), Myself, name.ToImmutableList()); - } + /// + /// Async version of EnterBarrier. Enter the named barriers in the order given. + /// Use the remaining duration from the innermost enclosing `within` block or the default `BarrierTimeout` + /// + public Task EnterBarrierAsync(params string[] name) + { + return EnterBarrierAsync(CancellationToken.None, name); + } - /// - /// Query the controller for the transport address of the given node (by role name) and - /// return that as an ActorPath for easy composition: - /// - /// var serviceA = Sys.ActorSelection(Node(new RoleName("master")) / "user" / "serviceA"); - /// - public ActorPath Node(RoleName role) - { - //TODO: Async stuff here - return new RootActorPath(TestConductor.GetAddressFor(role).Result); - } + /// + /// Async version of EnterBarrier with cancellation support. Enter the named barriers in the order given. + /// Use the remaining duration from the innermost enclosing `within` block or the default `BarrierTimeout` + /// + public Task EnterBarrierAsync(CancellationToken cancellationToken, params string[] name) + { + return TestConductor.EnterAsync(RemainingOr(TestConductor.Settings.BarrierTimeout), Myself, name.ToImmutableList(), cancellationToken); + } - public void MuteDeadLetters(ActorSystem system = null, params Type[] messageClasses) + /// + /// Query the controller for the transport address of the given node (by role name) and + /// return that as an ActorPath for easy composition: + /// + /// var serviceA = Sys.ActorSelection(Node(new RoleName("master")) / "user" / "serviceA"); + /// + public ActorPath Node(RoleName role) + { + return NodeAsync(role).GetAwaiter().GetResult(); + } + + /// + /// Async version of Node. Query the controller for the transport address of the given node (by role name) and + /// return that as an ActorPath for easy composition. + /// + public async Task NodeAsync(RoleName role, CancellationToken cancellationToken = default) + { + var address = await TestConductor.GetAddressForAsync(role, cancellationToken); + return new RootActorPath(address); + } + + public void MuteDeadLetters(ActorSystem system = null, params Type[] messageClasses) + { + if (system == null) system = Sys; + if (!system.Log.IsDebugEnabled) { - if (system == null) system = Sys; - if (!system.Log.IsDebugEnabled) - { - if (messageClasses.Any()) - foreach (var @class in messageClasses) EventFilter.DeadLetter(@class).Mute(); - else EventFilter.DeadLetter(typeof(object)).Mute(); - } + if (messageClasses.Any()) + foreach (var @class in messageClasses) EventFilter.DeadLetter(@class).Mute(); + else EventFilter.DeadLetter(typeof(object)).Mute(); } + } - /* - * Implementation (i.e. wait for start etc.) - */ + /* + * Implementation (i.e. wait for start etc.) + */ - private readonly IPEndPoint _controllerAddr; + private readonly IPEndPoint _controllerAddr; - protected void AttachConductor(TestConductor tc) + protected void AttachConductor(TestConductor tc) + { + AttachConductorAsync(tc, CancellationToken.None).GetAwaiter().GetResult(); + } + + protected async Task AttachConductorAsync(TestConductor tc, CancellationToken cancellationToken = default) + { + using var cts = cancellationToken is { CanBeCanceled: true } + ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) + : new CancellationTokenSource(); + cts.CancelAfter(tc.Settings.BarrierTimeout); + try { - var timeout = tc.Settings.BarrierTimeout; - try - { - //TODO: Async stuff - if (SelfIndex == 0) - tc.StartController(InitialParticipants, _myself, _controllerAddr).Wait(timeout); - else - tc.StartClient(_myself, _controllerAddr).Wait(timeout); - } - catch (Exception e) - { - throw new Exception("failure while attaching new conductor", e); - } - TestConductor = tc; + if (SelfIndex == 0) + await tc.StartControllerAsync(InitialParticipants, Myself, _controllerAddr, cts.Token); + else + await tc.StartClientAsync(Myself, _controllerAddr, cts.Token); + } + catch (Exception e) + { + throw new Exception("failure while attaching new conductor", e); } + TestConductor = tc; + } - // now add deployments, if so desired + // now add deployments, if so desired - private sealed class Replacement - { - public string Tag { get; } - public RoleName Role { get; } - private readonly Lazy _addr; - public string Addr { get { return _addr.Value; } } + private sealed class Replacement + { + public string Tag { get; } + public RoleName Role { get; } + private readonly Lazy _addr; + public string Addr { get { return _addr.Value; } } - public Replacement(string tag, RoleName role, MultiNodeSpec spec) - { - Tag = tag; - Role = role; - _addr = new Lazy(() => spec.Node(role).Address.ToString()); - } + public Replacement(string tag, RoleName role, MultiNodeSpec spec) + { + Tag = tag; + Role = role; + _addr = new Lazy(() => spec.Node(role).Address.ToString()); } + } - protected void InjectDeployments(ActorSystem system, RoleName role) + protected void InjectDeployments(ActorSystem system, RoleName role) + { + var deployer = system.AsInstanceOf().Provider.Deployer; + foreach (var str in _deployments(role)) { - var deployer = system.AsInstanceOf().Provider.Deployer; - foreach (var str in _deployments(role)) + var deployString = _replacements.Values.Aggregate(str, (@base, r) => { - var deployString = _replacements.Values.Aggregate(str, (@base, r) => + var indexOf = @base.IndexOf(r.Tag, StringComparison.Ordinal); + if (indexOf == -1) return @base; + string replaceWith; + try { - var indexOf = @base.IndexOf(r.Tag, StringComparison.Ordinal); - if (indexOf == -1) return @base; - string replaceWith; - try - { - replaceWith = r.Addr; - } - catch (Exception e) - { - // might happen if all test cases are ignored (excluded) and - // controller node is finished/exited before r.addr is run - // on the other nodes - var unresolved = "akka://unresolved-replacement-" + r.Role.Name; - Log.Warning(unresolved + " due to: {0}", e.ToString()); - replaceWith = unresolved; - } - return @base.Replace(r.Tag, replaceWith); - }); - foreach (var pair in ConfigurationFactory.ParseString(deployString).AsEnumerable()) + replaceWith = r.Addr; + } + catch (Exception e) { - if (pair.Value.IsObject()) - { - var deploy = - deployer.ParseConfig(pair.Key, new Config(new HoconRoot(pair.Value))); - deployer.SetDeploy(deploy); - } - else - { - throw new ArgumentException(String.Format("key {0} must map to deployment section, not simple value {1}", - pair.Key, pair.Value)); - } + // might happen if all test cases are ignored (excluded) and + // controller node is finished/exited before r.addr is run + // on the other nodes + var unresolved = "akka://unresolved-replacement-" + r.Role.Name; + Log.Warning(unresolved + " due to: {0}", e.ToString()); + replaceWith = unresolved; + } + return @base.Replace(r.Tag, replaceWith); + }); + foreach (var pair in ConfigurationFactory.ParseString(deployString).AsEnumerable()) + { + if (pair.Value.IsObject()) + { + var deploy = deployer.ParseConfig(pair.Key, new Config(new HoconRoot(pair.Value))); + deployer.SetDeploy(deploy); + } + else + { + throw new ArgumentException($"key {pair.Key} must map to deployment section, not simple value {pair.Value}"); } } } + } - protected ActorSystem StartNewSystem() - { - var sb = - new StringBuilder("akka.remote.dot-netty.tcp{").AppendLine() - .AppendFormat("port={0}", _myAddress.Port) - .AppendLine() - .AppendFormat(@"hostname=""{0}""", _myAddress.Host) - .AppendLine("}"); - var config = - ConfigurationFactory + protected ActorSystem StartNewSystem() + { + return StartNewSystemAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + + protected async Task StartNewSystemAsync(CancellationToken cancellationToken = default) + { + var sb = + new StringBuilder("akka.remote.dot-netty.tcp{").AppendLine() + .AppendFormat("port={0}", _myAddress.Port) + .AppendLine() + .AppendFormat(@"hostname=""{0}""", _myAddress.Host) + .AppendLine("}"); + var config = + ConfigurationFactory .ParseString(sb.ToString()) .WithFallback(Sys.Settings.Config); - var system = ActorSystem.Create(Sys.Name, config); - InjectDeployments(system, _myself); - AttachConductor(new TestConductor(system)); - return system; - } - + var system = ActorSystem.Create(Sys.Name, config); + InjectDeployments(system, Myself); + await AttachConductorAsync(new TestConductor(system), cancellationToken); + return system; + } - public void Dispose() - { - Dispose(true); - //Take this object off the finalization queue and prevent finalization code for this object - //from executing a second time. - GC.SuppressFinalize(this); - } + public void Dispose() + { + Dispose(true); + //Take this object off the finalization queue and prevent finalization code for this object + //from executing a second time. + GC.SuppressFinalize(this); + } - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// if set to true the method has been called directly or indirectly by a - /// user's code. Managed and unmanaged resources will be disposed.
- /// if set to false the method has been called by the runtime from inside the finalizer and only - /// unmanaged resources can be disposed. - protected void Dispose(bool disposing) - { - // If disposing equals false, the method has been called by the - // runtime from inside the finalizer and you should not reference - // other objects. Only unmanaged resources can be disposed. + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// if set to true the method has been called directly or indirectly by a + /// user's code. Managed and unmanaged resources will be disposed.
+ /// if set to false the method has been called by the runtime from inside the finalizer and only + /// unmanaged resources can be disposed. + protected void Dispose(bool disposing) + { + // If disposing equals false, the method has been called by the + // runtime from inside the finalizer and you should not reference + // other objects. Only unmanaged resources can be disposed. - //Make sure Dispose does not get called more than once, by checking the disposed field - if (!_isDisposed) + //Make sure Dispose does not get called more than once, by checking the disposed field + if (!_isDisposed) + { + if (disposing) { - if (disposing) - { - Console.WriteLine("---------------DISPOSING--------------------"); - MultiNodeSpecAfterAll(); - } + Console.WriteLine("---------------DISPOSING--------------------"); + MultiNodeSpecAfterAll(); } - _isDisposed = true; } + _isDisposed = true; } +} - //TODO: Improve docs +//TODO: Improve docs +/// +/// Use this to hook into your test framework lifecycle +/// +public interface IMultiNodeSpecCallbacks +{ /// - /// Use this to hook into your test framework lifecycle + /// Call this before the start of the test run. NOT before every test case. /// - public interface IMultiNodeSpecCallbacks - { - /// - /// Call this before the start of the test run. NOT before every test case. - /// - void MultiNodeSpecBeforeAll(); - - /// - /// Call this after the all test cases have run. NOT after every test case. - /// - void MultiNodeSpecAfterAll(); - } -} + void MultiNodeSpecBeforeAll(); + /// + /// Call this after the all test cases have run. NOT after every test case. + /// + void MultiNodeSpecAfterAll(); +} \ No newline at end of file diff --git a/src/core/Akka.Remote.TestKit/Player.cs b/src/core/Akka.Remote.TestKit/Player.cs index dd182dd9eff..4ebaf38d4c0 100644 --- a/src/core/Akka.Remote.TestKit/Player.cs +++ b/src/core/Akka.Remote.TestKit/Player.cs @@ -22,572 +22,646 @@ using DotNetty.Transport.Channels; using Akka.Configuration; -namespace Akka.Remote.TestKit +namespace Akka.Remote.TestKit; + +/// +/// The Player is the client component of the +/// test conductor extension. It registers with +/// the conductor's controller +/// in order to participate in barriers and enable network failure injection +/// +partial class TestConductor //Player trait in JVM version { - /// - /// The Player is the client component of the - /// test conductor extension. It registers with - /// the conductor's controller - /// in order to participate in barriers and enable network failure injection - /// - partial class TestConductor //Player trait in JVM version - { - private IActorRef _client; + private IActorRef _client; - public IActorRef Client + public IActorRef Client + { + get { - get - { - if(_client == null) throw new IllegalStateException("TestConductor client not yet started"); - if(_system.WhenTerminated.IsCompleted) throw new IllegalStateException("TestConductor unavailable because system is terminated; you need to StartNewSystem() before this point"); - return _client; - } + if(_client == null) throw new IllegalStateException("TestConductor client not yet started"); + if(_system.WhenTerminated.IsCompleted) throw new IllegalStateException("TestConductor unavailable because system is terminated; you need to StartNewSystem() before this point"); + return _client; } + } - /// - /// Connect to the conductor on the given port (the host is taken from setting - /// `akka.testconductor.host`). The connection is made asynchronously, but you - /// should await completion of the returned Future because that implies that - /// all expected participants of this test have successfully connected (i.e. - /// this is a first barrier in itself). The number of expected participants is - /// set in `.startController()`. - /// - public Task StartClient(RoleName name, IPEndPoint controllerAddr) - { - if(_client != null) throw new IllegalStateException("TestConductorClient already started"); - _client = - _system.ActorOf(Props.Create(() => new ClientFSM(name, controllerAddr)), "TestConductorClient"); - - var a = _system.ActorOf(Props.Create()); + /// + /// Connect to the conductor on the given port (the host is taken from setting + /// `akka.testconductor.host`). The connection is made asynchronously, but you + /// should await completion of the returned Future because that implies that + /// all expected participants of this test have successfully connected (i.e. + /// this is a first barrier in itself). The number of expected participants is + /// set in `.startController()`. + /// + public Task StartClient(RoleName name, IPEndPoint controllerAddr) + { + // Use the async version with no cancellation token for consistency + return StartClientAsync(name, controllerAddr, CancellationToken.None); + } - return a.Ask(_client); - } + /// + /// Connect to the conductor on the given port (the host is taken from setting + /// `akka.testconductor.host`). The connection is made asynchronously, but you + /// should await completion of the returned Future because that implies that + /// all expected participants of this test have successfully connected (i.e. + /// this is a first barrier in itself). The number of expected participants is + /// set in `.startController()`. + /// + public Task StartClientAsync(RoleName name, IPEndPoint controllerAddr, CancellationToken cancellationToken = default) + { + if(_client != null) + throw new IllegalStateException("TestConductorClient already started"); + + _client = _system.ActorOf(Props.Create(() => new ClientFSM(name, controllerAddr)), "TestConductorClient"); + + var a = _system.ActorOf(Props.Create()); + return a.Ask(_client, cancellationToken); + } - private class WaitForClientFSMToConnect : UntypedActor - { - IActorRef _waiting; + private class WaitForClientFSMToConnect : UntypedActor + { + IActorRef _waiting; - protected override void OnReceive(object message) + protected override void OnReceive(object message) + { + if (message is IActorRef fsm) { - if (message is IActorRef fsm) - { - _waiting = Sender; - fsm.Tell(new FSMBase.SubscribeTransitionCallBack(Self)); - return; - } + _waiting = Sender; + fsm.Tell(new FSMBase.SubscribeTransitionCallBack(Self)); + return; + } - if (message is FSMBase.Transition transition) + if (message is FSMBase.Transition transition) + { + switch (transition.From) { - switch (transition.From) - { - case ClientFSM.State.Connecting when transition.To == ClientFSM.State.AwaitDone: - return; - case ClientFSM.State.AwaitDone when transition.To == ClientFSM.State.Connected: - _waiting.Tell(Done.Instance); - Context.Stop(Self); - return; - default: - _waiting.Tell(new Exception("unexpected transition: " + transition)); - Context.Stop(Self); - break; - } + case ClientFSM.State.Connecting when transition.To == ClientFSM.State.AwaitDone: + return; + case ClientFSM.State.AwaitDone when transition.To == ClientFSM.State.Connected: + _waiting.Tell(Done.Instance); + Context.Stop(Self); + return; + default: + _waiting.Tell(new Exception("unexpected transition: " + transition)); + Context.Stop(Self); + break; } - - if (message is not FSMBase.CurrentState { State: ClientFSM.State.Connected }) return; - _waiting.Tell(Done.Instance); - Context.Stop(Self); } - } - /// - /// Enter the named barriers, one after the other, in the order given. Will - /// throw an exception in case of timeouts or other errors. - /// - public void Enter(RoleName roleName, string name) - { - Enter(Settings.BarrierTimeout, roleName, ImmutableList.Create(name)); + if (message is not FSMBase.CurrentState { State: ClientFSM.State.Connected }) return; + _waiting.Tell(Done.Instance); + Context.Stop(Self); } + } - /// - /// Enter the named barriers, one after the other, in the order given. Will - /// throw an exception in case of timeouts or other errors. - /// - public void Enter(TimeSpan timeout, RoleName roleName, ImmutableList names) + /// + /// Enter the named barriers, one after the other, in the order given. Will + /// throw an exception in case of timeouts or other errors. + /// + public void Enter(RoleName roleName, string name) + { + // Use sync-over-async pattern to maintain single source of truth + try { - _system.Log.Debug("entering barriers {0}", names.Aggregate((a, b) => "(" + a + "," + b + ")")); - var stop = Deadline.Now + timeout; - - foreach (var name in names) - { - var barrierTimeout = stop.TimeLeft; - if (barrierTimeout.Ticks < 0) - { - _client.Tell(new ToServer(new FailBarrier(name, roleName))); - throw new TimeoutException("Server timed out while waiting for barrier " + name); - } - try - { - var askTimeout = barrierTimeout + Settings.QueryTimeout; - // Need to force barrier to wait here, so we can pass along a "fail barrier" message in the event - // of a failed operation - var result = _client.Ask(new ToServer(new EnterBarrier(name, barrierTimeout, roleName)), askTimeout).Result; - } - catch (AggregateException ex) - { - _client.Tell(new ToServer(new FailBarrier(name, roleName))); - throw new TimeoutException("Client timed out while waiting for barrier " + name, ex); - } - catch (OperationCanceledException) - { - _system.Log.Debug("OperationCanceledException was thrown instead of AggregateException"); - } - _system.Log.Debug("passed barrier {0}", name); - } + EnterAsync(Settings.BarrierTimeout, roleName, ImmutableList.Create(name), CancellationToken.None).GetAwaiter().GetResult(); } - - public Task
GetAddressFor(RoleName name) + catch (AggregateException ex) when (ex.InnerException != null) { - return _client.Ask
(new ToServer(new GetAddress(name)), Settings.QueryTimeout); + throw ex.InnerException; } } /// - /// This is the controlling entity on the player - /// side: in a first step it registers itself with a symbolic name and its remote - /// address at the , then waits for the - /// `Done` message which signals that all other expected test participants have - /// done the same. After that, it will pass barrier requests to and from the - /// coordinator and react to the Conductors’s - /// requests for failure injection. - /// - /// Note that you can't perform requests concurrently, e.g. enter barrier - /// from one thread and ask for node address from another thread. - /// - /// INTERNAL API. + /// Async version of Enter. Enter the named barrier. + /// Will throw an exception in case of timeouts or other errors. /// - [InternalApi] - internal class ClientFSM : FSM, ILoggingFSM + public Task EnterAsync(RoleName roleName, string name, CancellationToken cancellationToken = default) { - public enum State + return EnterAsync(Settings.BarrierTimeout, roleName, ImmutableList.Create(name), cancellationToken); + } + + /// + /// Enter the named barriers, one after the other, in the order given. Will + /// throw an exception in case of timeouts or other errors. + /// + public void Enter(TimeSpan timeout, RoleName roleName, ImmutableList names) + { + // Use sync-over-async pattern to maintain single source of truth + try { - Connecting, - AwaitDone, - Connected, - Failed + EnterAsync(timeout, roleName, names, CancellationToken.None).GetAwaiter().GetResult(); } - - internal class Data + catch (AggregateException ex) when (ex.InnerException != null) { - readonly IChannel _channel; - public IChannel Channel { get { return _channel; } } - readonly (string, IActorRef)? _runningOp; - public (string, IActorRef)? RunningOp => _runningOp; + throw ex.InnerException; + } + } + + /// + /// Async version of Enter. Enter the named barriers, one after the other, in the order given. + /// Will throw an exception in case of timeouts or other errors. + /// + public async Task EnterAsync(TimeSpan timeout, RoleName roleName, ImmutableList names, CancellationToken cancellationToken = default) + { + _system.Log.Debug("entering barriers {0}", names.Aggregate((a, b) => "(" + a + "," + b + ")")); + var stop = Deadline.Now + timeout; - public Data(IChannel channel, (string, IActorRef)? runningOp) + foreach (var name in names) + { + var barrierTimeout = stop.TimeLeft; + if (barrierTimeout.Ticks < 0) { - _channel = channel; - _runningOp = runningOp; + _client.Tell(new ToServer(new FailBarrier(name, roleName))); + throw new TimeoutException("Server timed out while waiting for barrier " + name); } - - private bool Equals(Data other) + try { - return Equals(_channel, other._channel) && Equals(_runningOp, other._runningOp); + var askTimeout = barrierTimeout + Settings.QueryTimeout; + // Use async ask with cancellation token + var result = await _client.Ask(new ToServer(new EnterBarrier(name, barrierTimeout, roleName)), askTimeout, cancellationToken); } - - /// - public override bool Equals(object obj) + catch (TaskCanceledException ex) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((Data) obj); + _client.Tell(new ToServer(new FailBarrier(name, roleName))); + throw new TimeoutException("Client timed out while waiting for barrier " + name, ex); } - - /// - public override int GetHashCode() + catch (OperationCanceledException ex) { - unchecked - { - return ((_channel != null ? _channel.GetHashCode() : 0) * 397) - ^ (_runningOp != null ? _runningOp.GetHashCode() : 0); - } + _client.Tell(new ToServer(new FailBarrier(name, roleName))); + throw new TimeoutException("Operation was cancelled while waiting for barrier " + name, ex); } + _system.Log.Debug("passed barrier {0}", name); + } + } - /// - /// Compares two specified for equality. - /// - /// The first used for comparison - /// The second used for comparison - /// true if both are equal; otherwise false - public static bool operator ==(Data left, Data right) - { - return Equals(left, right); - } + public Task
GetAddressFor(RoleName name) + { + return GetAddressForAsync(name, CancellationToken.None); + } - /// - /// Compares two specified for inequality. - /// - /// The first used for comparison - /// The second used for comparison - /// true if both are not equal; otherwise false - public static bool operator !=(Data left, Data right) - { - return !Equals(left, right); - } + /// + /// Async version of GetAddressFor with cancellation token support. + /// + public Task
GetAddressForAsync(RoleName name, CancellationToken cancellationToken = default) + { + return _client.Ask
(new ToServer(new GetAddress(name)), Settings.QueryTimeout, cancellationToken); + } +} - public Data Copy((string, IActorRef)? runningOp) - { - return new Data(Channel, runningOp); - } +/// +/// This is the controlling entity on the player +/// side: in a first step it registers itself with a symbolic name and its remote +/// address at the , then waits for the +/// `Done` message which signals that all other expected test participants have +/// done the same. After that, it will pass barrier requests to and from the +/// coordinator and react to the Conductors’s +/// requests for failure injection. +/// +/// Note that you can't perform requests concurrently, e.g. enter barrier +/// from one thread and ask for node address from another thread. +/// +/// INTERNAL API. +/// +[InternalApi] +internal class ClientFSM : FSM, ILoggingFSM +{ + public enum State + { + Connecting, + AwaitDone, + Connected, + Failed + } + + internal class Data + { + public IChannel Channel { get; } + public (string, IActorRef)? RunningOp { get; } + + public Data(IChannel channel, (string, IActorRef)? runningOp) + { + Channel = channel; + RunningOp = runningOp; + } + + private bool Equals(Data other) + { + return Equals(Channel, other.Channel) && Equals(RunningOp, other.RunningOp); } - internal class Connected : INoSerializationVerificationNeeded + /// + public override bool Equals(object obj) { - readonly IChannel _channel; - public IChannel Channel{get { return _channel; }} + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is Data data && Equals(data); + } - public Connected(IChannel channel) + /// + public override int GetHashCode() + { + unchecked { - _channel = channel; + return ((Channel != null ? Channel.GetHashCode() : 0) * 397) + ^ (RunningOp != null ? RunningOp.GetHashCode() : 0); } + } - protected bool Equals(Connected other) - { - return Equals(_channel, other._channel); - } + /// + /// Compares two specified for equality. + /// + /// The first used for comparison + /// The second used for comparison + /// true if both are equal; otherwise false + public static bool operator ==(Data left, Data right) + { + return Equals(left, right); + } - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((Connected) obj); - } + /// + /// Compares two specified for inequality. + /// + /// The first used for comparison + /// The second used for comparison + /// true if both are not equal; otherwise false + public static bool operator !=(Data left, Data right) + { + return !Equals(left, right); + } - /// - public override int GetHashCode() - { - return (_channel != null ? _channel.GetHashCode() : 0); - } + public Data Copy((string, IActorRef)? runningOp) + { + return new Data(Channel, runningOp); + } + } - /// - /// Compares two specified for equality. - /// - /// The first used for comparison - /// The second used for comparison - /// true if both are equal; otherwise false - public static bool operator ==(Connected left, Connected right) - { - return Equals(left, right); - } + internal class Connected : INoSerializationVerificationNeeded + { + public IChannel Channel { get; } - /// - /// Compares two specified for inequality. - /// - /// The first used for comparison - /// The second used for comparison - /// true if both are not equal; otherwise false - public static bool operator !=(Connected left, Connected right) - { - return !Equals(left, right); - } + public Connected(IChannel channel) + { + Channel = channel; } - /// - /// TBD - /// - internal class ConnectionFailure : Exception + protected bool Equals(Connected other) { - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public ConnectionFailure(string message) : base(message) - { - } + return Equals(Channel, other.Channel); } - internal class Disconnected + /// + public override bool Equals(object obj) { - private Disconnected() { } - public static Disconnected Instance { get; } = new(); + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is Connected connected && Equals(connected); } - private readonly ILoggingAdapter _log = Context.GetLogger(); - readonly TestConductorSettings _settings; - readonly PlayerHandler _handler; - readonly RoleName _name; + /// + public override int GetHashCode() + { + return (Channel != null ? Channel.GetHashCode() : 0); + } - public ClientFSM(RoleName name, IPEndPoint controllerAddr) + /// + /// Compares two specified for equality. + /// + /// The first used for comparison + /// The second used for comparison + /// true if both are equal; otherwise false + public static bool operator ==(Connected left, Connected right) { - _settings = TestConductor.Get(Context.System).Settings; - _handler = new PlayerHandler(controllerAddr, _settings.ClientReconnects, _settings.ReconnectBackoff, - _settings.ClientSocketWorkerPoolSize, Self, Logging.GetLogger(Context.System, "PlayerHandler"), - Context.System.Scheduler); - _name = name; + return Equals(left, right); + } + + /// + /// Compares two specified for inequality. + /// + /// The first used for comparison + /// The second used for comparison + /// true if both are not equal; otherwise false + public static bool operator !=(Connected left, Connected right) + { + return !Equals(left, right); + } + } - InitFSM(); + /// + /// TBD + /// + internal class ConnectionFailure : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public ConnectionFailure(string message) : base(message) + { } + } + + internal class Disconnected + { + private Disconnected() { } + public static Disconnected Instance { get; } = new(); + } + + private readonly ILoggingAdapter _log = Context.GetLogger(); + private readonly TestConductorSettings _settings; + private readonly PlayerHandler _handler; + private readonly RoleName _name; + + public ClientFSM(RoleName name, IPEndPoint controllerAddr) + { + _settings = TestConductor.Get(Context.System).Settings; + _handler = new PlayerHandler(controllerAddr, _settings.ClientReconnects, _settings.ReconnectBackoff, + _settings.ClientSocketWorkerPoolSize, Self, Logging.GetLogger(Context.System, "PlayerHandler"), + Context.System.Scheduler); + _name = name; + + InitFSM(); + } + + public void InitFSM() + { + StartWith(State.Connecting, new Data(null, null)); - public void InitFSM() + When(State.Connecting, @event => { - StartWith(State.Connecting, new Data(null, null)); + if (@event.FsmEvent is IClientOp) + { + return Stay().Replying(new Status.Failure(new IllegalStateException("not connected yet"))); + } + var connected = @event.FsmEvent as Connected; + if (connected != null) + { + connected.Channel.WriteAndFlushAsync(new Hello(_name.Name, TestConductor.Get(Context.System).Address)); + return GoTo(State.AwaitDone).Using(new Data(connected.Channel, null)); + } + if (@event.FsmEvent is ConnectionFailure) + { + return GoTo(State.Failed); + } + if (@event.FsmEvent is StateTimeout) + { + _log.Error($"Failed to connect to test conductor within {_settings.ConnectTimeout.TotalMilliseconds} ms."); + return GoTo(State.Failed); + } + + return null; + }, _settings.ConnectTimeout); - When(State.Connecting, @event => + When(State.AwaitDone, @event => + { + switch (@event.FsmEvent) { - if (@event.FsmEvent is IClientOp) - { - return Stay().Replying(new Status.Failure(new IllegalStateException("not connected yet"))); - } - var connected = @event.FsmEvent as Connected; - if (connected != null) - { - connected.Channel.WriteAndFlushAsync(new Hello(_name.Name, TestConductor.Get(Context.System).Address)); - return GoTo(State.AwaitDone).Using(new Data(connected.Channel, null)); - } - if (@event.FsmEvent is ConnectionFailure) - { + case Done: + _log.Debug("received Done: starting test"); + return GoTo(State.Connected); + case INetworkOp: + _log.Error("Received {0} instead of Done", @event.FsmEvent); return GoTo(State.Failed); - } - if (@event.FsmEvent is StateTimeout) - { - _log.Error($"Failed to connect to test conductor within {_settings.ConnectTimeout.TotalMilliseconds} ms."); + case IServerOp: + return Stay().Replying(new Failure(new IllegalStateException("not connected yet"))); + case StateTimeout: + _log.Error("connect timeout to TestConductor"); return GoTo(State.Failed); - } - - return null; - }, _settings.ConnectTimeout); + default: + return null; + } + }, _settings.BarrierTimeout); - When(State.AwaitDone, @event => + When(State.Connected, @event => + { + if (@event.FsmEvent is Disconnected) { - switch (@event.FsmEvent) - { - case Done: - _log.Debug("received Done: starting test"); - return GoTo(State.Connected); - case INetworkOp: - _log.Error("Received {0} instead of Done", @event.FsmEvent); - return GoTo(State.Failed); - case IServerOp: - return Stay().Replying(new Failure(new IllegalStateException("not connected yet"))); - case StateTimeout: - _log.Error("connect timeout to TestConductor"); - return GoTo(State.Failed); - default: - return null; - } - }, _settings.BarrierTimeout); - - When(State.Connected, @event => + _log.Info("disconnected from TestConductor"); + throw new ConnectionFailure("disconnect"); + } + if(@event.FsmEvent is ToServer && @event.StateData.Channel != null) { - if (@event.FsmEvent is Disconnected) - { - _log.Info("disconnected from TestConductor"); - throw new ConnectionFailure("disconnect"); - } - if(@event.FsmEvent is ToServer && @event.StateData.Channel != null) + @event.StateData.Channel.WriteAndFlushAsync(Done.Instance); + return Stay(); + } + var toServer = @event.FsmEvent as IToServer; + if (toServer != null && @event.StateData.Channel != null && + @event.StateData.RunningOp == null) + { + @event.StateData.Channel.WriteAndFlushAsync(toServer.Msg); + string token = null; + var enterBarrier = @event.FsmEvent as ToServer; + if (enterBarrier != null) token = enterBarrier.Msg.Name; + else { - @event.StateData.Channel.WriteAndFlushAsync(Done.Instance); - return Stay(); + var getAddress = @event.FsmEvent as ToServer; + if (getAddress != null) token = getAddress.Msg.Node.Name; } - var toServer = @event.FsmEvent as IToServer; - if (toServer != null && @event.StateData.Channel != null && - @event.StateData.RunningOp == null) + return Stay().Using(@event.StateData.Copy(runningOp: (token, Sender))); + } + if (toServer != null && @event.StateData.Channel != null && + @event.StateData.RunningOp != null) + { + _log.Error("cannot write {0} while waiting for {1}", toServer.Msg, @event.StateData.RunningOp); + return Stay(); + } + if (@event.FsmEvent is IClientOp && @event.StateData.Channel != null) + { + var barrierResult = @event.FsmEvent as BarrierResult; + if (barrierResult != null) { - @event.StateData.Channel.WriteAndFlushAsync(toServer.Msg); - string token = null; - var enterBarrier = @event.FsmEvent as ToServer; - if (enterBarrier != null) token = enterBarrier.Msg.Name; - else + if (@event.StateData.RunningOp == null) { - var getAddress = @event.FsmEvent as ToServer; - if (getAddress != null) token = getAddress.Msg.Node.Name; + _log.Warning("did not expect {0}", @event.FsmEvent); } - return Stay().Using(@event.StateData.Copy(runningOp: (token, Sender))); - } - if (toServer != null && @event.StateData.Channel != null && - @event.StateData.RunningOp != null) - { - _log.Error("cannot write {0} while waiting for {1}", toServer.Msg, @event.StateData.RunningOp); - return Stay(); - } - if (@event.FsmEvent is IClientOp && @event.StateData.Channel != null) - { - var barrierResult = @event.FsmEvent as BarrierResult; - if (barrierResult != null) + else { - if (@event.StateData.RunningOp == null) + object response; + if (barrierResult.Name != @event.StateData.RunningOp.Value.Item1) { - _log.Warning("did not expect {0}", @event.FsmEvent); + response = + new Failure( + new Exception("wrong barrier " + barrierResult + " received while waiting for " + + @event.StateData.RunningOp.Value.Item1)); } - else - { - object response; - if (barrierResult.Name != @event.StateData.RunningOp.Value.Item1) - { - response = - new Failure( - new Exception("wrong barrier " + barrierResult + " received while waiting for " + - @event.StateData.RunningOp.Value.Item1)); - } - else if (!barrierResult.Success) - { - response = - new Failure( - new Exception("barrier failed:" + @event.StateData.RunningOp.Value.Item1)); - } - else - { - response = barrierResult.Name; - } - @event.StateData.RunningOp.Value.Item2.Tell(response); - } - return Stay().Using(@event.StateData.Copy(runningOp: null)); - } - var addressReply = @event.FsmEvent as AddressReply; - if (addressReply != null) - { - if (@event.StateData.RunningOp == null) + else if (!barrierResult.Success) { - _log.Warning("did not expect {0}", @event.FsmEvent); + response = + new Failure( + new Exception("barrier failed:" + @event.StateData.RunningOp.Value.Item1)); } else { - @event.StateData.RunningOp.Value.Item2.Tell(addressReply.Addr); + response = barrierResult.Name; } - return Stay().Using(@event.StateData.Copy(runningOp: null)); + @event.StateData.RunningOp.Value.Item2.Tell(response); } - var throttleMsg = @event.FsmEvent as ThrottleMsg; - if (@event.FsmEvent is ThrottleMsg) + return Stay().Using(@event.StateData.Copy(runningOp: null)); + } + var addressReply = @event.FsmEvent as AddressReply; + if (addressReply != null) + { + if (@event.StateData.RunningOp == null) { - ThrottleMode mode; - if (throttleMsg.RateMBit < 0.0f) mode = Unthrottled.Instance; - else if (throttleMsg.RateMBit == 0.0f) mode = Blackhole.Instance; - else mode = new Transport.TokenBucket(1000, throttleMsg.RateMBit*125000, 0, 0); - var cmdTask = - TestConductor.Get(Context.System) - .Transport.ManagementCommand(new SetThrottle(throttleMsg.Target, throttleMsg.Direction, - mode)); - - var self = Self; - cmdTask.ContinueWith(t => - { - if (t.IsFaulted) - throw new ConfigurationException("Throttle was requested from the TestConductor, but no transport " + - "adapters available that support throttling. Specify 'testTransport(on=true)' in your MultiNodeConfig"); - self.Tell(new ToServer(Done.Instance)); - }); - return Stay(); + _log.Warning("did not expect {0}", @event.FsmEvent); } - if (@event.FsmEvent is DisconnectMsg) - return Stay(); //FIXME is this the right EC for the future below? - var terminateMsg = @event.FsmEvent as TerminateMsg; - if (terminateMsg != null) + else { - _log.Info("Received TerminateMsg - shutting down..."); - if (terminateMsg.ShutdownOrExit.IsLeft && terminateMsg.ShutdownOrExit.ToLeft().Value == false) - { - Context.System.Terminate(); - return Stay(); - } - if (terminateMsg.ShutdownOrExit.IsLeft && terminateMsg.ShutdownOrExit.ToLeft().Value == true) - { - Context.System.AsInstanceOf().Abort(); - return Stay(); - } - if (terminateMsg.ShutdownOrExit.IsRight) - { - Environment.Exit(terminateMsg.ShutdownOrExit.ToRight().Value); - return Stay(); - } + @event.StateData.RunningOp.Value.Item2.Tell(addressReply.Addr); } - if (@event.FsmEvent is Done) return Stay(); //FIXME what should happen? + return Stay().Using(@event.StateData.Copy(runningOp: null)); } - return null; - }); - - When(State.Failed, @event => - { - if (@event.FsmEvent is IClientOp) + var throttleMsg = @event.FsmEvent as ThrottleMsg; + if (@event.FsmEvent is ThrottleMsg) { - return Stay().Replying(new Status.Failure(new Exception("cannot do " + @event.FsmEvent + " while failed"))); + ThrottleMode mode; + if (throttleMsg.RateMBit < 0.0f) mode = Unthrottled.Instance; + else if (throttleMsg.RateMBit == 0.0f) mode = Blackhole.Instance; + else mode = new Transport.TokenBucket(1000, throttleMsg.RateMBit*125000, 0, 0); + var cmdTask = + TestConductor.Get(Context.System) + .Transport.ManagementCommand(new SetThrottle(throttleMsg.Target, throttleMsg.Direction, + mode)); + + var self = Self; + cmdTask.ContinueWith(t => + { + if (t.IsFaulted) + throw new ConfigurationException("Throttle was requested from the TestConductor, but no transport " + + "adapters available that support throttling. Specify 'testTransport(on=true)' in your MultiNodeConfig"); + self.Tell(new ToServer(Done.Instance)); + }); + return Stay(); } - if (@event.FsmEvent is INetworkOp) + if (@event.FsmEvent is DisconnectMsg) + return Stay(); //FIXME is this the right EC for the future below? + var terminateMsg = @event.FsmEvent as TerminateMsg; + if (terminateMsg != null) { - _log.Warning("ignoring network message {0} while Failed", @event.FsmEvent); - return Stay(); + _log.Info("Received TerminateMsg - shutting down..."); + if (terminateMsg.ShutdownOrExit.IsLeft && terminateMsg.ShutdownOrExit.ToLeft().Value == false) + { + Context.System.Terminate(); + return Stay(); + } + if (terminateMsg.ShutdownOrExit.IsLeft && terminateMsg.ShutdownOrExit.ToLeft().Value == true) + { + Context.System.AsInstanceOf().Abort(); + return Stay(); + } + if (terminateMsg.ShutdownOrExit.IsRight) + { + Environment.Exit(terminateMsg.ShutdownOrExit.ToRight().Value); + return Stay(); + } } - return null; - }); + if (@event.FsmEvent is Done) return Stay(); //FIXME what should happen? + } + return null; + }); - OnTermination(e => + When(State.Failed, @event => + { + if (@event.FsmEvent is IClientOp) { - _log.Info("Terminating connection to multi-node test controller due to [{0}]", e.Reason); - if (e.StateData.Channel != null) + return Stay().Replying(new Status.Failure(new Exception("cannot do " + @event.FsmEvent + " while failed"))); + } + if (@event.FsmEvent is INetworkOp) + { + _log.Warning("ignoring network message {0} while Failed", @event.FsmEvent); + return Stay(); + } + return null; + }); + + OnTermination(e => + { + _log.Info("Terminating connection to multi-node test controller due to [{0}]", e.Reason); + if (e.StateData.Channel != null) + { + var disconnectTimeout = TimeSpan.FromSeconds(2); //todo: make into setting loaded from HOCON + if (!e.StateData.Channel.CloseAsync().Wait(disconnectTimeout)) { - var disconnectTimeout = TimeSpan.FromSeconds(2); //todo: make into setting loaded from HOCON - if (!e.StateData.Channel.CloseAsync().Wait(disconnectTimeout)) - { - _log.Warning("Failed to disconnect from conductor within {0}", disconnectTimeout); - } + _log.Warning("Failed to disconnect from conductor within {0}", disconnectTimeout); } - }); + } + }); - Initialize(); - } + Initialize(); } +} + +/// +/// This handler only forwards messages received from the conductor to the +/// +/// INTERNAL API. +/// +internal class PlayerHandler : ChannelHandlerAdapter +{ + private readonly IPEndPoint _server; + private int _reconnects; + private readonly TimeSpan _backoff; + private readonly int _poolSize; + private readonly IActorRef _fsm; + private readonly ILoggingAdapter _log; + private readonly IScheduler _scheduler; + private bool _loggedDisconnect = false; + + private Deadline _nextAttempt; /// - /// This handler only forwards messages received from the conductor to the - /// - /// INTERNAL API. + /// Shareable, since the handler may be added multiple times during reconnect /// - internal class PlayerHandler : ChannelHandlerAdapter - { - private readonly IPEndPoint _server; - private int _reconnects; - private readonly TimeSpan _backoff; - private readonly int _poolSize; - private readonly IActorRef _fsm; - private readonly ILoggingAdapter _log; - private readonly IScheduler _scheduler; - private bool _loggedDisconnect = false; + public override bool IsSharable => true; - private Deadline _nextAttempt; - - /// - /// Shareable, since the handler may be added multiple times during reconnect - /// - public override bool IsSharable => true; + public PlayerHandler(IPEndPoint server, int reconnects, TimeSpan backoff, int poolSize, IActorRef fsm, + ILoggingAdapter log, IScheduler scheduler) + { + _server = server; + _reconnects = reconnects; + _backoff = backoff; + _poolSize = poolSize; + _fsm = fsm; + _log = log; + _scheduler = scheduler; + + Reconnect(); + } - public PlayerHandler(IPEndPoint server, int reconnects, TimeSpan backoff, int poolSize, IActorRef fsm, - ILoggingAdapter log, IScheduler scheduler) - { - _server = server; - _reconnects = reconnects; - _backoff = backoff; - _poolSize = poolSize; - _fsm = fsm; - _log = log; - _scheduler = scheduler; - - Reconnect(); - } + private static string FormatConnectionFailure(IChannelHandlerContext context, Exception exception) + { + var sb = new StringBuilder(); + sb.AppendLine($"Connection between [Local: {context.Channel.LocalAddress}] and [Remote: {context.Channel.RemoteAddress}] has failed."); + sb.AppendLine($"Cause: {exception}"); + sb.AppendLine($"Trace: {exception.StackTrace}"); + return sb.ToString(); + } - private static string FormatConnectionFailure(IChannelHandlerContext context, Exception exception) + public override void ExceptionCaught(IChannelHandlerContext context, Exception exception) + { + _log.Debug("channel {0} exception {1}", context.Channel, exception); + if (exception is ConnectException && _reconnects > 0) { - var sb = new StringBuilder(); - sb.AppendLine($"Connection between [Local: {context.Channel.LocalAddress}] and [Remote: {context.Channel.RemoteAddress}] has failed."); - sb.AppendLine($"Cause: {exception}"); - sb.AppendLine($"Trace: {exception.StackTrace}"); - return sb.ToString(); + _reconnects -= 1; + if (_nextAttempt.IsOverdue) + { + Reconnect(); + } + else + { + _scheduler.Advanced.ScheduleOnce(_nextAttempt.TimeLeft, Reconnect); + } + return; } + _fsm.Tell(new ClientFSM.ConnectionFailure(FormatConnectionFailure(context, exception))); + } - public override void ExceptionCaught(IChannelHandlerContext context, Exception exception) + private void Reconnect() + { + _log.Debug("Connecting..."); + _nextAttempt = Deadline.Now + _backoff; + RemoteConnection.CreateConnection(Role.Client, _server, _poolSize, this).ContinueWith(_ => { - _log.Debug("channel {0} exception {1}", context.Channel, exception); - if (exception is ConnectException && _reconnects > 0) + _log.Debug("Failed to connect.... Retrying again in {0}s. {1} attempts left.", _nextAttempt.TimeLeft,_reconnects); + if (_reconnects > 0) { _reconnects -= 1; if (_nextAttempt.IsOverdue) @@ -598,79 +672,54 @@ public override void ExceptionCaught(IChannelHandlerContext context, Exception e { _scheduler.Advanced.ScheduleOnce(_nextAttempt.TimeLeft, Reconnect); } - return; } - _fsm.Tell(new ClientFSM.ConnectionFailure(FormatConnectionFailure(context, exception))); - } + }, TaskContinuationOptions.NotOnRanToCompletion); + } - private void Reconnect() - { - _log.Debug("Connecting..."); - _nextAttempt = Deadline.Now + _backoff; - RemoteConnection.CreateConnection(Role.Client, _server, _poolSize, this).ContinueWith(_ => - { - _log.Debug("Failed to connect.... Retrying again in {0}s. {1} attempts left.", _nextAttempt.TimeLeft,_reconnects); - if (_reconnects > 0) - { - _reconnects -= 1; - if (_nextAttempt.IsOverdue) - { - Reconnect(); - } - else - { - _scheduler.Advanced.ScheduleOnce(_nextAttempt.TimeLeft, Reconnect); - } - } - }, TaskContinuationOptions.NotOnRanToCompletion); - } + public override void ChannelActive(IChannelHandlerContext context) + { + _log.Debug("connected to {0}", context.Channel.RemoteAddress); + _fsm.Tell(new ClientFSM.Connected(context.Channel)); + context.FireChannelActive(); + } - public override void ChannelActive(IChannelHandlerContext context) + public override void ChannelInactive(IChannelHandlerContext context) + { + if (!_loggedDisconnect) //added this to help mute log messages { - _log.Debug("connected to {0}", context.Channel.RemoteAddress); - _fsm.Tell(new ClientFSM.Connected(context.Channel)); - context.FireChannelActive(); + _loggedDisconnect = true; + _log.Debug("disconnected from {0}", context.Channel.RemoteAddress); + } + _fsm.Tell(PoisonPill.Instance); - public override void ChannelInactive(IChannelHandlerContext context) + // run outside of the Helios / DotNetty threadpool + Task.Factory.StartNew(() => { - if (!_loggedDisconnect) //added this to help mute log messages - { - _loggedDisconnect = true; - _log.Debug("disconnected from {0}", context.Channel.RemoteAddress); - - } - _fsm.Tell(PoisonPill.Instance); - - // run outside of the Helios / DotNetty threadpool - Task.Factory.StartNew(() => - { - RemoteConnection.Shutdown(context.Channel); + RemoteConnection.Shutdown(context.Channel); #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - RemoteConnection.ReleaseAll(); // yep, let it run asynchronously. + RemoteConnection.ReleaseAll(); // yep, let it run asynchronously. #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - context.FireChannelInactive(); - } - - public override void ChannelRead(IChannelHandlerContext context, object message) - { - var channel = context.Channel; - _log.Debug("message from {0}, {1}", channel.RemoteAddress, message); - if (message is INetworkOp) - { - _fsm.Tell(message); - return; - } - _log.Info("server {0} sent garbage '{1}', disconnecting", channel.RemoteAddress, message); - channel.CloseAsync(); - } + }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + context.FireChannelInactive(); + } - public override Task CloseAsync(IChannelHandlerContext context) + public override void ChannelRead(IChannelHandlerContext context, object message) + { + var channel = context.Channel; + _log.Debug("message from {0}, {1}", channel.RemoteAddress, message); + if (message is INetworkOp) { - _log.Info("Client: disconnecting {0} from {1}", context.Channel.LocalAddress, context.Channel.RemoteAddress); - return base.CloseAsync(context); + _fsm.Tell(message); + return; } + _log.Info("server {0} sent garbage '{1}', disconnecting", channel.RemoteAddress, message); + channel.CloseAsync(); } -} + public override Task CloseAsync(IChannelHandlerContext context) + { + _log.Info("Client: disconnecting {0} from {1}", context.Channel.LocalAddress, context.Channel.RemoteAddress); + return base.CloseAsync(context); + } +} \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeDeathWatchSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeDeathWatchSpec.cs index a41fda8820a..5e97d3a73b6 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeDeathWatchSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/RemoteNodeDeathWatchSpec.cs @@ -8,6 +8,7 @@ using System; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Akka.Actor; using Akka.Configuration; using Akka.Event; @@ -17,547 +18,545 @@ using Akka.TestKit; using static Akka.Remote.Tests.MultiNode.RemoteNodeDeathWatchMultiNetSpec; -namespace Akka.Remote.Tests.MultiNode +namespace Akka.Remote.Tests.MultiNode; + +public class RemoteNodeDeathWatchMultiNetSpec : MultiNodeConfig { - public class RemoteNodeDeathWatchMultiNetSpec : MultiNodeConfig + public RemoteNodeDeathWatchMultiNetSpec() { - public RemoteNodeDeathWatchMultiNetSpec() - { - First = Role("first"); - Second = Role("second"); - Third = Role("third"); + First = Role("first"); + Second = Role("second"); + Third = Role("third"); - CommonConfig = DebugConfig(false).WithFallback(ConfigurationFactory.ParseString(@" + CommonConfig = DebugConfig(false).WithFallback(ConfigurationFactory.ParseString(@" akka.loglevel = INFO akka.remote.log-remote-lifecycle-events = off ## Use a tighter setting than the default, otherwise it takes 20s for DeathWatch to trigger akka.remote.watch-failure-detector.acceptable-heartbeat-pause = 3 s ")); - TestTransport = true; - } + TestTransport = true; + } - public RoleName First { get; } - public RoleName Second { get; } - public RoleName Third { get; } + public RoleName First { get; } + public RoleName Second { get; } + public RoleName Third { get; } - public sealed class WatchIt + public sealed class WatchIt + { + public WatchIt(IActorRef watchee) { - public WatchIt(IActorRef watchee) - { - Watchee = watchee; - } - - public IActorRef Watchee { get; } + Watchee = watchee; } - public sealed class UnwatchIt - { - public UnwatchIt(IActorRef watchee) - { - Watchee = watchee; - } + public IActorRef Watchee { get; } + } - public IActorRef Watchee { get; } + public sealed class UnwatchIt + { + public UnwatchIt(IActorRef watchee) + { + Watchee = watchee; } - public sealed class Ack - { - public static Ack Instance { get; } = new(); + public IActorRef Watchee { get; } + } - private Ack() - { - } - } + public sealed class Ack + { + public static Ack Instance { get; } = new(); - /// - /// Forwarding to non-watching testActor is not possible, - /// and therefore the message is wrapped. - /// - public sealed class WrappedTerminated + private Ack() { - public WrappedTerminated(Terminated t) - { - T = t; - } + } + } - public Terminated T { get; } + /// + /// Forwarding to non-watching testActor is not possible, + /// and therefore the message is wrapped. + /// + public sealed class WrappedTerminated + { + public WrappedTerminated(Terminated t) + { + T = t; } - public class ProbeActor : ReceiveActor + public Terminated T { get; } + } + + public class ProbeActor : ReceiveActor + { + private readonly IActorRef _testActor; + + public ProbeActor(IActorRef testActor) { - private readonly IActorRef _testActor; + _testActor = testActor; - public ProbeActor(IActorRef testActor) + Receive(w => { - _testActor = testActor; - - Receive(w => - { - Context.Watch(w.Watchee); - Sender.Tell(Ack.Instance); - }); - Receive(w => - { - Context.Unwatch(w.Watchee); - Sender.Tell(Ack.Instance); - }); - Receive(t => _testActor.Forward(new WrappedTerminated(t))); - ReceiveAny(msg => _testActor.Forward(msg)); - } + Context.Watch(w.Watchee); + Sender.Tell(Ack.Instance); + }); + Receive(w => + { + Context.Unwatch(w.Watchee); + Sender.Tell(Ack.Instance); + }); + Receive(t => _testActor.Forward(new WrappedTerminated(t))); + ReceiveAny(msg => _testActor.Forward(msg)); } } +} + +public abstract class RemoteNodeDeathWatchSpec : MultiNodeSpec +{ + private readonly RemoteNodeDeathWatchMultiNetSpec _config; + private readonly Lazy _remoteWatcher; + private readonly Func _identify; - public abstract class RemoteNodeDeathWatchSpec : MultiNodeSpec + protected RemoteNodeDeathWatchSpec(Type type) : this(new RemoteNodeDeathWatchMultiNetSpec(), type) { - private readonly RemoteNodeDeathWatchMultiNetSpec _config; - private readonly Lazy _remoteWatcher; - private readonly Func _identify; + } - protected RemoteNodeDeathWatchSpec(Type type) : this(new RemoteNodeDeathWatchMultiNetSpec(), type) - { - } + protected RemoteNodeDeathWatchSpec(RemoteNodeDeathWatchMultiNetSpec config, Type type) : base(config, type) + { + _config = config; - protected RemoteNodeDeathWatchSpec(RemoteNodeDeathWatchMultiNetSpec config, Type type) : base(config, type) + _remoteWatcher = new Lazy(() => { - _config = config; + Sys.ActorSelection("/system/remote-watcher").Tell(new Identify(null)); + return ExpectMsg(TimeSpan.FromSeconds(10)).Subject; + }); - _remoteWatcher = new Lazy(() => - { - Sys.ActorSelection("/system/remote-watcher").Tell(new Identify(null)); - return ExpectMsg(TimeSpan.FromSeconds(10)).Subject; - }); + _identify = (role, actorName) => + { + Sys.ActorSelection(Node(role) / "user" / actorName).Tell(new Identify(actorName)); + return ExpectMsg(TimeSpan.FromSeconds(10)).Subject; + }; - _identify = (role, actorName) => - { - Sys.ActorSelection(Node(role) / "user" / actorName).Tell(new Identify(actorName)); - return ExpectMsg(TimeSpan.FromSeconds(10)).Subject; - }; + MuteDeadLetters(null, typeof(Heartbeat)); + } - MuteDeadLetters(null, typeof(Heartbeat)); - } + protected override int InitialParticipantsValueFactory => Roles.Count; - protected override int InitialParticipantsValueFactory => Roles.Count; + protected abstract string Scenario { get; } - protected abstract string Scenario { get; } + protected abstract Func SleepAsync { get; } - protected abstract Action Sleep { get; } + private async Task AssertCleanup(TimeSpan? timeout = null) + { + timeout ??= TimeSpan.FromSeconds(5); - private void AssertCleanup(TimeSpan? timeout = null) + await WithinAsync(timeout.Value, async () => { - timeout = timeout ?? TimeSpan.FromSeconds(5); - - Within(timeout.Value, () => + await AwaitAssertAsync(async () => { - AwaitAssert(() => - { - _remoteWatcher.Value.Tell(RemoteWatcher.Stats.Empty); - ExpectMsg(s => Equals(s, RemoteWatcher.Stats.Empty)); - }); + _remoteWatcher.Value.Tell(RemoteWatcher.Stats.Empty); + await ExpectMsgAsync(s => Equals(s, RemoteWatcher.Stats.Empty)); }); - } + }); + } - [MultiNodeFact] - public void RemoteNodeDeathWatchSpecs() - { - Console.WriteLine($"Executing with {Scenario} scenario"); - - RemoteNodeDeathWatch_must_receive_Terminated_when_remote_actor_is_stopped(); - RemoteNodeDeathWatch_must_cleanup_after_watch_unwatch(); - RemoteNodeDeathWatch_must_cleanup_after_bi_directional_watch_unwatch(); - RemoteNodeDeathWatch_must_cleanup_after_bi_directional_watch_stop_unwatch(); - RemoteNodeDeathWatch_must_cleanup_after_stop(); - RemoteNodeDeathWatch_must_receive_Terminated_when_watched_node_crash(); - RemoteNodeDeathWatch_must_cleanup_when_watching_node_crash(); - } + [MultiNodeFact] + public async Task RemoteNodeDeathWatchSpecs() + { + Console.WriteLine($"Executing with {Scenario} scenario"); + + await RemoteNodeDeathWatch_must_receive_Terminated_when_remote_actor_is_stoppedAsync(); + await RemoteNodeDeathWatch_must_cleanup_after_watch_unwatchAsync(); + await RemoteNodeDeathWatch_must_cleanup_after_bi_directional_watch_unwatchAsync(); + await RemoteNodeDeathWatch_must_cleanup_after_bi_directional_watch_stop_unwatchAsync(); + await RemoteNodeDeathWatch_must_cleanup_after_stopAsync(); + await RemoteNodeDeathWatch_must_receive_Terminated_when_watched_node_crashAsync(); + await RemoteNodeDeathWatch_must_cleanup_when_watching_node_crashAsync(); + } - private void RemoteNodeDeathWatch_must_receive_Terminated_when_remote_actor_is_stopped() + private async Task RemoteNodeDeathWatch_must_receive_Terminated_when_remote_actor_is_stoppedAsync() + { + await RunOnAsync(async () => { - RunOn(() => - { - var watcher = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "watcher1"); - EnterBarrier("actors-started-1"); + var watcher = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "watcher1"); + await EnterBarrierAsync("actors-started-1"); - var subject = _identify(_config.Second, "subject1"); - watcher.Tell(new WatchIt(subject)); - ExpectMsg(TimeSpan.FromSeconds(1)); - subject.Tell("hello1"); - EnterBarrier("hello1-message-sent"); - EnterBarrier("watch-established-1"); + var subject = _identify(_config.Second, "subject1"); + watcher.Tell(new WatchIt(subject)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + subject.Tell("hello1"); + await EnterBarrierAsync("hello1-message-sent"); + await EnterBarrierAsync("watch-established-1"); - Sleep(); - ExpectMsg().T.ActorRef.ShouldBe(subject); - }, _config.First); + await SleepAsync(); + (await ExpectMsgAsync()).T.ActorRef.ShouldBe(subject); + }, _config.First); - RunOn(() => - { - var subject = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "subject1"); - EnterBarrier("actors-started-1"); + await RunOnAsync(async () => + { + var subject = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "subject1"); + await EnterBarrierAsync("actors-started-1"); - EnterBarrier("hello1-message-sent"); - ExpectMsg("hello1", TimeSpan.FromSeconds(3)); - EnterBarrier("watch-established-1"); + await EnterBarrierAsync("hello1-message-sent"); + await ExpectMsgAsync("hello1", TimeSpan.FromSeconds(3)); + await EnterBarrierAsync("watch-established-1"); - Sleep(); - Sys.Stop(subject); - }, _config.Second); + await SleepAsync(); + Sys.Stop(subject); + }, _config.Second); - RunOn(() => - { - EnterBarrier("actors-started-1"); - EnterBarrier("hello1-message-sent"); - EnterBarrier("watch-established-1"); - }, _config.Third); + await RunOnAsync(async () => + { + await EnterBarrierAsync("actors-started-1"); + await EnterBarrierAsync("hello1-message-sent"); + await EnterBarrierAsync("watch-established-1"); + }, _config.Third); - EnterBarrier("terminated-verified-1"); + await EnterBarrierAsync("terminated-verified-1"); - // verify that things are cleaned up, and heartbeating is stopped - AssertCleanup(); - ExpectNoMsg(TimeSpan.FromSeconds(2)); - AssertCleanup(); + // verify that things are cleaned up, and heartbeating is stopped + await AssertCleanup(); + await ExpectNoMsgAsync(TimeSpan.FromSeconds(2)); + await AssertCleanup(); - EnterBarrier("after-1"); - } + await EnterBarrierAsync("after-1"); + } - private void RemoteNodeDeathWatch_must_cleanup_after_watch_unwatch() + private async Task RemoteNodeDeathWatch_must_cleanup_after_watch_unwatchAsync() + { + await RunOnAsync(async () => { - RunOn(() => - { - var watcher = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "watcher2"); - EnterBarrier("actors-started-2"); + var watcher = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "watcher2"); + await EnterBarrierAsync("actors-started-2"); - var subject = _identify(_config.Second, "subject2"); - watcher.Tell(new WatchIt(subject)); - ExpectMsg(TimeSpan.FromSeconds(1)); - EnterBarrier("watch-2"); + var subject = _identify(_config.Second, "subject2"); + watcher.Tell(new WatchIt(subject)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + await EnterBarrierAsync("watch-2"); - Sleep(); + await SleepAsync(); - watcher.Tell(new UnwatchIt(subject)); - ExpectMsg(TimeSpan.FromSeconds(1)); - EnterBarrier("unwatch-2"); - }, _config.First); + watcher.Tell(new UnwatchIt(subject)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + await EnterBarrierAsync("unwatch-2"); + }, _config.First); - RunOn(() => Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "subject2"), _config.Second); + RunOn(() => Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "subject2"), _config.Second); - RunOn(() => - { - EnterBarrier("actors-started-2"); - EnterBarrier("watch-2"); - EnterBarrier("unwatch-2"); - }, _config.Second, _config.Third); + await RunOnAsync(async () => + { + await EnterBarrierAsync("actors-started-2"); + await EnterBarrierAsync("watch-2"); + await EnterBarrierAsync("unwatch-2"); + }, _config.Second, _config.Third); - // verify that things are cleaned up, and heartbeating is stopped - AssertCleanup(); - ExpectNoMsg(TimeSpan.FromSeconds(2)); - AssertCleanup(); + // verify that things are cleaned up, and heartbeating is stopped + await AssertCleanup(); + await ExpectNoMsgAsync(TimeSpan.FromSeconds(2)); + await AssertCleanup(); - EnterBarrier("after-2"); - } + await EnterBarrierAsync("after-2"); + } - private void RemoteNodeDeathWatch_must_cleanup_after_bi_directional_watch_unwatch() + private async Task RemoteNodeDeathWatch_must_cleanup_after_bi_directional_watch_unwatchAsync() + { + await RunOnAsync(async () => { - RunOn(() => - { - var watcher = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "watcher3"); - Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "subject3"); - EnterBarrier("actors-started-3"); + var watcher = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "watcher3"); + Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "subject3"); + await EnterBarrierAsync("actors-started-3"); - var other = Myself == _config.First ? _config.Second : _config.First; - var subject = _identify(other, "subject3"); - watcher.Tell(new WatchIt(subject)); - ExpectMsg(TimeSpan.FromSeconds(1)); - EnterBarrier("watch-3"); + var other = Myself == _config.First ? _config.Second : _config.First; + var subject = _identify(other, "subject3"); + watcher.Tell(new WatchIt(subject)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + await EnterBarrierAsync("watch-3"); - Sleep(); + await SleepAsync(); - watcher.Tell(new UnwatchIt(subject)); - ExpectMsg(TimeSpan.FromSeconds(1)); - EnterBarrier("unwatch-3"); - }, _config.First, _config.Second); + watcher.Tell(new UnwatchIt(subject)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + await EnterBarrierAsync("unwatch-3"); + }, _config.First, _config.Second); - RunOn(() => - { - EnterBarrier("actors-started-3"); - EnterBarrier("watch-3"); - EnterBarrier("unwatch-3"); - }, _config.Third); + await RunOnAsync(async () => + { + await EnterBarrierAsync("actors-started-3"); + await EnterBarrierAsync("watch-3"); + await EnterBarrierAsync("unwatch-3"); + }, _config.Third); - // verify that things are cleaned up, and heartbeating is stopped - AssertCleanup(); - ExpectNoMsg(TimeSpan.FromSeconds(2)); - AssertCleanup(); + // verify that things are cleaned up, and heartbeating is stopped + await AssertCleanup(); + await ExpectNoMsgAsync(TimeSpan.FromSeconds(2)); + await AssertCleanup(); - EnterBarrier("after-3"); - } + await EnterBarrierAsync("after-3"); + } - private void RemoteNodeDeathWatch_must_cleanup_after_bi_directional_watch_stop_unwatch() + private async Task RemoteNodeDeathWatch_must_cleanup_after_bi_directional_watch_stop_unwatchAsync() + { + await RunOnAsync(async () => { - RunOn(() => - { - var watcher1 = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "w1"); - var watcher2 = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "w2"); - var s1 = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "s1"); - var s2 = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "s2"); - EnterBarrier("actors-started-4"); - - var other = Myself == _config.First ? _config.Second : _config.First; - var subject1 = _identify(other, "s1"); - var subject2 = _identify(other, "s2"); - watcher1.Tell(new WatchIt(subject1)); - ExpectMsg(TimeSpan.FromSeconds(1)); - watcher2.Tell(new WatchIt(subject2)); - ExpectMsg(TimeSpan.FromSeconds(1)); - EnterBarrier("watch-4"); - - Sleep(); - - watcher1.Tell(new UnwatchIt(subject1)); - ExpectMsg(TimeSpan.FromSeconds(1)); - EnterBarrier("unwatch-s1-4"); - Sys.Stop(s1); - ExpectNoMsg(TimeSpan.FromSeconds(2)); - EnterBarrier("stop-s1-4"); - - Sys.Stop(s2); - EnterBarrier("stop-s2-4"); - ExpectMsg().T.ActorRef.ShouldBe(subject2); - }, _config.First, _config.Second); - - RunOn(() => - { - EnterBarrier("actors-started-4"); - EnterBarrier("watch-4"); - EnterBarrier("unwatch-s1-4"); - EnterBarrier("stop-s1-4"); - EnterBarrier("stop-s2-4"); - }, _config.Third); + var watcher1 = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "w1"); + var watcher2 = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "w2"); + var s1 = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "s1"); + var s2 = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "s2"); + await EnterBarrierAsync("actors-started-4"); + + var other = Myself == _config.First ? _config.Second : _config.First; + var subject1 = _identify(other, "s1"); + var subject2 = _identify(other, "s2"); + watcher1.Tell(new WatchIt(subject1)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + watcher2.Tell(new WatchIt(subject2)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + await EnterBarrierAsync("watch-4"); + + await SleepAsync(); + + watcher1.Tell(new UnwatchIt(subject1)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + await EnterBarrierAsync("unwatch-s1-4"); + Sys.Stop(s1); + await ExpectNoMsgAsync(TimeSpan.FromSeconds(2)); + await EnterBarrierAsync("stop-s1-4"); + + Sys.Stop(s2); + await EnterBarrierAsync("stop-s2-4"); + (await ExpectMsgAsync()).T.ActorRef.ShouldBe(subject2); + }, _config.First, _config.Second); + + await RunOnAsync(async () => + { + await EnterBarrierAsync("actors-started-4"); + await EnterBarrierAsync("watch-4"); + await EnterBarrierAsync("unwatch-s1-4"); + await EnterBarrierAsync("stop-s1-4"); + await EnterBarrierAsync("stop-s2-4"); + }, _config.Third); + + // verify that things are cleaned up, and heartbeating is stopped + await AssertCleanup(); + await ExpectNoMsgAsync(TimeSpan.FromSeconds(2)); + await AssertCleanup(); + + await EnterBarrierAsync("after-4"); + } - // verify that things are cleaned up, and heartbeating is stopped - AssertCleanup(); - ExpectNoMsg(TimeSpan.FromSeconds(2)); - AssertCleanup(); + private async Task RemoteNodeDeathWatch_must_cleanup_after_stopAsync() + { + await RunOnAsync(async () => + { + var p1 = CreateTestProbe(); + var p2 = CreateTestProbe(); + var p3 = CreateTestProbe(); + var a1 = Sys.ActorOf(Props.Create(() => new ProbeActor(p1.Ref)), "a1"); + var a2 = Sys.ActorOf(Props.Create(() => new ProbeActor(p2.Ref)), "a2"); + var a3 = Sys.ActorOf(Props.Create(() => new ProbeActor(p3.Ref)), "a3"); + + await EnterBarrierAsync("actors-started-5"); + + var b1 = _identify(_config.Second, "b1"); + var b2 = _identify(_config.Second, "b2"); + var b3 = _identify(_config.Second, "b3"); + + a1.Tell(new WatchIt(b1)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + a1.Tell(new WatchIt(b2)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + a2.Tell(new WatchIt(b2)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + a3.Tell(new WatchIt(b3)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + await SleepAsync(); + a2.Tell(new UnwatchIt(b2)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + + await EnterBarrierAsync("watch-established-5"); + + await SleepAsync(); + + a1.Tell(PoisonPill.Instance); + a2.Tell(PoisonPill.Instance); + a3.Tell(PoisonPill.Instance); + + await EnterBarrierAsync("stopped-5"); + await EnterBarrierAsync("terminated-verified-5"); - EnterBarrier("after-4"); - } + // verify that things are cleaned up, and heartbeating is stopped + await AssertCleanup(); + await ExpectNoMsgAsync(TimeSpan.FromSeconds(2)); + await AssertCleanup(); + }, _config.First); - private void RemoteNodeDeathWatch_must_cleanup_after_stop() + await RunOnAsync(async () => { - RunOn(() => - { - var p1 = CreateTestProbe(); - var p2 = CreateTestProbe(); - var p3 = CreateTestProbe(); - var a1 = Sys.ActorOf(Props.Create(() => new ProbeActor(p1.Ref)), "a1"); - var a2 = Sys.ActorOf(Props.Create(() => new ProbeActor(p2.Ref)), "a2"); - var a3 = Sys.ActorOf(Props.Create(() => new ProbeActor(p3.Ref)), "a3"); - - EnterBarrier("actors-started-5"); - - var b1 = _identify(_config.Second, "b1"); - var b2 = _identify(_config.Second, "b2"); - var b3 = _identify(_config.Second, "b3"); - - a1.Tell(new WatchIt(b1)); - ExpectMsg(TimeSpan.FromSeconds(1)); - a1.Tell(new WatchIt(b2)); - ExpectMsg(TimeSpan.FromSeconds(1)); - a2.Tell(new WatchIt(b2)); - ExpectMsg(TimeSpan.FromSeconds(1)); - a3.Tell(new WatchIt(b3)); - ExpectMsg(TimeSpan.FromSeconds(1)); - Sleep(); - a2.Tell(new UnwatchIt(b2)); - ExpectMsg(TimeSpan.FromSeconds(1)); - - EnterBarrier("watch-established-5"); - - Sleep(); - - a1.Tell(PoisonPill.Instance); - a2.Tell(PoisonPill.Instance); - a3.Tell(PoisonPill.Instance); - - EnterBarrier("stopped-5"); - EnterBarrier("terminated-verified-5"); - - // verify that things are cleaned up, and heartbeating is stopped - AssertCleanup(); - ExpectNoMsg(TimeSpan.FromSeconds(2)); - AssertCleanup(); - }, _config.First); - - RunOn(() => - { - var p1 = CreateTestProbe(); - var p2 = CreateTestProbe(); - var p3 = CreateTestProbe(); - var b1 = Sys.ActorOf(Props.Create(() => new ProbeActor(p1.Ref)), "b1"); - var b2 = Sys.ActorOf(Props.Create(() => new ProbeActor(p2.Ref)), "b2"); - var b3 = Sys.ActorOf(Props.Create(() => new ProbeActor(p3.Ref)), "b3"); - - EnterBarrier("actors-started-5"); - - var a1 = _identify(_config.First, "a1"); - var a2 = _identify(_config.First, "a2"); - var a3 = _identify(_config.First, "a3"); - - b1.Tell(new WatchIt(a1)); - ExpectMsg(TimeSpan.FromSeconds(1)); - b1.Tell(new WatchIt(a2)); - ExpectMsg(TimeSpan.FromSeconds(1)); - b2.Tell(new WatchIt(a2)); - ExpectMsg(TimeSpan.FromSeconds(1)); - b3.Tell(new WatchIt(a3)); - ExpectMsg(TimeSpan.FromSeconds(1)); - Sleep(); - b2.Tell(new UnwatchIt(a2)); - ExpectMsg(TimeSpan.FromSeconds(1)); - - EnterBarrier("watch-established-5"); - EnterBarrier("stopped-5"); - - p1.ReceiveN(2, TimeSpan.FromSeconds(20)) - .Cast() - .Select(w => w.T.ActorRef) - .OrderBy(r => r.Path.Name) - .ShouldBe(new[] {a1, a2}); - p3.ExpectMsg(TimeSpan.FromSeconds(5)).T.ActorRef.ShouldBe(a3); - p2.ExpectNoMsg(TimeSpan.FromSeconds(2)); - EnterBarrier("terminated-verified-5"); + var p1 = CreateTestProbe(); + var p2 = CreateTestProbe(); + var p3 = CreateTestProbe(); + var b1 = Sys.ActorOf(Props.Create(() => new ProbeActor(p1.Ref)), "b1"); + var b2 = Sys.ActorOf(Props.Create(() => new ProbeActor(p2.Ref)), "b2"); + var b3 = Sys.ActorOf(Props.Create(() => new ProbeActor(p3.Ref)), "b3"); + + await EnterBarrierAsync("actors-started-5"); + + var a1 = _identify(_config.First, "a1"); + var a2 = _identify(_config.First, "a2"); + var a3 = _identify(_config.First, "a3"); + + b1.Tell(new WatchIt(a1)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + b1.Tell(new WatchIt(a2)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + b2.Tell(new WatchIt(a2)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + b3.Tell(new WatchIt(a3)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + await SleepAsync(); + b2.Tell(new UnwatchIt(a2)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + + await EnterBarrierAsync("watch-established-5"); + await EnterBarrierAsync("stopped-5"); + + p1.ReceiveN(2, TimeSpan.FromSeconds(20)) + .Cast() + .Select(w => w.T.ActorRef) + .OrderBy(r => r.Path.Name) + .ShouldBe([a1, a2]); + (await p3.ExpectMsgAsync(TimeSpan.FromSeconds(5))).T.ActorRef.ShouldBe(a3); + await p2.ExpectNoMsgAsync(TimeSpan.FromSeconds(2)); + await EnterBarrierAsync("terminated-verified-5"); - // verify that things are cleaned up, and heartbeating is stopped - AssertCleanup(); - ExpectNoMsg(TimeSpan.FromSeconds(2)); - p1.ExpectNoMsg(100); - p2.ExpectNoMsg(100); - p3.ExpectNoMsg(100); - AssertCleanup(); - }, _config.Second); - - RunOn(() => - { - EnterBarrier("actors-started-5"); - EnterBarrier("watch-established-5"); - EnterBarrier("stopped-5"); - EnterBarrier("terminated-verified-5"); - }, _config.Third); + // verify that things are cleaned up, and heartbeating is stopped + await AssertCleanup(); + await ExpectNoMsgAsync(TimeSpan.FromSeconds(2)); + await p1.ExpectNoMsgAsync(100); + await p2.ExpectNoMsgAsync(100); + await p3.ExpectNoMsgAsync(100); + await AssertCleanup(); + }, _config.Second); + + await RunOnAsync(async () => + { + await EnterBarrierAsync("actors-started-5"); + await EnterBarrierAsync("watch-established-5"); + await EnterBarrierAsync("stopped-5"); + await EnterBarrierAsync("terminated-verified-5"); + }, _config.Third); - EnterBarrier("after-5"); - } + await EnterBarrierAsync("after-5"); + } - private void RemoteNodeDeathWatch_must_receive_Terminated_when_watched_node_crash() + private async Task RemoteNodeDeathWatch_must_receive_Terminated_when_watched_node_crashAsync() + { + await RunOnAsync(async () => { - RunOn(() => - { - var watcher = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "watcher6"); - var watcher2 = Sys.ActorOf(Props.Create(() => new ProbeActor(Sys.DeadLetters))); - EnterBarrier("actors-started-6"); + var watcher = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "watcher6"); + var watcher2 = Sys.ActorOf(Props.Create(() => new ProbeActor(Sys.DeadLetters))); + await EnterBarrierAsync("actors-started-6"); - var subject = _identify(_config.Second, "subject6"); - watcher.Tell(new WatchIt(subject)); - ExpectMsg(TimeSpan.FromSeconds(1)); - watcher2.Tell(new WatchIt(subject)); - ExpectMsg(TimeSpan.FromSeconds(1)); - subject.Tell("hello6"); + var subject = _identify(_config.Second, "subject6"); + watcher.Tell(new WatchIt(subject)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + watcher2.Tell(new WatchIt(subject)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + subject.Tell("hello6"); - // testing with this watch/unwatch of watcher2 to make sure that the unwatch doesn't - // remove the first watch - watcher2.Tell(new UnwatchIt(subject)); - ExpectMsg(TimeSpan.FromSeconds(1)); + // testing with this watch/unwatch of watcher2 to make sure that the unwatch doesn't + // remove the first watch + watcher2.Tell(new UnwatchIt(subject)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); - EnterBarrier("watch-established-6"); + await EnterBarrierAsync("watch-established-6"); - Sleep(); + await SleepAsync(); - Log.Info("exit second"); - TestConductor.Exit(_config.Second, 0).Wait(); - ExpectMsg(TimeSpan.FromSeconds(15)).T.ActorRef.ShouldBe(subject); + Log.Info("exit second"); + await TestConductor.ExitAsync(_config.Second, 0); + (await ExpectMsgAsync(TimeSpan.FromSeconds(15))).T.ActorRef.ShouldBe(subject); - // verify that things are cleaned up, and heartbeating is stopped - AssertCleanup(); - ExpectNoMsg(TimeSpan.FromSeconds(2)); - AssertCleanup(); - }, _config.First); + // verify that things are cleaned up, and heartbeating is stopped + await AssertCleanup(); + await ExpectNoMsgAsync(TimeSpan.FromSeconds(2)); + await AssertCleanup(); + }, _config.First); - RunOn(() => - { - Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "subject6"); - EnterBarrier("actors-started-6"); + await RunOnAsync(async () => + { + Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "subject6"); + await EnterBarrierAsync("actors-started-6"); - ExpectMsg("hello6", TimeSpan.FromSeconds(3)); - EnterBarrier("watch-established-6"); - }, _config.Second); + await ExpectMsgAsync("hello6", TimeSpan.FromSeconds(3)); + await EnterBarrierAsync("watch-established-6"); + }, _config.Second); - RunOn(() => - { - EnterBarrier("actors-started-6"); - EnterBarrier("watch-established-6"); - }, _config.Third); + await RunOnAsync(async () => + { + await EnterBarrierAsync("actors-started-6"); + await EnterBarrierAsync("watch-established-6"); + }, _config.Third); - EnterBarrier("after-6"); - } + await EnterBarrierAsync("after-6"); + } - private void RemoteNodeDeathWatch_must_cleanup_when_watching_node_crash() + private async Task RemoteNodeDeathWatch_must_cleanup_when_watching_node_crashAsync() + { + await RunOnAsync(async () => { - RunOn(() => - { - var watcher = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "watcher7"); - EnterBarrier("actors-started-7"); + var watcher = Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "watcher7"); + await EnterBarrierAsync("actors-started-7"); - var subject = _identify(_config.First, "subject7"); - watcher.Tell(new WatchIt(subject)); - ExpectMsg(TimeSpan.FromSeconds(1)); - subject.Tell("hello7"); - EnterBarrier("watch-established-7"); - }, _config.Third); + var subject = _identify(_config.First, "subject7"); + watcher.Tell(new WatchIt(subject)); + await ExpectMsgAsync(TimeSpan.FromSeconds(1)); + subject.Tell("hello7"); + await EnterBarrierAsync("watch-established-7"); + }, _config.Third); - RunOn(() => - { - Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "subject7"); - EnterBarrier("actors-started-7"); + await RunOnAsync(async () => + { + Sys.ActorOf(Props.Create(() => new ProbeActor(TestActor)), "subject7"); + await EnterBarrierAsync("actors-started-7"); - ExpectMsg("hello7", TimeSpan.FromSeconds(3)); - EnterBarrier("watch-established-7"); + await ExpectMsgAsync("hello7", TimeSpan.FromSeconds(3)); + await EnterBarrierAsync("watch-established-7"); - Sleep(); + await SleepAsync(); - Log.Info("exit third"); - TestConductor.Exit(_config.Third, 0).Wait(); + Log.Info("exit third"); + await TestConductor.ExitAsync(_config.Third, 0); - // verify that things are cleaned up, and heartbeating is stopped - AssertCleanup(TimeSpan.FromSeconds(20)); - ExpectNoMsg(TimeSpan.FromSeconds(2)); - AssertCleanup(); - }, _config.First); + // verify that things are cleaned up, and heartbeating is stopped + await AssertCleanup(TimeSpan.FromSeconds(20)); + await ExpectNoMsgAsync(TimeSpan.FromSeconds(2)); + await AssertCleanup(); + }, _config.First); - EnterBarrier("after-7"); - } + await EnterBarrierAsync("after-7"); } +} - #region Several different variations of the test - - public class RemoteNodeDeathWatchFastSpec : RemoteNodeDeathWatchSpec - { - public RemoteNodeDeathWatchFastSpec() : base(typeof(RemoteNodeDeathWatchFastSpec)) - { } - - protected override string Scenario { get; } = "fast"; +#region Several different variations of the test - protected override Action Sleep { get; } = () => Thread.Sleep(100); - } +public class RemoteNodeDeathWatchFastSpec : RemoteNodeDeathWatchSpec +{ + public RemoteNodeDeathWatchFastSpec() : base(typeof(RemoteNodeDeathWatchFastSpec)) + { } - public class RemoteNodeDeathWatchSlowSpec : RemoteNodeDeathWatchSpec - { - public RemoteNodeDeathWatchSlowSpec() : base(typeof(RemoteNodeDeathWatchSlowSpec)) - { } + protected override string Scenario { get; } = "fast"; - protected override string Scenario { get; } = "slow"; + protected override Func SleepAsync { get; } = async () => await Task.Delay(100); +} - protected override Action Sleep { get; } = () => Thread.Sleep(3000); - } +public class RemoteNodeDeathWatchSlowSpec : RemoteNodeDeathWatchSpec +{ + public RemoteNodeDeathWatchSlowSpec() : base(typeof(RemoteNodeDeathWatchSlowSpec)) + { } - #endregion + protected override string Scenario { get; } = "slow"; + protected override Func SleepAsync { get; } = async () => await Task.Delay(3000); } + +#endregion \ No newline at end of file diff --git a/src/core/Akka.Remote.Tests.MultiNode/TestConductor/TestConductorSpec.cs b/src/core/Akka.Remote.Tests.MultiNode/TestConductor/TestConductorSpec.cs index f2c9163f7c9..5dacaf87e88 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/TestConductor/TestConductorSpec.cs +++ b/src/core/Akka.Remote.Tests.MultiNode/TestConductor/TestConductorSpec.cs @@ -39,7 +39,7 @@ public TestConductorSpecConfig() public class TestConductorSpec : MultiNodeSpec { - private TestConductorSpecConfig _config; + private readonly TestConductorSpecConfig _config; public TestConductorSpec() : this(new TestConductorSpecConfig()) { } @@ -48,34 +48,28 @@ protected TestConductorSpec(TestConductorSpecConfig config) : base(config, typeo _config = config; } - protected override int InitialParticipantsValueFactory - { - get - { - return 2; - } - } + protected override int InitialParticipantsValueFactory => 2; private IActorRef _echo; - protected IActorRef GetEchoActorRef() + protected async Task GetEchoActorRef() { if (_echo == null) { Sys.ActorSelection(Node(_config.Master).Root / "user" / "echo").Tell(new Identify(null)); - _echo = ExpectMsg().Subject; + _echo = (await ExpectMsgAsync()).Subject; } return _echo; } [MultiNodeFact] - public void ATestConductorMust() + public async Task ATestConductorMust() { - Enter_a_Barrier(); - Support_Throttling_of_Network_Connections(); + await Enter_a_BarrierAsync(); + await Support_Throttling_of_Network_ConnectionsAsync(); } - public void Enter_a_Barrier() + public async Task Enter_a_BarrierAsync() { RunOn(() => { @@ -86,55 +80,55 @@ public void Enter_a_Barrier() }), "echo"); }, _config.Master); - EnterBarrier("name"); + await EnterBarrierAsync("name"); } - public void Support_Throttling_of_Network_Connections() + public async Task Support_Throttling_of_Network_ConnectionsAsync() { - RunOn(() => + await RunOnAsync(async () => { // start remote network connection so that it can be throttled - GetEchoActorRef().Tell("start"); + (await GetEchoActorRef()).Tell("start"); }, _config.Slave); - ExpectMsg("start"); + await ExpectMsgAsync("start"); - RunOn(() => + await RunOnAsync(async () => { - TestConductor.Throttle(_config.Slave, _config.Master, ThrottleTransportAdapter.Direction.Send, 0.01f).Wait(); + await TestConductor.ThrottleAsync(_config.Slave, _config.Master, ThrottleTransportAdapter.Direction.Send, 0.01f); }, _config.Master); - EnterBarrier("throttled_send"); + await EnterBarrierAsync("throttled_send"); - RunOn(() => + await RunOnAsync(async () => { foreach(var i in Enumerable.Range(0, 10)) { - GetEchoActorRef().Tell(i); + (await GetEchoActorRef()).Tell(i); } }, _config.Slave); // fudged the value to 0.5,since messages are a different size in Akka.NET - Within(TimeSpan.FromSeconds(0.5), TimeSpan.FromSeconds(2), () => + await WithinAsync(TimeSpan.FromSeconds(0.5), TimeSpan.FromSeconds(2), async () => { - ExpectMsg(0, TimeSpan.FromMilliseconds(500)); - ReceiveN(9).ShouldOnlyContainInOrder(Enumerable.Range(1,9).Cast().ToArray()); + await ExpectMsgAsync(0, TimeSpan.FromMilliseconds(500)); + (await ReceiveNAsync(9).ToListAsync()).ShouldOnlyContainInOrder(Enumerable.Range(1,9).Cast().ToArray()); }); - EnterBarrier("throttled_send2"); - RunOn(() => + await EnterBarrierAsync("throttled_send2"); + await RunOnAsync(async () => { - TestConductor.Throttle(_config.Slave, _config.Master, ThrottleTransportAdapter.Direction.Send, -1).Wait(); - TestConductor.Throttle(_config.Slave, _config.Master, ThrottleTransportAdapter.Direction.Receive, 0.01F).Wait(); + await TestConductor.ThrottleAsync(_config.Slave, _config.Master, ThrottleTransportAdapter.Direction.Send, -1); + await TestConductor.ThrottleAsync(_config.Slave, _config.Master, ThrottleTransportAdapter.Direction.Receive, 0.01F); }, _config.Master); - EnterBarrier("throttled_recv"); + await EnterBarrierAsync("throttled_recv"); - RunOn(() => + await RunOnAsync(async () => { foreach (var i in Enumerable.Range(10, 10)) { - GetEchoActorRef().Tell(i); + (await GetEchoActorRef()).Tell(i); } }, _config.Slave); @@ -142,20 +136,20 @@ public void Support_Throttling_of_Network_Connections() ? (TimeSpan.Zero, TimeSpan.FromMilliseconds(500)) : (TimeSpan.FromSeconds(0.3), TimeSpan.FromSeconds(3)); - Within(minMax.Item1, minMax.Item2, () => + await WithinAsync(minMax.Item1, minMax.Item2, async () => { - ExpectMsg(10, TimeSpan.FromMilliseconds(500)); - ReceiveN(9).ShouldOnlyContainInOrder(Enumerable.Range(11, 9).Cast().ToArray()); + await ExpectMsgAsync(10, TimeSpan.FromMilliseconds(500)); + (await ReceiveNAsync(9).ToListAsync()).ShouldOnlyContainInOrder(Enumerable.Range(11, 9).Cast().ToArray()); }); - EnterBarrier("throttled_recv2"); + await EnterBarrierAsync("throttled_recv2"); - RunOn(() => + await RunOnAsync(async () => { - TestConductor.Throttle(_config.Slave, _config.Master, ThrottleTransportAdapter.Direction.Receive, -1).Wait(); + await TestConductor.ThrottleAsync(_config.Slave, _config.Master, ThrottleTransportAdapter.Direction.Receive, -1); }, _config.Master); - EnterBarrier("after"); + await EnterBarrierAsync("after"); } } } diff --git a/src/core/Akka.Remote.Tests.Performance/Transports/RemoteMessagingThroughputSpecBase.cs b/src/core/Akka.Remote.Tests.Performance/Transports/RemoteMessagingThroughputSpecBase.cs index 3d7fa84d90a..c91fc804dce 100644 --- a/src/core/Akka.Remote.Tests.Performance/Transports/RemoteMessagingThroughputSpecBase.cs +++ b/src/core/Akka.Remote.Tests.Performance/Transports/RemoteMessagingThroughputSpecBase.cs @@ -28,9 +28,6 @@ public abstract class RemoteMessagingThroughputSpecBase private Counter _remoteMessageThroughput; private List _tasks = new(); private readonly List _receivers = new(); - private IActorRef _echo; - private IActorRef _remoteEcho; - private IActorRef _remoteReceiver; private static readonly AtomicCounter ActorSystemNameCounter = new(0); protected ActorSystem System1; diff --git a/src/core/Akka.Remote.Tests/RemotingSpec.cs b/src/core/Akka.Remote.Tests/RemotingSpec.cs index 89c1b06b5cf..994fc836f5f 100644 --- a/src/core/Akka.Remote.Tests/RemotingSpec.cs +++ b/src/core/Akka.Remote.Tests/RemotingSpec.cs @@ -688,7 +688,7 @@ public async Task Nobody_should_be_converted_back_to_its_singleton() } [Fact] - public async Task Should_reply_back_on_original_Transport() + public void Should_reply_back_on_original_Transport() { } diff --git a/src/core/Akka.Remote.Tests/RemotingTerminatorSpecs.cs b/src/core/Akka.Remote.Tests/RemotingTerminatorSpecs.cs index 58ed7a31183..35325d0caac 100644 --- a/src/core/Akka.Remote.Tests/RemotingTerminatorSpecs.cs +++ b/src/core/Akka.Remote.Tests/RemotingTerminatorSpecs.cs @@ -81,7 +81,7 @@ public async Task RemotingTerminator_should_shutdown_properly_with_remotely_depl _sys2 = ActorSystem.Create("System2", RemoteConfig); InitializeLogger(_sys2); var sys2Address = RARP.For(_sys2).Provider.DefaultAddress; - + // open an association via remote deployment var associated = Sys.ActorOf(BlackHoleActor.Props.WithDeploy(Deploy.None.WithScope(new RemoteScope(sys2Address))), @@ -89,9 +89,9 @@ public async Task RemotingTerminator_should_shutdown_properly_with_remotely_depl Watch(associated); // verify that the association is open (don't terminate until handshake is finished) - associated.Ask(new Identify("foo"), RemainingOrDefault).Result.MessageId.ShouldBe("foo"); - - + var actorIdentity = await associated.Ask(new Identify("foo"), RemainingOrDefault); + actorIdentity.MessageId.ShouldBe("foo"); + // terminate the DEPLOYED system Assert.True(await _sys2.Terminate().AwaitWithTimeout(10.Seconds()), "Expected to terminate within 10 seconds, but didn't."); await ExpectTerminatedAsync(associated); // expect that the remote deployed actor is dead diff --git a/src/core/Akka.Remote.Tests/Serialization/Bugfix3903Spec.cs b/src/core/Akka.Remote.Tests/Serialization/Bugfix3903Spec.cs index 62e16ec0d32..b93c98f0e24 100644 --- a/src/core/Akka.Remote.Tests/Serialization/Bugfix3903Spec.cs +++ b/src/core/Akka.Remote.Tests/Serialization/Bugfix3903Spec.cs @@ -99,14 +99,14 @@ public async Task ParentActor_should_be_able_to_deploy_EchoActor_to_remote_syste // have the ParentActor remotely deploy an EchoActor onto the second ActorSystem var child = await parent .Ask(new ParentActor.DeployChild( - system2.AsInstanceOf().Provider.DefaultAddress), RemainingOrDefault).ConfigureAwait(false); + system2.AsInstanceOf().Provider.DefaultAddress), RemainingOrDefault); // assert that Child is a remote actor reference child.Should().BeOfType(); Watch(child); // send a message to the EchoActor and verify that it is received - (await child.Ask("hello", RemainingOrDefault).ConfigureAwait(false)).Should().Be("hello"); + (await child.Ask("hello", RemainingOrDefault)).Should().Be("hello"); // cause the child to crash child.Tell(EchoActor.Fail.Instance); diff --git a/src/core/Akka.Remote.Tests/Serialization/RemoteAskFailureSpec.cs b/src/core/Akka.Remote.Tests/Serialization/RemoteAskFailureSpec.cs index 8ddcd13bc1b..0bd225409e1 100644 --- a/src/core/Akka.Remote.Tests/Serialization/RemoteAskFailureSpec.cs +++ b/src/core/Akka.Remote.Tests/Serialization/RemoteAskFailureSpec.cs @@ -128,9 +128,11 @@ public TestException() { } +#pragma warning disable SYSLIB0051 // Exception serialization needs to be enabled for this test protected TestException(SerializationInfo info, StreamingContext context) : base(info, context) { } +#pragma warning restore SYSLIB0051 public TestException(string message) : base(message) { diff --git a/src/core/Akka.Remote.Tests/Transport/AkkaProtocolSpec.cs b/src/core/Akka.Remote.Tests/Transport/AkkaProtocolSpec.cs index 6c0f8dc68c8..0978be0d1f3 100644 --- a/src/core/Akka.Remote.Tests/Transport/AkkaProtocolSpec.cs +++ b/src/core/Akka.Remote.Tests/Transport/AkkaProtocolSpec.cs @@ -113,14 +113,14 @@ private class TestFailureDetector : FailureDetector { private volatile bool _isAvailable = true; public override bool IsAvailable => _isAvailable; - public void SetAvailable(bool available) => Volatile.Write(ref _isAvailable, available); + public void SetAvailable(bool available) => _isAvailable = available; private volatile bool _called; public override bool IsMonitoring => _called; public override void HeartBeat() { - Volatile.Write(ref _called, true); + _called = true; } } @@ -273,7 +273,7 @@ public async Task ProtocolStateActor_must_handle_explicit_disassociate_messages( reader.Tell(TestAssociate(33), TestActor); await statusPromise.Task.WithTimeout(3.Seconds()); - var result = statusPromise.Task.Result; + var result = await statusPromise.Task; switch (result) { case AkkaProtocolHandle h: @@ -321,7 +321,7 @@ public async Task ProtocolStateActor_must_handle_transport_level_disassociations reader.Tell(TestAssociate(33), TestActor); await statusPromise.Task.WithTimeout(TimeSpan.FromSeconds(3)); - var result = statusPromise.Task.Result; + var result = await statusPromise.Task; switch (result) { case AkkaProtocolHandle h: @@ -369,7 +369,7 @@ public async Task ProtocolStateActor_must_disassociate_when_failure_detector_sig stateActor.Tell(TestAssociate(33), TestActor); await statusPromise.Task.WithTimeout(TimeSpan.FromSeconds(3)); - var result = statusPromise.Task.Result; + var result = await statusPromise.Task; switch (result) { case AkkaProtocolHandle h: @@ -420,7 +420,7 @@ public async Task ProtocolStateActor_must_handle_correctly_when_the_handler_is_r stateActor.Tell(TestAssociate(33), TestActor); await statusPromise.Task.WithTimeout(TimeSpan.FromSeconds(3)); - var result = statusPromise.Task.Result; + var result = await statusPromise.Task; switch (result) { case AkkaProtocolHandle h: diff --git a/src/core/Akka.Remote.Tests/Transport/AkkaProtocolStressTest.cs b/src/core/Akka.Remote.Tests/Transport/AkkaProtocolStressTest.cs index 3b8fd35f55b..f2b6e10441b 100644 --- a/src/core/Akka.Remote.Tests/Transport/AkkaProtocolStressTest.cs +++ b/src/core/Akka.Remote.Tests/Transport/AkkaProtocolStressTest.cs @@ -64,8 +64,11 @@ private ResendFinal() { } public static ResendFinal Instance { get; } = new(); } - private class SequenceVerifier : UntypedActor + private class SequenceVerifier : UntypedActor, IWithTimers { + private const string SendNextTimerKey = nameof(SendNextTimerKey); + private const string SendFinalTimerKey = nameof(SendFinalTimerKey); + private const int Limit = 100000; private int _nextSeq = 0; private int _maxSeq = -1; @@ -80,6 +83,8 @@ public SequenceVerifier(IActorRef remote, IActorRef controller) _controller = controller; } + public ITimerScheduler Timers { get; set; } = null!; + protected override void OnReceive(object message) { if (message.Equals("start")) @@ -91,7 +96,7 @@ protected override void OnReceive(object message) _remote.Tell(_nextSeq); _nextSeq++; if (_nextSeq%2000 == 0) - Context.System.Scheduler.ScheduleTellOnce(TimeSpan.FromMilliseconds(500), Self, "sendNext", Self); + Timers.StartSingleTimer(SendNextTimerKey, "sendNext", TimeSpan.FromMilliseconds(500), Self); else Self.Tell("sendNext"); } @@ -111,8 +116,7 @@ protected override void OnReceive(object message) if (seq > Limit*0.5) { _controller.Tell((_maxSeq, _losses)); - Context.System.Scheduler.ScheduleTellRepeatedly(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1), Self, - ResendFinal.Instance, Self); + Timers.StartPeriodicTimer(SendFinalTimerKey, ResendFinal.Instance, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1), Self); Context.Become(Done); } } diff --git a/src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs b/src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs index 84cc2adefae..5754f57db59 100644 --- a/src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs +++ b/src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs @@ -98,9 +98,7 @@ private static Config TestThumbprintConfig(string thumbPrint) } } }"); - return false - ? config - : config.WithFallback(@"akka.remote.dot-netty.tcp.ssl { + return config.WithFallback(@"akka.remote.dot-netty.tcp.ssl { suppress-validation = ""true"" certificate { use-thumprint-over-file = true diff --git a/src/core/Akka.Remote.Tests/Transport/TestTransportSpec.cs b/src/core/Akka.Remote.Tests/Transport/TestTransportSpec.cs index eccd0d5cd7c..ffa8c309ad7 100644 --- a/src/core/Akka.Remote.Tests/Transport/TestTransportSpec.cs +++ b/src/core/Akka.Remote.Tests/Transport/TestTransportSpec.cs @@ -130,7 +130,7 @@ public async Task TestTransport_should_emulate_sending_PDUs() handleB.ReadHandlerSource.SetResult(new ActorHandleEventListener(TestActor)); await associate.WithTimeout(DefaultTimeout); - var handleA = associate.Result; + var handleA = await associate; //Initialize handles handleA.ReadHandlerSource.SetResult(new ActorHandleEventListener(TestActor)); @@ -185,7 +185,7 @@ public async Task TestTransport_should_emulate_disassociation() handleB.ReadHandlerSource.SetResult(new ActorHandleEventListener(TestActor)); await associate.WithTimeout(DefaultTimeout); - var handleA = associate.Result; + var handleA = await associate; //Initialize handles handleA.ReadHandlerSource.SetResult(new ActorHandleEventListener(TestActor)); diff --git a/src/core/Akka.Streams.Tests.TCK/TransformProcessorTest.cs b/src/core/Akka.Streams.Tests.TCK/TransformProcessorTest.cs index 78f9065683d..6707e318f49 100644 --- a/src/core/Akka.Streams.Tests.TCK/TransformProcessorTest.cs +++ b/src/core/Akka.Streams.Tests.TCK/TransformProcessorTest.cs @@ -11,21 +11,45 @@ namespace Akka.Streams.Tests.TCK { - class TransformProcessorTest : AkkaIdentityProcessorVerification + internal class TransformProcessorTest : AkkaIdentityProcessorVerification { public override int? CreateElement(int element) => element; public override IProcessor CreateIdentityProcessor(int bufferSize) { - var settings = ActorMaterializerSettings.Create(System).WithInputBuffer(bufferSize/2, bufferSize); - var materializer = ActorMaterializer.Create(System, settings); - - return Flow.Create().Transform(() => new Stage()).ToProcessor().Run(materializer); + return Flow.Create() + .Via(new Stage()) + .ToProcessor() + .WithAttributes(Attributes.CreateInputBuffer(bufferSize / 2, bufferSize)) + .Run(System.Materializer()); } - private sealed class Stage : PushStage + private sealed class Stage : GraphStage> { - public override ISyncDirective OnPush(int? element, IContext context) => context.Push(element); + private class Logic: InAndOutGraphStageLogic + { + private readonly Stage _parent; + public Logic(Stage parent) : base(parent.Shape) + { + _parent = parent; + SetHandlers(_parent.In, _parent.Out, this); + } + + public override void OnPush() => Push(_parent.Out, Grab(_parent.In)); + public override void OnPull() => Pull(_parent.In); + } + + public Stage() + { + Shape = new FlowShape(In, Out); + } + + public readonly Inlet In = new("Stage.in"); + public readonly Outlet Out = new("Stage.out"); + public override FlowShape Shape { get; } + + protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) + => new Logic(this); } } } diff --git a/src/core/Akka.Streams.Tests/Dsl/AsyncEnumerableSpec.cs b/src/core/Akka.Streams.Tests/Dsl/AsyncEnumerableSpec.cs index 9c86946c38a..07d789c9a76 100644 --- a/src/core/Akka.Streams.Tests/Dsl/AsyncEnumerableSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/AsyncEnumerableSpec.cs @@ -225,7 +225,11 @@ await this.AssertAllStagesStoppedAsync(async () => subscription.Cancel(); // The cancellation token inside the IAsyncEnumerable should be cancelled - await WithinAsync(3.Seconds(), async () => latch.Value); + await WithinAsync(3.Seconds(), async () => + { + await Task.Yield(); + return latch.Value; + }); }, Materializer); } @@ -431,6 +435,7 @@ private static async IAsyncEnumerable ThrowingRangeAsync(int start, int cou { foreach (var i in Enumerable.Range(start, count)) { + await Task.Yield(); if(token.IsCancellationRequested) yield break; @@ -447,6 +452,7 @@ private static async IAsyncEnumerable ProbeableRangeAsync(int start, int co token.Register(() => { latch.GetAndSet(true); }); foreach (var i in Enumerable.Range(start, count)) { + await Task.Yield(); if(token.IsCancellationRequested) yield break; diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowCollectSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowCollectSpec.cs index 84bf6304746..edb55d0e09a 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowCollectSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowCollectSpec.cs @@ -76,9 +76,9 @@ await this.AssertAllStagesStoppedAsync(async () => int ThrowOnTwo(int x) => x == 2 ? throw new TestException("") : x; var probe = +#pragma warning disable CS0618 Source.From(Enumerable.Range(1, 3)) // This is intentional, testing backward compatibility with old obsolete method -#pragma warning disable CS0618 .Collect(ThrowOnTwo) #pragma warning restore CS0618 .WithAttributes(ActorAttributes.CreateSupervisionStrategy(Deciders.RestartingDecider)) diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowForeachSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowForeachSpec.cs index c3b62476ff7..27ac2ab2599 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowForeachSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowForeachSpec.cs @@ -66,7 +66,7 @@ public async Task A_Foreach_must_yield_the_first_error() { await this.AssertAllStagesStoppedAsync(async() => { var p = this.CreateManualPublisherProbe(); - Source.FromPublisher(p).RunForeach(i => TestActor.Tell(i), Materializer).ContinueWith(task => + _ = Source.FromPublisher(p).RunForeach(i => TestActor.Tell(i), Materializer).ContinueWith(task => { if (task.Exception != null) TestActor.Tell(task.Exception.InnerException); diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowOnCompleteSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowOnCompleteSpec.cs index 50c8a7e43e2..516f8faa3ea 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowOnCompleteSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowOnCompleteSpec.cs @@ -90,7 +90,7 @@ await this.AssertAllStagesStoppedAsync(async() => { onCompleteProbe.Ref.Tell("map-" + x); return x; }).RunWith(foreachSink, Materializer); - future.ContinueWith(t => onCompleteProbe.Tell(t.IsCompleted ? "done" : "failure")); + _ = future.ContinueWith(t => onCompleteProbe.Tell(t.IsCompleted ? "done" : "failure")); var proc = await p.ExpectSubscriptionAsync(); await proc.ExpectRequestAsync(); diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowSelectErrorSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowSelectErrorSpec.cs index 8f68322bed9..a12fdea558c 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowSelectErrorSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowSelectErrorSpec.cs @@ -33,15 +33,14 @@ public FlowSelectErrorSpec() [Fact] public async Task A_SelectError_must_select_when_there_is_a_handler() { - await this.AssertAllStagesStoppedAsync(async() => { - Source.From(Enumerable.Range(1, 3)) - .Select(ThrowOnTwo) - .SelectError(_ => Boom) - .RunWith(this.SinkProbe(), Materializer) - .Request(2) - .ExpectNext(1) - .ExpectError().Should().Be(Boom); - return Task.CompletedTask; + await this.AssertAllStagesStoppedAsync(async () => { + (await Source.From(Enumerable.Range(1, 3)) + .Select(ThrowOnTwo) + .SelectError(_ => Boom) + .RunWith(this.SinkProbe(), Materializer) + .Request(2) + .ExpectNext(1) + .ExpectErrorAsync()).Should().Be(Boom); }, Materializer); } diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowSplitWhenSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowSplitWhenSpec.cs index 2bd9daedcba..3c17b3c2814 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowSplitWhenSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowSplitWhenSpec.cs @@ -374,7 +374,7 @@ await this.AssertAllStagesStoppedAsync(async () => { StreamSubscriptionTimeoutTerminationMode.CancelTermination, TimeSpan.FromMilliseconds(500)))); var testSource = Source.Single(1) - .MapMaterializedValue>(_ => null) + .MapMaterializedValue>(_ => null!) .Concat(Source.Maybe()) .SplitWhen(_ => true); @@ -382,7 +382,7 @@ await Awaiting(async () => { await testSource.Lift() .Delay(TimeSpan.FromSeconds(1)) - .ConcatMany(s => s.MapMaterializedValue>(_ => null)) + .ConcatMany(s => s.MapMaterializedValue>(_ => null!)) .RunWith(Sink.Ignore(), tightTimeoutMaterializer); }).Should().ThrowAsync(); }, Materializer) diff --git a/src/core/Akka.Streams.Tests/Dsl/GraphUnzipWithSpec.cs b/src/core/Akka.Streams.Tests/Dsl/GraphUnzipWithSpec.cs index f3e82152c33..1ab097ecddd 100644 --- a/src/core/Akka.Streams.Tests/Dsl/GraphUnzipWithSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/GraphUnzipWithSpec.cs @@ -15,6 +15,7 @@ using Akka.Streams.TestKit; using Akka.TestKit; using FluentAssertions; +using FluentAssertions.Extensions; using Reactive.Streams; using Xunit; using Xunit.Abstractions; @@ -149,27 +150,29 @@ await this.AssertAllStagesStoppedAsync(async () => { var leftSubscription = await leftProbe.ExpectSubscriptionAsync(); var rightSubscription = await rightProbe.ExpectSubscriptionAsync(); - Action requestFromBoth = () => - { - leftSubscription.Request(1); - rightSubscription.Request(1); - }; - - requestFromBoth(); + await RequestFromBoth(); await leftProbe.ExpectNextAsync(1 / -2); await rightProbe.ExpectNextAsync("1/-2"); - requestFromBoth(); + await RequestFromBoth(); await leftProbe.ExpectNextAsync(1 / -1); await rightProbe.ExpectNextAsync("1/-1"); - await EventFilter.Exception().ExpectOneAsync(requestFromBoth); + await EventFilter.Exception().ExpectOneAsync(3.Seconds(), RequestFromBoth); - leftProbe.ExpectError().Should().BeOfType(); - rightProbe.ExpectError().Should().BeOfType(); + (await leftProbe.ExpectErrorAsync()).Should().BeOfType(); + (await rightProbe.ExpectErrorAsync()).Should().BeOfType(); await leftProbe.ExpectNoMsgAsync(TimeSpan.FromMilliseconds(100)); await rightProbe.ExpectNoMsgAsync(TimeSpan.FromMilliseconds(100)); + return; + + Task RequestFromBoth() + { + leftSubscription.Request(1); + rightSubscription.Request(1); + return Task.CompletedTask; + } }, Materializer); } diff --git a/src/core/Akka.Streams.Tests/Implementation/Fusing/GraphInterpreterSpecKit.cs b/src/core/Akka.Streams.Tests/Implementation/Fusing/GraphInterpreterSpecKit.cs index ba3b44881cc..28b740a94fb 100644 --- a/src/core/Akka.Streams.Tests/Implementation/Fusing/GraphInterpreterSpecKit.cs +++ b/src/core/Akka.Streams.Tests/Implementation/Fusing/GraphInterpreterSpecKit.cs @@ -980,21 +980,13 @@ public OneBoundedSetup(ActorSystem system, params IGraphStageWithMaterializedVal } } -#pragma warning disable CS0618 // Type or member is obsolete + [Obsolete("Obsolete, but we need to keep this because of backward compatibility unit tests")] public PushPullGraphStage ToGraphStage(IStage stage) -#pragma warning restore CS0618 // Type or member is obsolete { var s = stage; return new PushPullGraphStage(_ => s, Attributes.None); } -#pragma warning disable CS0618 // Type or member is obsolete - public IGraphStageWithMaterializedValue[] ToGraphStage(IStage[] stages) -#pragma warning restore CS0618 // Type or member is obsolete - { - return stages.Select(ToGraphStage).Cast>().ToArray(); - } - public void WithTestSetup(Action>> spec) { var setup = new TestSetup(Sys); @@ -1019,26 +1011,6 @@ public void WithTestSetup( spec(setup, setup.Builder, setup.LastEvents); } -#pragma warning disable CS0618 // Type or member is obsolete - public void WithOneBoundedSetup(IStage op, -#pragma warning restore CS0618 // Type or member is obsolete - Action - >, OneBoundedSetup.UpstreamOneBoundedProbe, - OneBoundedSetup.DownstreamOneBoundedPortProbe> spec) - { - WithOneBoundedSetup(ToGraphStage(op), spec); - } - -#pragma warning disable CS0618 // Type or member is obsolete - public void WithOneBoundedSetup(IStage[] ops, -#pragma warning restore CS0618 // Type or member is obsolete - Action - >, OneBoundedSetup.UpstreamOneBoundedProbe, - OneBoundedSetup.DownstreamOneBoundedPortProbe> spec) - { - WithOneBoundedSetup(ToGraphStage(ops), spec); - } - public void WithOneBoundedSetup(IGraphStageWithMaterializedValue op, Action >, OneBoundedSetup.UpstreamOneBoundedProbe, diff --git a/src/core/Akka.Streams.Tests/Implementation/Fusing/InterpreterSpec.cs b/src/core/Akka.Streams.Tests/Implementation/Fusing/InterpreterSpec.cs index 2a867547bdb..1989b68e6de 100644 --- a/src/core/Akka.Streams.Tests/Implementation/Fusing/InterpreterSpec.cs +++ b/src/core/Akka.Streams.Tests/Implementation/Fusing/InterpreterSpec.cs @@ -96,7 +96,8 @@ public void Interpreter_should_implement_chain_of_maps_correctly() [Fact] public void Interpreter_should_work_with_only_boundary_ops() { - WithOneBoundedSetup(Array.Empty>(), + WithOneBoundedSetup( + [], (lastEvents, upstream, downstream) => { lastEvents().Should().BeEmpty(); @@ -690,6 +691,7 @@ public void Interpreter_should_implement_take_take_with_PushAndFinish_from_upstr public void Interpreter_should_not_allow_AbsorbTermination_from_OnDownstreamFinish() { // This test must be kept since it tests the compatibility layer, which while is deprecated it is still here. +#pragma warning disable CS0618 // Type or member is obsolete, see comment above WithOneBoundedSetup(ToGraphStage(new InvalidAbsorbTermination()), (lastEvents, _, downstream) => { @@ -703,6 +705,7 @@ public void Interpreter_should_not_allow_AbsorbTermination_from_OnDownstreamFini lastEvents().Should().BeEquivalentTo(new Cancel(new NotSupportedException("It is not allowed to call AbsorbTermination() from OnDownstreamFinish."))); }); }); +#pragma warning restore CS0618 // Type or member is obsolete } public class Doubler : SimpleLinearGraphStage diff --git a/src/core/Akka.Streams.Tests/Sample.cs b/src/core/Akka.Streams.Tests/Sample.cs deleted file mode 100644 index 9cbc2d2adfe..00000000000 --- a/src/core/Akka.Streams.Tests/Sample.cs +++ /dev/null @@ -1,35 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2022 Lightbend Inc. -// Copyright (C) 2013-2025 .NET Foundation -// -//----------------------------------------------------------------------- - -using System; -using System.Threading.Tasks; -using Akka.Actor; -using Akka.Streams.Dsl; - -namespace Akka.Streams.Tests -{ - public class Sample - { - public static async Task Main() - { - var text = @" - Lorem Ipsum is simply dummy text of the printing and typesetting industry. - Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, - when an unknown printer took a galley of type and scrambled it to make a type - specimen book."; - - using (var system = ActorSystem.Create("streams-example")) - using (var materializer = system.Materializer()) - { - await Source - .From(text) - .Select(char.ToUpper) - .RunForeach(Console.WriteLine, materializer); - } - } - } -} diff --git a/src/core/Akka.Streams.Tests/ScriptedTest.cs b/src/core/Akka.Streams.Tests/ScriptedTest.cs index 84601aa38d1..00bdc9a35d1 100644 --- a/src/core/Akka.Streams.Tests/ScriptedTest.cs +++ b/src/core/Akka.Streams.Tests/ScriptedTest.cs @@ -30,7 +30,10 @@ public class ScriptException : Exception public ScriptException() { } public ScriptException(string message) : base(message) { } public ScriptException(string message, Exception inner) : base(message, inner) { } + +#pragma warning disable SYSLIB0051 // Needed for backward compatibility because of `Serializable` attribute protected ScriptException(SerializationInfo info, StreamingContext context) : base(info, context) { } +#pragma warning restore SYSLIB0051 } public abstract class ScriptedTest : AkkaSpec diff --git a/src/core/Akka.Streams/ActorMaterializer.cs b/src/core/Akka.Streams/ActorMaterializer.cs index f1415c38910..2ba15f8bdb1 100644 --- a/src/core/Akka.Streams/ActorMaterializer.cs +++ b/src/core/Akka.Streams/ActorMaterializer.cs @@ -662,6 +662,26 @@ public override bool Equals(object obj) s.StreamRefSettings == StreamRefSettings; } + public override int GetHashCode() + { + unchecked + { + var hash = (17 * 23) ^ InitialInputBufferSize; + hash = (hash * 23) ^ MaxInputBufferSize; + hash = (hash * 23) ^ Dispatcher.GetHashCode(); + hash = (hash * 23) ^ SupervisionDecider.GetHashCode(); + hash = (hash * 23) ^ SubscriptionTimeoutSettings.GetHashCode(); + hash = (hash * 23) ^ IsDebugLogging.GetHashCode(); + hash = (hash * 23) ^ OutputBurstLimit; + hash = (hash * 23) ^ SyncProcessingLimit; + hash = (hash * 23) ^ IsFuzzingMode.GetHashCode(); + hash = (hash * 23) ^ IsAutoFusing.GetHashCode(); + hash = (hash * 23) ^ MaxFixedBufferSize; + hash = (hash * 23) ^ StreamRefSettings.GetHashCode(); + return hash; + } + } + internal Attributes ToAttributes() { return new Attributes(new Attributes.IAttribute[] diff --git a/src/core/Akka.Streams/Actors/ActorPublisher.cs b/src/core/Akka.Streams/Actors/ActorPublisher.cs index f6ec15f71a1..d5c6f05c3a3 100644 --- a/src/core/Akka.Streams/Actors/ActorPublisher.cs +++ b/src/core/Akka.Streams/Actors/ActorPublisher.cs @@ -583,7 +583,13 @@ public override void AroundPreStart() if (SubscriptionTimeout != Timeout.InfiniteTimeSpan) { _scheduledSubscriptionTimeout = new Cancelable(Context.System.Scheduler); + // AK1004: Hard to replace this with IWithTimers implementation because SubscriptionTimeoutExceeded is + // being handled inside AroundReceive which interferes with IWithTimers internal message handling. + // + // Naive IWithTimers implementation will break this actor behavior. +#pragma warning disable AK1004 Context.System.Scheduler.ScheduleTellOnce(SubscriptionTimeout, Self, SubscriptionTimeoutExceeded.Instance, Self, _scheduledSubscriptionTimeout); +#pragma warning restore AK1004 } } @@ -622,6 +628,7 @@ public override void AroundPostRestart(Exception cause, object message) /// public override void AroundPostStop() { + _scheduledSubscriptionTimeout.Cancel(); _state.Remove(Self); try { @@ -666,11 +673,8 @@ public sealed class ActorPublisherImpl : IPublisher /// /// This exception is thrown when the specified is undefined. /// - public ActorPublisherImpl(IActorRef @ref) - { - if(@ref == null) throw new ArgumentNullException(nameof(@ref), "ActorPublisherImpl requires IActorRef to be defined"); - _ref = @ref; - } + public ActorPublisherImpl(IActorRef @ref) => + _ref = @ref ?? throw new ArgumentNullException(nameof(@ref), "ActorPublisherImpl requires IActorRef to be defined"); /// /// TBD @@ -700,11 +704,8 @@ public sealed class ActorPublisherSubscription : ISubscription /// /// This exception is thrown when the specified is undefined. /// - public ActorPublisherSubscription(IActorRef @ref) - { - if (@ref == null) throw new ArgumentNullException(nameof(@ref), "ActorPublisherSubscription requires IActorRef to be defined"); - _ref = @ref; - } + public ActorPublisherSubscription(IActorRef @ref) => + _ref = @ref ?? throw new ArgumentNullException(nameof(@ref), "ActorPublisherSubscription requires IActorRef to be defined"); /// /// TBD diff --git a/src/core/Akka.Streams/Actors/ActorSubscriber.cs b/src/core/Akka.Streams/Actors/ActorSubscriber.cs index f2a421f696b..5beed092c83 100644 --- a/src/core/Akka.Streams/Actors/ActorSubscriber.cs +++ b/src/core/Akka.Streams/Actors/ActorSubscriber.cs @@ -319,11 +319,8 @@ public sealed class ActorSubscriberImpl : ISubscriber /// /// This exception is thrown when the specified is undefined. /// - public ActorSubscriberImpl(IActorRef impl) - { - if (impl == null) throw new ArgumentNullException(nameof(impl), "ActorSubscriberImpl requires actor impl to be defined"); - _impl = impl; - } + public ActorSubscriberImpl(IActorRef impl) => + _impl = impl ?? throw new ArgumentNullException(nameof(impl), "ActorSubscriberImpl requires actor impl to be defined"); /// /// TBD diff --git a/src/core/Akka.Streams/Attributes.cs b/src/core/Akka.Streams/Attributes.cs index fadddc777e7..c7b4983d7a4 100644 --- a/src/core/Akka.Streams/Attributes.cs +++ b/src/core/Akka.Streams/Attributes.cs @@ -182,8 +182,9 @@ public sealed class AsyncBoundary : IAttribute, IEquatable { public static readonly AsyncBoundary Instance = new(); private AsyncBoundary() { } - public bool Equals(AsyncBoundary other) => other is AsyncBoundary; + public bool Equals(AsyncBoundary other) => other is not null; public override bool Equals(object obj) => obj is AsyncBoundary; + public override int GetHashCode() => 1087; public override string ToString() => "AsyncBoundary"; } @@ -397,9 +398,15 @@ public string GetNameOrDefault(string defaultIfNotFound = "unknown-operation") /// Note that operators in general should not inspect the whole hierarchy but instead use /// `get` to get the most specific attribute value. /// - [Obsolete("Use GetAttribute() instead")] + [Obsolete("Use Contains() instead")] public bool Contains(TAttr attribute) where TAttr : IAttribute => _attributes.Any(a => a is TAttr); + /// + /// Test whether the given attribute is contained within this attribute list. + /// + /// Note that operators in general should not inspect the whole hierarchy but instead use + /// `get` to get the most specific attribute value. + /// public bool Contains() where TAttr : IAttribute => _attributes.Any(a => a is TAttr); /// diff --git a/src/core/Akka.Streams/Dsl/FlowOperations.cs b/src/core/Akka.Streams/Dsl/FlowOperations.cs index 0a9705b5699..cf6b0f7a2c7 100644 --- a/src/core/Akka.Streams/Dsl/FlowOperations.cs +++ b/src/core/Akka.Streams/Dsl/FlowOperations.cs @@ -188,7 +188,7 @@ public static Flow Select(this Flow /// - /// For logging signals (elements, completion, error) consider using the stage instead, + /// For logging signals (elements, completion, error) consider using the stage instead, /// along with appropriate . /// /// diff --git a/src/core/Akka.Streams/Dsl/FlowWithContext.cs b/src/core/Akka.Streams/Dsl/FlowWithContext.cs index 501750448a3..b398446a89d 100644 --- a/src/core/Akka.Streams/Dsl/FlowWithContext.cs +++ b/src/core/Akka.Streams/Dsl/FlowWithContext.cs @@ -54,7 +54,7 @@ public FlowWithContext ViaMaterialized - /// Context-preserving variant of . + /// Context-preserving variant of . /// public FlowWithContext MapMaterializedValue(Func combine) => FlowWithContext.From(Flow.FromGraph(Inner).MapMaterializedValue(combine)); diff --git a/src/core/Akka.Streams/Dsl/FlowWithContextOperations.cs b/src/core/Akka.Streams/Dsl/FlowWithContextOperations.cs index 825252d3d3d..13e84674698 100644 --- a/src/core/Akka.Streams/Dsl/FlowWithContextOperations.cs +++ b/src/core/Akka.Streams/Dsl/FlowWithContextOperations.cs @@ -36,13 +36,22 @@ public static FlowWithContext SelectAsync /// Context-preserving variant of /// + [Obsolete("Deprecated. Please use Collect(FlowWithContext, Func, Func) instead. Since v1.5.44, will be removed in v1.6")] public static FlowWithContext Collect( this FlowWithContext flow, Func fn) where TOut2 : class + => Collect(flow, null, fn); + + /// + /// Context-preserving variant of + /// + public static FlowWithContext Collect( + this FlowWithContext flow, Func<(TOut, TCtx), bool>? isDefined, Func fn) where TOut2 : class { - var stage = new Collect<(TOut, TCtx), (TOut2, TCtx)>(func: x => + var stage = new Collect<(TOut, TCtx), (TOut2, TCtx)>(isDefined, x => { var result = fn(x.Item1); return ReferenceEquals(result, null) ? default((TOut2, TCtx)) : (result, x.Item2); @@ -178,13 +187,22 @@ public static SourceWithContext SelectAsync /// Context-preserving variant of /// + [Obsolete("Deprecated. Please use Collect(SourceWithContext, Func, Func) instead. Since v1.5.44, will be removed in v1.6")] public static SourceWithContext Collect( this SourceWithContext flow, Func fn) where TOut2 : class + => Collect(flow, null, fn); + + /// + /// Context-preserving variant of + /// + public static SourceWithContext Collect( + this SourceWithContext flow, Func<(TOut, TCtx), bool>? isDefined, Func fn) where TOut2 : class { - var stage = new Collect<(TOut, TCtx), (TOut2, TCtx)>(func: x => + var stage = new Collect<(TOut, TCtx), (TOut2, TCtx)>(isDefined, x => { var result = fn(x.Item1); return ReferenceEquals(result, null) ? default((TOut2, TCtx)) : (result, x.Item2); diff --git a/src/core/Akka.Streams/Dsl/Internal/InternalFlowOperations.cs b/src/core/Akka.Streams/Dsl/Internal/InternalFlowOperations.cs index 804519bd71f..3cbade68e3b 100644 --- a/src/core/Akka.Streams/Dsl/Internal/InternalFlowOperations.cs +++ b/src/core/Akka.Streams/Dsl/Internal/InternalFlowOperations.cs @@ -200,7 +200,7 @@ public static IFlow Select(this IFlow fl /// operations (such as `Log`, or emitting metrics), for each element without having to modify it. /// /// - /// For logging signals (elements, completion, error) consider using the stage instead, + /// For logging signals (elements, completion, error) consider using the stage instead, /// along with appropriate . /// /// @@ -450,7 +450,7 @@ public static IFlow SkipWhile(this IFlow flow, Predic /// TBD /// TBD /// TBD - [Obsolete("Deprecated. Please use Collect(isDefined, collector) instead")] + [Obsolete("Deprecated. Please use Collect(isDefined, collector) instead. Since v1.4.51")] public static IFlow Collect(this IFlow flow, Func collector) { return flow.Via(new Fusing.Collect(collector)); diff --git a/src/core/Akka.Streams/Dsl/Source.cs b/src/core/Akka.Streams/Dsl/Source.cs index 933d77d3177..7cf2efb8272 100644 --- a/src/core/Akka.Streams/Dsl/Source.cs +++ b/src/core/Akka.Streams/Dsl/Source.cs @@ -649,7 +649,7 @@ public static Source From(IEnumerable enumerable) /// beginning) regardless of when they subscribed. /// /// TBD - /// TBD + /// TBD /// TBD public static Source From(Func> asyncEnumerable) => FromGraph(new AsyncEnumerable(asyncEnumerable)).WithAttributes(DefaultAttributes.EnumerableSource); diff --git a/src/core/Akka.Streams/Dsl/SourceOperations.cs b/src/core/Akka.Streams/Dsl/SourceOperations.cs index 8b88a5ed71f..27aa33a4a13 100644 --- a/src/core/Akka.Streams/Dsl/SourceOperations.cs +++ b/src/core/Akka.Streams/Dsl/SourceOperations.cs @@ -1780,7 +1780,7 @@ public static Source AlsoTo(this Source flow /// operations (such as `Log`, or emitting metrics), for each element without having to modify it. /// /// - /// For logging signals (elements, completion, error) consider using the stage instead, + /// For logging signals (elements, completion, error) consider using the stage instead, /// along with appropriate . /// /// Emits when upstream emits an element @@ -1802,7 +1802,7 @@ public static Source WireTap(this Source flo /// through will also be sent to the wire-tap Sink, without the latter affecting the mainline flow. If the wire-tap Sink backpressures, /// elements that would've been sent to it will be dropped instead. /// - /// It is similar to which does backpressure instead of dropping elements. + /// It is similar to which does backpressure instead of dropping elements. /// Emits when element is available and demand exists from the downstream; the element will also be sent to the wire-tap Sink if there is demand. /// Backpressures when downstream backpressures /// Completes when upstream completes @@ -1815,9 +1815,9 @@ public static Source WireTap(this Source flo /// /// Attaches the given to this , as a wire tap, meaning that elements that pass /// through will also be sent to the wire-tap Sink, without the latter affecting the mainline flow. If the wire-tap Sink backpressures, - /// elements that would've been sent to it will be dropped instead.. + /// elements that would've been sent to it will be dropped instead. /// - /// It is similar to which does backpressure instead of dropping elements. + /// It is similar to which does backpressure instead of dropping elements. /// It is recommended to use the internally optimized and combiners /// where appropriate instead of manually writing functions that pass through one of the values. /// diff --git a/src/core/Akka.Streams/Dsl/SourceWithContext.cs b/src/core/Akka.Streams/Dsl/SourceWithContext.cs index 72350b04436..bffd4043cd2 100644 --- a/src/core/Akka.Streams/Dsl/SourceWithContext.cs +++ b/src/core/Akka.Streams/Dsl/SourceWithContext.cs @@ -72,7 +72,7 @@ public IRunnableGraph ToMaterialized(IGraph - /// Context-preserving variant of . + /// Context-preserving variant of . /// public SourceWithContext MapMaterializedValue(Func combine) => new(Source.FromGraph(Inner).MapMaterializedValue(combine)); diff --git a/src/core/Akka.Streams/Dsl/SubFlowOperations.cs b/src/core/Akka.Streams/Dsl/SubFlowOperations.cs index 04c2fc5ec98..a1352c494d7 100644 --- a/src/core/Akka.Streams/Dsl/SubFlowOperations.cs +++ b/src/core/Akka.Streams/Dsl/SubFlowOperations.cs @@ -184,7 +184,7 @@ public static SubFlow Select(this /// operations (such as `Log`, or emitting metrics), for each element without having to modify it. /// /// - /// For logging signals (elements, completion, error) consider using the stage instead, + /// For logging signals (elements, completion, error) consider using the stage instead, /// along with appropriate . /// /// @@ -467,6 +467,7 @@ public static SubFlow SkipWhile(this S return (SubFlow)InternalFlowOperations.SkipWhile(flow, predicate); } + // Backward compatibility /// /// Transform this stream by applying the given function to each of the elements /// on which the function is defined (read: returns not null) as they pass through this processing step. @@ -480,17 +481,25 @@ public static SubFlow SkipWhile(this S /// /// Cancels when downstream cancels /// - /// TBD - /// TBD - /// TBD - /// TBD - /// TBD - /// TBD - /// TBD + [Obsolete("Deprecated. Please use Collect(SubFlow, Func, Func) instead. Since v1.5.44, will be removed in v1.6")] public static SubFlow Collect(this SubFlow flow, Func collector) - { - return (SubFlow)InternalFlowOperations.Collect(flow, collector); - } + => (SubFlow)InternalFlowOperations.Collect(flow, null, collector); + + /// + /// Transform this stream by applying the given function to each of the elements + /// on which the delegate returns true as they pass through this processing step. + /// Non-matching elements are filtered out. + /// + /// Emits when the provided function is defined for the element + /// + /// Backpressures when the function is defined for the element and downstream backpressures + /// + /// Completes when upstream completes + /// + /// Cancels when downstream cancels + /// + public static SubFlow Collect(this SubFlow flow, Func? isDefined, Func collector) + => (SubFlow)InternalFlowOperations.Collect(flow, isDefined, collector); /// /// Chunk up this stream into groups of the given size, with the last group diff --git a/src/core/Akka.Streams/FanInShape.cs b/src/core/Akka.Streams/FanInShape.cs index 52d3f852770..e7a1202050c 100644 --- a/src/core/Akka.Streams/FanInShape.cs +++ b/src/core/Akka.Streams/FanInShape.cs @@ -91,11 +91,8 @@ public sealed class InitPorts : IInit /// TBD public InitPorts(Outlet outlet, IEnumerable inlets) { - if (outlet == null) throw new ArgumentNullException(nameof(outlet)); - if (inlets == null) throw new ArgumentNullException(nameof(inlets)); - - _outlet = outlet; - _inlets = inlets; + _outlet = outlet ?? throw new ArgumentNullException(nameof(outlet)); + _inlets = inlets ?? throw new ArgumentNullException(nameof(inlets)); } /// diff --git a/src/core/Akka.Streams/FanOutShape.cs b/src/core/Akka.Streams/FanOutShape.cs index bebf97a663e..18815ebdb58 100644 --- a/src/core/Akka.Streams/FanOutShape.cs +++ b/src/core/Akka.Streams/FanOutShape.cs @@ -87,11 +87,8 @@ public sealed class InitPorts : IInit /// TBD public InitPorts(Inlet inlet, IEnumerable outlets) { - if (outlets == null) throw new ArgumentNullException(nameof(outlets)); - if (inlet == null) throw new ArgumentNullException(nameof(inlet)); - - Inlet = inlet; - Outlets = outlets; + Inlet = inlet ?? throw new ArgumentNullException(nameof(inlet)); + Outlets = outlets ?? throw new ArgumentNullException(nameof(outlets)); Name = "FanOut"; } diff --git a/src/core/Akka.Streams/Fusing.cs b/src/core/Akka.Streams/Fusing.cs index eac31a423f2..506008a43cc 100644 --- a/src/core/Akka.Streams/Fusing.cs +++ b/src/core/Akka.Streams/Fusing.cs @@ -66,11 +66,8 @@ public sealed class FusedGraph : IGraph where TShape /// TBD public FusedGraph(FusedModule module, TShape shape) { - if (module == null) throw new ArgumentNullException(nameof(module)); - if (shape == null) throw new ArgumentNullException(nameof(shape)); - - Module = module; - Shape = shape; + Module = module ?? throw new ArgumentNullException(nameof(module)); + Shape = shape ?? throw new ArgumentNullException(nameof(shape)); } /// diff --git a/src/core/Akka.Streams/Implementation/ActorMaterializerImpl.cs b/src/core/Akka.Streams/Implementation/ActorMaterializerImpl.cs index 487821ab2bd..d36d29995ec 100644 --- a/src/core/Akka.Streams/Implementation/ActorMaterializerImpl.cs +++ b/src/core/Akka.Streams/Implementation/ActorMaterializerImpl.cs @@ -184,7 +184,7 @@ private void MaterializeGraph(GraphModule graph, Attributes effectiveAttributes, var logics = t.Item2; var shell = new GraphInterpreterShell(graph.Assembly, connections, logics, graph.Shape, calculatedSettings, _materializer); - var impl = _subflowFuser != null && !effectiveAttributes.Contains(Attributes.AsyncBoundary.Instance) + var impl = _subflowFuser != null && !effectiveAttributes.Contains() ? _subflowFuser(shell) : _materializer.ActorOf(ActorGraphInterpreter.Props(shell), StageName(effectiveAttributes), calculatedSettings.Dispatcher); diff --git a/src/core/Akka.Streams/Implementation/AsyncEnumerable.cs b/src/core/Akka.Streams/Implementation/AsyncEnumerable.cs index 51d00d2328e..8e27d04b263 100644 --- a/src/core/Akka.Streams/Implementation/AsyncEnumerable.cs +++ b/src/core/Akka.Streams/Implementation/AsyncEnumerable.cs @@ -16,7 +16,6 @@ namespace Akka.Streams.Dsl /// Used to treat an of /// as an /// - /// public sealed class StreamsAsyncEnumerableRerunnable : IAsyncEnumerable { private static readonly Sink> defaultSinkqueue = diff --git a/src/core/Akka.Streams/Implementation/FanOut.cs b/src/core/Akka.Streams/Implementation/FanOut.cs index cef26aee445..e0b2820e960 100644 --- a/src/core/Akka.Streams/Implementation/FanOut.cs +++ b/src/core/Akka.Streams/Implementation/FanOut.cs @@ -740,7 +740,7 @@ internal sealed class Unzip : FanOut /// This exception is thrown when the elements in /// are of an unknown type. /// > - /// If this gets changed you must change as well! + /// If this gets changed you must change as well! public Unzip(ActorMaterializerSettings settings, int outputCount = 2) : base(settings, outputCount) { OutputBunch.MarkAllOutputs(); diff --git a/src/core/Akka.Streams/Implementation/Fusing/Fusing.cs b/src/core/Akka.Streams/Implementation/Fusing/Fusing.cs index 82a9a79d670..610508e6e2e 100644 --- a/src/core/Akka.Streams/Implementation/Fusing/Fusing.cs +++ b/src/core/Akka.Streams/Implementation/Fusing/Fusing.cs @@ -330,8 +330,8 @@ private static LinkedList> Descend { var isAsync = module is GraphStageModule or GraphModule - ? module.Attributes.Contains(Attributes.AsyncBoundary.Instance) - : module.IsAtomic || module.Attributes.Contains(Attributes.AsyncBoundary.Instance); + ? module.Attributes.Contains() + : module.IsAtomic || module.Attributes.Contains(); if (IsDebug) Log(indent, $"entering {module.GetType().Name} (hash={module.GetHashCode()}, async={isAsync}, name={module.Attributes.GetNameLifted()}, dispatcher={GetDispatcher(module)})"); @@ -545,7 +545,7 @@ private static IMaterializedValueNode RewriteMaterializer(IDictionary(); } /// diff --git a/src/core/Akka.Streams/Implementation/Fusing/GraphInterpreter.cs b/src/core/Akka.Streams/Implementation/Fusing/GraphInterpreter.cs index 6d694e60da4..bcdd96bebeb 100644 --- a/src/core/Akka.Streams/Implementation/Fusing/GraphInterpreter.cs +++ b/src/core/Akka.Streams/Implementation/Fusing/GraphInterpreter.cs @@ -920,9 +920,7 @@ private Connection Dequeue() if (FuzzingMode) { var swapWith = (ThreadLocalRandom.Current.Next(_queueTail - _queueHead) + _queueHead) & _mask; - var ev = _eventQueue[swapWith]; - _eventQueue[swapWith] = _eventQueue[idx]; - _eventQueue[idx] = ev; + (_eventQueue[swapWith], _eventQueue[idx]) = (_eventQueue[idx], _eventQueue[swapWith]); } var element = _eventQueue[idx]; _eventQueue[idx] = NoEvent; @@ -938,7 +936,9 @@ private Connection Dequeue() /// TBD public void Enqueue(Connection connection) { +#pragma warning disable CS0162 // Unreachable code can be reached if IsDebug is set to true. if (IsDebug && _queueTail - _queueHead > _mask) throw new Exception($"{Name} internal queue full ({QueueStatus()}) + {connection}"); +#pragma warning restore CS0162 _eventQueue[_queueTail & _mask] = connection; _queueTail++; } diff --git a/src/core/Akka.Streams/Implementation/Fusing/Ops.cs b/src/core/Akka.Streams/Implementation/Fusing/Ops.cs index 93c028897cc..8acc0472cd3 100644 --- a/src/core/Akka.Streams/Implementation/Fusing/Ops.cs +++ b/src/core/Akka.Streams/Implementation/Fusing/Ops.cs @@ -1969,7 +1969,7 @@ public Logic(Buffer stage) : base(stage.Shape) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - if (_buffer.IsFull) + if (_buffer!.IsFull) _buffer.DropHead(); _buffer.Enqueue(element); Pull(_stage.Inlet); @@ -1980,7 +1980,7 @@ public Logic(Buffer stage) : base(stage.Shape) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - if (_buffer.IsFull) + if (_buffer!.IsFull) _buffer.DropTail(); _buffer.Enqueue(element); Pull(_stage.Inlet); @@ -1991,7 +1991,7 @@ public Logic(Buffer stage) : base(stage.Shape) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - if (_buffer.IsFull) + if (_buffer!.IsFull) _buffer.Clear(); _buffer.Enqueue(element); Pull(_stage.Inlet); @@ -2002,7 +2002,7 @@ public Logic(Buffer stage) : base(stage.Shape) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - if (!_buffer.IsFull) + if (!_buffer!.IsFull) _buffer.Enqueue(element); Pull(_stage.Inlet); @@ -2013,7 +2013,7 @@ public Logic(Buffer stage) : base(stage.Shape) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - _buffer.Enqueue(element); + _buffer!.Enqueue(element); if (!_buffer.IsFull) Pull(_stage.Inlet); @@ -2024,7 +2024,7 @@ public Logic(Buffer stage) : base(stage.Shape) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - if (_buffer.IsFull) + if (_buffer!.IsFull) FailStage(new BufferOverflowException( $"Buffer overflow (max capacity was {_stage._count})")); else @@ -2068,7 +2068,7 @@ public override void OnPull() { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - if (_buffer.NonEmpty) + if (_buffer!.NonEmpty) Push(_stage.Outlet, _buffer.Dequeue()); if (IsClosed(_stage.Inlet)) { @@ -2083,7 +2083,7 @@ public override void OnUpstreamFinish() { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - if (_buffer.IsEmpty) + if (_buffer!.IsEmpty) CompleteStage(); } } @@ -2855,7 +2855,7 @@ public override void OnPull() Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); var inlet = _stage.In; - if (!_buffer.IsEmpty) + if (!_buffer!.IsEmpty) Push(_stage.Out, _buffer.Dequeue()); else if (IsClosed(inlet) && Todo == 0) CompleteStage(); @@ -2878,7 +2878,7 @@ private void TaskCompleted(Result result) Push(_stage.Out, result.Value); } else - _buffer.Enqueue(result.Value); + _buffer!.Enqueue(result.Value); } else { @@ -2900,7 +2900,7 @@ private int Todo { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - return _inFlight + _buffer.Used; + return _inFlight + _buffer!.Used; } } @@ -3350,7 +3350,7 @@ public void OnPush() { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - if (_buffer.IsFull) + if (_buffer!.IsFull) _onPushWhenBufferFull(); else { @@ -3368,7 +3368,7 @@ public void OnPull() { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - if (!IsTimerActive(TimerName) && !_buffer.IsEmpty && NextElementWaitTime < 0) + if (!IsTimerActive(TimerName) && !_buffer!.IsEmpty && NextElementWaitTime < 0) Push(_stage.Outlet, _buffer.Dequeue().Item2); if (!IsClosed(_stage.Inlet) && !HasBeenPulled(_stage.Inlet) && PullCondition) @@ -3385,7 +3385,7 @@ private long NextElementWaitTime { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - return (long)_stage._delay.TotalMilliseconds - (DateTime.UtcNow.Ticks - _buffer.Peek().Item1) * 1000 * 10; + return (long)_stage._delay.TotalMilliseconds - (DateTime.UtcNow.Ticks - _buffer!.Peek().Item1) * 1000 * 10; } } @@ -3395,7 +3395,7 @@ private void CompleteIfReady() { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - if (IsClosed(_stage.Inlet) && _buffer.IsEmpty) + if (IsClosed(_stage.Inlet) && _buffer!.IsEmpty) CompleteStage(); } @@ -3404,9 +3404,9 @@ protected internal override void OnTimer(object timerKey) Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); if (IsAvailable(_stage.Outlet)) - Push(_stage.Outlet, _buffer.Dequeue().Item2); + Push(_stage.Outlet, _buffer!.Dequeue().Item2); - if (!_buffer.IsEmpty) + if (!_buffer!.IsEmpty) { var waitTime = NextElementWaitTime; if (waitTime > 10) @@ -3422,7 +3422,7 @@ private bool PullCondition { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - return _stage._strategy != DelayOverflowStrategy.Backpressure || _buffer.Used < _size; + return _stage._strategy != DelayOverflowStrategy.Backpressure || _buffer!.Used < _size; } } @@ -3430,7 +3430,7 @@ private void GrabAndPull() { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - _buffer.Enqueue((DateTime.UtcNow.Ticks, Grab(_stage.Inlet))); + _buffer!.Enqueue((DateTime.UtcNow.Ticks, Grab(_stage.Inlet))); if (PullCondition) Pull(_stage.Inlet); } @@ -3445,7 +3445,7 @@ private Action OnPushStrategy(DelayOverflowStrategy strategy) Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); if (!IsTimerActive(TimerName)) - Push(_stage.Outlet, _buffer.Dequeue().Item2); + Push(_stage.Outlet, _buffer!.Dequeue().Item2); else { CancelTimer(TimerName); @@ -3457,7 +3457,7 @@ private Action OnPushStrategy(DelayOverflowStrategy strategy) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - _buffer.DropHead(); + _buffer!.DropHead(); GrabAndPull(); }; case DelayOverflowStrategy.DropTail: @@ -3465,7 +3465,7 @@ private Action OnPushStrategy(DelayOverflowStrategy strategy) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - _buffer.DropTail(); + _buffer!.DropTail(); GrabAndPull(); }; case DelayOverflowStrategy.DropNew: @@ -3480,7 +3480,7 @@ private Action OnPushStrategy(DelayOverflowStrategy strategy) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - _buffer.Clear(); + _buffer!.Clear(); GrabAndPull(); }; case DelayOverflowStrategy.Fail: diff --git a/src/core/Akka.Streams/Implementation/Fusing/StreamOfStreams.cs b/src/core/Akka.Streams/Implementation/Fusing/StreamOfStreams.cs index 257a79af94b..4011bd46143 100644 --- a/src/core/Akka.Streams/Implementation/Fusing/StreamOfStreams.cs +++ b/src/core/Akka.Streams/Implementation/Fusing/StreamOfStreams.cs @@ -49,7 +49,7 @@ public Logic(FlattenMerge stage, Attributes enclosingAttributes Debug.Assert(_q != null, nameof(_q) + " != null"); // could be unavailable due to async input having been executed before this notification - if (_q.NonEmpty && IsAvailable(_stage._out)) + if (_q!.NonEmpty && IsAvailable(_stage._out)) PushOut(); }; @@ -86,7 +86,7 @@ private void PushOut() { Debug.Assert(_q != null, nameof(_q) + " != null"); - var src = _q.Dequeue(); + var src = _q!.Dequeue(); Push(_stage._out, src.Grab()); if (!src.IsClosed) src.Pull(); @@ -119,7 +119,7 @@ private void AddSource(IGraph, TMat> source) sinkIn.Pull(); } else - _q.Enqueue(sinkIn); + _q!.Enqueue(sinkIn); }, onUpstreamFinish: () => { diff --git a/src/core/Akka.Streams/Implementation/JsonObjectParser.cs b/src/core/Akka.Streams/Implementation/JsonObjectParser.cs index 63fbd66c42c..683e88a0623 100644 --- a/src/core/Akka.Streams/Implementation/JsonObjectParser.cs +++ b/src/core/Akka.Streams/Implementation/JsonObjectParser.cs @@ -49,7 +49,6 @@ public class JsonObjectParser private int _pos; // latest position of pointer while scanning for json object end private int _trimFront; private int _depth; - private int _charsInObject; private bool _completedObject; private bool _inStringExpression; private bool _isStartOfEscapeSequence; @@ -166,7 +165,6 @@ private void Proceed(byte input) _pos++; if (_depth == 0) { - _charsInObject = 0; _completedObject = true; } } diff --git a/src/core/Akka.Streams/Implementation/Sinks.cs b/src/core/Akka.Streams/Implementation/Sinks.cs index cf912275643..da7ef7f2208 100644 --- a/src/core/Akka.Streams/Implementation/Sinks.cs +++ b/src/core/Akka.Streams/Implementation/Sinks.cs @@ -727,7 +727,7 @@ public void OnPush() Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); EnqueueAndNotify(new Result>(Grab(_stage.In))); - if (_buffer.Used < _maxBuffer) Pull(_stage.In); + if (_buffer!.Used < _maxBuffer) Pull(_stage.In); } public void OnUpstreamFinish() => EnqueueAndNotify(new Result>(Option.None)); @@ -759,7 +759,7 @@ private Action>> Callback() { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - if (_buffer.IsEmpty) + if (_buffer!.IsEmpty) _currentRequest = promise; else { @@ -775,7 +775,7 @@ private void SendDownstream(TaskCompletionSource> promise) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - var e = _buffer.Dequeue(); + var e = _buffer!.Dequeue(); if (e.IsSuccess) { promise.SetResult(e.Value); @@ -784,7 +784,7 @@ private void SendDownstream(TaskCompletionSource> promise) } else { - promise.SetException(e.Exception); + promise.SetException(e.Exception!); FailStage(e.Exception); } } @@ -793,7 +793,7 @@ private void EnqueueAndNotify(Result> requested) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - _buffer.Enqueue(requested); + _buffer!.Enqueue(requested); if (_currentRequest.HasValue) { SendDownstream(_currentRequest.Value); diff --git a/src/core/Akka.Streams/Implementation/Sources.cs b/src/core/Akka.Streams/Implementation/Sources.cs index 5f9094ad8ea..34f9a56713d 100644 --- a/src/core/Akka.Streams/Implementation/Sources.cs +++ b/src/core/Akka.Streams/Implementation/Sources.cs @@ -185,7 +185,7 @@ private void EnqueueAndSuccess(Offer offer) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - _buffer.Enqueue(offer.Element); + _buffer!.Enqueue(offer.Element); offer.CompletionSource.NonBlockingTrySetResult(QueueOfferResult.Enqueued.Instance); } @@ -193,7 +193,7 @@ private void BufferElement(Offer offer) { Debug.Assert(_buffer != null, nameof(_buffer) + " != null"); - if (!_buffer.IsFull) + if (!_buffer!.IsFull) EnqueueAndSuccess(offer); else { diff --git a/src/core/Akka.Streams/Implementation/StreamRef/SourceRefImpl.cs b/src/core/Akka.Streams/Implementation/StreamRef/SourceRefImpl.cs index 7e4c787c854..e9bef29f744 100644 --- a/src/core/Akka.Streams/Implementation/StreamRef/SourceRefImpl.cs +++ b/src/core/Akka.Streams/Implementation/StreamRef/SourceRefImpl.cs @@ -100,7 +100,7 @@ public IActorRef Self get { Debug.Assert(_stageActor != null, nameof(_stageActor) + " != null"); - return _stageActor.Ref; + return _stageActor!.Ref; } } public IActorRef PartnerRef @@ -165,10 +165,10 @@ private void TriggerCumulativeDemand() Debug.Assert(_receiveBuffer != null, nameof(_receiveBuffer) + " != null"); Debug.Assert(_requestStrategy != null, nameof(_requestStrategy) + " != null"); - var i = _receiveBuffer.RemainingCapacity - _localRemainingRequested; + var i = _receiveBuffer!.RemainingCapacity - _localRemainingRequested; if (_partnerRef != null && i > 0) { - var addDemand = _requestStrategy.RequestDemand((int)(_receiveBuffer.Used + _localRemainingRequested)); + var addDemand = _requestStrategy!.RequestDemand((int)(_receiveBuffer.Used + _localRemainingRequested)); // only if demand has increased we shoot it right away // otherwise it's the same demand level, so it'd be triggered via redelivery anyway @@ -237,14 +237,14 @@ private void InitialReceive((IActorRef, object) args) ObserveAndValidateSender(sender, "Illegal sender in RemoteStreamCompleted"); ObserveAndValidateSequenceNr(completed.SeqNr, "Illegal sequence nr in RemoteStreamCompleted"); Log.Debug("[{0}] The remote stream has completed, completing as well...", StageActorName); - _stageActor.Unwatch(sender); + _stageActor!.Unwatch(sender); _completed = true; TryPush(); break; case RemoteStreamFailure failure: ObserveAndValidateSender(sender, "Illegal sender in RemoteStreamFailure"); Log.Warning("[{0}] The remote stream has failed, failing (reason: {1})", StageActorName, failure.Message); - _stageActor.Unwatch(sender); + _stageActor!.Unwatch(sender); FailStage(new RemoteStreamRefActorTerminatedException($"Remote stream ({sender.Path}) failed, reason: {failure.Message}")); break; case Terminated terminated: @@ -265,7 +265,7 @@ private void TryPush() { Debug.Assert(_receiveBuffer != null, nameof(_receiveBuffer) + " != null"); - if (!_receiveBuffer.IsEmpty && IsAvailable(_stage.Outlet)) Push(_stage.Outlet, _receiveBuffer.Dequeue()); + if (!_receiveBuffer!.IsEmpty && IsAvailable(_stage.Outlet)) Push(_stage.Outlet, _receiveBuffer.Dequeue()); else if (_receiveBuffer.IsEmpty && _completed) CompleteStage(); } @@ -275,7 +275,7 @@ private void OnReceiveElement(object payload) var outlet = _stage.Outlet; _localRemainingRequested--; - if (_receiveBuffer.IsEmpty && IsAvailable(outlet)) + if (_receiveBuffer!.IsEmpty && IsAvailable(outlet)) Push(outlet, (TOut)payload); else if (_receiveBuffer.IsFull) throw new IllegalStateException($"Attempted to overflow buffer! Capacity: {_receiveBuffer.Capacity}, incoming element: {payload}, localRemainingRequested: {_localRemainingRequested}, localCumulativeDemand: {_localCumulativeDemand}"); @@ -295,7 +295,7 @@ private void ObserveAndValidateSender(IActorRef partner, string failureMessage) { Log.Debug("Received first message from {0}, assuming it to be the remote partner for this stage", partner); _partnerRef = partner; - _stageActor.Watch(partner); + _stageActor!.Watch(partner); } else if (!Equals(_partnerRef, partner)) { diff --git a/src/core/Akka.Streams/Stage/GraphStage.cs b/src/core/Akka.Streams/Stage/GraphStage.cs index ff5182aa7c5..c14c75eba20 100644 --- a/src/core/Akka.Streams/Stage/GraphStage.cs +++ b/src/core/Akka.Streams/Stage/GraphStage.cs @@ -266,7 +266,7 @@ public interface IAsyncCallback /// /// /// To be thread safe the methods of this class must only be called from either the constructor of the graph operator during - /// materialization or one of the methods invoked by the graph operator machinery, such as + /// materialization or one of the methods invoked by the graph operator machinery, such as /// public abstract class TimerGraphStageLogic : GraphStageLogic { diff --git a/src/core/Akka.Streams/Stage/Stage.cs b/src/core/Akka.Streams/Stage/Stage.cs index 2444cddca0d..8fd994af261 100644 --- a/src/core/Akka.Streams/Stage/Stage.cs +++ b/src/core/Akka.Streams/Stage/Stage.cs @@ -275,12 +275,8 @@ protected StatefulStage(StageState current) /// /// This exception is thrown when the specified is undefined. /// - public void Become(StageState state) - { - if (state == null) - throw new ArgumentNullException(nameof(state)); - _current = state; - } + public void Become(StageState state) => + _current = state ?? throw new ArgumentNullException(nameof(state)); /// /// Invokes current state. @@ -417,7 +413,9 @@ private StageState EmittingState(IEnumerator enumerator, Statef { _isEmitting = false; - if (andThen is StatefulStage.Stay) ; + if (andThen is StatefulStage.Stay) + { + } else if (andThen is StatefulStage.Become become) { Become(become.State); diff --git a/src/core/Akka.TestKit.Tests/TestActorRefTests/TestActorRefSpec.cs b/src/core/Akka.TestKit.Tests/TestActorRefTests/TestActorRefSpec.cs index c1cbaf62429..cca688515be 100644 --- a/src/core/Akka.TestKit.Tests/TestActorRefTests/TestActorRefSpec.cs +++ b/src/core/Akka.TestKit.Tests/TestActorRefTests/TestActorRefSpec.cs @@ -7,6 +7,7 @@ using System; using System.Threading; +using System.Threading.Tasks; using Akka.Actor; using Akka.Configuration; using Akka.Dispatch; @@ -57,21 +58,21 @@ public void TestActorRef_name_must_start_with_double_dollar_sign() } [Fact] - public void TestActorRef_must_support_nested_Actor_creation_when_used_with_TestActorRef() + public async Task TestActorRef_must_support_nested_Actor_creation_when_used_with_TestActorRef() { var a = new TestActorRef(Sys, Props.Create(() => new NestingActor(true))); Assert.NotNull(a); - var nested = a.Ask("any", DefaultTimeout).Result; + var nested = await a.Ask("any", DefaultTimeout); Assert.NotNull(nested); Assert.NotSame(a, nested); } [Fact] - public void TestActorRef_must_support_nested_Actor_creation_when_used_with_ActorRef() + public async Task TestActorRef_must_support_nested_Actor_creation_when_used_with_ActorRef() { var a = new TestActorRef(Sys, Props.Create(() => new NestingActor(false))); Assert.NotNull(a); - var nested = a.Ask("any", DefaultTimeout).Result; + var nested = await a.Ask("any", DefaultTimeout); Assert.NotNull(nested); Assert.NotSame(a, nested); } diff --git a/src/core/Akka.TestKit.Tests/TestKitAsyncCancellationSpec.cs b/src/core/Akka.TestKit.Tests/TestKitAsyncCancellationSpec.cs new file mode 100644 index 00000000000..072c04773f3 --- /dev/null +++ b/src/core/Akka.TestKit.Tests/TestKitAsyncCancellationSpec.cs @@ -0,0 +1,147 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Akka.TestKit; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Tests.TestKit +{ + /// + /// Tests for GitHub issue #7743 - Excessive exception nesting when cancelling ExpectMsgAsync + /// + public class TestKitAsyncCancellationSpec : AkkaSpec + { + public TestKitAsyncCancellationSpec(ITestOutputHelper output) : base(output) + { + } + + [Fact(DisplayName = "ExpectMsgAsync should not have excessive exception nesting when cancelled")] + public async Task ExpectMsgAsync_Should_Not_Have_Excessive_Exception_Nesting_When_Cancelled() + { + var probe = CreateTestProbe(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + Exception caughtException = null; + try + { + await probe.ExpectMsgAsync(cancellationToken: cts.Token); + } + catch (Exception ex) + { + caughtException = ex; + } + + Assert.NotNull(caughtException); + + // The key issue from #7743 is that we should NOT have AggregateException containing AggregateException + // We should have at most one level of wrapping + if (caughtException is AggregateException aggEx) + { + // The inner exception should NOT be another AggregateException + Assert.IsNotType(aggEx.InnerException); + // It should be some form of OperationCanceledException + Assert.IsAssignableFrom(aggEx.InnerException); + } + else + { + // Or it could be OperationCanceledException directly (which is fine) + Assert.IsAssignableFrom(caughtException); + } + } + + [Fact(DisplayName = "ExpectMsgAsync accessed via Task.Exception should not have excessive nesting")] + public async Task ExpectMsgAsync_Task_Exception_Should_Not_Have_Excessive_Nesting() + { + var probe = CreateTestProbe(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var task = probe.ExpectMsgAsync(cancellationToken: cts.Token).AsTask(); + + // Wait for the task to complete + await Task.Delay(100); + + // The task should be either Canceled or Faulted + Assert.True(task.IsCanceled || task.IsFaulted); + + if (task.IsFaulted) + { + Assert.NotNull(task.Exception); + + // The key issue: we should NOT have nested AggregateExceptions + var aggEx = Assert.IsType(task.Exception); + + // Verify no excessive nesting - inner should not be AggregateException + Assert.IsNotType(aggEx.InnerException); + + // It should be some form of OperationCanceledException + Assert.IsAssignableFrom(aggEx.InnerException); + } + // If task.IsCanceled is true, Task.Exception will be null, which is also valid + } + + [Fact(DisplayName = "Synchronous ExpectMsg with cancellation should not have excessive nesting")] + public void ExpectMsg_Synchronous_Should_Not_Have_Excessive_Nesting() + { + var probe = CreateTestProbe(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + Exception syncException = null; + try + { + probe.ExpectMsg(cancellationToken: cts.Token); + } + catch (Exception ex) + { + syncException = ex; + } + + Assert.NotNull(syncException); + + // The key issue: verify no excessive nesting + if (syncException is AggregateException aggEx) + { + // The inner exception should NOT be another AggregateException + Assert.IsNotType(aggEx.InnerException); + // It should be some form of OperationCanceledException + Assert.IsAssignableFrom(aggEx.InnerException); + } + else + { + // Or it could be OperationCanceledException directly + Assert.IsAssignableFrom(syncException); + } + } + + [Fact(DisplayName = "Original bug report scenario from issue #7743")] + public async Task Original_Bug_Report_Scenario_Should_Pass() + { + // This is the exact test from the bug report that was failing + var probe = CreateTestProbe(); + + using var stopper = new CancellationTokenSource(); + stopper.Cancel(); + + var task = probe.ExpectMsgAsync( + cancellationToken: stopper.Token).AsTask(); + + await Task.Delay(TimeSpan.FromSeconds(1)); // default timeout is 3 seconds + + // Original test expected double nesting - we're verifying this is fixed + if (task.IsFaulted && task.Exception != null) + { + var outer = task.Exception; + Assert.IsType(outer); + + // The bug was that InnerException was ALSO an AggregateException + // Now it should be OperationCanceledException directly + Assert.IsNotType(outer.InnerException); + Assert.IsAssignableFrom(outer.InnerException); + } + // Task might also be in Canceled state, which is valid + } + } +} \ No newline at end of file diff --git a/src/core/Akka.TestKit/ActorCellKeepingSynchronizationContext.cs b/src/core/Akka.TestKit/ActorCellKeepingSynchronizationContext.cs index 68e42437da8..80fc5e63e0f 100644 --- a/src/core/Akka.TestKit/ActorCellKeepingSynchronizationContext.cs +++ b/src/core/Akka.TestKit/ActorCellKeepingSynchronizationContext.cs @@ -22,8 +22,6 @@ namespace Akka.TestKit class ActorCellKeepingSynchronizationContext : SynchronizationContext { private readonly ActorCell _cell; - - internal static ActorCell AsyncCache { get; set; } /// /// TBD @@ -46,7 +44,8 @@ public override void Post(SendOrPostCallback d, object state) var oldCell = InternalCurrentActorCellKeeper.Current; var oldContext = Current; SetSynchronizationContext(this); - InternalCurrentActorCellKeeper.Current = AsyncCache ?? _cell; + + InternalCurrentActorCellKeeper.Current = _cell; try { diff --git a/src/core/Akka.TestKit/CallingThreadDispatcher.cs b/src/core/Akka.TestKit/CallingThreadDispatcher.cs index e5cd34cb561..68727832df7 100644 --- a/src/core/Akka.TestKit/CallingThreadDispatcher.cs +++ b/src/core/Akka.TestKit/CallingThreadDispatcher.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Threading; using Akka.Configuration; using Akka.Dispatch; @@ -53,7 +54,22 @@ public CallingThreadDispatcher(MessageDispatcherConfigurator configurator) : bas protected override void ExecuteTask(IRunnable run) { - run.Run(); + var currentSyncContext = SynchronizationContext.Current; + + try + { + // Actors should not run with ActorCellKeepingSynchronizationContext + // (or any sync context that wraps ActorCellKeepingSynchronizationContext, e.g. Xunit's AsyncTestSyncContext) + // otherwise continuations in async message handlers will use ActorCellKeepingSynchronizationContext + // instead of ActorTaskScheduler which causes ActorContext to be incorrect. + SynchronizationContext.SetSynchronizationContext(null); + + run.Run(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(currentSyncContext); + } } protected override void Shutdown() diff --git a/src/core/Akka.TestKit/Internal/InternalTestActorRef.cs b/src/core/Akka.TestKit/Internal/InternalTestActorRef.cs index bac4c89709d..a386cf8787f 100644 --- a/src/core/Akka.TestKit/Internal/InternalTestActorRef.cs +++ b/src/core/Akka.TestKit/Internal/InternalTestActorRef.cs @@ -323,18 +323,6 @@ internal TestActorTaskScheduler(ActorCell testActorCell, Action - protected override void OnBeforeTaskStarted() - { - ActorCellKeepingSynchronizationContext.AsyncCache = _testActorCell; - } - - /// - protected override void OnAfterTaskCompleted() - { - ActorCellKeepingSynchronizationContext.AsyncCache = null; - } - public void OnTaskCompleted(object message, Exception exception) { _taskCallback(message, exception); diff --git a/src/core/Akka.TestKit/TestKitBase.cs b/src/core/Akka.TestKit/TestKitBase.cs index fe268f29fc6..2fb8dd7847b 100644 --- a/src/core/Akka.TestKit/TestKitBase.cs +++ b/src/core/Akka.TestKit/TestKitBase.cs @@ -198,13 +198,25 @@ private static void WaitUntilTestActorIsReady(IActorRef testActor, TestKitSettin var deadline = settings.TestKitStartupTimeout; var stopwatch = Stopwatch.StartNew(); var ready = false; + try { + // TestActor should start almost instantly (microseconds). + // Use SpinWait which will spin for ~10-20 microseconds then yield. + var spinWait = new SpinWait(); + while (stopwatch.Elapsed < deadline) { ready = testActor is not IRepointableRef repRef || repRef.IsStarted; if (ready) break; - Thread.Sleep(10); + + // SpinWait automatically handles the progression: + // - First ~10 iterations: tight spin loop (microseconds) + // - Next iterations: Thread.Yield() + // - Later: Thread.Sleep(0) + // - Finally: Thread.Sleep(1) + // This is optimal for both fast startup and system under load + spinWait.SpinOnce(); } } finally diff --git a/src/core/Akka.TestKit/TestKitBase_Receive.cs b/src/core/Akka.TestKit/TestKitBase_Receive.cs index fa77e20687e..3e0be70e522 100644 --- a/src/core/Akka.TestKit/TestKitBase_Receive.cs +++ b/src/core/Akka.TestKit/TestKitBase_Receive.cs @@ -259,19 +259,41 @@ public bool TryReceiveOne( else if (maxDuration.IsPositiveFinite()) { ConditionalLog(shouldLog, "Trying to receive message from TestActor queue within {0}", maxDuration); - var delayTask = Task.Delay(maxDuration, cancellationToken); - var readTask = _testState.Queue.Reader.WaitToReadAsync(cancellationToken).AsTask(); - var completedTask = await Task.WhenAny(readTask, delayTask); - - if (completedTask == readTask && readTask.Result) + + try { - // Data is available within the timeout. - var didTake = _testState.Queue.Reader.TryRead(out var item); - take = (didTake, item); + // Create timeout task + using var cts = new CancellationTokenSource(maxDuration); + + // Combine with user cancellation token if provided + using var linkedCts = cancellationToken.CanBeCanceled + ? CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken) + : null; + + var effectiveToken = linkedCts?.Token ?? cts.Token; + + // This will throw OperationCanceledException if cancelled + var canRead = await _testState.Queue.Reader.WaitToReadAsync(effectiveToken); + if (canRead) + { + // Data is available within the timeout. + var didTake = _testState.Queue.Reader.TryRead(out var item); + take = (didTake, item); + } + else + { + // Channel was completed + take = (false, null); + } } - else + catch (OperationCanceledException) when (cancellationToken.CanBeCanceled && cancellationToken.IsCancellationRequested) { - // Timeout occurred before data was available. + // User cancellation - let it propagate + throw; + } + catch (OperationCanceledException) + { + // This was a timeout - return false take = (false, null); } } diff --git a/src/core/Akka.Tests/Actor/ActorSystemSpec.cs b/src/core/Akka.Tests/Actor/ActorSystemSpec.cs index 3537e1b3bae..7ef340fa1c9 100644 --- a/src/core/Akka.Tests/Actor/ActorSystemSpec.cs +++ b/src/core/Akka.Tests/Actor/ActorSystemSpec.cs @@ -223,7 +223,7 @@ public async Task Reliably_create_waves_of_actors() await waves.AwaitWithTimeout(timeout.Duration() + TimeSpan.FromSeconds(5)); - Assert.Equal(new[] { "done", "done", "done" }, waves.Result); + Assert.Equal(new[] { "done", "done", "done" }, await waves); } [Fact] diff --git a/src/core/Akka.Tests/Actor/AskSpec.cs b/src/core/Akka.Tests/Actor/AskSpec.cs index eabb74ba5c4..82865afae8a 100644 --- a/src/core/Akka.Tests/Actor/AskSpec.cs +++ b/src/core/Akka.Tests/Actor/AskSpec.cs @@ -282,6 +282,47 @@ public async Task Bugfix5204_should_allow_null_response_without_error() resp.Should().BeNullOrEmpty(); } + /// + /// Reproduction for https://github.com/akkadotnet/akka.net/issues/7254 + /// + [Fact] + public async Task Bugfix7254_should_throw_error_when_expecting_object_type() + { + const string textExceptionMessage = "THIS IS TEST"; + + var actor = Sys.ActorOf(act => act.ReceiveAny((_, context) => + { + context.Sender.Tell(new Status.Failure(new Exception(textExceptionMessage))); + })); + + var ex = await Assert.ThrowsAsync(async () => await actor.Ask("answer")); + ex.Message.ShouldBe(textExceptionMessage); + + ex = await Assert.ThrowsAsync(async () => await actor.Ask("answer")); + ex.Message.ShouldBe(textExceptionMessage); + } + + /// + /// Reproduction for https://github.com/akkadotnet/akka.net/issues/7254 + /// + [Fact] + public async Task Bugfix7254_should_not_throw_error_when_expecting_Status_type() + { + const string textExceptionMessage = "THIS IS TEST"; + + var actor = Sys.ActorOf(act => act.ReceiveAny((_, context) => + { + context.Sender.Tell(new Status.Failure(new Exception(textExceptionMessage))); + })); + + var failure = await actor.Ask("answer"); + failure.Cause.Message.ShouldBe(textExceptionMessage); + + var status = await actor.Ask("answer"); + Assert.IsType(status); + ((Status.Failure)status).Cause.Message.ShouldBe(textExceptionMessage); + } + [Fact] public void AskDoesNotDeadlockWhenWaitForResultInGuiApplication() { diff --git a/src/core/Akka.Tests/Actor/CoordinatedShutdownSpec.cs b/src/core/Akka.Tests/Actor/CoordinatedShutdownSpec.cs index 648913c1c80..97091dc4d5c 100644 --- a/src/core/Akka.Tests/Actor/CoordinatedShutdownSpec.cs +++ b/src/core/Akka.Tests/Actor/CoordinatedShutdownSpec.cs @@ -53,6 +53,7 @@ private List CheckTopologicalSort(Dictionary phases) private class CustomReason : CoordinatedShutdown.Reason { + public override int ExitCode => 999; } private static CoordinatedShutdown.Reason customReason = new CustomReason(); diff --git a/src/core/Akka.Tests/Actor/InboxSpec.cs b/src/core/Akka.Tests/Actor/InboxSpec.cs index 7ae37985bc9..64b159bd696 100644 --- a/src/core/Akka.Tests/Actor/InboxSpec.cs +++ b/src/core/Akka.Tests/Actor/InboxSpec.cs @@ -157,9 +157,7 @@ public void Select_WithClient_should_update_Client_and_copy_the_rest_of_the_prop public async Task Inbox_Receive_will_timeout_gracefully_if_timeout_is_already_expired() { var task = _inbox.ReceiveAsync(TimeSpan.FromSeconds(-1)); - Assert.True(await task.AwaitWithTimeout(TimeSpan.FromMilliseconds(1000)), "Receive did not complete in time."); - Assert.IsType(task.Result); + await Assert.ThrowsAnyAsync(() => task.AwaitWithTimeout(TimeSpan.FromMilliseconds(1000))); } } } - diff --git a/src/core/Akka.Tests/Event/EventStreamSpec.cs b/src/core/Akka.Tests/Event/EventStreamSpec.cs index 94f57c569f1..48610f01b75 100644 --- a/src/core/Akka.Tests/Event/EventStreamSpec.cs +++ b/src/core/Akka.Tests/Event/EventStreamSpec.cs @@ -15,13 +15,16 @@ using Akka.Util.Internal; using Xunit; using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Extensions; +using Xunit.Abstractions; namespace Akka.Tests.Event { public class EventStreamSpec : AkkaSpec { - public EventStreamSpec() - : base(GetConfig()) + public EventStreamSpec(ITestOutputHelper output) + : base(GetConfig(), output) { } @@ -103,18 +106,20 @@ public void Not_allow_null_as_unsubscriber() [Fact] public async Task Be_able_to_log_unhandled_messages() { - using (var system = ActorSystem.Create("EventStreamSpecUnhandled", GetDebugUnhandledMessagesConfig())) + var testKit = new TestKit.Xunit2.TestKit(config: GetDebugUnhandledMessagesConfig(), output: Output); + try { - system.EventStream.Subscribe(TestActor, typeof(Debug)); - - var msg = new UnhandledMessage(42, system.DeadLetters, system.DeadLetters); - - system.EventStream.Publish(msg); - - var debugMsg = await ExpectMsgAsync(); - - debugMsg.Message.ToString().StartsWith("Unhandled message from").ShouldBeTrue(); - debugMsg.Message.ToString().EndsWith(": 42").ShouldBeTrue(); + var msg = new UnhandledMessage(42, testKit.Sys.DeadLetters, testKit.Sys.DeadLetters); + await testKit.EventFilter.Debug(start: "Unhandled message from", contains: "42") + .ExpectAsync(1, () => + { + testKit.Sys.EventStream.Publish(msg); + return Task.CompletedTask; + }); + } + finally + { + testKit.Shutdown(); } } @@ -124,19 +129,21 @@ public async Task Be_able_to_log_unhandled_messages() [Fact] public async Task Bugfix3267_able_to_log_unhandled_messages_with_nosender() { - using (var system = ActorSystem.Create("EventStreamSpecUnhandled", GetDebugUnhandledMessagesConfig())) + var testKit = new TestKit.Xunit2.TestKit(config: GetDebugUnhandledMessagesConfig(), output: Output); + try { - system.EventStream.Subscribe(TestActor, typeof(Debug)); - // sender is NoSender - var msg = new UnhandledMessage(42, ActorRefs.NoSender, system.DeadLetters); - - system.EventStream.Publish(msg); - - var debugMsg = await ExpectMsgAsync(); - - debugMsg.Message.ToString().StartsWith("Unhandled message from").ShouldBeTrue(); - debugMsg.Message.ToString().EndsWith(": 42").ShouldBeTrue(); + var msg = new UnhandledMessage(42, ActorRefs.NoSender, testKit.Sys.DeadLetters); + await testKit.EventFilter.Debug(start: "Unhandled message from", contains: "42") + .ExpectAsync(1, () => + { + testKit.Sys.EventStream.Publish(msg); + return Task.CompletedTask; + }); + } + finally + { + testKit.Shutdown(); } } @@ -300,9 +307,7 @@ private static string GetDebugUnhandledMessagesConfig() log-dead-letters = off stdout-loglevel = DEBUG loglevel = DEBUG - loggers = [""%logger%""] - } - ".Replace("%logger%", typeof(MyLog).AssemblyQualifiedName); + }"; } public class MyLog : ReceiveActor diff --git a/src/core/Akka.Tests/Util/MessageBufferSpec.cs b/src/core/Akka.Tests/Util/MessageBufferSpec.cs index 642fb13fdd7..32eae92e9a5 100644 --- a/src/core/Akka.Tests/Util/MessageBufferSpec.cs +++ b/src/core/Akka.Tests/Util/MessageBufferSpec.cs @@ -39,7 +39,7 @@ public override string ToString() private IActorRef String2ActorRef(string s) { - return new DummyActorRef(s); ; + return new DummyActorRef(s); } [Fact] diff --git a/src/core/Akka/Actor/ActorRef.cs b/src/core/Akka/Actor/ActorRef.cs index cf1aab6bd76..f55b6f0e82c 100644 --- a/src/core/Akka/Actor/ActorRef.cs +++ b/src/core/Akka/Actor/ActorRef.cs @@ -139,23 +139,26 @@ protected override void TellInternal(object message, IActorRef sender) switch (message) { - case T t: - handled = _result.TrySetResult(t); + case ISystemMessage msg: + handled = _result.TrySetException(new InvalidOperationException($"system message of type '{msg.GetType().Name}' is invalid for {nameof(FutureActorRef)}")); break; - case null: - handled = _result.TrySetResult(default); - break; - case Status.Failure f: + case Status.Failure f when !typeof(Status).IsAssignableFrom(typeof(T)): handled = _result.TrySetException(f.Cause ?? new TaskCanceledException("Task cancelled by actor via Failure message.")); break; #pragma warning disable CS0618 - // for backwards compatibility - case Failure f: + // for backwards compatibility, remove in v1.6 + case Failure f when !typeof(Failure).IsAssignableFrom(typeof(T)): handled = _result.TrySetException(f.Exception ?? new TaskCanceledException("Task cancelled by actor via Failure message.")); #pragma warning restore CS0618 break; + case T t: + handled = _result.TrySetResult(t); + break; + case null: + handled = _result.TrySetResult(default); + break; default: _ = _result.TrySetException(new ArgumentException( $"Received message of type [{message.GetType()}] - Ask expected message of type [{typeof(T)}]")); @@ -166,7 +169,7 @@ protected override void TellInternal(object message, IActorRef sender) if (!handled && !_result.Task.IsCanceled) _provider.DeadLetters.Tell(message ?? default(T), this); } - + public override void SendSystemMessage(ISystemMessage message) { if (message is Watch watch) @@ -1011,7 +1014,10 @@ public FunctionRef(ActorPath path, IActorRefProvider provider, EventStream event public override ActorPath Path { get; } public override IActorRefProvider Provider { get; } + [Obsolete("Use Context.Watch and Receive [1.1.0]")] +#pragma warning disable CS0809 public override bool IsTerminated => Volatile.Read(ref _watchedBy) == null; +#pragma warning restore CS0809 /// /// Have this FunctionRef watch the given Actor. This method must not be diff --git a/src/core/Akka/Actor/CoordinatedShutdown.cs b/src/core/Akka/Actor/CoordinatedShutdown.cs index 403e92bac8f..f0008de8981 100644 --- a/src/core/Akka/Actor/CoordinatedShutdown.cs +++ b/src/core/Akka/Actor/CoordinatedShutdown.cs @@ -156,7 +156,23 @@ public static CoordinatedShutdown Get(ActorSystem sys) public const string PhaseBeforeActorSystemTerminate = "before-actor-system-terminate"; public const string PhaseActorSystemTerminate = "actor-system-terminate"; - + /// + /// Common exit codes supported out of the box. + /// Note: When adding new exit codes, make sure that the exit code adheres + /// to the Linux standard. + /// See: https://manpages.ubuntu.com/manpages/lunar/man3/sysexits.h.3head.html + /// See: https://manpages.ubuntu.com/manpages/noble/man3/EXIT_SUCCESS.3const.html + /// + internal enum CommonExitCodes + { + Ok = 0, + UnknownReason = 1, + // Exit code 2 is reserved for Linux Bash for "Incorrect Usage" + ClusterDowned = 3, + ClusterJoinFailed = 4, + // Exit codes 64-78 is reserved by Linux sysexits.h + // Exit codes 126 and above is reserved by Linux shell + } /// /// Reason for the shutdown, which can be used by tasks in case they need to do @@ -164,8 +180,9 @@ public static CoordinatedShutdown Get(ActorSystem sys) /// predefined reasons, but external libraries applications may also define /// other reasons. /// - public class Reason + public abstract class Reason { + public abstract int ExitCode { get; } protected Reason() { @@ -179,6 +196,8 @@ public class UnknownReason : Reason { public static readonly Reason Instance = new UnknownReason(); + public override int ExitCode => (int)CommonExitCodes.UnknownReason; + private UnknownReason() { @@ -192,6 +211,8 @@ public class ActorSystemTerminateReason : Reason { public static readonly Reason Instance = new ActorSystemTerminateReason(); + public override int ExitCode => (int)CommonExitCodes.Ok; + private ActorSystemTerminateReason() { @@ -205,6 +226,8 @@ public class ClrExitReason : Reason { public static readonly Reason Instance = new ClrExitReason(); + public override int ExitCode => (int)CommonExitCodes.Ok; + private ClrExitReason() { @@ -219,6 +242,8 @@ public class ClusterDowningReason : Reason { public static readonly Reason Instance = new ClusterDowningReason(); + public override int ExitCode => (int)CommonExitCodes.ClusterDowned; + private ClusterDowningReason() { @@ -233,6 +258,8 @@ public class ClusterLeavingReason : Reason { public static readonly Reason Instance = new ClusterLeavingReason(); + public override int ExitCode => (int)CommonExitCodes.Ok; + private ClusterLeavingReason() { @@ -245,6 +272,9 @@ private ClusterLeavingReason() public class ClusterJoinUnsuccessfulReason : Reason { public static readonly Reason Instance = new ClusterJoinUnsuccessfulReason(); + + public override int ExitCode => (int)CommonExitCodes.ClusterJoinFailed; + private ClusterJoinUnsuccessfulReason() { } } @@ -646,7 +676,7 @@ internal static void InitPhaseActorSystemTerminate(ActorSystem system, Config co { if (!system.WhenTerminated.Wait(timeout) && !coord._runningClrHook) { - Environment.Exit(0); + Environment.Exit(coord.ShutdownReason?.ExitCode ?? 0); } }); } @@ -665,7 +695,7 @@ internal static void InitPhaseActorSystemTerminate(ActorSystem system, Config co } else if (exitClr) { - Environment.Exit(0); + Environment.Exit(coord.ShutdownReason?.ExitCode ?? 0); return TaskEx.Completed; } else diff --git a/src/core/Akka/Actor/Scheduler/IScheduledTellMsg.cs b/src/core/Akka/Actor/Scheduler/IScheduledTellMsg.cs index 4001aa1fb90..3f4666ed7d3 100644 --- a/src/core/Akka/Actor/Scheduler/IScheduledTellMsg.cs +++ b/src/core/Akka/Actor/Scheduler/IScheduledTellMsg.cs @@ -31,6 +31,9 @@ public ScheduledTellMsg(object message) Message = message; } public object Message { get; } + + public override string ToString() + => $"{{{nameof(ScheduledTellMsg)}: {{Message: {Message}}}}}"; } /// @@ -44,4 +47,7 @@ public ScheduledTellMsgNoInfluenceReceiveTimeout(object message) } public object Message { get; } + + public override string ToString() + => $"{{{nameof(ScheduledTellMsgNoInfluenceReceiveTimeout)}: {{Message: {Message}}}}}"; } diff --git a/src/core/Akka/Actor/Settings.cs b/src/core/Akka/Actor/Settings.cs index 985983add5b..120663f5dae 100644 --- a/src/core/Akka/Actor/Settings.cs +++ b/src/core/Akka/Actor/Settings.cs @@ -13,6 +13,7 @@ using Akka.Dispatch; using Akka.Event; using Akka.Routing; +using Akka.Util; using ConfigurationFactory = Akka.Configuration.ConfigurationFactory; namespace Akka.Actor @@ -27,7 +28,7 @@ public class Settings { private readonly Config _userConfig; //internal static readonly Config AkkaDllConfig = ConfigurationFactory.FromResource("Akka.Configuration.Pigeon.conf"); - private Config _fallbackConfig; + private readonly AtomicReference _fallbackConfig; private void RebuildConfig() { @@ -42,13 +43,20 @@ private void RebuildConfig() /// /// Injects a system config at the top of the fallback chain /// - /// TBD + /// The latest config to be added to the front of the fallback chain public void InjectTopLevelFallback(Config config) { if (Config.Contains(config)) return; - _fallbackConfig = config.SafeWithFallback(_fallbackConfig); + while(true) + { + var oldConfig = _fallbackConfig.Value; + var newConfig = config.SafeWithFallback(oldConfig); + if (_fallbackConfig.CompareAndSet(oldConfig, newConfig)) + break; + } + RebuildConfig(); } @@ -77,7 +85,7 @@ public Settings(ActorSystem system, Config config, ActorSystemSetup setup) { Setup = setup; _userConfig = config; - _fallbackConfig = ConfigurationFactory.Default(); + _fallbackConfig = new AtomicReference(ConfigurationFactory.Default()); RebuildConfig(); System = system; diff --git a/src/core/Akka/Actor/SupervisorStrategy.cs b/src/core/Akka/Actor/SupervisorStrategy.cs index 84ebbcabde0..ce5df336e29 100644 --- a/src/core/Akka/Actor/SupervisorStrategy.cs +++ b/src/core/Akka/Actor/SupervisorStrategy.cs @@ -732,7 +732,7 @@ public override int GetHashCode() /// /// Collection of failures, used to keep track of how many times a given actor has failed. /// - [Obsolete("Use List of Akka.Actor.Status.Failure")] + [Obsolete("Use List of Akka.Actor.Status.Failure. Will be removed in v1.6")] public class Failures { /// @@ -752,7 +752,7 @@ public Failures() /// /// Represents a single failure. /// - [Obsolete("Use Akka.Actor.Status.Failure")] + [Obsolete("Use Akka.Actor.Status.Failure. Will be removed in v1.6")] public class Failure { /// diff --git a/src/core/Akka/Delivery/ConsumerController.cs b/src/core/Akka/Delivery/ConsumerController.cs index 9cd976b185b..8409d7ec4d1 100644 --- a/src/core/Akka/Delivery/ConsumerController.cs +++ b/src/core/Akka/Delivery/ConsumerController.cs @@ -43,7 +43,13 @@ public static Props Create(IActorRefFactory actorRefFactory, Option(IActorRefFactory actorRefFactory, Option producerControllerReference, Func fuzzing, Settings? settings = null) + /// + /// INTERNAL API + /// + /// This method should only be used for testing purposes + /// + [InternalApi] + public static Props CreateWithFuzzing(IActorRefFactory actorRefFactory, Option producerControllerReference, Func fuzzing, Settings? settings = null) { Props p; switch (actorRefFactory) diff --git a/src/core/Akka/Delivery/ProducerController.cs b/src/core/Akka/Delivery/ProducerController.cs index 55954bc8baf..acc2ca6443f 100644 --- a/src/core/Akka/Delivery/ProducerController.cs +++ b/src/core/Akka/Delivery/ProducerController.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Akka.Actor; +using Akka.Annotations; using Akka.Configuration; using Akka.Delivery.Internal; using Akka.Event; @@ -47,7 +48,13 @@ public static Props Create(IActorRefFactory actorRefFactory, string producerI return p; } - internal static Props CreateWithFuzzing(IActorRefFactory actorRefFactory, string producerId, + /// + /// INTERNAL API + /// + /// This method should only be used for testing purposes + /// + [InternalApi] + public static Props CreateWithFuzzing(IActorRefFactory actorRefFactory, string producerId, Func fuzzing, Option durableProducerQueue, Settings? settings = null, Action>? sendAdapter = null) diff --git a/src/core/Akka/GlobalUsings.cs b/src/core/Akka/GlobalUsings.cs new file mode 100644 index 00000000000..7ea9ec9b26e --- /dev/null +++ b/src/core/Akka/GlobalUsings.cs @@ -0,0 +1,8 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +// ----------------------------------------------------------------------- + +global using ByteBuffer = System.ArraySegment; \ No newline at end of file diff --git a/src/core/Akka/IO/Buffers/DirectBufferPool.cs b/src/core/Akka/IO/Buffers/DirectBufferPool.cs index 3b7b3dc30da..bba7d68a663 100644 --- a/src/core/Akka/IO/Buffers/DirectBufferPool.cs +++ b/src/core/Akka/IO/Buffers/DirectBufferPool.cs @@ -17,8 +17,6 @@ namespace Akka.IO.Buffers { - using ByteBuffer = ArraySegment; - public class BufferPoolAllocationException : AkkaException { public BufferPoolAllocationException(string message) : base(message) @@ -68,8 +66,8 @@ public interface IBufferPool /// /// Rents a sequence of byte buffers representing (potentially non-continuous) range of memory - /// that is big enough to fit the requested. Once rent, byte - /// buffers are expected to be released using + /// that is big enough to fit the requested. Once rent, byte + /// buffers are expected to be released using /// method. /// /// diff --git a/src/core/Akka/IO/Buffers/DisabledBufferPool.cs b/src/core/Akka/IO/Buffers/DisabledBufferPool.cs index af42c5faaa7..c9a012ccc12 100644 --- a/src/core/Akka/IO/Buffers/DisabledBufferPool.cs +++ b/src/core/Akka/IO/Buffers/DisabledBufferPool.cs @@ -12,8 +12,6 @@ namespace Akka.IO.Buffers { - using ByteBuffer = ArraySegment; - internal class DisabledBufferPool : IBufferPool { private readonly int _bufferSize; diff --git a/src/core/Akka/IO/Dns.cs b/src/core/Akka/IO/Dns.cs index e907dfdc00e..85a1e23fbc9 100644 --- a/src/core/Akka/IO/Dns.cs +++ b/src/core/Akka/IO/Dns.cs @@ -275,7 +275,7 @@ public override IActorRef Manager { get { - return _manager = _manager ?? _system.SystemActorOf(Props.Create(() => new SimpleDnsManager(this)) + return _manager = _manager ?? _system.SystemActorOf(Props.Create(Provider.ManagerClass, this) .WithDeploy(Deploy.Local) .WithDispatcher(Settings.Dispatcher)); } diff --git a/src/core/Akka/IO/SocketEventArgsPool.cs b/src/core/Akka/IO/SocketEventArgsPool.cs index a09bae0331a..8a7f6462160 100644 --- a/src/core/Akka/IO/SocketEventArgsPool.cs +++ b/src/core/Akka/IO/SocketEventArgsPool.cs @@ -65,7 +65,7 @@ public void Release(SocketAsyncEventArgs e) { if (e.Buffer != null) { - _bufferPool.Release(new ArraySegment(e.Buffer, e.Offset, e.Count)); + _bufferPool.Release(new ByteBuffer(e.Buffer, e.Offset, e.Count)); } if (e.BufferList != null) { diff --git a/src/core/Akka/IO/TcpConnection.cs b/src/core/Akka/IO/TcpConnection.cs index 280c5f274d5..fb5f505e95d 100644 --- a/src/core/Akka/IO/TcpConnection.cs +++ b/src/core/Akka/IO/TcpConnection.cs @@ -22,7 +22,6 @@ namespace Akka.IO { using static Akka.IO.Tcp; - using ByteBuffer = ArraySegment; // A **green‑field** rewrite of the connection actor, distilled to // • 4 stable phases (Connecting ▸ AwaitRegistration ▸ Open ▸ HalfOpen) @@ -60,10 +59,10 @@ namespace Akka.IO /// Every TcpConnection gets assigned a single socket fields and pair of , /// allocated once per lifetime of the connection actor: /// - /// - used only for receiving data. It has assigned buffer, rent from + /// - used only for receiving data. It has assigned buffer, rent from /// once and recycled back upon actor termination. Once data has been received, it's /// copied to a separate object (so it's NOT a zero-copy operation). - /// - used only for sending data. Unlike receive args, it doesn't have any buffer + /// - used only for sending data. Unlike receive args, it doesn't have any buffer /// assigned. Instead it uses treats incoming data as a buffer (it's safe due to immutable nature of /// object). Therefore writes don't allocate any byte buffers. /// @@ -486,7 +485,7 @@ private void HandleSendCompleted(AckSocketAsyncEventArgs ea) { _totalSentBytes += ea.BytesTransferred; Log.Debug("[TcpConnection] completed write of {0}/{1} bytes (queued={2}/{3}) [{4} total sent]", - ea.BytesTransferred, ea.BufferList.Sum(c => c.Count), _state.QueuedBytes, _maxQueuedBytes, + ea.BytesTransferred, ea.BufferList?.Sum(c => c.Count) ?? 0, _state.QueuedBytes, _maxQueuedBytes, _totalSentBytes); } @@ -559,7 +558,7 @@ private void TrySend() if (!_state.CanSend) return; if (_state.IsSending || _pendingWrites.Count == 0) return; - var segs = new List>(8); + var segs = new List(8); var batchBytes = 0; while (_pendingWrites.Count > 0 && batchBytes < Settings.MaxFrameSizeBytes) diff --git a/src/core/Akka/IO/TcpManager.cs b/src/core/Akka/IO/TcpManager.cs index cfd369c34a6..41eabd8d2af 100644 --- a/src/core/Akka/IO/TcpManager.cs +++ b/src/core/Akka/IO/TcpManager.cs @@ -12,7 +12,6 @@ namespace Akka.IO { using static Tcp; - using ByteBuffer = ArraySegment; /// /// INTERNAL API diff --git a/src/core/Akka/IO/Udp.cs b/src/core/Akka/IO/Udp.cs index 48de359b816..ccf8531cd1f 100644 --- a/src/core/Akka/IO/Udp.cs +++ b/src/core/Akka/IO/Udp.cs @@ -18,8 +18,6 @@ namespace Akka.IO { - using ByteBuffer = ArraySegment; - /// /// UDP Extension for Akka's IO layer. /// diff --git a/src/core/Akka/IO/UdpConnected.cs b/src/core/Akka/IO/UdpConnected.cs index 9d88f8d15bc..b2780dc23a9 100644 --- a/src/core/Akka/IO/UdpConnected.cs +++ b/src/core/Akka/IO/UdpConnected.cs @@ -18,8 +18,6 @@ namespace Akka.IO { - using ByteBuffer = ArraySegment; - /// /// UDP Extension for Akka's IO layer. /// diff --git a/src/core/Akka/IO/UdpConnectedManager.cs b/src/core/Akka/IO/UdpConnectedManager.cs index 99308863f33..24162ef84d5 100644 --- a/src/core/Akka/IO/UdpConnectedManager.cs +++ b/src/core/Akka/IO/UdpConnectedManager.cs @@ -11,8 +11,6 @@ namespace Akka.IO { - using ByteBuffer = ArraySegment; - /// /// INTERNAL API /// diff --git a/src/core/Akka/IO/UdpConnection.cs b/src/core/Akka/IO/UdpConnection.cs index d7e54730874..79310e28e4b 100644 --- a/src/core/Akka/IO/UdpConnection.cs +++ b/src/core/Akka/IO/UdpConnection.cs @@ -17,7 +17,6 @@ namespace Akka.IO { using static UdpConnected; - using ByteBuffer = ArraySegment; internal class UdpConnection : ActorBase, IRequiresMessageQueue { diff --git a/src/core/Akka/IO/UdpListener.cs b/src/core/Akka/IO/UdpListener.cs index f8bdad44d7e..665658d83d5 100644 --- a/src/core/Akka/IO/UdpListener.cs +++ b/src/core/Akka/IO/UdpListener.cs @@ -17,7 +17,6 @@ namespace Akka.IO { using static Udp; - using ByteBuffer = ArraySegment; /// /// INTERNAL API diff --git a/src/core/Akka/IO/WithUdpSend.cs b/src/core/Akka/IO/WithUdpSend.cs index aed7f9e38e7..8a740e04652 100644 --- a/src/core/Akka/IO/WithUdpSend.cs +++ b/src/core/Akka/IO/WithUdpSend.cs @@ -14,7 +14,6 @@ namespace Akka.IO { using static Udp; - using ByteBuffer = ArraySegment; abstract class WithUdpSend : ActorBase { diff --git a/src/core/Akka/Pattern/RetrySupport.cs b/src/core/Akka/Pattern/RetrySupport.cs index fd2dea5902e..82c845f4c1c 100644 --- a/src/core/Akka/Pattern/RetrySupport.cs +++ b/src/core/Akka/Pattern/RetrySupport.cs @@ -118,14 +118,12 @@ Task tryAttempt() var nextAttempt = attempted + 1; switch (delayFunction(nextAttempt)) { - case Option delay when delay.HasValue: + case { HasValue: true } delay: return delay.Value.Ticks < 1 ? Retry(attempt, maxAttempts, delayFunction, nextAttempt, scheduler) : After(delay.Value, scheduler, () => Retry(attempt, maxAttempts, delayFunction, nextAttempt, scheduler)); - case Option _: - return Retry(attempt, maxAttempts, delayFunction, nextAttempt, scheduler); default: - throw new InvalidOperationException("The delayFunction of Retry should not return null."); + return Retry(attempt, maxAttempts, delayFunction, nextAttempt, scheduler); } } return t; diff --git a/src/core/Akka/Routing/ResizablePoolCell.cs b/src/core/Akka/Routing/ResizablePoolCell.cs index 24c1ecf81e5..e69def9bc0d 100644 --- a/src/core/Akka/Routing/ResizablePoolCell.cs +++ b/src/core/Akka/Routing/ResizablePoolCell.cs @@ -23,10 +23,12 @@ namespace Akka.Routing internal class ResizablePoolCell : RoutedActorCell { private Resizer resizer; + /// /// must always use ResizeInProgressState static class to compare or assign values /// private AtomicBoolean _resizeInProgress; + private AtomicCounterLong _resizeCounter; private Pool _pool; @@ -53,9 +55,7 @@ public ResizablePoolCell( Pool pool) : base(system, self, routerProps, dispatcher, routeeProps, supervisor) { - if (pool.Resizer == null) throw new ArgumentException("RouterConfig must be a Pool with defined resizer", nameof(pool)); - - resizer = pool.Resizer; + resizer = pool.Resizer ?? throw new ArgumentException("RouterConfig must be a Pool with defined resizer", nameof(pool)); _pool = pool; _resizeCounter = new AtomicCounterLong(0); _resizeInProgress = new AtomicBoolean(); @@ -79,7 +79,7 @@ protected override void PreSuperStart() /// TBD public override void SendMessage(Envelope envelope) { - if(!(RouterConfig.IsManagementMessage(envelope.Message)) && + if (!(RouterConfig.IsManagementMessage(envelope.Message)) && resizer.IsTimeForResize(_resizeCounter.GetAndIncrement()) && _resizeInProgress.CompareAndSet(false, true)) { @@ -123,4 +123,4 @@ internal void Resize(bool initial) } } } -} +} \ No newline at end of file diff --git a/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs b/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs index 990822a93c5..3a1239dd2d6 100644 --- a/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs +++ b/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs @@ -126,12 +126,9 @@ private static IEnumerable GetConverterTypes(Config config) /// Max retained size used for pooled string builders if enabled public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjectReferences, IEnumerable converters, bool usePooledStringBuilder, int stringBuilderMinSize, int stringBuilderMaxSize) { - if (converters == null) - throw new ArgumentNullException(nameof(converters), $"{nameof(NewtonSoftJsonSerializerSettings)} requires a sequence of converters."); - EncodeTypeNames = encodeTypeNames; PreserveObjectReferences = preserveObjectReferences; - Converters = converters; + Converters = converters ?? throw new ArgumentNullException(nameof(converters), $"{nameof(NewtonSoftJsonSerializerSettings)} requires a sequence of converters."); UsePooledStringBuilder = usePooledStringBuilder; StringBuilderMinSize = stringBuilderMinSize; StringBuilderMaxSize = stringBuilderMaxSize; diff --git a/src/core/Akka/Util/ByteString.cs b/src/core/Akka/Util/ByteString.cs index 46436cfc747..2d3643bda5c 100644 --- a/src/core/Akka/Util/ByteString.cs +++ b/src/core/Akka/Util/ByteString.cs @@ -17,8 +17,6 @@ namespace Akka.IO { // TODO: Move to Akka.Util namespace - this will require changes as name clashes with ProtoBuf class - using ByteBuffer = ArraySegment; - /// /// A rope-like immutable data structure containing bytes. /// The goal of this structure is to reduce copying of arrays @@ -173,7 +171,7 @@ public static ByteString CopyFrom(IEnumerable buffers) /// /// TBD /// TBD - public static ByteString FromBytes(ArraySegment buffer) => + public static ByteString FromBytes(ByteBuffer buffer) => FromBytes(buffer.Array, buffer.Offset, buffer.Count); /// diff --git a/src/core/Akka/Util/Internal/Collections/ListExtensions.cs b/src/core/Akka/Util/Internal/Collections/ListExtensions.cs index 032bfa7772e..de533f80a05 100644 --- a/src/core/Akka/Util/Internal/Collections/ListExtensions.cs +++ b/src/core/Akka/Util/Internal/Collections/ListExtensions.cs @@ -21,9 +21,7 @@ public static List Shuffle(this List @this) { int index = r.Next(i); //swap - var tmp = list[index]; - list[index] = list[i]; - list[i] = tmp; + (list[index], list[i]) = (list[i], list[index]); } return list; } @@ -38,9 +36,7 @@ public static IImmutableList Shuffle(this IImmutableList @this) { int index = r.Next(i); //swap - var tmp = list[index]; - list[index] = list[i]; - list[i] = tmp; + (list[index], list[i]) = (list[i], list[index]); } return list.ToImmutable(); } diff --git a/src/core/Akka/Util/Internal/Collections/ListSlice.cs b/src/core/Akka/Util/Internal/Collections/ListSlice.cs index eff4af8f65a..c63cd23e72e 100644 --- a/src/core/Akka/Util/Internal/Collections/ListSlice.cs +++ b/src/core/Akka/Util/Internal/Collections/ListSlice.cs @@ -81,33 +81,26 @@ public T Current public void Dispose() { - } } - + private readonly IReadOnlyList _array; public ListSlice(IReadOnlyList array) { - - if (array == null) - throw new ArgumentNullException(nameof(array)); - - _array = array; + _array = array ?? throw new ArgumentNullException(nameof(array)); Offset = 0; Count = array.Count; } - + public ListSlice(IReadOnlyList array, int offset, int count) { - if (array == null) - throw new ArgumentNullException(nameof(array)); if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset), "Cannot be below zero."); if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), "Cannot be below zero."); - - _array = array; + + _array = array ?? throw new ArgumentNullException(nameof(array)); Offset = offset; Count = count; } diff --git a/src/core/Akka/Util/ListPriorityQueue.cs b/src/core/Akka/Util/ListPriorityQueue.cs index 5893fe3661d..3e3fdef11ca 100644 --- a/src/core/Akka/Util/ListPriorityQueue.cs +++ b/src/core/Akka/Util/ListPriorityQueue.cs @@ -50,7 +50,7 @@ public void Enqueue(Envelope item) { var pi = (ci - 1) / 2; // parent index if (_priorityCalculator(_data[ci].Message).CompareTo(_priorityCalculator(_data[pi].Message)) >= 0) break; // child item is larger than (or equal) parent so we're done - var tmp = _data[ci]; _data[ci] = _data[pi]; _data[pi] = tmp; + (_data[ci], _data[pi]) = (_data[pi], _data[ci]); ci = pi; } } @@ -77,7 +77,7 @@ public Envelope Dequeue() if (rc <= li && _priorityCalculator(_data[rc].Message).CompareTo(_priorityCalculator(_data[ci].Message)) < 0) // if there is a rc (ci + 1), and it is smaller than left child, use the rc instead ci = rc; if (_priorityCalculator(_data[pi].Message).CompareTo(_priorityCalculator(_data[ci].Message)) <= 0) break; // parent is smaller than (or equal to) smallest child so done - var tmp = _data[pi]; _data[pi] = _data[ci]; _data[ci] = tmp; // swap parent and child + (_data[pi], _data[ci]) = (_data[ci], _data[pi]); // swap parent and child pi = ci; } return frontItem; diff --git a/src/core/Akka/Util/MatchHandler/MatchBuilder.cs b/src/core/Akka/Util/MatchHandler/MatchBuilder.cs index 032c2f8d336..b6f011f6ea5 100644 --- a/src/core/Akka/Util/MatchHandler/MatchBuilder.cs +++ b/src/core/Akka/Util/MatchHandler/MatchBuilder.cs @@ -52,11 +52,8 @@ internal class MatchBuilder /// /// This exception is thrown if the given is undefined. /// - public MatchBuilder(IMatchCompiler compiler) - { - if(compiler == null) throw new ArgumentNullException(nameof(compiler), "Compiler cannot be null"); - _compiler = compiler; - } + public MatchBuilder(IMatchCompiler compiler) => + _compiler = compiler ?? throw new ArgumentNullException(nameof(compiler), "Compiler cannot be null"); /// /// Adds a handler that is called if the item being matched is of type diff --git a/src/core/Akka/Util/MatchHandler/TypeHandler.cs b/src/core/Akka/Util/MatchHandler/TypeHandler.cs index 893d218b1ed..8b28a13a296 100644 --- a/src/core/Akka/Util/MatchHandler/TypeHandler.cs +++ b/src/core/Akka/Util/MatchHandler/TypeHandler.cs @@ -26,11 +26,8 @@ internal class TypeHandler /// /// This exception is thrown if the given is undefined. /// - public TypeHandler(Type handlesType) - { - if(handlesType == null) throw new ArgumentNullException(nameof(handlesType), "Type cannot be null"); - _handlesType = handlesType; - } + public TypeHandler(Type handlesType) => + _handlesType = handlesType ?? throw new ArgumentNullException(nameof(handlesType), "Type cannot be null"); /// /// TBD diff --git a/src/core/Akka/Util/StableListPriorityQueue.cs b/src/core/Akka/Util/StableListPriorityQueue.cs index 61baae15612..8166bdfb8fa 100644 --- a/src/core/Akka/Util/StableListPriorityQueue.cs +++ b/src/core/Akka/Util/StableListPriorityQueue.cs @@ -84,7 +84,7 @@ public void Enqueue(Envelope item) { var pi = (ci - 1) / 2; // parent index if (comparator.Compare(_data[ci], _data[pi]) >= 0) break; // child item is larger than (or equal) parent so we're done - var tmp = _data[ci]; _data[ci] = _data[pi]; _data[pi] = tmp; + (_data[ci], _data[pi]) = (_data[pi], _data[ci]); ci = pi; } } @@ -111,7 +111,7 @@ public Envelope Dequeue() if (rc <= li && comparator.Compare(_data[rc], _data[ci]) < 0) // if there is a rc (ci + 1), and it is smaller than left child, use the rc instead ci = rc; if (comparator.Compare(_data[pi], _data[ci]) <= 0) break; // parent is smaller than (or equal to) smallest child so done - var tmp = _data[pi]; _data[pi] = _data[ci]; _data[ci] = tmp; // swap parent and child + (_data[pi], _data[ci]) = (_data[ci], _data[pi]); // swap parent and child pi = ci; } return frontItem.Envelope; diff --git a/src/examples/Akka.Persistence.Custom/Settings.cs b/src/examples/Akka.Persistence.Custom/Settings.cs index 1218a71bd71..1db15fd8fc4 100644 --- a/src/examples/Akka.Persistence.Custom/Settings.cs +++ b/src/examples/Akka.Persistence.Custom/Settings.cs @@ -89,7 +89,9 @@ public SnapshotStoreSettings(Config config) ConnectionString = config.GetString("connection-string"); ConnectionTimeout = config.GetTimeSpan("connection-timeout"); AutoInitialize = config.GetBoolean("auto-initialize"); +#pragma warning disable CS0618 // Type or member is obsolete DefaultSerializer = config.GetString("serializer", null); +#pragma warning restore CS0618 // Type or member is obsolete } } } diff --git a/src/examples/AspNetCore/Akka.AspNetCore/AkkaService.cs b/src/examples/AspNetCore/Akka.AspNetCore/AkkaService.cs index a5a994d054f..47ec5df768b 100644 --- a/src/examples/AspNetCore/Akka.AspNetCore/AkkaService.cs +++ b/src/examples/AspNetCore/Akka.AspNetCore/AkkaService.cs @@ -14,10 +14,10 @@ namespace Akka.AspNetCore { public class AkkaService : IHostedService, IActorBridge { - private ActorSystem _actorSystem; + private ActorSystem? _actorSystem; private readonly IConfiguration _configuration; private readonly IServiceProvider _serviceProvider; - private IActorRef _actorRef; + private IActorRef? _actorRef; private readonly IHostApplicationLifetime _applicationLifetime; diff --git a/src/examples/Cluster/PublishSubscribe/SamplePublisher/PublisherWithAck.cs b/src/examples/Cluster/PublishSubscribe/SamplePublisher/PublisherWithAck.cs index c23c12f8730..f4a8b6b3dfb 100644 --- a/src/examples/Cluster/PublishSubscribe/SamplePublisher/PublisherWithAck.cs +++ b/src/examples/Cluster/PublishSubscribe/SamplePublisher/PublisherWithAck.cs @@ -20,7 +20,7 @@ public PublisherWithAck() var mediator = DistributedPubSub.Get(Context.System).Mediator; Receive(input => mediator.Tell( - new PublishWithAck("content", input.ToUpperInvariant(), TimeSpan.FromSeconds(30)))); + new PublishWithAck("content", input.ToUpperInvariant(), TimeSpan.FromMinutes(30)))); Receive(success => log.Info( "Published {0} to topic {1}.", success.Message.Message, success.Message.Topic)); diff --git a/src/examples/HeadlessService/AkkaHeadlesssService/AkkaService.cs b/src/examples/HeadlessService/AkkaHeadlesssService/AkkaService.cs index d2d132151eb..483c6e417c4 100644 --- a/src/examples/HeadlessService/AkkaHeadlesssService/AkkaService.cs +++ b/src/examples/HeadlessService/AkkaHeadlesssService/AkkaService.cs @@ -15,8 +15,8 @@ namespace AkkaHeadlesssService { public sealed class AkkaService : IHostedService { - private IActorRef _actorRef; - private ActorSystem _actorSystem; + private IActorRef? _actorRef; + private ActorSystem? _actorSystem; private readonly IServiceProvider _serviceProvider; private readonly IHostApplicationLifetime _applicationLifetime;