From d135f57c63035e160fd80d09b349ee484ab04455 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 12 Apr 2017 19:21:10 -0700 Subject: [PATCH 01/70] Deploy Akka.Cluster.Sharding and LightningDB package fixes (#2611) * switch akka.cluster.sharding dependency for akka.cluster.tools from prerelease to nuget release version (#2608) * Add LightningDB nuspec (#2609) * added Akka.DistributedData.LightningDB nuspec * updated assembly information --- build.fsx | 6 ++++++ src/SharedAssemblyInfo.cs | 8 ++++++++ .../Akka.DistributedData.LightningDB.csproj | 3 +++ .../Akka.DistributedData.LightningDB.nuspec | 20 +++++++++++++++++++ .../Akka.FSharp/Properties/AssemblyInfo.fs | 18 +++++++++++++++++ 5 files changed, 55 insertions(+) create mode 100644 src/SharedAssemblyInfo.cs create mode 100644 src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.nuspec create mode 100644 src/core/Akka.FSharp/Properties/AssemblyInfo.fs diff --git a/build.fsx b/build.fsx index 7b0e76866c1..56378203e8c 100644 --- a/build.fsx +++ b/build.fsx @@ -356,6 +356,12 @@ Target "PublishMntr" (fun _ -> VersionSuffix = versionSuffix })) ) +//-------------------------------------------------------------------------------- +// Clean nuget directory + +Target "CleanNuget" <| fun _ -> + CleanDir nugetDir + Target "CreateMntrNuget" (fun _ -> // uses the template file to create a temporary .nuspec file with the correct version CopyFile "./src/core/Akka.MultiNodeTestRunner/Akka.MultiNodeTestRunner.nuspec" "./src/core/Akka.MultiNodeTestRunner/Akka.MultiNodeTestRunner.nuspec.template" diff --git a/src/SharedAssemblyInfo.cs b/src/SharedAssemblyInfo.cs new file mode 100644 index 00000000000..7f3614bf7ca --- /dev/null +++ b/src/SharedAssemblyInfo.cs @@ -0,0 +1,8 @@ +// +using System.Reflection; + +[assembly: AssemblyCompanyAttribute("Akka.NET Team")] +[assembly: AssemblyCopyrightAttribute("Copyright © 2013-2017 Akka.NET Team")] +[assembly: AssemblyTrademarkAttribute("")] +[assembly: AssemblyVersionAttribute("1.2.0.0")] +[assembly: AssemblyFileVersionAttribute("1.2.0.0")] diff --git a/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.csproj b/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.csproj index ab5b75aafda..f26984e24a5 100644 --- a/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.csproj +++ b/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.csproj @@ -18,6 +18,9 @@ + + + diff --git a/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.nuspec b/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.nuspec new file mode 100644 index 00000000000..aecdde5980d --- /dev/null +++ b/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.nuspec @@ -0,0 +1,20 @@ + + + + @project@ + @project@@title@ + @build.number@ + @authors@ + @authors@ + Replicated data using CRDT structures + https://github.com/akkadotnet/akka.net/blob/master/LICENSE + https://github.com/akkadotnet/akka.net + http://getakka.net/images/AkkaNetLogo.Normal.png + false + @releaseNotes@ + @copyright@ + @tags@ cluster crdt replication lightningdb + @dependencies@ + @references@ + + diff --git a/src/core/Akka.FSharp/Properties/AssemblyInfo.fs b/src/core/Akka.FSharp/Properties/AssemblyInfo.fs new file mode 100644 index 00000000000..4f81d0454f3 --- /dev/null +++ b/src/core/Akka.FSharp/Properties/AssemblyInfo.fs @@ -0,0 +1,18 @@ +namespace System +open System +open System.Reflection +open System.Runtime.InteropServices + +[] +[] +[] +[] +[] +[] +[] +[] +[] +do () + +module internal AssemblyVersionInformation = + let [] Version = "1.2.0.0" From 3cc6386300735571a38aa63b05113dbd8343eebd Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Tue, 13 Feb 2018 23:07:22 +0100 Subject: [PATCH 02/70] initial commit: FunctionRef --- src/core/Akka.Tests/Actor/FunctionRefSpec.cs | 253 ++++++++++++++++++ src/core/Akka.Tests/Akka.Tests.csproj | 9 - src/core/Akka/Actor/ActorCell.Children.cs | 72 ++++- .../Akka/Actor/ActorCell.FaultHandling.cs | 20 +- src/core/Akka/Actor/ActorRef.cs | 200 ++++++++++++++ src/core/Akka/Actor/IAutoReceivedMessage.cs | 25 +- src/core/Akka/Actor/RepointableActorRef.cs | 7 +- src/core/Akka/Util/Base64Encoding.cs | 12 +- 8 files changed, 560 insertions(+), 38 deletions(-) create mode 100644 src/core/Akka.Tests/Actor/FunctionRefSpec.cs diff --git a/src/core/Akka.Tests/Actor/FunctionRefSpec.cs b/src/core/Akka.Tests/Actor/FunctionRefSpec.cs new file mode 100644 index 00000000000..f860a0b7f78 --- /dev/null +++ b/src/core/Akka.Tests/Actor/FunctionRefSpec.cs @@ -0,0 +1,253 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2018 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.Configuration; +using Akka.TestKit; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Tests.Actor +{ + public class FunctionRefSpec : AkkaSpec + { + #region internal classes + + sealed class GetForwarder : IEquatable + { + public IActorRef ReplyTo { get; } + + public GetForwarder(IActorRef replyTo) + { + ReplyTo = replyTo; + } + + public bool Equals(GetForwarder other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(ReplyTo, other.ReplyTo); + } + + public override bool Equals(object obj) => obj is GetForwarder forwarder && Equals(forwarder); + + public override int GetHashCode() => (ReplyTo != null ? ReplyTo.GetHashCode() : 0); + } + + sealed class DropForwarder : IEquatable + { + public FunctionRef Ref { get; } + + public DropForwarder(FunctionRef @ref) + { + Ref = @ref; + } + + public bool Equals(DropForwarder other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(Ref, other.Ref); + } + + public override bool Equals(object obj) => obj is DropForwarder forwarder && Equals(forwarder); + + public override int GetHashCode() => (Ref != null ? Ref.GetHashCode() : 0); + } + + sealed class Forwarded : IEquatable + { + public object Message { get; } + public IActorRef Sender { get; } + + public Forwarded(object message, IActorRef sender) + { + Message = message; + Sender = sender; + } + + public bool Equals(Forwarded other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(Message, other.Message) && Equals(Sender, other.Sender); + } + + public override bool Equals(object obj) => obj is Forwarded forwarded && Equals(forwarded); + + public override int GetHashCode() + { + unchecked + { + return ((Message != null ? Message.GetHashCode() : 0) * 397) ^ (Sender != null ? Sender.GetHashCode() : 0); + } + } + } + + sealed class Super : ReceiveActor + { + public Super() + { + Receive(get => + { + var cell = (ActorCell)Context; + var fref = cell.AddFunctionRef((sender, msg) => + { + get.ReplyTo.Tell(new Forwarded(msg, sender)); + }); + get.ReplyTo.Tell(fref); + }); + Receive(drop => { + var cell = (ActorCell)Context; + cell.RemoveFunctionRef(drop.Ref); + }); + } + } + + sealed class SupSuper : ReceiveActor + { + public SupSuper() + { + var s = Context.ActorOf(Props.Create(), "super"); + ReceiveAny(msg => s.Tell(msg)); + } + } + + #endregion + + public FunctionRefSpec(ITestOutputHelper output) : base(output, null) + { + } + + #region top level + + [Fact] + public void FunctionRef_created_by_top_level_actor_must_forward_messages() + { + var s = SuperActor(); + var forwarder = GetFunctionRef(s); + + forwarder.Tell("hello"); + ExpectMsg(new Forwarded("hello", TestActor)); + } + + [Fact] + public void FunctionRef_created_by_top_level_actor_must_be_watchable() + { + var s = SuperActor(); + var forwarder = GetFunctionRef(s); + + s.Tell(new GetForwarder(TestActor)); + var f = ExpectMsg(); + Watch(f); + s.Tell(new DropForwarder(f)); + ExpectTerminated(f); + } + + [Fact] + public void FunctionRef_created_by_top_level_actor_must_be_able_to_watch() + { + var s = SuperActor(); + var forwarder = GetFunctionRef(s); + + s.Tell(new GetForwarder(TestActor)); + var f = ExpectMsg(); + forwarder.Watch(f); + s.Tell(new DropForwarder(f)); + ExpectMsg(new Forwarded(new Terminated(f, true, false), f)); + } + + [Fact] + public void FunctionRef_created_by_top_level_actor_must_terminate_when_their_parent_terminates() + { + var s = SuperActor(); + var forwarder = GetFunctionRef(s); + + Watch(forwarder); + s.Tell(PoisonPill.Instance); + ExpectTerminated(forwarder); + } + + private FunctionRef GetFunctionRef(IActorRef s) + { + s.Tell(new GetForwarder(TestActor)); + return ExpectMsg(); + } + + private IActorRef SuperActor() => Sys.ActorOf(Props.Create(), "super"); + + #endregion + + #region non-top level + + [Fact] + public void FunctionRef_created_by_non_top_level_actor_must_forward_messages() + { + var s = SupSuperActor(); + var forwarder = GetFunctionRef(s); + + forwarder.Tell("hello"); + ExpectMsg(new Forwarded("hello", TestActor)); + } + + [Fact] + public void FunctionRef_created_by_non_top_level_actor_must_be_watchable() + { + var s = SupSuperActor(); + var forwarder = GetFunctionRef(s); + + s.Tell(new GetForwarder(TestActor)); + var f = ExpectMsg(); + Watch(f); + s.Tell(new DropForwarder(f)); + ExpectTerminated(f); + } + + [Fact] + public void FunctionRef_created_by_non_top_level_actor_must_be_able_to_watch() + { + var s = SupSuperActor(); + var forwarder = GetFunctionRef(s); + + s.Tell(new GetForwarder(TestActor)); + var f = ExpectMsg(); + forwarder.Watch(f); + s.Tell(new DropForwarder(f)); + ExpectMsg(new Forwarded(new Terminated(f, true, false), f)); + } + + [Fact] + public void FunctionRef_created_by_non_top_level_actor_must_terminate_when_their_parent_terminates() + { + var s = SupSuperActor(); + var forwarder = GetFunctionRef(s); + + Watch(forwarder); + s.Tell(PoisonPill.Instance); + ExpectTerminated(forwarder); + } + + private IActorRef SupSuperActor() => Sys.ActorOf(Props.Create(), "supsuper"); + + #endregion + + [Fact(Skip = "FIXME")] + public void FunctionRef_when_not_registered_must_not_be_found() + { + var provider = ((ExtendedActorSystem) Sys).Provider; + var fref = new FunctionRef(TestActor.Path / "blabla", provider, Sys.EventStream, (x, y) => { }); + EventFilter.Exception().ExpectOne(() => + { + // needs to be something that fails when the deserialized form is not a FunctionRef + // this relies upon serialize-messages during tests + TestActor.Tell(new DropForwarder(fref)); + ExpectNoMsg(TimeSpan.FromSeconds(1)); + }); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Tests/Akka.Tests.csproj b/src/core/Akka.Tests/Akka.Tests.csproj index bd521e79bbf..7a89113d73a 100644 --- a/src/core/Akka.Tests/Akka.Tests.csproj +++ b/src/core/Akka.Tests/Akka.Tests.csproj @@ -1,17 +1,14 @@  - Akka.Tests net452;netcoreapp1.1 - - @@ -21,28 +18,22 @@ - - - - $(DefineConstants);SERIALIZATION;CONFIGURATION;UNSAFE_THREADING - $(DefineConstants);CORECLR - $(DefineConstants);RELEASE diff --git a/src/core/Akka/Actor/ActorCell.Children.cs b/src/core/Akka/Actor/ActorCell.Children.cs index 3f49e007b4e..aa78dced96d 100644 --- a/src/core/Akka/Actor/ActorCell.Children.cs +++ b/src/core/Akka/Actor/ActorCell.Children.cs @@ -7,6 +7,8 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; using System.Threading; using Akka.Actor.Internal; using Akka.Serialization; @@ -19,18 +21,53 @@ public partial class ActorCell { private volatile IChildrenContainer _childrenContainerDoNotCallMeDirectly = EmptyChildrenContainer.Instance; private long _nextRandomNameDoNotCallMeDirectly = -1; // Interlocked.Increment automatically adds 1 to this value. Allows us to start from 0. + private ImmutableDictionary _functionRefsDoNotCallMeDirectly = ImmutableDictionary.Empty; /// /// The child container collection, used to house information about all child actors. /// - public IChildrenContainer ChildrenContainer + public IChildrenContainer ChildrenContainer => _childrenContainerDoNotCallMeDirectly; + + private IReadOnlyCollection Children => ChildrenContainer.Children; + + private ImmutableDictionary FunctionRefs => Volatile.Read(ref _functionRefsDoNotCallMeDirectly); + + internal bool TryGetFunctionRef(string name, out FunctionRef functionRef) => + FunctionRefs.TryGetValue(name, out functionRef); + + internal bool TryGetFunctionRef(string name, int uid, out FunctionRef functionRef) => + FunctionRefs.TryGetValue(name, out functionRef) && (uid == ActorCell.UndefinedUid || uid == functionRef.Path.Uid); + + internal FunctionRef AddFunctionRef(Action tell, string suffix = "") { - get { return _childrenContainerDoNotCallMeDirectly; } + var r = GetRandomActorName("$$"); + var n = string.IsNullOrEmpty(suffix) ? r : r + "-" + suffix; + var childPath = new ChildActorPath(Self.Path, n, NewUid()); + var functionRef = new FunctionRef(childPath, SystemImpl.Provider, SystemImpl.EventStream, tell); + + return ImmutableInterlocked.GetOrAdd(ref _functionRefsDoNotCallMeDirectly, childPath.Name, functionRef); + } + + internal bool RemoveFunctionRef(FunctionRef functionRef) + { + if (functionRef.Path.Parent != Self.Path) throw new InvalidOperationException($"Trying to remove FunctionRef {functionRef.Path} from wrong ActorCell"); + + var name = functionRef.Path.Name; + if (ImmutableInterlocked.TryRemove(ref _functionRefsDoNotCallMeDirectly, name, out var fref)) + { + fref.Stop(); + return true; + } + else return false; } - private IReadOnlyCollection Children + protected void StopFunctionRefs() { - get { return ChildrenContainer.Children; } + var refs = Interlocked.Exchange(ref _functionRefsDoNotCallMeDirectly, ImmutableDictionary.Empty); + foreach (var pair in refs) + { + pair.Value.Stop(); + } } /// @@ -87,10 +124,11 @@ private IActorRef ActorOf(Props props, string name, bool isAsync, bool isSystemS return MakeChild(props, name, isAsync, isSystemService); } - private string GetRandomActorName() + private string GetRandomActorName(string prefix = "$") { var id = Interlocked.Increment(ref _nextRandomNameDoNotCallMeDirectly); - return "$" + id.Base64Encode(); + var sb = new StringBuilder(prefix); + return id.Base64Encode(sb).ToString(); } /// @@ -331,8 +369,7 @@ protected bool TryGetChildStatsByRef(IActorRef actor, out ChildRestartStats chil [Obsolete("Use TryGetSingleChild [0.7.1]")] public IInternalActorRef GetSingleChild(string name) { - IInternalActorRef child; - return TryGetSingleChild(name, out child) ? child : ActorRefs.Nobody; + return TryGetSingleChild(name, out var child) ? child : ActorRefs.Nobody; } /// @@ -346,18 +383,21 @@ public bool TryGetSingleChild(string name, out IInternalActorRef child) if (name.IndexOf('#') < 0) { // optimization for the non-uid case - ChildRestartStats stats; - if (TryGetChildRestartStatsByName(name, out stats)) + if (TryGetChildRestartStatsByName(name, out var stats)) { child = stats.Child; return true; } + else if (TryGetFunctionRef(name, out var functionRef)) + { + child = functionRef; + return true; + } } else { var nameAndUid = SplitNameAndUid(name); - ChildRestartStats stats; - if (TryGetChildRestartStatsByName(nameAndUid.Name, out stats)) + if (TryGetChildRestartStatsByName(nameAndUid.Name, out var stats)) { var uid = nameAndUid.Uid; if (uid == ActorCell.UndefinedUid || uid == stats.Uid) @@ -366,6 +406,11 @@ public bool TryGetSingleChild(string name, out IInternalActorRef child) return true; } } + else if (TryGetFunctionRef(nameAndUid.Name, nameAndUid.Uid, out var functionRef)) + { + child = functionRef; + return true; + } } child = ActorRefs.Nobody; return false; @@ -378,8 +423,7 @@ public bool TryGetSingleChild(string name, out IInternalActorRef child) /// TBD protected SuspendReason RemoveChildAndGetStateChange(IActorRef child) { - var terminating = ChildrenContainer as TerminatingChildrenContainer; - if (terminating != null) + if (ChildrenContainer is TerminatingChildrenContainer terminating) { var newContainer = UpdateChildrenRefs(c => c.Remove(child)); if (newContainer is TerminatingChildrenContainer) return null; diff --git a/src/core/Akka/Actor/ActorCell.FaultHandling.cs b/src/core/Akka/Actor/ActorCell.FaultHandling.cs index e7133952a02..10bafcb0ecd 100644 --- a/src/core/Akka/Actor/ActorCell.FaultHandling.cs +++ b/src/core/Akka/Actor/ActorCell.FaultHandling.cs @@ -304,21 +304,25 @@ private void FinishTerminate() try { Parent.SendSystemMessage(new DeathWatchNotification(_self, existenceConfirmed: true, addressTerminated: false)); } finally { - try { TellWatchersWeDied(); } + try { StopFunctionRefs(); } finally { - try { UnwatchWatchedActors(a); } // stay here as we expect an emergency stop from HandleInvokeFailure + try { TellWatchersWeDied(); } finally { - if (System.Settings.DebugLifecycle) - Publish(new Debug(_self.Path.ToString(), ActorType, "Stopped")); + try { UnwatchWatchedActors(a); } // stay here as we expect an emergency stop from HandleInvokeFailure + finally + { + if (System.Settings.DebugLifecycle) + Publish(new Debug(_self.Path.ToString(), ActorType, "Stopped")); - ClearActor(a); - ClearActorCell(); + ClearActor(a); + ClearActorCell(); - _actor = null; + _actor = null; - } + } + } } } } diff --git a/src/core/Akka/Actor/ActorRef.cs b/src/core/Akka/Actor/ActorRef.cs index 4ea19017975..191db5abd58 100644 --- a/src/core/Akka/Actor/ActorRef.cs +++ b/src/core/Akka/Actor/ActorRef.cs @@ -9,6 +9,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -841,5 +842,204 @@ IEnumerator IEnumerable.GetEnumerator() } } } + + /// + /// INTERNAL API + /// + /// This kind of ActorRef passes all received messages to the given function for + /// performing a non-blocking side-effect. The intended use is to transform the + /// message before sending to the real target actor. Such references can be created + /// by calling and must be deregistered when no longer + /// needed by calling . FunctionRefs do not count + /// towards the live children of an actor, they do not receive the Terminate command + /// and do not prevent the parent from terminating. FunctionRef is properly + /// registered for remote lookup and ActorSelection. + /// + /// When using the feature you must ensure that upon reception of the + /// Terminated message the watched actorRef is ed. + /// + internal sealed class FunctionRef : MinimalActorRef + { + private readonly EventStream _eventStream; + private readonly Action _tell; + + private ImmutableHashSet _watching = ImmutableHashSet.Empty; + private ImmutableHashSet _watchedBy = ImmutableHashSet.Empty; + + public FunctionRef(ActorPath path, IActorRefProvider provider, EventStream eventStream, Action tell) + { + _eventStream = eventStream; + _tell = tell; + Path = path; + Provider = provider; + } + + public override ActorPath Path { get; } + public override IActorRefProvider Provider { get; } + public override bool IsTerminated => Volatile.Read(ref _watchedBy) == null; + + /// + /// Have this FunctionRef watch the given Actor. This method must not be + /// called concurrently from different threads, it should only be called by + /// its parent Actor. + /// + /// Upon receiving the Terminated message, must be called from a + /// safe context (i.e. normally from the parent Actor). + /// + public void Watch(IActorRef actorRef) + { + _watching = _watching.Add(actorRef); + var internalRef = (IInternalActorRef) actorRef; + internalRef.SendSystemMessage(new Watch(internalRef, this)); + } + + /// + /// Have this FunctionRef unwatch the given Actor. This method must not be + /// called concurrently from different threads, it should only be called by + /// its parent Actor. + /// + public void Unwatch(IActorRef actorRef) + { + _watching = _watching.Remove(actorRef); + var internalRef = (IInternalActorRef) actorRef; + internalRef.SendSystemMessage(new Unwatch(internalRef, this)); + + } + + /// + /// Query whether this FunctionRef is currently watching the given Actor. This + /// method must not be called concurrently from different threads, it should + /// only be called by its parent Actor. + /// + public bool IsWatching(IActorRef actorRef) => _watching.Contains(actorRef); + + protected override void TellInternal(object message, IActorRef sender) => _tell(sender, message); + + public override void SendSystemMessage(ISystemMessage message) + { + switch (message) + { + case Watch watch: + AddWatcher(watch.Watchee, watch.Watcher); + break; + case Unwatch unwatch: + RemoveWatcher(unwatch.Watchee, unwatch.Watcher); + break; + case DeathWatchNotification deathWatch: + this.Tell(new Terminated(deathWatch.Actor, existenceConfirmed: true, addressTerminated: false), deathWatch.Actor); + break; + } + } + + private void SendTerminated() + { + var watchedBy = Interlocked.Exchange(ref _watchedBy, null); + if (watchedBy != null) + { + if (!watchedBy.IsEmpty) + { + foreach (var watcher in watchedBy) + SendTerminated(watcher); + } + + if (!_watching.IsEmpty) + { + foreach (var watched in _watching) + UnwatchWatched(watched); + + _watching = ImmutableHashSet.Empty; + } + } + } + + private void SendTerminated(IActorRef watcher) + { + if (watcher is IInternalActorRef scope) + scope.SendSystemMessage(new DeathWatchNotification(this, existenceConfirmed: true, addressTerminated: false)); + } + + private void UnwatchWatched(IActorRef watched) + { + if (watched is IInternalActorRef internalActorRef) + internalActorRef.SendSystemMessage(new Unwatch(internalActorRef, this)); + } + + public override void Stop() => SendTerminated(); + + private void AddWatcher(IInternalActorRef watchee, IInternalActorRef watcher) + { + while (true) + { + var watchedBy = Volatile.Read(ref _watchedBy); + if (watchedBy == null) + SendTerminated(watcher); + else + { + var watcheeSelf = Equals(watchee, this); + var watcherSelf = Equals(watcher, this); + + if (watcheeSelf && !watcherSelf) + { + if (!watchedBy.Contains(watcher) && !ReferenceEquals(watchedBy, Interlocked.CompareExchange(ref _watchedBy, watchedBy.Add(watcher), watchedBy))) + { + continue; + } + } + else if (!watcheeSelf && watcherSelf) + { + Publish(new Warning(Path.ToString(), typeof(FunctionRef), $"Externally triggered watch from {watcher} to {watchee} is illegal on FunctionRef")); + } + else + { + Publish(new Warning(Path.ToString(), typeof(FunctionRef), $"BUG: illegal Watch({watchee},{watcher}) for {this}")); + } + } + + break; + } + } + + private void RemoveWatcher(IInternalActorRef watchee, IInternalActorRef watcher) + { + while (true) + { + var watchedBy = Volatile.Read(ref _watchedBy); + if (watchedBy == null) + SendTerminated(watcher); + else + { + var watcheeSelf = Equals(watchee, this); + var watcherSelf = Equals(watcher, this); + + if (watcheeSelf && !watcherSelf) + { + if (!watchedBy.Contains(watcher) && !ReferenceEquals(watchedBy, Interlocked.CompareExchange(ref _watchedBy, watchedBy.Remove(watcher), watchedBy))) + { + continue; + } + } + else if (!watcheeSelf && watcherSelf) + { + Publish(new Warning(Path.ToString(), typeof(FunctionRef), $"Externally triggered watch from {watcher} to {watchee} is illegal on FunctionRef")); + } + else + { + Publish(new Warning(Path.ToString(), typeof(FunctionRef), $"BUG: illegal Watch({watchee},{watcher}) for {this}")); + } + } + + break; + } + } + + private void Publish(LogEvent e) + { + try + { + _eventStream.Publish(e); + } + catch (Exception) { } + } + } } diff --git a/src/core/Akka/Actor/IAutoReceivedMessage.cs b/src/core/Akka/Actor/IAutoReceivedMessage.cs index 2e59a173b50..98e81b70952 100644 --- a/src/core/Akka/Actor/IAutoReceivedMessage.cs +++ b/src/core/Akka/Actor/IAutoReceivedMessage.cs @@ -5,6 +5,7 @@ // //----------------------------------------------------------------------- +using System; using Akka.Event; namespace Akka.Actor @@ -21,7 +22,7 @@ public interface IAutoReceivedMessage /// Terminated message can't be forwarded to another actor, since that actor might not be watching the subject. /// Instead, if you need to forward Terminated to another actor you should send the information in your own message. /// - public sealed class Terminated : IAutoReceivedMessage, IPossiblyHarmful, IDeadLetterSuppression, INoSerializationVerificationNeeded + public sealed class Terminated : IAutoReceivedMessage, IPossiblyHarmful, IDeadLetterSuppression, INoSerializationVerificationNeeded, IEquatable { /// /// Initializes a new instance of the class. @@ -59,7 +60,27 @@ public Terminated(IActorRef actorRef, bool existenceConfirmed, bool addressTermi /// A that represents this instance. public override string ToString() { - return $": {ActorRef} - ExistenceConfirmed={ExistenceConfirmed}"; + return $"Terminated(ref: {ActorRef}, existenceConfirmed: {ExistenceConfirmed}, addressTerminated: {AddressTerminated})"; + } + + public bool Equals(Terminated other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(ActorRef, other.ActorRef) && AddressTerminated == other.AddressTerminated && ExistenceConfirmed == other.ExistenceConfirmed; + } + + public override bool Equals(object obj) => obj is Terminated terminated && Equals(terminated); + + public override int GetHashCode() + { + unchecked + { + var hashCode = (ActorRef != null ? ActorRef.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ AddressTerminated.GetHashCode(); + hashCode = (hashCode * 397) ^ ExistenceConfirmed.GetHashCode(); + return hashCode; + } } } diff --git a/src/core/Akka/Actor/RepointableActorRef.cs b/src/core/Akka/Actor/RepointableActorRef.cs index c7253dc2049..2b6b1136c7e 100644 --- a/src/core/Akka/Actor/RepointableActorRef.cs +++ b/src/core/Akka/Actor/RepointableActorRef.cs @@ -293,8 +293,7 @@ public override IActorRef GetChild(IEnumerable name) return ActorRefs.Nobody; default: var nameAndUid = ActorCell.SplitNameAndUid(next); - IChildStats stats; - if (Lookup.TryGetChildStatsByName(nameAndUid.Name, out stats)) + if (Lookup.TryGetChildStatsByName(nameAndUid.Name, out var stats)) { var crs = stats as ChildRestartStats; var uid = nameAndUid.Uid; @@ -306,6 +305,10 @@ public override IActorRef GetChild(IEnumerable name) return crs.Child; } } + else if (Lookup is ActorCell cell && cell.TryGetFunctionRef(nameAndUid.Name, nameAndUid.Uid, out var functionRef)) + { + return functionRef; + } return ActorRefs.Nobody; } } diff --git a/src/core/Akka/Util/Base64Encoding.cs b/src/core/Akka/Util/Base64Encoding.cs index cbfb660dbbf..fafe1d5747b 100644 --- a/src/core/Akka/Util/Base64Encoding.cs +++ b/src/core/Akka/Util/Base64Encoding.cs @@ -24,9 +24,15 @@ public static class Base64Encoding /// /// TBD /// TBD - public static string Base64Encode(this long value) + public static string Base64Encode(this long value) => Base64Encode(value, new StringBuilder()).ToString(); + + /// + /// TBD + /// + /// TBD + /// TBD + public static StringBuilder Base64Encode(this long value, StringBuilder sb) { - var sb = new StringBuilder(); var next = value; do { @@ -34,7 +40,7 @@ public static string Base64Encode(this long value) sb.Append(Base64Chars[index]); next = next >> 6; } while(next != 0); - return sb.ToString(); + return sb; } /// From 2a4b202571d3281cd4f85c224ef41f678724ed96 Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Wed, 14 Feb 2018 08:09:47 +0100 Subject: [PATCH 03/70] introduced FunctionRef in streams: GetStageActor --- .../Dsl/StageActorRefSpec.cs | 14 +- .../ActorRefBackpressureSinkStage.cs | 16 +- .../Implementation/IO/TcpStages.cs | 50 +-- src/core/Akka.Streams/Stage/GraphStage.cs | 286 ++++++------------ 4 files changed, 126 insertions(+), 240 deletions(-) diff --git a/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs b/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs index fd0af56d69a..88734ea1803 100644 --- a/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs @@ -301,7 +301,7 @@ private class Logic : GraphStageLogic private readonly SumTestStage _stage; private readonly TaskCompletionSource _promise; private int _sum; - private StageActorRef _self; + private IActorRef Self => StageActor.Ref; public Logic(SumTestStage stage, TaskCompletionSource promise) : base(stage.Shape) { @@ -327,8 +327,8 @@ public Logic(SumTestStage stage, TaskCompletionSource promise) : base(stage public override void PreStart() { Pull(_stage._inlet); - _self = GetStageActorRef(Behaviour); - _stage._probe.Tell(_self); + GetStageActor(Behaviour); + _stage._probe.Tell(Self); } private void Behaviour(Tuple args) @@ -339,8 +339,8 @@ private void Behaviour(Tuple args) msg.Match() .With(a => _sum += a.N) .With(() => Pull(_stage._inlet)) - .With(() => sender.Tell(GetStageActorRef(Behaviour), _self)) - .With(() => GetStageActorRef(tuple => tuple.Item1.Tell(tuple.Item2.ToString()))) + .With(() => sender.Tell(GetStageActor(Behaviour), Self)) + .With(() => GetStageActor(tuple => tuple.Item1.Tell(tuple.Item2.ToString()))) .With(() => { _promise.TrySetResult(_sum); @@ -348,9 +348,9 @@ private void Behaviour(Tuple args) }).With(a => { _sum += a.N; - sender.Tell(_sum, _self); + sender.Tell(_sum, Self); }) - .With(w => _self.Watch(w.Watchee)) + .With(w => StageActor.Watch(w.Watchee)) .With(t => _stage._probe.Tell(new WatcheeTerminated(t.ActorRef))); } } diff --git a/src/core/Akka.Streams/Implementation/ActorRefBackpressureSinkStage.cs b/src/core/Akka.Streams/Implementation/ActorRefBackpressureSinkStage.cs index dd54dffa935..812040a69fe 100644 --- a/src/core/Akka.Streams/Implementation/ActorRefBackpressureSinkStage.cs +++ b/src/core/Akka.Streams/Implementation/ActorRefBackpressureSinkStage.cs @@ -30,7 +30,8 @@ private sealed class Logic : InGraphStageLogic private readonly int _maxBuffer; private readonly List _buffer; private readonly Type _ackType; - private StageActorRef _self; + + public IActorRef Self => StageActor.Ref; public Logic(ActorRefBackpressureSinkStage stage, int maxBuffer) : base(stage.Shape) { @@ -65,7 +66,7 @@ public override void OnUpstreamFinish() public override void OnUpstreamFailure(Exception ex) { - _stage._actorRef.Tell(_stage._onFailureMessage(ex), _self); + _stage._actorRef.Tell(_stage._onFailureMessage(ex), Self); _completionSignalled = true; FailStage(ex); } @@ -100,9 +101,8 @@ private void Receive(Tuple evt) public override void PreStart() { SetKeepGoing(true); - _self = GetStageActorRef(Receive); - _self.Watch(_stage._actorRef); - _stage._actorRef.Tell(_stage._onInitMessage, _self); + GetStageActor(Receive).Watch(_stage._actorRef); + _stage._actorRef.Tell(_stage._onInitMessage, Self); Pull(_stage._inlet); } @@ -110,14 +110,14 @@ private void DequeueAndSend() { var msg = _buffer[0]; _buffer.RemoveAt(0); - _stage._actorRef.Tell(msg, _self); + _stage._actorRef.Tell(msg, Self); if (_buffer.Count == 0 && _completeReceived) Finish(); } private void Finish() { - _stage._actorRef.Tell(_stage._onCompleteMessage, _self); + _stage._actorRef.Tell(_stage._onCompleteMessage, Self); _completionSignalled = true; CompleteStage(); } @@ -125,7 +125,7 @@ private void Finish() public override void PostStop() { if(!_completionSignalled) - StageActorRef.Tell(_stage._onFailureMessage(new AbruptStageTerminationException(this))); + Self.Tell(_stage._onFailureMessage(new AbruptStageTerminationException(this))); } public override string ToString() => "ActorRefBackpressureSink"; diff --git a/src/core/Akka.Streams/Implementation/IO/TcpStages.cs b/src/core/Akka.Streams/Implementation/IO/TcpStages.cs index 9021fc4a702..73f07d3abc7 100644 --- a/src/core/Akka.Streams/Implementation/IO/TcpStages.cs +++ b/src/core/Akka.Streams/Implementation/IO/TcpStages.cs @@ -53,7 +53,7 @@ public ConnectionSourceStageLogic(Shape shape, ConnectionSourceStage stage, Task public void OnPull() { // Ignore if still binding - _listener?.Tell(new Tcp.ResumeAccepting(1), StageActorRef); + _listener?.Tell(new Tcp.ResumeAccepting(1), StageActor.Ref); } public void OnDownstreamFinish() => TryUnbind(); @@ -85,13 +85,13 @@ private void TryUnbind() { _unbindStarted = true; SetKeepGoing(true); - _listener.Tell(Tcp.Unbind.Instance, StageActorRef); + _listener.Tell(Tcp.Unbind.Instance, StageActor.Ref); } } private void UnbindCompleted() { - StageActorRef.Unwatch(_listener); + StageActor.Unwatch(_listener); if (_connectionFlowsAwaitingInitialization.Current == 0) CompleteStage(); else @@ -106,8 +106,8 @@ protected internal override void OnTimer(object timerKey) public override void PreStart() { - GetStageActorRef(Receive); - _stage._tcpManager.Tell(new Tcp.Bind(StageActorRef, _stage._endpoint, _stage._backlog, _stage._options, pullMode: true), StageActorRef); + GetStageActor(Receive); + _stage._tcpManager.Tell(new Tcp.Bind(StageActor.Ref, _stage._endpoint, _stage._backlog, _stage._options, pullMode: true), StageActor.Ref); } private void Receive(Tuple args) @@ -118,12 +118,12 @@ private void Receive(Tuple args) { var bound = (Tcp.Bound)msg; _listener = sender; - StageActorRef.Watch(_listener); + StageActor.Watch(_listener); if (IsAvailable(_stage._out)) - _listener.Tell(new Tcp.ResumeAccepting(1), StageActorRef); + _listener.Tell(new Tcp.ResumeAccepting(1), StageActor.Ref); - var thisStage = StageActorRef; + var thisStage = StageActor.Ref; _bindingPromise.TrySetResult(new StreamTcp.ServerBinding(bound.LocalAddress, () => { // Beware, sender must be explicit since stageActor.ref will be invalid to access after the stage stopped @@ -404,14 +404,14 @@ public TcpStreamLogic(FlowShape shape, ITcpRole role, En _bytesOut = shape.Outlet; _readHandler = new LambdaOutHandler( - onPull: () => _connection.Tell(Tcp.ResumeReading.Instance, StageActorRef), + onPull: () => _connection.Tell(Tcp.ResumeReading.Instance, StageActor.Ref), onDownstreamFinish: () => { if (!IsClosed(_bytesIn)) - _connection.Tell(Tcp.ResumeReading.Instance, StageActorRef); + _connection.Tell(Tcp.ResumeReading.Instance, StageActor.Ref); else { - _connection.Tell(Tcp.Abort.Instance, StageActorRef); + _connection.Tell(Tcp.Abort.Instance, StageActor.Ref); CompleteStage(); } }); @@ -423,17 +423,17 @@ public TcpStreamLogic(FlowShape shape, ITcpRole role, En { var elem = Grab(_bytesIn); ReactiveStreamsCompliance.RequireNonNullElement(elem); - _connection.Tell(Tcp.Write.Create(elem, WriteAck.Instance), StageActorRef); + _connection.Tell(Tcp.Write.Create(elem, WriteAck.Instance), StageActor.Ref); }, onUpstreamFinish: () => { // Reading has stopped before, either because of cancel, or PeerClosed, so just Close now // (or half-close is turned off) if (IsClosed(_bytesOut) || !_role.HalfClose) - _connection.Tell(Tcp.Close.Instance, StageActorRef); + _connection.Tell(Tcp.Close.Instance, StageActor.Ref); // We still read, so we only close the write side else if (_connection != null) - _connection.Tell(Tcp.ConfirmedClose.Instance, StageActorRef); + _connection.Tell(Tcp.ConfirmedClose.Instance, StageActor.Ref); else CompleteStage(); }, @@ -444,7 +444,7 @@ public TcpStreamLogic(FlowShape shape, ITcpRole role, En if (Interpreter.Log.IsDebugEnabled) Interpreter.Log.Debug( $"Aborting tcp connection to {_remoteAddress} because of upstream failure: {ex.Message}\n{ex.StackTrace}"); - _connection.Tell(Tcp.Abort.Instance, StageActorRef); + _connection.Tell(Tcp.Abort.Instance, StageActor.Ref); } else FailStage(ex); @@ -463,15 +463,15 @@ public override void PreStart() var inbound = (Inbound)_role; SetHandler(_bytesOut, _readHandler); _connection = inbound.Connection; - GetStageActorRef(Connected).Watch(_connection); - _connection.Tell(new Tcp.Register(StageActorRef, keepOpenOnPeerClosed: true, useResumeWriting: false), StageActorRef); + GetStageActor(Connected).Watch(_connection); + _connection.Tell(new Tcp.Register(StageActor.Ref, keepOpenOnPeerClosed: true, useResumeWriting: false), StageActor.Ref); Pull(_bytesIn); } else { var outbound = (Outbound)_role; - GetStageActorRef(Connecting(outbound)).Watch(outbound.Manager); - outbound.Manager.Tell(outbound.ConnectCmd, StageActorRef); + GetStageActor(Connecting(outbound)).Watch(outbound.Manager); + outbound.Manager.Tell(outbound.ConnectCmd, StageActor.Ref); } } @@ -506,13 +506,13 @@ private StageActorRef.Receive Connecting(Outbound outbound) ((Outbound)_role).LocalAddressPromise.TrySetResult(connected.LocalAddress); _connection = sender; SetHandler(_bytesOut, _readHandler); - StageActorRef.Unwatch(outbound.Manager); - StageActorRef.Become(Connected); - StageActorRef.Watch(_connection); - _connection.Tell(new Tcp.Register(StageActorRef, keepOpenOnPeerClosed: true, useResumeWriting: false), StageActorRef); + StageActor.Unwatch(outbound.Manager); + StageActor.Become(Connected); + StageActor.Watch(_connection); + _connection.Tell(new Tcp.Register(StageActor.Ref, keepOpenOnPeerClosed: true, useResumeWriting: false), StageActor.Ref); if (IsAvailable(_bytesOut)) - _connection.Tell(Tcp.ResumeReading.Instance, StageActorRef); + _connection.Tell(Tcp.ResumeReading.Instance, StageActor.Ref); Pull(_bytesIn); } @@ -535,7 +535,7 @@ private void Connected(Tuple args) { var received = (Tcp.Received)msg; // Keep on reading even when closed. There is no "close-read-side" in TCP - if (IsClosed(_bytesOut)) _connection.Tell(Tcp.ResumeReading.Instance, StageActorRef); + if (IsClosed(_bytesOut)) _connection.Tell(Tcp.ResumeReading.Instance, StageActor.Ref); else Push(_bytesOut, received.Data); } else if (msg is WriteAck) diff --git a/src/core/Akka.Streams/Stage/GraphStage.cs b/src/core/Akka.Streams/Stage/GraphStage.cs index b10d236da1b..8e261261d0c 100644 --- a/src/core/Akka.Streams/Stage/GraphStage.cs +++ b/src/core/Akka.Streams/Stage/GraphStage.cs @@ -859,18 +859,18 @@ protected GraphStageLogic(Shape shape) : this(shape.Inlets.Count(), shape.Outlet /// public virtual bool KeepGoingAfterAllPortsClosed => false; - private StageActorRef _stageActorRef; + private StageActor _stageActor; /// /// TBD /// - public StageActorRef StageActorRef + public StageActor StageActor { get { - if (_stageActorRef == null) + if (_stageActor == null) throw StageActorRefNotInitializedException.Instance; - return _stageActorRef; + return _stageActor; } } @@ -1619,21 +1619,35 @@ protected Action GetAsyncCallback(Action handler) /// Callback that will be called upon receiving of a message by this special Actor /// Minimal actor with watch method [ApiMayChange] - protected StageActorRef GetStageActorRef(StageActorRef.Receive receive) + protected StageActor GetStageActor(StageActorRef.Receive receive) { - if (_stageActorRef == null) + if (_stageActor == null) { var actorMaterializer = ActorMaterializerHelper.Downcast(Interpreter.Materializer); - var provider = ((IInternalActorRef)actorMaterializer.Supervisor).Provider; - var path = actorMaterializer.Supervisor.Path / StageActorRef.Name.Next(); - _stageActorRef = new StageActorRef(provider, actorMaterializer.Logger, r => GetAsyncCallback>(tuple => r(tuple)), receive, path); + _stageActor = new StageActor( + actorMaterializer, + r => GetAsyncCallback>(message => r(message)), + receive, + StageActorName); } else - _stageActorRef.Become(receive); + _stageActor.Become(receive); - return _stageActorRef; + return _stageActor; } + /// + /// Override and return a name to be given to the StageActor of this stage. + /// + /// This method will be only invoked and used once, during the first + /// invocation whichc reates the actor, since subsequent `getStageActors` calls function + /// like `become`, rather than creating new actors. + /// + /// Returns an empty string by default, which means that the name will a unique generated String (e.g. "$$a"). + /// + [ApiMayChange] + protected virtual string StageActorName => ""; + /// /// TBD /// @@ -1644,10 +1658,10 @@ protected internal virtual void BeforePreStart() { } /// protected internal virtual void AfterPostStop() { - if (_stageActorRef != null) + if (_stageActor != null) { - _stageActorRef.Stop(); - _stageActorRef = null; + _stageActor.Stop(); + _stageActor = null; } } @@ -2349,225 +2363,97 @@ public override void OnDownstreamFinish() } } + public static class StageActorRef + { + public delegate void Receive(Tuple args); + } + /// /// Minimal actor to work with other actors and watch them in a synchronous ways. /// - public sealed class StageActorRef : MinimalActorRef + public sealed class StageActor { - /// - /// TBD - /// - /// TBD - public delegate void Receive(Tuple args); - - /// - /// TBD - /// - public readonly IImmutableSet StageTerminatedTombstone = null; - - /// - /// TBD - /// - public static readonly EnumerableActorName Name = new EnumerableActorNameImpl("StageActorRef", new AtomicCounterLong(0L)); - - /// - /// TBD - /// - public readonly ILoggingAdapter Log; private readonly Action> _callback; - private readonly AtomicReference> _watchedBy = new AtomicReference>(ImmutableHashSet.Empty); - - private volatile Receive _behavior; - private IImmutableSet _watching = ImmutableHashSet.Empty; - - /// - /// TBD - /// - /// TBD - /// TBD - /// TBD - /// TBD - /// TBD - /// TBD - public StageActorRef(IActorRefProvider provider, ILoggingAdapter log, Func>> getAsyncCallback, Receive initialReceive, ActorPath path) + private readonly ActorCell _cell; + private readonly FunctionRef _functionRef; + private StageActorRef.Receive _behavior; + + public StageActor( + ActorMaterializer materializer, + Func>> getAsyncCallback, + StageActorRef.Receive initialReceive, + string name = null) { - Log = log; - Provider = provider; + _callback = getAsyncCallback(initialReceive); _behavior = initialReceive; - Path = path; - - _callback = getAsyncCallback(args => _behavior(args)); - } - - /// - /// TBD - /// - public override ActorPath Path { get; } - /// - /// TBD - /// - public override IActorRefProvider Provider { get; } - - /// - /// TBD - /// - public override bool IsTerminated => _watchedBy.Value == StageTerminatedTombstone; + switch (materializer.Supervisor) + { + case LocalActorRef r: _cell = r.Cell; break; + case RepointableActorRef r: _cell = (ActorCell)r.Underlying; break; + default: throw new IllegalStateException($"Stream supervisor must be a local actor, was [{materializer.Supervisor.GetType()}]"); + } - private void LogIgnored(object message) => Log.Warning($"{message} message sent to StageActorRef({Path}) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); - /// - /// TBD - /// - /// TBD - /// TBD - protected override void TellInternal(object message, IActorRef sender) - { - switch (message) + _functionRef = _cell.AddFunctionRef((sender, message) => { - case PoisonPill _: - case Kill _: - LogIgnored(message); - return; - case Terminated t: - if (_watching.Contains(t.ActorRef)) - { - _watching.Remove(t.ActorRef); - _callback(Tuple.Create(sender, message)); + switch (message) + { + case PoisonPill _: + case Kill _: + materializer.Logger.Warning("{0} message sent to StageActor({1}) will be ignored, since it is not a real Actor." + + "Use a custom message type to communicate with it instead.", message, _functionRef.Path); break; - } - else return; - default: - _callback(Tuple.Create(sender, message)); - break; - } + default: _callback(Tuple.Create(sender, message)); break; + } + }); } /// - /// TBD + /// The by which this can be contacted from the outside. + /// This is a full-fledged that supports watching and being watched + /// as well as location transparent (remote) communication. /// - /// TBD - public override void SendSystemMessage(ISystemMessage message) - { - if (message is DeathWatchNotification death) - Tell(new Terminated(death.Actor, true, false), ActorRefs.NoSender); - else if (message is Watch w) - AddWatcher(w.Watchee, w.Watcher); - else if (message is Unwatch u) - RemoveWatcher(u.Watchee, u.Watcher); - } + public IActorRef Ref => _functionRef; /// - /// TBD + /// Special `Become` allowing to swap the behaviour of this . + /// Unbecome is not available. /// - /// TBD - public void Become(Receive behavior) => _behavior = behavior; - - private void SendTerminated() - { - var watchedBy = _watchedBy.GetAndSet(StageTerminatedTombstone); - if (watchedBy != StageTerminatedTombstone) - { - foreach (var actorRef in watchedBy.Cast()) - SendTerminated(actorRef); - - foreach (var actorRef in _watching.Cast()) - UnwatchWatched(actorRef); - - _watching = ImmutableHashSet.Empty; - } - } + public void Become(StageActorRef.Receive receive) => Volatile.Write(ref _behavior, receive); /// - /// TBD + /// Stops current . /// - /// TBD - public void Watch(IActorRef actorRef) - { - var iw = (IInternalActorRef) actorRef; - _watching = _watching.Add(actorRef); - iw.SendSystemMessage(new Watch(iw, this)); - } + public void Stop() => _cell.RemoveFunctionRef(_functionRef); /// - /// TBD + /// Makes current watch over given . + /// It will be notified when an underlying actor is . /// - /// TBD - public void Unwatch(IActorRef actorRef) - { - var iw = (IInternalActorRef)actorRef; - _watching = _watching.Remove(actorRef); - iw.SendSystemMessage(new Unwatch(iw, this)); - } - + /// + public void Watch(IActorRef actorRef) => _functionRef.Watch(actorRef); + /// - /// TBD + /// Makes current stop watching previously ed . + /// If was not watched over, this method has no result. /// - public override void Stop() => SendTerminated(); - - private void SendTerminated(IInternalActorRef actorRef) - => actorRef.SendSystemMessage(new DeathWatchNotification(this, true, false)); - - private void UnwatchWatched(IInternalActorRef actorRef) => actorRef.SendSystemMessage(new Unwatch(actorRef, this)); + /// + public void Unwatch(IActorRef actorRef) => _functionRef.Unwatch(actorRef); - private void AddWatcher(IInternalActorRef watchee, IInternalActorRef watcher) + internal void InternalReceive(Tuple pack) { - while (true) + if (pack.Item2 is Terminated terminated) { - var watchedBy = _watchedBy.Value; - if (watchedBy == StageTerminatedTombstone) - SendTerminated(watcher); - else + if (_functionRef.IsWatching(terminated.ActorRef)) { - var isWatcheeSelf = Equals(watchee, this); - var isWatcherSelf = Equals(watcher, this); - - if (isWatcheeSelf && !isWatcherSelf) - { - if (!watchedBy.Contains(watcher)) - if (!_watchedBy.CompareAndSet(watchedBy, watchedBy.Add(watcher))) - continue; // try again - } - else if (!isWatcheeSelf && isWatcherSelf) - Log.Warning("externally triggered watch from {0} to {1} is illegal on StageActorRef", - watcher, watchee); - else - Log.Error("BUG: illegal Watch({0}, {1}) for {2}", watchee, watcher, this); + _functionRef.Unwatch(terminated.ActorRef); + _behavior(pack); } - - break; } + else _behavior(pack); } - - private void RemoveWatcher(IInternalActorRef watchee, IInternalActorRef watcher) - { - while (true) - { - var watchedBy = _watchedBy.Value; - if (watchedBy == null) - SendTerminated(watcher); - else - { - var isWatcheeSelf = Equals(watchee, this); - var isWatcherSelf = Equals(watcher, this); - - if (isWatcheeSelf && !isWatcherSelf) - { - if (!watchedBy.Contains(watcher)) - if (!_watchedBy.CompareAndSet(watchedBy, watchedBy.Remove(watcher))) - continue; // try again - } - else if (!isWatcheeSelf && isWatcherSelf) - Log.Warning("externally triggered unwatch from {0} to {1} is illegal on StageActorRef", - watcher, watchee); - else - Log.Error("BUG: illegal Watch({0}, {1}) for {2}", watchee, watcher, this); - } - - break; - } - } - } - + } + /// /// /// This class wraps callback for instances and gracefully handles From bc601cdafb0a70ea60be72a37ed8b6ce1a697c17 Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Thu, 15 Feb 2018 07:28:17 +0100 Subject: [PATCH 04/70] removed post-rebase conflicts --- build.fsx | 6 ------ src/SharedAssemblyInfo.cs | 8 -------- src/common.props | 20 ++----------------- .../Akka.DistributedData.LightningDB.csproj | 3 --- .../Akka.DistributedData.LightningDB.nuspec | 20 ------------------- .../Akka.FSharp/Properties/AssemblyInfo.fs | 18 ----------------- 6 files changed, 2 insertions(+), 73 deletions(-) delete mode 100644 src/SharedAssemblyInfo.cs delete mode 100644 src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.nuspec delete mode 100644 src/core/Akka.FSharp/Properties/AssemblyInfo.fs diff --git a/build.fsx b/build.fsx index 56378203e8c..7b0e76866c1 100644 --- a/build.fsx +++ b/build.fsx @@ -356,12 +356,6 @@ Target "PublishMntr" (fun _ -> VersionSuffix = versionSuffix })) ) -//-------------------------------------------------------------------------------- -// Clean nuget directory - -Target "CleanNuget" <| fun _ -> - CleanDir nugetDir - Target "CreateMntrNuget" (fun _ -> // uses the template file to create a temporary .nuspec file with the correct version CopyFile "./src/core/Akka.MultiNodeTestRunner/Akka.MultiNodeTestRunner.nuspec" "./src/core/Akka.MultiNodeTestRunner/Akka.MultiNodeTestRunner.nuspec.template" diff --git a/src/SharedAssemblyInfo.cs b/src/SharedAssemblyInfo.cs deleted file mode 100644 index 7f3614bf7ca..00000000000 --- a/src/SharedAssemblyInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -// -using System.Reflection; - -[assembly: AssemblyCompanyAttribute("Akka.NET Team")] -[assembly: AssemblyCopyrightAttribute("Copyright © 2013-2017 Akka.NET Team")] -[assembly: AssemblyTrademarkAttribute("")] -[assembly: AssemblyVersionAttribute("1.2.0.0")] -[assembly: AssemblyFileVersionAttribute("1.2.0.0")] diff --git a/src/common.props b/src/common.props index f86aa05c5b6..bd12b64732c 100644 --- a/src/common.props +++ b/src/common.props @@ -2,7 +2,7 @@ Copyright © 2013-2017 Akka.NET Team Akka.NET Team - 1.3.4 + 1.3.5 http://getakka.net/images/akkalogo.png https://github.com/akkadotnet/akka.net https://github.com/akkadotnet/akka.net/blob/master/LICENSE @@ -17,22 +17,6 @@ true - Maintenance Release for Akka.NET 1.3** -Akka.NET v1.3.4 is a minor patch mostly focused on bugfixes. -Updates and Bugfixes** -1. [Akka: Ask interface should be clean](https://github.com/akkadotnet/akka.net/pull/3220) -1. [Akka.Cluster.Sharding: DData replicator is always assigned](https://github.com/akkadotnet/akka.net/issues/3297) -2. [Akka.Cluster: Akka.Cluster Group Routers don't respect role setting when running with allow-local-routees](https://github.com/akkadotnet/akka.net/issues/3294) -3. [Akka.Streams: Implement PartitionHub](https://github.com/akkadotnet/akka.net/pull/3287) -4. [Akka.Remote AkkaPduCodec performance fixes](https://github.com/akkadotnet/akka.net/pull/3299) -5. [Akka.Serialization.Hyperion updated](https://github.com/akkadotnet/akka.net/pull/3306) to [Hyperion v0.9.8](https://github.com/akkadotnet/Hyperion/releases/tag/v0.9.8) -You can see [the full set of changes for Akka.NET v1.3.4 here](https://github.com/akkadotnet/akka.net/milestone/22). -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 6 | 304 | 209 | Aaron Stannard | -| 1 | 250 | 220 | Maxim Cherednik | -| 1 | 1278 | 42 | Marc Piechura | -| 1 | 1 | 1 | zbynek001 | -| 1 | 1 | 1 | Vasily Kirichenko | + Placeholder for nightlies \ No newline at end of file diff --git a/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.csproj b/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.csproj index f26984e24a5..ab5b75aafda 100644 --- a/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.csproj +++ b/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.csproj @@ -18,9 +18,6 @@ - - - diff --git a/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.nuspec b/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.nuspec deleted file mode 100644 index aecdde5980d..00000000000 --- a/src/contrib/cluster/Akka.DistributedData.LightningDB/Akka.DistributedData.LightningDB.nuspec +++ /dev/null @@ -1,20 +0,0 @@ - - - - @project@ - @project@@title@ - @build.number@ - @authors@ - @authors@ - Replicated data using CRDT structures - https://github.com/akkadotnet/akka.net/blob/master/LICENSE - https://github.com/akkadotnet/akka.net - http://getakka.net/images/AkkaNetLogo.Normal.png - false - @releaseNotes@ - @copyright@ - @tags@ cluster crdt replication lightningdb - @dependencies@ - @references@ - - diff --git a/src/core/Akka.FSharp/Properties/AssemblyInfo.fs b/src/core/Akka.FSharp/Properties/AssemblyInfo.fs deleted file mode 100644 index 4f81d0454f3..00000000000 --- a/src/core/Akka.FSharp/Properties/AssemblyInfo.fs +++ /dev/null @@ -1,18 +0,0 @@ -namespace System -open System -open System.Reflection -open System.Runtime.InteropServices - -[] -[] -[] -[] -[] -[] -[] -[] -[] -do () - -module internal AssemblyVersionInformation = - let [] Version = "1.2.0.0" From 5873f1401692f4670ebd51eb66f8c30cc003aaac Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Thu, 15 Feb 2018 21:00:16 +0100 Subject: [PATCH 05/70] API Approvals + fixed GraphStage actor ref specs --- .../CoreAPISpec.ApproveCore.approved.txt | 7 +- .../CoreAPISpec.ApproveStreams.approved.txt | 26 ++-- .../Dsl/StageActorRefSpec.cs | 134 ++++++------------ src/core/Akka.Streams/Stage/GraphStage.cs | 4 +- 4 files changed, 66 insertions(+), 105 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index c23a80dddb0..3d7830e3ad1 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -112,6 +112,7 @@ namespace Akka.Actor protected void Stash(Akka.Dispatch.SysMsg.SystemMessage msg) { } public void Stop(Akka.Actor.IActorRef child) { } public void Stop() { } + protected void StopFunctionRefs() { } public void Suspend() { } protected void TellWatchersWeDied() { } public void TerminatedQueuedFor(Akka.Actor.IActorRef subject) { } @@ -1699,12 +1700,15 @@ namespace Akka.Actor protected override void PreRestart(System.Exception reason, object message) { } protected override bool Receive(object message) { } } - public sealed class Terminated : Akka.Actor.IAutoReceivedMessage, Akka.Actor.INoSerializationVerificationNeeded, Akka.Actor.IPossiblyHarmful, Akka.Event.IDeadLetterSuppression + public sealed class Terminated : Akka.Actor.IAutoReceivedMessage, Akka.Actor.INoSerializationVerificationNeeded, Akka.Actor.IPossiblyHarmful, Akka.Event.IDeadLetterSuppression, System.IEquatable { public Terminated(Akka.Actor.IActorRef actorRef, bool existenceConfirmed, bool addressTerminated) { } public Akka.Actor.IActorRef ActorRef { get; } public bool AddressTerminated { get; } public bool ExistenceConfirmed { get; } + public bool Equals(Akka.Actor.Terminated other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } public override string ToString() { } } public class TerminatedProps : Akka.Actor.Props @@ -4551,6 +4555,7 @@ namespace Akka.Util { public const string Base64Chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+~"; public static string Base64Encode(this long value) { } + public static System.Text.StringBuilder Base64Encode(this long value, System.Text.StringBuilder sb) { } public static string Base64Encode(this string s) { } } public class static BitArrayHelpers diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt index 7eaa8b30743..9ffd03e8b3b 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt @@ -4127,7 +4127,9 @@ namespace Akka.Streams.Stage public Akka.Event.ILoggingAdapter Log { get; } protected object LogSource { get; } protected Akka.Streams.IMaterializer Materializer { get; } - public Akka.Streams.Stage.StageActorRef StageActorRef { get; } + public Akka.Streams.Stage.StageActor StageActor { get; } + [Akka.Annotations.ApiMayChangeAttribute()] + protected virtual string StageActorName { get; } protected Akka.Streams.IMaterializer SubFusingMaterializer { get; } protected internal void AbortEmitting(Akka.Streams.Outlet outlet) { } protected void AbortReading(Akka.Streams.Inlet inlet) { } @@ -4152,7 +4154,7 @@ namespace Akka.Streams.Stage protected Akka.Streams.Stage.IInHandler GetHandler(Akka.Streams.Inlet inlet) { } protected Akka.Streams.Stage.IOutHandler GetHandler(Akka.Streams.Outlet outlet) { } [Akka.Annotations.ApiMayChangeAttribute()] - protected Akka.Streams.Stage.StageActorRef GetStageActorRef(Akka.Streams.Stage.StageActorRef.Receive receive) { } + protected Akka.Streams.Stage.StageActor GetStageActor(Akka.Streams.Stage.StageActorRef.Receive receive) { } protected internal T Grab(Akka.Streams.Inlet inlet) { } protected bool HasBeenPulled(Akka.Streams.Inlet inlet) { } protected internal bool IsAvailable(Akka.Streams.Inlet inlet) { } @@ -4400,21 +4402,17 @@ namespace Akka.Streams.Stage protected PushStage() { } public virtual Akka.Streams.Stage.ISyncDirective OnPull(Akka.Streams.Stage.IContext context) { } } - public sealed class StageActorRef : Akka.Actor.MinimalActorRef + public sealed class StageActor { - public readonly Akka.Event.ILoggingAdapter Log; - public static readonly Akka.Streams.Implementation.EnumerableActorName Name; - public readonly System.Collections.Immutable.IImmutableSet StageTerminatedTombstone; - public StageActorRef(Akka.Actor.IActorRefProvider provider, Akka.Event.ILoggingAdapter log, System.Func>> getAsyncCallback, Akka.Streams.Stage.StageActorRef.Receive initialReceive, Akka.Actor.ActorPath path) { } - public override bool IsTerminated { get; } - public override Akka.Actor.ActorPath Path { get; } - public override Akka.Actor.IActorRefProvider Provider { get; } - public void Become(Akka.Streams.Stage.StageActorRef.Receive behavior) { } - public override void SendSystemMessage(Akka.Dispatch.SysMsg.ISystemMessage message) { } - public override void Stop() { } - protected override void TellInternal(object message, Akka.Actor.IActorRef sender) { } + public StageActor(Akka.Streams.ActorMaterializer materializer, System.Func>> getAsyncCallback, Akka.Streams.Stage.StageActorRef.Receive initialReceive, string name = null) { } + public Akka.Actor.IActorRef Ref { get; } + public void Become(Akka.Streams.Stage.StageActorRef.Receive receive) { } + public void Stop() { } public void Unwatch(Akka.Actor.IActorRef actorRef) { } public void Watch(Akka.Actor.IActorRef actorRef) { } + } + public class static StageActorRef + { public delegate void Receive(System.Tuple args); } public class StageActorRefNotInitializedException : System.Exception diff --git a/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs b/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs index 88734ea1803..3f0e9026a0a 100644 --- a/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs @@ -34,26 +34,23 @@ private static GraphStageWithMaterializedValue, Task> SumSta => new SumTestStage(probe); [Fact] - public void A_Graph_stage_ActorRef_must_receive_messages() + public async Task A_Graph_stage_ActorRef_must_receive_messages() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); - var res = t.Item2; var stageRef = ExpectMsg(); stageRef.Tell(new Add(1)); stageRef.Tell(new Add(2)); stageRef.Tell(new Add(3)); - stageRef.Tell(StopNow.Instance); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(6); + + (await t.Item2).Should().Be(6); } [Fact] - public void A_Graph_stage_ActorRef_must_be_able_to_be_replied_to() + public async Task A_Graph_stage_ActorRef_must_be_able_to_be_replied_to() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); - var res = t.Item2; var stageRef = ExpectMsg(); stageRef.Tell(new AddAndTell(1)); @@ -63,15 +60,13 @@ public void A_Graph_stage_ActorRef_must_be_able_to_be_replied_to() ExpectMsg(10); stageRef.Tell(StopNow.Instance); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(10); + (await t.Item2).Should().Be(10); } [Fact] - public void A_Graph_stage_ActorRef_must_yield_the_same_self_ref_each_time() + public async Task A_Graph_stage_ActorRef_must_yield_the_same_self_ref_each_time() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); - var res = t.Item2; var stageRef = ExpectMsg(); stageRef.Tell(CallInitStageActorRef.Instance); @@ -85,16 +80,15 @@ public void A_Graph_stage_ActorRef_must_yield_the_same_self_ref_each_time() ExpectMsg(6); stageRef.Tell(StopNow.Instance); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(6); + + (await t.Item2).Should().Be(6); } [Fact] - public void A_Graph_stage_ActorRef_must_be_watchable() + public async Task A_Graph_stage_ActorRef_must_be_watchable() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; - var res = t.Item2; var stageRef = ExpectMsg(); Watch(stageRef); @@ -102,17 +96,15 @@ public void A_Graph_stage_ActorRef_must_be_watchable() stageRef.Tell(new Add(1)); source.SetResult(0); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(1); + (await t.Item2).Should().Be(1); ExpectTerminated(stageRef); } [Fact] - public void A_Graph_stage_ActorRef_must_be_able_to_become() + public async Task A_Graph_stage_ActorRef_must_be_able_to_become() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; - var res = t.Item2; var stageRef = ExpectMsg(); Watch(stageRef); @@ -123,24 +115,24 @@ public void A_Graph_stage_ActorRef_must_be_able_to_become() ExpectMsg("42"); source.SetResult(0); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(1); + (await t.Item2).Should().Be(1); + ExpectTerminated(stageRef); } [Fact] - public void A_Graph_stage_ActorRef_must_reply_Terminated_when_terminated_stage_is_watched() + public async Task A_Graph_stage_ActorRef_must_reply_Terminated_when_terminated_stage_is_watched() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; - var res = t.Item2; var stageRef = ExpectMsg(); Watch(stageRef); - stageRef.Tell(new Add(1)); + stageRef.Tell(new AddAndTell(1)); + ExpectMsg(1); source.SetResult(0); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(1); + + (await t.Item2).Should().Be(1); ExpectTerminated(stageRef); var p = CreateTestProbe(); @@ -149,30 +141,29 @@ public void A_Graph_stage_ActorRef_must_reply_Terminated_when_terminated_stage_i } [Fact] - public void A_Graph_stage_ActorRef_must_be_unwatchable() + public async Task A_Graph_stage_ActorRef_must_be_unwatchable() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; - var res = t.Item2; var stageRef = ExpectMsg(); Watch(stageRef); Unwatch(stageRef); - stageRef.Tell(new Add(1)); + stageRef.Tell(new AddAndTell(1)); + ExpectMsg(1); source.SetResult(0); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(1); + + (await t.Item2).Should().Be(1); ExpectNoMsg(100); } [Fact] - public void A_Graph_stage_ActorRef_must_ignore_and_log_warnings_for_PoisonPill_and_Kill_messages() + public async Task A_Graph_stage_ActorRef_must_ignore_and_log_warnings_for_PoisonPill_and_Kill_messages() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; - var res = t.Item2; var stageRef = ExpectMsg(); stageRef.Tell(new Add(40)); @@ -187,12 +178,12 @@ public void A_Graph_stage_ActorRef_must_ignore_and_log_warnings_for_PoisonPill_a warn.Message.ToString() .Should() .MatchRegex( - " message sent to StageActorRef\\(akka\\://AkkaSpec/user/StreamSupervisor-[0-9]+/StageActorRef-[0-9]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + " message sent to StageActor\\(akka\\://AkkaSpec/user/StreamSupervisor-[0-9]+/\\$\\$[a-z]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); #else warn.Message.ToString() .Should() .MatchRegex( - " message sent to StageActorRef\\(akka\\://StageActorRefSpec-[0-9]+/user/StreamSupervisor-[0-9]+/StageActorRef-[0-9]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + " message sent to StageActor\\(akka\\://StageActorRefSpec-[0-9]+/user/StreamSupervisor-[0-9]+/\\$\\$[a-z]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); #endif stageRef.Tell(Kill.Instance); warn = ExpectMsg(TimeSpan.FromSeconds(1)); @@ -201,36 +192,17 @@ public void A_Graph_stage_ActorRef_must_ignore_and_log_warnings_for_PoisonPill_a warn.Message.ToString() .Should() .MatchRegex( - " message sent to StageActorRef\\(akka\\://AkkaSpec/user/StreamSupervisor-[0-9]+/StageActorRef-[0-9]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + " message sent to StageActor\\(akka\\://AkkaSpec/user/StreamSupervisor-[0-9]+/\\$\\$[a-z]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); #else warn.Message.ToString() .Should() .MatchRegex( - " message sent to StageActorRef\\(akka\\://StageActorRefSpec-[0-9]+/user/StreamSupervisor-[0-9]+/StageActorRef-[0-9]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + " message sent to StageActor\\(akka\\://StageActorRefSpec-[0-9]+/user/StreamSupervisor-[0-9]+/\\$\\$[a-z]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); #endif source.SetResult(2); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(42); - } - - [Fact] - public void A_Graph_stage_ActorRef_must_be_able_to_watch_other_actors() - { - var killMe = ActorOf(dsl => { }, "KilMe"); - var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); - var source = t.Item1; - var res = t.Item2; - - var stageRef = ExpectMsg(); - stageRef.Tell(new WatchMe(killMe)); - stageRef.Tell(new Add(1)); - killMe.Tell(PoisonPill.Instance); - ExpectMsg().Watchee.Should().Be(killMe); - - source.SetResult(0); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(1); + + (await t.Item2).Should().Be(42); } private sealed class Add @@ -271,24 +243,6 @@ private sealed class StopNow public static readonly StopNow Instance = new StopNow(); private StopNow() { } } - private sealed class WatchMe - { - public WatchMe(IActorRef watchee) - { - Watchee = watchee; - } - - public IActorRef Watchee { get; } - } - private sealed class WatcheeTerminated - { - public WatcheeTerminated(IActorRef watchee) - { - Watchee = watchee; - } - - public IActorRef Watchee { get; } - } private class SumTestStage : GraphStageWithMaterializedValue, Task> { @@ -336,22 +290,26 @@ private void Behaviour(Tuple args) var msg = args.Item2; var sender = args.Item1; - msg.Match() - .With(a => _sum += a.N) - .With(() => Pull(_stage._inlet)) - .With(() => sender.Tell(GetStageActor(Behaviour), Self)) - .With(() => GetStageActor(tuple => tuple.Item1.Tell(tuple.Item2.ToString()))) - .With(() => + switch (msg) + { + case Add add: _sum += add.N; break; + case PullNow _: Pull(_stage._inlet); break; + case CallInitStageActorRef _: sender.Tell(GetStageActor(Behaviour).Ref, Self); break; + case BecomeStringEcho _: GetStageActor(tuple => { + var theSender = tuple.Item1; + var theMsg = tuple.Item2; + theSender.Tell(theMsg.ToString(), Self); + }); break; + case StopNow _: _promise.TrySetResult(_sum); CompleteStage(); - }).With(a => - { - _sum += a.N; + break; + case AddAndTell addAndTell: + _sum += addAndTell.N; sender.Tell(_sum, Self); - }) - .With(w => StageActor.Watch(w.Watchee)) - .With(t => _stage._probe.Tell(new WatcheeTerminated(t.ActorRef))); + break; + } } } #endregion diff --git a/src/core/Akka.Streams/Stage/GraphStage.cs b/src/core/Akka.Streams/Stage/GraphStage.cs index 8e261261d0c..aaff693aca4 100644 --- a/src/core/Akka.Streams/Stage/GraphStage.cs +++ b/src/core/Akka.Streams/Stage/GraphStage.cs @@ -2384,7 +2384,7 @@ public StageActor( StageActorRef.Receive initialReceive, string name = null) { - _callback = getAsyncCallback(initialReceive); + _callback = getAsyncCallback(InternalReceive); _behavior = initialReceive; switch (materializer.Supervisor) @@ -2400,7 +2400,7 @@ public StageActor( { case PoisonPill _: case Kill _: - materializer.Logger.Warning("{0} message sent to StageActor({1}) will be ignored, since it is not a real Actor." + + materializer.Logger.Warning("{0} message sent to StageActor({1}) will be ignored, since it is not a real Actor. " + "Use a custom message type to communicate with it instead.", message, _functionRef.Path); break; default: _callback(Tuple.Create(sender, message)); break; From de0243fad6a9ddc6ef9ebe85d9a62f8f548df48a Mon Sep 17 00:00:00 2001 From: Marc Piechura Date: Tue, 23 Jan 2018 20:17:57 +0100 Subject: [PATCH 06/70] Implement PartitonHub (#3287) * Implement PartitionHub * port docs * fix formatting * API approval * address remarks --- docs/articles/streams/stream-dynamic.md | 53 +- docs/articles/utilities/may-change.md | 28 + .../DocsExamples/Streams/HubsDocTests.cs | 101 +++ .../CoreAPISpec.ApproveStreams.approved.txt | 15 + src/core/Akka.Streams.Tests/Dsl/HubSpec.cs | 345 +++++++- src/core/Akka.Streams/Dsl/Hub.cs | 778 +++++++++++++++++- 6 files changed, 1278 insertions(+), 42 deletions(-) create mode 100644 docs/articles/utilities/may-change.md diff --git a/docs/articles/streams/stream-dynamic.md b/docs/articles/streams/stream-dynamic.md index 93c0ebd0061..47f92c33d2c 100644 --- a/docs/articles/streams/stream-dynamic.md +++ b/docs/articles/streams/stream-dynamic.md @@ -103,4 +103,55 @@ We now wrap the `Sink` and `Source` in a Flow using `Flow.FromSinkAndSource`. Th The resulting `Flow` now has a type of `Flow` representing a publish-subscribe channel which can be used any number of times to attach new producers or consumers. In addition, it materializes to a `UniqueKillSwitch` (see [UniqueKillSwitch](xref:streams-dynamic-handling#uniquekillswitch)) that can be used to deregister a single user externally: -[!code-csharp[HubsDocTests.cs](../../examples/DocsExamples/Streams/HubsDocTests.cs?name=pub-sub-4)] \ No newline at end of file +[!code-csharp[HubsDocTests.cs](../../examples/DocsExamples/Streams/HubsDocTests.cs?name=pub-sub-4)] + + +### Using the PartitionHub + +**This is a [may change](../utilities/may-change.md) feature** + +A `PartitionHub` can be used to route elements from a common producer to a dynamic set of consumers. +The selection of consumer is done with a function. Each element can be routed to only one consumer. + +The rate of the producer will be automatically adapted to the slowest consumer. In this case, the hub is a `Sink` +to which the single producer must be attached first. Consumers can only be attached once the `Sink` has +been materialized (i.e. the producer has been started). One example of using the `PartitionHub`: + +[!code-csharp[HubsDocTests.cs](../../examples/DocsExamples/Streams/HubsDocTests.cs?name=partition-hub)] + +The `partitioner` function takes two parameters; the first is the number of active consumers and the second +is the stream element. The function should return the index of the selected consumer for the given element, +i.e. `int` greater than or equal to 0 and less than number of consumers. + +The resulting `Source` can be materialized any number of times, each materialization effectively attaching +a new consumer. If there are no consumers attached to this hub then it will not drop any elements but instead +backpressure the upstream producer until consumers arrive. This behavior can be tweaked by using the combinators +`.Buffer` for example with a drop strategy, or just attaching a consumer that drops all messages. If there +are no other consumers, this will ensure that the producer is kept drained (dropping all elements) and once a new +consumer arrives and messages are routed to the new consumer it will adaptively slow down, ensuring no more messages +are dropped. + +It is possible to define how many initial consumers that are required before it starts emitting any messages +to the attached consumers. While not enough consumers have been attached messages are buffered and when the +buffer is full the upstream producer is backpressured. No messages are dropped. + +The above example illustrate a stateless partition function. For more advanced stateful routing the `StatefulSink` can be used. Here is an example of a stateful round-robin function: + +[!code-csharp[HubsDocTests.cs](../../examples/DocsExamples/Streams/HubsDocTests.cs?name=partition-hub-stateful)] + +Note that it is a factory of a function to to be able to hold stateful variables that are +unique for each materialization. + + +The function takes two parameters; the first is information about active consumers, including an array of +consumer identifiers and the second is the stream element. The function should return the selected consumer +identifier for the given element. The function will never be called when there are no active consumers, i.e. +there is always at least one element in the array of identifiers. + +Another interesting type of routing is to prefer routing to the fastest consumers. The `IConsumerInfo` +has an accessor `QueueSize` that is approximate number of buffered elements for a consumer. +Larger value than other consumers could be an indication of that the consumer is slow. +Note that this is a moving target since the elements are consumed concurrently. Here is an example of +a hub that routes to the consumer with least buffered elements: + +[!code-csharp[HubsDocTests.cs](../../examples/DocsExamples/Streams/HubsDocTests.cs?name=partition-hub-fastest)] \ No newline at end of file diff --git a/docs/articles/utilities/may-change.md b/docs/articles/utilities/may-change.md new file mode 100644 index 00000000000..447aacfd298 --- /dev/null +++ b/docs/articles/utilities/may-change.md @@ -0,0 +1,28 @@ +# Modules marked "May Change" + +To be able to introduce new modules and APIs without freezing them the moment they +are released we have introduced +the term **may change**. + +Concretely **may change** means that an API or module is in early access mode and that it: + + * is not guaranteed to be binary compatible in minor releases + * may have its API change in breaking ways in minor releases + * may be entirely dropped from Akka in a minor release + +Complete modules can be marked as **may change**, this will can be found in their module description and in the docs. + +Individual public APIs can be annotated with `Akka.Annotations.ApiMayChange` to signal that it has less +guarantees than the rest of the module it lives in. +Please use such methods and classes with care, however if you see such APIs that is the best point in time to try them +out and provide feedback (e.g. using the akka-user mailing list, GitHub issues or Gitter) before they are frozen as +fully stable API. + +Best effort migration guides may be provided, but this is decided on a case-by-case basis for **may change** modules. + +The purpose of this is to be able to release features early and +make them easily available and improve based on feedback, or even discover +that the module or API wasn't useful. + +These are the current complete modules marked as **may change**: + diff --git a/docs/examples/DocsExamples/Streams/HubsDocTests.cs b/docs/examples/DocsExamples/Streams/HubsDocTests.cs index d2cec66c265..58795c5ee0c 100644 --- a/docs/examples/DocsExamples/Streams/HubsDocTests.cs +++ b/docs/examples/DocsExamples/Streams/HubsDocTests.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Akka; using Akka.Streams; @@ -113,5 +114,105 @@ public void Hubs_must_demonstrate_combination() killSwitch.Shutdown(); #endregion } + + [Fact] + public void Hubs_must_demonstrate_creating_a_dynamic_partition_hub() + { + #region partition-hub + + // A simple producer that publishes a new "message-" every second + Source producer = Source.Tick(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1), "message") + .MapMaterializedValue(_ => NotUsed.Instance) + .ZipWith(Source.From(Enumerable.Range(1, 100)), (msg, i) => $"{msg}-{i}"); + + // Attach a PartitionHub Sink to the producer. This will materialize to a + // corresponding Source. + // (We need to use toMat and Keep.right since by default the materialized + // value to the left is used) + IRunnableGraph> runnableGraph = + producer.ToMaterialized(PartitionHub.Sink( + (size, element) => Math.Abs(element.GetHashCode()) % size, + startAfterNrOfConsumers: 2, bufferSize: 256), Keep.Right); + + // By running/materializing the producer, we get back a Source, which + // gives us access to the elements published by the producer. + Source fromProducer = runnableGraph.Run(Materializer); + + // Print out messages from the producer in two independent consumers + fromProducer.RunForeach(msg => Console.WriteLine("Consumer1: " + msg), Materializer); + fromProducer.RunForeach(msg => Console.WriteLine("Consumer2: " + msg), Materializer); + + #endregion + } + + [Fact] + public void Hubs_must_demonstrate_creating_a_dynamic_steful_partition_hub() + { + #region partition-hub-stateful + + // A simple producer that publishes a new "message-" every second + Source producer = Source.Tick(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1), "message") + .MapMaterializedValue(_ => NotUsed.Instance) + .ZipWith(Source.From(Enumerable.Range(1, 100)), (msg, i) => $"{msg}-{i}"); + + // New instance of the partitioner function and its state is created + // for each materialization of the PartitionHub. + Func RoundRobbin() + { + var i = -1L; + return (info, element) => + { + i++; + return info.ConsumerByIndex((int) (i % info.Size)); + }; + } + + // Attach a PartitionHub Sink to the producer. This will materialize to a + // corresponding Source. + // (We need to use toMat and Keep.right since by default the materialized + // value to the left is used) + IRunnableGraph> runnableGraph = + producer.ToMaterialized(PartitionHub.StatefulSink(RoundRobbin, + startAfterNrOfConsumers: 2, bufferSize: 256), Keep.Right); + + // By running/materializing the producer, we get back a Source, which + // gives us access to the elements published by the producer. + Source fromProducer = runnableGraph.Run(Materializer); + + // Print out messages from the producer in two independent consumers + fromProducer.RunForeach(msg => Console.WriteLine("Consumer1: " + msg), Materializer); + fromProducer.RunForeach(msg => Console.WriteLine("Consumer2: " + msg), Materializer); + + #endregion + } + + [Fact] + public void Hubs_must_demonstrate_creating_a_dynamic_partition_hub_routing_to_fastest_consumer() + { + #region partition-hub-fastest + + // A simple producer that publishes a new "message-" every second + Source producer = Source.From(Enumerable.Range(0, 100)); + + // Attach a PartitionHub Sink to the producer. This will materialize to a + // corresponding Source. + // (We need to use toMat and Keep.right since by default the materialized + // value to the left is used) + IRunnableGraph> runnableGraph = + producer.ToMaterialized(PartitionHub.StatefulSink( + () => ((info, element) => info.ConsumerIds.Min(info.QueueSize)), + startAfterNrOfConsumers: 2, bufferSize: 256), Keep.Right); + + // By running/materializing the producer, we get back a Source, which + // gives us access to the elements published by the producer. + Source fromProducer = runnableGraph.Run(Materializer); + + // Print out messages from the producer in two independent consumers + fromProducer.RunForeach(msg => Console.WriteLine("Consumer1: " + msg), Materializer); + fromProducer.Throttle(10, TimeSpan.FromMilliseconds(100), 10, ThrottleMode.Shaping) + .RunForeach(msg => Console.WriteLine("Consumer2: " + msg), Materializer); + + #endregion + } } } diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt index d46e956fc9d..7eaa8b30743 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt @@ -1428,6 +1428,21 @@ namespace Akka.Streams.Dsl public Akka.Streams.Outlet Out(int id) { } public override string ToString() { } } + public class static PartitionHub + { + [Akka.Annotations.ApiMayChangeAttribute()] + public static Akka.Streams.Dsl.Sink> Sink(System.Func partitioner, int startAfterNrOfConsumers, int bufferSize = 256) { } + [Akka.Annotations.ApiMayChangeAttribute()] + public static Akka.Streams.Dsl.Sink> StatefulSink(System.Func> partitioner, int startAfterNrOfConsumers, int bufferSize = 256) { } + [Akka.Annotations.ApiMayChangeAttribute()] + public interface IConsumerInfo + { + System.Collections.Immutable.ImmutableArray ConsumerIds { get; } + int Size { get; } + long ConsumerByIndex(int index); + int QueueSize(long consumerId); + } + } public sealed class PartitionOutOfBoundsException : System.Exception { public PartitionOutOfBoundsException(string message) { } diff --git a/src/core/Akka.Streams.Tests/Dsl/HubSpec.cs b/src/core/Akka.Streams.Tests/Dsl/HubSpec.cs index 119971b21b6..6c2e9204b68 100644 --- a/src/core/Akka.Streams.Tests/Dsl/HubSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/HubSpec.cs @@ -166,7 +166,8 @@ public void MergeHub_must_work_with_long_streams() { this.AssertAllStagesStopped(() => { - var t = MergeHub.Source(16).Take(20000).ToMaterialized(Sink.Seq(), Keep.Both).Run(Materializer); + var t = MergeHub.Source(16).Take(20000).ToMaterialized(Sink.Seq(), Keep.Both) + .Run(Materializer); var sink = t.Item1; var result = t.Item2; @@ -182,7 +183,8 @@ public void MergeHub_must_work_with_long_streams_when_buffer_size_is_1() { this.AssertAllStagesStopped(() => { - var t = MergeHub.Source(1).Take(20000).ToMaterialized(Sink.Seq(), Keep.Both).Run(Materializer); + var t = MergeHub.Source(1).Take(20000).ToMaterialized(Sink.Seq(), Keep.Both) + .Run(Materializer); var sink = t.Item1; var result = t.Item2; @@ -219,7 +221,8 @@ public void MergeHub_must_work_with_long_streams_if_one_of_the_producers_is_slow { this.AssertAllStagesStopped(() => { - var t = MergeHub.Source(16).Take(2000).ToMaterialized(Sink.Seq(), Keep.Both).Run(Materializer); + var t = MergeHub.Source(16).Take(2000).ToMaterialized(Sink.Seq(), Keep.Both) + .Run(Materializer); var sink = t.Item1; var result = t.Item2; @@ -569,5 +572,341 @@ public void BroadcastHub_must_properly_signal_error_to_consumers_arriving_after_ task.Invoking(t => t.Wait(TimeSpan.FromSeconds(3))).ShouldThrow(); }, Materializer); } + + [Fact] + public void PartitionHub_must_work_in_the_happy_case_with_one_stream() + { + this.AssertAllStagesStopped(() => + { + var items = Enumerable.Range(1, 10).ToList(); + var source = Source.From(items) + .RunWith(PartitionHub.Sink((size, e) => 0, 0, 8), Materializer); + var result = source.RunWith(Sink.Seq(), Materializer).AwaitResult(); + result.ShouldAllBeEquivalentTo(items); + }, Materializer); + } + + [Fact] + public void PartitionHub_must_work_in_the_happy_case_with_two_streams() + { + this.AssertAllStagesStopped(() => + { + var source = Source.From(Enumerable.Range(0, 10)) + .RunWith(PartitionHub.Sink((size, e) => e % size, 2, 8), Materializer); + var result1 = source.RunWith(Sink.Seq(), Materializer); + // it should not start publishing until startAfterNrOfConsumers = 2 + Thread.Sleep(50); + var result2 = source.RunWith(Sink.Seq(), Materializer); + result1.AwaitResult().ShouldAllBeEquivalentTo(new[] { 0, 2, 4, 6, 8 }); + result2.AwaitResult().ShouldAllBeEquivalentTo(new[] { 1, 3, 5, 7, 9 }); + }, Materializer); + } + + [Fact] + public void PartitionHub_must_be_able_to_use_as_rount_robin_router() + { + this.AssertAllStagesStopped(() => + { + var source = Source.From(Enumerable.Range(0, 10)) + .RunWith(PartitionHub.StatefulSink(() => + { + var n = 0L; + return ((info, e) => + { + n++; + return info.ConsumerByIndex((int)n % info.Size); + }); + }, 2, 8), Materializer); + var result1 = source.RunWith(Sink.Seq(), Materializer); + var result2 = source.RunWith(Sink.Seq(), Materializer); + result1.AwaitResult().ShouldAllBeEquivalentTo(new[] { 1, 3, 5, 7, 9 }); + result2.AwaitResult().ShouldAllBeEquivalentTo(new[] { 0, 2, 4, 6, 8 }); + }, Materializer); + } + + [Fact] + public void PartitionHub_must_be_able_to_use_as__sticky_session_rount_robin_router() + { + this.AssertAllStagesStopped(() => + { + var source = Source.From(new[] { "usr-1", "usr-2", "usr-1", "usr-3" }) + .RunWith(PartitionHub.StatefulSink(() => + { + var session = new Dictionary(); + var n = 0L; + return ((info, e) => + { + if (session.TryGetValue(e, out var i) && info.ConsumerIds.Contains(i)) + return i; + n++; + var id = info.ConsumerByIndex((int)n % info.Size); + session[e] = id; + return id; + }); + }, 2, 8), Materializer); + var result1 = source.RunWith(Sink.Seq(), Materializer); + var result2 = source.RunWith(Sink.Seq(), Materializer); + result1.AwaitResult().ShouldAllBeEquivalentTo(new[] { "usr-2" }); + result2.AwaitResult().ShouldAllBeEquivalentTo(new[] { "usr-1", "usr-1", "usr-3" }); + }, Materializer); + } + + [Fact] + public void PartitionHub_must_be_able_to_use_as_fastest_consumer_router() + { + this.AssertAllStagesStopped(() => + { + var items = Enumerable.Range(0, 999).ToList(); + var source = Source.From(items) + .RunWith( + PartitionHub.StatefulSink(() => ((info, i) => info.ConsumerIds.Min(info.QueueSize)), 2, 4), + Materializer); + var result1 = source.RunWith(Sink.Seq(), Materializer); + var result2 = source.Throttle(10, TimeSpan.FromMilliseconds(100), 10, ThrottleMode.Shaping) + .RunWith(Sink.Seq(), Materializer); + + result1.AwaitResult().Count.ShouldBeGreaterThan(result2.AwaitResult().Count); + }, Materializer); + } + + [Fact] + public void PartitionHub_must_route_evenly() + { + this.AssertAllStagesStopped(() => + { + var t = this.SourceProbe() + .ToMaterialized(PartitionHub.Sink((size, e) => e % size, 2, 8), Keep.Both) + .Run(Materializer); + + var testSource = t.Item1; + var hub = t.Item2; + var probe0 = hub.RunWith(this.SinkProbe(), Materializer); + var probe1 = hub.RunWith(this.SinkProbe(), Materializer); + + probe0.Request(3); + probe1.Request(10); + testSource.SendNext(0); + probe0.ExpectNext(0); + testSource.SendNext(1); + probe1.ExpectNext(1); + + testSource.SendNext(2); + testSource.SendNext(3); + testSource.SendNext(4); + probe0.ExpectNext(2); + probe1.ExpectNext(3); + probe0.ExpectNext(4); + + // probe1 has not requested more + testSource.SendNext(5); + testSource.SendNext(6); + testSource.SendNext(7); + probe1.ExpectNext(5); + probe1.ExpectNext(7); + probe0.ExpectNoMsg(TimeSpan.FromMilliseconds(50)); + probe0.Request(10); + probe0.ExpectNext(6); + + testSource.SendComplete(); + probe0.ExpectComplete(); + probe1.ExpectComplete(); + + }, Materializer); + } + + [Fact] + public void PartitionHub_must_route_unevenly() + { + this.AssertAllStagesStopped(() => + { + var t = this.SourceProbe() + .ToMaterialized(PartitionHub.Sink((size, e) => (e % 3) % 2, 2, 8), Keep.Both) + .Run(Materializer); + + var testSource = t.Item1; + var hub = t.Item2; + var probe0 = hub.RunWith(this.SinkProbe(), Materializer); + var probe1 = hub.RunWith(this.SinkProbe(), Materializer); + + // (_ % 3) % 2 + // 0 => 0 + // 1 => 1 + // 2 => 0 + // 3 => 0 + // 4 => 1 + + probe0.Request(10); + probe1.Request(10); + testSource.SendNext(0); + probe0.ExpectNext(0); + testSource.SendNext(1); + probe1.ExpectNext(1); + testSource.SendNext(2); + probe0.ExpectNext(2); + testSource.SendNext(3); + probe0.ExpectNext(3); + testSource.SendNext(4); + probe1.ExpectNext(4); + + testSource.SendComplete(); + probe0.ExpectComplete(); + probe1.ExpectComplete(); + + }, Materializer); + } + + [Fact] + public void PartitionHub_must_backpressure() + { + this.AssertAllStagesStopped(() => + { + var t = this.SourceProbe() + .ToMaterialized(PartitionHub.Sink((size, e) => 0, 2, 4), Keep.Both) + .Run(Materializer); + + var testSource = t.Item1; + var hub = t.Item2; + var probe0 = hub.RunWith(this.SinkProbe(), Materializer); + var probe1 = hub.RunWith(this.SinkProbe(), Materializer); + + probe0.Request(10); + probe1.Request(10); + testSource.SendNext(0); + probe0.ExpectNext(0); + testSource.SendNext(1); + probe0.ExpectNext(1); + testSource.SendNext(2); + probe0.ExpectNext(2); + testSource.SendNext(3); + probe0.ExpectNext(3); + testSource.SendNext(4); + probe0.ExpectNext(4); + + testSource.SendComplete(); + probe0.ExpectComplete(); + probe1.ExpectComplete(); + + }, Materializer); + } + + [Fact] + public void PartitionHub_must_ensure_that_from_two_different_speed_consumers_the_slower_controls_the_rate() + { + this.AssertAllStagesStopped(() => + { + var t = Source.Maybe().ConcatMaterialized(Source.From(Enumerable.Range(1, 19)), Keep.Left) + .ToMaterialized(PartitionHub.Sink((size, e) => e % size, 2, 1), Keep.Both) + .Run(Materializer); + var firstElement = t.Item1; + var source = t.Item2; + + var f1 = source.Throttle(1, TimeSpan.FromMilliseconds(10), 1, ThrottleMode.Shaping) + .RunWith(Sink.Seq(), Materializer); + + // Second cannot be overwhelmed since the first one throttles the overall rate, and second allows a higher rate + var f2 = source.Throttle(10, TimeSpan.FromMilliseconds(10), 8, ThrottleMode.Enforcing) + .RunWith(Sink.Seq(), Materializer); + + // Ensure subscription of Sinks. This is racy but there is no event we can hook into here. + Thread.Sleep(100); + // on the jvm Some 0 is used, unfortunately haven't we used Option for the Maybe source + // and therefore firstElement.SetResult(0) will complete the source without pushing an element + // since 0 is the default value for int and if you set the result to default(T) it will ignore + // the element and complete the source. We should probably fix this in the feature. + firstElement.SetResult(50); + + var expectationF1 = Enumerable.Range(1, 18).Where(v => v % 2 == 0).ToList(); + expectationF1.Insert(0, 50); + + f1.AwaitResult().ShouldAllBeEquivalentTo(expectationF1); + f2.AwaitResult().ShouldAllBeEquivalentTo(Enumerable.Range(1, 19).Where(v => v % 2 != 0)); + }, Materializer); + } + + [Fact] + public void PartitionHub_must_properly_signal_error_to_consumer() + { + this.AssertAllStagesStopped(() => + { + var upstream = this.CreatePublisherProbe(); + var source = Source.FromPublisher(upstream) + .RunWith(PartitionHub.Sink((s, e) => e % s, 2, 8), Materializer); + + var downstream1 = this.CreateSubscriberProbe(); + source.RunWith(Sink.FromSubscriber(downstream1), Materializer); + var downstream2 = this.CreateSubscriberProbe(); + source.RunWith(Sink.FromSubscriber(downstream2), Materializer); + + downstream1.Request(4); + downstream2.Request(8); + + Enumerable.Range(0, 16).ForEach(i => upstream.SendNext(i)); + + downstream1.ExpectNext(0, 2, 4, 6); + downstream2.ExpectNext(1, 3, 5, 7, 9, 11, 13, 15); + + downstream1.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + downstream2.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + + var failure = new TestException("Failed"); + upstream.SendError(failure); + + downstream1.ExpectError().Should().Be(failure); + downstream2.ExpectError().Should().Be(failure); + }, Materializer); + } + + [Fact] + public void PartitionHub_must_properly_signal_completion_to_consumers_arriving_after_producer_finished() + { + this.AssertAllStagesStopped(() => + { + var source = Source.Empty().RunWith(PartitionHub.Sink((s, e) => e % s, 0), Materializer); + // Wait enough so the Hub gets the completion. This is racy, but this is fine because both + // cases should work in the end + Thread.Sleep(50); + + source.RunWith(Sink.Seq(), Materializer).AwaitResult().Should().BeEmpty(); + }, Materializer); + } + + [Fact] + public void PartitionHub_must_remeber_completion_for_materialisations_after_completion() + { + var t = this.SourceProbe().ToMaterialized(PartitionHub.Sink((s, e) => 0, 0), Keep.Both) + .Run(Materializer); + var sourceProbe = t.Item1; + var source = t.Item2; + var sinkProbe = source.RunWith(this.SinkProbe(), Materializer); + + sourceProbe.SendComplete(); + + sinkProbe.Request(1); + sinkProbe.ExpectComplete(); + + // Materialize a second time. There was a race here, where we managed to enqueue our Source registration just + // immediately before the Hub shut down. + var sink2Probe = source.RunWith(this.SinkProbe(), Materializer); + + sink2Probe.Request(1); + sink2Probe.ExpectComplete(); + } + + [Fact] + public void PartitionHub_must_properly_signal_error_to_consumer_arriving_after_producer_finished() + { + this.AssertAllStagesStopped(() => + { + var failure = new TestException("Fail!"); + var source = Source.Failed(failure).RunWith(PartitionHub.Sink((s, e) => 0, 0), Materializer); + // Wait enough so the Hub gets the completion. This is racy, but this is fine because both + // cases should work in the end + Thread.Sleep(50); + + Action a = () => source.RunWith(Sink.Seq(), Materializer).AwaitResult(); + a.ShouldThrow().WithMessage("Fail!"); + }, Materializer); + } } + } diff --git a/src/core/Akka.Streams/Dsl/Hub.cs b/src/core/Akka.Streams/Dsl/Hub.cs index faf4e9c3726..8b525d86434 100644 --- a/src/core/Akka.Streams/Dsl/Hub.cs +++ b/src/core/Akka.Streams/Dsl/Hub.cs @@ -11,6 +11,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; +using Akka.Annotations; using Akka.Streams.Stage; using Akka.Util; using Akka.Util.Internal; @@ -77,7 +78,7 @@ public sealed class ProducerFailed : Exception /// The exception that is the cause of the current exception. public ProducerFailed(string message, Exception cause) : base(message, cause) { - + } } } @@ -117,7 +118,7 @@ public Register(long id, Action demandCallback) } public long Id { get; } - + public Action DemandCallback { get; } } @@ -229,7 +230,7 @@ private bool OnEvent(IEvent e) } public override void OnPull() => TryProcessNext(true); - + private void TryProcessNext(bool firstAttempt) { while (true) @@ -359,14 +360,14 @@ public override void PreStart() public override void PostStop() { // Unlike in the case of preStart, we don't care about the Hub no longer looking at the queue. - if(!_logic.IsShuttingDown) + if (!_logic.IsShuttingDown) _logic.Enqueue(new Deregister(_id)); } public override void OnPush() { _logic.Enqueue(new Element(_id, Grab(_stage.In))); - if(_demand > 0) + if (_demand > 0) PullWithDemand(); } @@ -390,7 +391,7 @@ private void OnDemand(long moreDemand) else { _demand += moreDemand; - if(!HasBeenPulled(_stage.In)) + if (!HasBeenPulled(_stage.In)) PullWithDemand(); } } @@ -434,7 +435,7 @@ public MergeHub(int perProducerBufferSize) throw new ArgumentException("Buffer size must be positive", nameof(perProducerBufferSize)); _perProducerBufferSize = perProducerBufferSize; - DemandThreshold = perProducerBufferSize/2 + perProducerBufferSize%2; + DemandThreshold = perProducerBufferSize / 2 + perProducerBufferSize % 2; Shape = new SourceShape(Out); } @@ -480,7 +481,7 @@ public class BroadcastHub /// Creates a that receives elements from its upstream producer and broadcasts them to a dynamic set /// of consumers. After the returned by this method is materialized, it returns a as materialized /// value. This can be materialized arbitrary many times and each materialization will receive the - /// broadcast elements form the original . + /// broadcast elements from the original . /// /// Every new materialization of the results in a new, independent hub, which materializes to its own /// for consuming the of that materialization. @@ -525,7 +526,6 @@ public static Sink> Sink(int bufferSize) /// /// INTERNAL API /// - /// TBD internal class BroadcastHub : GraphStageWithMaterializedValue, Source> { #region internal classes @@ -538,7 +538,7 @@ private sealed class RegistrationPending : IHubEvent private RegistrationPending() { - + } } @@ -601,7 +601,7 @@ public Consumer(long id, Action callback) } - private sealed class Completed + private sealed class Completed { public static Completed Instance { get; } = new Completed(); @@ -680,7 +680,7 @@ private sealed class HubLogic : InGraphStageLogic private readonly TaskCompletionSource> _callbackCompletion = new TaskCompletionSource>(); - + private readonly Open _noRegistrationState; internal readonly AtomicReference State; @@ -714,7 +714,7 @@ public HubLogic(BroadcastHub stage) : base(stage.Shape) _noRegistrationState = new Open(_callbackCompletion.Task, ImmutableList.Empty); State = new AtomicReference(_noRegistrationState); _queue = new object[stage._bufferSize]; - _consumerWheel = Enumerable.Repeat(0, stage._bufferSize*2) + _consumerWheel = Enumerable.Repeat(0, stage._bufferSize * 2) .Select(_ => ImmutableList.Empty) .ToArray(); @@ -738,7 +738,7 @@ public override void OnUpstreamFinish() public override void OnPush() { Publish(Grab(_stage.In)); - if(!IsFull) + if (!IsFull) Pull(_stage.In); } @@ -746,14 +746,15 @@ private void OnEvent(IHubEvent hubEvent) { if (hubEvent == RegistrationPending.Instance) { - var open = (Open) State.GetAndSet(_noRegistrationState); - open.Registrations.ForEach(c => + var open = (Open)State.GetAndSet(_noRegistrationState); + foreach (var c in open.Registrations) { var startFrom = _head; _activeConsumer++; AddConsumer(c, startFrom); c.Callback(new Initialize(startFrom)); - }); + } + return; } @@ -763,7 +764,7 @@ private void OnEvent(IHubEvent hubEvent) FindAndRemoveConsumer(unregister.Id, unregister.PreviousOffset); if (_activeConsumer == 0) { - if(IsClosed(_stage.In)) + if (IsClosed(_stage.In)) CompleteStage(); else if (_head != unregister.FinalOffset) { @@ -788,7 +789,7 @@ private void OnEvent(IHubEvent hubEvent) if (hubEvent is Advanced advance) { - var newOffset = advance.PreviousOffset + _stage.DemandThreshold; + var newOffset = advance.PreviousOffset + _stage._demandThreshold; // Move the consumer from its last known offset to its new one. Check if we are unblocked. var c = FindAndRemoveConsumer(advance.Id, advance.PreviousOffset); AddConsumer(c, newOffset); @@ -797,7 +798,7 @@ private void OnEvent(IHubEvent hubEvent) } // only NeedWakeup left - var wakeup = (NeedWakeup) hubEvent; + var wakeup = (NeedWakeup)hubEvent; // Move the consumer from its last known offset to its new one. Check if we are unblocked. var consumer = FindAndRemoveConsumer(wakeup.Id, wakeup.PreviousOffset); AddConsumer(consumer, wakeup.CurrentOffset); @@ -818,8 +819,8 @@ public override void OnUpstreamFailure(Exception e) var failMessage = new HubCompleted(e); // Notify pending consumers and set tombstone - var open = (Open) State.GetAndSet(new Closed(e)); - open.Registrations.ForEach(c=>c.Callback(failMessage)); + var open = (Open)State.GetAndSet(new Closed(e)); + open.Registrations.ForEach(c => c.Callback(failMessage)); // Notify registered consumers _consumerWheel.SelectMany(x => x).ForEach(c => c.Callback(failMessage)); @@ -862,7 +863,7 @@ private void CheckUnblock(int offsetOfConsumerRemoved) { if (UnblockIfPossible(offsetOfConsumerRemoved)) { - if(IsClosed(_stage.In)) + if (IsClosed(_stage.In)) Complete(); else if (!HasBeenPulled(_stage.In)) Pull(_stage.In); @@ -899,7 +900,7 @@ private void AddConsumer(Consumer consumer, int offset) /// which is offset modulo (bufferSize + 1). /// /// TBD - private void WakeupIndex(int index) + private void WakeupIndex(int index) => _consumerWheel[index].ForEach(c => c.Callback(Wakeup.Instance)); private void Complete() @@ -978,7 +979,7 @@ public Logic(HubSourceLogic stage, long id) : base(stage.Shape) { _stage = stage; _id = id; - _untilNextAdvanceSignal = stage._hub.DemandThreshold; + _untilNextAdvanceSignal = stage._hub._demandThreshold; SetHandler(stage.Out, this); } @@ -1012,7 +1013,7 @@ void OnHubReady(Result> result) var state = _stage._hubLogic.State.Value; if (state is Closed closed) { - if(closed.Failure != null) + if (closed.Failure != null) FailStage(closed.Failure); else CompleteStage(); @@ -1047,7 +1048,7 @@ public override void OnPull() { _hubCallback(new NeedWakeup(_id, _previousPublishedOffset, _offset)); _previousPublishedOffset = _offset; - _untilNextAdvanceSignal = _stage._hub.DemandThreshold; + _untilNextAdvanceSignal = _stage._hub._demandThreshold; } else if (element == Completed.Instance) CompleteStage(); @@ -1058,9 +1059,9 @@ public override void OnPull() _untilNextAdvanceSignal--; if (_untilNextAdvanceSignal == 0) { - _untilNextAdvanceSignal = _stage._hub.DemandThreshold; + _untilNextAdvanceSignal = _stage._hub._demandThreshold; var previousOffset = _previousPublishedOffset; - _previousPublishedOffset += _stage._hub.DemandThreshold; + _previousPublishedOffset += _stage._hub._demandThreshold; _hubCallback(new Advanced(_id, previousOffset)); } } @@ -1069,12 +1070,12 @@ public override void OnPull() public override void PostStop() => _hubCallback?.Invoke(new UnRegister(_id, _previousPublishedOffset, _offset)); - + private void OnCommand(IConsumerEvent e) { if (e is HubCompleted completed) { - if(completed.Failure != null) + if (completed.Failure != null) FailStage(completed.Failure); else CompleteStage(); @@ -1117,6 +1118,7 @@ protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) private readonly int _bufferSize; private readonly int _mask; private readonly int _wheelMask; + private readonly int _demandThreshold; /// /// TBD @@ -1137,17 +1139,14 @@ public BroadcastHub(int bufferSize) _bufferSize = bufferSize; _mask = _bufferSize - 1; - _wheelMask = bufferSize*2 - 1; - DemandThreshold = bufferSize / 2 + bufferSize % 2; + _wheelMask = bufferSize * 2 - 1; + + // Half of buffer size, rounded up + _demandThreshold = bufferSize / 2 + bufferSize % 2; Shape = new SinkShape(In); } - /// - /// Half of buffer size, rounded up - /// - private int DemandThreshold { get; } - private Inlet In { get; } = new Inlet("BroadcastHub.in"); /// @@ -1169,4 +1168,707 @@ public override ILogicAndMaterializedValue> CreateLogicAndMat return new LogicAndMaterializedValue>(logic, Source.FromGraph(source)); } } + + /// + /// A is a special streaming hub that is able to route streamed elements to a dynamic set of consumers. + /// It consists of two parts, a and a . The e elements from a producer to the + /// actually live consumers it has.The selection of consumer is done with a function. Each element can be routed to + /// only one consumer.Once the producer has been materialized, the it feeds into returns a + /// materialized value which is the corresponding . This can be materialized an arbitrary number + /// of times, where each of the new materializations will receive their elements from the original . + /// + public static class PartitionHub + { + private const int DefaultBufferSize = 256; + + /// + /// Creates a that receives elements from its upstream producer and routes them to a dynamic set + /// of consumers.After the returned by this method is materialized, it returns a + /// as materialized value. + /// This can be materialized an arbitrary number of times and each materialization will receive the + /// elements from the original . + /// + /// Every new materialization of the results in a new, independent hub, which materializes to its own + /// for consuming the of that materialization. + /// + /// If the original is failed, then the failure is immediately propagated to all of its materialized + /// s (possibly jumping over already buffered elements). If the original is completed, then + /// all corresponding s are completed.Both failure and normal completion is "remembered" and later + /// materializations of the will see the same (failure or completion) state. s that are + /// cancelled are simply removed from the dynamic set of consumers. + /// + /// This should be used when there is a need to keep mutable state in the partition function, + /// e.g. for implemening round-robin or sticky session kind of routing. If state is not needed the can + /// be more convenient to use. + /// + /// + /// Function that decides where to route an element.It is a factory of a function to + /// to be able to hold stateful variables that are unique for each materialization.The function + /// takes two parameters; the first is information about active consumers, including an array of consumer + /// identifiers and the second is the stream element.The function should return the selected consumer + /// identifier for the given element.The function will never be called when there are no active consumers, + /// i.e.there is always at least one element in the array of identifiers. + /// + /// + /// Elements are buffered until this number of consumers have been connected. + /// This is only used initially when the stage is starting up, i.e.it is not honored when consumers have been removed (canceled). + /// + /// Total number of elements that can be buffered. If this buffer is full, the producer is backpressured. + [ApiMayChange] + public static Sink> StatefulSink(Func> partitioner, + int startAfterNrOfConsumers, int bufferSize = DefaultBufferSize) + { + return Dsl.Sink.FromGraph(new PartitionHub(partitioner, startAfterNrOfConsumers, bufferSize)); + } + + /// + /// Creates a that receives elements from its upstream producer and routes them to a dynamic set + /// of consumers.After the returned by this method is materialized, it returns a + /// as materialized value. + /// This can be materialized an arbitrary number of times and each materialization will receive the + /// elements from the original . + /// + /// Every new materialization of the results in a new, independent hub, which materializes to its own + /// for consuming the of that materialization. + /// + /// If the original is failed, then the failure is immediately propagated to all of its materialized + /// s (possibly jumping over already buffered elements). If the original is completed, then + /// all corresponding s are completed.Both failure and normal completion is "remembered" and later + /// materializations of the will see the same (failure or completion) state. s that are + /// cancelled are simply removed from the dynamic set of consumers. + /// + /// This should be used when the routing function is stateless, e.g. based on a hashed value of the + /// elements. Otherwise the can be used to implement more advanced routing logic. + /// + /// + /// Function that decides where to route an element. The function takes two parameters; + /// the first is the number of active consumers and the second is the stream element. The function should + /// return the index of the selected consumer for the given element, i.e. int greater than or equal to 0 + /// and less than number of consumers. E.g. `(size, elem) => math.abs(elem.hashCode) % size`. + /// + /// + /// Elements are buffered until this number of consumers have been connected. + /// This is only used initially when the stage is starting up, i.e.it is not honored when consumers have been removed (canceled). + /// + /// Total number of elements that can be buffered. If this buffer is full, the producer is backpressured. + [ApiMayChange] + public static Sink> Sink(Func partitioner, + int startAfterNrOfConsumers, int bufferSize = DefaultBufferSize) + { + return StatefulSink(() => ((info, element) => info.ConsumerByIndex(partitioner(info.Size, element))), + startAfterNrOfConsumers, bufferSize); + } + + /// + /// DO NOT INHERIT + /// + [ApiMayChange] + public interface IConsumerInfo + { + /// + /// Sequence of all identifiers of current consumers. + /// + /// Use this method only if you need to enumerate consumer existing ids. + /// When selecting a specific consumerId by its index, prefer using the dedicated method instead, + /// which is optimised for this use case. + /// + ImmutableArray ConsumerIds { get; } + + /// + /// Obtain consumer identifier by index + /// + long ConsumerByIndex(int index); + + /// + /// Approximate number of buffered elements for a consumer. + /// Larger value than other consumers could be an indication of that the consumer is slow. + /// + /// Note that this is a moving target since the elements are consumed concurrently. + /// + int QueueSize(long consumerId); + + /// + /// Number of attached consumers. + /// + int Size { get; } + } + } + + /// + /// INTERNAL API + /// + internal class PartitionHub : GraphStageWithMaterializedValue, Source> + { + #region queue implementation + + private interface IPartitionQueue + { + void Init(long id); + int TotalSize { get; } + int Size(long id); + bool IsEmpty(long id); + bool NonEmpty(long id); + void Offer(long id, object element); + object Poll(long id); + void Remove(long id); + } + + private sealed class ConsumerQueue + { + public static ConsumerQueue Empty { get; } = new ConsumerQueue(ImmutableQueue.Empty, 0); + + private readonly ImmutableQueue _queue; + + public ConsumerQueue(ImmutableQueue queue, int size) + { + _queue = queue; + Size = size; + } + + public ConsumerQueue Enqueue(object element) => new ConsumerQueue(_queue.Enqueue(element), Size + 1); + + public bool IsEmpty => Size == 0; + + public object Head => _queue.First(); + + public ConsumerQueue Tail => new ConsumerQueue(_queue.Dequeue(), Size - 1); + + public int Size { get; } + } + + private sealed class PartitionQueue : IPartitionQueue + { + private readonly AtomicCounter _totalSize = new AtomicCounter(); + private readonly ConcurrentDictionary _queues = new ConcurrentDictionary(); + + public void Init(long id) => _queues.TryAdd(id, ConsumerQueue.Empty); + + public int TotalSize => _totalSize.Current; + + public int Size(long id) + { + if (_queues.TryGetValue(id, out var queue)) + return queue.Size; + + throw new ArgumentException($"Invalid stream identifier: {id}", nameof(id)); + } + + public bool IsEmpty(long id) + { + if (_queues.TryGetValue(id, out var queue)) + return queue.IsEmpty; + + throw new ArgumentException($"Invalid stream identifier: {id}", nameof(id)); + } + + public bool NonEmpty(long id) => !IsEmpty(id); + + public void Offer(long id, object element) + { + if (_queues.TryGetValue(id, out var queue)) + { + if (_queues.TryUpdate(id, queue.Enqueue(element), queue)) + _totalSize.IncrementAndGet(); + else + Offer(id, element); + } + else + throw new ArgumentException($"Invalid stream identifier: {id}", nameof(id)); + } + + public object Poll(long id) + { + var success = _queues.TryGetValue(id, out var queue); + if (!success || queue.IsEmpty) + return null; + + if (_queues.TryUpdate(id, queue.Tail, queue)) + { + _totalSize.Decrement(); + return queue.Head; + } + + return Poll(id); + } + + public void Remove(long id) + { + if (_queues.TryRemove(id, out var queue)) + _totalSize.AddAndGet(-queue.Size); + } + } + + #endregion + + #region internal classes + + private interface IConsumerEvent { } + + private sealed class Wakeup : IConsumerEvent + { + public static Wakeup Instance { get; } = new Wakeup(); + + private Wakeup() { } + } + + private sealed class Initialize : IConsumerEvent + { + public static Initialize Instance { get; } = new Initialize(); + + private Initialize() { } + } + + private sealed class HubCompleted : IConsumerEvent + { + public Exception Failure { get; } + + public HubCompleted(Exception failure) + { + Failure = failure; + } + } + + + private interface IHubEvent { } + + private sealed class RegistrationPending : IHubEvent + { + public static RegistrationPending Instance { get; } = new RegistrationPending(); + + private RegistrationPending() { } + } + + private sealed class UnRegister : IHubEvent + { + public long Id { get; } + + public UnRegister(long id) + { + Id = id; + } + } + + private sealed class NeedWakeup : IHubEvent + { + public Consumer Consumer { get; } + + public NeedWakeup(Consumer consumer) + { + Consumer = consumer; + } + + } + + private sealed class Consumer : IHubEvent + { + public long Id { get; } + public Action Callback { get; } + + public Consumer(long id, Action callback) + { + Id = id; + Callback = callback; + } + } + + private sealed class TryPull : IHubEvent + { + public static TryPull Instance { get; } = new TryPull(); + + private TryPull() { } + } + + private sealed class Completed + { + public static Completed Instance { get; } = new Completed(); + + private Completed() { } + } + + + private interface IHubState { } + + private sealed class Open : IHubState + { + public Task> CallbackTask { get; } + public ImmutableList Registrations { get; } + + public Open(Task> callbackTask, ImmutableList registrations) + { + CallbackTask = callbackTask; + Registrations = registrations; + } + } + + private sealed class Closed : IHubState + { + public Exception Failure { get; } + + public Closed(Exception failure) + { + Failure = failure; + } + } + + #endregion + + private sealed class PartitionSinkLogic : InGraphStageLogic + { + private sealed class ConsumerInfo : PartitionHub.IConsumerInfo + { + private readonly PartitionSinkLogic _partitionSinkLogic; + + public ConsumerInfo(PartitionSinkLogic partitionSinkLogic, ImmutableList consumers) + { + _partitionSinkLogic = partitionSinkLogic; + Consumers = consumers; + ConsumerIds = Consumers.Select(c => c.Id).ToImmutableArray(); + Size = consumers.Count; + } + + public ImmutableArray ConsumerIds { get; } + + public long ConsumerByIndex(int index) => Consumers[index].Id; + + public int QueueSize(long consumerId) => _partitionSinkLogic._queue.Size(consumerId); + + public int Size { get; } + + public ImmutableList Consumers { get; } + } + + private readonly PartitionHub _hub; + private readonly int _demandThreshold; + private readonly Func _materializedPartitioner; + private readonly TaskCompletionSource> _callbackCompletion = new TaskCompletionSource>(); + private readonly IHubState _noRegistrationsState; + private bool _initialized; + private readonly IPartitionQueue _queue = new PartitionQueue(); + private readonly List _pending = new List(); + private ConsumerInfo _consumerInfo; + private readonly Dictionary _needWakeup = new Dictionary(); + private long _callbackCount; + + public PartitionSinkLogic(PartitionHub hub) : base(hub.Shape) + { + _hub = hub; + // Half of buffer size, rounded up + _demandThreshold = hub._bufferSize / 2 + hub._bufferSize % 2; + _materializedPartitioner = hub._partitioner(); + _noRegistrationsState = new Open(_callbackCompletion.Task, ImmutableList.Empty); + _consumerInfo = new ConsumerInfo(this, ImmutableList.Empty); + + State = new AtomicReference(_noRegistrationsState); + + SetHandler(hub.In, this); + } + + public override void PreStart() + { + SetKeepGoing(true); + _callbackCompletion.SetResult(GetAsyncCallback(OnEvent)); + + if (_hub._startAfterNrOfConsumers == 0) + Pull(_hub.In); + } + + public override void OnPush() + { + Publish(Grab(_hub.In)); + if (!IsFull) Pull(_hub.In); + } + + private bool IsFull => _queue.TotalSize + _pending.Count >= _hub._bufferSize; + + public AtomicReference State { get; } + + private void Publish(T element) + { + if (!_initialized || _consumerInfo.Consumers.Count == 0) + { + // will be published when first consumers are registered + _pending.Add(element); + } + else + { + var id = _materializedPartitioner(_consumerInfo, element); + _queue.Offer(id, element); + Wakeup(id); + } + } + + private void Wakeup(long id) + { + if (_needWakeup.TryGetValue(id, out var consumer)) + { + _needWakeup.Remove(consumer.Id); + consumer.Callback(PartitionHub.Wakeup.Instance); + } + } + + public override void OnUpstreamFinish() + { + if (_consumerInfo.Consumers.Count == 0) + CompleteStage(); + else + { + foreach (var consumer in _consumerInfo.Consumers) + Complete(consumer.Id); + } + } + + private void Complete(long id) + { + _queue.Offer(id, Completed.Instance); + Wakeup(id); + } + + private void TryPull() + { + if (_initialized && !IsClosed(_hub.In) && !HasBeenPulled(_hub.In) && !IsFull) + Pull(_hub.In); + } + + private void OnEvent(IHubEvent e) + { + _callbackCount++; + + if (e is NeedWakeup n) + { + // Also check if the consumer is now unblocked since we published an element since it went asleep. + if (_queue.NonEmpty(n.Consumer.Id)) + n.Consumer.Callback(PartitionHub.Wakeup.Instance); + else + { + _needWakeup[n.Consumer.Id] = n.Consumer; + TryPull(); + } + } + else if (e is TryPull) + TryPull(); + else if (e is RegistrationPending) + { + var o = (Open)State.GetAndSet(_noRegistrationsState); + foreach (var consumer in o.Registrations) + { + var newConsumers = _consumerInfo.Consumers.Add(consumer).Sort((c1, c2) => c1.Id.CompareTo(c2.Id)); + _consumerInfo = new ConsumerInfo(this, newConsumers); + _queue.Init(consumer.Id); + if (newConsumers.Count >= _hub._startAfterNrOfConsumers) + _initialized = true; + + consumer.Callback(Initialize.Instance); + + if (_initialized && _pending.Count != 0) + { + foreach (var p in _pending) + Publish(p); + + _pending.Clear(); + } + + TryPull(); + } + } + else if (e is UnRegister u) + { + var newConsumers = _consumerInfo.Consumers.RemoveAll(c => c.Id == u.Id); + _consumerInfo = new ConsumerInfo(this, newConsumers); + _queue.Remove(u.Id); + if (newConsumers.IsEmpty) + { + if (IsClosed(_hub.In)) + CompleteStage(); + } + else + TryPull(); + } + } + + public override void OnUpstreamFailure(Exception e) + { + var failMessage = new HubCompleted(e); + + // Notify pending consumers and set tombstone + var o = (Open)State.GetAndSet(new Closed(e)); + foreach (var consumer in o.Registrations) + consumer.Callback(failMessage); + + // Notify registered consumers + foreach (var consumer in _consumerInfo.Consumers) + consumer.Callback(failMessage); + + FailStage(e); + } + + public override void PostStop() + { + // Notify pending consumers and set tombstone + + var s = State.Value; + if (s is Open o) + { + if (State.CompareAndSet(o, new Closed(null))) + { + var completeMessage = new HubCompleted(null); + foreach (var consumer in o.Registrations) + consumer.Callback(completeMessage); + } + else + PostStop(); + } + // Already closed, ignore + } + + // Consumer API + public object Poll(long id, Action hubCallback) + { + // try pull via async callback when half full + // this is racy with other threads doing poll but doesn't matter + if (_queue.TotalSize == _demandThreshold) + hubCallback(PartitionHub.TryPull.Instance); + + return _queue.Poll(id); + } + } + + private sealed class PartitionSource : GraphStage> + { + private sealed class Logic : OutGraphStageLogic + { + private readonly PartitionSource _source; + private readonly long _id; + private readonly Consumer _consumer; + private long _callbackCount; + private Action _hubCallback; + + public Logic(PartitionSource source) : base(source.Shape) + { + _source = source; + _id = source._counter.IncrementAndGet(); + var callback = GetAsyncCallback(OnCommand); + _consumer = new Consumer(_id, callback); + + SetHandler(source._out, this); + } + + public override void PreStart() + { + void OnHubReady(Task> t) + { + if (t.IsCanceled || t.IsFaulted) + FailStage(t.Exception); + else + { + _hubCallback = t.Result; + _hubCallback(RegistrationPending.Instance); + if (IsAvailable(_source._out)) + OnPull(); + } + } + + void Register() + { + var s = _source._logic.State.Value; + if (s is Closed c) + { + if (c.Failure != null) + FailStage(c.Failure); + else + CompleteStage(); + return; + } + + var o = (Open)s; + var newRegistrations = o.Registrations.Add(_consumer); + if (_source._logic.State.CompareAndSet(o, new Open(o.CallbackTask, newRegistrations))) + { + var callback = GetAsyncCallback>>(OnHubReady); + o.CallbackTask.ContinueWith(callback); + } + else Register(); + } + + Register(); + } + + public override void OnPull() + { + if (_hubCallback == null) return; + + var element = _source._logic.Poll(_id, _hubCallback); + if (element == null) + _hubCallback(new NeedWakeup(_consumer)); + else if (element is Completed) + CompleteStage(); + else + Push(_source._out, (T)element); + } + + public override void PostStop() => _hubCallback?.Invoke(new UnRegister(_id)); + + private void OnCommand(IConsumerEvent command) + { + _callbackCount++; + switch (command) + { + case HubCompleted c when c.Failure != null: + FailStage(c.Failure); + break; + case HubCompleted _: + CompleteStage(); + break; + case Wakeup _: + if (IsAvailable(_source._out)) + OnPull(); + break; + case Initialize _: + if (IsAvailable(_source._out) && _hubCallback != null) + OnPull(); + break; + } + } + } + + private readonly AtomicCounterLong _counter; + private readonly PartitionSinkLogic _logic; + private readonly Outlet _out = new Outlet("PartitionHub.out"); + + public PartitionSource(AtomicCounterLong counter, PartitionSinkLogic logic) + { + _counter = counter; + _logic = logic; + Shape = new SourceShape(_out); + } + + public override SourceShape Shape { get; } + + protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) => new Logic(this); + } + + private readonly Func> _partitioner; + private readonly int _startAfterNrOfConsumers; + private readonly int _bufferSize; + + public PartitionHub(Func> partitioner, int startAfterNrOfConsumers, int bufferSize) + { + _partitioner = partitioner; + _startAfterNrOfConsumers = startAfterNrOfConsumers; + _bufferSize = bufferSize; + Shape = new SinkShape(In); + } + + public Inlet In { get; } = new Inlet("PartitionHub.in"); + + public override SinkShape Shape { get; } + + public override ILogicAndMaterializedValue> CreateLogicAndMaterializedValue(Attributes inheritedAttributes) + { + var idCounter = new AtomicCounterLong(); + var logic = new PartitionSinkLogic(this); + var source = new PartitionSource(idCounter, logic); + + return new LogicAndMaterializedValue>(logic, Source.FromGraph(source)); + } + } } From ce969434af78dc3a79395e91f16001243266edb3 Mon Sep 17 00:00:00 2001 From: Vasily Kirichenko Date: Tue, 23 Jan 2018 22:18:22 +0300 Subject: [PATCH 07/70] fix Source.Queue XML doc (#3291) --- src/core/Akka.Streams/Dsl/Source.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Akka.Streams/Dsl/Source.cs b/src/core/Akka.Streams/Dsl/Source.cs index 465603ac61f..95662cbbebd 100644 --- a/src/core/Akka.Streams/Dsl/Source.cs +++ b/src/core/Akka.Streams/Dsl/Source.cs @@ -762,7 +762,7 @@ public static Source ZipWithN(Func, /// Can also complete with - when stream failed /// or when downstream is completed. /// - /// The strategy will not complete until buffer is full. + /// The strategy will not complete when buffer is full. /// /// The buffer can be disabled by using of 0 and then received messages will wait /// for downstream demand unless there is another message waiting for downstream demand, in that case From 1738c0d9c3bfddd7ffd0b4b9b16e86ccf063a71d Mon Sep 17 00:00:00 2001 From: zbynek001 Date: Sat, 27 Jan 2018 00:26:51 +0100 Subject: [PATCH 08/70] StartProxy replicator used based on configuration (#3298) --- .../cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs index 3df10e13394..6d72cab2182 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs @@ -222,7 +222,7 @@ public ClusterShardingGuardian() var coordinatorSingletonManagerName = CoordinatorSingletonManagerName(encName); var coordinatorPath = CoordinatorPath(encName); var shardRegion = Context.Child(encName); - var replicator = DistributedData.DistributedData.Get(Context.System).Replicator; + var replicator = Replicator(settings); if (Equals(shardRegion, ActorRefs.Nobody)) { From c5bf60c0b5f648f70068f6a9c070bef0bf1548cd Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Sat, 27 Jan 2018 08:36:38 -0800 Subject: [PATCH 09/70] AkkaPduCodec performance fixes [remoting] (#3299) * AkkaPduCodec performance fixes * made HeartbeatPdu immutable * made all internal formatting methods static --- .../Akka.Remote/Transport/AkkaPduCodec.cs | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs index a9964c52195..14cf1b11a9f 100644 --- a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs +++ b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs @@ -228,14 +228,19 @@ protected AkkaPduCodec(ActorSystem system) /// TBD public virtual ByteString EncodePdu(IAkkaPdu pdu) { - ByteString finalBytes = null; - pdu.Match() - .With(a => finalBytes = ConstructAssociate(a.Info)) - .With(p => finalBytes = ConstructPayload(p.Bytes)) - .With(d => finalBytes = ConstructDisassociate(d.Reason)) - .With(h => finalBytes = ConstructHeartbeat()); - - return finalBytes; + switch (pdu) + { + case Payload p: + return ConstructPayload(p.Bytes); + case Heartbeat h: + return ConstructHeartbeat(); + case Associate a: + return ConstructAssociate(a.Info); + case Disassociate d: + return ConstructDisassociate(d.Reason); + default: + return null; // unsupported message type + } } /// @@ -376,13 +381,20 @@ public override ByteString ConstructDisassociate(DisassociateInfo reason) } } + /* + * Since there's never any ActorSystem-specific information coded directly + * into the heartbeat messages themselves (i.e. no handshake info,) there's no harm in caching in the + * same heartbeat byte buffer and re-using it. + */ + private static readonly ByteString HeartbeatPdu = ConstructControlMessagePdu(CommandType.Heartbeat); + /// - /// TBD + /// Creates a new Heartbeat message instance. /// - /// TBD + /// The Heartbeat message. public override ByteString ConstructHeartbeat() { - return ConstructControlMessagePdu(CommandType.Heartbeat); + return HeartbeatPdu; } /// @@ -533,7 +545,7 @@ private ByteString DISASSOCIATE_QUARANTINED get { return ConstructControlMessagePdu(CommandType.DisassociateQuarantined); } } - private ByteString ConstructControlMessagePdu(CommandType code, AkkaHandshakeInfo handshakeInfo = null) + private static ByteString ConstructControlMessagePdu(CommandType code, AkkaHandshakeInfo handshakeInfo = null) { var controlMessage = new AkkaControlMessage() { CommandType = code }; if (handshakeInfo != null) @@ -544,12 +556,12 @@ private ByteString ConstructControlMessagePdu(CommandType code, AkkaHandshakeInf return new AkkaProtocolMessage() { Instruction = controlMessage }.ToByteString(); } - private Address DecodeAddress(AddressData origin) + private static Address DecodeAddress(AddressData origin) { return new Address(origin.Protocol, origin.System, origin.Hostname, (int)origin.Port); } - private ActorRefData SerializeActorRef(Address defaultAddress, IActorRef actorRef) + private static ActorRefData SerializeActorRef(Address defaultAddress, IActorRef actorRef) { return new ActorRefData() { @@ -559,7 +571,7 @@ private ActorRefData SerializeActorRef(Address defaultAddress, IActorRef actorRe }; } - private AddressData SerializeAddress(Address address) + private static AddressData SerializeAddress(Address address) { if (string.IsNullOrEmpty(address.Host) || !address.Port.HasValue) throw new ArgumentException($"Address {address} could not be serialized: host or port missing"); From 0e6d005888c9d898e8ab6b81bf2acbe8ffafb41a Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Sat, 27 Jan 2018 17:30:10 -0600 Subject: [PATCH 10/70] changed ActorCell.ReceiveMessage method to be protected and virtual --- .../Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt | 1 + src/core/Akka/Actor/ActorCell.DefaultMessages.cs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index 4cd41324af6..1f79ac268ee 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -93,6 +93,7 @@ namespace Akka.Actor protected void PrepareForNewActor() { } protected virtual void PreStart() { } protected void ReceivedTerminated(Akka.Actor.Terminated t) { } + protected virtual void ReceiveMessage(object message) { } public void ReceiveMessageForTest(Akka.Actor.Envelope envelope) { } protected Akka.Actor.Internal.SuspendReason RemoveChildAndGetStateChange(Akka.Actor.IActorRef child) { } protected void RemWatcher(Akka.Actor.IActorRef watchee, Akka.Actor.IActorRef watcher) { } diff --git a/src/core/Akka/Actor/ActorCell.DefaultMessages.cs b/src/core/Akka/Actor/ActorCell.DefaultMessages.cs index d9cae8f7112..9543b51ef24 100644 --- a/src/core/Akka/Actor/ActorCell.DefaultMessages.cs +++ b/src/core/Akka/Actor/ActorCell.DefaultMessages.cs @@ -171,10 +171,10 @@ public void ReceiveMessageForTest(Envelope envelope) } /// - /// TBD + /// Receives the next message from the mailbox and feeds it to the underlying actor instance. /// - /// TBD - internal void ReceiveMessage(object message) + /// The message that will be sent to the actor. + protected virtual void ReceiveMessage(object message) { var wasHandled = _actor.AroundReceive(_state.GetCurrentBehavior(), message); From 50f2710a737db9d8dbd806b1814cdcf5fac589ae Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Mon, 29 Jan 2018 12:59:35 -0800 Subject: [PATCH 11/70] placeholder for nightlies (#3302) --- RELEASE_NOTES.md | 5 ++++- src/common.props | 58 ++---------------------------------------------- 2 files changed, 6 insertions(+), 57 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4324c4b14d6..ecceefd2452 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,7 @@ -#### 1.3.3 January 19 2019 #### +#### 1.3.4 January 28 2018 #### +Placeholder + +#### 1.3.3 January 19 2018 #### **Maintenance Release for Akka.NET 1.3** The largest changes featured in Akka.NET v1.3.3 are the introduction of [Splint brain resolvers](http://getakka.net/articles/clustering/split-brain-resolver.html) and `WeaklyUp` members in Akka.Cluster. diff --git a/src/common.props b/src/common.props index bfeae03904e..ba33aee35bf 100644 --- a/src/common.props +++ b/src/common.props @@ -2,7 +2,7 @@ Copyright © 2013-2017 Akka.NET Team Akka.NET Team - 1.3.3 + 1.3.4 http://getakka.net/images/akkalogo.png https://github.com/akkadotnet/akka.net https://github.com/akkadotnet/akka.net/blob/master/LICENSE @@ -17,60 +17,6 @@ true - Maintenance Release for Akka.NET 1.3** -The largest changes featured in Akka.NET v1.3.3 are the introduction of [Splint brain resolvers](http://getakka.net/articles/clustering/split-brain-resolver.html) and `WeaklyUp` members in Akka.Cluster. -Akka.Cluster Split Brain Resolvers** -Split brain resolvers are specialized [`IDowningProvider`](http://getakka.net/api/Akka.Cluster.IDowningProvider.html) implementations that give Akka.Cluster users the ability to automatically down `Unreachable` cluster nodes in accordance with well-defined partition resolution strategies, namely: -Static quorums; -Keep majority; -Keep oldest; and -Keep-referee. -You can learn more about why you may want to use these and which strategy is right for you by reading our [Splint brain resolver documentation](http://getakka.net/articles/clustering/split-brain-resolver.html). -Akka.Cluster `WeaklyUp` Members** -One common problem that occurs in Akka.Cluster is that once a current member of the cluster becomes `Unreachable`, the leader of the cluster isn't able to allow any new members of the cluster to join until that `Unreachable` member becomes `Reachable` again or is removed from the cluster via a [`Cluster.Down` command](http://getakka.net/api/Akka.Cluster.Cluster.html#Akka_Cluster_Cluster_Down_Akka_Actor_Address_). -Beginning in Akka.NET 1.3.3, you can allow nodes to still join and participate in the cluster even while other member nodes are unreachable by opting into the `WeaklyUp` status for members. You can do this by setting the following in your HOCON configuration beginning in Akka.NET v1.3.3: -``` -akka.cluster.allow-weakly-up-members = on -``` -This will allow nodes who have joined the cluster when at least one other member was unreachable to become functioning cluster members with a status of `WeaklyUp`. If the unreachable members of the cluster are downed or become reachable again, all `WeaklyUp` nodes will be upgraded to the usual `Up` status for available cluster members. -Akka.Cluster.Sharding and Akka.Cluster.DistributedData Integration** -A new experimental feature we've added in Akka.NET v1.3.3 is the ability to fully decouple [Akka.Cluster.Sharding](http://getakka.net/articles/clustering/cluster-sharding.html) from Akka.Persistence and instead run it on top of [Akka.Cluster.DistributedData, our library for creating eventually consistent replicated data structures on top of Akka.Cluster](http://getakka.net/articles/clustering/distributed-data.html). -Beginning in Akka.NET 1.3.3, you can set the following HOCON configuration option to have the `ShardingCoordinator` replicate its shard placement state using DData instead of persisting it to storage via Akka.Persistence: -``` -akka.cluster.sharding.state-store-mode = ddata -``` -This setting only affects how Akka.Cluster.Sharding's internal state is managed. If you're using Akka.Persistence with your own entity actors inside Akka.Cluster.Sharding, this change will have no impact on them. -Updates and bugfixes**: -[Added `Cluster.JoinAsync` and `Clutser.JoinSeedNodesAsync` methods](https://github.com/akkadotnet/akka.net/pull/3196) -[Updated Akka.Serialization.Hyperion to Hyperion v0.9.7](https://github.com/akkadotnet/akka.net/pull/3279) - see [Hyperion v0.9.7 release notes here](https://github.com/akkadotnet/Hyperion/releases/tag/v0.9.7). -[Fixed: A Source.SplitAfter Akka example extra output](https://github.com/akkadotnet/akka.net/issues/3222) -[Fixed: Udp.Received give incorrect ByteString when client send several packets at once](https://github.com/akkadotnet/akka.net/issues/3210) -[Fixed: TcpOutgoingConnection does not dispose properly - memory leak](https://github.com/akkadotnet/akka.net/issues/3211) -[Fixed: Akka.IO & WSAEWOULDBLOCK socket error](https://github.com/akkadotnet/akka.net/issues/3188) -[Fixed: Sharding-RegionProxyTerminated fix](https://github.com/akkadotnet/akka.net/pull/3192) -[Fixed: Excessive rebalance in LeastShardAllocationStrategy](https://github.com/akkadotnet/akka.net/pull/3191) -[Fixed: Persistence - fix double return of recovery permit](https://github.com/akkadotnet/akka.net/pull/3201) -[Change: Changed Akka.IO configured buffer-size to 512B](https://github.com/akkadotnet/akka.net/pull/3176) -[Change: Added human-friendly error for failed MNTK discovery](https://github.com/akkadotnet/akka.net/pull/3198) -You can [see the full changeset for Akka.NET 1.3.3 here](https://github.com/akkadotnet/akka.net/milestone/21). -| COMMITS | LOC+ | LOC- | AUTHOR | -| --- | --- | --- | --- | -| 17 | 2094 | 1389 | Marc Piechura | -| 13 | 5426 | 2827 | Bartosz Sypytkowski | -| 12 | 444 | 815 | Aaron Stannard | -| 11 | 346 | 217 | ravengerUA | -| 3 | 90 | 28 | zbynek001 | -| 3 | 78 | 84 | Maxim Cherednik | -| 2 | 445 | 1 | Vasily Kirichenko | -| 2 | 22 | 11 | Ismael Hamed | -| 2 | 11 | 9 | Nicola Sanitate | -| 1 | 9 | 10 | mrrd | -| 1 | 7 | 2 | Richard Dobson | -| 1 | 33 | 7 | Ivars Auzins | -| 1 | 30 | 11 | Will | -| 1 | 3 | 3 | HaniOB | -| 1 | 11 | 199 | Jon Galloway | -| 1 | 1 | 1 | Sam Neirinck | -| 1 | 1 | 1 | Irvin Dominin | + Placeholder \ No newline at end of file From cd05a40d708795a4f48b0d171e85ed2c7ba39760 Mon Sep 17 00:00:00 2001 From: Maxim Cherednik Date: Tue, 30 Jan 2018 16:26:49 +0100 Subject: [PATCH 12/70] Ask interface should be clean (#3220) * Tests should be precise - in temrs of what to expect * Ask interface refined #3220 * ClusterRouter unit test fix #3220 * Ask deadlock test added #3220 * Handle deadlock by removing the SynchronizationContext #3220 * Fixing ScatterGather router test #3220 * Ask interface refined #3220 AskSpecs consolidated Api change approval - internal CastTask removed * Fixing header #3220 --- .../AsyncWriteProxyEx.cs | 61 ++++++---- .../CoreAPISpec.ApproveCore.approved.txt | 1 - .../Routing/ClusterRouterAsk1343BugFixSpec.cs | 9 +- .../AsyncContext.SynchronizationContext.cs | 0 .../AsyncContext/AsyncContext.TaskQueue.cs | 0 .../AsyncContext.TaskScheduler.cs | 0 .../AsyncContext/AsyncContext.cs | 0 .../AsyncContext/AsyncEx.LICENSE | 0 .../AsyncContext/BoundAction.cs | 0 .../AsyncContext/Disposables.LICENSE | 0 .../AsyncContext/ExceptionHelpers.cs | 0 .../AsyncContext/SingleDisposable (of T).cs | 0 .../SynchronizationContextSwitcher.cs | 0 .../SynchronousTaskExtensions.cs.cs | 0 .../AsyncContext/about.txt | 0 src/core/Akka.Tests/Actor/AskSpec.cs | 108 ++++++++++++++---- src/core/Akka.Tests/Actor/AskTimeoutSpec.cs | 82 ------------- src/core/Akka/Actor/Futures.cs | 64 +++++++---- .../Routing/ScatterGatherFirstCompleted.cs | 39 +++---- .../Internal/SynchronizationContextManager.cs | 72 ++++++++++++ src/core/Akka/Util/Internal/TaskExtensions.cs | 34 ------ 21 files changed, 250 insertions(+), 220 deletions(-) rename src/core/{Akka.Remote.Tests => Akka.Tests.Shared.Internals}/AsyncContext/AsyncContext.SynchronizationContext.cs (100%) rename src/core/{Akka.Remote.Tests => Akka.Tests.Shared.Internals}/AsyncContext/AsyncContext.TaskQueue.cs (100%) rename src/core/{Akka.Remote.Tests => Akka.Tests.Shared.Internals}/AsyncContext/AsyncContext.TaskScheduler.cs (100%) rename src/core/{Akka.Remote.Tests => Akka.Tests.Shared.Internals}/AsyncContext/AsyncContext.cs (100%) rename src/core/{Akka.Remote.Tests => Akka.Tests.Shared.Internals}/AsyncContext/AsyncEx.LICENSE (100%) rename src/core/{Akka.Remote.Tests => Akka.Tests.Shared.Internals}/AsyncContext/BoundAction.cs (100%) rename src/core/{Akka.Remote.Tests => Akka.Tests.Shared.Internals}/AsyncContext/Disposables.LICENSE (100%) rename src/core/{Akka.Remote.Tests => Akka.Tests.Shared.Internals}/AsyncContext/ExceptionHelpers.cs (100%) rename src/core/{Akka.Remote.Tests => Akka.Tests.Shared.Internals}/AsyncContext/SingleDisposable (of T).cs (100%) rename src/core/{Akka.Remote.Tests => Akka.Tests.Shared.Internals}/AsyncContext/SynchronizationContextSwitcher.cs (100%) rename src/core/{Akka.Remote.Tests => Akka.Tests.Shared.Internals}/AsyncContext/SynchronousTaskExtensions.cs.cs (100%) rename src/core/{Akka.Remote.Tests => Akka.Tests.Shared.Internals}/AsyncContext/about.txt (100%) delete mode 100644 src/core/Akka.Tests/Actor/AskTimeoutSpec.cs create mode 100644 src/core/Akka/Util/Internal/SynchronizationContextManager.cs 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 4d5d1e5bf39..4ac2269a7e5 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/AsyncWriteProxyEx.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/AsyncWriteProxyEx.cs @@ -388,13 +388,13 @@ public static Task AskEx(this ICanTell self, Func messa return self.AskEx(messageFactory, null, cancellationToken); } - public static Task AskEx(this ICanTell self, Func messageFactory, TimeSpan? timeout, CancellationToken cancellationToken) + public static async Task AskEx(this ICanTell self, Func messageFactory, TimeSpan? timeout, CancellationToken cancellationToken) { IActorRefProvider provider = ResolveProvider(self); if (provider == null) throw new ArgumentException("Unable to resolve the target Provider", nameof(self)); - return AskEx(self, messageFactory, provider, timeout, cancellationToken).CastTask(); + return (T)await AskEx(self, messageFactory, provider, timeout, cancellationToken); } internal static IActorRefProvider ResolveProvider(ICanTell self) { @@ -410,49 +410,60 @@ internal static IActorRefProvider ResolveProvider(ICanTell self) return null; } - private static Task AskEx(ICanTell self, Func messageFactory, IActorRefProvider provider, TimeSpan? timeout, CancellationToken cancellationToken) + private static async Task AskEx(ICanTell self, Func messageFactory, IActorRefProvider provider, TimeSpan? timeout, CancellationToken cancellationToken) { var result = new TaskCompletionSource(); CancellationTokenSource timeoutCancellation = null; timeout = timeout ?? provider.Settings.AskTimeout; - List ctrList = new List(2); + var ctrList = new List(2); - if (timeout != System.Threading.Timeout.InfiniteTimeSpan && timeout.Value > default(TimeSpan)) + if (timeout != Timeout.InfiniteTimeSpan && timeout.Value > default(TimeSpan)) { timeoutCancellation = new CancellationTokenSource(); - ctrList.Add(timeoutCancellation.Token.Register(() => result.TrySetCanceled())); + + ctrList.Add(timeoutCancellation.Token.Register(() => + { + result.TrySetException(new AskTimeoutException($"Timeout after {timeout} seconds")); + })); + timeoutCancellation.CancelAfter(timeout.Value); } if (cancellationToken.CanBeCanceled) + { ctrList.Add(cancellationToken.Register(() => result.TrySetCanceled())); + } //create a new tempcontainer path ActorPath path = provider.TempPath(); - //callback to unregister from tempcontainer - Action unregister = - () => - { - // cancelling timeout (if any) in order to prevent memory leaks - // (a reference to 'result' variable in CancellationToken's callback) - if (timeoutCancellation != null) - { - timeoutCancellation.Cancel(); - timeoutCancellation.Dispose(); - } - for (var i = 0; i < ctrList.Count; i++) - { - ctrList[i].Dispose(); - } - provider.UnregisterTempActor(path); - }; - var future = new FutureActorRef(result, unregister, path); + var future = new FutureActorRef(result, () => { }, path); //The future actor needs to be registered in the temp container provider.RegisterTempActor(future, path); + self.Tell(messageFactory(future), future); - return result.Task; + + try + { + return await result.Task; + } + finally + { + //callback to unregister from tempcontainer + + provider.UnregisterTempActor(path); + + for (var i = 0; i < ctrList.Count; i++) + { + ctrList[i].Dispose(); + } + + if (timeoutCancellation != null) + { + timeoutCancellation.Dispose(); + } + } } } } diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index 1f79ac268ee..47453b7bc88 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -4892,7 +4892,6 @@ namespace Akka.Util.Internal [Akka.Annotations.InternalApiAttribute()] public class static TaskExtensions { - public static System.Threading.Tasks.Task CastTask(this System.Threading.Tasks.Task task) { } public static System.Threading.Tasks.Task WithCancellation(this System.Threading.Tasks.Task task, System.Threading.CancellationToken cancellationToken) { } } } diff --git a/src/core/Akka.Cluster.Tests/Routing/ClusterRouterAsk1343BugFixSpec.cs b/src/core/Akka.Cluster.Tests/Routing/ClusterRouterAsk1343BugFixSpec.cs index 308735aedad..471c233b301 100644 --- a/src/core/Akka.Cluster.Tests/Routing/ClusterRouterAsk1343BugFixSpec.cs +++ b/src/core/Akka.Cluster.Tests/Routing/ClusterRouterAsk1343BugFixSpec.cs @@ -96,14 +96,7 @@ public async Task Should_Ask_Clustered_Group_Router_and_with_no_routees_and_time var router = Sys.ActorOf(Props.Empty.WithRouter(FromConfig.Instance), "router3"); Assert.IsType(router); - try - { - var result = await router.Ask("foo"); - } - catch (Exception ex) - { - Assert.IsType(ex); - } + await Assert.ThrowsAsync(async () => await router.Ask("foo")); } } } diff --git a/src/core/Akka.Remote.Tests/AsyncContext/AsyncContext.SynchronizationContext.cs b/src/core/Akka.Tests.Shared.Internals/AsyncContext/AsyncContext.SynchronizationContext.cs similarity index 100% rename from src/core/Akka.Remote.Tests/AsyncContext/AsyncContext.SynchronizationContext.cs rename to src/core/Akka.Tests.Shared.Internals/AsyncContext/AsyncContext.SynchronizationContext.cs diff --git a/src/core/Akka.Remote.Tests/AsyncContext/AsyncContext.TaskQueue.cs b/src/core/Akka.Tests.Shared.Internals/AsyncContext/AsyncContext.TaskQueue.cs similarity index 100% rename from src/core/Akka.Remote.Tests/AsyncContext/AsyncContext.TaskQueue.cs rename to src/core/Akka.Tests.Shared.Internals/AsyncContext/AsyncContext.TaskQueue.cs diff --git a/src/core/Akka.Remote.Tests/AsyncContext/AsyncContext.TaskScheduler.cs b/src/core/Akka.Tests.Shared.Internals/AsyncContext/AsyncContext.TaskScheduler.cs similarity index 100% rename from src/core/Akka.Remote.Tests/AsyncContext/AsyncContext.TaskScheduler.cs rename to src/core/Akka.Tests.Shared.Internals/AsyncContext/AsyncContext.TaskScheduler.cs diff --git a/src/core/Akka.Remote.Tests/AsyncContext/AsyncContext.cs b/src/core/Akka.Tests.Shared.Internals/AsyncContext/AsyncContext.cs similarity index 100% rename from src/core/Akka.Remote.Tests/AsyncContext/AsyncContext.cs rename to src/core/Akka.Tests.Shared.Internals/AsyncContext/AsyncContext.cs diff --git a/src/core/Akka.Remote.Tests/AsyncContext/AsyncEx.LICENSE b/src/core/Akka.Tests.Shared.Internals/AsyncContext/AsyncEx.LICENSE similarity index 100% rename from src/core/Akka.Remote.Tests/AsyncContext/AsyncEx.LICENSE rename to src/core/Akka.Tests.Shared.Internals/AsyncContext/AsyncEx.LICENSE diff --git a/src/core/Akka.Remote.Tests/AsyncContext/BoundAction.cs b/src/core/Akka.Tests.Shared.Internals/AsyncContext/BoundAction.cs similarity index 100% rename from src/core/Akka.Remote.Tests/AsyncContext/BoundAction.cs rename to src/core/Akka.Tests.Shared.Internals/AsyncContext/BoundAction.cs diff --git a/src/core/Akka.Remote.Tests/AsyncContext/Disposables.LICENSE b/src/core/Akka.Tests.Shared.Internals/AsyncContext/Disposables.LICENSE similarity index 100% rename from src/core/Akka.Remote.Tests/AsyncContext/Disposables.LICENSE rename to src/core/Akka.Tests.Shared.Internals/AsyncContext/Disposables.LICENSE diff --git a/src/core/Akka.Remote.Tests/AsyncContext/ExceptionHelpers.cs b/src/core/Akka.Tests.Shared.Internals/AsyncContext/ExceptionHelpers.cs similarity index 100% rename from src/core/Akka.Remote.Tests/AsyncContext/ExceptionHelpers.cs rename to src/core/Akka.Tests.Shared.Internals/AsyncContext/ExceptionHelpers.cs diff --git a/src/core/Akka.Remote.Tests/AsyncContext/SingleDisposable (of T).cs b/src/core/Akka.Tests.Shared.Internals/AsyncContext/SingleDisposable (of T).cs similarity index 100% rename from src/core/Akka.Remote.Tests/AsyncContext/SingleDisposable (of T).cs rename to src/core/Akka.Tests.Shared.Internals/AsyncContext/SingleDisposable (of T).cs diff --git a/src/core/Akka.Remote.Tests/AsyncContext/SynchronizationContextSwitcher.cs b/src/core/Akka.Tests.Shared.Internals/AsyncContext/SynchronizationContextSwitcher.cs similarity index 100% rename from src/core/Akka.Remote.Tests/AsyncContext/SynchronizationContextSwitcher.cs rename to src/core/Akka.Tests.Shared.Internals/AsyncContext/SynchronizationContextSwitcher.cs diff --git a/src/core/Akka.Remote.Tests/AsyncContext/SynchronousTaskExtensions.cs.cs b/src/core/Akka.Tests.Shared.Internals/AsyncContext/SynchronousTaskExtensions.cs.cs similarity index 100% rename from src/core/Akka.Remote.Tests/AsyncContext/SynchronousTaskExtensions.cs.cs rename to src/core/Akka.Tests.Shared.Internals/AsyncContext/SynchronousTaskExtensions.cs.cs diff --git a/src/core/Akka.Remote.Tests/AsyncContext/about.txt b/src/core/Akka.Tests.Shared.Internals/AsyncContext/about.txt similarity index 100% rename from src/core/Akka.Remote.Tests/AsyncContext/about.txt rename to src/core/Akka.Tests.Shared.Internals/AsyncContext/about.txt diff --git a/src/core/Akka.Tests/Actor/AskSpec.cs b/src/core/Akka.Tests/Actor/AskSpec.cs index 7fcb46eea47..d33eb1de642 100644 --- a/src/core/Akka.Tests/Actor/AskSpec.cs +++ b/src/core/Akka.Tests/Actor/AskSpec.cs @@ -10,12 +10,17 @@ using Akka.Actor; using System; using System.Threading; +using System.Threading.Tasks; +using Nito.AsyncEx; namespace Akka.Tests.Actor { - public class AskSpec : AkkaSpec { + public AskSpec() + : base(@"akka.actor.ask-timeout = 3000ms") + { } + public class SomeActor : UntypedActor { protected override void OnReceive(object message) @@ -24,6 +29,7 @@ protected override void OnReceive(object message) { Thread.Sleep(5000); } + if (message.Equals("answer")) { Sender.Tell("answer"); @@ -39,9 +45,9 @@ public WaitActor(IActorRef replyActor, IActorRef testActor) _testActor = testActor; } - private IActorRef _replyActor; + private readonly IActorRef _replyActor; - private IActorRef _testActor; + private readonly IActorRef _testActor; protected override void OnReceive(object message) { @@ -66,52 +72,108 @@ protected override void OnReceive(object message) } [Fact] - public void Can_Ask_actor() + public async Task Can_Ask_actor() { var actor = Sys.ActorOf(); - actor.Ask("answer").Result.ShouldBe("answer"); + var res = await actor.Ask("answer"); + res.ShouldBe("answer"); } [Fact] - public void Can_Ask_actor_with_timeout() + public async Task Can_Ask_actor_with_timeout() { var actor = Sys.ActorOf(); - actor.Ask("answer",TimeSpan.FromSeconds(10)).Result.ShouldBe("answer"); + var res = await actor.Ask("answer", TimeSpan.FromSeconds(10)); + res.ShouldBe("answer"); } [Fact] - public void Can_get_timeout_when_asking_actor() + public async Task Can_get_timeout_when_asking_actor() { var actor = Sys.ActorOf(); - Assert.Throws(() => { actor.Ask("timeout", TimeSpan.FromSeconds(3)).Wait(); }); + await Assert.ThrowsAsync(async () => await actor.Ask("timeout", TimeSpan.FromSeconds(3))); } [Fact] - public void Can_cancel_when_asking_actor() - { + public async Task Can_cancel_when_asking_actor() + { var actor = Sys.ActorOf(); - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); - Assert.Throws(() => { actor.Ask("timeout", Timeout.InfiniteTimeSpan, cts.Token).Wait(); }); - Assert.True(cts.IsCancellationRequested); + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) + { + await Assert.ThrowsAsync(async () => await actor.Ask("timeout", Timeout.InfiniteTimeSpan, cts.Token)); + } } + [Fact] - public void Cancelled_ask_with_null_timeout_should_remove_temp_actor() + public async Task Ask_should_honor_config_specified_timeout() { var actor = Sys.ActorOf(); - var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); - Assert.Throws(() => { actor.Ask("cancel", cts.Token).Wait(); }); - Assert.True(cts.IsCancellationRequested); + try + { + await actor.Ask("timeout"); + Assert.True(false, "the ask should have timed out with default timeout"); + } + catch (AskTimeoutException e) + { + Assert.Equal("Timeout after 00:00:03 seconds", e.Message); + } + } + + [Fact] + public async Task Cancelled_ask_with_null_timeout_should_remove_temp_actor() + { + var actor = Sys.ActorOf(); + + using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100))) + { + await Assert.ThrowsAsync(async () => await actor.Ask("cancel", cts.Token)); + } + Are_Temp_Actors_Removed(actor); } + [Fact] - public void Cancelled_ask_with_timeout_should_remove_temp_actor() + public async Task Cancelled_ask_with_timeout_should_remove_temp_actor() { var actor = Sys.ActorOf(); - var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); - Assert.Throws(() => { actor.Ask("cancel", TimeSpan.FromSeconds(30), cts.Token).Wait(); }); - Assert.True(cts.IsCancellationRequested); + using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100))) + { + await Assert.ThrowsAsync(async () => await actor.Ask("cancel", TimeSpan.FromSeconds(30), cts.Token)); + } + Are_Temp_Actors_Removed(actor); } + + [Fact] + public async Task AskTimeout_with_default_timeout_should_remove_temp_actor() + { + var actor = Sys.ActorOf(); + + await Assert.ThrowsAsync(async () => await actor.Ask("timeout")); + + Are_Temp_Actors_Removed(actor); + } + + [Fact] + public async Task ShouldFailWhenAskExpectsWrongType() + { + var actor = Sys.ActorOf(); + + // expect int, but in fact string + await Assert.ThrowsAsync(async () => await actor.Ask("answer")); + } + + [Fact] + public void AskDoesNotDeadlockWhenWaitForResultInGuiApplication() + { + AsyncContext.Run(() => + { + var actor = Sys.ActorOf(); + var res = actor.Ask("answer").Result; // blocking on purpose + res.ShouldBe("answer"); + }); + } + private void Are_Temp_Actors_Removed(IActorRef actor) { var actorCell = actor as ActorRefWithCell; @@ -126,7 +188,7 @@ private void Are_Temp_Actors_Removed(IActorRef actor) container.ForEachChild(x => childCounter++); Assert.True(childCounter == 0, "Temp actors not all removed."); }); - + } /// diff --git a/src/core/Akka.Tests/Actor/AskTimeoutSpec.cs b/src/core/Akka.Tests/Actor/AskTimeoutSpec.cs deleted file mode 100644 index 7cdac3d1b27..00000000000 --- a/src/core/Akka.Tests/Actor/AskTimeoutSpec.cs +++ /dev/null @@ -1,82 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2016 Lightbend Inc. -// Copyright (C) 2013-2016 Akka.NET project -// -//----------------------------------------------------------------------- - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using Akka.Actor; -using Akka.TestKit; - -using Xunit; - -namespace Akka.Tests.Actor -{ - public class AskTimeoutSpec : AkkaSpec - { - - public class SleepyActor : UntypedActor - { - - protected override void OnReceive(object message) - { - Thread.Sleep(5000); - Sender.Tell(message); - } - - } - - public AskTimeoutSpec() - : base(@"akka.actor.ask-timeout = 100ms") - {} - - [Fact] - public async Task Ask_should_honor_config_specified_timeout() - { - var actor = Sys.ActorOf(); - try - { - await actor.Ask("should time out"); - Assert.True(false, "the ask should have timed out"); - } - catch (Exception e) - { - Assert.True(e is TaskCanceledException); - } - } - - [Fact] - public async Task TimedOut_ask_should_remove_temp_actor() - { - var actor = Sys.ActorOf(); - - var actorCell = actor as ActorRefWithCell; - Assert.NotNull(actorCell); - - var container = actorCell.Provider.TempContainer as VirtualPathContainer; - Assert.NotNull(container); - try - { - await actor.Ask("should time out"); - } - catch (Exception) - { - // Need to spin here, since the continuation function may not execute immediately - AwaitAssert(() => - { - var childCounter = 0; - container.ForEachChild(x => childCounter++); - Assert.True(childCounter == 0, "Number of children in temp container should be 0."); - }); - - } - - } - - } -} diff --git a/src/core/Akka/Actor/Futures.cs b/src/core/Akka/Actor/Futures.cs index ee222d6437f..63a87a4abea 100644 --- a/src/core/Akka/Actor/Futures.cs +++ b/src/core/Akka/Actor/Futures.cs @@ -102,11 +102,13 @@ public static Task Ask(this ICanTell self, object message, CancellationTok /// TBD public static async Task Ask(this ICanTell self, object message, TimeSpan? timeout, CancellationToken cancellationToken) { + await SynchronizationContextManager.RemoveContext; + IActorRefProvider provider = ResolveProvider(self); if (provider == null) throw new ArgumentException("Unable to resolve the target Provider", nameof(self)); - return (T) await Ask(self, message, provider, timeout, cancellationToken).ConfigureAwait(false); + return (T) await Ask(self, message, provider, timeout, cancellationToken); } /// @@ -132,54 +134,68 @@ internal static IActorRefProvider ResolveProvider(ICanTell self) private static readonly bool isRunContinuationsAsynchronouslyAvailable = Enum.IsDefined(typeof(TaskCreationOptions), RunContinuationsAsynchronously); - private static Task Ask(ICanTell self, object message, IActorRefProvider provider, + private static async Task Ask(ICanTell self, object message, IActorRefProvider provider, TimeSpan? timeout, CancellationToken cancellationToken) { TaskCompletionSource result; if (isRunContinuationsAsynchronouslyAvailable) + { result = new TaskCompletionSource((TaskCreationOptions)RunContinuationsAsynchronously); + } else + { result = new TaskCompletionSource(); + } CancellationTokenSource timeoutCancellation = null; timeout = timeout ?? provider.Settings.AskTimeout; - List ctrList = new List(2); + var ctrList = new List(2); if (timeout != Timeout.InfiniteTimeSpan && timeout.Value > default(TimeSpan)) { timeoutCancellation = new CancellationTokenSource(); - ctrList.Add(timeoutCancellation.Token.Register(() => result.TrySetCanceled())); + + ctrList.Add(timeoutCancellation.Token.Register(() => + { + result.TrySetException(new AskTimeoutException($"Timeout after {timeout} seconds")); + })); + timeoutCancellation.CancelAfter(timeout.Value); } if (cancellationToken.CanBeCanceled) + { ctrList.Add(cancellationToken.Register(() => result.TrySetCanceled())); - + } + //create a new tempcontainer path ActorPath path = provider.TempPath(); - //callback to unregister from tempcontainer - Action unregister = - () => - { - // cancelling timeout (if any) in order to prevent memory leaks - // (a reference to 'result' variable in CancellationToken's callback) - if (timeoutCancellation != null) - { - timeoutCancellation.Cancel(); - timeoutCancellation.Dispose(); - } - for (var i = 0; i < ctrList.Count; i++) - { - ctrList[i].Dispose(); - } - provider.UnregisterTempActor(path); - }; - var future = new FutureActorRef(result, unregister, path, isRunContinuationsAsynchronouslyAvailable); + var future = new FutureActorRef(result, () => { }, path, isRunContinuationsAsynchronouslyAvailable); //The future actor needs to be registered in the temp container provider.RegisterTempActor(future, path); self.Tell(message, future); - return result.Task; + + try + { + return await result.Task; + } + finally + { + //callback to unregister from tempcontainer + + provider.UnregisterTempActor(path); + + for (var i = 0; i < ctrList.Count; i++) + { + ctrList[i].Dispose(); + } + + if (timeoutCancellation != null) + { + timeoutCancellation.Dispose(); + } + } } } diff --git a/src/core/Akka/Routing/ScatterGatherFirstCompleted.cs b/src/core/Akka/Routing/ScatterGatherFirstCompleted.cs index 3a6becbf5a8..3140b86262d 100644 --- a/src/core/Akka/Routing/ScatterGatherFirstCompleted.cs +++ b/src/core/Akka/Routing/ScatterGatherFirstCompleted.cs @@ -44,7 +44,7 @@ public ScatterGatherFirstCompletedRoutingLogic(TimeSpan within) /// A that receives the . public override Routee Select(object message, Routee[] routees) { - return new ScatterGatherFirstCompletedRoutees(routees,_within); + return new ScatterGatherFirstCompletedRoutees(routees, _within); } } @@ -77,38 +77,31 @@ public ScatterGatherFirstCompletedRoutees(Routee[] routees, TimeSpan within) /// The actor sending the message. public override void Send(object message, IActorRef sender) { - var tcs = new TaskCompletionSource(); + SendMessage(message).PipeTo(sender); + } + private async Task SendMessage(object message) + { if (_routees.IsNullOrEmpty()) { - tcs.SetResult(new Status.Failure(new AskTimeoutException("Timeout due to no routees"))); + return new Status.Failure(new AskTimeoutException("Timeout due to no routees")); } - else + + try { + var tasks = _routees .Select(routee => routee.Ask(message, _within)) .ToList(); - Task - .WhenAny(tasks) - .ContinueWith(task => - { - if (task.Result.IsCanceled) - { - tcs.SetResult(new Status.Failure(new AskTimeoutException($"Timeout after {_within.TotalSeconds} seconds"))); - } - else if (task.Result.IsFaulted) - { - tcs.SetResult(new Status.Failure(task.Result.Exception)); - } - else - { - tcs.SetResult(task.Result.Result); - } - }); - } + var firstFinishedTask = await Task.WhenAny(tasks); - tcs.Task.PipeTo(sender); + return await firstFinishedTask; + } + catch (Exception e) + { + return new Status.Failure(e); + } } } diff --git a/src/core/Akka/Util/Internal/SynchronizationContextManager.cs b/src/core/Akka/Util/Internal/SynchronizationContextManager.cs new file mode 100644 index 00000000000..c58eeb9fcd4 --- /dev/null +++ b/src/core/Akka/Util/Internal/SynchronizationContextManager.cs @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2016 Lightbend Inc. +// Copyright (C) 2013-2016 Akka.NET project +// +//----------------------------------------------------------------------- + +using Akka.Annotations; +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Akka.Util.Internal +{ + /// + /// SynchronizationContextManager controls SynchronizationContext of the async pipeline. + /// Does the same thing as .ConfigureAwait(false) but better - it should be written once only, + /// unlike .ConfigureAwait(false). + /// await SynchronizationContextManager.RemoveContext; + /// Should be used as a very first line inside async public API of the library code + /// + /// + /// This sample shows how to use . + /// + /// class CoolLib + /// { + /// public async Task DoSomething() + /// { + /// await SynchronizationContextManager.RemoveContext; + /// + /// await DoSomethingElse(); + /// } + /// } + /// + /// + [InternalApi] + internal static class SynchronizationContextManager + { + public static ContextRemover RemoveContext { get; } = new ContextRemover(); + } + + [InternalApi] + internal class ContextRemover : INotifyCompletion + { + public bool IsCompleted => SynchronizationContext.Current == null; + + public void OnCompleted(Action continuation) + { + var prevContext = SynchronizationContext.Current; + + try + { + SynchronizationContext.SetSynchronizationContext(null); + continuation(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(prevContext); + } + } + + public ContextRemover GetAwaiter() + { + return this; + } + + public void GetResult() + { + // empty on purpose + } + } +} diff --git a/src/core/Akka/Util/Internal/TaskExtensions.cs b/src/core/Akka/Util/Internal/TaskExtensions.cs index 968f252894d..dabb714f4f4 100644 --- a/src/core/Akka/Util/Internal/TaskExtensions.cs +++ b/src/core/Akka/Util/Internal/TaskExtensions.cs @@ -20,40 +20,6 @@ namespace Akka.Util.Internal [InternalApi] public static class TaskExtensions { - /// - /// TBD - /// - /// TBD - /// TBD - /// TBD - /// TBD - public static Task CastTask(this Task task) - { - if (task.IsCompleted) - return Task.FromResult((TResult) (object)task.Result); - var tcs = new TaskCompletionSource(); - if (task.IsFaulted) - tcs.SetException(task.Exception); - else - task.ContinueWith(_ => - { - if (task.IsFaulted || task.Exception != null) - tcs.SetException(task.Exception); - else if (task.IsCanceled) - tcs.SetCanceled(); - else - try - { - tcs.SetResult((TResult) (object) task.Result); - } - catch (Exception e) - { - tcs.SetException(e); - } - }, TaskContinuationOptions.ExecuteSynchronously); - return tcs.Task; - } - /// /// Returns the task which completes with result of original task if cancellation token not canceled it before completion. /// From 96cd99a7079abb8288516dc364bf8db4ab68bde4 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 31 Jan 2018 09:27:37 -0800 Subject: [PATCH 13/70] Fixed cluster group router UseRole ignored bug (#3303) * fixed bug in UseRoleIgnoredSpec * close #3294 - fixed usages of GetPaths inside all Group router implementations --- .../CoreAPISpec.ApproveCore.approved.txt | 2 ++ .../Routing/UseRoleIgnoredSpec.cs | 2 +- .../Akka.Cluster.Tests/ClusterDeployerSpec.cs | 4 ++-- .../Routing/ClusterRoutingConfig.cs | 14 ++++++++------ .../Routing/ConfiguredLocalRoutingSpec.cs | 2 +- src/core/Akka/Routing/Broadcast.cs | 6 +++--- src/core/Akka/Routing/ConsistentHashRouter.cs | 10 +++++----- src/core/Akka/Routing/Random.cs | 6 +++--- src/core/Akka/Routing/RoundRobin.cs | 6 +++--- src/core/Akka/Routing/RoutedActorCell.cs | 2 +- src/core/Akka/Routing/RouterConfig.cs | 17 ++++++++++++----- .../Akka/Routing/ScatterGatherFirstCompleted.cs | 6 +++--- src/core/Akka/Routing/TailChopping.cs | 6 +++--- 13 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index 47453b7bc88..c23a80dddb0 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -4061,7 +4061,9 @@ namespace Akka.Routing } public abstract class Group : Akka.Routing.RouterConfig, System.IEquatable { + protected readonly string[] InternalPaths; protected Group(System.Collections.Generic.IEnumerable paths, string routerDispatcher) { } + [System.ObsoleteAttribute("Deprecated since Akka.NET v1.1. Use Paths(ActorSystem) instead.")] public System.Collections.Generic.IEnumerable Paths { get; } public bool Equals(Akka.Routing.Group other) { } public override bool Equals(object obj) { } diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Routing/UseRoleIgnoredSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/Routing/UseRoleIgnoredSpec.cs index 8584f9fc910..245f2cdf3d6 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Routing/UseRoleIgnoredSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Routing/UseRoleIgnoredSpec.cs @@ -272,7 +272,7 @@ private void A_cluster_must_group_local_on_role_b() var router = Sys.ActorOf( new ClusterRouterGroup( new RoundRobinGroup(paths: null), - new ClusterRouterGroupSettings(6, ImmutableHashSet.Create("/user/foo", "/user/bar"), allowLocalRoutees: false, useRole: role)).Props(), + new ClusterRouterGroupSettings(6, ImmutableHashSet.Create("/user/foo", "/user/bar"), allowLocalRoutees: true, useRole: role)).Props(), "router-3b"); AwaitAssert(() => CurrentRoutees(router).Count().Should().Be(4)); diff --git a/src/core/Akka.Cluster.Tests/ClusterDeployerSpec.cs b/src/core/Akka.Cluster.Tests/ClusterDeployerSpec.cs index 9996082bbbd..7fe8059ad89 100644 --- a/src/core/Akka.Cluster.Tests/ClusterDeployerSpec.cs +++ b/src/core/Akka.Cluster.Tests/ClusterDeployerSpec.cs @@ -84,7 +84,7 @@ public void RemoteDeployer_must_be_able_to_parse_akka_actor_deployment_with_spec deployment.Path.ShouldBe(service); deployment.RouterConfig.GetType().ShouldBe(typeof(ClusterRouterGroup)); deployment.RouterConfig.AsInstanceOf().Local.GetType().ShouldBe(typeof(RoundRobinGroup)); - deployment.RouterConfig.AsInstanceOf().Local.AsInstanceOf().Paths.ShouldBe(new[]{ "/user/myservice" }); + deployment.RouterConfig.AsInstanceOf().Local.AsInstanceOf().GetPaths(Sys).ShouldBe(new[]{ "/user/myservice" }); deployment.RouterConfig.AsInstanceOf().Settings.TotalInstances.ShouldBe(20); deployment.RouterConfig.AsInstanceOf().Settings.AllowLocalRoutees.ShouldBe(false); deployment.RouterConfig.AsInstanceOf().Settings.UseRole.ShouldBe("backend"); @@ -104,7 +104,7 @@ public void BugFix2266RemoteDeployer_must_be_able_to_parse_broadcast_group_clust deployment.Path.ShouldBe(service); deployment.RouterConfig.GetType().ShouldBe(typeof(ClusterRouterGroup)); deployment.RouterConfig.AsInstanceOf().Local.GetType().ShouldBe(typeof(BroadcastGroup)); - deployment.RouterConfig.AsInstanceOf().Local.AsInstanceOf().Paths.ShouldBe(new[] { "/user/myservice" }); + deployment.RouterConfig.AsInstanceOf().Local.AsInstanceOf().GetPaths(Sys).ShouldBe(new[] { "/user/myservice" }); deployment.RouterConfig.AsInstanceOf().Settings.TotalInstances.ShouldBe(10000); deployment.RouterConfig.AsInstanceOf().Settings.AllowLocalRoutees.ShouldBe(false); deployment.RouterConfig.AsInstanceOf().Settings.UseRole.ShouldBe("backend"); diff --git a/src/core/Akka.Cluster/Routing/ClusterRoutingConfig.cs b/src/core/Akka.Cluster/Routing/ClusterRoutingConfig.cs index 34bcfc5042b..fe7e6fba533 100644 --- a/src/core/Akka.Cluster/Routing/ClusterRoutingConfig.cs +++ b/src/core/Akka.Cluster/Routing/ClusterRoutingConfig.cs @@ -52,10 +52,12 @@ public ClusterRouterGroupSettings(int totalInstances, bool allowLocalRoutees, st /// /// Initializes a new instance of the class. /// - /// TBD - /// TBD - /// TBD - /// TBD + /// The total number of routees. Defaults to 10000. + /// The actor selection paths to use for each routee. + /// When true, allows routees to be deployed locally + /// on the node doing the deploying so long as that node also + /// satisfies the useRole setting when used. + /// The role of the node upon which we are able to create routees. /// /// This exception is thrown when either the specified is undefined /// or a path defined in the specified is an invalid relative actor path. @@ -79,7 +81,7 @@ public ClusterRouterGroupSettings(int totalInstances, IEnumerable routee } /// - /// TBD + /// The paths of the routees to use on each qualified node. /// public IEnumerable RouteesPaths { get; } @@ -150,7 +152,7 @@ public ClusterRouterPoolSettings(int totalInstances, int maxInstancesPerNode, bo } /// - /// TBD + /// The maximum number of routee actors that can be deployed per valid node. /// public int MaxInstancesPerNode { get; } diff --git a/src/core/Akka.Tests/Routing/ConfiguredLocalRoutingSpec.cs b/src/core/Akka.Tests/Routing/ConfiguredLocalRoutingSpec.cs index 07d587b8cac..d20b2d9612e 100644 --- a/src/core/Akka.Tests/Routing/ConfiguredLocalRoutingSpec.cs +++ b/src/core/Akka.Tests/Routing/ConfiguredLocalRoutingSpec.cs @@ -234,7 +234,7 @@ public async Task RouterConfig_must_use_routeesPaths_from_config() routerConfig.Should().BeOfType(); var randomGroup = (RandomGroup)routerConfig; - randomGroup.Paths.ShouldAllBeEquivalentTo(new List { "/user/service1", "/user/service2" }); + randomGroup.GetPaths(Sys).ShouldAllBeEquivalentTo(new List { "/user/service1", "/user/service2" }); var result = await actor.GracefulStop(3.Seconds()); result.Should().BeTrue(); diff --git a/src/core/Akka/Routing/Broadcast.cs b/src/core/Akka/Routing/Broadcast.cs index 7e3c675931c..fbc09bfc086 100644 --- a/src/core/Akka/Routing/Broadcast.cs +++ b/src/core/Akka/Routing/Broadcast.cs @@ -301,7 +301,7 @@ public BroadcastGroup(IEnumerable paths, string routerDispatcher) : base /// An enumeration of actor paths used during routee selection public override IEnumerable GetPaths(ActorSystem system) { - return Paths; + return InternalPaths; } /// @@ -325,7 +325,7 @@ public override Router CreateRouter(ActorSystem system) /// A new router with the provided dispatcher id. public Group WithDispatcher(string dispatcher) { - return new BroadcastGroup(Paths, dispatcher); + return new BroadcastGroup(InternalPaths, dispatcher); } /// @@ -337,7 +337,7 @@ public override ISurrogate ToSurrogate(ActorSystem system) { return new BroadcastGroupSurrogate { - Paths = Paths, + Paths = InternalPaths, RouterDispatcher = RouterDispatcher }; } diff --git a/src/core/Akka/Routing/ConsistentHashRouter.cs b/src/core/Akka/Routing/ConsistentHashRouter.cs index f5e73437cdc..c0ff513252a 100644 --- a/src/core/Akka/Routing/ConsistentHashRouter.cs +++ b/src/core/Akka/Routing/ConsistentHashRouter.cs @@ -696,7 +696,7 @@ public ConsistentHashingGroup( /// An enumeration of actor paths used during routee selection public override IEnumerable GetPaths(ActorSystem system) { - return Paths; + return InternalPaths; } /// @@ -722,7 +722,7 @@ public override Router CreateRouter(ActorSystem system) /// A new router with the provided dispatcher id. public ConsistentHashingGroup WithDispatcher(string dispatcher) { - return new ConsistentHashingGroup(Paths, VirtualNodesFactor, _hashMapping, dispatcher); + return new ConsistentHashingGroup(InternalPaths, VirtualNodesFactor, _hashMapping, dispatcher); } /// @@ -736,7 +736,7 @@ public ConsistentHashingGroup WithDispatcher(string dispatcher) /// A new router with the provided . public ConsistentHashingGroup WithVirtualNodesFactor(int vnodes) { - return new ConsistentHashingGroup(Paths, vnodes, _hashMapping, RouterDispatcher); + return new ConsistentHashingGroup(InternalPaths, vnodes, _hashMapping, RouterDispatcher); } /// @@ -750,7 +750,7 @@ public ConsistentHashingGroup WithVirtualNodesFactor(int vnodes) /// A new router with the provided . public ConsistentHashingGroup WithHashMapping(ConsistentHashMapping mapping) { - return new ConsistentHashingGroup(Paths, VirtualNodesFactor, mapping, RouterDispatcher); + return new ConsistentHashingGroup(InternalPaths, VirtualNodesFactor, mapping, RouterDispatcher); } /// @@ -786,7 +786,7 @@ public override ISurrogate ToSurrogate(ActorSystem system) { return new ConsistentHashingGroupSurrogate { - Paths = Paths, + Paths = InternalPaths, RouterDispatcher = RouterDispatcher }; } diff --git a/src/core/Akka/Routing/Random.cs b/src/core/Akka/Routing/Random.cs index 5dd30297537..0de2fa203ab 100644 --- a/src/core/Akka/Routing/Random.cs +++ b/src/core/Akka/Routing/Random.cs @@ -290,7 +290,7 @@ public RandomGroup(IEnumerable paths, string routerDispatcher) /// An enumeration of actor paths used during routee selection public override IEnumerable GetPaths(ActorSystem system) { - return Paths; + return InternalPaths; } /// @@ -313,7 +313,7 @@ public override Router CreateRouter(ActorSystem system) /// A new router with the provided dispatcher id. public RandomGroup WithDispatcher(string dispatcher) { - return new RandomGroup(Paths, dispatcher); + return new RandomGroup(InternalPaths, dispatcher); } /// @@ -325,7 +325,7 @@ public override ISurrogate ToSurrogate(ActorSystem system) { return new RandomGroupSurrogate { - Paths = Paths, + Paths = InternalPaths, RouterDispatcher = RouterDispatcher }; } diff --git a/src/core/Akka/Routing/RoundRobin.cs b/src/core/Akka/Routing/RoundRobin.cs index 5f4403ae6ec..f9f7dc660b5 100644 --- a/src/core/Akka/Routing/RoundRobin.cs +++ b/src/core/Akka/Routing/RoundRobin.cs @@ -354,7 +354,7 @@ public RoundRobinGroup(IEnumerable paths, string routerDispatcher) /// An enumeration of actor paths used during routee selection public override IEnumerable GetPaths(ActorSystem system) { - return Paths; + return InternalPaths; } /// @@ -377,7 +377,7 @@ public override Router CreateRouter(ActorSystem system) /// A new router with the provided dispatcher id. public Group WithDispatcher(string dispatcherId) { - return new RoundRobinGroup(Paths, dispatcherId); + return new RoundRobinGroup(InternalPaths, dispatcherId); } /// @@ -389,7 +389,7 @@ public override ISurrogate ToSurrogate(ActorSystem system) { return new RoundRobinGroupSurrogate { - Paths = Paths, + Paths = InternalPaths, RouterDispatcher = RouterDispatcher }; } diff --git a/src/core/Akka/Routing/RoutedActorCell.cs b/src/core/Akka/Routing/RoutedActorCell.cs index b3a34cb6ecc..ff8bc6e4831 100644 --- a/src/core/Akka/Routing/RoutedActorCell.cs +++ b/src/core/Akka/Routing/RoutedActorCell.cs @@ -167,7 +167,7 @@ public override void Start() var deprecatedPaths = group.Paths; var paths = deprecatedPaths == null - ? group.GetPaths(System).ToArray() + ? group.GetPaths(System)?.ToArray() : deprecatedPaths.ToArray(); if (paths.NonEmpty()) diff --git a/src/core/Akka/Routing/RouterConfig.cs b/src/core/Akka/Routing/RouterConfig.cs index c84cccff82c..edd50569015 100644 --- a/src/core/Akka/Routing/RouterConfig.cs +++ b/src/core/Akka/Routing/RouterConfig.cs @@ -145,13 +145,20 @@ public abstract class Group : RouterConfig, IEquatable /// TBD protected Group(IEnumerable paths, string routerDispatcher) : base(routerDispatcher) { - Paths = paths; + // equivalent of turning the paths into an immutable sequence + InternalPaths = paths?.ToArray() ?? new string[0]; } /// - /// TBD + /// Internal property for holding the supplied paths + /// + protected readonly string[] InternalPaths; + + /// + /// Retrieves the paths of all routees declared on this router. /// - public IEnumerable Paths { get; } + [Obsolete("Deprecated since Akka.NET v1.1. Use Paths(ActorSystem) instead.")] + public IEnumerable Paths => null; /// /// Retrieves the actor paths used by this router during routee selection. @@ -194,7 +201,7 @@ public bool Equals(Group other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Paths.SequenceEqual(other.Paths); + return InternalPaths.SequenceEqual(other.InternalPaths); } /// @@ -207,7 +214,7 @@ public override bool Equals(object obj) } /// - public override int GetHashCode() => Paths?.GetHashCode() ?? 0; + public override int GetHashCode() => InternalPaths?.GetHashCode() ?? 0; } /// diff --git a/src/core/Akka/Routing/ScatterGatherFirstCompleted.cs b/src/core/Akka/Routing/ScatterGatherFirstCompleted.cs index 3140b86262d..f54e0b1f88a 100644 --- a/src/core/Akka/Routing/ScatterGatherFirstCompleted.cs +++ b/src/core/Akka/Routing/ScatterGatherFirstCompleted.cs @@ -441,7 +441,7 @@ public override Router CreateRouter(ActorSystem system) /// An enumeration of actor paths used during routee selection public override IEnumerable GetPaths(ActorSystem system) { - return Paths; + return InternalPaths; } /// @@ -454,7 +454,7 @@ public override IEnumerable GetPaths(ActorSystem system) /// A new router with the provided dispatcher id. public ScatterGatherFirstCompletedGroup WithDispatcher(string dispatcher) { - return new ScatterGatherFirstCompletedGroup(Paths, Within, RouterDispatcher); + return new ScatterGatherFirstCompletedGroup(InternalPaths, Within, RouterDispatcher); } #region Surrogate @@ -467,7 +467,7 @@ public override ISurrogate ToSurrogate(ActorSystem system) { return new ScatterGatherFirstCompletedGroupSurrogate { - Paths = Paths, + Paths = InternalPaths, Within = Within, RouterDispatcher = RouterDispatcher }; diff --git a/src/core/Akka/Routing/TailChopping.cs b/src/core/Akka/Routing/TailChopping.cs index fb2ca5f6ba6..c03eca78c10 100644 --- a/src/core/Akka/Routing/TailChopping.cs +++ b/src/core/Akka/Routing/TailChopping.cs @@ -456,7 +456,7 @@ public override Router CreateRouter(ActorSystem system) /// An enumeration of actor paths used during routee selection public override IEnumerable GetPaths(ActorSystem system) { - return Paths; + return InternalPaths; } /// @@ -469,7 +469,7 @@ public override IEnumerable GetPaths(ActorSystem system) /// A new router with the provided dispatcher id. public TailChoppingGroup WithDispatcher(string dispatcher) { - return new TailChoppingGroup(Paths, Within, Interval, dispatcher); + return new TailChoppingGroup(InternalPaths, Within, Interval, dispatcher); } /// @@ -481,7 +481,7 @@ public override ISurrogate ToSurrogate(ActorSystem system) { return new TailChoppingGroupSurrogate { - Paths = Paths, + Paths = InternalPaths, Within = Within, Interval = Interval, RouterDispatcher = RouterDispatcher From 142db19f81adda87ad0a6aa26c312ed5902c1740 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 31 Jan 2018 18:10:23 -0600 Subject: [PATCH 14/70] upgraded Akka.Serialization.Hyperion to Hyperion 0.9.8 --- .../Akka.Serialization.Hyperion.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contrib/serializers/Akka.Serialization.Hyperion/Akka.Serialization.Hyperion.csproj b/src/contrib/serializers/Akka.Serialization.Hyperion/Akka.Serialization.Hyperion.csproj index b8c2bdc05d0..53fbf5dbd38 100644 --- a/src/contrib/serializers/Akka.Serialization.Hyperion/Akka.Serialization.Hyperion.csproj +++ b/src/contrib/serializers/Akka.Serialization.Hyperion/Akka.Serialization.Hyperion.csproj @@ -10,7 +10,7 @@ - + From 1df0d66ab87b79bf355325d6101707a1d902cb17 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 1 Feb 2018 09:08:35 -0800 Subject: [PATCH 15/70] expose RemoteActorRef APIs for extensibility (#3304) * expose RemoteActorRef APIs for extensibility * made ClusterActorRefProvider public * created IClusterActorRefProvider marker interface --- .../CoreAPISpec.ApproveCluster.approved.txt | 9 ++ .../CoreAPISpec.ApproveRemote.approved.txt | 26 +++- src/core/Akka.Cluster/Cluster.cs | 7 +- .../Akka.Cluster/ClusterActorRefProvider.cs | 13 +- src/core/Akka.Remote.TestKit/Extension.cs | 2 +- .../InboundMessageDispatcherSpec.cs | 4 +- src/core/Akka.Remote/Endpoint.cs | 72 +++++------ src/core/Akka.Remote/RemoteActorRef.cs | 13 +- .../Akka.Remote/RemoteActorRefProvider.cs | 122 ++++++++++++++---- src/core/Akka.Remote/RemoteTransport.cs | 1 + src/core/Akka.Remote/RemoteWatcher.cs | 18 +-- src/core/Akka.Remote/Remoting.cs | 10 +- .../Serialization/ActorPathCache.cs | 3 +- .../Serialization/ActorRefResolveCache.cs | 10 +- .../Akka.Remote/Transport/AkkaPduCodec.cs | 4 +- .../Transport/AkkaProtocolTransport.cs | 2 +- 16 files changed, 219 insertions(+), 97 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCluster.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCluster.approved.txt index b443d08311c..65be772a7d0 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCluster.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCluster.approved.txt @@ -48,6 +48,13 @@ namespace Akka.Cluster public void Unsubscribe(Akka.Actor.IActorRef subscriber) { } public void Unsubscribe(Akka.Actor.IActorRef subscriber, System.Type to) { } } + [Akka.Annotations.InternalApiAttribute()] + public class ClusterActorRefProvider : Akka.Remote.RemoteActorRefProvider, Akka.Actor.IActorRefProvider, Akka.Cluster.IClusterActorRefProvider, Akka.Remote.IRemoteActorRefProvider + { + public ClusterActorRefProvider(string systemName, Akka.Actor.Settings settings, Akka.Event.EventStream eventStream) { } + protected override Akka.Actor.IActorRef CreateRemoteWatcher(Akka.Actor.Internal.ActorSystemImpl system) { } + public override void Init(Akka.Actor.Internal.ActorSystemImpl system) { } + } public class ClusterEvent { public static readonly Akka.Cluster.ClusterEvent.SubscriptionInitialStateMode InitialStateAsEvents; @@ -201,6 +208,8 @@ namespace Akka.Cluster public bool VerboseGossipReceivedLogging { get; } public bool VerboseHeartbeatLogging { get; } } + [Akka.Annotations.InternalApiAttribute()] + public interface IClusterActorRefProvider : Akka.Actor.IActorRefProvider, Akka.Remote.IRemoteActorRefProvider { } public interface IClusterMessage { } public interface IDowningProvider { diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveRemote.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveRemote.approved.txt index 50622c1bff1..231567d4ff0 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveRemote.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveRemote.approved.txt @@ -121,6 +121,22 @@ namespace Akka.Remote void Remove(T resource); void Reset(); } + [Akka.Annotations.InternalApiAttribute()] + public interface IRemoteActorRefProvider : Akka.Actor.IActorRefProvider + { + Akka.Actor.IInternalActorRef RemoteDaemon { get; } + Akka.Remote.RemoteSettings RemoteSettings { get; } + Akka.Actor.IActorRef RemoteWatcher { get; } + Akka.Remote.RemoteTransport Transport { get; } + bool HasAddress(Akka.Actor.Address address); + Akka.Actor.IActorRef InternalResolveActorRef(string path); + Akka.Actor.Deploy LookUpRemotes(System.Collections.Generic.IEnumerable p); + void Quarantine(Akka.Actor.Address address, System.Nullable uid); + Akka.Actor.IInternalActorRef ResolveActorRefWithLocalAddress(string path, Akka.Actor.Address localAddress); + void UseActorOnNode(Akka.Remote.RemoteActorRef actor, Akka.Actor.Props props, Akka.Actor.Deploy deploy, Akka.Actor.IInternalActorRef supervisor); + } + [Akka.Annotations.InternalApiAttribute()] + public interface IRemoteRef : Akka.Actor.IActorRefScope { } public class PhiAccrualFailureDetector : Akka.Remote.FailureDetector { public PhiAccrualFailureDetector(double threshold, int maxSampleSize, System.TimeSpan minStdDeviation, System.TimeSpan acceptableHeartbeatPause, System.TimeSpan firstHeartbeatEstimate, Akka.Remote.Clock clock = null) { } @@ -140,6 +156,7 @@ namespace Akka.Remote } public class RemoteActorRef : Akka.Actor.InternalActorRefBase, Akka.Actor.IActorRefScope, Akka.Remote.IRemoteRef { + public RemoteActorRef(Akka.Remote.RemoteTransport remote, Akka.Actor.Address localAddressToUse, Akka.Actor.ActorPath path, Akka.Actor.IInternalActorRef parent, Akka.Actor.Props props, Akka.Actor.Deploy deploy) { } public override bool IsLocal { get; } [System.ObsoleteAttribute("Use Context.Watch and Receive [1.1.0]")] public override bool IsTerminated { get; } @@ -158,7 +175,7 @@ namespace Akka.Remote protected override void TellInternal(object message, Akka.Actor.IActorRef sender) { } } [Akka.Annotations.InternalApiAttribute()] - public class RemoteActorRefProvider : Akka.Actor.IActorRefProvider + public class RemoteActorRefProvider : Akka.Actor.IActorRefProvider, Akka.Remote.IRemoteActorRefProvider { public RemoteActorRefProvider(string systemName, Akka.Actor.Settings settings, Akka.Event.EventStream eventStream) { } public Akka.Actor.IActorRef DeadLetters { get; } @@ -166,6 +183,8 @@ namespace Akka.Remote public Akka.Actor.Deployer Deployer { get; set; } public Akka.Actor.LocalActorRef Guardian { get; } public Akka.Actor.IInternalActorRef RemoteDaemon { get; } + public Akka.Remote.RemoteSettings RemoteSettings { get; } + public Akka.Actor.IActorRef RemoteWatcher { get; } public Akka.Actor.IInternalActorRef RootGuardian { get; } public Akka.Actor.ActorPath RootPath { get; } public Akka.Actor.Settings Settings { get; } @@ -175,14 +194,19 @@ namespace Akka.Remote public Akka.Remote.RemoteTransport Transport { get; } public Akka.Actor.IInternalActorRef ActorOf(Akka.Actor.Internal.ActorSystemImpl system, Akka.Actor.Props props, Akka.Actor.IInternalActorRef supervisor, Akka.Actor.ActorPath path, bool systemService, Akka.Actor.Deploy deploy, bool lookupDeploy, bool async) { } protected virtual Akka.Actor.IActorRef CreateRemoteDeploymentWatcher(Akka.Actor.Internal.ActorSystemImpl system) { } + protected virtual Akka.Actor.IInternalActorRef CreateRemoteRef(Akka.Actor.ActorPath actorPath, Akka.Actor.Address localAddress) { } protected virtual Akka.Actor.IActorRef CreateRemoteWatcher(Akka.Actor.Internal.ActorSystemImpl system) { } protected Akka.Remote.DefaultFailureDetectorRegistry CreateRemoteWatcherFailureDetector(Akka.Actor.ActorSystem system) { } public Akka.Actor.Address GetExternalAddressFor(Akka.Actor.Address address) { } + public bool HasAddress(Akka.Actor.Address address) { } public virtual void Init(Akka.Actor.Internal.ActorSystemImpl system) { } + public Akka.Actor.IActorRef InternalResolveActorRef(string path) { } + public Akka.Actor.Deploy LookUpRemotes(System.Collections.Generic.IEnumerable p) { } public void Quarantine(Akka.Actor.Address address, System.Nullable uid) { } public void RegisterTempActor(Akka.Actor.IInternalActorRef actorRef, Akka.Actor.ActorPath path) { } public Akka.Actor.IActorRef ResolveActorRef(string path) { } public Akka.Actor.IActorRef ResolveActorRef(Akka.Actor.ActorPath actorPath) { } + public Akka.Actor.IInternalActorRef ResolveActorRefWithLocalAddress(string path, Akka.Actor.Address localAddress) { } public Akka.Actor.IActorRef RootGuardianAt(Akka.Actor.Address address) { } public Akka.Actor.ActorPath TempPath() { } public void UnregisterTempActor(Akka.Actor.ActorPath path) { } diff --git a/src/core/Akka.Cluster/Cluster.cs b/src/core/Akka.Cluster/Cluster.cs index 38d55b7efbd..b9a00169ed6 100644 --- a/src/core/Akka.Cluster/Cluster.cs +++ b/src/core/Akka.Cluster/Cluster.cs @@ -52,8 +52,6 @@ public override Cluster CreateExtension(ExtendedActorSystem system) /// public class Cluster : IExtension { - //TODO: Issue with missing overrides for Get and Lookup - /// /// Retrieves the extension from the specified actor system. /// @@ -95,10 +93,9 @@ public Cluster(ActorSystemImpl system) System = system; Settings = new ClusterSettings(system.Settings.Config, system.Name); - var provider = system.Provider as ClusterActorRefProvider; - if (provider == null) + if (!(system.Provider is IClusterActorRefProvider provider)) throw new ConfigurationException( - $"ActorSystem {system} needs to have a 'ClusterActorRefProvider' enabled in the configuration, currently uses {system.Provider.GetType().FullName}"); + $"ActorSystem {system} needs to have a 'IClusterActorRefProvider' enabled in the configuration, currently uses {system.Provider.GetType().FullName}"); SelfUniqueAddress = new UniqueAddress(provider.Transport.DefaultAddress, AddressUidExtension.Uid(system)); _log = Logging.GetLogger(system, "Cluster"); diff --git a/src/core/Akka.Cluster/ClusterActorRefProvider.cs b/src/core/Akka.Cluster/ClusterActorRefProvider.cs index 5d0bed37823..cadc2718a8f 100644 --- a/src/core/Akka.Cluster/ClusterActorRefProvider.cs +++ b/src/core/Akka.Cluster/ClusterActorRefProvider.cs @@ -8,6 +8,7 @@ using System; using Akka.Actor; using Akka.Actor.Internal; +using Akka.Annotations; using Akka.Cluster.Configuration; using Akka.Cluster.Routing; using Akka.Configuration; @@ -18,6 +19,15 @@ namespace Akka.Cluster { + /// + /// INTERNAL API. + /// + /// Marker interface for signifying that this can be used in combination with the + /// ActorSystem extension. + /// + [InternalApi] + public interface IClusterActorRefProvider : IRemoteActorRefProvider { } + /// /// INTERNAL API /// @@ -25,7 +35,8 @@ namespace Akka.Cluster /// extension, i.e. the cluster will automatically be started when /// the `ClusterActorRefProvider` is used. /// - internal class ClusterActorRefProvider : RemoteActorRefProvider + [InternalApi] + public class ClusterActorRefProvider : RemoteActorRefProvider, IClusterActorRefProvider { /// /// TBD diff --git a/src/core/Akka.Remote.TestKit/Extension.cs b/src/core/Akka.Remote.TestKit/Extension.cs index e46fa135603..9d779943fa9 100644 --- a/src/core/Akka.Remote.TestKit/Extension.cs +++ b/src/core/Akka.Remote.TestKit/Extension.cs @@ -76,7 +76,7 @@ public TestConductor(ActorSystem system) { _settings = new TestConductorSettings(system.Settings.Config.WithFallback(TestConductorConfigFactory.Default()) .GetConfig("akka.testconductor")); - _transport = system.AsInstanceOf().Provider.AsInstanceOf().Transport; + _transport = system.AsInstanceOf().Provider.AsInstanceOf().Transport; _address = _transport.DefaultAddress; _system = system; } diff --git a/src/core/Akka.Remote.Tests.Performance/InboundMessageDispatcherSpec.cs b/src/core/Akka.Remote.Tests.Performance/InboundMessageDispatcherSpec.cs index 9d5a5984bf6..1f54167f56d 100644 --- a/src/core/Akka.Remote.Tests.Performance/InboundMessageDispatcherSpec.cs +++ b/src/core/Akka.Remote.Tests.Performance/InboundMessageDispatcherSpec.cs @@ -40,11 +40,11 @@ private class BenchmarkActorRef : MinimalActorRef { private readonly Counter _counter; - public BenchmarkActorRef(Counter counter, RemoteActorRefProvider provider) + public BenchmarkActorRef(Counter counter, IRemoteActorRefProvider provider) { _counter = counter; Provider = provider; - Path = new RootActorPath(Provider.DefaultAddress) / "user" / "tempRef"; + Path = new RootActorPath(provider.DefaultAddress) / "user" / "tempRef"; } protected override void TellInternal(object message, IActorRef sender) diff --git a/src/core/Akka.Remote/Endpoint.cs b/src/core/Akka.Remote/Endpoint.cs index 7f31a040bc5..4d0b3749c4e 100644 --- a/src/core/Akka.Remote/Endpoint.cs +++ b/src/core/Akka.Remote/Endpoint.cs @@ -50,11 +50,11 @@ void Dispatch(IInternalActorRef recipient, Address recipientAddress, SerializedM /// internal class DefaultMessageDispatcher : IInboundMessageDispatcher { - private ActorSystem system; - private RemoteActorRefProvider provider; - private ILoggingAdapter log; - private IInternalActorRef remoteDaemon; - private RemoteSettings settings; + private readonly ActorSystem _system; + private readonly IRemoteActorRefProvider _provider; + private readonly ILoggingAdapter _log; + private readonly IInternalActorRef _remoteDaemon; + private readonly RemoteSettings _settings; /// /// TBD @@ -62,13 +62,13 @@ internal class DefaultMessageDispatcher : IInboundMessageDispatcher /// TBD /// TBD /// TBD - public DefaultMessageDispatcher(ActorSystem system, RemoteActorRefProvider provider, ILoggingAdapter log) + public DefaultMessageDispatcher(ActorSystem system, IRemoteActorRefProvider provider, ILoggingAdapter log) { - this.system = system; - this.provider = provider; - this.log = log; - remoteDaemon = provider.RemoteDaemon; - settings = provider.RemoteSettings; + this._system = system; + this._provider = provider; + this._log = log; + _remoteDaemon = provider.RemoteDaemon; + _settings = provider.RemoteSettings; } /// @@ -81,45 +81,45 @@ public DefaultMessageDispatcher(ActorSystem system, RemoteActorRefProvider provi public void Dispatch(IInternalActorRef recipient, Address recipientAddress, SerializedMessage message, IActorRef senderOption = null) { - var payload = MessageSerializer.Deserialize(system, message); + var payload = MessageSerializer.Deserialize(_system, message); Type payloadClass = payload?.GetType(); - var sender = senderOption ?? system.DeadLetters; + var sender = senderOption ?? _system.DeadLetters; var originalReceiver = recipient.Path; // message is intended for the RemoteDaemon, usually a command to create a remote actor - if (recipient.Equals(remoteDaemon)) + if (recipient.Equals(_remoteDaemon)) { - if (settings.UntrustedMode) log.Debug("dropping daemon message in untrusted mode"); + if (_settings.UntrustedMode) _log.Debug("dropping daemon message in untrusted mode"); else { - if (settings.LogReceive) + if (_settings.LogReceive) { var msgLog = $"RemoteMessage: {payload} to {recipient}<+{originalReceiver} from {sender}"; - log.Debug("received daemon message [{0}]", msgLog); + _log.Debug("received daemon message [{0}]", msgLog); } - remoteDaemon.Tell(payload); + _remoteDaemon.Tell(payload); } } //message is intended for a local recipient else if ((recipient is ILocalRef || recipient is RepointableActorRef) && recipient.IsLocal) { - if (settings.LogReceive) + if (_settings.LogReceive) { var msgLog = $"RemoteMessage: {payload} to {recipient}<+{originalReceiver} from {sender}"; - log.Debug("received local message [{0}]", msgLog); + _log.Debug("received local message [{0}]", msgLog); } if (payload is ActorSelectionMessage) { var sel = (ActorSelectionMessage)payload; var actorPath = "/" + string.Join("/", sel.Elements.Select(x => x.ToString())); - if (settings.UntrustedMode - && (!settings.TrustedSelectionPaths.Contains(actorPath) + if (_settings.UntrustedMode + && (!_settings.TrustedSelectionPaths.Contains(actorPath) || sel.Message is IPossiblyHarmful - || !recipient.Equals(provider.RootGuardian))) + || !recipient.Equals(_provider.RootGuardian))) { - log.Debug( + _log.Debug( "operating in UntrustedMode, dropping inbound actor selection to [{0}], allow it" + "by adding the path to 'akka.remote.trusted-selection-paths' in configuration", actorPath); @@ -130,9 +130,9 @@ public void Dispatch(IInternalActorRef recipient, Address recipientAddress, Seri ActorSelection.DeliverSelection(recipient, sender, sel); } } - else if (payload is IPossiblyHarmful && settings.UntrustedMode) + else if (payload is IPossiblyHarmful && _settings.UntrustedMode) { - log.Debug("operating in UntrustedMode, dropping inbound IPossiblyHarmful message of type {0}", + _log.Debug("operating in UntrustedMode, dropping inbound IPossiblyHarmful message of type {0}", payload.GetType()); } else if (payload is ISystemMessage) @@ -147,30 +147,30 @@ public void Dispatch(IInternalActorRef recipient, Address recipientAddress, Seri // message is intended for a remote-deployed recipient else if ((recipient is IRemoteRef || recipient is RepointableActorRef) && !recipient.IsLocal && - !settings.UntrustedMode) + !_settings.UntrustedMode) { - if (settings.LogReceive) + if (_settings.LogReceive) { var msgLog = string.Format("RemoteMessage: {0} to {1}<+{2} from {3}", payload, recipient, originalReceiver, sender); - log.Debug("received remote-destined message {0}", msgLog); + _log.Debug("received remote-destined message {0}", msgLog); } - if (provider.Transport.Addresses.Contains(recipientAddress)) + if (_provider.Transport.Addresses.Contains(recipientAddress)) { //if it was originally addressed to us but is in fact remote from our point of view (i.e. remote-deployed) recipient.Tell(payload, sender); } else { - log.Error( + _log.Error( "Dropping message [{0}] for non-local recipient [{1}] arriving at [{2}] inbound addresses [{3}]", - payloadClass, recipient, recipientAddress, string.Join(",", provider.Transport.Addresses)); + payloadClass, recipient, recipientAddress, string.Join(",", _provider.Transport.Addresses)); } } else { - log.Error( + _log.Error( "Dropping message [{0}] for non-local recipient [{1}] arriving at [{2}] inbound addresses [{3}]", - payloadClass, recipient, recipientAddress, string.Join(",", provider.Transport.Addresses)); + payloadClass, recipient, recipientAddress, string.Join(",", _provider.Transport.Addresses)); } } } @@ -1036,7 +1036,7 @@ public EndpointWriter( private readonly AkkaPduCodec _codec; private readonly IActorRef _reliableDeliverySupervisor; private readonly ActorSystem _system; - private readonly RemoteActorRefProvider _provider; + private readonly IRemoteActorRefProvider _provider; private readonly ConcurrentDictionary _receiveBuffers; private DisassociateInfo _stopReason = DisassociateInfo.Unknown; @@ -1847,7 +1847,7 @@ public EndpointReader( private readonly int _uid; private readonly IInboundMessageDispatcher _msgDispatch; - private readonly RemoteActorRefProvider _provider; + private readonly IRemoteActorRefProvider _provider; private AckedReceiveBuffer _ackedReceiveBuffer = new AckedReceiveBuffer(); #region ActorBase overrides diff --git a/src/core/Akka.Remote/RemoteActorRef.cs b/src/core/Akka.Remote/RemoteActorRef.cs index 4ded8020eb9..27c054bc2b7 100644 --- a/src/core/Akka.Remote/RemoteActorRef.cs +++ b/src/core/Akka.Remote/RemoteActorRef.cs @@ -10,6 +10,7 @@ using System.Linq.Expressions; using System.Runtime.InteropServices; using Akka.Actor; +using Akka.Annotations; using Akka.Dispatch.SysMsg; using Akka.Event; @@ -19,10 +20,12 @@ namespace Akka.Remote /// Marker interface for Actors that are deployed in a remote scope /// // ReSharper disable once InconsistentNaming - internal interface IRemoteRef : IActorRefScope { } + [InternalApi] + public interface IRemoteRef : IActorRefScope { } /// - /// Class RemoteActorRef. + /// RemoteActorRef - used to provide a local handle to an actor + /// running in a remote process. /// public class RemoteActorRef : InternalActorRefBase, IRemoteRef { @@ -51,7 +54,7 @@ public class RemoteActorRef : InternalActorRefBase, IRemoteRef /// The parent. /// The props. /// The deploy. - internal RemoteActorRef(RemoteTransport remote, Address localAddressToUse, ActorPath path, IInternalActorRef parent, + public RemoteActorRef(RemoteTransport remote, Address localAddressToUse, ActorPath path, IInternalActorRef parent, Props props, Deploy deploy) { Remote = remote; @@ -92,10 +95,10 @@ public override IActorRefProvider Provider get { return Remote.Provider; } } - private RemoteActorRefProvider RemoteProvider => Provider as RemoteActorRefProvider; + private IRemoteActorRefProvider RemoteProvider => Provider as IRemoteActorRefProvider; /// - /// Obsolete. Use or Receive<> + /// Obsolete. Use or Receive<> /// [Obsolete("Use Context.Watch and Receive [1.1.0]")] public override bool IsTerminated { get { return false; } } diff --git a/src/core/Akka.Remote/RemoteActorRefProvider.cs b/src/core/Akka.Remote/RemoteActorRefProvider.cs index ed9e23ad53c..91600bb12d0 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -27,7 +27,84 @@ namespace Akka.Remote /// INTERNAL API /// [InternalApi] - public class RemoteActorRefProvider : IActorRefProvider + public interface IRemoteActorRefProvider : IActorRefProvider + { + /// + /// Remoting system daemon responsible for powering remote deployment capabilities. + /// + IInternalActorRef RemoteDaemon { get; } + + /// + /// The remote death watcher. + /// + IActorRef RemoteWatcher { get; } + + /// + /// The remote transport. Wraps all of the underlying physical network transports. + /// + RemoteTransport Transport { get; } + + /// + /// The remoting settings + /// + RemoteSettings RemoteSettings { get; } + + /// + /// Looks up local overrides for remote deployments + /// + /// + /// + Deploy LookUpRemotes(IEnumerable p); + + /// + /// Determines if a particular network address is assigned to any of this 's transports. + /// + /// The address to check. + /// true if the address is assigned to any bound transports; false otherwise. + bool HasAddress(Address address); + + /// + /// INTERNAL API. + /// + /// Called in deserialization of incoming remote messages where the correct local address is known. + /// + /// TBD + /// TBD + /// TBD + IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address localAddress); + + /// + /// INTERNAL API: this is used by the via the public + /// method. + /// + /// The path of the actor we intend to resolve. + /// An if a match was found. Otherwise nobody. + IActorRef InternalResolveActorRef(string path); + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + void UseActorOnNode(RemoteActorRef actor, Props props, Deploy deploy, IInternalActorRef supervisor); + + /// + /// Marks a remote system as out of sync and prevents reconnects until the quarantine timeout elapses. + /// + /// Address of the remote system to be quarantined + /// UID of the remote system, if the uid is not defined it will not be a strong quarantine but + /// the current endpoint writer will be stopped (dropping system messages) and the address will be gated + /// + void Quarantine(Address address, int? uid); + } + + /// + /// INTERNAL API + /// + [InternalApi] + public class RemoteActorRefProvider : IRemoteActorRefProvider { private readonly ILoggingAdapter _log; @@ -80,7 +157,7 @@ private Internals CreateInternals() /// /// The remoting settings /// - internal RemoteSettings RemoteSettings { get; private set; } + public RemoteSettings RemoteSettings { get; private set; } /* these are only available after Init() is called */ @@ -146,7 +223,7 @@ public void UnregisterTempActor(ActorPath path) /// /// The remote death watcher. /// - internal IActorRef RemoteWatcher => _remoteWatcher; + public IActorRef RemoteWatcher => _remoteWatcher; private volatile IActorRef _remoteDeploymentWatcher; /// @@ -328,7 +405,7 @@ public IInternalActorRef ActorOf(ActorSystemImpl system, Props props, IInternalA /// /// /// - private Deploy LookUpRemotes(IEnumerable p) + public Deploy LookUpRemotes(IEnumerable p) { if (p == null || !p.Any()) return Deploy.None; if (p.Head().Equals("remote")) return LookUpRemotes(p.Drop(3)); @@ -336,7 +413,7 @@ private Deploy LookUpRemotes(IEnumerable p) return Deploy.None; } - private bool HasAddress(Address address) + public bool HasAddress(Address address) { return address == _local.RootPath.Address || address == RootPath.Address || Transport.Addresses.Any(a => a == address); } @@ -352,13 +429,7 @@ public IActorRef RootGuardianAt(Address address) { return RootGuardian; } - return new RemoteActorRef( - Transport, - Transport.LocalAddressForRemote(address), - new RootActorPath(address), - ActorRefs.Nobody, - Props.None, - Deploy.None); + return CreateRemoteRef(new RootActorPath(address), Transport.LocalAddressForRemote(address)); } private IInternalActorRef LocalActorOf(ActorSystemImpl system, Props props, IInternalActorRef supervisor, @@ -390,7 +461,7 @@ private bool TryParseCachedPath(string actorPath, out ActorPath path) /// TBD /// TBD /// TBD - internal IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address localAddress) + public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address localAddress) { ActorPath actorPath; if (TryParseCachedPath(path, out actorPath)) @@ -403,13 +474,25 @@ internal IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address return RootGuardian; return _local.ResolveActorRef(RootGuardian, actorPath.ElementsWithUid); } - - return new RemoteActorRef(Transport, localAddress, new RootActorPath(actorPath.Address) / actorPath.ElementsWithUid, ActorRefs.Nobody, Props.None, Deploy.None); + + return CreateRemoteRef(new RootActorPath(actorPath.Address) / actorPath.ElementsWithUid, localAddress); } _log.Debug("resolve of unknown path [{0}] failed", path); return InternalDeadLetters; } + + /// + /// Used to create instances upon deserialiation inside the Akka.Remote pipeline. + /// + /// The remote path of the actor on its physical location on the network. + /// The local path of the actor. + /// An instance. + protected virtual IInternalActorRef CreateRemoteRef(ActorPath actorPath, Address localAddress) + { + return new RemoteActorRef(Transport, localAddress, actorPath, ActorRefs.Nobody, Props.None, Deploy.None); + } + /// /// Resolves a deserialized path into an /// @@ -432,7 +515,7 @@ public IActorRef ResolveActorRef(string path) /// /// The path of the actor we intend to resolve. /// An if a match was found. Otherwise nobody. - internal IActorRef InternalResolveActorRef(string path) + public IActorRef InternalResolveActorRef(string path) { if (path == String.Empty) return ActorRefs.NoSender; @@ -459,12 +542,7 @@ public IActorRef ResolveActorRef(ActorPath actorPath) } try { - return new RemoteActorRef(Transport, - Transport.LocalAddressForRemote(actorPath.Address), - actorPath, - ActorRefs.Nobody, - Props.None, - Deploy.None); + return CreateRemoteRef(actorPath, Transport.LocalAddressForRemote(actorPath.Address)); } catch (Exception ex) { diff --git a/src/core/Akka.Remote/RemoteTransport.cs b/src/core/Akka.Remote/RemoteTransport.cs index 37843c81848..82ed772561b 100644 --- a/src/core/Akka.Remote/RemoteTransport.cs +++ b/src/core/Akka.Remote/RemoteTransport.cs @@ -42,6 +42,7 @@ protected RemoteTransport(ExtendedActorSystem system, RemoteActorRefProvider pro /// TBD /// public ExtendedActorSystem System { get; private set; } + /// /// TBD /// diff --git a/src/core/Akka.Remote/RemoteWatcher.cs b/src/core/Akka.Remote/RemoteWatcher.cs index 88bdc43019e..324f2f33d35 100644 --- a/src/core/Akka.Remote/RemoteWatcher.cs +++ b/src/core/Akka.Remote/RemoteWatcher.cs @@ -375,7 +375,7 @@ TimeSpan heartbeatExpectedResponseAfter { _failureDetector = failureDetector; _heartbeatExpectedResponseAfter = heartbeatExpectedResponseAfter; - var systemProvider = Context.System.AsInstanceOf().Provider as RemoteActorRefProvider; + var systemProvider = Context.System.AsInstanceOf().Provider as IRemoteActorRefProvider; if (systemProvider != null) _remoteProvider = systemProvider; else throw new ConfigurationException( $"ActorSystem {Context.System} needs to have a 'RemoteActorRefProvider' enabled in the configuration, current uses {Context.System.AsInstanceOf().Provider.GetType().FullName}"); @@ -384,11 +384,11 @@ TimeSpan heartbeatExpectedResponseAfter _failureDetectorReaperCancelable = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(unreachableReaperInterval, unreachableReaperInterval, Self, ReapUnreachableTick.Instance, Self); } - readonly IFailureDetectorRegistry
_failureDetector; - readonly TimeSpan _heartbeatExpectedResponseAfter; - readonly IScheduler _scheduler = Context.System.Scheduler; - readonly RemoteActorRefProvider _remoteProvider; - readonly HeartbeatRsp _selfHeartbeatRspMsg = new HeartbeatRsp(AddressUidExtension.Uid(Context.System)); + private readonly IFailureDetectorRegistry
_failureDetector; + private readonly TimeSpan _heartbeatExpectedResponseAfter; + private readonly IScheduler _scheduler = Context.System.Scheduler; + private readonly IRemoteActorRefProvider _remoteProvider; + private readonly HeartbeatRsp _selfHeartbeatRspMsg = new HeartbeatRsp(AddressUidExtension.Uid(Context.System)); /// /// Actors that this node is watching, map of watchee --> Set(watchers) @@ -409,10 +409,10 @@ TimeSpan heartbeatExpectedResponseAfter /// protected HashSet
Unreachable { get; } = new HashSet
(); - readonly Dictionary _addressUids = new Dictionary(); + private readonly Dictionary _addressUids = new Dictionary(); - readonly ICancelable _heartbeatCancelable; - readonly ICancelable _failureDetectorReaperCancelable; + private readonly ICancelable _heartbeatCancelable; + private readonly ICancelable _failureDetectorReaperCancelable; /// /// TBD diff --git a/src/core/Akka.Remote/Remoting.cs b/src/core/Akka.Remote/Remoting.cs index 9bfb05a0add..d18c607670c 100644 --- a/src/core/Akka.Remote/Remoting.cs +++ b/src/core/Akka.Remote/Remoting.cs @@ -45,14 +45,14 @@ public static string Encode(Address address) internal sealed class RARP : ExtensionIdProvider, IExtension { //this is why this extension is called "RARP" - private readonly RemoteActorRefProvider _provider; + private readonly IRemoteActorRefProvider _provider; /// /// Used as part of the /// public RARP() { } - private RARP(RemoteActorRefProvider provider) + private RARP(IRemoteActorRefProvider provider) { _provider = provider; } @@ -74,13 +74,13 @@ public Props ConfigureDispatcher(Props props) /// TBD public override RARP CreateExtension(ExtendedActorSystem system) { - return new RARP(system.Provider.AsInstanceOf()); + return new RARP((IRemoteActorRefProvider)system.Provider); } /// - /// TBD + /// The underlying remote actor reference provider. /// - public RemoteActorRefProvider Provider + public IRemoteActorRefProvider Provider { get { return _provider; } } diff --git a/src/core/Akka.Remote/Serialization/ActorPathCache.cs b/src/core/Akka.Remote/Serialization/ActorPathCache.cs index 8a80ef78ebf..7ad42e8d7e7 100644 --- a/src/core/Akka.Remote/Serialization/ActorPathCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorPathCache.cs @@ -47,8 +47,7 @@ protected override int Hash(string k) protected override ActorPath Compute(string k) { - ActorPath actorPath; - if (ActorPath.TryParse(k, out actorPath)) + if (ActorPath.TryParse(k, out var actorPath)) return actorPath; return null; } diff --git a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs index 030f0e98c27..d84704d0300 100644 --- a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs @@ -16,11 +16,11 @@ namespace Akka.Remote.Serialization /// internal sealed class ActorRefResolveThreadLocalCache : ExtensionIdProvider, IExtension { - private readonly RemoteActorRefProvider _provider; + private readonly IRemoteActorRefProvider _provider; public ActorRefResolveThreadLocalCache() { } - public ActorRefResolveThreadLocalCache(RemoteActorRefProvider provider) + public ActorRefResolveThreadLocalCache(IRemoteActorRefProvider provider) { _provider = provider; _current = new ThreadLocal(() => new ActorRefResolveCache(_provider)); @@ -28,7 +28,7 @@ public ActorRefResolveThreadLocalCache(RemoteActorRefProvider provider) public override ActorRefResolveThreadLocalCache CreateExtension(ExtendedActorSystem system) { - return new ActorRefResolveThreadLocalCache(system.Provider.AsInstanceOf()); + return new ActorRefResolveThreadLocalCache((IRemoteActorRefProvider)system.Provider); } private readonly ThreadLocal _current; @@ -46,9 +46,9 @@ public static ActorRefResolveThreadLocalCache For(ActorSystem system) ///
internal sealed class ActorRefResolveCache : LruBoundedCache { - private readonly RemoteActorRefProvider _provider; + private readonly IRemoteActorRefProvider _provider; - public ActorRefResolveCache(RemoteActorRefProvider provider, int capacity = 1024, int evictAgeThreshold = 600) : base(capacity, evictAgeThreshold) + public ActorRefResolveCache(IRemoteActorRefProvider provider, int capacity = 1024, int evictAgeThreshold = 600) : base(capacity, evictAgeThreshold) { _provider = provider; } diff --git a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs index 14cf1b11a9f..6d73a0134e2 100644 --- a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs +++ b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs @@ -277,7 +277,7 @@ public virtual ByteString EncodePdu(IAkkaPdu pdu) /// TBD /// TBD /// TBD - public abstract AckAndMessage DecodeMessage(ByteString raw, RemoteActorRefProvider provider, Address localAddress); + public abstract AckAndMessage DecodeMessage(ByteString raw, IRemoteActorRefProvider provider, Address localAddress); /// /// TBD @@ -409,7 +409,7 @@ public override ByteString ConstructHeartbeat() /// TBD /// TBD /// TBD - public override AckAndMessage DecodeMessage(ByteString raw, RemoteActorRefProvider provider, Address localAddress) + public override AckAndMessage DecodeMessage(ByteString raw, IRemoteActorRefProvider provider, Address localAddress) { var ackAndEnvelope = AckAndEnvelopeContainer.Parser.ParseFrom(raw); diff --git a/src/core/Akka.Remote/Transport/AkkaProtocolTransport.cs b/src/core/Akka.Remote/Transport/AkkaProtocolTransport.cs index 5640c6c3155..54a6d59aee9 100644 --- a/src/core/Akka.Remote/Transport/AkkaProtocolTransport.cs +++ b/src/core/Akka.Remote/Transport/AkkaProtocolTransport.cs @@ -860,7 +860,7 @@ private void InitializeFSM() { //Otherwise, retry SetTimer("associate-retry", new HandleMsg(wrappedHandle), - ((RemoteActorRefProvider)((ActorSystemImpl)Context.System).Provider) //TODO: rewrite using RARP ActorSystem Extension + RARP.For(Context.System).Provider .RemoteSettings.BackoffPeriod, repeat: false); nextState = Stay(); } From b206aa79bedab46d8512b9cdd9873645d0818668 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 1 Feb 2018 10:58:26 -0800 Subject: [PATCH 16/70] Akka.NET v1.3.4 release notes (#3307) * added Akka.NET v1.3.4 release notes --- RELEASE_NOTES.md | 24 ++++++++++++++++++++++-- src/common.props | 18 +++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ecceefd2452..04b361e5102 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,25 @@ -#### 1.3.4 January 28 2018 #### -Placeholder +#### 1.3.4 February 1 2018 #### +**Maintenance Release for Akka.NET 1.3** + +Akka.NET v1.3.4 is a minor patch mostly focused on bugfixes. + +**Updates and Bugfixes** +1. [Akka: Ask interface should be clean](https://github.com/akkadotnet/akka.net/pull/3220) +1. [Akka.Cluster.Sharding: DData replicator is always assigned](https://github.com/akkadotnet/akka.net/issues/3297) +2. [Akka.Cluster: Akka.Cluster Group Routers don't respect role setting when running with allow-local-routees](https://github.com/akkadotnet/akka.net/issues/3294) +3. [Akka.Streams: Implement PartitionHub](https://github.com/akkadotnet/akka.net/pull/3287) +4. [Akka.Remote AkkaPduCodec performance fixes](https://github.com/akkadotnet/akka.net/pull/3299) +5. [Akka.Serialization.Hyperion updated](https://github.com/akkadotnet/akka.net/pull/3306) to [Hyperion v0.9.8](https://github.com/akkadotnet/Hyperion/releases/tag/v0.9.8) + +You can see [the full set of changes for Akka.NET v1.3.4 here](https://github.com/akkadotnet/akka.net/milestone/22). + +| COMMITS | LOC+ | LOC- | AUTHOR | +| --- | --- | --- | --- | +| 6 | 304 | 209 | Aaron Stannard | +| 1 | 250 | 220 | Maxim Cherednik | +| 1 | 1278 | 42 | Marc Piechura | +| 1 | 1 | 1 | zbynek001 | +| 1 | 1 | 1 | Vasily Kirichenko | #### 1.3.3 January 19 2018 #### **Maintenance Release for Akka.NET 1.3** diff --git a/src/common.props b/src/common.props index ba33aee35bf..f86aa05c5b6 100644 --- a/src/common.props +++ b/src/common.props @@ -17,6 +17,22 @@ true - Placeholder + Maintenance Release for Akka.NET 1.3** +Akka.NET v1.3.4 is a minor patch mostly focused on bugfixes. +Updates and Bugfixes** +1. [Akka: Ask interface should be clean](https://github.com/akkadotnet/akka.net/pull/3220) +1. [Akka.Cluster.Sharding: DData replicator is always assigned](https://github.com/akkadotnet/akka.net/issues/3297) +2. [Akka.Cluster: Akka.Cluster Group Routers don't respect role setting when running with allow-local-routees](https://github.com/akkadotnet/akka.net/issues/3294) +3. [Akka.Streams: Implement PartitionHub](https://github.com/akkadotnet/akka.net/pull/3287) +4. [Akka.Remote AkkaPduCodec performance fixes](https://github.com/akkadotnet/akka.net/pull/3299) +5. [Akka.Serialization.Hyperion updated](https://github.com/akkadotnet/akka.net/pull/3306) to [Hyperion v0.9.8](https://github.com/akkadotnet/Hyperion/releases/tag/v0.9.8) +You can see [the full set of changes for Akka.NET v1.3.4 here](https://github.com/akkadotnet/akka.net/milestone/22). +| COMMITS | LOC+ | LOC- | AUTHOR | +| --- | --- | --- | --- | +| 6 | 304 | 209 | Aaron Stannard | +| 1 | 250 | 220 | Maxim Cherednik | +| 1 | 1278 | 42 | Marc Piechura | +| 1 | 1 | 1 | zbynek001 | +| 1 | 1 | 1 | Vasily Kirichenko | \ No newline at end of file From 3bf32dfca55741df69a141a76f6f083af5a928cf Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 1 Feb 2018 11:39:34 -0800 Subject: [PATCH 17/70] added placeholder for nightlies (#3311) --- RELEASE_NOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 04b361e5102..06460ecbec4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,6 @@ +#### 1.3.5 February 1 2018 #### +Placeholder for nightlies + #### 1.3.4 February 1 2018 #### **Maintenance Release for Akka.NET 1.3** From 3661b5214e8f5db8ab35a773bb3a46e4014cb779 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 1 Feb 2018 15:47:12 -0800 Subject: [PATCH 18/70] fixed issues with supervision docs (#3308) * fixed issues with supervision docs * one more bugfix in the example code --- docs/articles/actors/fault-tolerance.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/articles/actors/fault-tolerance.md b/docs/articles/actors/fault-tolerance.md index 052ba83be42..4bb49ce5106 100644 --- a/docs/articles/actors/fault-tolerance.md +++ b/docs/articles/actors/fault-tolerance.md @@ -58,15 +58,12 @@ currently failed child (available as the `Sender` of the failure message). ### Default Supervisor Strategy -`Escalate` is used if the defined strategy doesn't cover the exception that was thrown. - When the supervisor strategy is not defined for an actor the following exceptions are handled by default: -* `ActorInitializationException` will stop the failing child actor -* `ActorKilledException` will stop the failing child actor -* `Exception` will restart the failing child actor -* Other types of `Exception` will be escalated to parent actor +* `ActorInitializationException` will stop the failing child actor; +* `ActorKilledException` will stop the failing child actor; and +* Any other type of `Exception` will restart the failing child actor. If the exception escalate all the way up to the root guardian it will handle it in the same way as the default strategy defined above. @@ -155,7 +152,8 @@ public class Supervisor : UntypedActor { if (message is Props p) { - Sender.Tell(p); + var child = Context.ActorOf(p); // create child + Sender.Tell(child); // send back reference to child actor } } } @@ -194,7 +192,7 @@ Let us create actors: var supervisor = system.ActorOf("supervisor"); supervisor.Tell(Props.Create()); -var child = ExpectMsg(); // retrieve answer from TestKit’s testActor +var child = ExpectMsg(); // retrieve answer from TestKit’s TestActor ``` The first test shall demonstrate the `Resume` directive, so we try it out by @@ -210,7 +208,7 @@ child.Tell("get"); ExpectMsg(42); ``` -As you can see the value 42 survives the fault handling directive. Now, if we +As you can see the value 42 survives the fault handling directive because we're using the `Resume` directive, which does not cause the actor to restart. Now, if we change the failure to a more serious `NullReferenceException`, that will no longer be the case: @@ -220,6 +218,8 @@ child.Tell("get"); ExpectMsg(0); ``` +This is because the actor has restarted and the original `Child` actor instance that was processing messages will be destroyed and replaced by a brand-new instance defined using the original `Props` passed to its parent. + And finally in case of the fatal `IllegalArgumentException` the child will be terminated by the supervisor: @@ -288,7 +288,8 @@ public class Supervisor2 : UntypedActor { if (message is Props p) { - Sender.Tell(p); + var child = Context.ActorOf(p); // create child + Sender.Tell(child); // send back reference to child actor } } } From 4af182952674a091c677fce685b866b0fd27e264 Mon Sep 17 00:00:00 2001 From: Joshua Garnett Date: Thu, 15 Feb 2018 03:52:11 -0800 Subject: [PATCH 19/70] Fixing premature pruning of Topics (#3322) * Fixing premature pruning of Topics The DistributedPubSubMediator wasn't checking if the TopicActor was actually terminated before pruning it from the bucket. This can cause problems if a TopicActor is re-suscribed to before being stopped. The Subscribe message only checks Context.Child, but does not check if the bucket is still valid. So it was possible to get in a state where subscribes/unsubscribes were succeeding, but any publishes to the topic where being dropped on the floor. I've also switched from null to ActorRefs.Nobody. Previously, if a Topic actor had terminated and a publish for that topic was received before the DistributedPubSubMediator did a prune, the publish would throw an exception. * Switching to IsNobody() extension method --- .../PublishSubscribe/DistributedPubSubMediator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedPubSubMediator.cs b/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedPubSubMediator.cs index 949d73f0d3b..ea3d7a499c9 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedPubSubMediator.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedPubSubMediator.cs @@ -220,7 +220,7 @@ public DistributedPubSubMediator(DistributedPubSubSettings settings) { if (_registry.TryGetValue(_cluster.SelfAddress, out var bucket)) { - if (bucket.Content.TryGetValue(remove.Path, out var valueHolder) && valueHolder.Ref != null) + if (bucket.Content.TryGetValue(remove.Path, out var valueHolder) && !valueHolder.Ref.IsNobody()) { Context.Unwatch(valueHolder.Ref); PutToRegistry(remove.Path, null); @@ -371,7 +371,7 @@ public DistributedPubSubMediator(DistributedPubSubSettings settings) Receive(_ => { /* ignore */ }); Receive(_ => { - var count = _registry.Sum(entry => entry.Value.Content.Count(kv => kv.Value.Ref != null)); + var count = _registry.Sum(entry => entry.Value.Content.Count(kv => !kv.Value.Ref.IsNobody())); Sender.Tell(count); }); Receive(_ => @@ -488,7 +488,7 @@ IEnumerable Refs() if (!(allButSelf && address == _cluster.SelfAddress) && bucket.Content.TryGetValue(path, out var valueHolder)) { - if (valueHolder != null && !Equals(valueHolder.Ref, ActorRefs.Nobody)) + if (valueHolder != null && !valueHolder.Ref.IsNobody()) yield return valueHolder.Ref; } } @@ -549,7 +549,7 @@ private void HandlePrune() var bucket = entry.Value; var oldRemoved = bucket.Content - .Where(kv => (bucket.Version - kv.Value.Version) > _settings.RemovedTimeToLive.TotalMilliseconds) + .Where(kv => kv.Value.Ref.IsNobody() && (bucket.Version - kv.Value.Version) > _settings.RemovedTimeToLive.TotalMilliseconds) .Select(kv => kv.Key); if (oldRemoved.Any()) From 184e7c7593e65cd809e8219507724cc53ddac4fb Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Sat, 24 Feb 2018 23:16:40 +0100 Subject: [PATCH 20/70] StreamRefs (#3321) * initial commit for StreamRefs * more work over Stream-Ref serializer * fixes in serializer and config * fixes in stream-refs and tests * StreamRefs docs * added defaults for StreamRefsSettings * StreamRefs approvals API * fixed missing impl + added animation gifs to docs * applied docs, fixed namespaces * fixed stream ref serializer namespaces --- build.fsx | 3 +- docs/articles/streams/streamrefs.md | 88 + .../Streams/StreamRefsDocTests.cs | 140 ++ docs/images/sink-ref-animation.gif | Bin 0 -> 96908 bytes docs/images/source-ref-animation.gif | Bin 0 -> 96884 bytes src/Akka.sln | 1 + .../CoreAPISpec.ApproveStreams.approved.txt | 77 +- src/core/Akka.Streams.TestKit/TestSink.cs | 7 +- .../Akka.Streams.Tests.csproj | 12 +- .../Akka.Streams.Tests/Dsl/StreamRefsSpec.cs | 405 +++++ src/core/Akka.Streams/ActorMaterializer.cs | 34 +- src/core/Akka.Streams/Akka.Streams.csproj | 4 + src/core/Akka.Streams/Attributes.cs | 28 + src/core/Akka.Streams/Dsl/StreamRefs.cs | 731 +++++++++ .../Akka.Streams/Implementation/Buffers.cs | 2 + .../Proto/StreamRefMessages.g.cs | 1413 +++++++++++++++++ .../Serialization/StreamRefSerializer.cs | 235 +++ src/core/Akka.Streams/StreamRefs.cs | 112 ++ src/core/Akka.Streams/reference.conf | 43 +- src/protobuf/StreamRefMessages.proto | 58 + 20 files changed, 3364 insertions(+), 29 deletions(-) create mode 100644 docs/articles/streams/streamrefs.md create mode 100644 docs/examples/DocsExamples/Streams/StreamRefsDocTests.cs create mode 100644 docs/images/sink-ref-animation.gif create mode 100644 docs/images/source-ref-animation.gif create mode 100644 src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs create mode 100644 src/core/Akka.Streams/Dsl/StreamRefs.cs create mode 100644 src/core/Akka.Streams/Serialization/Proto/StreamRefMessages.g.cs create mode 100644 src/core/Akka.Streams/Serialization/StreamRefSerializer.cs create mode 100644 src/core/Akka.Streams/StreamRefs.cs create mode 100644 src/protobuf/StreamRefMessages.proto diff --git a/build.fsx b/build.fsx index 7b0e76866c1..54896c517bb 100644 --- a/build.fsx +++ b/build.fsx @@ -444,7 +444,8 @@ Target "Protobuf" <| fun _ -> ("DistributedPubSubMessages.proto", "/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/Serialization/Proto/"); ("ClusterShardingMessages.proto", "/src/contrib/cluster/Akka.Cluster.Sharding/Serialization/Proto/"); ("TestConductorProtocol.proto", "/src/core/Akka.Remote.TestKit/Proto/"); - ("Persistence.proto", "/src/core/Akka.Persistence/Serialization/Proto/") ] + ("Persistence.proto", "/src/core/Akka.Persistence/Serialization/Proto/"); + ("StreamRefMessages.proto", "/src/core/Akka.Streams/Serialization/Proto/")] printfn "Using proto.exe: %s" protocPath diff --git a/docs/articles/streams/streamrefs.md b/docs/articles/streams/streamrefs.md new file mode 100644 index 00000000000..c63a06bc44a --- /dev/null +++ b/docs/articles/streams/streamrefs.md @@ -0,0 +1,88 @@ +--- +uid: stream-ref +title: StreamRefs - Reactive Streams over the network +--- + +> **Warning** +This module is currently marked as may change in the sense of being the subject of active research. This means that API or semantics can change without warning or deprecation period and it is not recommended to use this module in production just yet—you have been warned. + +Stream references, or “stream refs” for short, allow running Akka Streams across multiple nodes within an Akka Remote boundaries. + +Unlike heavier “streaming data processing” frameworks, Akka Streams are not “deployed” nor automatically distributed. Akka stream refs are, as the name implies, references to existing parts of a stream, and can be used to create a distributed processing framework or introduce such capabilities in specific parts of your application. + +Stream refs are trivial to make use of in existing clustered Akka applications, and require no additional configuration or setup. They automatically maintain flow-control / back-pressure over the network, and employ Akka’s failure detection mechanisms to fail-fast (“let it crash!”) in the case of failures of remote nodes. They can be seen as an implementation of the Work Pulling Pattern, which one would otherwise implement manually. + +> **Note** +A useful way to think about stream refs is: “like an `IActorRef`, but for Akka Streams’s `Source` and `Sink`”. +Stream refs refer to an already existing, possibly remote, `Sink` or `Source`. This is not to be mistaken with deploying streams remotely, which this feature is not intended for. + +## Stream References + +The prime use case for stream refs is to replace raw actor or HTTP messaging between systems where a long running stream of data is expected between two entities. Often times, they can be used to effectively achieve point to point streaming without the need of setting up additional message brokers or similar secondary clusters. + +Stream refs are well suited for any system in which you need to send messages between nodes and need to do so in a flow-controlled fashion. Typical examples include sending work requests to worker nodes, as fast as possible, but not faster than the worker node can process them, or sending data elements which the downstream may be slow at processing. It is recommended to mix and introduce stream refs in Actor messaging based systems, where the actor messaging is used to orchestrate and prepare such message flows, and later the stream refs are used to do the flow-controlled message transfer. + +Stream refs are not persistent, however it is simple to build a recover-able stream by introducing such protocol on the actor messaging layer. Stream refs are absolutely expected to be sent over Akka remoting to other nodes within a cluster, and as such, complement and do not compete with plain Actor messaging. Actors would usually be used to establish the stream, by means of some initial message saying “I want to offer you many log elements (the stream ref)”, or alternatively in the opposite way “If you need to send me much data, here is the stream ref you can use to do so”. + +Since the two sides (“local” and “remote”) of each reference may be confusing to simply refer to as “remote” and “local” – since either side can be seen as “local” or “remote” depending how we look at it – we propose to use the terminology “origin” and “target”, which is defined by where the stream ref was created. For SourceRefs, the “origin” is the side which has the data that it is going to stream out. For SinkRefs the “origin” side is the actor system that is ready to receive the data and has allocated the ref. Those two may be seen as duals of each other, however to explain patterns about sharing references, we found this wording to be rather useful. + +### Source Refs - offering streaming data to a remote system + +A `SourceRef` can be offered to a remote actor system in order for it to consume some source of data that we have prepared locally. + +In order to share a `Source` with a remote endpoint you need to materialize it by running it into the `StreamRefs.SourceRef`. That sink materializes the `ISourceRef` that you can then send to other nodes. Please note that it materializes into a Task so you will have to use the continuation (either `PipeTo` or async/await pattern). + +[!code-csharp[StreamRefsDocTests.cs](../../examples/DocsExamples/Streams/StreamRefsDocTests.cs?name=data-source-actor)] + +The origin actor which creates and owns the `Source` could also perform some validation or additional setup when preparing the source. Once it has handed out the `ISourceRef` the remote side can run it like this: + +[!code-csharp[StreamRefsDocTests.cs](../../examples/DocsExamples/Streams/StreamRefsDocTests.cs?name=source-ref-materialization)] + +The process of preparing and running a `ISourceRef` powered distributed stream is shown by the animation below: + +![source ref](/images/source-ref-animation.gif) + +> **Warning** +A `ISourceRef` is by design “single-shot”. i.e. it may only be materialized once. This is in order to not complicate the mental model what materializing such value would mean. +While stream refs are designed to be single shot, you may use them to mimic multicast scenarios, simply by starting a `Broadcast` stage once, and attaching multiple new streams to it, for each emitting a new stream ref. This way each output of the broadcast is by itself an unique single-shot reference, however they can all be powered using a single `Source` – located before the `Broadcast` stage. + +### Sink Refs - offering to receive streaming data from a remote system + +They can be used to offer the other side the capability to send to the origin side data in a streaming, flow-controlled fashion. The origin here allocates a Sink, which could be as simple as a `Sink.ForEach` or as advanced as a complex sink which streams the incoming data into various other systems (e.g. any of the Alpakka provided Sinks). + +> **Note** +To form a good mental model of `SinkRef`s, you can think of them as being similar to “passive mode” in FTP. + +[!code-csharp[StreamRefsDocTests.cs](../../examples/DocsExamples/Streams/StreamRefsDocTests.cs?name=data-sink-actor)] + +Using the offered `ISinkRef<>` to send data to the origin of the `Sink` is also simple, as we can treat the `ISinkRef<>` just as any other sink and directly runWith or run with it. + +[!code-csharp[StreamRefsDocTests.cs](../../examples/DocsExamples/Streams/StreamRefsDocTests.cs?name=sink-ref-materialization)] + +The process of preparing and running a `ISinkRef<>` powered distributed stream is shown by the animation below: + +![sink ref](/images/sink-ref-animation.gif) + +> **Warning** +A `ISinkRef<>` is *by design* “single-shot”. i.e. it may only be materialized once. This is in order to not complicate the mental model what materializing such value would mean. If you have an use case for building a fan-in operation accepting writes from multiple remote nodes, you can build your `Sink` and prepend it with a `Merge` stage, each time materializing a new `ISinkRef<>` targeting that `Merge`. This has the added benefit of giving you full control how to merge these streams (i.e. by using “merge preferred” or any other variation of the fan-in stages). + +## Configuration + +### Stream reference subscription timeouts + +All stream references have a subscription timeout, which is intended to prevent resource leaks in situations in which a remote node would requests the allocation of many streams yet never actually run them. In order to prevent this, each stream reference has a default timeout (of 30 seconds), after which the origin will abort the stream offer if the target has not materialized the stream ref in time. After the timeout has triggered, materialization of the target side will fail pointing out that the origin is missing. + +Since these timeouts are often very different based on the kind of stream offered, and there can be many different kinds of them in the same application, it is possible to not only configure this setting globally (`akka.stream.materializer.stream-ref.subscription-timeout`), but also via attributes: + +```csharp +Source.Repeat("hello") + .RunWith(StreamRefs.SourceRef() + .AddAttributes(StreamRefAttributes + .SubscriptionTimeout(TimeSpan.FromSeconds(5))) + , materializer); + +StreamRefs.SinkRef() + .AddAttributes(StreamRefAttributes + .SubscriptionTimeout(TimeSpan.FromSeconds(5))) + .RunWith(Sink.Ignore(), materializer); +``` \ No newline at end of file diff --git a/docs/examples/DocsExamples/Streams/StreamRefsDocTests.cs b/docs/examples/DocsExamples/Streams/StreamRefsDocTests.cs new file mode 100644 index 00000000000..380ec2f8754 --- /dev/null +++ b/docs/examples/DocsExamples/Streams/StreamRefsDocTests.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Akka; +using Akka.Streams; +using Akka.Streams.Dsl; +using Akka.TestKit.Xunit2; +using Xunit; +using Xunit.Abstractions; +using Akka.Actor; +using Akka.IO; +using Akka.Util; +using System.Linq; + +namespace DocsExamples.Streams +{ + public class StreamRefsDocTests : TestKit + { + #region data-source-actor + public sealed class RequestLogs + { + public int StreamId { get; } + + public RequestLogs(int streamId) + { + StreamId = streamId; + } + } + + public sealed class LogsOffer + { + public int StreamId { get; } + public ISourceRef SourceRef { get; } + + public LogsOffer(int streamId, ISourceRef sourceRef) + { + StreamId = streamId; + SourceRef = sourceRef; + } + } + + public class DataSource : ReceiveActor + { + public DataSource() + { + Receive(request => + { + // create a source + StreamLogs(request.StreamId) + // materialize it using stream refs + .RunWith(StreamRefs.SourceRef(), Context.System.Materializer()) + // and send to sender + .PipeTo(Sender, success: sourceRef => new LogsOffer(request.StreamId, sourceRef)); + }); + } + + private Source StreamLogs(int streamId) => + Source.From(Enumerable.Range(1, 100)).Select(i => i.ToString()); + } + #endregion + + #region data-sink-actor + public sealed class PrepareUpload + { + public string Id { get; } + public PrepareUpload(string id) + { + Id = id; + } + } + + public sealed class MeasurementsSinkReady + { + public string Id { get; } + public ISinkRef SinkRef { get; } + public MeasurementsSinkReady(string id, ISinkRef sinkRef) + { + Id = id; + SinkRef = sinkRef; + } + } + + class DataReceiver : ReceiveActor + { + public DataReceiver() + { + Receive(prepare => + { + // obtain a source you want to offer + var sink = LogsSinksFor(prepare.Id); + + // materialize sink ref (remote is source data for us) + StreamRefs.SinkRef() + .To(sink) + .Run(Context.System.Materializer()) + .PipeTo(Sender, success: sinkRef => new MeasurementsSinkReady(prepare.Id, sinkRef)); + }); + } + + private Sink LogsSinksFor(string id) => + Sink.ForEach(Console.WriteLine); + } + #endregion + + private ActorMaterializer Materializer { get; } + + public StreamRefsDocTests(ITestOutputHelper output) + : base("", output) + { + Materializer = Sys.Materializer(); + } + + [Fact] + public async Task SourceRef_must_propagate_source_from_another_system() + { + #region source-ref-materialization + var sourceActor = Sys.ActorOf(Props.Create(), "dataSource"); + + var offer = await sourceActor.Ask(new RequestLogs(1337)); + await offer.SourceRef.Source.RunForeach(Console.WriteLine, Materializer); + #endregion + } + + [Fact] + public async Task SinkRef_must_receive_messages_from_another_system() + { + #region sink-ref-materialization + var receiver = Sys.ActorOf(Props.Create(), "receiver"); + + var ready = await receiver.Ask(new PrepareUpload("id"), timeout: TimeSpan.FromSeconds(30)); + + // stream local metrics to Sink's origin: + Source.From(Enumerable.Range(1, 100)) + .Select(i => i.ToString()) + .RunWith(ready.SinkRef.Sink, Materializer); + #endregion + } + } +} \ No newline at end of file diff --git a/docs/images/sink-ref-animation.gif b/docs/images/sink-ref-animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..52e26d2103c04b9350176956f35bed0aede9aa41 GIT binary patch literal 96908 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/images/source-ref-animation.gif b/docs/images/source-ref-animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..76fd2c936dc075e821534e11ccf666d3be529b06 GIT binary patch literal 96884 zcmeFYXH=72xA&V)LJ|^skrH}n5@`Y=gepieN)Zt?(wm5gh=>|`*9a(GLKQ*1BOs!J zB=k<`MbyxXEqaT9Vsf~j{f@KuK4))^8rMvoSO} z?+)4pYyl4d_8uMp<^$&Qr=cz*&`7jE1VT`oBxvX-=!F#XNRkkfkctw;ow%mxhf}uj zS5Z>Yx~aC`z+(TUBJI>SIyySW7Yq&=A3Ah0^3eGchrCiuor5jSEiF@tR@W`8bIt8M zLhR07wevk{Z)fiin(Yu?=#WNt3P^T!adnMxam#RadvM}Rz?HL}XU~%Ty&_V*uHW}g zdgz_y<>%`caxeHo@P+Wg(1`l5h?fgjE?>D)bS1Jd^jb>cwL7)fZ$w`&iM|C z%ej|3kds&OydbZjpnmYdU{T475?V=VS$k>4ljjwSysEe54`1|DKYUbOU0dDp?&;&F z&;EK=*InOG*U-SOZ+h{viP`&daQ^kn*Uhh*TVA)kdHv>1bL*SA*Nhj>7;hMiwob;# zB4ezF@u``a>cC_$nLRCSZ5{0$9UW~Q9i1H=JzX8WgB{~59W(8nX?ES6T|FFbZ;Dx8 zPk(>1asPC0|L48|>d}GOkAoFg2L}cQN2iAVN*x*+9aVBuHeMGx9{G~vEOmH?`Nmque|**Ir3rd-Ta67g&dQG zXV(@!FD@=DEiElCahI20rYx^i?DUkr`(4Ug~*2Wb8yrhjw=fb#%d=p zT|zf#uIE{K_Z^&NnzUVAMQ=9VxyG}%uChOuaG;>juD)up(D3rcTyOou;Sypzzs%8w z>XCBWZ1ppJ4UfhilFE-3AAMdkQR`kG^seuD?bKi7c9l%K#>X@D0b@01`Wv6TeG#_U zS8V^{>ATlae{a0&fAQD3IxjF%)}e_$-<~3@@n+}6v&C+zqP@-fUvAx?*zf zbyD*xhgRr1w(E)hIoLn?D3}-1{Cevv`<6Ga;lio&zOk!(RUb47g}*}}AN=iyIXZ29 zUsOL{%|P$_6Vq~hyfEfZr5pM>AJYbVjraM*2cy*`@7wy*KfE1{mP77mn=R*M-?{a9 z@ak!!pH_-;F+019n9I}dV;G|+2O3T#O{7RHgzp$tm!DmWFdQ47wDWZOvs58^ISE%00}{5l&g7)$THW zaNq2un6;vp9>E37^#^*RQETT*cpW=;XvWLl)mcJAId`t#e8pEvOm_`Z^Ac*@x>L+^ z0W0TJeby^5;*u9n-FhhSDF4EmUcOSoY7_6|Sgrq?ojdo{+`v9r+W|8^=D9F?5i3EJ z)QtVSrthfidw-6S>S>y7^2T*FCx^^NyoAfjS2F%Srb=vch@$577>=gvji*Vd z;tC$;mE4zo8u-;?LS4~8za8&Pd-S%_A#O*O}4>k<53gg%+#42 z%ONux4rNGB%HF%opS9cGj_fw!bej>9+y{q8!ty@BDBIq5uJ>y#4kOQzb4{=P(p#OvS?W8t-8sOPN#w`)1Kq?;e;k(Ffk-L3z z7yFJF(&tezbL!H~uSN?w=QATkKRO8g>c(3UGj8SKr3IU_biY4C>dBq$5hDwIhKj|*GeR|8ItEnO0UAJZHD&C8 zUTLbY`J#Vc4@}1*_D+Zh){?gd73Y0J`QjZz9-K&O(-TY!wqBHrsJ1YAOl=v_>UMj| zO3*Nv(180c-tHZ>(*Ay_RlM_&I88z+X>_UMs7D=M(4&;!bB%I52P_rtleoXA^!6>f zg3v3@5mS+e8U7~mLOGn%`c_?O8SvQDm6v&^cPW^N8~qVrfU!-~5K7b8Oo^+i%5_Nx(-ZPhJSs^e6| zjzfLfi;{u3Skv=t9{iAb8J`#29QFNpqPk1G)J&5F{3s2mQXR|J%Q<5HTaaJ2S{_C) z1foLa(YFjO#Cr%GRVL%*hMPEu5(6Nd^$p_%F&CC;fnxMXapEaJ*mk^)P9AYgn7}+N z*;Y(CT}FTT%mUHM;ghBk6XAHKWINS@uPQoz^I040ES<@xK#O-2F^9@+6P2=JnW_!K*K4<0KOI)aqw0PMSDkvv%8f?oeq&W|H4DOiwMBnnv&5Y0q* zUP2O!$l#N5K?jR=M5E+vWD>l_e)@m;dbs_a-uPt4Ai8@waMURa=9ZVty%4o zzI2A9>0^*2X;Dr+$|7yIaNcSc2hy+=64eCoxsfbjXfbE~_x3)8<6$aGv@WBn`95Kn zdC7IUGb#!zU~ED}uab@!L@^R>&Ve9-R2yRlXgpTBT#B>?Gw0=b4L6EW9ZoH8CO}~w>kUsdxn0XGxnUn@Tx!{F^os`D-#Dj#raJz!k$vm zw)JP>^^(qf@-1vQ4gd~}IKyK=OxP&bQKDip#sL8C2A~HBa>HS$DZ15W-6_u~Av>WP zvUIg>DpZGdeu#>ii^t4~BUS-7^;qUh2d_pFuC~)}_K~i1=77pMw;Ug0^mR~^SXG;Z zeOQQN&l1M83N-u^17Ewz=7|W@#9y7p!W~bdpDsz>bQO>vO;kHBLdZ2x4TJiYDG;#H zx57X#SQvxsi@Y!tjssl*n2vHlI0mYD0=!0uE}&w(SI)d5p>4tVisQP=RFnxDCYOb# zP=GH8(diU{ivZ9ID#oCm9}@=M=J3g;pf{-)8V6KL#cbg;B;9YPp0}N$OOHGRiDaSf z;(|5^j*oEQ4>_O|D!PwD(PPQktAgs?f;UA4826z(DqM}h&!PsM#!}mfU|)`DY`tXv zam#5MLP}nqLttM}-%X z`2zJ(g*Z^kUieHybZC<8fLqTRpwJa>9U7>eL=~gL9}`XqvdN3FbxngeUT{(kXnA8=J|0%QE z3~&oAZP*pM7KTDx0kx??zD1(;cFqoAIog|;sF!d8alh_-{QX#gVJg~!M<;1>K!>W4 z+B8IA_&rtHF)u*z$IHkAtOxQmzoT9PMs0<^@&&0U&T+$FdSSUgJNa!{Kn?P#r8a&& zg3gnA=n<^)I1!;ox9=uG)v#_siRcXm{~S%}B(@I9E9~(02{hlvVN@A!PqeWroJz9JX(Va*#__y)Squ zpCohFYR^=R@(KJ1Au>+!T=Ea)b0>s{lCw(4NHq|8_cGcdTd9wVVCxEFPAU$u5FI2l zPAqNI*qa@oVAQbBvSLxXX$1EUzD zT(l+uT2u$$$Ke|#W45RUR^OE+7L>`a5<$<-dg`D*>!Oz^{u1F@j&bTEVg64nyjB=^ zYpJKAmlPDdr0!i3(DAk~Q+(1ltGG-?Pg3*~z0qToC;A=opWBtWSoHgFjg}6X2q8yS zgzV-C%@;)AEkj27L@y2!~Z0%ngq@!b-APWX~AIEu=Wcg@R?%a3X0jVm!QUiZ* zU7D^E#p0>_eBwA3Q!PSu!FgY>(a``%8GC zI~V1OiU3&28D)GG{S*HLPydxpxPEKrxh)egmo950xD!~6$%rMG{rLohfWIb@7L@RRn`Bl-X$fU zFE39vetp><+qCc)o-Jz`tw4JF{EbGvs?h7?Y=c0{ih@g$-!JWZ!IZo|FIOior>`Kl z6}#_SX6MH=o|VITkC%-uLGF zq1GS4VqdPeZWp%x3J%e=|GWVmjR&2rkW? zo^zdNcRR@rLZ?i+d_24SV!8r~x`LX!LguO$Nt32Pb0&r;7$>n+NCSp0job*>l40Ooo<1 zu=6oPYehpF%|oB&hQ90$aioX8n+&Ua4*iT7-Yy#c)jYiWsr$$7@U^*N&|wzj3=4LH z1ut#~XR}a_tw+qG)`NxD-{OV;WX0_p0Thqi*AcuOUQ&HjXDGVl*BJI)ywco0A=!5< z_0QsQ3P=`J4SYbL6@dQ2TO>BpPv#nfv5Y3|K+35+e0BAQFe-#;Or-a9T#*-b#GTSCkN|I`~30NedY-KKaF4AW8kT#FG(Ugh=W>V#KsytW4&Rjx2azO_nZxK| zqd!j^M)itmP)8kMBQ5No0;L_}nUp6=nTKbOTALGKY^9wh#^IfyqagB6tjB zCRL$<`ri1$I4rcn?5^7TQMfaq*oUU%OofCJQBG6{l>jFa=bSl+EH?Na5oyPUWD@87 zI0z>Wj7NqN$*?{$w2I8{K}6ZnP%F4a;8n|fDkPHuGote^vLSvX1c?IU0wB&fgt{p# zlM2ZKpiSA3EF2#Zha@>7`vA~T++J^p9l$b=4T%q%tUJ7t_6_5KU8!P2)UqJabVnj% zoX6mEqb^jk7Pv%|5oM|ouo^{Jbtc2?yr8v!MJ^5c{WFZrBSfj`K<|;vqR9O5v>BUR z2#XHk9!464Sw<0IJ5=!UA51U+wnKpSkzp7v<{kmML*sX5d`NRzp`VpdD?=F(pyz2& zUDA6u4#MUj^i?sYcF%~=y#-~Xk+|`G6p@IAc(5^UthF87T7tj_E@Q3RPeAvLyB!D? zMUK#QpDaF#tl?s!iLk~q7S*AX&J3gn9daH3b0bgk$S_^%1YPFi_K@g%FSrv45l=<9 z5n%115l*-XJDHjNXFu(PeQKp_5D5$SXfqxF#8HLMgYOq?O~-?Wkak|kRx&J@I(=sU zmkcfB-9MN@Q?xk|r8WxpAPYDXQ64-BteM7NOPTUQLlvG!@4Wx}?7Wu3kWvj6GBWhh zY5%$hb;Pc3G=6fJWfcj$^~CIh$0uhxMEmQWSD+s3 zk80d^4TP;E3XBE#?Z$?r3ZOk`vt%}e#D;XxK79y+7ku98yCouYd~C>A_<13wjxx2w zGJOw1*WvI7_0Y9EY*-&luiE&@hq8_5!83J?Z}L{CTEHmOGRn~#=|(`ea^6Y&{o$io z6zy=7{tfo-9X8;+6Pp+ZMXw0wVM>h>7P)`+Rfl<+rhvK+#28+ZH&5UxDc}9^RiQ0W zaxnAq_Qm<5al8P><11f3G-Ttu9s6y^R2<_E-{3-|T-Hcs)|s-_1up9+svL_=6Qx`? zTxwhu)jJAYHQbVQ5}pM4z8^i*D{&3UxoR-z=S9`fk1h+g59&+l8dBR`&fbK?&e zD(%i8wM2!d39yVm?cS{`o7eScxGB;=X|N}=^bl>j(yQezN3|nB{sz#n?aLL z&b0cyJy^=P70x^P=+o}cTHlt7$Im7f7W#%YG5_K|Rr_@?WM#0}ELrx}u<&a~JEZbh{{h3IPn%|?wMJ2}EfjujbPV(UHNHRS!`)7oKWy8Yqt5r{xOB{~s| zu_D#YS5gm1Apy*kbU)G|R{YtH~vbV|QIlJ5gP_*4{??$Q9BxzaP ztEzN4`>4`VdFC;Efpxykd(yJJhWgzlv=?qe+uUfiqTcDy*ACrdhk;gbsemk^qRIe= z;)T}e+VdUsZXIv_>Z9`pdV8x+uQagM?sw@htC%FE6c4ywFUf-% zl4JQ)-1VU!qI={#aJ|e7rz*}7NfO_D{Om2M8$WVyF{q2Zw_Jd6C>@q<8&!3awNP^K zIl)R&8d?C&JU{_}i}fEV!O{X7!l=u`2~gvJ(qxra0U-I`5u(`_B0xeKyQi*UO~n3m z^Uyl9{5XkQ4Q$2Mn4BFSge@viDf{3hDG2Jam)DGMnlZ2~OSRGflmM!c!fdOdtEwLi!!}pBs$-$v63Bs^rwY1+1eP4-rXHT z+bKyFw*06%7-7_VPGrZ{DRMNs#1s8hNk~va)+alD-do-Ejv4SaHho8ao zmyTG+vBD3KRRzffE!~uXqE#z@EZ z1Q4Odq&Ts08dyJ3sexd3-C(>Fb3?$g>pW5m>n(k=Ex<imgQDI?YApuz5NM%#ocs_r>b+pu8+4It|3^YPM@nB3^q;{?b-&yBw${tAU)IoNR0?{;cj+ z%73cKOSEqL!Ug~9rx!*InmVmR!vllU#&1{LA5IL7IK3{>?lCW1M@kHR{`$=4(fcoI z>52Oz=F>iLBSoKbo`$;4BV8METb7qYW3Hs1*_qsWQMn!L?(zB@SL(}ipU(;E4hP5} z@jM2|AW>5p6Kr?)$y`5PSB2wIV7?$ph@-!FIx8Y_FK6n8NUt@|!Qz$S)3*k!lL=`>r`4ce~ z3>#UFstqvh)BC2L0*OR*t4s4zUn5!GHW7J_$5S=7rbh0FP2W1Bo@y)?!AfT^z+xlF z(?8gdYk)*4G6jg@kOgarU}O!A|41_fPGh$ro&vxs)eA@h9Ef~FUeap@K(uY+RbcNB zUgB@3JAaeYB<09;G*g#LfYtaq>7y_T?cL)em z#+gS=^1TtFA)|}7O|Q=)56|w`Pf&7ez5Z^CcXLxH2ZFPpP$XhX`IYi^MP(_##Seu7 zu0#rKlo7uc%>95eNZLf;F2W(#V{fEypp9R7?Z$aYpoJt5KCN3#&6JO zdn@{}nXN=Ze0(cb#C)8pr@;VQ5}6_{Oi{fA^X+)EYLE&!?!ex)iUi~cJ5H7iYWNMd zwT+i>h}UL=EZyVPZNXM+@nRWF?KN|e45qpS#7+VtR$);$A8%9JqB(Bi7muooju(?o zAc(XdkZu9U#uJ3jcaO@Q8!fL-hJAy!6g zaAb~6z&JR{aKXvr}usis!j)d zFd#m^qPL)-x1gw}sHiu!x%a`3-V#V(zE6B<2>2i|zI8O?=jGl?6jMkqJ}n~tPDoEy zMPE%rUv0RhDT4aFf$rGbdi3Sjk>_R#4jw+C#LMt^Zktx{Vx*wn{xVJR`kF4 zB;c`qw9==q#li3qTS%9Zql6t`Dh;&h4YXSgbhr$3Iym5o;8V7J&vFKO2Nb)AcP?!w z9t(gx&9N&S=pBX(vP1?)lmy9!`vUk zJO~RQ$^t60K>94OH4Ea(g8H&x%gR3!Snxy=UnS{d4hz}ILQSyH%dFB`7G}9$Ky*a# z-65?0@m!@55!Vq>-x0AZ$Bm3eB<_t!R*pzLAKCY(OMGHP=8UrR=7=106el_=f8&^} z{;1-hf`aR)vhS#hwX#y;DE_meYUOBK)~H7339Xt@t>+Ifb4InHV+7H$1IlAM`eVA* zV|uP*Jbm9WgDYbP6UPkijTu#r89yI8)H!A{F?M))%ye^%usli>9XE%LXzGt!z7w`^ zbqTN-x4GhCG+$+VZ``hO{OI#>`_6HPiE+o}ai`7kW6%kb=)`g5i4*!0jyJ|#Tqn+- z8h5)gaWZkj{ocf>$_bC>u75lyJSQfmUQL|coHz%aB#Ta-S9W7gPk38T`nXQ|`cC>? zneo#;EAbY(g@aXRJR z^g-q6GihfsD4p(`4bz#+)3-OL?<8ghV`uIv&t&P(WLwYVxX#@3owa`5jT9b^8XcabC=e78z+;*Ux~1y_@mC5!2gZ7@$M_K6Q0+<#IIU# zXqB*<8>0U&;%1F?UcHW{8MJYZS&2FbKUJY2*M5Csw-`OXV3Ml!&)agVrBw)kxeKK(K_ zb;)4Pi`lQ@22@B8AT?dYI`+i}e;E2Be_v(g_A%b>>Q_68D6q#Z-L0V9HDXNjRoWyr zbnExF?;XqJ{S@7hNm?V70wgaUGpp>$at`%c9y@&#YlAPf`r!+)1Q7D{=98o&w+keI zwu|#AO80HEz45;Bw~a6bYYXXwvpJc1D=g8)Ov9BPlK#w(>cu-mnA#H6N-S{cuC2N}EA#wn)~kvl zH`X80lGHb6Z4e4=-V z1%G3;B+m*?xMv+WIp z^qgL={nAJ4tq@X3F~AJrbLmneo1x}A#CFlFQ@+q|v zy-xzRXGiO)pJ680p(EYXqOD`&Z}q4@k7`j{oc*6{PI_Mbyg8*JglbIp%K#$!z1#7T zvmxSk_HVx~i&}lM^*&}x?(^0M3g7;1c7k~5_I!%!)9rGdA8V<@$ZzH<;v;aI^Vur!atg+S831G;Qa}H zd4hbb@ZTu4mFK}L0~Z|n#)}vg*8fJSF@u1+-+T}gHUDVl&5x6x&*_IugUG+t{za*Y zKj`1WoTFR@)|}7$hf@1%cvN6loU#64k5U^pv3Wu^?A)W&hN|tun8De)!S7?}(##Mb z>zhzdpCu)x$hp?$lLIYHHxk5XBwz*KPc&Ul|13^>ZgGSgu}7(0`O0YUxF~#XC}!>1 z3ai|{#Hr=g=Ev#!J-~9Q#{rATY z!>z5nz56{pn*tFFV8_B$s@ZWUgEe;io@V9~MBD=A6D9nr|Iy6ad@?Qxzp$s7fQ3|i zRrNxeX5&Aa>B28&=uHOvqnYZ(+a^EP{^v9kPw- zO@Ow?M9E`SBEX6>GWLy+M+G9;5c=h+YKAdy;2##Tf|PidGqJ}aer?Zz6hos_erhN% zZVGESh5XH~{>RI*?k=B}`ty2^qDgAOE5o#G(2&@{hV`*(=i0M_ubzM0n;H8bIKFQD z{I==;$s#WHWNVxoYJU0m>i^5jU)k;H%W}uwH1GVqHdcFX_)W|2EzV+p$+6b|SugjF z9oIw{iQWY|D6$Ogq;`a}!I|L7Y}<9Fjp~tJ1=2p3UlrE5E|(+$BIMDA4zm+MYE-dH zb|*aYb$o>Yk`PCr>IoUgZ!5+7m&baEPLNVmiLTlsKhqMzsb1BDHoeN@AlK3AV-1G- zkL+yE@;m=W&z}VS-lJFdNLDyxZ?E#7gf9a)3^=*hkO=_8Metqs8e;#K@GWD9A!WK- z^gTQkQE#VvP+%`4(6zq%5dRrZ{g?3n_wm%Ue?sEF#8b^z`o*#TaccTc7H%u&y!>|y zM+TKk=LXS6)AK%ybIP2<`lX?1g<1_C@k#4~;o!|nUfLvYg_m5xd`uWl<(`6lB?JepRrH=RY zZ?7*-(JwkJ`ug6}@%q=IetPS#e+mqE%cl(wEc7Jvb+_y`iMlR&*_Z4OFL=D#eQz&y ztNo|IU@tTKF5sDa?aX}V#TUMvE)g4?Mb(D)58QwK-oCK@PwK|~QV$oVX*ExmrN{Di zrp{e$!`kAs>l?Nn#EA6eVs4ZWZTtT%b-UUdm#ca0uAx_I^q|**=A#~kveX7fQ<-KP z3%{VLUb8)?_tbSwF+}|AlwHPtF|{&vpv78Qn$1eUzqUL5>e~GH47C$tr^>aD4aG@B zm?cS`rn>nkq^s9fJX%qI6_8~kRWMMuk`wsgkxFs!Wd3T7|7{^fe7a#>ipDEizJD=o z_gPx$)q=Y6JgTWpNq&t>&?B{{F_%E71I^SVjj9Co8ucow$KH0QKs~6s>b63fMqmh! zAB@t5%lFrm-~0{|E}?E9@n!c%&sKE2u|B6$opBpiq03w+vO_>zq8-EuXwQXA1*0c9{@ie1flxDf^*Xj5V2OlKLZMlcuDb^Ic332kkUBRcx5Fq=W)kr)R$$r}IlC-1}~71aR2 zzV^%YR2WX19%ijlM!2~-k#8uoxukq|20nJ@+?muuLJ?g<=}~iSs!Az@38|;y`oY32 z2@Ff^g66$6zCKLQ3SXOTiCRc^0a{>8I9Ll*jnDTui59#f3>EWRBMpC2`;VQ9(239+ zSEeMh?3CxOp0iUaP>P14i$a6QySIEih20ON-H~q+H@Ax-2nP}@Rh6E>U7Okb zwTjk?#8Rk4k=EHtcaX^3{Uqy(SmyTdV5DDq@m827^B2^*@)3s7e@E>)#m5t-3KYrAb|J1m@Hl8#en13p} zS##t!A<@;d?vY0V{056L@iKDa*|m2;?o8vUF4M-k>+p|0n>4;BL6LRTck$k!hyzoMu5&u+e& zM{_bS#w;_EzC=$&&o&E4&e4;&zqVQd0(1(W0~@q2iV@-tW{!uS>sy}U4`8=nQFWN- zKRx~GLSEc;!YsLZ`6|B?y-nmX&P(V{QwQGkS(@X(g2;=g&i%I!WE?R`lzKbe?m_ z+-t+9WhuFQGE2=rcbd{=Wp2sIyqCCWG<1|S`Q~QA z<6XI#aI_T|ey)g0d*rmyuZ!W&Upz4htUrp3M`Ml{^H8@%Z>~f;sko z{Zg{H^!h}c(A~;?zG{s>Q+ev18H7ty z!53*L(-F8L0baubxnTi#8b3S*^z$UpjV$1XP4wdg*AYNb9Hed-WrdR9%mQ@~;L-#? zC>B;n0QVZe6sd@;Fd(D*yf*wgk97Iki%9+P5!FZMzZAfa zCx>NGKs*X4i7o6$Nm;oXk?S80&&A5EL2A~(-oz9#2cb)d%x8mGY)mjvs&yJ{Xy82r{CA_1WNg9Ja*(ymJ!hun2-p!F6dUJP}z_o#O59 zcr_(CkS_u`pPVHDFbW7W0>CN>;KRoeT4cu)$HCF`G$JQ6ejC$B0p(SvDsqq>#0{H{O#3Lb) z09YDc^d2d>5e8KxBUd;GaU3F=0BRth^{J343P>6bgA!40oLf8+n1}^9AHI%H1_)G8?1?fDBK=@f46L7Sch1oyUeWQo;KvAW;NNYwwPVhu2G{Um4rw{nDy0QSo@u>tmH2LD<<>8lS{&_v@|pMpy@7& zML+%;_XsWcsvqxsXW4~a#;Fo?ds z9$VvATF*hY^aO76fy!1^1wWGxrc( zEToPt97P2a>6gglcw1YT5mmw*f$$*OJPLwJrYFe%#Dro2NmR(|Kq%%kg3A{&eGsWg zL&2^-Tq9mI;t`;aPa(d)$V(y!xavc54?vBgf|ZaEC;F{m7Klp@W)VOqdg{zc{5%3o z919sv4dS`cfs_O?07xc7rCA_}*U>%%&_x0?7tbdSfN?3e1Pj4a1gJO_Tu-RmPX))b zKz5`W4*;6BjUeSfupDF-0pvr9!xYARaYuAeiw~!QE@B~#)AwNFFe45|kpLo-^2}*} zT}Q(3I6iyU-b4e_3PYcWE#T4;ylK#VfpBpebl~Wv=BngG<5*o%y%x5jkMcx*T^LW{ z+hKuf1nXxIX{XkNO(XyzheYcoyjIzuDstFfT^^SW657V>A*x?@FsQjrE)1gl-e?BO|!6cFR*y$~D%$-XTO0N&$Zbg3;QMvE>kk@oPWV$f@; z8tB~bd*a(jEi!VCoUS>N8DxhVlYo$WfyHE~6D`P|2IVs9j0m9N-wF9^!uvV5fDE86 zD<98j3I8U{!lmou5Zg7VHG-En_04$zWN$Lom4vv_iYreh?6CuiWLV;B{tX?lv>`~V z2pqi+Kr{(;-*?gDP^253p-;&bc)W)-!AwOu|3$kSxTK6m?CUB3qGF$y z3o4{MUBLSw=mHBdclknP_iL0PD2ez==Qq=b&cH?qXz|#gc8u6G=3zyEBUTR`MSS%t zAYC4x+!OxfiyK4q;d5Ktm)wt?cn(rJ>_R;;lN1oO_6yS*mTKe|W`~8vlhb*Cy(q|b z3lnfzgW(BJv)v2z8I6qwNGvs-M~5j=UWT$;q(1f67=rXU?I+Utq$#1wQR%^e=6q7W z;iCk@>b&+00oPBxBDtM(V<^lB4sXh!?Mu*QJk=+GA4cU#6FQs$ZL|9dzT|=J2%vBp z)PsuAr}5(%Kq3jOnVOn(>dxaWj2{E|w+TqRI^T!_;+uZ0k^++P1aBhq^|8H=qw*91 zH=-C%Yy4O*fQ9r_Nh93zm7=ic_7V2Hk!uij(NezFEQVq|F)s@nvxnBF2Hy}L6<#DE z#?tq_QjP2*;#%p&`SeR2y}=!9SP~`SFc6&uVD_Fwd{Gex*QC{Q#!yWSU+W{tpQ*j# z6Q&0u-}EIT9zLh>MLew$o4=YhC>hN{lwWSCy?}m@J|Dok7Cdj)Fe4H@aHM2pWKwhF zLEMeYlY9?u%3OMF9B-H#lZn0__arP_h88zS)-%73m=yx6hpQC`LClM1To^jXu_%?w z%s;XRSAA8FR349>mJzSake@!(J2O%1 zk|G&Rzh+rC-ZeT_o{V8wU7hXaxz2TJg==5J4jir$kbgV=4vWvNeS0C)ihr)|;XAg> z?1GH&(w|BBZ?ezdiL?}lS@K=l&-d=pjdzfOU{v20_J_w`P7fAxZSpSA zBoYE)kJn>^8OZ3c3;t7x?^D@yKvMSVl`Eu$BrI&&!y-{<7j>P$L6~O^$o=vcjIP z_>axix4#OHG8LC40nF74pYCsjARpHz1!iwwxJQMUhP*$Ni&^vASY7uveMT`o4t}oo z!qnkneU#)2e5^COy`GlV1$%L#6xF~esNKe3+X~WS_XU@tTFH*bKDR7-;BLtPf(yhB zBLKWY`UjQ7_rb`;ZOm2@3`j&u6B$T>a29nK&q0vMaM(%yj-J)>Ltv5Gh7|(1`X$A6 z9doD=hNV2MK9!_Jyuk_s-lL^Ogx7M z5IQ0#O`0g7gMb7?rC0(|1vC^D6eP6JAvEa_qzH%@5D^456lqdIQ*faMq}u=+Ai|4# zt-X$W?X&hi?>YbXy8h?A*5`TUOU5(D7;}vKxqqaDcBfP_tq-dQeZ&OCuE9j8h!#?6 zV{DVB{*cvv27v~gr~&g*7DTA=BgX(&1}i{p+6Mq47|>_~dk1CpzyUUZ!2XDKj+cty z2LLHLi)Rt~lK3#7+~sx}7D)wz@n6pX00sb!l4)Q(AXb2OC9y65msysX=ZQ~l&OhK+7Ca;jlrQ@_(%7A`uogVPK_d|W zz;1_6#71jlzWSm8&Ps^qAZZ^4|7O4JWZgtrF~$Oc*}z~W{Ccy8wdIQ!xu zIe?#a(;GyMP=In+uHR$h{orBY;-%LJb@BDYHAn}{=9EBtZuX12^q}8*8`Z-_BUi%XRvC?&X8j!w&nba!UPPn`CTcpP6|i z$lGQh@r1?kV3$ZnrcW&{?I;gpTWP6?K{=BB<=SaG-wXCR*B@ z1#pcux0@?E$_LQl*i*LdGRky$5?dr-KV5S8aAYSOaAcm5Ra*@QNX1()HgI5PPnRaQ zwUN!eedUa$bdIZWZ*PPhY67AKO+2S^rOTlN5f?nxT_|i%#gw=wtJ(Fsm=hIGU?_wj z8?+=t-#If(%d?|f1Y-b80_<#%65^~zk#w-Tfng5o9#VO!%Gw$0-3N7y%u~Xs03B8$ zvYVO;s2Ws|@9dGM_lEhKv2GW5*}B%-n5Imt6otg9#Am08*>)zGEZfKeZaVus@_&i9 zbnZc^4>V6-U$t=ueP~JI&-LPI;1`O%@lB3Y)mf<^9^VxnbXy~36Z+cC zLb|ppmjB$ivpL7Ab6g0Y?>)Dyf{3W4tFd{gl0(GVAEN+wId=FI9$i$u|4k88m4weW2)m-}l}t z^8Ry89-a5kHIJ2l^?va6Va|t=_?y~)YcL?TqtSQWoB2=Qw|XRgY4ln-qrAoc$=)vA zfVwNCxhu`)2&z%HsmJkv5o^-}pG$PNPXrF1K40%W{RN31V#C6jx5BA^iAN?oS zKSwLvEVJXB-=g}SJgYhV@XF^yAVu13=SOVc``_*Ib~zK3!e(?)%fa8!p@Gl)Wna^s z`N7|>Cn|c}pQ`U1!{6~z54~)44C;F){ias)i?JceD2br33(_Ig<$x;z5tv!E#$fV= z8KMcsSm?abtQlMx;#?McoFKn#`pJ}#$nVG{)OIx>lw|Tj*Z@vD0QiN6K9tL_D|#@X zAE`V8F^CL|VTiFH1aKslJ)=zQ?E1WIB+K$pS0w9-KEW`=egPvMxgha_#`>Ksku9}N zP(DTN+4d%C znTmNC#hONkVxgHuEzr_&-*0o_kO+1T>1n>auJQT(LqX88+p-^7zoM1~Tqoe#x}3 zKjIU+O>f|+0T6$Z(S7pn+-tca`$H{WrYBIYhx_lq1#yQIdU`8w;=WZ^RvVy2>yz(_ zC8^&sp05rZ(Myv0JZg9R#o`F=49_1pg`W!jXHMbgLN9}DX8jJ=_&XmAcvkcGLhmnl z3;jP+=vGkKzU;U&|6J(X{qMAYD)e@T`hPC;|0!Gpxq2X$B8$&qp(=^XU9B$t@;A8V zbam#&4}|K`h)D3(pCME>+r~ymqPf>0r%l%byCsa(b2R^<(El@ps%`$&4t5wrr~JjAKKTF5Z1q1M#QrSW>$PNsOlxjEKCWL0sl6BBzjAOnDVtk=!okJb zUFOP}KW)5@7dT})_hS&}a^(K~AZiZ$U`Jg^|2&9)7?Qu{}&d-|I@pGzuv3= zb?cSZlgQ5WJXwvyO&^EmD0=^h{QiJJqke@!8_PIKVRr6-=SW_O+-|R5VbDw7Zhu35 z?d<|t{|x!{tAub`33ZMZ2aC>1xy14eb%>Gw9r7#a8u$Z~j{%Y^X3`{Ge%?q70C)fd zfOmGxm>ncjM2<#P_1w`HJ&{xXiTe3#1#4XS*NiXZ>R&6^KQp#}?F@ftd=>xn_4(5W zf7pinjO_lrz5l=eJNB;??0?+~_V1^<|69Je4ggL-0buakY0p2|t_$;m_WT>$^?#$C z;JEx&3)gphDWzrZH;Fe{NvvggUvNiq zKco6WzfJLn=dyyriyec}R-UYcAWr(l74lKI3j-uWig?3sj!nic9I^tCY}NwR5iB7F zZ#elS;I!`V`sV!l-<>6w!@nmK+126E*v)$wG%GB-Q(r~|i-Dfjav|uS5SeG^XBnM> zNV89AGQ>qRT)LxJ)K$B2YXt)ISP)XxMT)&ifRtF>Up$@2R?yvLusDCawzYEEE@3?WV-Ja2l ze%+V(X&zmm7p3T27b1vtx5@==Pl;#+$m!=vU)WId*h6nhV@=52@4=X}415-%BVziq zklb$s7ccsRv%sO^8Fae+()$y4?~NHfe0=QIOjF3EZ8x<9WS+X$`CsOqJiB+E86dgX z`Tds$;_0wR$mxWo^S(~u=Vy;Ke>--AS+Y&jM=gInee!`6WZF$}?bQpwo!&HKQSB(-fl1k^#2XZw~cCqbJqsc^dzR~06Pczg`nvclbaWEgk$EKVb z*raq{7?`}?A-f&LjWhQdi?YI(eOD{cSt67i(Z<*8EA^hzk}{T_j|xCp@EtarvcX)- zKwcejWt-7U9pwbn6`I(sG~Dq1ht<^_M))F&wcF#l9<05-rq02;FIm+wLp4G1=k~~| zT}qjroH+Y^*K|ZoWXjrl_YELEB4pAnx8>eQS5~y=>r~=Z6S?9}Xd!m1j!Ovbjw@9~ zIN2Z8OuR5|X#l%9fN$@W9<@t9R6P)D0+SiuY>!8|(b7&zX4vN()|oI;>#G~kO@E`j zjvF~EnTNR6uXal(y;r`=GHWm;Z(*rVeMrmDTc_PfvR!r%+j)8)d{!-4$#I;kNVTIQ zOQlojQr}9KaLi6aCN?ERrAIgmp{mZq&Ha>6-Z{x>VTnwSM(L=x_9{CqD0g5w6^1%E z)qV7n^qd$ZBTn`4p;9%spyl>@4UBUn|JtXo){<6;)W^!Uh~h}D0sLwtkNP;ilk?2& z-H%ac?zou3b@2Z3Lk?Q$uhky#Y}U@~nYLn1DI`64s z4g_Os46AKL^y_&DR~Q}Ud-k10so%8M=TND>_kQC|&9>yXxAo7?eS25ep1l8E?cm`= z#bcYQRyL~qw>d2g+a%iewKH($k~#?PWPY8SlxUr}Qk){P8uu=I#2KX)RijD|ACy%O zT%FUq7zA{w*o$xi2d#qi;~?p40g>=GI$@otQRNt(TmUHpY7@5_Z_^{Mu;IOPKHrhz5M5utnNU&vDf4=gJfGIVKr-6{-IfUi3x=vneC}RsjSi&9FVh%Jk zL|%?z|c}&E-hKG%j_kauE^k}(O*oxsE_W6Y6K&ZtkXHv-K zb(rdoS2dTm9jW9h!9g7Fz?ih6S9}fNZbXw1p|uqM1;3DRT!bFE6%qE*P-ua~#`*Ma zq~w|!N32;C5}VT+W>x~dsKqL>#fdz&W?M!-+opGq603PP{>mag0$H(ay`Me>ZKtyD z6L7V-GLGdPppQW^m0^nV=aPd7kI@)EgpY`Rj)?%-hW&M4)XW*7N~dTgPyJ(cXVdxS zE#D~i9(sDW?(;38vG<85X9w!bC3(G!-)OtFJ`JeL z9yv4n#^L)Rjw2Q4Xdl$8BiJa{fKOoYt?rk6iULrA?-U~5`=xx9#6z=29;&gKzT{i( zClWrb?np$w>Tv3$5q(&k$YcuIG8{!`UW*I%#i-e7b*kovvtOW-xk>>8A-Xe%BnlaF zu)v(N%Rh-pkr*w$2i#@B`(j~cC>G9k`O_MG9KwNEqivT; zwP?j-`t#cD$9bSf#DNb9X#A*IJ_P2}XE$5ph zUAWdyf|Ydqn9mf~M%eUy0K}Z8QE9tiNss~OD%Di|9-Q+59U_x_h25>@vEVu`iYIUi zt;i$6QB5Zw7}ig+16|)c5DS{Y6rV71l@c$>jyffo8t-8_4YRHQX%wo!cg>tnwKg*b zElwk}5K^LKVWwc|P!)ztTV&{BCuRT#wuJL?KI|s%slZ36VihrM)Kb-gDex}bu*BkB zgI$;x^PjItfI(Os=o~iQCVP5X;E|TRYT-(fM+;B+vR0Q>YP@P7osDxlmISfGlWiCi zjLk|D7@3UIK7;IaG1QKZzx0M(9EhqW_ft>$Jqk)^Q5=siA#^P=2Z%1{QDAZy6YT^>soxnOP?dZ=w|64eeZ!9y}| z+@G59P*rb1gqOs`wKn0GSYm^LoN^8PNaA%)PAm!TN(Wdl;avXLj7j@d77~tB(QByD ziYFvE@S9ou;A7E?l@3fBpKt)NfP+Y40gfOE-ZJ|FMLQd~OEs3o@tSd`+K^YF@Rxag zIj9K}q^9*^b}>j0dT=>1e-`59p@~h>u2kJ{; z6jub)rlJ*Vxt!#9u5*&*Y`8O~&~D@0hl|PHX58-G-IALJoSRmA&4Mt## z!?2+6!A>+Vj1GXXfGQ4VjgLn#f!)fm3ax~6OcV?Uo{$1+7l2bF5IX@-m*YS~{nK#K zCj+CS%iui_>tnG*B&WzAls`&S;8H7jJUi+1f>?5~yrqh8+;LcvKzI=acAdtG!UC)q zmfaLc$?_E>D1H|KB2EF6j)P!Swm5vsE*$t2lLfU5+V*6zXRx4fU=IchjB$va4u~mV zVW+Xhky2Xy*{VGuk4%r`Fk%aqVM(OKsItUU9f^_-3X&3Btf?fCX^dPBQqfQ3DGq64 zgGsJ~9|2%FJB+YSPCAup|LqLG%3$%p#S@ib9t0LnCiwHXKQ)N$B9)?S21YF(D8j-_ zI|0Npi#YCz6%Jgfbp?S*ja$CLNzDWiSOmOM(y8hD*Hcf}rL9B>5QowN9kMP4X9eYC zU2e(>naHYK;obuHBLw-;YS~c}+?^`qwH@&^L8P~ppaFyV^jveJ9k~y{g@P!lS&&p> z+{3jio;cW=C!2K+5UdQdtz`AU!pH!`o(^yl*wmGG+)Ut#HI{G;%nlcS3W!%AvZ)f; ztdy_8b}qsISQ;filK}08W-Y6u#_$5~T9ZZ{3Z4cRjOP?QZz_mpN&g&<>~1X>+wRG> zpC&h@q1Zi9=Tp(oFgcmj{4@;AnU?GXK$_QciajBk1i)bZ%C*tB!?4VBW{x#61C--` ziU2Xgv3XFyFdAzb7JiCAu>;~;r@_1Fc7k!xo{8eV9u5XAZNNc+5r_2g6DMa~SGa#& z@%i;V8`qVEN|dTdI~`{Vhf2PNYqR9QYl_9Ch6;}{plQGk`2jP6!yaYDvr~Y(#$Y}Q zklF?8w+Epp@tORJoP^voD)pEGz-3-mnGL*82kXZ{V*!X51pw!=wqye^Dkwi2a3(>! zwM)~Kq5ay|mq(QX!jW~WH%LMi5jqu7juqs~6|p*^SMOIOZs#gp&8;ByR@h7WjN#c? zv3|x_#28NZHARQafF=>3MGB=?vEbcQR&^{mjR6T~9>O+)s|BIQmhV?4~mn(Cakn4_jlD4UkmsJRXmBgt-eB(sa9KqL)1U7Q2H@Q-8 zmREnMx!&@Z`orJqP4?I0_usX>Qm={edt(O9AfY^l3y;tsWCjG>d=u6TiKIg!2@Piw z8hoBWe4J>WyBj>;K-jM|oUx#REoedB_sA5QuQ$}wf_8p)!8=b5)oO7kAD5ZR*= z7C9wM5pGJ`-;^#K^W3b-d7Qgx79B=Qw#WEn4&BwLgm?(w3uQpg-GqeFAQe~cS2ROt z9L?vNAwiht+BXfA3C;I!LRu1<>+eyCss&euNsJ+uYc;f_{eG%V4%J-XI2d zKN#Bo;HlGtaf=7fuRQoA@4@7^2QRmUAHLZCkZJL7VE2Pj0NIO2-tUCZ0(@gJr$(?G zN3f8xo6R@!njbuA-u#=8{QG`L<(o$#0R8*^mgXmqB5CZ}c`d!)=om}R5SGG)<$_r} zw|*8{lEvc)lRbn4xr}YOOoIF-BtOw6)z^&GgPZMcj_g`i|zT+g{WIsoLe17@y ziMQ-7e-o0QM0B1~>~!9I>`?vKGqLlGBE&_p%OBfy^6kz~+I5b1sEnjZ9^zE1Y&k!~ zcM&U^>7P$9$fqn2@KMTnpwmg3Lw!K?E@Vcecw2Iz}cdxwAqnZ}k{|23AQvw4Sx2Pun=s2&(m-#$5_5jvt(FtYC*Xh+!{ z@j39b&!Dd@MBg3xJ_{YC6}qM$8xwwtzz++i+rGbq5*5Wl*Z9T~cbbJlp<06SLV_cZ zt6yTSj@P=c#*7^v=3~nBo3I2<;ZMe5#-4ldSgMfL@%~rRND;-Ya&edrW@-?z^SthO z)X8yj=y+_w`02#1Br3dP`KrE>i0?B6J*M1U78Ogsq1{tLY&0gLRx?$5oJn-(KUyW(*F_7@e9ixjJLGbrn_pLa_D4xE}~6b2JqP^UFZe zQz51lxY{+y07+~DFcqEy17M#fb=ZC!Yzv1PC9<2+;Z9W0A`NUsgUQvy+CC{=AZn#9 z6L+F4hecq!Fi;OBixUoZYwbw&_Oe0Du%kK=?}SBM0v*79V!Qi^Esw}9$vomfgY6=+ zXHYE88131`08P;JgJ$?J+)1gNt*Ny$mIIv5JhD0%=mZHI@DOH90&giJ+Zmt%S=R*K zz^P?KLk8NfgXwk$CV7Emu2g3DWX38PFtTPwv5CzT7 z3)lh>V;s2meTMNesG7Rl77x-Gb*RL?oBYUj-wvsxJQZETHj6<4Zh0?U-b_#tIyBqi zNkWxanC{jR>JeuwudE>j3KUftUW1O@vo634JJ9T3cqkcDpob_(#9^evPG}2(E$7pV zn?!@uk4pW8PBGilbHNz9SUlB%FzU<&b*7#;M1hY3cugXkt+H#uXAN6r(eM&bJJwWh z8MGgJG*8*ymTDG#6*NjkIWVeD5qT0mp+_l>xErt0r@!pudb8SK??wUd8G@!$&{J5~ zVEVbUuhB~kboU(En8@bIM01{l=#T`=2#}IY^kK}2Jj?-g3Ovuq^$=yjipcLkbao=4 zt*TM|mr-ixSBY4#2UafBh5bsZ<0%s2^6B&TuWd(|<{UhVrY11U(>3i;Aekvm;H5f` z!58;O-_mZ$PZ3rfmJ!w1m)2D47i4rL$;_IR*G>g9v0PVF7w(ggoP}#n%OC|Tm?I74 zuuXX*VFJ@L0c#Ll6dSE=iMO!z;Q5_I5hgm8WI&9vn=gc_-6{2&tBpGqD95L|tOuNZ}Gnw$Aa`D*VB!w)cU(~z?Y}cQZCmtu+ z1e=uQ&U`4k<9uNYk(aSFQL39taX`BEI~3sV)B5YYQK$EL+*w=l|6=`2*{gDuE=cyZ zr>d5WY8#Gb`(U-IWs{z(!#QPS;>fi^8c)OZD3XO9C(8KAUP?q42-jnj^{n{4Kq2S}T5NqHy%#7f(C^B2jhr-3v& z8&fTAD+HJuqir=09MlL{eR|~DWCW-wKmMGch(~8yCI{)%tRp zFu78i0mpe^Wm3pMkB7j?isH_~D}}+b!d}Zga-50^-{yrXw<_61!>asLimuXnsalgXQ?K~M&fHmzs9R4&ofC$|t}JVN z2=ETZb`}u}5gs@8rIyz&%D|(@Kzv;Gu*Rbor z!*^W$CIyC~ym1}7%@b=xcAc@vX!1F%+BLHK&f<-tfJ{i??C1Eu5kRdYF4-m7># z&7i7Pid<1QzF`w~J-o)0l|`n({!B`=dFPwX{k!tc+KP6G2wqU|l38Y@^xi*fb^lnI z_gxutjg1Np>`@j&H;ofZs_Jsj&!t9do(fhHc`D;k>1KX1{nKL&;j4bbPs`uu_q(6S zc=`R(sc3%pAdN_2@{Wi?uDsDDVQ;6_nIz4*#xp5;?AorWrm{`0>DDINZkfm3o7}Qa zleOJ*Jj$Eg^Uil_dlX!nYw{=xWB*M^UXo&><5iaBe&6dx5n1PK<&EP#-c-! zR_F1A?Yokg68@>t2-F6Ge@FSeM6UBm{oNq;zPX2t^Dxu^cMqp1HoQ3P@IAYf;s|)? za2;japI}0g>Ta&$z=SaUYPVS3w#E03=vb~god;ttCcg{(bmY;Q<}V*klfu@Y&l_%z z6?q0+Z9o&%=i^Um4mrUC)P}NRqNR-c5C+#%= zOPa=7rCZ1EdY2{TI3izaVf_>_VW%yvU9uw^afiD8=2ko?Z>8Zd@a1Gy0vI}X>X~}S zt87^-{^4X6sv17jXcu|>iUB*}aE_;Yselu4&MQ^>iVLWsAbi8?C%57`54fGD^?Fp` z%Mv5rxW#CS&+l%SsJ$BrR=l+#jvH6rxYCivLiDKQZXwU*RU8mIkF5^FO zD=w7Ze1HFK!<+Zb%bmMFw0wH%{o%pd+|Bb&uRp!{@Cf+O{I(v%pZM-H%TDs{Zwb7L ze<;B};Q{|vf;PGIu=Rf^!NJM9-oKaNoFn=FixMQZ1su2}^z>H*_n%8}d#416KbPPg z&;KaFe+<<9*MLjH_OIL$Y^7Ly)B4lpGVPK=!_Aj(T8qb*xT7m~RE2M*Hh+{LpC{qO zM^OO*9XM+zAu`}VBO?^!l3@i%xCJ-^TZoJl7yY42lq*|-u@cd|I-u*0;y-+ae0O&| zIX|fiUq(ri4IV?RQVt{;x(1|){I2&zxf%aff<;o-vxL5KQq8TKN$S^!5`}APk@g9 zrBm5|tB%@>#oyk3zEL(dY1kjL_TZV9(X9zN$8$UU+xQgQZ^}zQ^K@iHxN|#}aQ%SR zvwg@hISG^Z(LAqA*}i{7+r&%Aiifi17*0iFc6DBhk=Joeju&{ab5*VMlkzesJz55y zX>F#w-pzg0L=|HGADUSF5zAjSv1NYJ=l-Q8w%4K5Na9q+V8MlWp5X-5wrm$l=zm!g z3xB5dH%)B00%+kTZ5U*4kNZQ%%1>+DUlYt<6U+`V_VYaQ*97y2F?5#Mr!&g}NDvoNg|0-J+I39{( zCm)U?S(#0p;M-%wjOKUa#+gk736S*IW=rhxUqnGgdeT=LC1Q{T71!dFb&N_()L+{F zEL+xiAb-aCqrRc5^@dVUzVzYW1?7eg;lByWJCV?TDJYMdPe8xxkh>DLlw)HiJ7+d@ z*cAR>7nD1){ScIgm%|(vs_U1`{$cqU*xAV_0FWI%Oa2G`ZAPQEMnXAqExKvN?cuM* ze(+h>_1zeXXD7S;2oFO z6vf#NXO_!TcP`h-{0JP8y>+d>=n=5hH_Ng7p~61a_tlRmikcVAK_8cPq9}GkdVV`d zp4tf<+5Lw`f$W{Ykw0i5{@zX_-{1QCK{86obm09DYc+cRt+o28Zqa$Cyg!Yi_-)zw zFGNvLao$QXTmg`2jRCDWBdr1RxkNT(6mmer89>*$*+vPzbhiNd`c;uiQe^2Ky%9tm zdz?g@JG&5TkA7zi>kEMrZwnBkGl6AUh3q8Rqe>AuhG{DCED`~^nubW*odCR}YG=;& zf&dfXd{BKcpX#n%6f{eqzuVbI%5n+9pd=P6(=FFvOr%GlJ3Ei&02f!%g_p+;9emB0 z;M{xpOK_g{d7<1FQQp35oJ#Q*`&yASu8B$kZ09Gi@4a~GcQ8_`{3s>fb-%4u9USaGc>y)2=|=k1}nnnr9V$3)B0 zQ}6RT7-&M~I+tIzx7JY+Ev_{sUO3(y4ivex`*6=wvM z#wDswS6NN|V-@eX*LUP5QVrLBjm`NrJ;O)a^$1Dv`$f3uq56uUlAS8{HB%y~iN>k# z`4OlxcGLb&=UeUNvZp&${8L+V-sfY0%l!D?s`zC7c+K~3{LIr28`oD~h;FLTeyZYl zUX5#rCV#iKohl}K{g9g!aKQQ2v?LZr$eKHWDninypWU{}&C@tc)eg7|P8QbeZ)gJQ z&u@#ZUD4`##-Pr2`sNb?!bb}46&`+|)Ank8Q|M*F{VTH%UV}btN6Wsv{R+6-wQ@^I zYrpy*ReAw{J zWSimgL~m(A^@;Xf&Jwsv*{8=-T4nOwI6rz$J49uB@)(7<@a{`~;!-6myFkHmnWfyy zkUT}Ts@rowFJ!Zh5N@fVJ)XLd7~rbCme0+vv4uD6Kkzjp5~TyVrk12PH))8{o^U;r zq66vVl{)yP!@+7Fzv{Cr;yrV1y#KbRO}pNQeq~nTwsfkIoYJOLf*9#K4IVS}z@rnf z+h8Owk#4Sy^zB_Izw`+`MKMJ2+pi4D$+z5F^!adf+90Fl`}Q3puC0#w{Pe~-ou`W1 zvm+@wJgCt!J)UwwK)u!dqh)R%$XhsywSa9-jm;r|84o>~&cKR5XV4JdWg!6W2x6E6 zp;Qb>Rb*L}(!fHbb=?QIT2O91Hwa2q2Wl0Vp#tc53uY-#1 z;@U@@8q{;90t?pB-)jcHdNs59cF~Do5)1D>5TR4mXl!-HFmJL@F0ahYOCULfmFd zd09u>S<`4_X(Dh=zR^T#4~_glV478NDIaRu$n9DwC3>$hf|ZDas)R`JoWzqb2KrEL zbrMFUx=jtL&&rLFgu|De<+n+(k!NDVK_?j}dCvLi$WFEi_U1(Z+s@E?sUGYrHv#D{ zX_3;K?eKlerX1`l2@6URkZn&8eAPKtkhs9=Ic|Yb?qbn>Ym9d7J}kFK8Kr#!yMLQW zk|=Kt@A3+_ov}fy@2ECWR4V5>{xNj3I7aV%G+SDChrnLigRjJQeh(yzzNzNnvhX-K#;~V~O4>7{3^aT!e*ch?=+uD?8ZhhHmu#a8&nB_LV_R3QXS0P@ ztk|>AT>z714mZWU(5-F|+){=0R56ZAN!bvn5+G^R6HZD|j3erqYyl^*+>E|g2rW9x z);nusC5oZ?E$p_*-Pr_e8Icuqa3$f~-KoOYXQLDp@ZlWxA8}EeS*>Q~L-A=H;Vcy< zssw@PGaA@9cw>ZGvIGb;Zp(6%n!|pGvLD={%41OBreRx5o&Fpa$bv3U7boMX3~rBGxn?^hp|B_qiz@{1@fsXO04> zp`g!hKlFfGe1~ALgsPBg7S?d9l3!4{Db)imH$6pI1ioB9QJg!)`mAy37e{(b7^VcQ z1nl_0k2L8k0w#T9Z6}}Mc0sNKTotQNplM%H4z$T<#3$Vj;Z@~u|KtL2L%~>f`YV>T zWxk^eAAltyhXL*VB^gqgK;Z)X*~%qyaV$&Nr5iAxKEe(J^~=TWsR&NXVs+KhaRPvo`cfCq>iJcC?g@F<9)O+g;8KD!!a(3#2g`2hVlW09De`np?9TUgalAah zdP73sGf>QWjA6iGLQOn+f-dfkUD7il#2zM6kMiS<4?e#}ZSUK0uk%4~U$2FZy~A%K zY%qrM=cZE5ZsHM9L1eW}6~w;L3`_#iM1Fw|_rkLY-`iQsf*g?^9o6OIq&)ep+W7D! zb3uYx8`e&rBg`y9BSRzlbkGpAKZ%X2Zk$6!dWc`$-dQM()Tzx=c2DrehPW@WO(3Hc zqASl99=_PIx9$viAvNaUr}Y>S5)h@Vg%JwENbIvZRFkRA6=P{97xzdG`aH?{s>W7cuc(FKqzOblQK~E{z;kcr_Al?xfAKF3 ze=GB(2(IZcEg4W++9UpqmX>)nz1(w6SIDo&MEy${E#~MMA$zV^^=o2DK9=&rmMNW& zJ&#Du^|>E`Je!QaR?_qf$W-&b`5>`e#lML?Tp%aS_ME?Ah3fUxrGaAPMD$4Bi-n%| z-eD=+8yc%lW1AoNc#{WSN9>x$Zd%OZD0&YAl_C}A~U><{)jB>k?B&7+MkV7sfe=G zirzIPqnRDKUmkhdAo|33v{Y-9p?#F@LbR(rS!61ti-LU5jL;Q`c3&eCJuwE2WEW^O zhC3z*8WSK8vB@A`%8nr|L?4Td)LDxN4~li~jyh`=WfB{kygeQr9~*75Ku)obQ(p`5 zGK(R5#^tX?4ql2*G>a(EidWT&iC%~)&5jpYkm0F_v$z*sC6EAKi*sI!pvfmRYF({g zh##gTJgi8VB}U&EPk3aXNQ#NOD<9w4n8^GR-V&71Cy*4+8&@0@H5`=0dpDuDB6@+8 z^wP|RARkw;mNcVfFlrx@VV{h#30?}~zhWQruA39WpAw}KJ8hQuDLduxYVtHeabqn7 zd_Bn}HU)5?jD*J}WJj|%QEqW2M=zux1XFXIQ`(_PT*0XUQz30@QJgT4Ky>Pb+63N- zB&G85d*g&iEFK$_a|cn`b)a@E&YR;%mwzRPspXWH>uqvnWpO z$qvx8x7k6rGrh;}>kRbPHcWUrNR$v@Tcp~VYa&n=ZxxE#+ zHNlwY+0m!hb8B|xWSi$T?!tUo$SldpySsR;rsCSe%G}zfxc2p2R5>?z6bu7v?z^wtKRs7IR)X6c#PYSalaHROU=|$7=Ft zy<5y0cosiqR`4+{t7js2-HU(wYvd+0^}D%%g=FL}+KFsu3tEG+UuRPzgQ>2IRP=qS z-g?R!>>O)s6w17C#Z#tBQ!YTVSaK>|XFLWZnDWREb=h4-@@&omq3H8+#)jFsfN2#a z1+E5I;dl_%D~%LV9Im;NBPDKXwLSHb^gH+TKP*n`CwAZS7r&ZoVM>k=i`IiwhvNz8QWk(!J zhO8pGsOVNAWR-|`=!l{VRq4@$_HFF#J3yzq>+SRYtWZrGmOynX4HE+XObL9?IGplaTUanbiyxkUj zdx3KM_5Iu5mu}M>P{()Wt;Q#WM!>OcU#{?UTE_9JM#hQ!Fg%doXu&E$$4R zz2mk!N!y~qH?O9Cg93Y#bmqxTSGcT^&TX$%8L>0r(^QlNf5T&~d*e%pzU7dxgqnF0 zdYph(=|#mXBVPeGQ?4W#U6(+r-dT#epT~MI`{pWZ}(}!&2{^m z>n)nQzAFDhg~nWl-(YeqVvv&pcb%Tx>|*j+KWlixg}y|oe2zta!4$RqQe(IqZJmMo zOhVRPt}Aq`+yHh)8v1^RCHIEcKWVJLw2a(fAg0T`sDQJ%auoK zd5<=lAN|@(0f^99b?KlJ^lwiPee~)@EFvZzzPI_#TgS3i;NctMjW)Rs@33s?I6T>b-|Vmzd2FZq_}GcZ_92fQ^BDLm>Z1#bu|eZW8hv7IBM?oFF0k;t_q5 zeFK|)43Yj}-Tu)N{ZB*s$MgH2xAad=_P^ZhXNn9=_w_%)p5H1@u@VAi<^s3zsNy?U zYo3Ly>JEN7G5959a4mmuqh)Y&a&T*N5D;as9$@Tzudo*kf@+&KVgvZuu45hV3Ci#y zCQ5X(W%+I~FeejNH$!Lw*V(2Rs0Vw+eD9r}xChD`;mMBfxZZO9}RIaNWec|HXv=M;~yF0H>8^_U~ydhK^l512pcyPXu)5ka}s$K zIz}D}GA%%c4+VL(jjl=ruC|t09eDa?9_>EY$)6-JhpRktE67U;bK2_oGvW|hN*tH; zG_dSyP{A1C`ZM?M&!EF&kp<`@qKd&U1Q@@JhC8FKSdCxxjv;;@|LFJhvgorn& zi74NvF$q}ycSV(S36hv$tND7b~9>a`)ou3cF%$~lxH^xBMZ~LNG zRf5DG?EU7C3cSm|u*}a|yO$6ebg_BwhfpNIuSi=MGw2%QeDQoP>1Eh~NpxrsEqma= z{#Ux5G6!N_JxM~bqL>eIn9}biWg}i(+&#rPf23zo}<4o5KIheo+7)*DT4cHSf?ONvIh<{ztYP-*D)D zjNBaN^#lXm#^l(<%N#s7W4Q-I+n{YNOfOB|+Qy+DGSQo4WLMU}oBW-v(d=TrY~dkH z4+Qm)KK;TGwQ7Q<;W;*mD)&SIeqWi_$2_JmsA(og8yyWAIxi;L_+VRGF67|3ab{&8 z7qEtZwM9ZtDNC@>XWR;9t7bSpF=YG;GULC>6vhv4g<_KmWw&tw_-Fh_4bJ%*1RV39 z+{>wGx9553)x~)nYUbiOj6YWF-J6fckWD!B0v6e&`TFw@ET1IsJy!Q@RV@+rg2FMn zHBE00+-4wJsj{wBWj=|p6+$(v5uMLKe563M!~o-yQQ1u7yR+wJ8Y;9j&m~kXtYLKC zz30eZ-udGK2Wg0J7!Dc@9r(O-Zl@cQR*@a??!^nV&L%n^gGL3Y7>NRp0gfdsDxqqA zke@%{zGAo&%EB~aUU_VofDCpXW5FQu>75C^A^z{tE88TPr2PQiLq_WS(8@za#sF-S z5g2Zvh}T>_c2WkIZ5rOXpdVs5tOf61e&4dRV>@3)593Eu zy_v&I8O`Hw4L^iv?M3c?6zJ0jo2JdoFww0PjxFp3z9UHYqOYivLlcYz3&?%Lu*HP4 zGMjjgCd}*_ZKaju&A4-QQfc6 zx6n5?vCA7cIlp1_EaTk64`}i?^d=2Gy-i2I6VhQ4t6mZ|6MAJoVQU@}kc%|*XN=Ae zee*@w#wJEbw|h%4pY&0@#I6eU9kT^+qWK!Ge*3ciCQB*rfX`&(xdVcpy>zWB-|3pJVh)L! zUR@f$%R#cfKIFr~iWyt@@M-no}gG1TZg7~p?nyZf@A(%AE7 z$PpR)>9;K2&#I1AU*6CP{$_IamG0qN7cUrRe&-6jaM!U|vS%>#=1y=Bb@wH7x~6l} z(qjepEYoRoIez;u#WCD2!@+Yop_7Ku+*yFnY+BI!kL>Bk8e63c!@AcM^zAN&yYYuD zP0Vsx=6c=EhBq&o7@r^2=gedoTalq#T+~P8uU>>v#82BwpHpx?6)ck#8gEmZ=N}8B zl(G-mxMxB0IFq@ra*Yn~Ux;wnX*!>FI4n?;;cYAs)&fiC4!!R@!xh{(3C@qN%bDjg zVeZ0YzN6@TSi<;0P<$Mv&?dZR-{66f6)^7_-KIh$uv=|#z&0WNBbKEF&g7ay(Wu)U0pXZIe@v z&3JXCrE|~UKVqkIA-ykpacf@0Z`6FO`PdTpL)wXjT?emJPJz|)>gY0$LOu?fKGFy* z71F$Ryg?$BC$KSiR+t#OVtVcZz4At35J~&F%zc|{MT=7MBf}8o#Nw3DQY7~(K1N5e zGwGyswQxnZy~D{|5i$9u-o+D+SjCD;Eb?S zbH=5XA*5z_x50RqTkoZBOrNnFfo$*8+5Gw zzC_CF6U#298C`qMN+cIFA-I`ACaN7nX#2@0Ck0l(jI@=+Tl=I$RqosCE_u|h=(b^5 z7fp6=gr}rEj*MB_CcS#h42Rz-WfSfkXFu@8CdKRR4dG^L+(Ada{)Aw|!*p}2jTvq0 z%TKvAr@3H9)NKX*<0+7C8EW6fyL!i&3I0ZP|Fe#(G63rC{TUC=Iaxy zw5VaNm4cI$>9}m#<6Pxh^d+u9Pu)p~p4o~PRsXg4`Pb(0S}G}FC*CA3y?s?0!CKEg z^}6c+VedTyn#}fg?>m(c5(uG4mC!qeUZeyFRnUO+CRM6RQ&CYufDn3+A|Oa;f(S?# z6*Lr)Cen+d6FMq7*bp6+mzmkKpV|A_`#I-*-gC}}^UgOu@CEMwTKBrwbzQ$Jog}2x zE`&NckidP9B{=5IjYtYL+cf!~u4^>uE8i11&E#83Z`-*3Nc4!rt*jYJR^H78~YI+)@dgA0_ z?aakknCM({V9djVULO|K`K!}K17pWUPvMjVs_TmO`Ym-F#5AJmi#vZgH68CvL_M@0HnuO!Et=yFxoy$-2*PF1}-guP>|+Wm6umOgnG zUL3U}xqtYcm3po{DlVmEKdbOH5g#$@ecVRw8+}2SZ;;n!iXzsm!>>P3gvu7F@d=yv z*AtjXJwrPcdhR{d@Z-A-=&~e$c)|9fo!j=>X79shl!k2OnPH#9Sy|E_lFUO?@%_^r zBFhYh&Z`JbUSF#R@a$(fyT-O=$8wmz>`=AqvbindzfeA%cT@Z^&ULyUvcGR}@Vu^? z;rs2UvkyjXUG*d`53u*nt+M*5cEyeNKhH{dH~0@JCYg`ChR`M)3N_>9QvmAvzN^aOTpQ zMobxopEUsMS^N64GTZx;v-gGipeE?k-@*cm&nLD#|Ag`%cx0=3K1eauE;GobSB%+6 zj6yuUVchO)RUW1IHn+XaV(_{^jw z&yt+yw2tA+&g~_S)vtSWD2A{&FW{&yWci;VR5ubEs53bVW2oiE{hq`)Vt@`VJ|vCE`*e3#hSjY!^0YxAC+~ z8_9=0vPCYVf@wHohvc9RRPct`kHJ)#<>ZS&ePIN)z8#e~Jo_PNiu1>0_t?IuAhT-& zsm(0*HaQNzMYCYMY6>ekoP-Fal>rQ?u%Lcg0erLrekzqct|QsTBsrK4zp$g?-b#eO zXN%p^iDf3cNU_;W(Z0_o2d$tk$g!ntu_x?EB{8YHAp#E{@05oS9%T$r1>WmiWAu|& zkXA~`u>(}5-sLeFHS`!pj;bj*|R{XshoAv#J?vD6?PrJ&qY z4?Sw;1}%!*39YOQa1qYzfR`jn9;exCm?ZOU(qd`U&<=RNzIkpAb3?){x}qWSdL#>EUj@5_qB+rG=CPIcI3$vu9&Fw(kd8wo6wDYIL;?Nh4t-x43iPJV-Qmx|J>LGv@smF_ z)J4o3G|UCFd(Vy9^fz+~dl)+)g?)H<)N-8HHsh0>9seh`241sV5xY_HcyS+mGI4xC zgTF+_7N=%=wi7iumBRUHEXJf4F~RO-YAlkZoy}u)x^o3pQb!f-f@{AaSym0t zG7gQ<(UJj^Pw|#B40}>E+D_Dd7C&(GbcXJlx44EJvpTGl#1WO(CDssx;j$ zbUFLhyFS3p3Y6O%{xmlwHJvFvcMIlz%6RSt&hA9vbcHt>`(34dlVjHV*}Q=J0<-SU z>5P^n_s!Uh=6ZL&6XRu@`52$4Aqw%2!P5;;&)>{G|E|gC*TCQ(W}nU-8=PL0=W{QsO{#jmbE{b zeTLiXpWO~${%>2>&W9To$sM}sb>p`pBoD51+h=Vjp0)%W>QioPKlKdqy?(!n>kM%j zk z%T9Igc;SP%;E*4%okF9}>_M!5nSEx-AB9jwW*#OJ2vC-JvXuG9BPnt{ju~o8xgQ zCo6By`CmD)Js(tEzx_1q_M7cz;g6I*FGSCt`20L(z5ery*v~p|KL12!Q`uo9?mM}& zm?WCGv$W4gJ&Uj{H&ZkD0!k9|wGGkJ)P7>=QLuJYF>_9L$-IQCr1sB2m0vSZ(V*@> z7Sn%`f%~-QUF&}y&R;*b z=l}eF;R^nj{?99Er*?PZZhhHTaKx_UXd$^#RD)Lw@z(5U0>)gfjI?MIw3sAtyk?Pl zRB&MN>_+T{Cr!fKV#x%V-{GhQqUWO1Ks)Lr!z-}UEMw4DNxwAtS^>w5knxS>EP|U< zCEZl!?DB97Ot|7^2s^b6TVp7^-PWD)Cb#s@{!U6F& z9gPG|$>OZcKXV2CdjZcs&G3JbApdQKpFqpi|8a)rD+`4`|9ys=-P%k4x6bhW!UM8{ zgU8#_+0|A5^9(b7o#CNB%`hqdf6WXFpWKa!o8FOi(FB>~9SH#UXrb+9q&l2+InCu3 z6HOQQ(&E3(u*hL)TP@K`$+ol?UFN>gvl@x46nB^f4O!wykB1S zxbEoxV21Z<98bz;XY5;*l&^HlCaz?gnMAu*7yQc^j{i3^+{f?!LT5lin4H(Yzb*Lk z^DsBIv5D3-5@l4BFtiPwbW$I-G|Ky*|1Uos{I#?GwX;h8mdt*b_Qwa{|BuhC52h2Mzkeu-i~qtqt((W)!Kb<`2V;=t z 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 { System.Threading.Tasks.Task> PullAsync(); } + public interface ISinkRef + { + Akka.Streams.Dsl.Sink Sink { get; } + } public interface ISourceQueue { System.Threading.Tasks.Task OfferAsync(T element); @@ -620,6 +638,10 @@ namespace Akka.Streams void Fail(System.Exception ex); new System.Threading.Tasks.Task WatchCompletionAsync(); } + public interface ISourceRef + { + Akka.Streams.Dsl.Source Source { get; } + } public interface ITransformerLike { bool IsComplete { get; } @@ -718,6 +740,10 @@ namespace Akka.Streams public static readonly Akka.Streams.QueueOfferResult.QueueClosed Instance; } } + public sealed class RemoteStreamRefActorTerminatedException : System.Exception + { + public RemoteStreamRefActorTerminatedException(string message) { } + } public abstract class Shape : System.ICloneable { protected Shape() { } @@ -764,6 +790,20 @@ namespace Akka.Streams public StreamLimitReachedException(long max) { } protected StreamLimitReachedException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } } + public class static StreamRefAttributes + { + public static Akka.Streams.Attributes CreateSubscriptionTimeout(System.TimeSpan timeout) { } + public interface IStreamRefAttribute : Akka.Streams.Attributes.IAttribute { } + public sealed class SubscriptionTimeout : Akka.Streams.Attributes.IAttribute, Akka.Streams.StreamRefAttributes.IStreamRefAttribute + { + public SubscriptionTimeout(System.TimeSpan timeout) { } + public System.TimeSpan Timeout { get; } + } + } + public sealed class StreamRefSubscriptionTimeoutException : Akka.Pattern.IllegalStateException + { + public StreamRefSubscriptionTimeoutException(string message) { } + } public sealed class StreamSubscriptionTimeoutSettings : System.IEquatable { public readonly Akka.Streams.StreamSubscriptionTimeoutTerminationMode Mode; @@ -792,6 +832,10 @@ namespace Akka.Streams Propagate = 0, Drain = 1, } + public sealed class TargetRefNotInitializedYetException : Akka.Pattern.IllegalStateException + { + public TargetRefNotInitializedYetException() { } + } public enum ThrottleMode { Shaping = 0, @@ -1698,6 +1742,27 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.Source> FromInputStream(System.Func createInputStream, int chunkSize = 8192) { } public static Akka.Streams.Dsl.Sink> FromOutputStream(System.Func createOutputStream, bool autoFlush = False) { } } + [Akka.Annotations.ApiMayChangeAttribute()] + public class static StreamRefs + { + [Akka.Annotations.ApiMayChangeAttribute()] + public static Akka.Streams.Dsl.Source>> SinkRef() { } + [Akka.Annotations.ApiMayChangeAttribute()] + public static Akka.Streams.Dsl.Sink>> SourceRef() { } + } + public sealed class StreamRefSettings + { + public StreamRefSettings(int bufferCapacity, System.TimeSpan demandRedeliveryInterval, System.TimeSpan subscriptionTimeout) { } + public int BufferCapacity { get; } + public System.TimeSpan DemandRedeliveryInterval { get; } + public string ProductPrefix { get; } + public System.TimeSpan SubscriptionTimeout { get; } + public Akka.Streams.Dsl.StreamRefSettings Copy(System.Nullable bufferCapacity = null, System.Nullable demandRedeliveryInterval = null, System.Nullable subscriptionTimeout = null) { } + public static Akka.Streams.Dsl.StreamRefSettings Create(Akka.Configuration.Config config) { } + public Akka.Streams.Dsl.StreamRefSettings WithBufferCapacity(int value) { } + public Akka.Streams.Dsl.StreamRefSettings WithDemandRedeliveryInterval(System.TimeSpan value) { } + public Akka.Streams.Dsl.StreamRefSettings WithSubscriptionTimeout(System.TimeSpan value) { } + } public abstract class SubFlow : Akka.Streams.Dsl.IFlow { protected SubFlow() { } @@ -4034,6 +4099,16 @@ namespace Akka.Streams.IO public static Akka.Streams.IO.IOResult Success(long count) { } } } +namespace Akka.Streams.Serialization +{ + public sealed class StreamRefSerializer : Akka.Serialization.SerializerWithStringManifest + { + public StreamRefSerializer(Akka.Actor.ExtendedActorSystem system) { } + public override object FromBinary(byte[] bytes, string manifest) { } + public override string Manifest(object o) { } + public override byte[] ToBinary(object o) { } + } +} namespace Akka.Streams.Stage { [System.ObsoleteAttribute("Please use GraphStage instead. [1.1.2]")] diff --git a/src/core/Akka.Streams.TestKit/TestSink.cs b/src/core/Akka.Streams.TestKit/TestSink.cs index e293e352fc2..d0bd2c37495 100644 --- a/src/core/Akka.Streams.TestKit/TestSink.cs +++ b/src/core/Akka.Streams.TestKit/TestSink.cs @@ -5,6 +5,7 @@ // //----------------------------------------------------------------------- +using Akka.Actor; using Akka.Streams.Dsl; using Akka.TestKit; @@ -18,9 +19,7 @@ public static class TestSink /// /// /// - public static Sink> SinkProbe(this TestKitBase testKit) - { - return new Sink>(new StreamTestKit.ProbeSink(testKit, Attributes.None, new SinkShape(new Inlet("ProbeSink.in")))); - } + public static Sink> SinkProbe(this TestKitBase testKit) => + new Sink>(new StreamTestKit.ProbeSink(testKit, Attributes.None, new SinkShape(new Inlet("ProbeSink.in")))); } } \ No newline at end of file diff --git a/src/core/Akka.Streams.Tests/Akka.Streams.Tests.csproj b/src/core/Akka.Streams.Tests/Akka.Streams.Tests.csproj index 60a7b72c1cf..c4ebd2dac7d 100644 --- a/src/core/Akka.Streams.Tests/Akka.Streams.Tests.csproj +++ b/src/core/Akka.Streams.Tests/Akka.Streams.Tests.csproj @@ -1,19 +1,17 @@  - Akka.Streams.Tests net452;netcoreapp1.1 - + - @@ -21,33 +19,27 @@ - - - - TRACE;DEBUG;SERIALIZATION;CONFIGURATION;UNSAFE_THREADING;NET452;NET452 $(DefineConstants);SERIALIZATION;CONFIGURATION;UNSAFE_THREADING;AKKAIO - $(DefineConstants);CORECLR - $(DefineConstants);RELEASE - + \ No newline at end of file diff --git a/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs b/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs new file mode 100644 index 00000000000..87ced56c8d7 --- /dev/null +++ b/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs @@ -0,0 +1,405 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Actor.Internal; +using Akka.Configuration; +using Akka.IO; +using Akka.Streams.Dsl; +using Akka.Streams.Implementation; +using Akka.Streams.TestKit; +using Akka.TestKit; +using Xunit; +using Xunit.Abstractions; +using FluentAssertions; + +namespace Akka.Streams.Tests +{ + internal sealed class DataSourceActor : ActorBase + { + public static Props Props(IActorRef probe) => + Akka.Actor.Props.Create(() => new DataSourceActor(probe));//.WithDispatcher("akka.test.stream-dispatcher"); + + private readonly IActorRef _probe; + private readonly ActorMaterializer _materializer; + + public DataSourceActor(IActorRef probe) + { + _probe = probe; + _materializer = Context.System.Materializer(); + } + + protected override void PostStop() + { + base.PostStop(); + _materializer.Dispose(); + } + + protected override bool Receive(object message) + { + switch (message) + { + case "give": + { + /* + * Here we're able to send a source to a remote recipient + * For them it's a Source; for us it is a Sink we run data "into" + */ + var source = Source.From(new[] { "hello", "world" }); + var aref = source.RunWith(StreamRefs.SourceRef(), _materializer); + aref.PipeTo(Sender); + return true; + } + case "give-infinite": + { + var source = Source.From(Enumerable.Range(1, int.MaxValue).Select(i => "ping-" + i)); + var t = source.ToMaterialized(StreamRefs.SourceRef(), Keep.Right).Run(_materializer); + t.PipeTo(Sender); + return true; + } + case "give-fail": + { + var r = Source.Failed(new Exception("Boom!")) + .RunWith(StreamRefs.SourceRef(), _materializer); + r.PipeTo(Sender); + return true; + } + case "give-complete-asap": + { + var r = Source.Empty().RunWith(StreamRefs.SourceRef(), _materializer); + r.PipeTo(Sender); + return true; + } + case "give-subscribe-timeout": + { + var r = Source.Repeat("is anyone there?") + .ToMaterialized(StreamRefs.SourceRef(), Keep.Right) + .WithAttributes(StreamRefAttributes.CreateSubscriptionTimeout(TimeSpan.FromMilliseconds(500))) + .Run(_materializer); + r.PipeTo(Sender); + return true; + } + case "receive": + { + /* + * We write out code, knowing that the other side will stream the data into it. + * For them it's a Sink; for us it's a Source. + */ + var sink = StreamRefs.SinkRef().To(Sink.ActorRef(_probe, "")) + .Run(_materializer); + sink.PipeTo(Sender); + return true; + } + case "receive-subscribe-timeout": + { + var sink = StreamRefs.SinkRef() + .WithAttributes(StreamRefAttributes.CreateSubscriptionTimeout(TimeSpan.FromMilliseconds(500))) + .To(Sink.ActorRef(_probe, "")) + .Run(_materializer); + sink.PipeTo(Sender); + return true; + } + case "receive-32": + { +// var t = StreamRefs.SinkRef() +// .ToMaterialized(TestSink.SinkProbe(Context.System), Keep.Both) +// .Run(_materializer); +// +// var sink = t.Item1; +// var driver = t.Item2; +// Task.Run(() => +// { +// driver.EnsureSubscription(); +// driver.Request(2); +// driver.ExpectNext(); +// driver.ExpectNext(); +// driver.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); +// driver.Request(30); +// driver.ExpectNextN(30); +// +// return ""; +// }).PipeTo(_probe); + + return true; + } + default: return false; + } + } + } + + internal sealed class SourceMsg + { + public ISourceRef DataSource { get; } + + public SourceMsg(ISourceRef dataSource) + { + DataSource = dataSource; + } + } + + internal sealed class BulkSourceMsg + { + public ISourceRef DataSource { get; } + + public BulkSourceMsg(ISourceRef dataSource) + { + DataSource = dataSource; + } + } + + internal sealed class SinkMsg + { + public ISinkRef DataSink { get; } + + public SinkMsg(ISinkRef dataSink) + { + DataSink = dataSink; + } + } + + internal sealed class BulkSinkMsg + { + public ISinkRef DataSink { get; } + + public BulkSinkMsg(ISinkRef dataSink) + { + DataSink = dataSink; + } + } + + public class StreamRefsSpec : AkkaSpec + { + public static Config Config() + { + var address = TestUtils.TemporaryServerAddress(); + return ConfigurationFactory.ParseString($@" + akka {{ + loglevel = INFO + actor {{ + provider = remote + serialize-messages = off + }} + remote.dot-netty.tcp {{ + port = {address.Port} + hostname = ""{address.Address}"" + }} + }}").WithFallback(ConfigurationFactory.Load()); + } + + public StreamRefsSpec(ITestOutputHelper output) : this(Config(), output: output) + { + } + + protected StreamRefsSpec(Config config, ITestOutputHelper output = null) : base(config, output) + { + Materializer = Sys.Materializer(); + RemoteSystem = ActorSystem.Create("remote-system", Config()); + InitializeLogger(RemoteSystem); + _probe = CreateTestProbe(); + + var it = RemoteSystem.ActorOf(DataSourceActor.Props(_probe.Ref), "remoteActor"); + var remoteAddress = ((ActorSystemImpl)RemoteSystem).Provider.DefaultAddress; + Sys.ActorSelection(it.Path.ToStringWithAddress(remoteAddress)).Tell(new Identify("hi")); + + _remoteActor = ExpectMsg().Subject; + } + + protected readonly ActorSystem RemoteSystem; + protected readonly ActorMaterializer Materializer; + private readonly TestProbe _probe; + private readonly IActorRef _remoteActor; + + protected override void BeforeTermination() + { + base.BeforeTermination(); + RemoteSystem.Dispose(); + Materializer.Dispose(); + } + + [Fact] + public void SourceRef_must_send_messages_via_remoting() + { + _remoteActor.Tell("give"); + var sourceRef = ExpectMsg>(); + + sourceRef.Source.RunWith(Sink.ActorRef(_probe.Ref, ""), Materializer); + + _probe.ExpectMsg("hello"); + _probe.ExpectMsg("world"); + _probe.ExpectMsg(""); + } + + [Fact] + public void SourceRef_must_fail_when_remote_source_failed() + { + _remoteActor.Tell("give-fail"); + var sourceRef = ExpectMsg>(); + + sourceRef.Source.RunWith(Sink.ActorRef(_probe.Ref, ""), Materializer); + + var f = _probe.ExpectMsg(); + f.Cause.Message.Should().Contain("Remote stream ("); + f.Cause.Message.Should().Contain("Boom!"); + } + + [Fact] + public void SourceRef_must_complete_properly_when_remote_source_is_empty() + { + // this is a special case since it makes sure that the remote stage is still there when we connect to it + _remoteActor.Tell("give-complete-asap"); + var sourceRef = ExpectMsg>(); + + sourceRef.Source.RunWith(Sink.ActorRef(_probe.Ref, ""), Materializer); + + _probe.ExpectMsg(""); + } + + [Fact] + public void SourceRef_must_respect_backpressure_from_implied_by_target_Sink() + { + _remoteActor.Tell("give-infinite"); + var sourceRef = ExpectMsg>(); + + var probe = sourceRef.Source.RunWith(this.SinkProbe(), Materializer); + + probe.EnsureSubscription(); + probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + + probe.Request(1); + probe.ExpectNext("ping-1"); + probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + + probe.Request(20); + probe.ExpectNextN(Enumerable.Range(1, 20).Select(i => "ping-" + (i + 1))); + probe.Cancel(); + + // since no demand anyway + probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + + // should not cause more pulling, since we issued a cancel already + probe.Request(10); + probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void SourceRef_must_receive_timeout_if_subscribing_too_late_to_the_source_ref() + { + _remoteActor.Tell("give-subscribe-timeout"); + var sourceRef = ExpectMsg>(); + + + // not materializing it, awaiting the timeout... + Thread.Sleep(800); + + var probe = sourceRef.Source.RunWith(this.SinkProbe(), Materializer); + + // the local "remote sink" should cancel, since it should notice the origin target actor is dead + probe.EnsureSubscription(); + var ex = probe.ExpectError(); + ex.Message.Should().Contain("has terminated! Tearing down this side of the stream as well."); + } + + [Fact] + public void SinkRef_must_receive_elements_via_remoting() + { + _remoteActor.Tell("receive"); + var remoteSink = ExpectMsg>(); + + Source.From(new[] { "hello", "world" }) + .To(remoteSink.Sink) + .Run(Materializer); + + _probe.ExpectMsg("hello"); + _probe.ExpectMsg("world"); + _probe.ExpectMsg(""); + } + + [Fact] + public void SinkRef_must_fail_origin_if_remote_Sink_gets_a_failure() + { + _remoteActor.Tell("receive"); + var remoteSink = ExpectMsg>(); + + Source.Failed(new Exception("Boom!")) + .To(remoteSink.Sink) + .Run(Materializer); + + var failure = _probe.ExpectMsg(); + failure.Cause.Message.Should().Contain("Remote stream ("); + failure.Cause.Message.Should().Contain("Boom!"); + } + + [Fact] + public void SinkRef_must_receive_hundreds_of_elements_via_remoting() + { + _remoteActor.Tell("receive"); + var remoteSink = ExpectMsg>(); + + var msgs = Enumerable.Range(1, 100).Select(i => "payload-" + i).ToArray(); + + Source.From(msgs).RunWith(remoteSink.Sink, Materializer); + + foreach (var msg in msgs) + { + _probe.ExpectMsg(msg); + } + + _probe.ExpectMsg(""); + } + + [Fact] + public void SinkRef_must_receive_timeout_if_subscribing_too_late_to_the_sink_ref() + { + _remoteActor.Tell("receive-subscribe-timeout"); + var remoteSink = ExpectMsg>(); + + // not materializing it, awaiting the timeout... + Thread.Sleep(800); + + var probe = this.SourceProbe().To(remoteSink.Sink).Run(Materializer); + + var failure = _probe.ExpectMsg(); + failure.Cause.Message.Should().Contain("Remote side did not subscribe (materialize) handed out Sink reference"); + + // the local "remote sink" should cancel, since it should notice the origin target actor is dead + probe.ExpectCancellation(); + } + + [Fact(Skip="FIXME: how to pass test assertions to remote system?")] + public void SinkRef_must_respect_backpressure_implied_by_origin_Sink() + { + _remoteActor.Tell("receive-32"); + var sinkRef = ExpectMsg>(); + + Source.Repeat("hello").RunWith(sinkRef.Sink, Materializer); + + // if we get this message, it means no checks in the request/expect semantics were broken, good! + _probe.ExpectMsg(""); + } + + [Fact] + public void SinkRef_must_not_allow_materializing_multiple_times() + { + _remoteActor.Tell("receive-subscribe-timeout"); + var sinkRef = ExpectMsg>(); + + var p1 = this.SourceProbe().To(sinkRef.Sink).Run(Materializer); + var p2 = this.SourceProbe().To(sinkRef.Sink).Run(Materializer); + + p1.EnsureSubscription(); + var req = p1.ExpectRequest(); + + // will be cancelled immediately, since it's 2nd: + p2.EnsureSubscription(); + p2.ExpectCancellation(); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Streams/ActorMaterializer.cs b/src/core/Akka.Streams/ActorMaterializer.cs index 88fa0b05b07..c9f13baeaa5 100644 --- a/src/core/Akka.Streams/ActorMaterializer.cs +++ b/src/core/Akka.Streams/ActorMaterializer.cs @@ -318,7 +318,8 @@ private static ActorMaterializerSettings Create(Config config) isFuzzingMode: config.GetBoolean("debug.fuzzing-mode"), isAutoFusing: config.GetBoolean("auto-fusing", true), maxFixedBufferSize: config.GetInt("max-fixed-buffer-size", 1000000000), - syncProcessingLimit: config.GetInt("sync-processing-limit", 1000)); + syncProcessingLimit: config.GetInt("sync-processing-limit", 1000), + streamRefSettings: StreamRefSettings.Create(config.GetConfig("stream-ref") ?? Config.Empty)); } private const int DefaultlMaxFixedbufferSize = 1000; @@ -367,6 +368,8 @@ private static ActorMaterializerSettings Create(Config config) /// public readonly int SyncProcessingLimit; + public readonly StreamRefSettings StreamRefSettings; + /// /// TBD /// @@ -381,7 +384,7 @@ private static ActorMaterializerSettings Create(Config config) /// TBD /// TBD /// TBD - public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferSize, string dispatcher, Decider supervisionDecider, StreamSubscriptionTimeoutSettings subscriptionTimeoutSettings, bool isDebugLogging, int outputBurstLimit, bool isFuzzingMode, bool isAutoFusing, int maxFixedBufferSize, int syncProcessingLimit = DefaultlMaxFixedbufferSize) + public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferSize, string dispatcher, Decider supervisionDecider, StreamSubscriptionTimeoutSettings subscriptionTimeoutSettings, StreamRefSettings streamRefSettings, bool isDebugLogging, int outputBurstLimit, bool isFuzzingMode, bool isAutoFusing, int maxFixedBufferSize, int syncProcessingLimit = DefaultlMaxFixedbufferSize) { InitialInputBufferSize = initialInputBufferSize; MaxInputBufferSize = maxInputBufferSize; @@ -394,7 +397,7 @@ public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferS IsAutoFusing = isAutoFusing; MaxFixedBufferSize = maxFixedBufferSize; SyncProcessingLimit = syncProcessingLimit; - + StreamRefSettings = streamRefSettings; } /// @@ -405,7 +408,7 @@ public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferS /// TBD public ActorMaterializerSettings WithInputBuffer(int initialSize, int maxSize) { - return new ActorMaterializerSettings(initialSize, maxSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(initialSize, maxSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -415,7 +418,7 @@ public ActorMaterializerSettings WithInputBuffer(int initialSize, int maxSize) /// TBD public ActorMaterializerSettings WithDispatcher(string dispatcher) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -425,7 +428,7 @@ public ActorMaterializerSettings WithDispatcher(string dispatcher) /// TBD public ActorMaterializerSettings WithSupervisionStrategy(Decider decider) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, decider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, decider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -435,7 +438,7 @@ public ActorMaterializerSettings WithSupervisionStrategy(Decider decider) /// TBD public ActorMaterializerSettings WithDebugLogging(bool isEnabled) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, isEnabled, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, isEnabled, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -445,7 +448,7 @@ public ActorMaterializerSettings WithDebugLogging(bool isEnabled) /// TBD public ActorMaterializerSettings WithFuzzingMode(bool isFuzzingMode) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, isFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, isFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -455,7 +458,7 @@ public ActorMaterializerSettings WithFuzzingMode(bool isFuzzingMode) /// TBD public ActorMaterializerSettings WithAutoFusing(bool isAutoFusing) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, isAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, isAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -465,7 +468,7 @@ public ActorMaterializerSettings WithAutoFusing(bool isAutoFusing) /// TBD public ActorMaterializerSettings WithMaxFixedBufferSize(int maxFixedBufferSize) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, maxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, maxFixedBufferSize, SyncProcessingLimit); } /// @@ -475,7 +478,7 @@ public ActorMaterializerSettings WithMaxFixedBufferSize(int maxFixedBufferSize) /// TBD public ActorMaterializerSettings WithSyncProcessingLimit(int limit) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, limit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, limit); } /// @@ -488,7 +491,14 @@ public ActorMaterializerSettings WithSubscriptionTimeoutSettings(StreamSubscript if (Equals(settings, SubscriptionTimeoutSettings)) return this; - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, settings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, settings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + } + + public ActorMaterializerSettings WithStreamRefSettings(StreamRefSettings settings) + { + if (settings == null) throw new ArgumentNullException(nameof(settings)); + if (ReferenceEquals(settings, this.StreamRefSettings)) return this; + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, settings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } } diff --git a/src/core/Akka.Streams/Akka.Streams.csproj b/src/core/Akka.Streams/Akka.Streams.csproj index 6c5fab2efe6..2b40d287c6b 100644 --- a/src/core/Akka.Streams/Akka.Streams.csproj +++ b/src/core/Akka.Streams/Akka.Streams.csproj @@ -66,8 +66,12 @@ + + + + $(DefineConstants);SERIALIZATION;CLONEABLE;AKKAIO diff --git a/src/core/Akka.Streams/Attributes.cs b/src/core/Akka.Streams/Attributes.cs index aebf81227c5..e17aa36358c 100644 --- a/src/core/Akka.Streams/Attributes.cs +++ b/src/core/Akka.Streams/Attributes.cs @@ -471,4 +471,32 @@ public SupervisionStrategy(Decider decider) public static Attributes CreateSupervisionStrategy(Decider strategy) => new Attributes(new SupervisionStrategy(strategy)); } + + /// + /// Attributes for stream refs ( and ). + /// Note that more attributes defined in and . + /// + public static class StreamRefAttributes + { + /// + /// Attributes specific to stream refs. + /// + public interface IStreamRefAttribute : Attributes.IAttribute { } + + public sealed class SubscriptionTimeout : IStreamRefAttribute + { + public TimeSpan Timeout { get; } + + public SubscriptionTimeout(TimeSpan timeout) + { + Timeout = timeout; + } + } + + /// + /// Specifies the subscription timeout within which the remote side MUST subscribe to the handed out stream reference. + /// + public static Attributes CreateSubscriptionTimeout(TimeSpan timeout) => + new Attributes(new SubscriptionTimeout(timeout)); + } } \ No newline at end of file diff --git a/src/core/Akka.Streams/Dsl/StreamRefs.cs b/src/core/Akka.Streams/Dsl/StreamRefs.cs new file mode 100644 index 00000000000..e4e0a62eccb --- /dev/null +++ b/src/core/Akka.Streams/Dsl/StreamRefs.cs @@ -0,0 +1,731 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Annotations; +using Akka.Configuration; +using Akka.Event; +using Akka.Pattern; +using Akka.Streams.Actors; +using Akka.Streams.Dsl; +using Akka.Streams.Implementation; +using Akka.Streams.Stage; +using Akka.Util.Internal; +using Reactive.Streams; + +namespace Akka.Streams.Dsl +{ + + /// + /// API MAY CHANGE: The functionality of stream refs is working, however it is expected that the materialized value + /// will eventually be able to remove the Task wrapping the stream references. For this reason the API is now marked + /// as API may change. See ticket https://github.com/akka/akka/issues/24372 for more details. + /// + /// Factories for creating stream refs. + /// + [ApiMayChange] + public static class StreamRefs + { + /// + /// A local which materializes a which can be used by other streams (including remote ones), + /// to consume data from this local stream, as if they were attached in the spot of the local Sink directly. + /// + /// Adheres to . + /// + /// + [ApiMayChange] + public static Sink>> SourceRef() => + Sink.FromGraph>>(new SinkRefStageImpl(null)); + + /// + /// A local which materializes a which can be used by other streams (including remote ones), + /// to consume data from this local stream, as if they were attached in the spot of the local Sink directly. + /// + /// Adheres to . + /// + /// See more detailed documentation on [[SinkRef]]. + /// + /// + [ApiMayChange] + public static Source>> SinkRef() => + Source.FromGraph>>(new SourceRefStageImpl(null)); + } + + #region StreamRef messages + + internal interface IStreamRefsProtocol { } + + /// + /// Sequenced equivalent. + /// The receiving end of these messages MUST fail the stream if it observes gaps in the sequence, + /// as these messages will not be re-delivered. + /// + /// Sequence numbers start from `0`. + /// + internal sealed class SequencedOnNext : IStreamRefsProtocol, IDeadLetterSuppression + { + public long SeqNr { get; } + public object Payload { get; } + + public SequencedOnNext(long seqNr, object payload) + { + SeqNr = seqNr; + Payload = payload ?? throw ReactiveStreamsCompliance.ElementMustNotBeNullException; + } + } + + /// + /// Initial message sent to remote side to establish partnership between origin and remote stream refs. + /// + internal sealed class OnSubscribeHandshake : IStreamRefsProtocol, IDeadLetterSuppression + { + public OnSubscribeHandshake(IActorRef targetRef) + { + TargetRef = targetRef; + } + + public IActorRef TargetRef { get; } + } + + /// + /// Sent to a the receiver side of a stream ref, once the sending side of the SinkRef gets signalled a Failure. + /// + internal sealed class RemoteStreamFailure : IStreamRefsProtocol + { + public RemoteStreamFailure(string message) + { + Message = message; + } + + public string Message { get; } + } + + /// + /// Sent to a the receiver side of a stream ref, once the sending side of the SinkRef gets signalled a completion. + /// + internal sealed class RemoteStreamCompleted : IStreamRefsProtocol + { + public RemoteStreamCompleted(long seqNr) + { + SeqNr = seqNr; + } + + public long SeqNr { get; } + } + + /// + /// INTERNAL API: Cumulative demand, equivalent to sequence numbering all events in a stream. + /// + /// This message may be re-delivered. + /// + internal sealed class CumulativeDemand : IStreamRefsProtocol, IDeadLetterSuppression + { + public CumulativeDemand(long seqNr) + { + if (seqNr <= 0) throw ReactiveStreamsCompliance.NumberOfElementsInRequestMustBePositiveException; + SeqNr = seqNr; + } + + public long SeqNr { get; } + } + + #endregion + + #region extension + + internal sealed class StreamRefsMaster : IExtension + { + public static StreamRefsMaster Get(ActorSystem system) => + system.WithExtension(); + + private readonly EnumerableActorName sourceRefStageNames = new EnumerableActorNameImpl("SourceRef", new AtomicCounterLong(0L)); + private readonly EnumerableActorName sinkRefStageNames = new EnumerableActorNameImpl("SinkRef", new AtomicCounterLong(0L)); + + public StreamRefsMaster(ExtendedActorSystem system) + { + + } + + public string NextSourceRefName() => sourceRefStageNames.Next(); + public string NextSinkRefName() => sinkRefStageNames.Next(); + } + + internal sealed class StreamRefsMasterProvider : ExtensionIdProvider + { + public override StreamRefsMaster CreateExtension(ExtendedActorSystem system) => + new StreamRefsMaster(system); + } + + #endregion + + public sealed class StreamRefSettings + { + public static StreamRefSettings Create(Config config) + { + if (config == null) throw new ArgumentNullException(nameof(config), "`akka.stream.materializer.stream-ref` was not present"); + + return new StreamRefSettings( + bufferCapacity: config.GetInt("buffer-capacity", 32), + demandRedeliveryInterval: config.GetTimeSpan("demand-redelivery-interval", TimeSpan.FromSeconds(1)), + subscriptionTimeout: config.GetTimeSpan("subscription-timeout", TimeSpan.FromSeconds(30))); + } + + public int BufferCapacity { get; } + public TimeSpan DemandRedeliveryInterval { get; } + public TimeSpan SubscriptionTimeout { get; } + + public StreamRefSettings(int bufferCapacity, TimeSpan demandRedeliveryInterval, TimeSpan subscriptionTimeout) + { + BufferCapacity = bufferCapacity; + DemandRedeliveryInterval = demandRedeliveryInterval; + SubscriptionTimeout = subscriptionTimeout; + } + + public string ProductPrefix => nameof(StreamRefSettings); + + public StreamRefSettings WithBufferCapacity(int value) => Copy(bufferCapacity: value); + public StreamRefSettings WithDemandRedeliveryInterval(TimeSpan value) => Copy(demandRedeliveryInterval: value); + public StreamRefSettings WithSubscriptionTimeout(TimeSpan value) => Copy(subscriptionTimeout: value); + + public StreamRefSettings Copy(int? bufferCapacity = null, + TimeSpan? demandRedeliveryInterval = null, + TimeSpan? subscriptionTimeout = null) => new StreamRefSettings( + bufferCapacity: bufferCapacity ?? this.BufferCapacity, + demandRedeliveryInterval: demandRedeliveryInterval ?? this.DemandRedeliveryInterval, + subscriptionTimeout: subscriptionTimeout ?? this.SubscriptionTimeout); + } + + /// + /// Abstract class defined serialization purposes of . + /// + internal abstract class SourceRefImpl + { + public static SourceRefImpl Create(Type eventType, IActorRef initialPartnerRef) + { + var destType = typeof(SourceRefImpl<>).MakeGenericType(eventType); + return (SourceRefImpl)Activator.CreateInstance(destType, initialPartnerRef); + } + + protected SourceRefImpl(IActorRef initialPartnerRef) + { + InitialPartnerRef = initialPartnerRef; + } + + public IActorRef InitialPartnerRef { get; } + public abstract Type EventType { get; } + } + internal sealed class SourceRefImpl : SourceRefImpl, ISourceRef + { + public SourceRefImpl(IActorRef initialPartnerRef) : base(initialPartnerRef) { } + public override Type EventType => typeof(T); + public Source Source => + Dsl.Source.FromGraph(new SourceRefStageImpl(InitialPartnerRef)).MapMaterializedValue(_ => NotUsed.Instance); + } + + /// + /// Abstract class defined serialization purposes of . + /// + internal abstract class SinkRefImpl + { + public static SinkRefImpl Create(Type eventType, IActorRef initialPartnerRef) + { + var destType = typeof(SinkRefImpl<>).MakeGenericType(eventType); + return (SinkRefImpl)Activator.CreateInstance(destType, initialPartnerRef); + } + + protected SinkRefImpl(IActorRef initialPartnerRef) + { + InitialPartnerRef = initialPartnerRef; + } + + public IActorRef InitialPartnerRef { get; } + public abstract Type EventType { get; } + } + + internal sealed class SinkRefImpl : SinkRefImpl, ISinkRef + { + public SinkRefImpl(IActorRef initialPartnerRef) : base(initialPartnerRef) { } + public override Type EventType => typeof(T); + public Sink Sink => Dsl.Sink.FromGraph(new SinkRefStageImpl(InitialPartnerRef)).MapMaterializedValue(_ => NotUsed.Instance); + } + + /// + /// INTERNAL API: Actual stage implementation backing s. + /// + /// If initialPartnerRef is set, then the remote side is already set up. If it is none, then we are the side creating + /// the ref. + /// + /// + internal sealed class SinkRefStageImpl : GraphStageWithMaterializedValue, Task>> + { + #region logic + + private sealed class Logic : TimerGraphStageLogic, IInHandler + { + private const string SubscriptionTimeoutKey = "SubscriptionTimeoutKey"; + + private readonly SinkRefStageImpl _stage; + private readonly TaskCompletionSource> _promise; + private readonly Attributes _inheritedAttributes; + + private StreamRefsMaster _streamRefsMaster; + private StreamRefSettings _settings; + private StreamRefAttributes.SubscriptionTimeout _subscriptionTimeout; + private string _stageActorName; + + private StreamRefsMaster StreamRefsMaster => _streamRefsMaster ?? (_streamRefsMaster = StreamRefsMaster.Get(ActorMaterializerHelper.Downcast(Materializer).System)); + private StreamRefSettings Settings => _settings ?? (_settings = ActorMaterializerHelper.Downcast(Materializer).Settings.StreamRefSettings); + private StreamRefAttributes.SubscriptionTimeout SubscriptionTimeout => _subscriptionTimeout ?? (_subscriptionTimeout = + _inheritedAttributes.GetAttribute(new StreamRefAttributes.SubscriptionTimeout(Settings.SubscriptionTimeout))); + protected override string StageActorName => _stageActorName ?? (_stageActorName = StreamRefsMaster.NextSinkRefName()); + + private StageActor _stageActor; + + private IActorRef _partnerRef = null; + + #region demand management + private long _remoteCumulativeDemandReceived = 0L; + private long _remoteCumulativeDemandConsumed = 0L; + #endregion + + private Status _completedBeforeRemoteConnected = null; + + public IActorRef Self => _stageActor.Ref; + public IActorRef PartnerRef + { + get + { + if (_partnerRef == null) throw new TargetRefNotInitializedYetException(); + return _partnerRef; + } + } + + public Logic(SinkRefStageImpl stage, TaskCompletionSource> promise, + Attributes inheritedAttributes) : base(stage.Shape) + { + _stage = stage; + _promise = promise; + _inheritedAttributes = inheritedAttributes; + + this.SetHandler(_stage.Inlet, this); + } + + public override void PreStart() + { + _stageActor = GetStageActor(InitialReceive); + var initialPartnerRef = _stage._initialPartnerRef; + if (initialPartnerRef != null) + ObserveAndValidateSender(initialPartnerRef, "Illegal initialPartnerRef! This would be a bug in the SinkRef usage or impl."); + + Log.Debug("Created SinkRef, pointing to remote Sink receiver: {0}, local worker: {1}", initialPartnerRef, Self); + + _promise.SetResult(new SourceRefImpl(Self)); + + if (_partnerRef != null) + { + _partnerRef.Tell(new OnSubscribeHandshake(Self), Self); + TryPull(); + } + + ScheduleOnce(SubscriptionTimeoutKey, SubscriptionTimeout.Timeout); + } + + private void InitialReceive(Tuple args) + { + var sender = args.Item1; + var message = args.Item2; + + switch (message) + { + case Terminated terminated: + if (Equals(terminated.ActorRef, PartnerRef)) + FailStage(new RemoteStreamRefActorTerminatedException($"Remote target receiver of data {PartnerRef} terminated. " + + "Local stream terminating, message loss (on remote side) may have happened.")); + break; + case CumulativeDemand demand: + // the other side may attempt to "double subscribe", which we want to fail eagerly since we're 1:1 pairings + ObserveAndValidateSender(sender, "Illegal sender for CumulativeDemand"); + if (_remoteCumulativeDemandReceived < demand.SeqNr) + { + _remoteCumulativeDemandReceived = demand.SeqNr; + Log.Debug("Received cumulative demand [{0}], consumable demand: [{1}]", demand.SeqNr, _remoteCumulativeDemandReceived - _remoteCumulativeDemandConsumed); + } + TryPull(); + break; + } + } + + public void OnPush() + { + var element = GrabSequenced(_stage.Inlet); + PartnerRef.Tell(element, Self); + Log.Debug("Sending sequenced: {0} to {1}", element, PartnerRef); + TryPull(); + } + + private void TryPull() + { + if (_remoteCumulativeDemandConsumed < _remoteCumulativeDemandReceived && !HasBeenPulled(_stage.Inlet)) + { + Pull(_stage.Inlet); + } + } + + protected internal override void OnTimer(object timerKey) + { + if ((string)timerKey == SubscriptionTimeoutKey) + { + // we know the future has been competed by now, since it is in preStart + var ex = new StreamRefSubscriptionTimeoutException($"[{StageActorName}] Remote side did not subscribe (materialize) handed out Sink reference [${_promise.Task.Result}], " + + "within subscription timeout: ${PrettyDuration.format(subscriptionTimeout.timeout)}!"); + + throw ex; // this will also log the exception, unlike failStage; this should fail rarely, but would be good to have it "loud" + } + } + + private SequencedOnNext GrabSequenced(Inlet inlet) + { + var onNext = new SequencedOnNext(_remoteCumulativeDemandConsumed, Grab(inlet)); + _remoteCumulativeDemandConsumed++; + return onNext; + } + + public void OnUpstreamFailure(Exception cause) + { + if (_partnerRef != null) + { + _partnerRef.Tell(new RemoteStreamFailure(cause.ToString()), Self); + _stageActor.Unwatch(_partnerRef); + FailStage(cause); + } + else + { + _completedBeforeRemoteConnected = new Status.Failure(cause); + // not terminating on purpose, since other side may subscribe still and then we want to fail it + // the stage will be terminated either by timeout, or by the handling in `observeAndValidateSender` + SetKeepGoing(true); + } + } + + public void OnUpstreamFinish() + { + if (_partnerRef != null) + { + _partnerRef.Tell(new RemoteStreamCompleted(_remoteCumulativeDemandConsumed), Self); + _stageActor.Unwatch(_partnerRef); + CompleteStage(); + } + else + { + _completedBeforeRemoteConnected = new Status.Success(Done.Instance); + // not terminating on purpose, since other side may subscribe still and then we want to complete it + SetKeepGoing(true); + } + } + + private void ObserveAndValidateSender(IActorRef partner, string failureMessage) + { + if (_partnerRef == null) + { + _partnerRef = partner; + _stageActor.Watch(_partnerRef); + + switch (_completedBeforeRemoteConnected) + { + case Status.Failure failure: + Log.Warning("Stream already terminated with exception before remote side materialized, failing now."); + partner.Tell(new RemoteStreamFailure(failure.Cause.ToString()), Self); + FailStage(failure.Cause); + break; + case Status.Success _: + Log.Warning("Stream already completed before remote side materialized, failing now."); + partner.Tell(new RemoteStreamCompleted(_remoteCumulativeDemandConsumed), Self); + CompleteStage(); + break; + case null: + if (!Equals(partner, PartnerRef)) + { + var ex = new InvalidPartnerActorException(partner, PartnerRef, failureMessage); + partner.Tell(new RemoteStreamFailure(ex.ToString()), Self); + throw ex; + } + break; + } + } + } + } + + #endregion + + private readonly IActorRef _initialPartnerRef; + + public SinkRefStageImpl(IActorRef initialPartnerRef) + { + _initialPartnerRef = initialPartnerRef; + Shape = new SinkShape(Inlet); + } + + public Inlet Inlet { get; } = new Inlet("SinkRef.in"); + public override SinkShape Shape { get; } + public override ILogicAndMaterializedValue>> CreateLogicAndMaterializedValue(Attributes inheritedAttributes) + { + var promise = new TaskCompletionSource>(); + return new LogicAndMaterializedValue>>(new Logic(this, promise, inheritedAttributes), promise.Task); + } + } + + /// + /// INTERNAL API: Actual stage implementation backing [[SourceRef]]s. + /// + /// If initialPartnerRef is set, then the remote side is already set up. + /// If it is none, then we are the side creating the ref. + /// + internal sealed class SourceRefStageImpl : GraphStageWithMaterializedValue, Task>> + { + + #region logic + + private sealed class Logic : TimerGraphStageLogic, IOutHandler + { + private const string SubscriptionTimeoutKey = "SubscriptionTimeoutKey"; + private const string DemandRedeliveryTimerKey = "DemandRedeliveryTimerKey"; + + private readonly SourceRefStageImpl _stage; + private readonly TaskCompletionSource> _promise; + private readonly Attributes _inheritedAttributes; + + private StreamRefsMaster _streamRefsMaster; + private StreamRefSettings _settings; + private StreamRefAttributes.SubscriptionTimeout _subscriptionTimeout; + private string _stageActorName; + + private StageActor _stageActor; + private IActorRef _partnerRef = null; + + private StreamRefsMaster StreamRefsMaster => _streamRefsMaster ?? (_streamRefsMaster = StreamRefsMaster.Get(ActorMaterializerHelper.Downcast(Materializer).System)); + private StreamRefSettings Settings => _settings ?? (_settings = ActorMaterializerHelper.Downcast(Materializer).Settings.StreamRefSettings); + private StreamRefAttributes.SubscriptionTimeout SubscriptionTimeout => _subscriptionTimeout ?? (_subscriptionTimeout = + _inheritedAttributes.GetAttribute(new StreamRefAttributes.SubscriptionTimeout(Settings.SubscriptionTimeout))); + protected override string StageActorName => _stageActorName ?? (_stageActorName = StreamRefsMaster.NextSourceRefName()); + + public IActorRef Self => _stageActor.Ref; + public IActorRef PartnerRef + { + get + { + if (_partnerRef == null) throw new TargetRefNotInitializedYetException(); + return _partnerRef; + } + } + + #region demand management + + private bool _completed = false; + private long _expectingSeqNr = 0L; + private long _localCumulativeDemand = 0L; + private long _localRemainingRequested = 0L; + private FixedSizeBuffer _receiveBuffer; // initialized in preStart since depends on settings + private IRequestStrategy _requestStrategy; // initialized in preStart since depends on receiveBuffer's size + #endregion + + public Logic(SourceRefStageImpl stage, TaskCompletionSource> promise, Attributes inheritedAttributes) : base(stage.Shape) + { + _stage = stage; + _promise = promise; + _inheritedAttributes = inheritedAttributes; + + SetHandler(_stage.Outlet, this); + } + + public override void PreStart() + { + _receiveBuffer = new ModuloFixedSizeBuffer(Settings.BufferCapacity); + _requestStrategy = new WatermarkRequestStrategy(highWatermark: _receiveBuffer.Capacity); + + _stageActor = GetStageActor(InitialReceive); + + Log.Debug("[{0}] Allocated receiver: {1}", StageActorName, Self); + + var initialPartnerRef = _stage._initialPartnerRef; + if (initialPartnerRef != null) // this will set the partnerRef + ObserveAndValidateSender(initialPartnerRef, ""); + + _promise.SetResult(new SinkRefImpl(Self)); + + ScheduleOnce(SubscriptionTimeoutKey, SubscriptionTimeout.Timeout); + } + + public void OnPull() + { + TryPush(); + TriggerCumulativeDemand(); + } + + public void OnDownstreamFinish() + { + CompleteStage(); + } + + private void TriggerCumulativeDemand() + { + var i = _receiveBuffer.RemainingCapacity - _localRemainingRequested; + if (_partnerRef != null && i > 0) + { + 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 + if (addDemand > 0) + { + _localCumulativeDemand += addDemand; + _localRemainingRequested += addDemand; + var demand = new CumulativeDemand(_localCumulativeDemand); + + Log.Debug("[{0}] Demanding until [{1}] (+{2})", _stageActorName, _localCumulativeDemand, addDemand); + PartnerRef.Tell(demand, Self); + ScheduleDemandRedelivery(); + } + } + } + + private void ScheduleDemandRedelivery() => + ScheduleOnce(DemandRedeliveryTimerKey, Settings.DemandRedeliveryInterval); + + protected internal override void OnTimer(object timerKey) + { + switch (timerKey) + { + case SubscriptionTimeoutKey: + var ex = new StreamRefSubscriptionTimeoutException( + // we know the future has been competed by now, since it is in preStart + $"[{StageActorName}] Remote side did not subscribe (materialize) handed out Sink reference [{_promise.Task.Result}]," + + $"within subscription timeout: {SubscriptionTimeout.Timeout}!"); + throw ex; + case DemandRedeliveryTimerKey: + Log.Debug("[{0}] Scheduled re-delivery of demand until [{1}]", StageActorName, _localCumulativeDemand); + PartnerRef.Tell(new CumulativeDemand(_localCumulativeDemand), Self); + ScheduleDemandRedelivery(); + break; + } + } + + private void InitialReceive(Tuple args) + { + var sender = args.Item1; + var message = args.Item2; + + switch (message) + { + case OnSubscribeHandshake handshake: + CancelTimer(SubscriptionTimeoutKey); + ObserveAndValidateSender(sender, "Illegal sender in OnSubscribeHandshake"); + Log.Debug("[{0}] Received handshake {1} from {2}", StageActorName, message, sender); + TriggerCumulativeDemand(); + break; + case SequencedOnNext onNext: + ObserveAndValidateSender(sender, "Illegal sender in SequencedOnNext"); + ObserveAndValidateSequenceNr(onNext.SeqNr, "Illegal sequence nr in SequencedOnNext"); + Log.Debug("[{0}] Received seq {1} from {2}", StageActorName, message, sender); + OnReceiveElement(onNext.Payload); + TriggerCumulativeDemand(); + break; + case RemoteStreamCompleted completed: + 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); + _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); + FailStage(new RemoteStreamRefActorTerminatedException($"Remote stream ({sender.Path}) failed, reason: {failure.Message}")); + break; + case Terminated terminated: + if (Equals(_partnerRef, terminated.ActorRef)) + FailStage(new RemoteStreamRefActorTerminatedException( + $"The remote partner {terminated.ActorRef} has terminated! Tearing down this side of the stream as well.")); + else + FailStage(new RemoteStreamRefActorTerminatedException( + $"Received UNEXPECTED Terminated({terminated.ActorRef}) message! This actor was NOT our trusted remote partner, which was: {_partnerRef}. Tearing down.")); + + break; + } + } + + private void TryPush() + { + if (!_receiveBuffer.IsEmpty && IsAvailable(_stage.Outlet)) Push(_stage.Outlet, _receiveBuffer.Dequeue()); + else if (_receiveBuffer.IsEmpty && _completed) CompleteStage(); + } + + private void OnReceiveElement(object payload) + { + var outlet = _stage.Outlet; + _localRemainingRequested--; + 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}"); + else + _receiveBuffer.Enqueue((TOut)payload); + } + + /// + /// TBD + /// + /// Thrown when is invalid + private void ObserveAndValidateSender(IActorRef partner, string failureMessage) + { + if (_partnerRef == null) + { + Log.Debug("Received first message from {0}, assuming it to be the remote partner for this stage", partner); + _partnerRef = partner; + _stageActor.Watch(partner); + } + else if (!Equals(_partnerRef, partner)) + { + var ex = new InvalidPartnerActorException(partner, PartnerRef, failureMessage); + partner.Tell(new RemoteStreamFailure(ex.Message), Self); + throw ex; + } + } + + private void ObserveAndValidateSequenceNr(long seqNr, string failureMessage) + { + if (seqNr != _expectingSeqNr) + throw new InvalidSequenceNumberException(_expectingSeqNr, seqNr, failureMessage); + else + _expectingSeqNr++; + } + } + + #endregion + + private readonly IActorRef _initialPartnerRef; + + public SourceRefStageImpl(IActorRef initialPartnerRef) + { + _initialPartnerRef = initialPartnerRef; + + Shape = new SourceShape(Outlet); + } + + public Outlet Outlet { get; } = new Outlet("SourceRef.out"); + public override SourceShape Shape { get; } + + public override ILogicAndMaterializedValue>> CreateLogicAndMaterializedValue(Attributes inheritedAttributes) + { + var promise= new TaskCompletionSource>(); + return new LogicAndMaterializedValue>>(new Logic(this, promise, inheritedAttributes), promise.Task); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Streams/Implementation/Buffers.cs b/src/core/Akka.Streams/Implementation/Buffers.cs index d5d57e242de..dcfc9c7ba13 100644 --- a/src/core/Akka.Streams/Implementation/Buffers.cs +++ b/src/core/Akka.Streams/Implementation/Buffers.cs @@ -194,6 +194,8 @@ protected FixedSizeBuffer(int capacity) /// public bool NonEmpty => Used != 0; + public long RemainingCapacity => Capacity - Used; + // for the maintenance parameter see dropHead /// /// TBD diff --git a/src/core/Akka.Streams/Serialization/Proto/StreamRefMessages.g.cs b/src/core/Akka.Streams/Serialization/Proto/StreamRefMessages.g.cs new file mode 100644 index 00000000000..3854e88d887 --- /dev/null +++ b/src/core/Akka.Streams/Serialization/Proto/StreamRefMessages.g.cs @@ -0,0 +1,1413 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: StreamRefMessages.proto +#pragma warning disable 1591, 0612, 3021 +#region Designer generated code + +using pb = global::Google.Protobuf; +using pbc = global::Google.Protobuf.Collections; +using pbr = global::Google.Protobuf.Reflection; +using scg = global::System.Collections.Generic; +namespace Akka.Streams.Serialization.Proto.Msg { + + /// Holder for reflection information generated from StreamRefMessages.proto + internal static partial class StreamRefMessagesReflection { + + #region Descriptor + /// File descriptor for StreamRefMessages.proto + public static pbr::FileDescriptor Descriptor { + get { return descriptor; } + } + private static pbr::FileDescriptor descriptor; + + static StreamRefMessagesReflection() { + byte[] descriptorData = global::System.Convert.FromBase64String( + string.Concat( + "ChdTdHJlYW1SZWZNZXNzYWdlcy5wcm90bxIkQWtrYS5TdHJlYW1zLlNlcmlh", + "bGl6YXRpb24uUHJvdG8uTXNnIh0KCUV2ZW50VHlwZRIQCgh0eXBlTmFtZRgB", + "IAEoCSKQAQoHU2lua1JlZhJBCgl0YXJnZXRSZWYYASABKAsyLi5Ba2thLlN0", + "cmVhbXMuU2VyaWFsaXphdGlvbi5Qcm90by5Nc2cuQWN0b3JSZWYSQgoJZXZl", + "bnRUeXBlGAIgASgLMi8uQWtrYS5TdHJlYW1zLlNlcmlhbGl6YXRpb24uUHJv", + "dG8uTXNnLkV2ZW50VHlwZSKSAQoJU291cmNlUmVmEkEKCW9yaWdpblJlZhgB", + "IAEoCzIuLkFra2EuU3RyZWFtcy5TZXJpYWxpemF0aW9uLlByb3RvLk1zZy5B", + "Y3RvclJlZhJCCglldmVudFR5cGUYAiABKAsyLy5Ba2thLlN0cmVhbXMuU2Vy", + "aWFsaXphdGlvbi5Qcm90by5Nc2cuRXZlbnRUeXBlIhgKCEFjdG9yUmVmEgwK", + "BHBhdGgYASABKAkiUQoHUGF5bG9hZBIXCg9lbmNsb3NlZE1lc3NhZ2UYASAB", + "KAwSFAoMc2VyaWFsaXplcklkGAIgASgFEhcKD21lc3NhZ2VNYW5pZmVzdBgD", + "IAEoDCJZChRPblN1YnNjcmliZUhhbmRzaGFrZRJBCgl0YXJnZXRSZWYYASAB", + "KAsyLi5Ba2thLlN0cmVhbXMuU2VyaWFsaXphdGlvbi5Qcm90by5Nc2cuQWN0", + "b3JSZWYiIQoQQ3VtdWxhdGl2ZURlbWFuZBINCgVzZXFOchgBIAEoAyJgCg9T", + "ZXF1ZW5jZWRPbk5leHQSDQoFc2VxTnIYASABKAMSPgoHcGF5bG9hZBgCIAEo", + "CzItLkFra2EuU3RyZWFtcy5TZXJpYWxpemF0aW9uLlByb3RvLk1zZy5QYXls", + "b2FkIiQKE1JlbW90ZVN0cmVhbUZhaWx1cmUSDQoFY2F1c2UYASABKAwiJgoV", + "UmVtb3RlU3RyZWFtQ29tcGxldGVkEg0KBXNlcU5yGAEgASgDQgJIAWIGcHJv", + "dG8z")); + descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, + new pbr::FileDescriptor[] { }, + new pbr::GeneratedClrTypeInfo(null, new pbr::GeneratedClrTypeInfo[] { + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.EventType), global::Akka.Streams.Serialization.Proto.Msg.EventType.Parser, new[]{ "TypeName" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.SinkRef), global::Akka.Streams.Serialization.Proto.Msg.SinkRef.Parser, new[]{ "TargetRef", "EventType" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.SourceRef), global::Akka.Streams.Serialization.Proto.Msg.SourceRef.Parser, new[]{ "OriginRef", "EventType" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.ActorRef), global::Akka.Streams.Serialization.Proto.Msg.ActorRef.Parser, new[]{ "Path" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.Payload), global::Akka.Streams.Serialization.Proto.Msg.Payload.Parser, new[]{ "EnclosedMessage", "SerializerId", "MessageManifest" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.OnSubscribeHandshake), global::Akka.Streams.Serialization.Proto.Msg.OnSubscribeHandshake.Parser, new[]{ "TargetRef" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.CumulativeDemand), global::Akka.Streams.Serialization.Proto.Msg.CumulativeDemand.Parser, new[]{ "SeqNr" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.SequencedOnNext), global::Akka.Streams.Serialization.Proto.Msg.SequencedOnNext.Parser, new[]{ "SeqNr", "Payload" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.RemoteStreamFailure), global::Akka.Streams.Serialization.Proto.Msg.RemoteStreamFailure.Parser, new[]{ "Cause" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.RemoteStreamCompleted), global::Akka.Streams.Serialization.Proto.Msg.RemoteStreamCompleted.Parser, new[]{ "SeqNr" }, null, null, null) + })); + } + #endregion + + } + #region Messages + internal sealed partial class EventType : pb::IMessage { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new EventType()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[0]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public EventType() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public EventType(EventType other) : this() { + typeName_ = other.typeName_; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public EventType Clone() { + return new EventType(this); + } + + /// Field number for the "typeName" field. + public const int TypeNameFieldNumber = 1; + private string typeName_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public string TypeName { + get { return typeName_; } + set { + typeName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) { + return Equals(other as EventType); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(EventType other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (TypeName != other.TypeName) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() { + int hash = 1; + if (TypeName.Length != 0) hash ^= TypeName.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) { + if (TypeName.Length != 0) { + output.WriteRawTag(10); + output.WriteString(TypeName); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() { + int size = 0; + if (TypeName.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(TypeName); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(EventType other) { + if (other == null) { + return; + } + if (other.TypeName.Length != 0) { + TypeName = other.TypeName; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + input.SkipLastField(); + break; + case 10: { + TypeName = input.ReadString(); + break; + } + } + } + } + + } + + internal sealed partial class SinkRef : pb::IMessage { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new SinkRef()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[1]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SinkRef() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SinkRef(SinkRef other) : this() { + TargetRef = other.targetRef_ != null ? other.TargetRef.Clone() : null; + EventType = other.eventType_ != null ? other.EventType.Clone() : null; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SinkRef Clone() { + return new SinkRef(this); + } + + /// Field number for the "targetRef" field. + public const int TargetRefFieldNumber = 1; + private global::Akka.Streams.Serialization.Proto.Msg.ActorRef targetRef_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public global::Akka.Streams.Serialization.Proto.Msg.ActorRef TargetRef { + get { return targetRef_; } + set { + targetRef_ = value; + } + } + + /// Field number for the "eventType" field. + public const int EventTypeFieldNumber = 2; + private global::Akka.Streams.Serialization.Proto.Msg.EventType eventType_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public global::Akka.Streams.Serialization.Proto.Msg.EventType EventType { + get { return eventType_; } + set { + eventType_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) { + return Equals(other as SinkRef); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(SinkRef other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (!object.Equals(TargetRef, other.TargetRef)) return false; + if (!object.Equals(EventType, other.EventType)) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() { + int hash = 1; + if (targetRef_ != null) hash ^= TargetRef.GetHashCode(); + if (eventType_ != null) hash ^= EventType.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) { + if (targetRef_ != null) { + output.WriteRawTag(10); + output.WriteMessage(TargetRef); + } + if (eventType_ != null) { + output.WriteRawTag(18); + output.WriteMessage(EventType); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() { + int size = 0; + if (targetRef_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(TargetRef); + } + if (eventType_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(EventType); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(SinkRef other) { + if (other == null) { + return; + } + if (other.targetRef_ != null) { + if (targetRef_ == null) { + targetRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); + } + TargetRef.MergeFrom(other.TargetRef); + } + if (other.eventType_ != null) { + if (eventType_ == null) { + eventType_ = new global::Akka.Streams.Serialization.Proto.Msg.EventType(); + } + EventType.MergeFrom(other.EventType); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + input.SkipLastField(); + break; + case 10: { + if (targetRef_ == null) { + targetRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); + } + input.ReadMessage(targetRef_); + break; + } + case 18: { + if (eventType_ == null) { + eventType_ = new global::Akka.Streams.Serialization.Proto.Msg.EventType(); + } + input.ReadMessage(eventType_); + break; + } + } + } + } + + } + + internal sealed partial class SourceRef : pb::IMessage { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new SourceRef()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[2]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SourceRef() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SourceRef(SourceRef other) : this() { + OriginRef = other.originRef_ != null ? other.OriginRef.Clone() : null; + EventType = other.eventType_ != null ? other.EventType.Clone() : null; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SourceRef Clone() { + return new SourceRef(this); + } + + /// Field number for the "originRef" field. + public const int OriginRefFieldNumber = 1; + private global::Akka.Streams.Serialization.Proto.Msg.ActorRef originRef_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public global::Akka.Streams.Serialization.Proto.Msg.ActorRef OriginRef { + get { return originRef_; } + set { + originRef_ = value; + } + } + + /// Field number for the "eventType" field. + public const int EventTypeFieldNumber = 2; + private global::Akka.Streams.Serialization.Proto.Msg.EventType eventType_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public global::Akka.Streams.Serialization.Proto.Msg.EventType EventType { + get { return eventType_; } + set { + eventType_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) { + return Equals(other as SourceRef); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(SourceRef other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (!object.Equals(OriginRef, other.OriginRef)) return false; + if (!object.Equals(EventType, other.EventType)) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() { + int hash = 1; + if (originRef_ != null) hash ^= OriginRef.GetHashCode(); + if (eventType_ != null) hash ^= EventType.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) { + if (originRef_ != null) { + output.WriteRawTag(10); + output.WriteMessage(OriginRef); + } + if (eventType_ != null) { + output.WriteRawTag(18); + output.WriteMessage(EventType); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() { + int size = 0; + if (originRef_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(OriginRef); + } + if (eventType_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(EventType); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(SourceRef other) { + if (other == null) { + return; + } + if (other.originRef_ != null) { + if (originRef_ == null) { + originRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); + } + OriginRef.MergeFrom(other.OriginRef); + } + if (other.eventType_ != null) { + if (eventType_ == null) { + eventType_ = new global::Akka.Streams.Serialization.Proto.Msg.EventType(); + } + EventType.MergeFrom(other.EventType); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + input.SkipLastField(); + break; + case 10: { + if (originRef_ == null) { + originRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); + } + input.ReadMessage(originRef_); + break; + } + case 18: { + if (eventType_ == null) { + eventType_ = new global::Akka.Streams.Serialization.Proto.Msg.EventType(); + } + input.ReadMessage(eventType_); + break; + } + } + } + } + + } + + internal sealed partial class ActorRef : pb::IMessage { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new ActorRef()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[3]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public ActorRef() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public ActorRef(ActorRef other) : this() { + path_ = other.path_; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public ActorRef Clone() { + return new ActorRef(this); + } + + /// Field number for the "path" field. + public const int PathFieldNumber = 1; + private string path_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public string Path { + get { return path_; } + set { + path_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) { + return Equals(other as ActorRef); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(ActorRef other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (Path != other.Path) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() { + int hash = 1; + if (Path.Length != 0) hash ^= Path.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) { + if (Path.Length != 0) { + output.WriteRawTag(10); + output.WriteString(Path); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() { + int size = 0; + if (Path.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(Path); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(ActorRef other) { + if (other == null) { + return; + } + if (other.Path.Length != 0) { + Path = other.Path; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + input.SkipLastField(); + break; + case 10: { + Path = input.ReadString(); + break; + } + } + } + } + + } + + internal sealed partial class Payload : pb::IMessage { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Payload()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[4]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public Payload() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public Payload(Payload other) : this() { + enclosedMessage_ = other.enclosedMessage_; + serializerId_ = other.serializerId_; + messageManifest_ = other.messageManifest_; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public Payload Clone() { + return new Payload(this); + } + + /// Field number for the "enclosedMessage" field. + public const int EnclosedMessageFieldNumber = 1; + private pb::ByteString enclosedMessage_ = pb::ByteString.Empty; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public pb::ByteString EnclosedMessage { + get { return enclosedMessage_; } + set { + enclosedMessage_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "serializerId" field. + public const int SerializerIdFieldNumber = 2; + private int serializerId_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int SerializerId { + get { return serializerId_; } + set { + serializerId_ = value; + } + } + + /// Field number for the "messageManifest" field. + public const int MessageManifestFieldNumber = 3; + private pb::ByteString messageManifest_ = pb::ByteString.Empty; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public pb::ByteString MessageManifest { + get { return messageManifest_; } + set { + messageManifest_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) { + return Equals(other as Payload); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(Payload other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (EnclosedMessage != other.EnclosedMessage) return false; + if (SerializerId != other.SerializerId) return false; + if (MessageManifest != other.MessageManifest) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() { + int hash = 1; + if (EnclosedMessage.Length != 0) hash ^= EnclosedMessage.GetHashCode(); + if (SerializerId != 0) hash ^= SerializerId.GetHashCode(); + if (MessageManifest.Length != 0) hash ^= MessageManifest.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) { + if (EnclosedMessage.Length != 0) { + output.WriteRawTag(10); + output.WriteBytes(EnclosedMessage); + } + if (SerializerId != 0) { + output.WriteRawTag(16); + output.WriteInt32(SerializerId); + } + if (MessageManifest.Length != 0) { + output.WriteRawTag(26); + output.WriteBytes(MessageManifest); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() { + int size = 0; + if (EnclosedMessage.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeBytesSize(EnclosedMessage); + } + if (SerializerId != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(SerializerId); + } + if (MessageManifest.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeBytesSize(MessageManifest); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(Payload other) { + if (other == null) { + return; + } + if (other.EnclosedMessage.Length != 0) { + EnclosedMessage = other.EnclosedMessage; + } + if (other.SerializerId != 0) { + SerializerId = other.SerializerId; + } + if (other.MessageManifest.Length != 0) { + MessageManifest = other.MessageManifest; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + input.SkipLastField(); + break; + case 10: { + EnclosedMessage = input.ReadBytes(); + break; + } + case 16: { + SerializerId = input.ReadInt32(); + break; + } + case 26: { + MessageManifest = input.ReadBytes(); + break; + } + } + } + } + + } + + internal sealed partial class OnSubscribeHandshake : pb::IMessage { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new OnSubscribeHandshake()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[5]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public OnSubscribeHandshake() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public OnSubscribeHandshake(OnSubscribeHandshake other) : this() { + TargetRef = other.targetRef_ != null ? other.TargetRef.Clone() : null; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public OnSubscribeHandshake Clone() { + return new OnSubscribeHandshake(this); + } + + /// Field number for the "targetRef" field. + public const int TargetRefFieldNumber = 1; + private global::Akka.Streams.Serialization.Proto.Msg.ActorRef targetRef_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public global::Akka.Streams.Serialization.Proto.Msg.ActorRef TargetRef { + get { return targetRef_; } + set { + targetRef_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) { + return Equals(other as OnSubscribeHandshake); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(OnSubscribeHandshake other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (!object.Equals(TargetRef, other.TargetRef)) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() { + int hash = 1; + if (targetRef_ != null) hash ^= TargetRef.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) { + if (targetRef_ != null) { + output.WriteRawTag(10); + output.WriteMessage(TargetRef); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() { + int size = 0; + if (targetRef_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(TargetRef); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(OnSubscribeHandshake other) { + if (other == null) { + return; + } + if (other.targetRef_ != null) { + if (targetRef_ == null) { + targetRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); + } + TargetRef.MergeFrom(other.TargetRef); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + input.SkipLastField(); + break; + case 10: { + if (targetRef_ == null) { + targetRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); + } + input.ReadMessage(targetRef_); + break; + } + } + } + } + + } + + internal sealed partial class CumulativeDemand : pb::IMessage { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new CumulativeDemand()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[6]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public CumulativeDemand() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public CumulativeDemand(CumulativeDemand other) : this() { + seqNr_ = other.seqNr_; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public CumulativeDemand Clone() { + return new CumulativeDemand(this); + } + + /// Field number for the "seqNr" field. + public const int SeqNrFieldNumber = 1; + private long seqNr_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public long SeqNr { + get { return seqNr_; } + set { + seqNr_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) { + return Equals(other as CumulativeDemand); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(CumulativeDemand other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (SeqNr != other.SeqNr) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() { + int hash = 1; + if (SeqNr != 0L) hash ^= SeqNr.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) { + if (SeqNr != 0L) { + output.WriteRawTag(8); + output.WriteInt64(SeqNr); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() { + int size = 0; + if (SeqNr != 0L) { + size += 1 + pb::CodedOutputStream.ComputeInt64Size(SeqNr); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(CumulativeDemand other) { + if (other == null) { + return; + } + if (other.SeqNr != 0L) { + SeqNr = other.SeqNr; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + input.SkipLastField(); + break; + case 8: { + SeqNr = input.ReadInt64(); + break; + } + } + } + } + + } + + internal sealed partial class SequencedOnNext : pb::IMessage { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new SequencedOnNext()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[7]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SequencedOnNext() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SequencedOnNext(SequencedOnNext other) : this() { + seqNr_ = other.seqNr_; + Payload = other.payload_ != null ? other.Payload.Clone() : null; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SequencedOnNext Clone() { + return new SequencedOnNext(this); + } + + /// Field number for the "seqNr" field. + public const int SeqNrFieldNumber = 1; + private long seqNr_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public long SeqNr { + get { return seqNr_; } + set { + seqNr_ = value; + } + } + + /// Field number for the "payload" field. + public const int PayloadFieldNumber = 2; + private global::Akka.Streams.Serialization.Proto.Msg.Payload payload_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public global::Akka.Streams.Serialization.Proto.Msg.Payload Payload { + get { return payload_; } + set { + payload_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) { + return Equals(other as SequencedOnNext); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(SequencedOnNext other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (SeqNr != other.SeqNr) return false; + if (!object.Equals(Payload, other.Payload)) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() { + int hash = 1; + if (SeqNr != 0L) hash ^= SeqNr.GetHashCode(); + if (payload_ != null) hash ^= Payload.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) { + if (SeqNr != 0L) { + output.WriteRawTag(8); + output.WriteInt64(SeqNr); + } + if (payload_ != null) { + output.WriteRawTag(18); + output.WriteMessage(Payload); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() { + int size = 0; + if (SeqNr != 0L) { + size += 1 + pb::CodedOutputStream.ComputeInt64Size(SeqNr); + } + if (payload_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(Payload); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(SequencedOnNext other) { + if (other == null) { + return; + } + if (other.SeqNr != 0L) { + SeqNr = other.SeqNr; + } + if (other.payload_ != null) { + if (payload_ == null) { + payload_ = new global::Akka.Streams.Serialization.Proto.Msg.Payload(); + } + Payload.MergeFrom(other.Payload); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + input.SkipLastField(); + break; + case 8: { + SeqNr = input.ReadInt64(); + break; + } + case 18: { + if (payload_ == null) { + payload_ = new global::Akka.Streams.Serialization.Proto.Msg.Payload(); + } + input.ReadMessage(payload_); + break; + } + } + } + } + + } + + internal sealed partial class RemoteStreamFailure : pb::IMessage { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new RemoteStreamFailure()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[8]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public RemoteStreamFailure() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public RemoteStreamFailure(RemoteStreamFailure other) : this() { + cause_ = other.cause_; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public RemoteStreamFailure Clone() { + return new RemoteStreamFailure(this); + } + + /// Field number for the "cause" field. + public const int CauseFieldNumber = 1; + private pb::ByteString cause_ = pb::ByteString.Empty; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public pb::ByteString Cause { + get { return cause_; } + set { + cause_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) { + return Equals(other as RemoteStreamFailure); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(RemoteStreamFailure other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (Cause != other.Cause) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() { + int hash = 1; + if (Cause.Length != 0) hash ^= Cause.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) { + if (Cause.Length != 0) { + output.WriteRawTag(10); + output.WriteBytes(Cause); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() { + int size = 0; + if (Cause.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeBytesSize(Cause); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(RemoteStreamFailure other) { + if (other == null) { + return; + } + if (other.Cause.Length != 0) { + Cause = other.Cause; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + input.SkipLastField(); + break; + case 10: { + Cause = input.ReadBytes(); + break; + } + } + } + } + + } + + internal sealed partial class RemoteStreamCompleted : pb::IMessage { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new RemoteStreamCompleted()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[9]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public RemoteStreamCompleted() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public RemoteStreamCompleted(RemoteStreamCompleted other) : this() { + seqNr_ = other.seqNr_; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public RemoteStreamCompleted Clone() { + return new RemoteStreamCompleted(this); + } + + /// Field number for the "seqNr" field. + public const int SeqNrFieldNumber = 1; + private long seqNr_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public long SeqNr { + get { return seqNr_; } + set { + seqNr_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) { + return Equals(other as RemoteStreamCompleted); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(RemoteStreamCompleted other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (SeqNr != other.SeqNr) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() { + int hash = 1; + if (SeqNr != 0L) hash ^= SeqNr.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) { + if (SeqNr != 0L) { + output.WriteRawTag(8); + output.WriteInt64(SeqNr); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() { + int size = 0; + if (SeqNr != 0L) { + size += 1 + pb::CodedOutputStream.ComputeInt64Size(SeqNr); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(RemoteStreamCompleted other) { + if (other == null) { + return; + } + if (other.SeqNr != 0L) { + SeqNr = other.SeqNr; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + input.SkipLastField(); + break; + case 8: { + SeqNr = input.ReadInt64(); + break; + } + } + } + } + + } + + #endregion + +} + +#endregion Designer generated code diff --git a/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs b/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs new file mode 100644 index 00000000000..14196a3188e --- /dev/null +++ b/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs @@ -0,0 +1,235 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Text; +using Akka.Actor; +using Akka.Serialization; +using Akka.Streams.Implementation; +using Akka.Streams.Serialization.Proto.Msg; +using Akka.Util; +using Google.Protobuf; +using Akka.Streams.Dsl; +using CumulativeDemand = Akka.Streams.Dsl.CumulativeDemand; +using OnSubscribeHandshake = Akka.Streams.Dsl.OnSubscribeHandshake; +using RemoteStreamCompleted = Akka.Streams.Dsl.RemoteStreamCompleted; +using RemoteStreamFailure = Akka.Streams.Dsl.RemoteStreamFailure; +using SequencedOnNext = Akka.Streams.Dsl.SequencedOnNext; + +namespace Akka.Streams.Serialization +{ + public sealed class StreamRefSerializer : SerializerWithStringManifest + { + private readonly ExtendedActorSystem _system; + private readonly Akka.Serialization.Serialization _serialization; + + private const string SequencedOnNextManifest = "A"; + private const string CumulativeDemandManifest = "B"; + private const string RemoteSinkFailureManifest = "C"; + private const string RemoteSinkCompletedManifest = "D"; + private const string SourceRefManifest = "E"; + private const string SinkRefManifest = "F"; + private const string OnSubscribeHandshakeManifest = "G"; + + public StreamRefSerializer(ExtendedActorSystem system) : base(system) + { + _system = system; + _serialization = system.Serialization; + } + + public override string Manifest(object o) + { + switch (o) + { + case SequencedOnNext _: return SequencedOnNextManifest; + case CumulativeDemand _: return CumulativeDemandManifest; + case OnSubscribeHandshake _: return OnSubscribeHandshakeManifest; + case RemoteStreamFailure _: return RemoteSinkFailureManifest; + case RemoteStreamCompleted _: return RemoteSinkCompletedManifest; + case SourceRefImpl _: return SourceRefManifest; + case SinkRefImpl _: return SinkRefManifest; + default: throw new ArgumentException($"Unsupported object of type {o.GetType()}", nameof(o)); + } + } + + public override byte[] ToBinary(object o) + { + switch (o) + { + case SequencedOnNext onNext: return SerializeSequencedOnNext(onNext).ToByteArray(); + case CumulativeDemand demand: return SerializeCumulativeDemand(demand).ToByteArray(); + case OnSubscribeHandshake handshake: return SerializeOnSubscribeHandshake(handshake).ToByteArray(); + case RemoteStreamFailure failure: return SerializeRemoteStreamFailure(failure).ToByteArray(); + case RemoteStreamCompleted completed: return SerializeRemoteStreamCompleted(completed).ToByteArray(); + case SourceRefImpl sourceRef: return SerializeSourceRef(sourceRef).ToByteArray(); + case SinkRefImpl sinkRef: return SerializeSinkRef(sinkRef).ToByteArray(); + default: throw new ArgumentException($"Unsupported object of type {o.GetType()}", nameof(o)); + } + } + + public override object FromBinary(byte[] bytes, string manifest) + { + switch (manifest) + { + case SequencedOnNextManifest: return DeserializeSequenceOnNext(bytes); + case CumulativeDemandManifest: return DeserializeCumulativeDemand(bytes); + case OnSubscribeHandshakeManifest: return DeserializeOnSubscribeHandshake(bytes); + case RemoteSinkFailureManifest: return DeserializeRemoteSinkFailure(bytes); + case RemoteSinkCompletedManifest: return DeserializeRemoteSinkCompleted(bytes); + case SourceRefManifest: return DeserializeSourceRef(bytes); + case SinkRefManifest: return DeserializeSinkRef(bytes); + default: throw new ArgumentException($"Unsupported manifest '{manifest}'", nameof(manifest)); + } + } + + private Type TypeFromProto(Proto.Msg.EventType eventType) + { + var typeName = eventType.TypeName; + return Type.GetType(typeName, throwOnError: true); + } + + private Proto.Msg.EventType TypeToProto(Type clrType) => new Proto.Msg.EventType + { + TypeName = clrType.TypeQualifiedName() + }; + + private SinkRefImpl DeserializeSinkRef(byte[] bytes) + { + var sinkRef = SinkRef.Parser.ParseFrom(bytes); + var type = TypeFromProto(sinkRef.EventType); + var targetRef = _system.Provider.ResolveActorRef(sinkRef.TargetRef.Path); + return SinkRefImpl.Create(type, targetRef); + } + + private SourceRefImpl DeserializeSourceRef(byte[] bytes) + { + var sourceRef = SourceRef.Parser.ParseFrom(bytes); + var type = TypeFromProto(sourceRef.EventType); + var originRef = _system.Provider.ResolveActorRef(sourceRef.OriginRef.Path); + return SourceRefImpl.Create(type, originRef); + } + + private RemoteStreamCompleted DeserializeRemoteSinkCompleted(byte[] bytes) + { + var completed = Proto.Msg.RemoteStreamCompleted.Parser.ParseFrom(bytes); + return new RemoteStreamCompleted(completed.SeqNr); + } + + private RemoteStreamFailure DeserializeRemoteSinkFailure(byte[] bytes) + { + var failure = Proto.Msg.RemoteStreamFailure.Parser.ParseFrom(bytes); + var errorMessage = Encoding.UTF8.GetString(failure.Cause.ToByteArray()); + return new RemoteStreamFailure(errorMessage); + } + + private OnSubscribeHandshake DeserializeOnSubscribeHandshake(byte[] bytes) + { + var handshake = Proto.Msg.OnSubscribeHandshake.Parser.ParseFrom(bytes); + var targetRef = _system.Provider.ResolveActorRef(handshake.TargetRef.Path); + return new OnSubscribeHandshake(targetRef); + } + + private CumulativeDemand DeserializeCumulativeDemand(byte[] bytes) + { + var demand = Proto.Msg.CumulativeDemand.Parser.ParseFrom(bytes); + return new CumulativeDemand(demand.SeqNr); + } + + private SequencedOnNext DeserializeSequenceOnNext(byte[] bytes) + { + var onNext = Proto.Msg.SequencedOnNext.Parser.ParseFrom(bytes); + var serializer = _serialization.GetSerializerById(onNext.Payload.SerializerId); + object payload; + if (onNext.Payload.MessageManifest != null) + { + var manifest = Encoding.UTF8.GetString(onNext.Payload.MessageManifest.ToByteArray()); + if (serializer is SerializerWithStringManifest s) + { + payload = s.FromBinary(onNext.Payload.EnclosedMessage.ToByteArray(), manifest); + } + else + { + var type = Type.GetType(manifest, throwOnError: true); + payload = serializer.FromBinary(onNext.Payload.EnclosedMessage.ToByteArray(), type); + } + } + else + { + payload = serializer.FromBinary(onNext.Payload.EnclosedMessage.ToByteArray(), null); + } + + return new SequencedOnNext(onNext.SeqNr, payload); + } + + private ByteString SerializeSinkRef(SinkRefImpl sinkRef) => new SinkRef + { + EventType = TypeToProto(sinkRef.EventType), + TargetRef = new ActorRef + { + Path = Akka.Serialization.Serialization.SerializedActorPath(sinkRef.InitialPartnerRef) + } + }.ToByteString(); + + private ByteString SerializeSourceRef(SourceRefImpl sourceRef) + { + return new SourceRef + { + EventType = TypeToProto(sourceRef.EventType), + OriginRef = new ActorRef + { + Path = Akka.Serialization.Serialization.SerializedActorPath(sourceRef.InitialPartnerRef) + } + }.ToByteString(); + } + + private ByteString SerializeRemoteStreamCompleted(RemoteStreamCompleted completed) => + new Proto.Msg.RemoteStreamCompleted { SeqNr = completed.SeqNr }.ToByteString(); + + private ByteString SerializeRemoteStreamFailure(RemoteStreamFailure failure) => new Proto.Msg.RemoteStreamFailure + { + Cause = ByteString.CopyFromUtf8(failure.Message) + }.ToByteString(); + + private ByteString SerializeOnSubscribeHandshake(OnSubscribeHandshake handshake) => + new Proto.Msg.OnSubscribeHandshake + { + TargetRef = new ActorRef + { Path = Akka.Serialization.Serialization.SerializedActorPath(handshake.TargetRef) } + }.ToByteString(); + + private ByteString SerializeCumulativeDemand(CumulativeDemand demand) => + new Proto.Msg.CumulativeDemand { SeqNr = demand.SeqNr }.ToByteString(); + + private ByteString SerializeSequencedOnNext(SequencedOnNext onNext) + { + var payload = onNext.Payload; + var serializer = _serialization.FindSerializerFor(payload); + string manifest = null; + if (serializer.IncludeManifest) + { + manifest = serializer is SerializerWithStringManifest s + ? s.Manifest(payload) + : payload.GetType().TypeQualifiedName(); + } + + var p = new Payload + { + EnclosedMessage = ByteString.CopyFrom(serializer.ToBinary(payload)), + SerializerId = serializer.Identifier + }; + + if (!string.IsNullOrEmpty(manifest)) + p.MessageManifest = ByteString.CopyFromUtf8(manifest); + + return new Proto.Msg.SequencedOnNext + { + SeqNr = onNext.SeqNr, + Payload = p + }.ToByteString(); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Streams/StreamRefs.cs b/src/core/Akka.Streams/StreamRefs.cs new file mode 100644 index 00000000000..5ad80c7d7a1 --- /dev/null +++ b/src/core/Akka.Streams/StreamRefs.cs @@ -0,0 +1,112 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2015-2016 Lightbend Inc. +// Copyright (C) 2013-2016 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.Pattern; +using Akka.Streams.Dsl; +using Akka.Streams.Implementation; + +namespace Akka.Streams +{ + /// + /// A allows sharing a "reference" to a with others, + /// with the main purpose of crossing a network boundary. Usually obtaining a SinkRef would be done via Actor messaging, + /// in which one system asks a remote one, to accept some data from it, and the remote one decides to accept the + /// request to send data in a back-pressured streaming fashion -- using a sink ref. + /// + /// To create a you have to materialize the that you want to obtain + /// a reference to by attaching it to a . + /// + /// Stream refs can be seen as Reactive Streams over network boundaries. + /// + /// For additional configuration see `reference.conf` as well as . + /// + /// + public interface ISinkRef + { + Sink Sink { get; } + } + + /// + /// A SourceRef allows sharing a "reference" with others, with the main purpose of crossing a network boundary. + /// Usually obtaining a SourceRef would be done via Actor messaging, in which one system asks a remote one, + /// to share some data with it, and the remote one decides to do so in a back-pressured streaming fashion -- using a stream ref. + /// + /// To create a you have to materialize the that you want to + /// obtain a reference to by attaching it to a . + /// + /// Stream refs can be seen as Reactive Streams over network boundaries. + /// + /// For additional configuration see `reference.conf` as well as . + /// + /// + public interface ISourceRef + { + Source Source { get; } + } + + public sealed class TargetRefNotInitializedYetException : IllegalStateException + { + public TargetRefNotInitializedYetException() : base( + "Internal remote target actor ref not yet resolved, yet attempted to send messages to it. " + + "This should not happen due to proper flow-control, please open a ticket on the issue tracker: https://github.com/akkadotnet/akka.net") + { + } + } + + public sealed class StreamRefSubscriptionTimeoutException : IllegalStateException + { + public StreamRefSubscriptionTimeoutException(string message) : base(message) + { + } + } + + public sealed class RemoteStreamRefActorTerminatedException : Exception + { + public RemoteStreamRefActorTerminatedException(string message) : base(message) + { + } + } + + public sealed class InvalidSequenceNumberException : IllegalStateException + { + public long ExpectedSeqNr { get; } + public long GotSeqNr { get; } + + public InvalidSequenceNumberException(long expectedSeqNr, long gotSeqNr, string message) : base( + $"{message} (expected: {expectedSeqNr}, got: {gotSeqNr}). In most cases this means that message loss on this connection has occurred and the stream will fail eagerly.") + { + ExpectedSeqNr = expectedSeqNr; + GotSeqNr = gotSeqNr; + } + } + + /// + /// Stream refs establish a connection between a local and remote actor, representing the origin and remote sides + /// of a stream. Each such actor refers to the other side as its "partner". We make sure that no other actor than + /// the initial partner can send demand/messages to the other side accidentally. + /// + /// This exception is thrown when a message is recived from a non-partner actor, + /// which could mean a bug or some actively malicient behavior from the other side. + /// + /// This is not meant as a security feature, but rather as plain sanity-check. + /// + public sealed class InvalidPartnerActorException : IllegalStateException + { + public IActorRef ExpectedRef { get; } + public IActorRef GotRef { get; } + + public InvalidPartnerActorException(IActorRef expectedRef, IActorRef gotRef, string message) : base( + $"{message} (expected: {expectedRef}, got: {gotRef}). "+ + "This may happen due to 'double-materialization' on the other side of this stream ref. " + + "Do note that stream refs are one-shot references and have to be paired up in 1:1 pairs. " + + "Multi-cast such as broadcast etc can be implemented by sharing multiple new stream references. ") + { + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Streams/reference.conf b/src/core/Akka.Streams/reference.conf index f6815aa6b88..3c48cf4183e 100644 --- a/src/core/Akka.Streams/reference.conf +++ b/src/core/Akka.Streams/reference.conf @@ -83,6 +83,30 @@ akka { # Note: If you change this value also change the fallback value in ActorMaterializerSettings fuzzing-mode = off } + + stream-ref { + # Buffer of a SinkRef that is used to batch Request elements from the other side of the stream ref + # + # The buffer will be attempted to be filled eagerly even while the local stage did not request elements, + # because the delay of requesting over network boundaries is much higher. + buffer-capacity = 32 + + # Demand is signalled by sending a cumulative demand message ("requesting messages until the n-th sequence number) + # Using a cumulative demand model allows us to re-deliver the demand message in case of message loss (which should + # be very rare in any case, yet possible -- mostly under connection break-down and re-establishment). + # + # The semantics of handling and updating the demand however are in-line with what Reactive Streams dictates. + # + # In normal operation, demand is signalled in response to arriving elements, however if no new elements arrive + # within `demand-redelivery-interval` a re-delivery of the demand will be triggered, assuming that it may have gotten lost. + demand-redelivery-interval = 1 second + + # Subscription timeout, during which the "remote side" MUST subscribe (materialize) the handed out stream ref. + # This timeout does not have to be very low in normal situations, since the remote side may also need to + # prepare things before it is ready to materialize the reference. However the timeout is needed to avoid leaking + # in-active streams which are never subscribed to. + subscription-timeout = 30 seconds + } } # Fully qualified config path which holds the dispatcher configuration @@ -102,9 +126,26 @@ akka { } } } - + # configure overrides to ssl-configuration here (to be used by akka-streams, and akka-http – i.e. when serving https connections) ssl-config { protocol = "TLSv1" } + + actor { + + serializers { + akka-stream-ref = "Akka.Streams.Serialization.StreamRefSerializer, Akka.Streams" + } + + serialization-bindings { + "Akka.Streams.Dsl.SinkRefImpl, Akka.Streams" = akka-stream-ref + "Akka.Streams.Dsl.SourceRefImpl, Akka.Streams" = akka-stream-ref + "Akka.Streams.Dsl.IStreamRefsProtocol, Akka.Streams" = akka-stream-ref + } + + serialization-identifiers { + "Akka.Streams.Serialization.StreamRefSerializer, Akka.Streams" = 30 + } + } } diff --git a/src/protobuf/StreamRefMessages.proto b/src/protobuf/StreamRefMessages.proto new file mode 100644 index 00000000000..69a5194a299 --- /dev/null +++ b/src/protobuf/StreamRefMessages.proto @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2009-2017 Lightbend Inc. + * Copyright (C) 2017-2018 Akka.NET project + */ + +syntax = 'proto3'; +package Akka.Streams.Serialization.Proto.Msg; +option optimize_for = SPEED; + +/************************************************* + StreamRefs (SourceRef / SinkRef) related formats +**************************************************/ + +message EventType { + string typeName = 1; +} + +message SinkRef { + ActorRef targetRef = 1; + EventType eventType = 2; +} + +message SourceRef { + ActorRef originRef = 1; + EventType eventType = 2; +} + +message ActorRef { + string path = 1; +} + +message Payload { + bytes enclosedMessage = 1; + int32 serializerId = 2; + bytes messageManifest = 3; +} + +// stream refs protocol + +message OnSubscribeHandshake { + ActorRef targetRef = 1; +} +message CumulativeDemand { + int64 seqNr = 1; +} + +message SequencedOnNext { + int64 seqNr = 1; + Payload payload = 2; +} + +message RemoteStreamFailure { + bytes cause = 1; +} + +message RemoteStreamCompleted { + int64 seqNr = 1; +} \ No newline at end of file From d6f55febbb515c7d6505d3b85bcfff40eceb4075 Mon Sep 17 00:00:00 2001 From: Nick Polideropoulos Date: Sun, 4 Mar 2018 11:00:14 +0200 Subject: [PATCH 21/70] Fixing Issue 3286 (#3342) * Change TaskCompletionSource from TGeneric to Option * update Code and Unit tests to make them build * Fix TcpSpec.Outgoing_TCP_stream_must_properly_full_close_if_requested Unit Test * Fix KeepGoingStageSpec Unit Tests --- .../CoreAPISpec.ApproveStreams.approved.txt | 6 +- .../Akka.Streams.Tests/Dsl/FlowScanSpec.cs | 23 ++++---- .../Dsl/FlowSplitWhenSpec.cs | 9 +-- .../Dsl/GraphStageTimersSpec.cs | 13 +++-- src/core/Akka.Streams.Tests/Dsl/HubSpec.cs | 13 +++-- src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs | 2 +- src/core/Akka.Streams.Tests/IO/TcpSpec.cs | 55 ++++++++++--------- .../Fusing/KeepGoingStageSpec.cs | 13 +++-- src/core/Akka.Streams/Dsl/Source.cs | 4 +- .../Implementation/CompletedPublishers.cs | 14 ++--- .../Akka.Streams/Implementation/Modules.cs | 13 +++-- 11 files changed, 86 insertions(+), 79 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt index 3a4096ae791..e73fff0e617 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt @@ -1610,7 +1610,7 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.Source FromPublisher(Reactive.Streams.IPublisher publisher) { } public static Akka.Streams.Dsl.Source FromTask(System.Threading.Tasks.Task task) { } public static Akka.Streams.Dsl.Source> Lazily(System.Func> create) { } - public static Akka.Streams.Dsl.Source> Maybe() { } + public static Akka.Streams.Dsl.Source>> Maybe() { } public static Akka.Streams.Dsl.Source> Queue(int bufferSize, Akka.Streams.OverflowStrategy overflowStrategy) { } public static Akka.Streams.Dsl.Source Repeat(T element) { } public static Akka.Streams.SourceShape Shape(string name) { } @@ -2832,12 +2832,12 @@ namespace Akka.Streams.Implementation } } [Akka.Annotations.InternalApiAttribute()] - public sealed class MaybeSource : Akka.Streams.Implementation.SourceModule> + public sealed class MaybeSource : Akka.Streams.Implementation.SourceModule>> { public MaybeSource(Akka.Streams.Attributes attributes, Akka.Streams.SourceShape shape) { } public override Akka.Streams.Attributes Attributes { get; } public override Reactive.Streams.IPublisher Create(Akka.Streams.MaterializationContext context, out System.Threading.Tasks.TaskCompletionSource<> materializer) { } - protected override Akka.Streams.Implementation.SourceModule> NewInstance(Akka.Streams.SourceShape shape) { } + protected override Akka.Streams.Implementation.SourceModule>> NewInstance(Akka.Streams.SourceShape shape) { } public override Akka.Streams.Implementation.IModule WithAttributes(Akka.Streams.Attributes attributes) { } } public abstract class Module : Akka.Streams.Implementation.IModule, System.IComparable diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowScanSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowScanSpec.cs index 724d99ff9a9..53f30b0c2b2 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowScanSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowScanSpec.cs @@ -13,6 +13,7 @@ using Akka.Streams.Supervision; using Akka.Streams.TestKit; using Akka.Streams.TestKit.Tests; +using Akka.Streams.Util; using Akka.TestKit; using FluentAssertions; using Xunit; @@ -24,7 +25,7 @@ public class FlowScanSpec : AkkaSpec { private ActorMaterializer Materializer { get; } - public FlowScanSpec(ITestOutputHelper helper):base(helper) + public FlowScanSpec(ITestOutputHelper helper) : base(helper) { var settings = ActorMaterializerSettings.Create(Sys).WithInputBuffer(2, 16); Materializer = ActorMaterializer.Create(Sys, settings); @@ -43,13 +44,13 @@ private IEnumerable Scan(Source source, TimeSpan? duration = t.Wait(duration.Value).Should().BeTrue(); return t.Result; } - + [Fact] public void A_Scan_must_Scan() { Func scan = source => { - var result = new int[source.Length+1]; + var result = new int[source.Length + 1]; result[0] = 0; for (var i = 1; i <= source.Length; i++) @@ -79,19 +80,19 @@ public void A_Scan_must_Scan_empty_failed() [Fact] public void A_Scan_must_Scan_empty() => - this.AssertAllStagesStopped(() => Scan(Source.Empty()).ShouldAllBeEquivalentTo(new[] {0}), Materializer); + this.AssertAllStagesStopped(() => Scan(Source.Empty()).ShouldAllBeEquivalentTo(new[] { 0 }), Materializer); [Fact] public void A_Scan_must_emit_values_promptly() { - var task = Source.Single(1).MapMaterializedValue>(_ => null) + var task = Source.Single(1).MapMaterializedValue>>(_ => null) .Concat(Source.Maybe()) .Scan(0, (i, i1) => i + i1) .Take(2) .RunWith(Sink.Seq(), Materializer); task.Wait(TimeSpan.FromSeconds(1)).Should().BeTrue(); - task.Result.ShouldAllBeEquivalentTo(new[] {0, 1}); + task.Result.ShouldAllBeEquivalentTo(new[] { 0, 1 }); } [Fact] @@ -105,11 +106,11 @@ public void A_Scan_must_restart_properly() return old + current; }).WithAttributes(ActorAttributes.CreateSupervisionStrategy(Deciders.RestartingDecider)); - Source.From(new[] {1, 3, -1, 5, 7}) + Source.From(new[] { 1, 3, -1, 5, 7 }) .Via(scan) .RunWith(this.SinkProbe(), Materializer) .ToStrict(TimeSpan.FromSeconds(1)) - .ShouldAllBeEquivalentTo(new[] {0, 1, 4, 0, 5, 12}); + .ShouldAllBeEquivalentTo(new[] { 0, 1, 4, 0, 5, 12 }); } @@ -124,13 +125,13 @@ public void A_Scan_must_resume_properly() return old + current; }).WithAttributes(ActorAttributes.CreateSupervisionStrategy(Deciders.ResumingDecider)); - Source.From(new[] {1, 3, -1, 5, 7}) + Source.From(new[] { 1, 3, -1, 5, 7 }) .Via(scan) .RunWith(this.SinkProbe(), Materializer) .ToStrict(TimeSpan.FromSeconds(1)) - .ShouldAllBeEquivalentTo(new[] {0, 1, 4, 9, 16}); + .ShouldAllBeEquivalentTo(new[] { 0, 1, 4, 9, 16 }); } - + [Fact] public void A_Scan_must_scan_normally_for_empty_source() { diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowSplitWhenSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowSplitWhenSpec.cs index df07ef134a8..cdd21aa5404 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowSplitWhenSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowSplitWhenSpec.cs @@ -14,6 +14,7 @@ using Akka.Streams.Implementation; using Akka.Streams.TestKit; using Akka.Streams.TestKit.Tests; +using Akka.Streams.Util; using Akka.TestKit; using FluentAssertions; using Reactive.Streams; @@ -49,7 +50,7 @@ public StreamPuppet(IPublisher p, TestKitBase kit) p.Subscribe(_probe); _subscription = _probe.ExpectSubscription(); } - + public void Request(int demand) => _subscription.Request(demand); public void ExpectNext(int element) => _probe.ExpectNext(element); @@ -144,7 +145,7 @@ public void SplitWhen_must_work_when_first_element_is_split_by() WithSubstreamsSupport(1, 3, run: (masterSubscriber, masterSubscription, getSubFlow) => { var s1 = new StreamPuppet(getSubFlow().RunWith(Sink.AsPublisher(false), Materializer), this); - + s1.Request(5); s1.ExpectNext(1); s1.ExpectNext(2); @@ -365,7 +366,7 @@ public void SplitWhen_must_fail_stream_if_substream_not_materialized_in_time() var testSource = Source.Single(1) - .MapMaterializedValue>(_ => null) + .MapMaterializedValue>>(_ => null) .Concat(Source.Maybe()) .SplitWhen(_ => true); Action action = () => @@ -373,7 +374,7 @@ public void SplitWhen_must_fail_stream_if_substream_not_materialized_in_time() var task = testSource.Lift() .Delay(TimeSpan.FromSeconds(1)) - .ConcatMany(s => s.MapMaterializedValue>(_ => null)) + .ConcatMany(s => s.MapMaterializedValue>>(_ => null)) .RunWith(Sink.Ignore(), tightTimeoutMaterializer); task.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); }; diff --git a/src/core/Akka.Streams.Tests/Dsl/GraphStageTimersSpec.cs b/src/core/Akka.Streams.Tests/Dsl/GraphStageTimersSpec.cs index 5b3cb81ba4e..d32763c1dc2 100644 --- a/src/core/Akka.Streams.Tests/Dsl/GraphStageTimersSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/GraphStageTimersSpec.cs @@ -13,6 +13,7 @@ using Akka.Streams.Stage; using Akka.Streams.TestKit; using Akka.Streams.TestKit.Tests; +using Akka.Streams.Util; using Akka.TestKit; using FluentAssertions; using Xunit; @@ -41,7 +42,7 @@ private SideChannel SetupIsolatedStage() .To(Sink.Ignore()) .Run(Materializer); channel.StopPromise = stopPromise; - AwaitCondition(()=>channel.IsReady); + AwaitCondition(() => channel.IsReady); return channel; } @@ -217,7 +218,7 @@ public override bool Equals(object obj) private sealed class SideChannel { public volatile Action AsyncCallback; - public volatile TaskCompletionSource StopPromise; + public volatile TaskCompletionSource> StopPromise; public bool IsReady => AsyncCallback != null; public void Tell(object message) => AsyncCallback(message); @@ -291,7 +292,7 @@ public TestStage(IActorRef probe, SideChannel sideChannel, TestKitBase testKit) protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) => new Logic(this); } - + private sealed class TestStage2 : SimpleLinearGraphStage { private sealed class Logic : TimerGraphStageLogic @@ -304,7 +305,7 @@ public Logic(TestStage2 stage) : base(stage.Shape) { _stage = stage; - SetHandler(stage.Inlet, onPush: DoNothing, + SetHandler(stage.Inlet, onPush: DoNothing, onUpstreamFinish: CompleteStage, onUpstreamFailure: FailStage); @@ -317,9 +318,9 @@ public Logic(TestStage2 stage) : base(stage.Shape) protected internal override void OnTimer(object timerKey) { _tickCount++; - if(IsAvailable(_stage.Outlet)) + if (IsAvailable(_stage.Outlet)) Push(_stage.Outlet, _tickCount); - if(_tickCount == 3) + if (_tickCount == 3) CancelTimer(TimerKey); } } diff --git a/src/core/Akka.Streams.Tests/Dsl/HubSpec.cs b/src/core/Akka.Streams.Tests/Dsl/HubSpec.cs index 6c2e9204b68..894612f3c8d 100644 --- a/src/core/Akka.Streams.Tests/Dsl/HubSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/HubSpec.cs @@ -18,6 +18,7 @@ using FluentAssertions; using Xunit; using Akka.Actor; +using Akka.Streams.Util; using Akka.Util.Internal; using Xunit.Abstractions; @@ -292,7 +293,7 @@ public void BroadcastHub_must_send_the_same_elements_to_consumers_attaching_arou this.AssertAllStagesStopped(() => { var other = Source.From(Enumerable.Range(2, 9)) - .MapMaterializedValue>(_ => null); + .MapMaterializedValue>>(_ => null); var t = Source.Maybe() .Concat(other) .ToMaterialized(BroadcastHub.Sink(8), Keep.Both) @@ -317,7 +318,7 @@ public void BroadcastHub_must_send_the_same_prefix_to_consumers_attaching_around this.AssertAllStagesStopped(() => { var other = Source.From(Enumerable.Range(2, 19)) - .MapMaterializedValue>(_ => null); + .MapMaterializedValue>>(_ => null); var t = Source.Maybe() .Concat(other) .ToMaterialized(BroadcastHub.Sink(8), Keep.Both) @@ -359,7 +360,7 @@ public void BroadcastHub_must_send_the_same_elements_to_consumers_of_different_s this.AssertAllStagesStopped(() => { var other = Source.From(Enumerable.Range(2, 9)) - .MapMaterializedValue>(_ => null); + .MapMaterializedValue>>(_ => null); var t = Source.Maybe() .Concat(other) .ToMaterialized(BroadcastHub.Sink(8), Keep.Both) @@ -385,7 +386,7 @@ public void BroadcastHub_must_send_the_same_elements_to_consumers_of_attaching_a this.AssertAllStagesStopped(() => { var other = Source.From(Enumerable.Range(2, 9)) - .MapMaterializedValue>(_ => null); + .MapMaterializedValue>>(_ => null); var t = Source.Maybe() .Concat(other) .Throttle(1, TimeSpan.FromMilliseconds(10), 3, ThrottleMode.Shaping) @@ -411,7 +412,7 @@ public void BroadcastHub_must_ensure_that_from_two_different_speed_consumers_the this.AssertAllStagesStopped(() => { var other = Source.From(Enumerable.Range(2, 19)) - .MapMaterializedValue>(_ => null); + .MapMaterializedValue>>(_ => null); var t = Source.Maybe() .Concat(other) .ToMaterialized(BroadcastHub.Sink(1), Keep.Both) @@ -441,7 +442,7 @@ public void BroadcastHub_must_send_the_same_elements_to_consumers_attaching_arou this.AssertAllStagesStopped(() => { var other = Source.From(Enumerable.Range(2, 9)) - .MapMaterializedValue>(_ => null); + .MapMaterializedValue>>(_ => null); var t = Source.Maybe() .Concat(other) .ToMaterialized(BroadcastHub.Sink(1), Keep.Both) diff --git a/src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs b/src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs index 7b56feb5df1..40449c0f225 100644 --- a/src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs @@ -107,7 +107,7 @@ public void Maybe_Source_must_complete_materialized_future_with_None_when_stream c.ExpectNoMsg(TimeSpan.FromMilliseconds(300)); subs.Cancel(); - f.Task.AwaitResult().Should().Be(null); + f.Task.AwaitResult().Should().Be(Util.Option.None); }, Materializer); } diff --git a/src/core/Akka.Streams.Tests/IO/TcpSpec.cs b/src/core/Akka.Streams.Tests/IO/TcpSpec.cs index edb664e44e2..9554f913210 100644 --- a/src/core/Akka.Streams.Tests/IO/TcpSpec.cs +++ b/src/core/Akka.Streams.Tests/IO/TcpSpec.cs @@ -18,6 +18,7 @@ using Akka.Streams.Dsl; using Akka.Streams.TestKit; using Akka.Streams.TestKit.Tests; +using Akka.Streams.Util; using Akka.TestKit; using FluentAssertions; using Xunit; @@ -38,7 +39,7 @@ public void Outgoing_TCP_stream_must_work_in_the_happy_case() { this.AssertAllStagesStopped(() => { - var testData = ByteString.FromBytes(new byte[] {1, 2, 3, 4, 5}); + var testData = ByteString.FromBytes(new byte[] { 1, 2, 3, 4, 5 }); var server = new Server(this); @@ -62,7 +63,7 @@ public void Outgoing_TCP_stream_must_work_in_the_happy_case() public void Outgoing_TCP_stream_must_be_able_to_write_a_sequence_of_ByteStrings() { var server = new Server(this); - var testInput = Enumerable.Range(0, 256).Select(i => ByteString.FromBytes(new[] {Convert.ToByte(i)})); + var testInput = Enumerable.Range(0, 256).Select(i => ByteString.FromBytes(new[] { Convert.ToByte(i) })); var expectedOutput = ByteString.FromBytes(Enumerable.Range(0, 256).Select(Convert.ToByte).ToArray()); Source.From(testInput) @@ -74,7 +75,7 @@ public void Outgoing_TCP_stream_must_be_able_to_write_a_sequence_of_ByteStrings( serverConnection.Read(256); serverConnection.WaitRead().ShouldBeEquivalentTo(expectedOutput); } - + [Fact] public async Task Outgoing_TCP_stream_must_be_able_to_read_a_sequence_of_ByteStrings() { @@ -83,7 +84,7 @@ public async Task Outgoing_TCP_stream_must_be_able_to_read_a_sequence_of_ByteStr var testOutput = new byte[255]; for (byte i = 0; i < 255; i++) { - testInput[i] = ByteString.FromBytes(new [] {i}); + testInput[i] = ByteString.FromBytes(new[] { i }); testOutput[i] = i; } @@ -104,7 +105,7 @@ public async Task Outgoing_TCP_stream_must_be_able_to_read_a_sequence_of_ByteStr result.ShouldBe(expectedOutput); } - [Fact(Skip="FIXME .net core / linux")] + [Fact(Skip = "FIXME .net core / linux")] public void Outgoing_TCP_stream_must_fail_the_materialized_task_when_the_connection_fails() { this.AssertAllStagesStopped(() => @@ -169,7 +170,7 @@ public void Outgoing_TCP_stream_must_work_when_remote_closes_write_then_client_c { this.AssertAllStagesStopped(() => { - var testData = ByteString.FromBytes(new byte[] {1, 2, 3, 4, 5}); + var testData = ByteString.FromBytes(new byte[] { 1, 2, 3, 4, 5 }); var server = new Server(this); var tcpWriteProbe = new TcpWriteProbe(this); @@ -298,7 +299,7 @@ public void Outgoing_TCP_stream_must_shut_everything_down_if_client_signals_erro // Server can still write serverConnection.Write(testData); tcpReadProbe.Read(5).ShouldBeEquivalentTo(testData); - + // Client can still write tcpWriteProbe.Write(testData); serverConnection.Read(5); @@ -308,7 +309,7 @@ public void Outgoing_TCP_stream_must_shut_everything_down_if_client_signals_erro tcpWriteProbe.TcpWriteSubscription.Value.SendError(new IllegalStateException("test")); tcpReadProbe.SubscriberProbe.ExpectError(); - serverConnection.ExpectClosed(c=>c.IsErrorClosed); + serverConnection.ExpectClosed(c => c.IsErrorClosed); serverConnection.ExpectTerminated(); }, Materializer); } @@ -341,7 +342,7 @@ public void Outgoing_TCP_stream_must_shut_everything_down_if_client_signals_erro tcpWriteProbe.Write(testData); serverConnection.Read(5); serverConnection.WaitRead().ShouldBeEquivalentTo(testData); - + tcpWriteProbe.TcpWriteSubscription.Value.SendError(new IllegalStateException("test")); serverConnection.ExpectClosed(c => c.IsErrorClosed); serverConnection.ExpectTerminated(); @@ -376,7 +377,7 @@ public void Outgoing_TCP_stream_must_shut_down_both_streams_when_connection_is_a [Fact] public async Task Outgoing_TCP_stream_must_materialize_correctly_when_used_in_multiple_flows() { - var testData = ByteString.FromBytes(new byte[] {1, 2, 3, 4, 5}); + var testData = ByteString.FromBytes(new byte[] { 1, 2, 3, 4, 5 }); var server = new Server(this); var tcpWriteProbe1 = new TcpWriteProbe(this); @@ -399,14 +400,14 @@ public async Task Outgoing_TCP_stream_must_materialize_correctly_when_used_in_mu ValidateServerClientCommunication(testData, serverConnection1, tcpReadProbe1, tcpWriteProbe1); ValidateServerClientCommunication(testData, serverConnection2, tcpReadProbe2, tcpWriteProbe2); - + var conn1 = await conn1F; var conn2 = await conn2F; // Since we have already communicated over the connections we can have short timeouts for the tasks - ((IPEndPoint) conn1.RemoteAddress).Port.Should().Be(((IPEndPoint) server.Address).Port); - ((IPEndPoint) conn2.RemoteAddress).Port.Should().Be(((IPEndPoint) server.Address).Port); - ((IPEndPoint) conn1.LocalAddress).Port.Should().NotBe(((IPEndPoint) conn2.LocalAddress).Port); + ((IPEndPoint)conn1.RemoteAddress).Port.Should().Be(((IPEndPoint)server.Address).Port); + ((IPEndPoint)conn2.RemoteAddress).Port.Should().Be(((IPEndPoint)server.Address).Port); + ((IPEndPoint)conn1.LocalAddress).Port.Should().NotBe(((IPEndPoint)conn2.LocalAddress).Port); tcpWriteProbe1.Close(); tcpReadProbe1.Close(); @@ -423,7 +424,7 @@ public void Outgoing_TCP_stream_must_properly_full_close_if_requested() var writeButIgnoreRead = Flow.FromSinkAndSource(Sink.Ignore(), Source.Single(ByteString.FromString("Early response")), Keep.Right); - var task = + var task = Sys.TcpStream() .Bind(serverAddress.Address.ToString(), serverAddress.Port, halfClose: false) .ToMaterialized( @@ -442,7 +443,7 @@ public void Outgoing_TCP_stream_must_properly_full_close_if_requested() result.Wait(TimeSpan.FromSeconds(5)).Should().BeTrue(); result.Result.ShouldBeEquivalentTo(ByteString.FromString("Early response")); - promise.SetResult(null); // close client upstream, no more data + promise.SetResult(Option.None); // close client upstream, no more data binding.Unbind(); }, Materializer); @@ -461,10 +462,10 @@ public async Task Outgoing_TCP_stream_must_Echo_should_work_even_if_server_is_in .Run(Materializer); var result = await Source.From(Enumerable.Repeat(0, 1000) - .Select(i => ByteString.FromBytes(new[] {Convert.ToByte(i)}))) + .Select(i => ByteString.FromBytes(new[] { Convert.ToByte(i) }))) .Via(Sys.TcpStream().OutgoingConnection(serverAddress, halfClose: true)) .RunAggregate(0, (i, s) => i + s.Count, Materializer); - + result.Should().Be(1000); await binding.Unbind(); @@ -532,7 +533,7 @@ private void ValidateServerClientCommunication(ByteString testData, ServerConnec writeProbe.Write(testData); serverConnection.WaitRead().ShouldBeEquivalentTo(testData); } - + private Sink EchoHandler() => Sink.ForEach(c => c.Flow.Join(Flow.Create()).Run(Materializer)); @@ -550,7 +551,7 @@ public async Task Tcp_listen_stream_must_be_able_to_implement_echo() var echoServerFinish = t.Item2; var testInput = Enumerable.Range(0, 255) - .Select(i => ByteString.FromBytes(new[] {Convert.ToByte(i)})) + .Select(i => ByteString.FromBytes(new[] { Convert.ToByte(i) })) .ToList(); var expectedOutput = testInput.Aggregate(ByteString.Empty, (agg, b) => agg.Concat(b)); @@ -558,7 +559,7 @@ public async Task Tcp_listen_stream_must_be_able_to_implement_echo() var result = await Source.From(testInput) .Via(Sys.TcpStream().OutgoingConnection(serverAddress)) .RunAggregate(ByteString.Empty, (agg, b) => agg.Concat(b), Materializer); - + result.ShouldBeEquivalentTo(expectedOutput); await binding.Unbind(); await echoServerFinish; @@ -591,7 +592,7 @@ public async Task Tcp_listen_stream_must_work_with_a_chain_of_echoes() .Via(echoConnection) .Via(echoConnection) .RunAggregate(ByteString.Empty, (agg, b) => agg.Concat(b), Materializer); - + result.ShouldBeEquivalentTo(expectedOutput); await binding.Unbind(); await echoServerFinish; @@ -619,10 +620,10 @@ public void Tcp_listen_stream_must_bind_and_unbind_correctly() var probe3 = this.CreateManualSubscriberProbe(); var binding3F = bind.To(Sink.FromSubscriber(probe3)).Run(Materializer); probe3.ExpectSubscriptionAndError().Should().BeOfType(); - + binding2F.Invoking(x => x.Wait(TimeSpan.FromSeconds(3))).ShouldThrow(); binding3F.Invoking(x => x.Wait(TimeSpan.FromSeconds(3))).ShouldThrow(); - + // Now unbind first binding1.Unbind().Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); probe1.ExpectComplete(); @@ -675,8 +676,8 @@ public void Tcp_listen_stream_must_not_shut_down_connections_after_the_connectio }, Materializer); } - [Fact(Skip="FIXME")] - public void Tcp_listen_stream_must_shut_down_properly_even_if_some_accepted_connection_Flows_have_not_been_subscribed_to () + [Fact(Skip = "FIXME")] + public void Tcp_listen_stream_must_shut_down_properly_even_if_some_accepted_connection_Flows_have_not_been_subscribed_to() { this.AssertAllStagesStopped(() => { @@ -693,7 +694,7 @@ public void Tcp_listen_stream_must_shut_down_properly_even_if_some_accepted_conn .Via(takeTwoAndDropSecond) .RunForeach(c => c.Flow.Join(Flow.Create()).Run(Materializer), Materializer); - var folder = Source.From(Enumerable.Range(0, 100).Select(_ => ByteString.FromBytes(new byte[] {0}))) + var folder = Source.From(Enumerable.Range(0, 100).Select(_ => ByteString.FromBytes(new byte[] { 0 }))) .Via(Sys.TcpStream().OutgoingConnection(serverAddress)) .Aggregate(0, (i, s) => i + s.Count) .ToMaterialized(Sink.First(), Keep.Right); diff --git a/src/core/Akka.Streams.Tests/Implementation/Fusing/KeepGoingStageSpec.cs b/src/core/Akka.Streams.Tests/Implementation/Fusing/KeepGoingStageSpec.cs index 5dacb3c621f..6c694723a34 100644 --- a/src/core/Akka.Streams.Tests/Implementation/Fusing/KeepGoingStageSpec.cs +++ b/src/core/Akka.Streams.Tests/Implementation/Fusing/KeepGoingStageSpec.cs @@ -11,6 +11,7 @@ using Akka.Streams.Dsl; using Akka.Streams.Stage; using Akka.Streams.TestKit.Tests; +using Akka.Streams.Util; using Akka.TestKit; using FluentAssertions; using Xunit; @@ -126,7 +127,7 @@ public PingableLogic(PingableSink pingable) : base(pingable.Shape) { _pingable = pingable; - SetHandler(_pingable.Shape.Inlet, + SetHandler(_pingable.Shape.Inlet, () => Pull(_pingable.Shape.Inlet), //Ignore finish () => { _listener.Tell(UpstreamCompleted.Instance); }); @@ -213,7 +214,7 @@ public void A_stage_with_keep_going_must_still_be_alive_after_all_ports_have_bee pinger.Ping(); ExpectMsg(); - maybePromise.TrySetResult(0); + maybePromise.TrySetResult(Option.None); ExpectMsg(); ExpectNoMsg(200); @@ -248,7 +249,7 @@ public void A_stage_with_keep_going_must_still_be_alive_after_all_ports_have_bee pinger.Ping(); ExpectMsg(); - maybePromise.TrySetResult(0); + maybePromise.TrySetResult(Option.None); ExpectMsg(); ExpectNoMsg(200); @@ -287,7 +288,7 @@ public void A_stage_with_keep_going_must_still_be_alive_after_all_ports_have_bee pinger.Ping(); ExpectMsg(); - maybePromise.TrySetResult(0); + maybePromise.TrySetResult(Option.None); ExpectMsg(); ExpectNoMsg(200); @@ -300,7 +301,7 @@ public void A_stage_with_keep_going_must_still_be_alive_after_all_ports_have_bee // We need to catch the exception otherwise the test fails // ReSharper disable once EmptyGeneralCatchClause - try { pinger.ThrowEx();} catch { } + try { pinger.ThrowEx(); } catch { } // PostStop should not be concurrent with the event handler. This event here tests this. ExpectMsg(); ExpectMsg(); @@ -328,7 +329,7 @@ public void A_stage_with_keep_going_must_close_down_earls_if_keepAlive_is_not_re pinger.Ping(); ExpectMsg(); - maybePromise.TrySetResult(0); + maybePromise.TrySetResult(Option.None); ExpectMsg(); ExpectMsg(); }, Materializer); diff --git a/src/core/Akka.Streams/Dsl/Source.cs b/src/core/Akka.Streams/Dsl/Source.cs index 95662cbbebd..94b32c20921 100644 --- a/src/core/Akka.Streams/Dsl/Source.cs +++ b/src/core/Akka.Streams/Dsl/Source.cs @@ -584,9 +584,9 @@ public static Source UnfoldInfinite(TState state, /// /// TBD /// TBD - public static Source> Maybe() + public static Source>> Maybe() { - return new Source>( + return new Source>>( new MaybeSource(DefaultAttributes.MaybeSource, new SourceShape(new Outlet("MaybeSource")))); } diff --git a/src/core/Akka.Streams/Implementation/CompletedPublishers.cs b/src/core/Akka.Streams/Implementation/CompletedPublishers.cs index 1124112afdf..2f3949cb456 100644 --- a/src/core/Akka.Streams/Implementation/CompletedPublishers.cs +++ b/src/core/Akka.Streams/Implementation/CompletedPublishers.cs @@ -113,10 +113,10 @@ internal sealed class MaybePublisher : IPublisher private class MaybeSubscription : ISubscription { private readonly ISubscriber _subscriber; - private readonly TaskCompletionSource _promise; + private readonly TaskCompletionSource> _promise; private bool _done; - public MaybeSubscription(ISubscriber subscriber, TaskCompletionSource promise) + public MaybeSubscription(ISubscriber subscriber, TaskCompletionSource> promise) { _subscriber = subscriber; _promise = promise; @@ -131,9 +131,9 @@ public void Request(long n) _done = true; _promise.Task.ContinueWith(t => { - if (!_promise.Task.Result.IsDefaultForType()) + if (!_promise.Task.Result.Equals(Option.None)) { - ReactiveStreamsCompliance.TryOnNext(_subscriber, _promise.Task.Result); + ReactiveStreamsCompliance.TryOnNext(_subscriber, _promise.Task.Result.Value); ReactiveStreamsCompliance.TryOnComplete(_subscriber); } else @@ -145,14 +145,14 @@ public void Request(long n) public void Cancel() { _done = true; - _promise.TrySetResult(default(T)); + _promise.TrySetResult(Option.None); } } /// /// TBD /// - public readonly TaskCompletionSource Promise; + public readonly TaskCompletionSource> Promise; /// /// TBD /// @@ -163,7 +163,7 @@ public void Cancel() /// /// TBD /// TBD - public MaybePublisher(TaskCompletionSource promise, string name) + public MaybePublisher(TaskCompletionSource> promise, string name) { Promise = promise; Name = name; diff --git a/src/core/Akka.Streams/Implementation/Modules.cs b/src/core/Akka.Streams/Implementation/Modules.cs index 93e30b87d4e..b27d4c2e32b 100644 --- a/src/core/Akka.Streams/Implementation/Modules.cs +++ b/src/core/Akka.Streams/Implementation/Modules.cs @@ -10,6 +10,7 @@ using Akka.Actor; using Akka.Annotations; using Akka.Streams.Actors; +using Akka.Streams.Util; using Reactive.Streams; namespace Akka.Streams.Implementation @@ -252,7 +253,7 @@ public override IPublisher Create(MaterializationContext context, out NotU /// /// TBD [InternalApi] - public sealed class MaybeSource : SourceModule> + public sealed class MaybeSource : SourceModule>> { /// /// TBD @@ -282,7 +283,7 @@ public override IModule WithAttributes(Attributes attributes) /// /// TBD /// TBD - protected override SourceModule> NewInstance(SourceShape shape) + protected override SourceModule>> NewInstance(SourceShape shape) => new MaybeSource(Attributes, shape); /// @@ -291,9 +292,9 @@ protected override SourceModule> NewInstance(So /// TBD /// TBD /// TBD - public override IPublisher Create(MaterializationContext context, out TaskCompletionSource materializer) + public override IPublisher Create(MaterializationContext context, out TaskCompletionSource> materializer) { - materializer = new TaskCompletionSource(); + materializer = new TaskCompletionSource>(); return new MaybePublisher(materializer, Attributes.GetNameOrDefault("MaybeSource")); } } @@ -396,7 +397,7 @@ public ActorRefSource(int bufferSize, OverflowStrategy overflowStrategy, Attribu /// /// TBD /// TBD - public override IModule WithAttributes(Attributes attributes) + public override IModule WithAttributes(Attributes attributes) => new ActorRefSource(_bufferSize, _overflowStrategy, attributes, AmendShape(attributes)); /// @@ -404,7 +405,7 @@ public override IModule WithAttributes(Attributes attributes) /// /// TBD /// TBD - protected override SourceModule NewInstance(SourceShape shape) + protected override SourceModule NewInstance(SourceShape shape) => new ActorRefSource(_bufferSize, _overflowStrategy, Attributes, shape); /// From 34dd214cd9c314c217b8b5621c962caf47078aa2 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Mon, 4 Jun 2018 22:04:30 -0500 Subject: [PATCH 22/70] added v1.3.9 placeholder for nightlies --- RELEASE_NOTES.md | 3 +++ src/common.props | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 147890e1035..49092bd036f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,6 @@ +#### 1.3.9 June 04 2018 #### +Placeholder for nightlies. + #### 1.3.8 June 04 2018 #### **Maintenance Release for Akka.NET 1.3** diff --git a/src/common.props b/src/common.props index dc755eb08b6..faceef1c8ae 100644 --- a/src/common.props +++ b/src/common.props @@ -2,7 +2,7 @@ Copyright © 2013-2018 Akka.NET Team Akka.NET Team - 1.3.8 + 1.3.9 http://getakka.net/images/akkalogo.png https://github.com/akkadotnet/akka.net https://github.com/akkadotnet/akka.net/blob/master/LICENSE @@ -17,6 +17,6 @@ true - Placeholder* + Placeholder for nightlies. \ No newline at end of file From 5b9f979a4094cbd569fd4029588ef5d33f392748 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 5 Jun 2018 06:04:47 -0500 Subject: [PATCH 23/70] Revert "Merge branch 'v1.4' into dev" (#3483) This reverts commit 466360c0a095f521e66d42056b09965e99502390, reversing changes made to 84da3d51af23ad1270d4e34a2cfef60dda4acf03. --- build.fsx | 3 +- docs/articles/streams/streamrefs.md | 88 - .../Streams/StreamRefsDocTests.cs | 140 -- docs/images/sink-ref-animation.gif | Bin 96908 -> 0 bytes docs/images/source-ref-animation.gif | Bin 96884 -> 0 bytes src/Akka.sln | 1 - .../CoreAPISpec.ApproveCore.approved.txt | 7 +- .../CoreAPISpec.ApproveStreams.approved.txt | 109 +- src/core/Akka.Streams.TestKit/TestSink.cs | 7 +- .../Akka.Streams.Tests.csproj | 12 +- .../Akka.Streams.Tests/Dsl/FlowScanSpec.cs | 23 +- .../Dsl/FlowSplitWhenSpec.cs | 9 +- .../Dsl/GraphStageTimersSpec.cs | 13 +- src/core/Akka.Streams.Tests/Dsl/HubSpec.cs | 13 +- src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs | 2 +- .../Dsl/StageActorRefSpec.cs | 142 +- .../Akka.Streams.Tests/Dsl/StreamRefsSpec.cs | 405 ----- src/core/Akka.Streams.Tests/IO/TcpSpec.cs | 55 +- .../Fusing/KeepGoingStageSpec.cs | 13 +- src/core/Akka.Streams/ActorMaterializer.cs | 34 +- src/core/Akka.Streams/Akka.Streams.csproj | 4 - src/core/Akka.Streams/Attributes.cs | 28 - src/core/Akka.Streams/Dsl/Source.cs | 4 +- src/core/Akka.Streams/Dsl/StreamRefs.cs | 731 --------- .../ActorRefBackpressureSinkStage.cs | 16 +- .../Akka.Streams/Implementation/Buffers.cs | 2 - .../Implementation/CompletedPublishers.cs | 14 +- .../Implementation/IO/TcpStages.cs | 50 +- .../Akka.Streams/Implementation/Modules.cs | 13 +- .../Proto/StreamRefMessages.g.cs | 1413 ----------------- .../Serialization/StreamRefSerializer.cs | 235 --- src/core/Akka.Streams/Stage/GraphStage.cs | 286 +++- src/core/Akka.Streams/StreamRefs.cs | 112 -- src/core/Akka.Streams/reference.conf | 43 +- src/core/Akka.Tests/Actor/FunctionRefSpec.cs | 253 --- src/core/Akka.Tests/Akka.Tests.csproj | 9 + src/core/Akka/Actor/ActorCell.Children.cs | 72 +- .../Akka/Actor/ActorCell.FaultHandling.cs | 20 +- src/core/Akka/Actor/ActorRef.cs | 200 --- src/core/Akka/Actor/IAutoReceivedMessage.cs | 25 +- src/core/Akka/Actor/RepointableActorRef.cs | 7 +- src/core/Akka/Util/Base64Encoding.cs | 12 +- src/protobuf/StreamRefMessages.proto | 58 - 43 files changed, 486 insertions(+), 4197 deletions(-) delete mode 100644 docs/articles/streams/streamrefs.md delete mode 100644 docs/examples/DocsExamples/Streams/StreamRefsDocTests.cs delete mode 100644 docs/images/sink-ref-animation.gif delete mode 100644 docs/images/source-ref-animation.gif delete mode 100644 src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs delete mode 100644 src/core/Akka.Streams/Dsl/StreamRefs.cs delete mode 100644 src/core/Akka.Streams/Serialization/Proto/StreamRefMessages.g.cs delete mode 100644 src/core/Akka.Streams/Serialization/StreamRefSerializer.cs delete mode 100644 src/core/Akka.Streams/StreamRefs.cs delete mode 100644 src/core/Akka.Tests/Actor/FunctionRefSpec.cs delete mode 100644 src/protobuf/StreamRefMessages.proto diff --git a/build.fsx b/build.fsx index a4cf3a1d15f..44c456b3cc2 100644 --- a/build.fsx +++ b/build.fsx @@ -452,8 +452,7 @@ Target "Protobuf" <| fun _ -> ("DistributedPubSubMessages.proto", "/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/Serialization/Proto/"); ("ClusterShardingMessages.proto", "/src/contrib/cluster/Akka.Cluster.Sharding/Serialization/Proto/"); ("TestConductorProtocol.proto", "/src/core/Akka.Remote.TestKit/Proto/"); - ("Persistence.proto", "/src/core/Akka.Persistence/Serialization/Proto/"); - ("StreamRefMessages.proto", "/src/core/Akka.Streams/Serialization/Proto/")] + ("Persistence.proto", "/src/core/Akka.Persistence/Serialization/Proto/") ] printfn "Using proto.exe: %s" protocPath diff --git a/docs/articles/streams/streamrefs.md b/docs/articles/streams/streamrefs.md deleted file mode 100644 index c63a06bc44a..00000000000 --- a/docs/articles/streams/streamrefs.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -uid: stream-ref -title: StreamRefs - Reactive Streams over the network ---- - -> **Warning** -This module is currently marked as may change in the sense of being the subject of active research. This means that API or semantics can change without warning or deprecation period and it is not recommended to use this module in production just yet—you have been warned. - -Stream references, or “stream refs” for short, allow running Akka Streams across multiple nodes within an Akka Remote boundaries. - -Unlike heavier “streaming data processing” frameworks, Akka Streams are not “deployed” nor automatically distributed. Akka stream refs are, as the name implies, references to existing parts of a stream, and can be used to create a distributed processing framework or introduce such capabilities in specific parts of your application. - -Stream refs are trivial to make use of in existing clustered Akka applications, and require no additional configuration or setup. They automatically maintain flow-control / back-pressure over the network, and employ Akka’s failure detection mechanisms to fail-fast (“let it crash!”) in the case of failures of remote nodes. They can be seen as an implementation of the Work Pulling Pattern, which one would otherwise implement manually. - -> **Note** -A useful way to think about stream refs is: “like an `IActorRef`, but for Akka Streams’s `Source` and `Sink`”. -Stream refs refer to an already existing, possibly remote, `Sink` or `Source`. This is not to be mistaken with deploying streams remotely, which this feature is not intended for. - -## Stream References - -The prime use case for stream refs is to replace raw actor or HTTP messaging between systems where a long running stream of data is expected between two entities. Often times, they can be used to effectively achieve point to point streaming without the need of setting up additional message brokers or similar secondary clusters. - -Stream refs are well suited for any system in which you need to send messages between nodes and need to do so in a flow-controlled fashion. Typical examples include sending work requests to worker nodes, as fast as possible, but not faster than the worker node can process them, or sending data elements which the downstream may be slow at processing. It is recommended to mix and introduce stream refs in Actor messaging based systems, where the actor messaging is used to orchestrate and prepare such message flows, and later the stream refs are used to do the flow-controlled message transfer. - -Stream refs are not persistent, however it is simple to build a recover-able stream by introducing such protocol on the actor messaging layer. Stream refs are absolutely expected to be sent over Akka remoting to other nodes within a cluster, and as such, complement and do not compete with plain Actor messaging. Actors would usually be used to establish the stream, by means of some initial message saying “I want to offer you many log elements (the stream ref)”, or alternatively in the opposite way “If you need to send me much data, here is the stream ref you can use to do so”. - -Since the two sides (“local” and “remote”) of each reference may be confusing to simply refer to as “remote” and “local” – since either side can be seen as “local” or “remote” depending how we look at it – we propose to use the terminology “origin” and “target”, which is defined by where the stream ref was created. For SourceRefs, the “origin” is the side which has the data that it is going to stream out. For SinkRefs the “origin” side is the actor system that is ready to receive the data and has allocated the ref. Those two may be seen as duals of each other, however to explain patterns about sharing references, we found this wording to be rather useful. - -### Source Refs - offering streaming data to a remote system - -A `SourceRef` can be offered to a remote actor system in order for it to consume some source of data that we have prepared locally. - -In order to share a `Source` with a remote endpoint you need to materialize it by running it into the `StreamRefs.SourceRef`. That sink materializes the `ISourceRef` that you can then send to other nodes. Please note that it materializes into a Task so you will have to use the continuation (either `PipeTo` or async/await pattern). - -[!code-csharp[StreamRefsDocTests.cs](../../examples/DocsExamples/Streams/StreamRefsDocTests.cs?name=data-source-actor)] - -The origin actor which creates and owns the `Source` could also perform some validation or additional setup when preparing the source. Once it has handed out the `ISourceRef` the remote side can run it like this: - -[!code-csharp[StreamRefsDocTests.cs](../../examples/DocsExamples/Streams/StreamRefsDocTests.cs?name=source-ref-materialization)] - -The process of preparing and running a `ISourceRef` powered distributed stream is shown by the animation below: - -![source ref](/images/source-ref-animation.gif) - -> **Warning** -A `ISourceRef` is by design “single-shot”. i.e. it may only be materialized once. This is in order to not complicate the mental model what materializing such value would mean. -While stream refs are designed to be single shot, you may use them to mimic multicast scenarios, simply by starting a `Broadcast` stage once, and attaching multiple new streams to it, for each emitting a new stream ref. This way each output of the broadcast is by itself an unique single-shot reference, however they can all be powered using a single `Source` – located before the `Broadcast` stage. - -### Sink Refs - offering to receive streaming data from a remote system - -They can be used to offer the other side the capability to send to the origin side data in a streaming, flow-controlled fashion. The origin here allocates a Sink, which could be as simple as a `Sink.ForEach` or as advanced as a complex sink which streams the incoming data into various other systems (e.g. any of the Alpakka provided Sinks). - -> **Note** -To form a good mental model of `SinkRef`s, you can think of them as being similar to “passive mode” in FTP. - -[!code-csharp[StreamRefsDocTests.cs](../../examples/DocsExamples/Streams/StreamRefsDocTests.cs?name=data-sink-actor)] - -Using the offered `ISinkRef<>` to send data to the origin of the `Sink` is also simple, as we can treat the `ISinkRef<>` just as any other sink and directly runWith or run with it. - -[!code-csharp[StreamRefsDocTests.cs](../../examples/DocsExamples/Streams/StreamRefsDocTests.cs?name=sink-ref-materialization)] - -The process of preparing and running a `ISinkRef<>` powered distributed stream is shown by the animation below: - -![sink ref](/images/sink-ref-animation.gif) - -> **Warning** -A `ISinkRef<>` is *by design* “single-shot”. i.e. it may only be materialized once. This is in order to not complicate the mental model what materializing such value would mean. If you have an use case for building a fan-in operation accepting writes from multiple remote nodes, you can build your `Sink` and prepend it with a `Merge` stage, each time materializing a new `ISinkRef<>` targeting that `Merge`. This has the added benefit of giving you full control how to merge these streams (i.e. by using “merge preferred” or any other variation of the fan-in stages). - -## Configuration - -### Stream reference subscription timeouts - -All stream references have a subscription timeout, which is intended to prevent resource leaks in situations in which a remote node would requests the allocation of many streams yet never actually run them. In order to prevent this, each stream reference has a default timeout (of 30 seconds), after which the origin will abort the stream offer if the target has not materialized the stream ref in time. After the timeout has triggered, materialization of the target side will fail pointing out that the origin is missing. - -Since these timeouts are often very different based on the kind of stream offered, and there can be many different kinds of them in the same application, it is possible to not only configure this setting globally (`akka.stream.materializer.stream-ref.subscription-timeout`), but also via attributes: - -```csharp -Source.Repeat("hello") - .RunWith(StreamRefs.SourceRef() - .AddAttributes(StreamRefAttributes - .SubscriptionTimeout(TimeSpan.FromSeconds(5))) - , materializer); - -StreamRefs.SinkRef() - .AddAttributes(StreamRefAttributes - .SubscriptionTimeout(TimeSpan.FromSeconds(5))) - .RunWith(Sink.Ignore(), materializer); -``` \ No newline at end of file diff --git a/docs/examples/DocsExamples/Streams/StreamRefsDocTests.cs b/docs/examples/DocsExamples/Streams/StreamRefsDocTests.cs deleted file mode 100644 index 380ec2f8754..00000000000 --- a/docs/examples/DocsExamples/Streams/StreamRefsDocTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading.Tasks; -using Akka; -using Akka.Streams; -using Akka.Streams.Dsl; -using Akka.TestKit.Xunit2; -using Xunit; -using Xunit.Abstractions; -using Akka.Actor; -using Akka.IO; -using Akka.Util; -using System.Linq; - -namespace DocsExamples.Streams -{ - public class StreamRefsDocTests : TestKit - { - #region data-source-actor - public sealed class RequestLogs - { - public int StreamId { get; } - - public RequestLogs(int streamId) - { - StreamId = streamId; - } - } - - public sealed class LogsOffer - { - public int StreamId { get; } - public ISourceRef SourceRef { get; } - - public LogsOffer(int streamId, ISourceRef sourceRef) - { - StreamId = streamId; - SourceRef = sourceRef; - } - } - - public class DataSource : ReceiveActor - { - public DataSource() - { - Receive(request => - { - // create a source - StreamLogs(request.StreamId) - // materialize it using stream refs - .RunWith(StreamRefs.SourceRef(), Context.System.Materializer()) - // and send to sender - .PipeTo(Sender, success: sourceRef => new LogsOffer(request.StreamId, sourceRef)); - }); - } - - private Source StreamLogs(int streamId) => - Source.From(Enumerable.Range(1, 100)).Select(i => i.ToString()); - } - #endregion - - #region data-sink-actor - public sealed class PrepareUpload - { - public string Id { get; } - public PrepareUpload(string id) - { - Id = id; - } - } - - public sealed class MeasurementsSinkReady - { - public string Id { get; } - public ISinkRef SinkRef { get; } - public MeasurementsSinkReady(string id, ISinkRef sinkRef) - { - Id = id; - SinkRef = sinkRef; - } - } - - class DataReceiver : ReceiveActor - { - public DataReceiver() - { - Receive(prepare => - { - // obtain a source you want to offer - var sink = LogsSinksFor(prepare.Id); - - // materialize sink ref (remote is source data for us) - StreamRefs.SinkRef() - .To(sink) - .Run(Context.System.Materializer()) - .PipeTo(Sender, success: sinkRef => new MeasurementsSinkReady(prepare.Id, sinkRef)); - }); - } - - private Sink LogsSinksFor(string id) => - Sink.ForEach(Console.WriteLine); - } - #endregion - - private ActorMaterializer Materializer { get; } - - public StreamRefsDocTests(ITestOutputHelper output) - : base("", output) - { - Materializer = Sys.Materializer(); - } - - [Fact] - public async Task SourceRef_must_propagate_source_from_another_system() - { - #region source-ref-materialization - var sourceActor = Sys.ActorOf(Props.Create(), "dataSource"); - - var offer = await sourceActor.Ask(new RequestLogs(1337)); - await offer.SourceRef.Source.RunForeach(Console.WriteLine, Materializer); - #endregion - } - - [Fact] - public async Task SinkRef_must_receive_messages_from_another_system() - { - #region sink-ref-materialization - var receiver = Sys.ActorOf(Props.Create(), "receiver"); - - var ready = await receiver.Ask(new PrepareUpload("id"), timeout: TimeSpan.FromSeconds(30)); - - // stream local metrics to Sink's origin: - Source.From(Enumerable.Range(1, 100)) - .Select(i => i.ToString()) - .RunWith(ready.SinkRef.Sink, Materializer); - #endregion - } - } -} \ No newline at end of file diff --git a/docs/images/sink-ref-animation.gif b/docs/images/sink-ref-animation.gif deleted file mode 100644 index 52e26d2103c04b9350176956f35bed0aede9aa41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96908 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 z 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 diff --git a/docs/images/source-ref-animation.gif b/docs/images/source-ref-animation.gif deleted file mode 100644 index 76fd2c936dc075e821534e11ccf666d3be529b06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96884 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 z 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 + public sealed class Terminated : Akka.Actor.IAutoReceivedMessage, Akka.Actor.INoSerializationVerificationNeeded, Akka.Actor.IPossiblyHarmful, Akka.Event.IDeadLetterSuppression { public Terminated(Akka.Actor.IActorRef actorRef, bool existenceConfirmed, bool addressTerminated) { } public Akka.Actor.IActorRef ActorRef { get; } public bool AddressTerminated { get; } public bool ExistenceConfirmed { get; } - public bool Equals(Akka.Actor.Terminated other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } public override string ToString() { } } public class TerminatedProps : Akka.Actor.Props @@ -4558,7 +4554,6 @@ namespace Akka.Util { public const string Base64Chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+~"; public static string Base64Encode(this long value) { } - public static System.Text.StringBuilder Base64Encode(this long value, System.Text.StringBuilder sb) { } public static string Base64Encode(this string s) { } } public class static BitArrayHelpers diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt index 7cd7a64802f..595d16b2a59 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt @@ -72,11 +72,10 @@ namespace Akka.Streams public readonly int MaxFixedBufferSize; public readonly int MaxInputBufferSize; public readonly int OutputBurstLimit; - public readonly Akka.Streams.Dsl.StreamRefSettings StreamRefSettings; public readonly Akka.Streams.StreamSubscriptionTimeoutSettings SubscriptionTimeoutSettings; public readonly Akka.Streams.Supervision.Decider SupervisionDecider; public readonly int SyncProcessingLimit; - 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 ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferSize, string dispatcher, Akka.Streams.Supervision.Decider supervisionDecider, Akka.Streams.StreamSubscriptionTimeoutSettings subscriptionTimeoutSettings, bool isDebugLogging, int outputBurstLimit, bool isFuzzingMode, bool isAutoFusing, int maxFixedBufferSize, int syncProcessingLimit = 1000) { } public static Akka.Streams.ActorMaterializerSettings Create(Akka.Actor.ActorSystem system) { } public Akka.Streams.ActorMaterializerSettings WithAutoFusing(bool isAutoFusing) { } public Akka.Streams.ActorMaterializerSettings WithDebugLogging(bool isEnabled) { } @@ -84,7 +83,6 @@ namespace Akka.Streams public Akka.Streams.ActorMaterializerSettings WithFuzzingMode(bool isFuzzingMode) { } public Akka.Streams.ActorMaterializerSettings WithInputBuffer(int initialSize, int maxSize) { } public Akka.Streams.ActorMaterializerSettings WithMaxFixedBufferSize(int maxFixedBufferSize) { } - public Akka.Streams.ActorMaterializerSettings WithStreamRefSettings(Akka.Streams.Dsl.StreamRefSettings settings) { } public Akka.Streams.ActorMaterializerSettings WithSubscriptionTimeoutSettings(Akka.Streams.StreamSubscriptionTimeoutSettings settings) { } public Akka.Streams.ActorMaterializerSettings WithSupervisionStrategy(Akka.Streams.Supervision.Decider decider) { } public Akka.Streams.ActorMaterializerSettings WithSyncProcessingLimit(int limit) { } @@ -607,27 +605,11 @@ namespace Akka.Streams { protected InPort() { } } - public sealed class InvalidPartnerActorException : Akka.Pattern.IllegalStateException - { - public InvalidPartnerActorException(Akka.Actor.IActorRef expectedRef, Akka.Actor.IActorRef gotRef, string message) { } - public Akka.Actor.IActorRef ExpectedRef { get; } - public Akka.Actor.IActorRef GotRef { get; } - } - public sealed class InvalidSequenceNumberException : Akka.Pattern.IllegalStateException - { - public InvalidSequenceNumberException(long expectedSeqNr, long gotSeqNr, string message) { } - public long ExpectedSeqNr { get; } - public long GotSeqNr { get; } - } public interface IQueueOfferResult { } public interface ISinkQueue { System.Threading.Tasks.Task> PullAsync(); } - public interface ISinkRef - { - Akka.Streams.Dsl.Sink Sink { get; } - } public interface ISourceQueue { System.Threading.Tasks.Task OfferAsync(T element); @@ -639,10 +621,6 @@ namespace Akka.Streams void Fail(System.Exception ex); new System.Threading.Tasks.Task WatchCompletionAsync(); } - public interface ISourceRef - { - Akka.Streams.Dsl.Source Source { get; } - } public interface ITransformerLike { bool IsComplete { get; } @@ -741,10 +719,6 @@ namespace Akka.Streams public static readonly Akka.Streams.QueueOfferResult.QueueClosed Instance; } } - public sealed class RemoteStreamRefActorTerminatedException : System.Exception - { - public RemoteStreamRefActorTerminatedException(string message) { } - } public abstract class Shape : System.ICloneable { protected Shape() { } @@ -791,20 +765,6 @@ namespace Akka.Streams public StreamLimitReachedException(long max) { } protected StreamLimitReachedException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } } - public class static StreamRefAttributes - { - public static Akka.Streams.Attributes CreateSubscriptionTimeout(System.TimeSpan timeout) { } - public interface IStreamRefAttribute : Akka.Streams.Attributes.IAttribute { } - public sealed class SubscriptionTimeout : Akka.Streams.Attributes.IAttribute, Akka.Streams.StreamRefAttributes.IStreamRefAttribute - { - public SubscriptionTimeout(System.TimeSpan timeout) { } - public System.TimeSpan Timeout { get; } - } - } - public sealed class StreamRefSubscriptionTimeoutException : Akka.Pattern.IllegalStateException - { - public StreamRefSubscriptionTimeoutException(string message) { } - } public sealed class StreamSubscriptionTimeoutSettings : System.IEquatable { public readonly Akka.Streams.StreamSubscriptionTimeoutTerminationMode Mode; @@ -833,10 +793,6 @@ namespace Akka.Streams Propagate = 0, Drain = 1, } - public sealed class TargetRefNotInitializedYetException : Akka.Pattern.IllegalStateException - { - public TargetRefNotInitializedYetException() { } - } public enum ThrottleMode { Shaping = 0, @@ -1675,7 +1631,7 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.Source FromPublisher(Reactive.Streams.IPublisher publisher) { } public static Akka.Streams.Dsl.Source FromTask(System.Threading.Tasks.Task task) { } public static Akka.Streams.Dsl.Source> Lazily(System.Func> create) { } - public static Akka.Streams.Dsl.Source>> Maybe() { } + public static Akka.Streams.Dsl.Source> Maybe() { } public static Akka.Streams.Dsl.Source> Queue(int bufferSize, Akka.Streams.OverflowStrategy overflowStrategy) { } public static Akka.Streams.Dsl.Source Repeat(T element) { } public static Akka.Streams.SourceShape Shape(string name) { } @@ -1808,27 +1764,6 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.Source> FromInputStream(System.Func createInputStream, int chunkSize = 8192) { } public static Akka.Streams.Dsl.Sink> FromOutputStream(System.Func createOutputStream, bool autoFlush = False) { } } - [Akka.Annotations.ApiMayChangeAttribute()] - public class static StreamRefs - { - [Akka.Annotations.ApiMayChangeAttribute()] - public static Akka.Streams.Dsl.Source>> SinkRef() { } - [Akka.Annotations.ApiMayChangeAttribute()] - public static Akka.Streams.Dsl.Sink>> SourceRef() { } - } - public sealed class StreamRefSettings - { - public StreamRefSettings(int bufferCapacity, System.TimeSpan demandRedeliveryInterval, System.TimeSpan subscriptionTimeout) { } - public int BufferCapacity { get; } - public System.TimeSpan DemandRedeliveryInterval { get; } - public string ProductPrefix { get; } - public System.TimeSpan SubscriptionTimeout { get; } - public Akka.Streams.Dsl.StreamRefSettings Copy(System.Nullable bufferCapacity = null, System.Nullable demandRedeliveryInterval = null, System.Nullable subscriptionTimeout = null) { } - public static Akka.Streams.Dsl.StreamRefSettings Create(Akka.Configuration.Config config) { } - public Akka.Streams.Dsl.StreamRefSettings WithBufferCapacity(int value) { } - public Akka.Streams.Dsl.StreamRefSettings WithDemandRedeliveryInterval(System.TimeSpan value) { } - public Akka.Streams.Dsl.StreamRefSettings WithSubscriptionTimeout(System.TimeSpan value) { } - } public abstract class SubFlow : Akka.Streams.Dsl.IFlow { protected SubFlow() { } @@ -2912,12 +2847,12 @@ namespace Akka.Streams.Implementation } } [Akka.Annotations.InternalApiAttribute()] - public sealed class MaybeSource : Akka.Streams.Implementation.SourceModule>> + public sealed class MaybeSource : Akka.Streams.Implementation.SourceModule> { public MaybeSource(Akka.Streams.Attributes attributes, Akka.Streams.SourceShape shape) { } public override Akka.Streams.Attributes Attributes { get; } public override Reactive.Streams.IPublisher Create(Akka.Streams.MaterializationContext context, out System.Threading.Tasks.TaskCompletionSource<> materializer) { } - protected override Akka.Streams.Implementation.SourceModule>> NewInstance(Akka.Streams.SourceShape shape) { } + protected override Akka.Streams.Implementation.SourceModule> NewInstance(Akka.Streams.SourceShape shape) { } public override Akka.Streams.Implementation.IModule WithAttributes(Akka.Streams.Attributes attributes) { } } public abstract class Module : Akka.Streams.Implementation.IModule, System.IComparable @@ -4179,16 +4114,6 @@ namespace Akka.Streams.IO public static Akka.Streams.IO.IOResult Success(long count) { } } } -namespace Akka.Streams.Serialization -{ - public sealed class StreamRefSerializer : Akka.Serialization.SerializerWithStringManifest - { - public StreamRefSerializer(Akka.Actor.ExtendedActorSystem system) { } - public override object FromBinary(byte[] bytes, string manifest) { } - public override string Manifest(object o) { } - public override byte[] ToBinary(object o) { } - } -} namespace Akka.Streams.Stage { [System.ObsoleteAttribute("Please use GraphStage instead. [1.1.2]")] @@ -4282,9 +4207,7 @@ namespace Akka.Streams.Stage public Akka.Event.ILoggingAdapter Log { get; } protected object LogSource { get; } protected Akka.Streams.IMaterializer Materializer { get; } - public Akka.Streams.Stage.StageActor StageActor { get; } - [Akka.Annotations.ApiMayChangeAttribute()] - protected virtual string StageActorName { get; } + public Akka.Streams.Stage.StageActorRef StageActorRef { get; } protected Akka.Streams.IMaterializer SubFusingMaterializer { get; } protected internal void AbortEmitting(Akka.Streams.Outlet outlet) { } protected void AbortReading(Akka.Streams.Inlet inlet) { } @@ -4309,7 +4232,7 @@ namespace Akka.Streams.Stage protected Akka.Streams.Stage.IInHandler GetHandler(Akka.Streams.Inlet inlet) { } protected Akka.Streams.Stage.IOutHandler GetHandler(Akka.Streams.Outlet outlet) { } [Akka.Annotations.ApiMayChangeAttribute()] - protected Akka.Streams.Stage.StageActor GetStageActor(Akka.Streams.Stage.StageActorRef.Receive receive) { } + protected Akka.Streams.Stage.StageActorRef GetStageActorRef(Akka.Streams.Stage.StageActorRef.Receive receive) { } protected internal T Grab(Akka.Streams.Inlet inlet) { } protected bool HasBeenPulled(Akka.Streams.Inlet inlet) { } protected internal bool IsAvailable(Akka.Streams.Inlet inlet) { } @@ -4557,17 +4480,21 @@ namespace Akka.Streams.Stage protected PushStage() { } public virtual Akka.Streams.Stage.ISyncDirective OnPull(Akka.Streams.Stage.IContext context) { } } - public sealed class StageActor + public sealed class StageActorRef : Akka.Actor.MinimalActorRef { - public StageActor(Akka.Streams.ActorMaterializer materializer, System.Func>> getAsyncCallback, Akka.Streams.Stage.StageActorRef.Receive initialReceive, string name = null) { } - public Akka.Actor.IActorRef Ref { get; } - public void Become(Akka.Streams.Stage.StageActorRef.Receive receive) { } - public void Stop() { } + public readonly Akka.Event.ILoggingAdapter Log; + public static readonly Akka.Streams.Implementation.EnumerableActorName Name; + public readonly System.Collections.Immutable.IImmutableSet StageTerminatedTombstone; + public StageActorRef(Akka.Actor.IActorRefProvider provider, Akka.Event.ILoggingAdapter log, System.Func>> getAsyncCallback, Akka.Streams.Stage.StageActorRef.Receive initialReceive, Akka.Actor.ActorPath path) { } + public override bool IsTerminated { get; } + public override Akka.Actor.ActorPath Path { get; } + public override Akka.Actor.IActorRefProvider Provider { get; } + public void Become(Akka.Streams.Stage.StageActorRef.Receive behavior) { } + public override void SendSystemMessage(Akka.Dispatch.SysMsg.ISystemMessage message) { } + public override void Stop() { } + protected override void TellInternal(object message, Akka.Actor.IActorRef sender) { } public void Unwatch(Akka.Actor.IActorRef actorRef) { } public void Watch(Akka.Actor.IActorRef actorRef) { } - } - public class static StageActorRef - { public delegate void Receive(System.Tuple args); } public class StageActorRefNotInitializedException : System.Exception diff --git a/src/core/Akka.Streams.TestKit/TestSink.cs b/src/core/Akka.Streams.TestKit/TestSink.cs index 52aef721225..c7b0a21a852 100644 --- a/src/core/Akka.Streams.TestKit/TestSink.cs +++ b/src/core/Akka.Streams.TestKit/TestSink.cs @@ -5,7 +5,6 @@ // //----------------------------------------------------------------------- -using Akka.Actor; using Akka.Streams.Dsl; using Akka.TestKit; @@ -19,7 +18,9 @@ public static class TestSink /// /// /// - public static Sink> SinkProbe(this TestKitBase testKit) => - new Sink>(new StreamTestKit.ProbeSink(testKit, Attributes.None, new SinkShape(new Inlet("ProbeSink.in")))); + public static Sink> SinkProbe(this TestKitBase testKit) + { + return new Sink>(new StreamTestKit.ProbeSink(testKit, Attributes.None, new SinkShape(new Inlet("ProbeSink.in")))); + } } } diff --git a/src/core/Akka.Streams.Tests/Akka.Streams.Tests.csproj b/src/core/Akka.Streams.Tests/Akka.Streams.Tests.csproj index c4ebd2dac7d..60a7b72c1cf 100644 --- a/src/core/Akka.Streams.Tests/Akka.Streams.Tests.csproj +++ b/src/core/Akka.Streams.Tests/Akka.Streams.Tests.csproj @@ -1,17 +1,19 @@  + Akka.Streams.Tests net452;netcoreapp1.1 + - + @@ -19,27 +21,33 @@ + + + + TRACE;DEBUG;SERIALIZATION;CONFIGURATION;UNSAFE_THREADING;NET452;NET452 $(DefineConstants);SERIALIZATION;CONFIGURATION;UNSAFE_THREADING;AKKAIO + $(DefineConstants);CORECLR + $(DefineConstants);RELEASE - \ No newline at end of file + diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowScanSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowScanSpec.cs index ffa41c30488..6d3f08f7481 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowScanSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowScanSpec.cs @@ -13,7 +13,6 @@ using Akka.Streams.Supervision; using Akka.Streams.TestKit; using Akka.Streams.TestKit.Tests; -using Akka.Streams.Util; using Akka.TestKit; using FluentAssertions; using Xunit; @@ -25,7 +24,7 @@ public class FlowScanSpec : AkkaSpec { private ActorMaterializer Materializer { get; } - public FlowScanSpec(ITestOutputHelper helper) : base(helper) + public FlowScanSpec(ITestOutputHelper helper):base(helper) { var settings = ActorMaterializerSettings.Create(Sys).WithInputBuffer(2, 16); Materializer = ActorMaterializer.Create(Sys, settings); @@ -44,13 +43,13 @@ private IEnumerable Scan(Source source, TimeSpan? duration = t.Wait(duration.Value).Should().BeTrue(); return t.Result; } - + [Fact] public void A_Scan_must_Scan() { Func scan = source => { - var result = new int[source.Length + 1]; + var result = new int[source.Length+1]; result[0] = 0; for (var i = 1; i <= source.Length; i++) @@ -80,19 +79,19 @@ public void A_Scan_must_Scan_empty_failed() [Fact] public void A_Scan_must_Scan_empty() => - this.AssertAllStagesStopped(() => Scan(Source.Empty()).ShouldAllBeEquivalentTo(new[] { 0 }), Materializer); + this.AssertAllStagesStopped(() => Scan(Source.Empty()).ShouldAllBeEquivalentTo(new[] {0}), Materializer); [Fact] public void A_Scan_must_emit_values_promptly() { - var task = Source.Single(1).MapMaterializedValue>>(_ => null) + var task = Source.Single(1).MapMaterializedValue>(_ => null) .Concat(Source.Maybe()) .Scan(0, (i, i1) => i + i1) .Take(2) .RunWith(Sink.Seq(), Materializer); task.Wait(TimeSpan.FromSeconds(1)).Should().BeTrue(); - task.Result.ShouldAllBeEquivalentTo(new[] { 0, 1 }); + task.Result.ShouldAllBeEquivalentTo(new[] {0, 1}); } [Fact] @@ -106,11 +105,11 @@ public void A_Scan_must_restart_properly() return old + current; }).WithAttributes(ActorAttributes.CreateSupervisionStrategy(Deciders.RestartingDecider)); - Source.From(new[] { 1, 3, -1, 5, 7 }) + Source.From(new[] {1, 3, -1, 5, 7}) .Via(scan) .RunWith(this.SinkProbe(), Materializer) .ToStrict(TimeSpan.FromSeconds(1)) - .ShouldAllBeEquivalentTo(new[] { 0, 1, 4, 0, 5, 12 }); + .ShouldAllBeEquivalentTo(new[] {0, 1, 4, 0, 5, 12}); } @@ -125,13 +124,13 @@ public void A_Scan_must_resume_properly() return old + current; }).WithAttributes(ActorAttributes.CreateSupervisionStrategy(Deciders.ResumingDecider)); - Source.From(new[] { 1, 3, -1, 5, 7 }) + Source.From(new[] {1, 3, -1, 5, 7}) .Via(scan) .RunWith(this.SinkProbe(), Materializer) .ToStrict(TimeSpan.FromSeconds(1)) - .ShouldAllBeEquivalentTo(new[] { 0, 1, 4, 9, 16 }); + .ShouldAllBeEquivalentTo(new[] {0, 1, 4, 9, 16}); } - + [Fact] public void A_Scan_must_scan_normally_for_empty_source() { diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowSplitWhenSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowSplitWhenSpec.cs index c4afe91165e..7ca5b5f6c60 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowSplitWhenSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowSplitWhenSpec.cs @@ -14,7 +14,6 @@ using Akka.Streams.Implementation; using Akka.Streams.TestKit; using Akka.Streams.TestKit.Tests; -using Akka.Streams.Util; using Akka.TestKit; using FluentAssertions; using Reactive.Streams; @@ -50,7 +49,7 @@ public StreamPuppet(IPublisher p, TestKitBase kit) p.Subscribe(_probe); _subscription = _probe.ExpectSubscription(); } - + public void Request(int demand) => _subscription.Request(demand); public void ExpectNext(int element) => _probe.ExpectNext(element); @@ -145,7 +144,7 @@ public void SplitWhen_must_work_when_first_element_is_split_by() WithSubstreamsSupport(1, 3, run: (masterSubscriber, masterSubscription, getSubFlow) => { var s1 = new StreamPuppet(getSubFlow().RunWith(Sink.AsPublisher(false), Materializer), this); - + s1.Request(5); s1.ExpectNext(1); s1.ExpectNext(2); @@ -366,7 +365,7 @@ public void SplitWhen_must_fail_stream_if_substream_not_materialized_in_time() var testSource = Source.Single(1) - .MapMaterializedValue>>(_ => null) + .MapMaterializedValue>(_ => null) .Concat(Source.Maybe()) .SplitWhen(_ => true); Action action = () => @@ -374,7 +373,7 @@ public void SplitWhen_must_fail_stream_if_substream_not_materialized_in_time() var task = testSource.Lift() .Delay(TimeSpan.FromSeconds(1)) - .ConcatMany(s => s.MapMaterializedValue>>(_ => null)) + .ConcatMany(s => s.MapMaterializedValue>(_ => null)) .RunWith(Sink.Ignore(), tightTimeoutMaterializer); task.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); }; diff --git a/src/core/Akka.Streams.Tests/Dsl/GraphStageTimersSpec.cs b/src/core/Akka.Streams.Tests/Dsl/GraphStageTimersSpec.cs index 6bf1b2605f7..1eb5bba0bb5 100644 --- a/src/core/Akka.Streams.Tests/Dsl/GraphStageTimersSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/GraphStageTimersSpec.cs @@ -13,7 +13,6 @@ using Akka.Streams.Stage; using Akka.Streams.TestKit; using Akka.Streams.TestKit.Tests; -using Akka.Streams.Util; using Akka.TestKit; using FluentAssertions; using Xunit; @@ -42,7 +41,7 @@ private SideChannel SetupIsolatedStage() .To(Sink.Ignore()) .Run(Materializer); channel.StopPromise = stopPromise; - AwaitCondition(() => channel.IsReady); + AwaitCondition(()=>channel.IsReady); return channel; } @@ -218,7 +217,7 @@ public override bool Equals(object obj) private sealed class SideChannel { public volatile Action AsyncCallback; - public volatile TaskCompletionSource> StopPromise; + public volatile TaskCompletionSource StopPromise; public bool IsReady => AsyncCallback != null; public void Tell(object message) => AsyncCallback(message); @@ -292,7 +291,7 @@ public TestStage(IActorRef probe, SideChannel sideChannel, TestKitBase testKit) protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) => new Logic(this); } - + private sealed class TestStage2 : SimpleLinearGraphStage { private sealed class Logic : TimerGraphStageLogic @@ -305,7 +304,7 @@ public Logic(TestStage2 stage) : base(stage.Shape) { _stage = stage; - SetHandler(stage.Inlet, onPush: DoNothing, + SetHandler(stage.Inlet, onPush: DoNothing, onUpstreamFinish: CompleteStage, onUpstreamFailure: FailStage); @@ -318,9 +317,9 @@ public Logic(TestStage2 stage) : base(stage.Shape) protected internal override void OnTimer(object timerKey) { _tickCount++; - if (IsAvailable(_stage.Outlet)) + if(IsAvailable(_stage.Outlet)) Push(_stage.Outlet, _tickCount); - if (_tickCount == 3) + if(_tickCount == 3) CancelTimer(TimerKey); } } diff --git a/src/core/Akka.Streams.Tests/Dsl/HubSpec.cs b/src/core/Akka.Streams.Tests/Dsl/HubSpec.cs index f7a3821ed5e..fb34210cf7f 100644 --- a/src/core/Akka.Streams.Tests/Dsl/HubSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/HubSpec.cs @@ -18,7 +18,6 @@ using FluentAssertions; using Xunit; using Akka.Actor; -using Akka.Streams.Util; using Akka.Util.Internal; using Xunit.Abstractions; @@ -293,7 +292,7 @@ public void BroadcastHub_must_send_the_same_elements_to_consumers_attaching_arou this.AssertAllStagesStopped(() => { var other = Source.From(Enumerable.Range(2, 9)) - .MapMaterializedValue>>(_ => null); + .MapMaterializedValue>(_ => null); var t = Source.Maybe() .Concat(other) .ToMaterialized(BroadcastHub.Sink(8), Keep.Both) @@ -318,7 +317,7 @@ public void BroadcastHub_must_send_the_same_prefix_to_consumers_attaching_around this.AssertAllStagesStopped(() => { var other = Source.From(Enumerable.Range(2, 19)) - .MapMaterializedValue>>(_ => null); + .MapMaterializedValue>(_ => null); var t = Source.Maybe() .Concat(other) .ToMaterialized(BroadcastHub.Sink(8), Keep.Both) @@ -360,7 +359,7 @@ public void BroadcastHub_must_send_the_same_elements_to_consumers_of_different_s this.AssertAllStagesStopped(() => { var other = Source.From(Enumerable.Range(2, 9)) - .MapMaterializedValue>>(_ => null); + .MapMaterializedValue>(_ => null); var t = Source.Maybe() .Concat(other) .ToMaterialized(BroadcastHub.Sink(8), Keep.Both) @@ -386,7 +385,7 @@ public void BroadcastHub_must_send_the_same_elements_to_consumers_of_attaching_a this.AssertAllStagesStopped(() => { var other = Source.From(Enumerable.Range(2, 9)) - .MapMaterializedValue>>(_ => null); + .MapMaterializedValue>(_ => null); var t = Source.Maybe() .Concat(other) .Throttle(1, TimeSpan.FromMilliseconds(10), 3, ThrottleMode.Shaping) @@ -412,7 +411,7 @@ public void BroadcastHub_must_ensure_that_from_two_different_speed_consumers_the this.AssertAllStagesStopped(() => { var other = Source.From(Enumerable.Range(2, 19)) - .MapMaterializedValue>>(_ => null); + .MapMaterializedValue>(_ => null); var t = Source.Maybe() .Concat(other) .ToMaterialized(BroadcastHub.Sink(1), Keep.Both) @@ -442,7 +441,7 @@ public void BroadcastHub_must_send_the_same_elements_to_consumers_attaching_arou this.AssertAllStagesStopped(() => { var other = Source.From(Enumerable.Range(2, 9)) - .MapMaterializedValue>>(_ => null); + .MapMaterializedValue>(_ => null); var t = Source.Maybe() .Concat(other) .ToMaterialized(BroadcastHub.Sink(1), Keep.Both) diff --git a/src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs b/src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs index 13d41695ccf..56af56fb43f 100644 --- a/src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs @@ -107,7 +107,7 @@ public void Maybe_Source_must_complete_materialized_future_with_None_when_stream c.ExpectNoMsg(TimeSpan.FromMilliseconds(300)); subs.Cancel(); - f.Task.AwaitResult().Should().Be(Util.Option.None); + f.Task.AwaitResult().Should().Be(null); }, Materializer); } diff --git a/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs b/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs index e17fdaf1ee3..8180339e889 100644 --- a/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs @@ -34,23 +34,26 @@ private static GraphStageWithMaterializedValue, Task> SumSta => new SumTestStage(probe); [Fact] - public async Task A_Graph_stage_ActorRef_must_receive_messages() + public void A_Graph_stage_ActorRef_must_receive_messages() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); + var res = t.Item2; var stageRef = ExpectMsg(); stageRef.Tell(new Add(1)); stageRef.Tell(new Add(2)); stageRef.Tell(new Add(3)); - stageRef.Tell(StopNow.Instance); - (await t.Item2).Should().Be(6); + stageRef.Tell(StopNow.Instance); + res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + res.Result.Should().Be(6); } [Fact] - public async Task A_Graph_stage_ActorRef_must_be_able_to_be_replied_to() + public void A_Graph_stage_ActorRef_must_be_able_to_be_replied_to() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); + var res = t.Item2; var stageRef = ExpectMsg(); stageRef.Tell(new AddAndTell(1)); @@ -60,13 +63,15 @@ public async Task A_Graph_stage_ActorRef_must_be_able_to_be_replied_to() ExpectMsg(10); stageRef.Tell(StopNow.Instance); - (await t.Item2).Should().Be(10); + res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + res.Result.Should().Be(10); } [Fact] - public async Task A_Graph_stage_ActorRef_must_yield_the_same_self_ref_each_time() + public void A_Graph_stage_ActorRef_must_yield_the_same_self_ref_each_time() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); + var res = t.Item2; var stageRef = ExpectMsg(); stageRef.Tell(CallInitStageActorRef.Instance); @@ -80,15 +85,16 @@ public async Task A_Graph_stage_ActorRef_must_yield_the_same_self_ref_each_time( ExpectMsg(6); stageRef.Tell(StopNow.Instance); - - (await t.Item2).Should().Be(6); + res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + res.Result.Should().Be(6); } [Fact] - public async Task A_Graph_stage_ActorRef_must_be_watchable() + public void A_Graph_stage_ActorRef_must_be_watchable() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; + var res = t.Item2; var stageRef = ExpectMsg(); Watch(stageRef); @@ -96,15 +102,17 @@ public async Task A_Graph_stage_ActorRef_must_be_watchable() stageRef.Tell(new Add(1)); source.SetResult(0); - (await t.Item2).Should().Be(1); + res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + res.Result.Should().Be(1); ExpectTerminated(stageRef); } [Fact] - public async Task A_Graph_stage_ActorRef_must_be_able_to_become() + public void A_Graph_stage_ActorRef_must_be_able_to_become() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; + var res = t.Item2; var stageRef = ExpectMsg(); Watch(stageRef); @@ -115,24 +123,24 @@ public async Task A_Graph_stage_ActorRef_must_be_able_to_become() ExpectMsg("42"); source.SetResult(0); - (await t.Item2).Should().Be(1); - ExpectTerminated(stageRef); + res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + res.Result.Should().Be(1); } [Fact] - public async Task A_Graph_stage_ActorRef_must_reply_Terminated_when_terminated_stage_is_watched() + public void A_Graph_stage_ActorRef_must_reply_Terminated_when_terminated_stage_is_watched() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; + var res = t.Item2; var stageRef = ExpectMsg(); Watch(stageRef); - stageRef.Tell(new AddAndTell(1)); - ExpectMsg(1); + stageRef.Tell(new Add(1)); source.SetResult(0); - - (await t.Item2).Should().Be(1); + res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + res.Result.Should().Be(1); ExpectTerminated(stageRef); var p = CreateTestProbe(); @@ -141,29 +149,30 @@ public async Task A_Graph_stage_ActorRef_must_reply_Terminated_when_terminated_s } [Fact] - public async Task A_Graph_stage_ActorRef_must_be_unwatchable() + public void A_Graph_stage_ActorRef_must_be_unwatchable() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; + var res = t.Item2; var stageRef = ExpectMsg(); Watch(stageRef); Unwatch(stageRef); - stageRef.Tell(new AddAndTell(1)); - ExpectMsg(1); + stageRef.Tell(new Add(1)); source.SetResult(0); - - (await t.Item2).Should().Be(1); + res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + res.Result.Should().Be(1); ExpectNoMsg(100); } [Fact] - public async Task A_Graph_stage_ActorRef_must_ignore_and_log_warnings_for_PoisonPill_and_Kill_messages() + public void A_Graph_stage_ActorRef_must_ignore_and_log_warnings_for_PoisonPill_and_Kill_messages() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; + var res = t.Item2; var stageRef = ExpectMsg(); stageRef.Tell(new Add(40)); @@ -178,12 +187,12 @@ public async Task A_Graph_stage_ActorRef_must_ignore_and_log_warnings_for_Poison warn.Message.ToString() .Should() .MatchRegex( - " message sent to StageActor\\(akka\\://AkkaSpec/user/StreamSupervisor-[0-9]+/\\$\\$[a-z]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + " message sent to StageActorRef\\(akka\\://AkkaSpec/user/StreamSupervisor-[0-9]+/StageActorRef-[0-9]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); #else warn.Message.ToString() .Should() .MatchRegex( - " message sent to StageActor\\(akka\\://StageActorRefSpec-[0-9]+/user/StreamSupervisor-[0-9]+/\\$\\$[a-z]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + " message sent to StageActorRef\\(akka\\://StageActorRefSpec-[0-9]+/user/StreamSupervisor-[0-9]+/StageActorRef-[0-9]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); #endif stageRef.Tell(Kill.Instance); warn = ExpectMsg(TimeSpan.FromSeconds(1)); @@ -192,17 +201,36 @@ public async Task A_Graph_stage_ActorRef_must_ignore_and_log_warnings_for_Poison warn.Message.ToString() .Should() .MatchRegex( - " message sent to StageActor\\(akka\\://AkkaSpec/user/StreamSupervisor-[0-9]+/\\$\\$[a-z]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + " message sent to StageActorRef\\(akka\\://AkkaSpec/user/StreamSupervisor-[0-9]+/StageActorRef-[0-9]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); #else warn.Message.ToString() .Should() .MatchRegex( - " message sent to StageActor\\(akka\\://StageActorRefSpec-[0-9]+/user/StreamSupervisor-[0-9]+/\\$\\$[a-z]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + " message sent to StageActorRef\\(akka\\://StageActorRefSpec-[0-9]+/user/StreamSupervisor-[0-9]+/StageActorRef-[0-9]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); #endif source.SetResult(2); - - (await t.Item2).Should().Be(42); + res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + res.Result.Should().Be(42); + } + + [Fact] + public void A_Graph_stage_ActorRef_must_be_able_to_watch_other_actors() + { + var killMe = ActorOf(dsl => { }, "KilMe"); + var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); + var source = t.Item1; + var res = t.Item2; + + var stageRef = ExpectMsg(); + stageRef.Tell(new WatchMe(killMe)); + stageRef.Tell(new Add(1)); + killMe.Tell(PoisonPill.Instance); + ExpectMsg().Watchee.Should().Be(killMe); + + source.SetResult(0); + res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + res.Result.Should().Be(1); } private sealed class Add @@ -243,6 +271,24 @@ private sealed class StopNow public static readonly StopNow Instance = new StopNow(); private StopNow() { } } + private sealed class WatchMe + { + public WatchMe(IActorRef watchee) + { + Watchee = watchee; + } + + public IActorRef Watchee { get; } + } + private sealed class WatcheeTerminated + { + public WatcheeTerminated(IActorRef watchee) + { + Watchee = watchee; + } + + public IActorRef Watchee { get; } + } private class SumTestStage : GraphStageWithMaterializedValue, Task> { @@ -255,7 +301,7 @@ private class Logic : GraphStageLogic private readonly SumTestStage _stage; private readonly TaskCompletionSource _promise; private int _sum; - private IActorRef Self => StageActor.Ref; + private StageActorRef _self; public Logic(SumTestStage stage, TaskCompletionSource promise) : base(stage.Shape) { @@ -281,8 +327,8 @@ public Logic(SumTestStage stage, TaskCompletionSource promise) : base(stage public override void PreStart() { Pull(_stage._inlet); - GetStageActor(Behaviour); - _stage._probe.Tell(Self); + _self = GetStageActorRef(Behaviour); + _stage._probe.Tell(_self); } private void Behaviour(Tuple args) @@ -290,26 +336,22 @@ private void Behaviour(Tuple args) var msg = args.Item2; var sender = args.Item1; - switch (msg) - { - case Add add: _sum += add.N; break; - case PullNow _: Pull(_stage._inlet); break; - case CallInitStageActorRef _: sender.Tell(GetStageActor(Behaviour).Ref, Self); break; - case BecomeStringEcho _: GetStageActor(tuple => + msg.Match() + .With(a => _sum += a.N) + .With(() => Pull(_stage._inlet)) + .With(() => sender.Tell(GetStageActorRef(Behaviour), _self)) + .With(() => GetStageActorRef(tuple => tuple.Item1.Tell(tuple.Item2.ToString()))) + .With(() => { - var theSender = tuple.Item1; - var theMsg = tuple.Item2; - theSender.Tell(theMsg.ToString(), Self); - }); break; - case StopNow _: _promise.TrySetResult(_sum); CompleteStage(); - break; - case AddAndTell addAndTell: - _sum += addAndTell.N; - sender.Tell(_sum, Self); - break; - } + }).With(a => + { + _sum += a.N; + sender.Tell(_sum, _self); + }) + .With(w => _self.Watch(w.Watchee)) + .With(t => _stage._probe.Tell(new WatcheeTerminated(t.ActorRef))); } } #endregion diff --git a/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs b/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs deleted file mode 100644 index 87ced56c8d7..00000000000 --- a/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs +++ /dev/null @@ -1,405 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2018 Lightbend Inc. -// Copyright (C) 2013-2018 .NET Foundation -// -//----------------------------------------------------------------------- - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Akka.Actor; -using Akka.Actor.Internal; -using Akka.Configuration; -using Akka.IO; -using Akka.Streams.Dsl; -using Akka.Streams.Implementation; -using Akka.Streams.TestKit; -using Akka.TestKit; -using Xunit; -using Xunit.Abstractions; -using FluentAssertions; - -namespace Akka.Streams.Tests -{ - internal sealed class DataSourceActor : ActorBase - { - public static Props Props(IActorRef probe) => - Akka.Actor.Props.Create(() => new DataSourceActor(probe));//.WithDispatcher("akka.test.stream-dispatcher"); - - private readonly IActorRef _probe; - private readonly ActorMaterializer _materializer; - - public DataSourceActor(IActorRef probe) - { - _probe = probe; - _materializer = Context.System.Materializer(); - } - - protected override void PostStop() - { - base.PostStop(); - _materializer.Dispose(); - } - - protected override bool Receive(object message) - { - switch (message) - { - case "give": - { - /* - * Here we're able to send a source to a remote recipient - * For them it's a Source; for us it is a Sink we run data "into" - */ - var source = Source.From(new[] { "hello", "world" }); - var aref = source.RunWith(StreamRefs.SourceRef(), _materializer); - aref.PipeTo(Sender); - return true; - } - case "give-infinite": - { - var source = Source.From(Enumerable.Range(1, int.MaxValue).Select(i => "ping-" + i)); - var t = source.ToMaterialized(StreamRefs.SourceRef(), Keep.Right).Run(_materializer); - t.PipeTo(Sender); - return true; - } - case "give-fail": - { - var r = Source.Failed(new Exception("Boom!")) - .RunWith(StreamRefs.SourceRef(), _materializer); - r.PipeTo(Sender); - return true; - } - case "give-complete-asap": - { - var r = Source.Empty().RunWith(StreamRefs.SourceRef(), _materializer); - r.PipeTo(Sender); - return true; - } - case "give-subscribe-timeout": - { - var r = Source.Repeat("is anyone there?") - .ToMaterialized(StreamRefs.SourceRef(), Keep.Right) - .WithAttributes(StreamRefAttributes.CreateSubscriptionTimeout(TimeSpan.FromMilliseconds(500))) - .Run(_materializer); - r.PipeTo(Sender); - return true; - } - case "receive": - { - /* - * We write out code, knowing that the other side will stream the data into it. - * For them it's a Sink; for us it's a Source. - */ - var sink = StreamRefs.SinkRef().To(Sink.ActorRef(_probe, "")) - .Run(_materializer); - sink.PipeTo(Sender); - return true; - } - case "receive-subscribe-timeout": - { - var sink = StreamRefs.SinkRef() - .WithAttributes(StreamRefAttributes.CreateSubscriptionTimeout(TimeSpan.FromMilliseconds(500))) - .To(Sink.ActorRef(_probe, "")) - .Run(_materializer); - sink.PipeTo(Sender); - return true; - } - case "receive-32": - { -// var t = StreamRefs.SinkRef() -// .ToMaterialized(TestSink.SinkProbe(Context.System), Keep.Both) -// .Run(_materializer); -// -// var sink = t.Item1; -// var driver = t.Item2; -// Task.Run(() => -// { -// driver.EnsureSubscription(); -// driver.Request(2); -// driver.ExpectNext(); -// driver.ExpectNext(); -// driver.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); -// driver.Request(30); -// driver.ExpectNextN(30); -// -// return ""; -// }).PipeTo(_probe); - - return true; - } - default: return false; - } - } - } - - internal sealed class SourceMsg - { - public ISourceRef DataSource { get; } - - public SourceMsg(ISourceRef dataSource) - { - DataSource = dataSource; - } - } - - internal sealed class BulkSourceMsg - { - public ISourceRef DataSource { get; } - - public BulkSourceMsg(ISourceRef dataSource) - { - DataSource = dataSource; - } - } - - internal sealed class SinkMsg - { - public ISinkRef DataSink { get; } - - public SinkMsg(ISinkRef dataSink) - { - DataSink = dataSink; - } - } - - internal sealed class BulkSinkMsg - { - public ISinkRef DataSink { get; } - - public BulkSinkMsg(ISinkRef dataSink) - { - DataSink = dataSink; - } - } - - public class StreamRefsSpec : AkkaSpec - { - public static Config Config() - { - var address = TestUtils.TemporaryServerAddress(); - return ConfigurationFactory.ParseString($@" - akka {{ - loglevel = INFO - actor {{ - provider = remote - serialize-messages = off - }} - remote.dot-netty.tcp {{ - port = {address.Port} - hostname = ""{address.Address}"" - }} - }}").WithFallback(ConfigurationFactory.Load()); - } - - public StreamRefsSpec(ITestOutputHelper output) : this(Config(), output: output) - { - } - - protected StreamRefsSpec(Config config, ITestOutputHelper output = null) : base(config, output) - { - Materializer = Sys.Materializer(); - RemoteSystem = ActorSystem.Create("remote-system", Config()); - InitializeLogger(RemoteSystem); - _probe = CreateTestProbe(); - - var it = RemoteSystem.ActorOf(DataSourceActor.Props(_probe.Ref), "remoteActor"); - var remoteAddress = ((ActorSystemImpl)RemoteSystem).Provider.DefaultAddress; - Sys.ActorSelection(it.Path.ToStringWithAddress(remoteAddress)).Tell(new Identify("hi")); - - _remoteActor = ExpectMsg().Subject; - } - - protected readonly ActorSystem RemoteSystem; - protected readonly ActorMaterializer Materializer; - private readonly TestProbe _probe; - private readonly IActorRef _remoteActor; - - protected override void BeforeTermination() - { - base.BeforeTermination(); - RemoteSystem.Dispose(); - Materializer.Dispose(); - } - - [Fact] - public void SourceRef_must_send_messages_via_remoting() - { - _remoteActor.Tell("give"); - var sourceRef = ExpectMsg>(); - - sourceRef.Source.RunWith(Sink.ActorRef(_probe.Ref, ""), Materializer); - - _probe.ExpectMsg("hello"); - _probe.ExpectMsg("world"); - _probe.ExpectMsg(""); - } - - [Fact] - public void SourceRef_must_fail_when_remote_source_failed() - { - _remoteActor.Tell("give-fail"); - var sourceRef = ExpectMsg>(); - - sourceRef.Source.RunWith(Sink.ActorRef(_probe.Ref, ""), Materializer); - - var f = _probe.ExpectMsg(); - f.Cause.Message.Should().Contain("Remote stream ("); - f.Cause.Message.Should().Contain("Boom!"); - } - - [Fact] - public void SourceRef_must_complete_properly_when_remote_source_is_empty() - { - // this is a special case since it makes sure that the remote stage is still there when we connect to it - _remoteActor.Tell("give-complete-asap"); - var sourceRef = ExpectMsg>(); - - sourceRef.Source.RunWith(Sink.ActorRef(_probe.Ref, ""), Materializer); - - _probe.ExpectMsg(""); - } - - [Fact] - public void SourceRef_must_respect_backpressure_from_implied_by_target_Sink() - { - _remoteActor.Tell("give-infinite"); - var sourceRef = ExpectMsg>(); - - var probe = sourceRef.Source.RunWith(this.SinkProbe(), Materializer); - - probe.EnsureSubscription(); - probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); - - probe.Request(1); - probe.ExpectNext("ping-1"); - probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); - - probe.Request(20); - probe.ExpectNextN(Enumerable.Range(1, 20).Select(i => "ping-" + (i + 1))); - probe.Cancel(); - - // since no demand anyway - probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); - - // should not cause more pulling, since we issued a cancel already - probe.Request(10); - probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); - } - - [Fact] - public void SourceRef_must_receive_timeout_if_subscribing_too_late_to_the_source_ref() - { - _remoteActor.Tell("give-subscribe-timeout"); - var sourceRef = ExpectMsg>(); - - - // not materializing it, awaiting the timeout... - Thread.Sleep(800); - - var probe = sourceRef.Source.RunWith(this.SinkProbe(), Materializer); - - // the local "remote sink" should cancel, since it should notice the origin target actor is dead - probe.EnsureSubscription(); - var ex = probe.ExpectError(); - ex.Message.Should().Contain("has terminated! Tearing down this side of the stream as well."); - } - - [Fact] - public void SinkRef_must_receive_elements_via_remoting() - { - _remoteActor.Tell("receive"); - var remoteSink = ExpectMsg>(); - - Source.From(new[] { "hello", "world" }) - .To(remoteSink.Sink) - .Run(Materializer); - - _probe.ExpectMsg("hello"); - _probe.ExpectMsg("world"); - _probe.ExpectMsg(""); - } - - [Fact] - public void SinkRef_must_fail_origin_if_remote_Sink_gets_a_failure() - { - _remoteActor.Tell("receive"); - var remoteSink = ExpectMsg>(); - - Source.Failed(new Exception("Boom!")) - .To(remoteSink.Sink) - .Run(Materializer); - - var failure = _probe.ExpectMsg(); - failure.Cause.Message.Should().Contain("Remote stream ("); - failure.Cause.Message.Should().Contain("Boom!"); - } - - [Fact] - public void SinkRef_must_receive_hundreds_of_elements_via_remoting() - { - _remoteActor.Tell("receive"); - var remoteSink = ExpectMsg>(); - - var msgs = Enumerable.Range(1, 100).Select(i => "payload-" + i).ToArray(); - - Source.From(msgs).RunWith(remoteSink.Sink, Materializer); - - foreach (var msg in msgs) - { - _probe.ExpectMsg(msg); - } - - _probe.ExpectMsg(""); - } - - [Fact] - public void SinkRef_must_receive_timeout_if_subscribing_too_late_to_the_sink_ref() - { - _remoteActor.Tell("receive-subscribe-timeout"); - var remoteSink = ExpectMsg>(); - - // not materializing it, awaiting the timeout... - Thread.Sleep(800); - - var probe = this.SourceProbe().To(remoteSink.Sink).Run(Materializer); - - var failure = _probe.ExpectMsg(); - failure.Cause.Message.Should().Contain("Remote side did not subscribe (materialize) handed out Sink reference"); - - // the local "remote sink" should cancel, since it should notice the origin target actor is dead - probe.ExpectCancellation(); - } - - [Fact(Skip="FIXME: how to pass test assertions to remote system?")] - public void SinkRef_must_respect_backpressure_implied_by_origin_Sink() - { - _remoteActor.Tell("receive-32"); - var sinkRef = ExpectMsg>(); - - Source.Repeat("hello").RunWith(sinkRef.Sink, Materializer); - - // if we get this message, it means no checks in the request/expect semantics were broken, good! - _probe.ExpectMsg(""); - } - - [Fact] - public void SinkRef_must_not_allow_materializing_multiple_times() - { - _remoteActor.Tell("receive-subscribe-timeout"); - var sinkRef = ExpectMsg>(); - - var p1 = this.SourceProbe().To(sinkRef.Sink).Run(Materializer); - var p2 = this.SourceProbe().To(sinkRef.Sink).Run(Materializer); - - p1.EnsureSubscription(); - var req = p1.ExpectRequest(); - - // will be cancelled immediately, since it's 2nd: - p2.EnsureSubscription(); - p2.ExpectCancellation(); - } - } -} \ No newline at end of file diff --git a/src/core/Akka.Streams.Tests/IO/TcpSpec.cs b/src/core/Akka.Streams.Tests/IO/TcpSpec.cs index fce7275697d..78beaebbcc8 100644 --- a/src/core/Akka.Streams.Tests/IO/TcpSpec.cs +++ b/src/core/Akka.Streams.Tests/IO/TcpSpec.cs @@ -18,7 +18,6 @@ using Akka.Streams.Dsl; using Akka.Streams.TestKit; using Akka.Streams.TestKit.Tests; -using Akka.Streams.Util; using Akka.TestKit; using FluentAssertions; using Xunit; @@ -39,7 +38,7 @@ public void Outgoing_TCP_stream_must_work_in_the_happy_case() { this.AssertAllStagesStopped(() => { - var testData = ByteString.FromBytes(new byte[] { 1, 2, 3, 4, 5 }); + var testData = ByteString.FromBytes(new byte[] {1, 2, 3, 4, 5}); var server = new Server(this); @@ -63,7 +62,7 @@ public void Outgoing_TCP_stream_must_work_in_the_happy_case() public void Outgoing_TCP_stream_must_be_able_to_write_a_sequence_of_ByteStrings() { var server = new Server(this); - var testInput = Enumerable.Range(0, 256).Select(i => ByteString.FromBytes(new[] { Convert.ToByte(i) })); + var testInput = Enumerable.Range(0, 256).Select(i => ByteString.FromBytes(new[] {Convert.ToByte(i)})); var expectedOutput = ByteString.FromBytes(Enumerable.Range(0, 256).Select(Convert.ToByte).ToArray()); Source.From(testInput) @@ -75,7 +74,7 @@ public void Outgoing_TCP_stream_must_be_able_to_write_a_sequence_of_ByteStrings( serverConnection.Read(256); serverConnection.WaitRead().ShouldBeEquivalentTo(expectedOutput); } - + [Fact] public async Task Outgoing_TCP_stream_must_be_able_to_read_a_sequence_of_ByteStrings() { @@ -84,7 +83,7 @@ public async Task Outgoing_TCP_stream_must_be_able_to_read_a_sequence_of_ByteStr var testOutput = new byte[255]; for (byte i = 0; i < 255; i++) { - testInput[i] = ByteString.FromBytes(new[] { i }); + testInput[i] = ByteString.FromBytes(new [] {i}); testOutput[i] = i; } @@ -105,7 +104,7 @@ public async Task Outgoing_TCP_stream_must_be_able_to_read_a_sequence_of_ByteStr result.ShouldBe(expectedOutput); } - [Fact(Skip = "FIXME .net core / linux")] + [Fact(Skip="FIXME .net core / linux")] public void Outgoing_TCP_stream_must_fail_the_materialized_task_when_the_connection_fails() { this.AssertAllStagesStopped(() => @@ -170,7 +169,7 @@ public void Outgoing_TCP_stream_must_work_when_remote_closes_write_then_client_c { this.AssertAllStagesStopped(() => { - var testData = ByteString.FromBytes(new byte[] { 1, 2, 3, 4, 5 }); + var testData = ByteString.FromBytes(new byte[] {1, 2, 3, 4, 5}); var server = new Server(this); var tcpWriteProbe = new TcpWriteProbe(this); @@ -299,7 +298,7 @@ public void Outgoing_TCP_stream_must_shut_everything_down_if_client_signals_erro // Server can still write serverConnection.Write(testData); tcpReadProbe.Read(5).ShouldBeEquivalentTo(testData); - + // Client can still write tcpWriteProbe.Write(testData); serverConnection.Read(5); @@ -309,7 +308,7 @@ public void Outgoing_TCP_stream_must_shut_everything_down_if_client_signals_erro tcpWriteProbe.TcpWriteSubscription.Value.SendError(new IllegalStateException("test")); tcpReadProbe.SubscriberProbe.ExpectError(); - serverConnection.ExpectClosed(c => c.IsErrorClosed); + serverConnection.ExpectClosed(c=>c.IsErrorClosed); serverConnection.ExpectTerminated(); }, Materializer); } @@ -342,7 +341,7 @@ public void Outgoing_TCP_stream_must_shut_everything_down_if_client_signals_erro tcpWriteProbe.Write(testData); serverConnection.Read(5); serverConnection.WaitRead().ShouldBeEquivalentTo(testData); - + tcpWriteProbe.TcpWriteSubscription.Value.SendError(new IllegalStateException("test")); serverConnection.ExpectClosed(c => c.IsErrorClosed); serverConnection.ExpectTerminated(); @@ -377,7 +376,7 @@ public void Outgoing_TCP_stream_must_shut_down_both_streams_when_connection_is_a [Fact] public async Task Outgoing_TCP_stream_must_materialize_correctly_when_used_in_multiple_flows() { - var testData = ByteString.FromBytes(new byte[] { 1, 2, 3, 4, 5 }); + var testData = ByteString.FromBytes(new byte[] {1, 2, 3, 4, 5}); var server = new Server(this); var tcpWriteProbe1 = new TcpWriteProbe(this); @@ -400,14 +399,14 @@ public async Task Outgoing_TCP_stream_must_materialize_correctly_when_used_in_mu ValidateServerClientCommunication(testData, serverConnection1, tcpReadProbe1, tcpWriteProbe1); ValidateServerClientCommunication(testData, serverConnection2, tcpReadProbe2, tcpWriteProbe2); - + var conn1 = await conn1F; var conn2 = await conn2F; // Since we have already communicated over the connections we can have short timeouts for the tasks - ((IPEndPoint)conn1.RemoteAddress).Port.Should().Be(((IPEndPoint)server.Address).Port); - ((IPEndPoint)conn2.RemoteAddress).Port.Should().Be(((IPEndPoint)server.Address).Port); - ((IPEndPoint)conn1.LocalAddress).Port.Should().NotBe(((IPEndPoint)conn2.LocalAddress).Port); + ((IPEndPoint) conn1.RemoteAddress).Port.Should().Be(((IPEndPoint) server.Address).Port); + ((IPEndPoint) conn2.RemoteAddress).Port.Should().Be(((IPEndPoint) server.Address).Port); + ((IPEndPoint) conn1.LocalAddress).Port.Should().NotBe(((IPEndPoint) conn2.LocalAddress).Port); tcpWriteProbe1.Close(); tcpReadProbe1.Close(); @@ -424,7 +423,7 @@ public void Outgoing_TCP_stream_must_properly_full_close_if_requested() var writeButIgnoreRead = Flow.FromSinkAndSource(Sink.Ignore(), Source.Single(ByteString.FromString("Early response")), Keep.Right); - var task = + var task = Sys.TcpStream() .Bind(serverAddress.Address.ToString(), serverAddress.Port, halfClose: false) .ToMaterialized( @@ -443,7 +442,7 @@ public void Outgoing_TCP_stream_must_properly_full_close_if_requested() result.Wait(TimeSpan.FromSeconds(5)).Should().BeTrue(); result.Result.ShouldBeEquivalentTo(ByteString.FromString("Early response")); - promise.SetResult(Option.None); // close client upstream, no more data + promise.SetResult(null); // close client upstream, no more data binding.Unbind(); }, Materializer); @@ -462,10 +461,10 @@ public async Task Outgoing_TCP_stream_must_Echo_should_work_even_if_server_is_in .Run(Materializer); var result = await Source.From(Enumerable.Repeat(0, 1000) - .Select(i => ByteString.FromBytes(new[] { Convert.ToByte(i) }))) + .Select(i => ByteString.FromBytes(new[] {Convert.ToByte(i)}))) .Via(Sys.TcpStream().OutgoingConnection(serverAddress, halfClose: true)) .RunAggregate(0, (i, s) => i + s.Count, Materializer); - + result.Should().Be(1000); await binding.Unbind(); @@ -533,7 +532,7 @@ private void ValidateServerClientCommunication(ByteString testData, ServerConnec writeProbe.Write(testData); serverConnection.WaitRead().ShouldBeEquivalentTo(testData); } - + private Sink EchoHandler() => Sink.ForEach(c => c.Flow.Join(Flow.Create()).Run(Materializer)); @@ -551,7 +550,7 @@ public async Task Tcp_listen_stream_must_be_able_to_implement_echo() var echoServerFinish = t.Item2; var testInput = Enumerable.Range(0, 255) - .Select(i => ByteString.FromBytes(new[] { Convert.ToByte(i) })) + .Select(i => ByteString.FromBytes(new[] {Convert.ToByte(i)})) .ToList(); var expectedOutput = testInput.Aggregate(ByteString.Empty, (agg, b) => agg.Concat(b)); @@ -559,7 +558,7 @@ public async Task Tcp_listen_stream_must_be_able_to_implement_echo() var result = await Source.From(testInput) .Via(Sys.TcpStream().OutgoingConnection(serverAddress)) .RunAggregate(ByteString.Empty, (agg, b) => agg.Concat(b), Materializer); - + result.ShouldBeEquivalentTo(expectedOutput); await binding.Unbind(); await echoServerFinish; @@ -592,7 +591,7 @@ public async Task Tcp_listen_stream_must_work_with_a_chain_of_echoes() .Via(echoConnection) .Via(echoConnection) .RunAggregate(ByteString.Empty, (agg, b) => agg.Concat(b), Materializer); - + result.ShouldBeEquivalentTo(expectedOutput); await binding.Unbind(); await echoServerFinish; @@ -620,10 +619,10 @@ public void Tcp_listen_stream_must_bind_and_unbind_correctly() var probe3 = this.CreateManualSubscriberProbe(); var binding3F = bind.To(Sink.FromSubscriber(probe3)).Run(Materializer); probe3.ExpectSubscriptionAndError().Should().BeOfType(); - + binding2F.Invoking(x => x.Wait(TimeSpan.FromSeconds(3))).ShouldThrow(); binding3F.Invoking(x => x.Wait(TimeSpan.FromSeconds(3))).ShouldThrow(); - + // Now unbind first binding1.Unbind().Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); probe1.ExpectComplete(); @@ -676,8 +675,8 @@ public void Tcp_listen_stream_must_not_shut_down_connections_after_the_connectio }, Materializer); } - [Fact(Skip = "FIXME")] - public void Tcp_listen_stream_must_shut_down_properly_even_if_some_accepted_connection_Flows_have_not_been_subscribed_to() + [Fact(Skip="FIXME")] + public void Tcp_listen_stream_must_shut_down_properly_even_if_some_accepted_connection_Flows_have_not_been_subscribed_to () { this.AssertAllStagesStopped(() => { @@ -694,7 +693,7 @@ public void Tcp_listen_stream_must_shut_down_properly_even_if_some_accepted_conn .Via(takeTwoAndDropSecond) .RunForeach(c => c.Flow.Join(Flow.Create()).Run(Materializer), Materializer); - var folder = Source.From(Enumerable.Range(0, 100).Select(_ => ByteString.FromBytes(new byte[] { 0 }))) + var folder = Source.From(Enumerable.Range(0, 100).Select(_ => ByteString.FromBytes(new byte[] {0}))) .Via(Sys.TcpStream().OutgoingConnection(serverAddress)) .Aggregate(0, (i, s) => i + s.Count) .ToMaterialized(Sink.First(), Keep.Right); diff --git a/src/core/Akka.Streams.Tests/Implementation/Fusing/KeepGoingStageSpec.cs b/src/core/Akka.Streams.Tests/Implementation/Fusing/KeepGoingStageSpec.cs index 99268264601..fc7e373ef3c 100644 --- a/src/core/Akka.Streams.Tests/Implementation/Fusing/KeepGoingStageSpec.cs +++ b/src/core/Akka.Streams.Tests/Implementation/Fusing/KeepGoingStageSpec.cs @@ -11,7 +11,6 @@ using Akka.Streams.Dsl; using Akka.Streams.Stage; using Akka.Streams.TestKit.Tests; -using Akka.Streams.Util; using Akka.TestKit; using FluentAssertions; using Xunit; @@ -127,7 +126,7 @@ public PingableLogic(PingableSink pingable) : base(pingable.Shape) { _pingable = pingable; - SetHandler(_pingable.Shape.Inlet, + SetHandler(_pingable.Shape.Inlet, () => Pull(_pingable.Shape.Inlet), //Ignore finish () => { _listener.Tell(UpstreamCompleted.Instance); }); @@ -214,7 +213,7 @@ public void A_stage_with_keep_going_must_still_be_alive_after_all_ports_have_bee pinger.Ping(); ExpectMsg(); - maybePromise.TrySetResult(Option.None); + maybePromise.TrySetResult(0); ExpectMsg(); ExpectNoMsg(200); @@ -249,7 +248,7 @@ public void A_stage_with_keep_going_must_still_be_alive_after_all_ports_have_bee pinger.Ping(); ExpectMsg(); - maybePromise.TrySetResult(Option.None); + maybePromise.TrySetResult(0); ExpectMsg(); ExpectNoMsg(200); @@ -288,7 +287,7 @@ public void A_stage_with_keep_going_must_still_be_alive_after_all_ports_have_bee pinger.Ping(); ExpectMsg(); - maybePromise.TrySetResult(Option.None); + maybePromise.TrySetResult(0); ExpectMsg(); ExpectNoMsg(200); @@ -301,7 +300,7 @@ public void A_stage_with_keep_going_must_still_be_alive_after_all_ports_have_bee // We need to catch the exception otherwise the test fails // ReSharper disable once EmptyGeneralCatchClause - try { pinger.ThrowEx(); } catch { } + try { pinger.ThrowEx();} catch { } // PostStop should not be concurrent with the event handler. This event here tests this. ExpectMsg(); ExpectMsg(); @@ -329,7 +328,7 @@ public void A_stage_with_keep_going_must_close_down_earls_if_keepAlive_is_not_re pinger.Ping(); ExpectMsg(); - maybePromise.TrySetResult(Option.None); + maybePromise.TrySetResult(0); ExpectMsg(); ExpectMsg(); }, Materializer); diff --git a/src/core/Akka.Streams/ActorMaterializer.cs b/src/core/Akka.Streams/ActorMaterializer.cs index d2ad60b091e..16f1b5d026c 100644 --- a/src/core/Akka.Streams/ActorMaterializer.cs +++ b/src/core/Akka.Streams/ActorMaterializer.cs @@ -318,8 +318,7 @@ private static ActorMaterializerSettings Create(Config config) isFuzzingMode: config.GetBoolean("debug.fuzzing-mode"), isAutoFusing: config.GetBoolean("auto-fusing", true), maxFixedBufferSize: config.GetInt("max-fixed-buffer-size", 1000000000), - syncProcessingLimit: config.GetInt("sync-processing-limit", 1000), - streamRefSettings: StreamRefSettings.Create(config.GetConfig("stream-ref") ?? Config.Empty)); + syncProcessingLimit: config.GetInt("sync-processing-limit", 1000)); } private const int DefaultlMaxFixedbufferSize = 1000; @@ -368,8 +367,6 @@ private static ActorMaterializerSettings Create(Config config) /// public readonly int SyncProcessingLimit; - public readonly StreamRefSettings StreamRefSettings; - /// /// TBD /// @@ -384,7 +381,7 @@ private static ActorMaterializerSettings Create(Config config) /// TBD /// TBD /// TBD - public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferSize, string dispatcher, Decider supervisionDecider, StreamSubscriptionTimeoutSettings subscriptionTimeoutSettings, StreamRefSettings streamRefSettings, bool isDebugLogging, int outputBurstLimit, bool isFuzzingMode, bool isAutoFusing, int maxFixedBufferSize, int syncProcessingLimit = DefaultlMaxFixedbufferSize) + public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferSize, string dispatcher, Decider supervisionDecider, StreamSubscriptionTimeoutSettings subscriptionTimeoutSettings, bool isDebugLogging, int outputBurstLimit, bool isFuzzingMode, bool isAutoFusing, int maxFixedBufferSize, int syncProcessingLimit = DefaultlMaxFixedbufferSize) { InitialInputBufferSize = initialInputBufferSize; MaxInputBufferSize = maxInputBufferSize; @@ -397,7 +394,7 @@ public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferS IsAutoFusing = isAutoFusing; MaxFixedBufferSize = maxFixedBufferSize; SyncProcessingLimit = syncProcessingLimit; - StreamRefSettings = streamRefSettings; + } /// @@ -408,7 +405,7 @@ public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferS /// TBD public ActorMaterializerSettings WithInputBuffer(int initialSize, int maxSize) { - return new ActorMaterializerSettings(initialSize, maxSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(initialSize, maxSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -418,7 +415,7 @@ public ActorMaterializerSettings WithInputBuffer(int initialSize, int maxSize) /// TBD public ActorMaterializerSettings WithDispatcher(string dispatcher) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -428,7 +425,7 @@ public ActorMaterializerSettings WithDispatcher(string dispatcher) /// TBD public ActorMaterializerSettings WithSupervisionStrategy(Decider decider) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, decider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, decider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -438,7 +435,7 @@ public ActorMaterializerSettings WithSupervisionStrategy(Decider decider) /// TBD public ActorMaterializerSettings WithDebugLogging(bool isEnabled) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, isEnabled, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, isEnabled, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -448,7 +445,7 @@ public ActorMaterializerSettings WithDebugLogging(bool isEnabled) /// TBD public ActorMaterializerSettings WithFuzzingMode(bool isFuzzingMode) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, isFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, isFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -458,7 +455,7 @@ public ActorMaterializerSettings WithFuzzingMode(bool isFuzzingMode) /// TBD public ActorMaterializerSettings WithAutoFusing(bool isAutoFusing) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, isAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, isAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -468,7 +465,7 @@ public ActorMaterializerSettings WithAutoFusing(bool isAutoFusing) /// TBD public ActorMaterializerSettings WithMaxFixedBufferSize(int maxFixedBufferSize) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, maxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, maxFixedBufferSize, SyncProcessingLimit); } /// @@ -478,7 +475,7 @@ public ActorMaterializerSettings WithMaxFixedBufferSize(int maxFixedBufferSize) /// TBD public ActorMaterializerSettings WithSyncProcessingLimit(int limit) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, limit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, limit); } /// @@ -491,14 +488,7 @@ public ActorMaterializerSettings WithSubscriptionTimeoutSettings(StreamSubscript if (Equals(settings, SubscriptionTimeoutSettings)) return this; - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, settings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); - } - - public ActorMaterializerSettings WithStreamRefSettings(StreamRefSettings settings) - { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - if (ReferenceEquals(settings, this.StreamRefSettings)) return this; - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, settings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, settings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } } diff --git a/src/core/Akka.Streams/Akka.Streams.csproj b/src/core/Akka.Streams/Akka.Streams.csproj index 2b40d287c6b..6c5fab2efe6 100644 --- a/src/core/Akka.Streams/Akka.Streams.csproj +++ b/src/core/Akka.Streams/Akka.Streams.csproj @@ -66,12 +66,8 @@ - - - - $(DefineConstants);SERIALIZATION;CLONEABLE;AKKAIO diff --git a/src/core/Akka.Streams/Attributes.cs b/src/core/Akka.Streams/Attributes.cs index a8944a9af46..8b248c512b5 100644 --- a/src/core/Akka.Streams/Attributes.cs +++ b/src/core/Akka.Streams/Attributes.cs @@ -471,32 +471,4 @@ public SupervisionStrategy(Decider decider) public static Attributes CreateSupervisionStrategy(Decider strategy) => new Attributes(new SupervisionStrategy(strategy)); } - - /// - /// Attributes for stream refs ( and ). - /// Note that more attributes defined in and . - /// - public static class StreamRefAttributes - { - /// - /// Attributes specific to stream refs. - /// - public interface IStreamRefAttribute : Attributes.IAttribute { } - - public sealed class SubscriptionTimeout : IStreamRefAttribute - { - public TimeSpan Timeout { get; } - - public SubscriptionTimeout(TimeSpan timeout) - { - Timeout = timeout; - } - } - - /// - /// Specifies the subscription timeout within which the remote side MUST subscribe to the handed out stream reference. - /// - public static Attributes CreateSubscriptionTimeout(TimeSpan timeout) => - new Attributes(new SubscriptionTimeout(timeout)); - } } diff --git a/src/core/Akka.Streams/Dsl/Source.cs b/src/core/Akka.Streams/Dsl/Source.cs index f3cda634756..f394b194de3 100644 --- a/src/core/Akka.Streams/Dsl/Source.cs +++ b/src/core/Akka.Streams/Dsl/Source.cs @@ -596,9 +596,9 @@ public static Source UnfoldInfinite(TState state, /// /// TBD /// TBD - public static Source>> Maybe() + public static Source> Maybe() { - return new Source>>( + return new Source>( new MaybeSource(DefaultAttributes.MaybeSource, new SourceShape(new Outlet("MaybeSource")))); } diff --git a/src/core/Akka.Streams/Dsl/StreamRefs.cs b/src/core/Akka.Streams/Dsl/StreamRefs.cs deleted file mode 100644 index e4e0a62eccb..00000000000 --- a/src/core/Akka.Streams/Dsl/StreamRefs.cs +++ /dev/null @@ -1,731 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2018 Lightbend Inc. -// Copyright (C) 2013-2018 .NET Foundation -// -//----------------------------------------------------------------------- - -using System; -using System.Threading.Tasks; -using Akka.Actor; -using Akka.Annotations; -using Akka.Configuration; -using Akka.Event; -using Akka.Pattern; -using Akka.Streams.Actors; -using Akka.Streams.Dsl; -using Akka.Streams.Implementation; -using Akka.Streams.Stage; -using Akka.Util.Internal; -using Reactive.Streams; - -namespace Akka.Streams.Dsl -{ - - /// - /// API MAY CHANGE: The functionality of stream refs is working, however it is expected that the materialized value - /// will eventually be able to remove the Task wrapping the stream references. For this reason the API is now marked - /// as API may change. See ticket https://github.com/akka/akka/issues/24372 for more details. - /// - /// Factories for creating stream refs. - /// - [ApiMayChange] - public static class StreamRefs - { - /// - /// A local which materializes a which can be used by other streams (including remote ones), - /// to consume data from this local stream, as if they were attached in the spot of the local Sink directly. - /// - /// Adheres to . - /// - /// - [ApiMayChange] - public static Sink>> SourceRef() => - Sink.FromGraph>>(new SinkRefStageImpl(null)); - - /// - /// A local which materializes a which can be used by other streams (including remote ones), - /// to consume data from this local stream, as if they were attached in the spot of the local Sink directly. - /// - /// Adheres to . - /// - /// See more detailed documentation on [[SinkRef]]. - /// - /// - [ApiMayChange] - public static Source>> SinkRef() => - Source.FromGraph>>(new SourceRefStageImpl(null)); - } - - #region StreamRef messages - - internal interface IStreamRefsProtocol { } - - /// - /// Sequenced equivalent. - /// The receiving end of these messages MUST fail the stream if it observes gaps in the sequence, - /// as these messages will not be re-delivered. - /// - /// Sequence numbers start from `0`. - /// - internal sealed class SequencedOnNext : IStreamRefsProtocol, IDeadLetterSuppression - { - public long SeqNr { get; } - public object Payload { get; } - - public SequencedOnNext(long seqNr, object payload) - { - SeqNr = seqNr; - Payload = payload ?? throw ReactiveStreamsCompliance.ElementMustNotBeNullException; - } - } - - /// - /// Initial message sent to remote side to establish partnership between origin and remote stream refs. - /// - internal sealed class OnSubscribeHandshake : IStreamRefsProtocol, IDeadLetterSuppression - { - public OnSubscribeHandshake(IActorRef targetRef) - { - TargetRef = targetRef; - } - - public IActorRef TargetRef { get; } - } - - /// - /// Sent to a the receiver side of a stream ref, once the sending side of the SinkRef gets signalled a Failure. - /// - internal sealed class RemoteStreamFailure : IStreamRefsProtocol - { - public RemoteStreamFailure(string message) - { - Message = message; - } - - public string Message { get; } - } - - /// - /// Sent to a the receiver side of a stream ref, once the sending side of the SinkRef gets signalled a completion. - /// - internal sealed class RemoteStreamCompleted : IStreamRefsProtocol - { - public RemoteStreamCompleted(long seqNr) - { - SeqNr = seqNr; - } - - public long SeqNr { get; } - } - - /// - /// INTERNAL API: Cumulative demand, equivalent to sequence numbering all events in a stream. - /// - /// This message may be re-delivered. - /// - internal sealed class CumulativeDemand : IStreamRefsProtocol, IDeadLetterSuppression - { - public CumulativeDemand(long seqNr) - { - if (seqNr <= 0) throw ReactiveStreamsCompliance.NumberOfElementsInRequestMustBePositiveException; - SeqNr = seqNr; - } - - public long SeqNr { get; } - } - - #endregion - - #region extension - - internal sealed class StreamRefsMaster : IExtension - { - public static StreamRefsMaster Get(ActorSystem system) => - system.WithExtension(); - - private readonly EnumerableActorName sourceRefStageNames = new EnumerableActorNameImpl("SourceRef", new AtomicCounterLong(0L)); - private readonly EnumerableActorName sinkRefStageNames = new EnumerableActorNameImpl("SinkRef", new AtomicCounterLong(0L)); - - public StreamRefsMaster(ExtendedActorSystem system) - { - - } - - public string NextSourceRefName() => sourceRefStageNames.Next(); - public string NextSinkRefName() => sinkRefStageNames.Next(); - } - - internal sealed class StreamRefsMasterProvider : ExtensionIdProvider - { - public override StreamRefsMaster CreateExtension(ExtendedActorSystem system) => - new StreamRefsMaster(system); - } - - #endregion - - public sealed class StreamRefSettings - { - public static StreamRefSettings Create(Config config) - { - if (config == null) throw new ArgumentNullException(nameof(config), "`akka.stream.materializer.stream-ref` was not present"); - - return new StreamRefSettings( - bufferCapacity: config.GetInt("buffer-capacity", 32), - demandRedeliveryInterval: config.GetTimeSpan("demand-redelivery-interval", TimeSpan.FromSeconds(1)), - subscriptionTimeout: config.GetTimeSpan("subscription-timeout", TimeSpan.FromSeconds(30))); - } - - public int BufferCapacity { get; } - public TimeSpan DemandRedeliveryInterval { get; } - public TimeSpan SubscriptionTimeout { get; } - - public StreamRefSettings(int bufferCapacity, TimeSpan demandRedeliveryInterval, TimeSpan subscriptionTimeout) - { - BufferCapacity = bufferCapacity; - DemandRedeliveryInterval = demandRedeliveryInterval; - SubscriptionTimeout = subscriptionTimeout; - } - - public string ProductPrefix => nameof(StreamRefSettings); - - public StreamRefSettings WithBufferCapacity(int value) => Copy(bufferCapacity: value); - public StreamRefSettings WithDemandRedeliveryInterval(TimeSpan value) => Copy(demandRedeliveryInterval: value); - public StreamRefSettings WithSubscriptionTimeout(TimeSpan value) => Copy(subscriptionTimeout: value); - - public StreamRefSettings Copy(int? bufferCapacity = null, - TimeSpan? demandRedeliveryInterval = null, - TimeSpan? subscriptionTimeout = null) => new StreamRefSettings( - bufferCapacity: bufferCapacity ?? this.BufferCapacity, - demandRedeliveryInterval: demandRedeliveryInterval ?? this.DemandRedeliveryInterval, - subscriptionTimeout: subscriptionTimeout ?? this.SubscriptionTimeout); - } - - /// - /// Abstract class defined serialization purposes of . - /// - internal abstract class SourceRefImpl - { - public static SourceRefImpl Create(Type eventType, IActorRef initialPartnerRef) - { - var destType = typeof(SourceRefImpl<>).MakeGenericType(eventType); - return (SourceRefImpl)Activator.CreateInstance(destType, initialPartnerRef); - } - - protected SourceRefImpl(IActorRef initialPartnerRef) - { - InitialPartnerRef = initialPartnerRef; - } - - public IActorRef InitialPartnerRef { get; } - public abstract Type EventType { get; } - } - internal sealed class SourceRefImpl : SourceRefImpl, ISourceRef - { - public SourceRefImpl(IActorRef initialPartnerRef) : base(initialPartnerRef) { } - public override Type EventType => typeof(T); - public Source Source => - Dsl.Source.FromGraph(new SourceRefStageImpl(InitialPartnerRef)).MapMaterializedValue(_ => NotUsed.Instance); - } - - /// - /// Abstract class defined serialization purposes of . - /// - internal abstract class SinkRefImpl - { - public static SinkRefImpl Create(Type eventType, IActorRef initialPartnerRef) - { - var destType = typeof(SinkRefImpl<>).MakeGenericType(eventType); - return (SinkRefImpl)Activator.CreateInstance(destType, initialPartnerRef); - } - - protected SinkRefImpl(IActorRef initialPartnerRef) - { - InitialPartnerRef = initialPartnerRef; - } - - public IActorRef InitialPartnerRef { get; } - public abstract Type EventType { get; } - } - - internal sealed class SinkRefImpl : SinkRefImpl, ISinkRef - { - public SinkRefImpl(IActorRef initialPartnerRef) : base(initialPartnerRef) { } - public override Type EventType => typeof(T); - public Sink Sink => Dsl.Sink.FromGraph(new SinkRefStageImpl(InitialPartnerRef)).MapMaterializedValue(_ => NotUsed.Instance); - } - - /// - /// INTERNAL API: Actual stage implementation backing s. - /// - /// If initialPartnerRef is set, then the remote side is already set up. If it is none, then we are the side creating - /// the ref. - /// - /// - internal sealed class SinkRefStageImpl : GraphStageWithMaterializedValue, Task>> - { - #region logic - - private sealed class Logic : TimerGraphStageLogic, IInHandler - { - private const string SubscriptionTimeoutKey = "SubscriptionTimeoutKey"; - - private readonly SinkRefStageImpl _stage; - private readonly TaskCompletionSource> _promise; - private readonly Attributes _inheritedAttributes; - - private StreamRefsMaster _streamRefsMaster; - private StreamRefSettings _settings; - private StreamRefAttributes.SubscriptionTimeout _subscriptionTimeout; - private string _stageActorName; - - private StreamRefsMaster StreamRefsMaster => _streamRefsMaster ?? (_streamRefsMaster = StreamRefsMaster.Get(ActorMaterializerHelper.Downcast(Materializer).System)); - private StreamRefSettings Settings => _settings ?? (_settings = ActorMaterializerHelper.Downcast(Materializer).Settings.StreamRefSettings); - private StreamRefAttributes.SubscriptionTimeout SubscriptionTimeout => _subscriptionTimeout ?? (_subscriptionTimeout = - _inheritedAttributes.GetAttribute(new StreamRefAttributes.SubscriptionTimeout(Settings.SubscriptionTimeout))); - protected override string StageActorName => _stageActorName ?? (_stageActorName = StreamRefsMaster.NextSinkRefName()); - - private StageActor _stageActor; - - private IActorRef _partnerRef = null; - - #region demand management - private long _remoteCumulativeDemandReceived = 0L; - private long _remoteCumulativeDemandConsumed = 0L; - #endregion - - private Status _completedBeforeRemoteConnected = null; - - public IActorRef Self => _stageActor.Ref; - public IActorRef PartnerRef - { - get - { - if (_partnerRef == null) throw new TargetRefNotInitializedYetException(); - return _partnerRef; - } - } - - public Logic(SinkRefStageImpl stage, TaskCompletionSource> promise, - Attributes inheritedAttributes) : base(stage.Shape) - { - _stage = stage; - _promise = promise; - _inheritedAttributes = inheritedAttributes; - - this.SetHandler(_stage.Inlet, this); - } - - public override void PreStart() - { - _stageActor = GetStageActor(InitialReceive); - var initialPartnerRef = _stage._initialPartnerRef; - if (initialPartnerRef != null) - ObserveAndValidateSender(initialPartnerRef, "Illegal initialPartnerRef! This would be a bug in the SinkRef usage or impl."); - - Log.Debug("Created SinkRef, pointing to remote Sink receiver: {0}, local worker: {1}", initialPartnerRef, Self); - - _promise.SetResult(new SourceRefImpl(Self)); - - if (_partnerRef != null) - { - _partnerRef.Tell(new OnSubscribeHandshake(Self), Self); - TryPull(); - } - - ScheduleOnce(SubscriptionTimeoutKey, SubscriptionTimeout.Timeout); - } - - private void InitialReceive(Tuple args) - { - var sender = args.Item1; - var message = args.Item2; - - switch (message) - { - case Terminated terminated: - if (Equals(terminated.ActorRef, PartnerRef)) - FailStage(new RemoteStreamRefActorTerminatedException($"Remote target receiver of data {PartnerRef} terminated. " + - "Local stream terminating, message loss (on remote side) may have happened.")); - break; - case CumulativeDemand demand: - // the other side may attempt to "double subscribe", which we want to fail eagerly since we're 1:1 pairings - ObserveAndValidateSender(sender, "Illegal sender for CumulativeDemand"); - if (_remoteCumulativeDemandReceived < demand.SeqNr) - { - _remoteCumulativeDemandReceived = demand.SeqNr; - Log.Debug("Received cumulative demand [{0}], consumable demand: [{1}]", demand.SeqNr, _remoteCumulativeDemandReceived - _remoteCumulativeDemandConsumed); - } - TryPull(); - break; - } - } - - public void OnPush() - { - var element = GrabSequenced(_stage.Inlet); - PartnerRef.Tell(element, Self); - Log.Debug("Sending sequenced: {0} to {1}", element, PartnerRef); - TryPull(); - } - - private void TryPull() - { - if (_remoteCumulativeDemandConsumed < _remoteCumulativeDemandReceived && !HasBeenPulled(_stage.Inlet)) - { - Pull(_stage.Inlet); - } - } - - protected internal override void OnTimer(object timerKey) - { - if ((string)timerKey == SubscriptionTimeoutKey) - { - // we know the future has been competed by now, since it is in preStart - var ex = new StreamRefSubscriptionTimeoutException($"[{StageActorName}] Remote side did not subscribe (materialize) handed out Sink reference [${_promise.Task.Result}], " + - "within subscription timeout: ${PrettyDuration.format(subscriptionTimeout.timeout)}!"); - - throw ex; // this will also log the exception, unlike failStage; this should fail rarely, but would be good to have it "loud" - } - } - - private SequencedOnNext GrabSequenced(Inlet inlet) - { - var onNext = new SequencedOnNext(_remoteCumulativeDemandConsumed, Grab(inlet)); - _remoteCumulativeDemandConsumed++; - return onNext; - } - - public void OnUpstreamFailure(Exception cause) - { - if (_partnerRef != null) - { - _partnerRef.Tell(new RemoteStreamFailure(cause.ToString()), Self); - _stageActor.Unwatch(_partnerRef); - FailStage(cause); - } - else - { - _completedBeforeRemoteConnected = new Status.Failure(cause); - // not terminating on purpose, since other side may subscribe still and then we want to fail it - // the stage will be terminated either by timeout, or by the handling in `observeAndValidateSender` - SetKeepGoing(true); - } - } - - public void OnUpstreamFinish() - { - if (_partnerRef != null) - { - _partnerRef.Tell(new RemoteStreamCompleted(_remoteCumulativeDemandConsumed), Self); - _stageActor.Unwatch(_partnerRef); - CompleteStage(); - } - else - { - _completedBeforeRemoteConnected = new Status.Success(Done.Instance); - // not terminating on purpose, since other side may subscribe still and then we want to complete it - SetKeepGoing(true); - } - } - - private void ObserveAndValidateSender(IActorRef partner, string failureMessage) - { - if (_partnerRef == null) - { - _partnerRef = partner; - _stageActor.Watch(_partnerRef); - - switch (_completedBeforeRemoteConnected) - { - case Status.Failure failure: - Log.Warning("Stream already terminated with exception before remote side materialized, failing now."); - partner.Tell(new RemoteStreamFailure(failure.Cause.ToString()), Self); - FailStage(failure.Cause); - break; - case Status.Success _: - Log.Warning("Stream already completed before remote side materialized, failing now."); - partner.Tell(new RemoteStreamCompleted(_remoteCumulativeDemandConsumed), Self); - CompleteStage(); - break; - case null: - if (!Equals(partner, PartnerRef)) - { - var ex = new InvalidPartnerActorException(partner, PartnerRef, failureMessage); - partner.Tell(new RemoteStreamFailure(ex.ToString()), Self); - throw ex; - } - break; - } - } - } - } - - #endregion - - private readonly IActorRef _initialPartnerRef; - - public SinkRefStageImpl(IActorRef initialPartnerRef) - { - _initialPartnerRef = initialPartnerRef; - Shape = new SinkShape(Inlet); - } - - public Inlet Inlet { get; } = new Inlet("SinkRef.in"); - public override SinkShape Shape { get; } - public override ILogicAndMaterializedValue>> CreateLogicAndMaterializedValue(Attributes inheritedAttributes) - { - var promise = new TaskCompletionSource>(); - return new LogicAndMaterializedValue>>(new Logic(this, promise, inheritedAttributes), promise.Task); - } - } - - /// - /// INTERNAL API: Actual stage implementation backing [[SourceRef]]s. - /// - /// If initialPartnerRef is set, then the remote side is already set up. - /// If it is none, then we are the side creating the ref. - /// - internal sealed class SourceRefStageImpl : GraphStageWithMaterializedValue, Task>> - { - - #region logic - - private sealed class Logic : TimerGraphStageLogic, IOutHandler - { - private const string SubscriptionTimeoutKey = "SubscriptionTimeoutKey"; - private const string DemandRedeliveryTimerKey = "DemandRedeliveryTimerKey"; - - private readonly SourceRefStageImpl _stage; - private readonly TaskCompletionSource> _promise; - private readonly Attributes _inheritedAttributes; - - private StreamRefsMaster _streamRefsMaster; - private StreamRefSettings _settings; - private StreamRefAttributes.SubscriptionTimeout _subscriptionTimeout; - private string _stageActorName; - - private StageActor _stageActor; - private IActorRef _partnerRef = null; - - private StreamRefsMaster StreamRefsMaster => _streamRefsMaster ?? (_streamRefsMaster = StreamRefsMaster.Get(ActorMaterializerHelper.Downcast(Materializer).System)); - private StreamRefSettings Settings => _settings ?? (_settings = ActorMaterializerHelper.Downcast(Materializer).Settings.StreamRefSettings); - private StreamRefAttributes.SubscriptionTimeout SubscriptionTimeout => _subscriptionTimeout ?? (_subscriptionTimeout = - _inheritedAttributes.GetAttribute(new StreamRefAttributes.SubscriptionTimeout(Settings.SubscriptionTimeout))); - protected override string StageActorName => _stageActorName ?? (_stageActorName = StreamRefsMaster.NextSourceRefName()); - - public IActorRef Self => _stageActor.Ref; - public IActorRef PartnerRef - { - get - { - if (_partnerRef == null) throw new TargetRefNotInitializedYetException(); - return _partnerRef; - } - } - - #region demand management - - private bool _completed = false; - private long _expectingSeqNr = 0L; - private long _localCumulativeDemand = 0L; - private long _localRemainingRequested = 0L; - private FixedSizeBuffer _receiveBuffer; // initialized in preStart since depends on settings - private IRequestStrategy _requestStrategy; // initialized in preStart since depends on receiveBuffer's size - #endregion - - public Logic(SourceRefStageImpl stage, TaskCompletionSource> promise, Attributes inheritedAttributes) : base(stage.Shape) - { - _stage = stage; - _promise = promise; - _inheritedAttributes = inheritedAttributes; - - SetHandler(_stage.Outlet, this); - } - - public override void PreStart() - { - _receiveBuffer = new ModuloFixedSizeBuffer(Settings.BufferCapacity); - _requestStrategy = new WatermarkRequestStrategy(highWatermark: _receiveBuffer.Capacity); - - _stageActor = GetStageActor(InitialReceive); - - Log.Debug("[{0}] Allocated receiver: {1}", StageActorName, Self); - - var initialPartnerRef = _stage._initialPartnerRef; - if (initialPartnerRef != null) // this will set the partnerRef - ObserveAndValidateSender(initialPartnerRef, ""); - - _promise.SetResult(new SinkRefImpl(Self)); - - ScheduleOnce(SubscriptionTimeoutKey, SubscriptionTimeout.Timeout); - } - - public void OnPull() - { - TryPush(); - TriggerCumulativeDemand(); - } - - public void OnDownstreamFinish() - { - CompleteStage(); - } - - private void TriggerCumulativeDemand() - { - var i = _receiveBuffer.RemainingCapacity - _localRemainingRequested; - if (_partnerRef != null && i > 0) - { - 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 - if (addDemand > 0) - { - _localCumulativeDemand += addDemand; - _localRemainingRequested += addDemand; - var demand = new CumulativeDemand(_localCumulativeDemand); - - Log.Debug("[{0}] Demanding until [{1}] (+{2})", _stageActorName, _localCumulativeDemand, addDemand); - PartnerRef.Tell(demand, Self); - ScheduleDemandRedelivery(); - } - } - } - - private void ScheduleDemandRedelivery() => - ScheduleOnce(DemandRedeliveryTimerKey, Settings.DemandRedeliveryInterval); - - protected internal override void OnTimer(object timerKey) - { - switch (timerKey) - { - case SubscriptionTimeoutKey: - var ex = new StreamRefSubscriptionTimeoutException( - // we know the future has been competed by now, since it is in preStart - $"[{StageActorName}] Remote side did not subscribe (materialize) handed out Sink reference [{_promise.Task.Result}]," + - $"within subscription timeout: {SubscriptionTimeout.Timeout}!"); - throw ex; - case DemandRedeliveryTimerKey: - Log.Debug("[{0}] Scheduled re-delivery of demand until [{1}]", StageActorName, _localCumulativeDemand); - PartnerRef.Tell(new CumulativeDemand(_localCumulativeDemand), Self); - ScheduleDemandRedelivery(); - break; - } - } - - private void InitialReceive(Tuple args) - { - var sender = args.Item1; - var message = args.Item2; - - switch (message) - { - case OnSubscribeHandshake handshake: - CancelTimer(SubscriptionTimeoutKey); - ObserveAndValidateSender(sender, "Illegal sender in OnSubscribeHandshake"); - Log.Debug("[{0}] Received handshake {1} from {2}", StageActorName, message, sender); - TriggerCumulativeDemand(); - break; - case SequencedOnNext onNext: - ObserveAndValidateSender(sender, "Illegal sender in SequencedOnNext"); - ObserveAndValidateSequenceNr(onNext.SeqNr, "Illegal sequence nr in SequencedOnNext"); - Log.Debug("[{0}] Received seq {1} from {2}", StageActorName, message, sender); - OnReceiveElement(onNext.Payload); - TriggerCumulativeDemand(); - break; - case RemoteStreamCompleted completed: - 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); - _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); - FailStage(new RemoteStreamRefActorTerminatedException($"Remote stream ({sender.Path}) failed, reason: {failure.Message}")); - break; - case Terminated terminated: - if (Equals(_partnerRef, terminated.ActorRef)) - FailStage(new RemoteStreamRefActorTerminatedException( - $"The remote partner {terminated.ActorRef} has terminated! Tearing down this side of the stream as well.")); - else - FailStage(new RemoteStreamRefActorTerminatedException( - $"Received UNEXPECTED Terminated({terminated.ActorRef}) message! This actor was NOT our trusted remote partner, which was: {_partnerRef}. Tearing down.")); - - break; - } - } - - private void TryPush() - { - if (!_receiveBuffer.IsEmpty && IsAvailable(_stage.Outlet)) Push(_stage.Outlet, _receiveBuffer.Dequeue()); - else if (_receiveBuffer.IsEmpty && _completed) CompleteStage(); - } - - private void OnReceiveElement(object payload) - { - var outlet = _stage.Outlet; - _localRemainingRequested--; - 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}"); - else - _receiveBuffer.Enqueue((TOut)payload); - } - - /// - /// TBD - /// - /// Thrown when is invalid - private void ObserveAndValidateSender(IActorRef partner, string failureMessage) - { - if (_partnerRef == null) - { - Log.Debug("Received first message from {0}, assuming it to be the remote partner for this stage", partner); - _partnerRef = partner; - _stageActor.Watch(partner); - } - else if (!Equals(_partnerRef, partner)) - { - var ex = new InvalidPartnerActorException(partner, PartnerRef, failureMessage); - partner.Tell(new RemoteStreamFailure(ex.Message), Self); - throw ex; - } - } - - private void ObserveAndValidateSequenceNr(long seqNr, string failureMessage) - { - if (seqNr != _expectingSeqNr) - throw new InvalidSequenceNumberException(_expectingSeqNr, seqNr, failureMessage); - else - _expectingSeqNr++; - } - } - - #endregion - - private readonly IActorRef _initialPartnerRef; - - public SourceRefStageImpl(IActorRef initialPartnerRef) - { - _initialPartnerRef = initialPartnerRef; - - Shape = new SourceShape(Outlet); - } - - public Outlet Outlet { get; } = new Outlet("SourceRef.out"); - public override SourceShape Shape { get; } - - public override ILogicAndMaterializedValue>> CreateLogicAndMaterializedValue(Attributes inheritedAttributes) - { - var promise= new TaskCompletionSource>(); - return new LogicAndMaterializedValue>>(new Logic(this, promise, inheritedAttributes), promise.Task); - } - } -} \ No newline at end of file diff --git a/src/core/Akka.Streams/Implementation/ActorRefBackpressureSinkStage.cs b/src/core/Akka.Streams/Implementation/ActorRefBackpressureSinkStage.cs index 0ae226d05fe..9f726e54ee6 100644 --- a/src/core/Akka.Streams/Implementation/ActorRefBackpressureSinkStage.cs +++ b/src/core/Akka.Streams/Implementation/ActorRefBackpressureSinkStage.cs @@ -30,8 +30,7 @@ private sealed class Logic : InGraphStageLogic private readonly int _maxBuffer; private readonly List _buffer; private readonly Type _ackType; - - public IActorRef Self => StageActor.Ref; + private StageActorRef _self; public Logic(ActorRefBackpressureSinkStage stage, int maxBuffer) : base(stage.Shape) { @@ -66,7 +65,7 @@ public override void OnUpstreamFinish() public override void OnUpstreamFailure(Exception ex) { - _stage._actorRef.Tell(_stage._onFailureMessage(ex), Self); + _stage._actorRef.Tell(_stage._onFailureMessage(ex), _self); _completionSignalled = true; FailStage(ex); } @@ -101,8 +100,9 @@ private void Receive(Tuple evt) public override void PreStart() { SetKeepGoing(true); - GetStageActor(Receive).Watch(_stage._actorRef); - _stage._actorRef.Tell(_stage._onInitMessage, Self); + _self = GetStageActorRef(Receive); + _self.Watch(_stage._actorRef); + _stage._actorRef.Tell(_stage._onInitMessage, _self); Pull(_stage._inlet); } @@ -110,14 +110,14 @@ private void DequeueAndSend() { var msg = _buffer[0]; _buffer.RemoveAt(0); - _stage._actorRef.Tell(msg, Self); + _stage._actorRef.Tell(msg, _self); if (_buffer.Count == 0 && _completeReceived) Finish(); } private void Finish() { - _stage._actorRef.Tell(_stage._onCompleteMessage, Self); + _stage._actorRef.Tell(_stage._onCompleteMessage, _self); _completionSignalled = true; CompleteStage(); } @@ -125,7 +125,7 @@ private void Finish() public override void PostStop() { if(!_completionSignalled) - Self.Tell(_stage._onFailureMessage(new AbruptStageTerminationException(this))); + StageActorRef.Tell(_stage._onFailureMessage(new AbruptStageTerminationException(this))); } public override string ToString() => "ActorRefBackpressureSink"; diff --git a/src/core/Akka.Streams/Implementation/Buffers.cs b/src/core/Akka.Streams/Implementation/Buffers.cs index ba08816131e..cec8295d7ef 100644 --- a/src/core/Akka.Streams/Implementation/Buffers.cs +++ b/src/core/Akka.Streams/Implementation/Buffers.cs @@ -194,8 +194,6 @@ protected FixedSizeBuffer(int capacity) /// public bool NonEmpty => Used != 0; - public long RemainingCapacity => Capacity - Used; - // for the maintenance parameter see dropHead /// /// TBD diff --git a/src/core/Akka.Streams/Implementation/CompletedPublishers.cs b/src/core/Akka.Streams/Implementation/CompletedPublishers.cs index 3c081630612..26f7f647b87 100644 --- a/src/core/Akka.Streams/Implementation/CompletedPublishers.cs +++ b/src/core/Akka.Streams/Implementation/CompletedPublishers.cs @@ -113,10 +113,10 @@ internal sealed class MaybePublisher : IPublisher private class MaybeSubscription : ISubscription { private readonly ISubscriber _subscriber; - private readonly TaskCompletionSource> _promise; + private readonly TaskCompletionSource _promise; private bool _done; - public MaybeSubscription(ISubscriber subscriber, TaskCompletionSource> promise) + public MaybeSubscription(ISubscriber subscriber, TaskCompletionSource promise) { _subscriber = subscriber; _promise = promise; @@ -131,9 +131,9 @@ public void Request(long n) _done = true; _promise.Task.ContinueWith(t => { - if (!_promise.Task.Result.Equals(Option.None)) + if (!_promise.Task.Result.IsDefaultForType()) { - ReactiveStreamsCompliance.TryOnNext(_subscriber, _promise.Task.Result.Value); + ReactiveStreamsCompliance.TryOnNext(_subscriber, _promise.Task.Result); ReactiveStreamsCompliance.TryOnComplete(_subscriber); } else @@ -145,14 +145,14 @@ public void Request(long n) public void Cancel() { _done = true; - _promise.TrySetResult(Option.None); + _promise.TrySetResult(default(T)); } } /// /// TBD /// - public readonly TaskCompletionSource> Promise; + public readonly TaskCompletionSource Promise; /// /// TBD /// @@ -163,7 +163,7 @@ public void Cancel() /// /// TBD /// TBD - public MaybePublisher(TaskCompletionSource> promise, string name) + public MaybePublisher(TaskCompletionSource promise, string name) { Promise = promise; Name = name; diff --git a/src/core/Akka.Streams/Implementation/IO/TcpStages.cs b/src/core/Akka.Streams/Implementation/IO/TcpStages.cs index d724ca8cbfb..fe0330263f9 100644 --- a/src/core/Akka.Streams/Implementation/IO/TcpStages.cs +++ b/src/core/Akka.Streams/Implementation/IO/TcpStages.cs @@ -53,7 +53,7 @@ public ConnectionSourceStageLogic(Shape shape, ConnectionSourceStage stage, Task public void OnPull() { // Ignore if still binding - _listener?.Tell(new Tcp.ResumeAccepting(1), StageActor.Ref); + _listener?.Tell(new Tcp.ResumeAccepting(1), StageActorRef); } public void OnDownstreamFinish() => TryUnbind(); @@ -85,13 +85,13 @@ private void TryUnbind() { _unbindStarted = true; SetKeepGoing(true); - _listener.Tell(Tcp.Unbind.Instance, StageActor.Ref); + _listener.Tell(Tcp.Unbind.Instance, StageActorRef); } } private void UnbindCompleted() { - StageActor.Unwatch(_listener); + StageActorRef.Unwatch(_listener); if (_connectionFlowsAwaitingInitialization.Current == 0) CompleteStage(); else @@ -106,8 +106,8 @@ protected internal override void OnTimer(object timerKey) public override void PreStart() { - GetStageActor(Receive); - _stage._tcpManager.Tell(new Tcp.Bind(StageActor.Ref, _stage._endpoint, _stage._backlog, _stage._options, pullMode: true), StageActor.Ref); + GetStageActorRef(Receive); + _stage._tcpManager.Tell(new Tcp.Bind(StageActorRef, _stage._endpoint, _stage._backlog, _stage._options, pullMode: true), StageActorRef); } private void Receive(Tuple args) @@ -118,12 +118,12 @@ private void Receive(Tuple args) { var bound = (Tcp.Bound)msg; _listener = sender; - StageActor.Watch(_listener); + StageActorRef.Watch(_listener); if (IsAvailable(_stage._out)) - _listener.Tell(new Tcp.ResumeAccepting(1), StageActor.Ref); + _listener.Tell(new Tcp.ResumeAccepting(1), StageActorRef); - var thisStage = StageActor.Ref; + var thisStage = StageActorRef; _bindingPromise.TrySetResult(new StreamTcp.ServerBinding(bound.LocalAddress, () => { // Beware, sender must be explicit since stageActor.ref will be invalid to access after the stage stopped @@ -404,14 +404,14 @@ public TcpStreamLogic(FlowShape shape, ITcpRole role, En _bytesOut = shape.Outlet; _readHandler = new LambdaOutHandler( - onPull: () => _connection.Tell(Tcp.ResumeReading.Instance, StageActor.Ref), + onPull: () => _connection.Tell(Tcp.ResumeReading.Instance, StageActorRef), onDownstreamFinish: () => { if (!IsClosed(_bytesIn)) - _connection.Tell(Tcp.ResumeReading.Instance, StageActor.Ref); + _connection.Tell(Tcp.ResumeReading.Instance, StageActorRef); else { - _connection.Tell(Tcp.Abort.Instance, StageActor.Ref); + _connection.Tell(Tcp.Abort.Instance, StageActorRef); CompleteStage(); } }); @@ -423,17 +423,17 @@ public TcpStreamLogic(FlowShape shape, ITcpRole role, En { var elem = Grab(_bytesIn); ReactiveStreamsCompliance.RequireNonNullElement(elem); - _connection.Tell(Tcp.Write.Create(elem, WriteAck.Instance), StageActor.Ref); + _connection.Tell(Tcp.Write.Create(elem, WriteAck.Instance), StageActorRef); }, onUpstreamFinish: () => { // Reading has stopped before, either because of cancel, or PeerClosed, so just Close now // (or half-close is turned off) if (IsClosed(_bytesOut) || !_role.HalfClose) - _connection.Tell(Tcp.Close.Instance, StageActor.Ref); + _connection.Tell(Tcp.Close.Instance, StageActorRef); // We still read, so we only close the write side else if (_connection != null) - _connection.Tell(Tcp.ConfirmedClose.Instance, StageActor.Ref); + _connection.Tell(Tcp.ConfirmedClose.Instance, StageActorRef); else CompleteStage(); }, @@ -444,7 +444,7 @@ public TcpStreamLogic(FlowShape shape, ITcpRole role, En if (Interpreter.Log.IsDebugEnabled) Interpreter.Log.Debug( $"Aborting tcp connection to {_remoteAddress} because of upstream failure: {ex.Message}\n{ex.StackTrace}"); - _connection.Tell(Tcp.Abort.Instance, StageActor.Ref); + _connection.Tell(Tcp.Abort.Instance, StageActorRef); } else FailStage(ex); @@ -463,15 +463,15 @@ public override void PreStart() var inbound = (Inbound)_role; SetHandler(_bytesOut, _readHandler); _connection = inbound.Connection; - GetStageActor(Connected).Watch(_connection); - _connection.Tell(new Tcp.Register(StageActor.Ref, keepOpenOnPeerClosed: true, useResumeWriting: false), StageActor.Ref); + GetStageActorRef(Connected).Watch(_connection); + _connection.Tell(new Tcp.Register(StageActorRef, keepOpenOnPeerClosed: true, useResumeWriting: false), StageActorRef); Pull(_bytesIn); } else { var outbound = (Outbound)_role; - GetStageActor(Connecting(outbound)).Watch(outbound.Manager); - outbound.Manager.Tell(outbound.ConnectCmd, StageActor.Ref); + GetStageActorRef(Connecting(outbound)).Watch(outbound.Manager); + outbound.Manager.Tell(outbound.ConnectCmd, StageActorRef); } } @@ -506,13 +506,13 @@ private StageActorRef.Receive Connecting(Outbound outbound) ((Outbound)_role).LocalAddressPromise.TrySetResult(connected.LocalAddress); _connection = sender; SetHandler(_bytesOut, _readHandler); - StageActor.Unwatch(outbound.Manager); - StageActor.Become(Connected); - StageActor.Watch(_connection); - _connection.Tell(new Tcp.Register(StageActor.Ref, keepOpenOnPeerClosed: true, useResumeWriting: false), StageActor.Ref); + StageActorRef.Unwatch(outbound.Manager); + StageActorRef.Become(Connected); + StageActorRef.Watch(_connection); + _connection.Tell(new Tcp.Register(StageActorRef, keepOpenOnPeerClosed: true, useResumeWriting: false), StageActorRef); if (IsAvailable(_bytesOut)) - _connection.Tell(Tcp.ResumeReading.Instance, StageActor.Ref); + _connection.Tell(Tcp.ResumeReading.Instance, StageActorRef); Pull(_bytesIn); } @@ -535,7 +535,7 @@ private void Connected(Tuple args) { var received = (Tcp.Received)msg; // Keep on reading even when closed. There is no "close-read-side" in TCP - if (IsClosed(_bytesOut)) _connection.Tell(Tcp.ResumeReading.Instance, StageActor.Ref); + if (IsClosed(_bytesOut)) _connection.Tell(Tcp.ResumeReading.Instance, StageActorRef); else Push(_bytesOut, received.Data); } else if (msg is WriteAck) diff --git a/src/core/Akka.Streams/Implementation/Modules.cs b/src/core/Akka.Streams/Implementation/Modules.cs index b0798d8c712..5c7a949dc33 100644 --- a/src/core/Akka.Streams/Implementation/Modules.cs +++ b/src/core/Akka.Streams/Implementation/Modules.cs @@ -10,7 +10,6 @@ using Akka.Actor; using Akka.Annotations; using Akka.Streams.Actors; -using Akka.Streams.Util; using Reactive.Streams; namespace Akka.Streams.Implementation @@ -253,7 +252,7 @@ public override IPublisher Create(MaterializationContext context, out NotU /// /// TBD [InternalApi] - public sealed class MaybeSource : SourceModule>> + public sealed class MaybeSource : SourceModule> { /// /// TBD @@ -283,7 +282,7 @@ public override IModule WithAttributes(Attributes attributes) /// /// TBD /// TBD - protected override SourceModule>> NewInstance(SourceShape shape) + protected override SourceModule> NewInstance(SourceShape shape) => new MaybeSource(Attributes, shape); /// @@ -292,9 +291,9 @@ protected override SourceModule>> NewIns /// TBD /// TBD /// TBD - public override IPublisher Create(MaterializationContext context, out TaskCompletionSource> materializer) + public override IPublisher Create(MaterializationContext context, out TaskCompletionSource materializer) { - materializer = new TaskCompletionSource>(); + materializer = new TaskCompletionSource(); return new MaybePublisher(materializer, Attributes.GetNameOrDefault("MaybeSource")); } } @@ -397,7 +396,7 @@ public ActorRefSource(int bufferSize, OverflowStrategy overflowStrategy, Attribu /// /// TBD /// TBD - public override IModule WithAttributes(Attributes attributes) + public override IModule WithAttributes(Attributes attributes) => new ActorRefSource(_bufferSize, _overflowStrategy, attributes, AmendShape(attributes)); /// @@ -405,7 +404,7 @@ public override IModule WithAttributes(Attributes attributes) /// /// TBD /// TBD - protected override SourceModule NewInstance(SourceShape shape) + protected override SourceModule NewInstance(SourceShape shape) => new ActorRefSource(_bufferSize, _overflowStrategy, Attributes, shape); /// diff --git a/src/core/Akka.Streams/Serialization/Proto/StreamRefMessages.g.cs b/src/core/Akka.Streams/Serialization/Proto/StreamRefMessages.g.cs deleted file mode 100644 index 3854e88d887..00000000000 --- a/src/core/Akka.Streams/Serialization/Proto/StreamRefMessages.g.cs +++ /dev/null @@ -1,1413 +0,0 @@ -// Generated by the protocol buffer compiler. DO NOT EDIT! -// source: StreamRefMessages.proto -#pragma warning disable 1591, 0612, 3021 -#region Designer generated code - -using pb = global::Google.Protobuf; -using pbc = global::Google.Protobuf.Collections; -using pbr = global::Google.Protobuf.Reflection; -using scg = global::System.Collections.Generic; -namespace Akka.Streams.Serialization.Proto.Msg { - - /// Holder for reflection information generated from StreamRefMessages.proto - internal static partial class StreamRefMessagesReflection { - - #region Descriptor - /// File descriptor for StreamRefMessages.proto - public static pbr::FileDescriptor Descriptor { - get { return descriptor; } - } - private static pbr::FileDescriptor descriptor; - - static StreamRefMessagesReflection() { - byte[] descriptorData = global::System.Convert.FromBase64String( - string.Concat( - "ChdTdHJlYW1SZWZNZXNzYWdlcy5wcm90bxIkQWtrYS5TdHJlYW1zLlNlcmlh", - "bGl6YXRpb24uUHJvdG8uTXNnIh0KCUV2ZW50VHlwZRIQCgh0eXBlTmFtZRgB", - "IAEoCSKQAQoHU2lua1JlZhJBCgl0YXJnZXRSZWYYASABKAsyLi5Ba2thLlN0", - "cmVhbXMuU2VyaWFsaXphdGlvbi5Qcm90by5Nc2cuQWN0b3JSZWYSQgoJZXZl", - "bnRUeXBlGAIgASgLMi8uQWtrYS5TdHJlYW1zLlNlcmlhbGl6YXRpb24uUHJv", - "dG8uTXNnLkV2ZW50VHlwZSKSAQoJU291cmNlUmVmEkEKCW9yaWdpblJlZhgB", - "IAEoCzIuLkFra2EuU3RyZWFtcy5TZXJpYWxpemF0aW9uLlByb3RvLk1zZy5B", - "Y3RvclJlZhJCCglldmVudFR5cGUYAiABKAsyLy5Ba2thLlN0cmVhbXMuU2Vy", - "aWFsaXphdGlvbi5Qcm90by5Nc2cuRXZlbnRUeXBlIhgKCEFjdG9yUmVmEgwK", - "BHBhdGgYASABKAkiUQoHUGF5bG9hZBIXCg9lbmNsb3NlZE1lc3NhZ2UYASAB", - "KAwSFAoMc2VyaWFsaXplcklkGAIgASgFEhcKD21lc3NhZ2VNYW5pZmVzdBgD", - "IAEoDCJZChRPblN1YnNjcmliZUhhbmRzaGFrZRJBCgl0YXJnZXRSZWYYASAB", - "KAsyLi5Ba2thLlN0cmVhbXMuU2VyaWFsaXphdGlvbi5Qcm90by5Nc2cuQWN0", - "b3JSZWYiIQoQQ3VtdWxhdGl2ZURlbWFuZBINCgVzZXFOchgBIAEoAyJgCg9T", - "ZXF1ZW5jZWRPbk5leHQSDQoFc2VxTnIYASABKAMSPgoHcGF5bG9hZBgCIAEo", - "CzItLkFra2EuU3RyZWFtcy5TZXJpYWxpemF0aW9uLlByb3RvLk1zZy5QYXls", - "b2FkIiQKE1JlbW90ZVN0cmVhbUZhaWx1cmUSDQoFY2F1c2UYASABKAwiJgoV", - "UmVtb3RlU3RyZWFtQ29tcGxldGVkEg0KBXNlcU5yGAEgASgDQgJIAWIGcHJv", - "dG8z")); - descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, - new pbr::FileDescriptor[] { }, - new pbr::GeneratedClrTypeInfo(null, new pbr::GeneratedClrTypeInfo[] { - new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.EventType), global::Akka.Streams.Serialization.Proto.Msg.EventType.Parser, new[]{ "TypeName" }, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.SinkRef), global::Akka.Streams.Serialization.Proto.Msg.SinkRef.Parser, new[]{ "TargetRef", "EventType" }, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.SourceRef), global::Akka.Streams.Serialization.Proto.Msg.SourceRef.Parser, new[]{ "OriginRef", "EventType" }, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.ActorRef), global::Akka.Streams.Serialization.Proto.Msg.ActorRef.Parser, new[]{ "Path" }, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.Payload), global::Akka.Streams.Serialization.Proto.Msg.Payload.Parser, new[]{ "EnclosedMessage", "SerializerId", "MessageManifest" }, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.OnSubscribeHandshake), global::Akka.Streams.Serialization.Proto.Msg.OnSubscribeHandshake.Parser, new[]{ "TargetRef" }, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.CumulativeDemand), global::Akka.Streams.Serialization.Proto.Msg.CumulativeDemand.Parser, new[]{ "SeqNr" }, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.SequencedOnNext), global::Akka.Streams.Serialization.Proto.Msg.SequencedOnNext.Parser, new[]{ "SeqNr", "Payload" }, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.RemoteStreamFailure), global::Akka.Streams.Serialization.Proto.Msg.RemoteStreamFailure.Parser, new[]{ "Cause" }, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.RemoteStreamCompleted), global::Akka.Streams.Serialization.Proto.Msg.RemoteStreamCompleted.Parser, new[]{ "SeqNr" }, null, null, null) - })); - } - #endregion - - } - #region Messages - internal sealed partial class EventType : pb::IMessage { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new EventType()); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pbr::MessageDescriptor Descriptor { - get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[0]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public EventType() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public EventType(EventType other) : this() { - typeName_ = other.typeName_; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public EventType Clone() { - return new EventType(this); - } - - /// Field number for the "typeName" field. - public const int TypeNameFieldNumber = 1; - private string typeName_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public string TypeName { - get { return typeName_; } - set { - typeName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override bool Equals(object other) { - return Equals(other as EventType); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public bool Equals(EventType other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (TypeName != other.TypeName) return false; - return true; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override int GetHashCode() { - int hash = 1; - if (TypeName.Length != 0) hash ^= TypeName.GetHashCode(); - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void WriteTo(pb::CodedOutputStream output) { - if (TypeName.Length != 0) { - output.WriteRawTag(10); - output.WriteString(TypeName); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public int CalculateSize() { - int size = 0; - if (TypeName.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(TypeName); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(EventType other) { - if (other == null) { - return; - } - if (other.TypeName.Length != 0) { - TypeName = other.TypeName; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(pb::CodedInputStream input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - switch(tag) { - default: - input.SkipLastField(); - break; - case 10: { - TypeName = input.ReadString(); - break; - } - } - } - } - - } - - internal sealed partial class SinkRef : pb::IMessage { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new SinkRef()); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pbr::MessageDescriptor Descriptor { - get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[1]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public SinkRef() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public SinkRef(SinkRef other) : this() { - TargetRef = other.targetRef_ != null ? other.TargetRef.Clone() : null; - EventType = other.eventType_ != null ? other.EventType.Clone() : null; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public SinkRef Clone() { - return new SinkRef(this); - } - - /// Field number for the "targetRef" field. - public const int TargetRefFieldNumber = 1; - private global::Akka.Streams.Serialization.Proto.Msg.ActorRef targetRef_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public global::Akka.Streams.Serialization.Proto.Msg.ActorRef TargetRef { - get { return targetRef_; } - set { - targetRef_ = value; - } - } - - /// Field number for the "eventType" field. - public const int EventTypeFieldNumber = 2; - private global::Akka.Streams.Serialization.Proto.Msg.EventType eventType_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public global::Akka.Streams.Serialization.Proto.Msg.EventType EventType { - get { return eventType_; } - set { - eventType_ = value; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override bool Equals(object other) { - return Equals(other as SinkRef); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public bool Equals(SinkRef other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (!object.Equals(TargetRef, other.TargetRef)) return false; - if (!object.Equals(EventType, other.EventType)) return false; - return true; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override int GetHashCode() { - int hash = 1; - if (targetRef_ != null) hash ^= TargetRef.GetHashCode(); - if (eventType_ != null) hash ^= EventType.GetHashCode(); - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void WriteTo(pb::CodedOutputStream output) { - if (targetRef_ != null) { - output.WriteRawTag(10); - output.WriteMessage(TargetRef); - } - if (eventType_ != null) { - output.WriteRawTag(18); - output.WriteMessage(EventType); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public int CalculateSize() { - int size = 0; - if (targetRef_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(TargetRef); - } - if (eventType_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(EventType); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(SinkRef other) { - if (other == null) { - return; - } - if (other.targetRef_ != null) { - if (targetRef_ == null) { - targetRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); - } - TargetRef.MergeFrom(other.TargetRef); - } - if (other.eventType_ != null) { - if (eventType_ == null) { - eventType_ = new global::Akka.Streams.Serialization.Proto.Msg.EventType(); - } - EventType.MergeFrom(other.EventType); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(pb::CodedInputStream input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - switch(tag) { - default: - input.SkipLastField(); - break; - case 10: { - if (targetRef_ == null) { - targetRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); - } - input.ReadMessage(targetRef_); - break; - } - case 18: { - if (eventType_ == null) { - eventType_ = new global::Akka.Streams.Serialization.Proto.Msg.EventType(); - } - input.ReadMessage(eventType_); - break; - } - } - } - } - - } - - internal sealed partial class SourceRef : pb::IMessage { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new SourceRef()); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pbr::MessageDescriptor Descriptor { - get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[2]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public SourceRef() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public SourceRef(SourceRef other) : this() { - OriginRef = other.originRef_ != null ? other.OriginRef.Clone() : null; - EventType = other.eventType_ != null ? other.EventType.Clone() : null; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public SourceRef Clone() { - return new SourceRef(this); - } - - /// Field number for the "originRef" field. - public const int OriginRefFieldNumber = 1; - private global::Akka.Streams.Serialization.Proto.Msg.ActorRef originRef_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public global::Akka.Streams.Serialization.Proto.Msg.ActorRef OriginRef { - get { return originRef_; } - set { - originRef_ = value; - } - } - - /// Field number for the "eventType" field. - public const int EventTypeFieldNumber = 2; - private global::Akka.Streams.Serialization.Proto.Msg.EventType eventType_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public global::Akka.Streams.Serialization.Proto.Msg.EventType EventType { - get { return eventType_; } - set { - eventType_ = value; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override bool Equals(object other) { - return Equals(other as SourceRef); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public bool Equals(SourceRef other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (!object.Equals(OriginRef, other.OriginRef)) return false; - if (!object.Equals(EventType, other.EventType)) return false; - return true; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override int GetHashCode() { - int hash = 1; - if (originRef_ != null) hash ^= OriginRef.GetHashCode(); - if (eventType_ != null) hash ^= EventType.GetHashCode(); - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void WriteTo(pb::CodedOutputStream output) { - if (originRef_ != null) { - output.WriteRawTag(10); - output.WriteMessage(OriginRef); - } - if (eventType_ != null) { - output.WriteRawTag(18); - output.WriteMessage(EventType); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public int CalculateSize() { - int size = 0; - if (originRef_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(OriginRef); - } - if (eventType_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(EventType); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(SourceRef other) { - if (other == null) { - return; - } - if (other.originRef_ != null) { - if (originRef_ == null) { - originRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); - } - OriginRef.MergeFrom(other.OriginRef); - } - if (other.eventType_ != null) { - if (eventType_ == null) { - eventType_ = new global::Akka.Streams.Serialization.Proto.Msg.EventType(); - } - EventType.MergeFrom(other.EventType); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(pb::CodedInputStream input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - switch(tag) { - default: - input.SkipLastField(); - break; - case 10: { - if (originRef_ == null) { - originRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); - } - input.ReadMessage(originRef_); - break; - } - case 18: { - if (eventType_ == null) { - eventType_ = new global::Akka.Streams.Serialization.Proto.Msg.EventType(); - } - input.ReadMessage(eventType_); - break; - } - } - } - } - - } - - internal sealed partial class ActorRef : pb::IMessage { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new ActorRef()); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pbr::MessageDescriptor Descriptor { - get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[3]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public ActorRef() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public ActorRef(ActorRef other) : this() { - path_ = other.path_; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public ActorRef Clone() { - return new ActorRef(this); - } - - /// Field number for the "path" field. - public const int PathFieldNumber = 1; - private string path_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public string Path { - get { return path_; } - set { - path_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override bool Equals(object other) { - return Equals(other as ActorRef); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public bool Equals(ActorRef other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Path != other.Path) return false; - return true; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override int GetHashCode() { - int hash = 1; - if (Path.Length != 0) hash ^= Path.GetHashCode(); - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void WriteTo(pb::CodedOutputStream output) { - if (Path.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Path); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public int CalculateSize() { - int size = 0; - if (Path.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Path); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(ActorRef other) { - if (other == null) { - return; - } - if (other.Path.Length != 0) { - Path = other.Path; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(pb::CodedInputStream input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - switch(tag) { - default: - input.SkipLastField(); - break; - case 10: { - Path = input.ReadString(); - break; - } - } - } - } - - } - - internal sealed partial class Payload : pb::IMessage { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Payload()); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pbr::MessageDescriptor Descriptor { - get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[4]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public Payload() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public Payload(Payload other) : this() { - enclosedMessage_ = other.enclosedMessage_; - serializerId_ = other.serializerId_; - messageManifest_ = other.messageManifest_; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public Payload Clone() { - return new Payload(this); - } - - /// Field number for the "enclosedMessage" field. - public const int EnclosedMessageFieldNumber = 1; - private pb::ByteString enclosedMessage_ = pb::ByteString.Empty; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public pb::ByteString EnclosedMessage { - get { return enclosedMessage_; } - set { - enclosedMessage_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "serializerId" field. - public const int SerializerIdFieldNumber = 2; - private int serializerId_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public int SerializerId { - get { return serializerId_; } - set { - serializerId_ = value; - } - } - - /// Field number for the "messageManifest" field. - public const int MessageManifestFieldNumber = 3; - private pb::ByteString messageManifest_ = pb::ByteString.Empty; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public pb::ByteString MessageManifest { - get { return messageManifest_; } - set { - messageManifest_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override bool Equals(object other) { - return Equals(other as Payload); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public bool Equals(Payload other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (EnclosedMessage != other.EnclosedMessage) return false; - if (SerializerId != other.SerializerId) return false; - if (MessageManifest != other.MessageManifest) return false; - return true; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override int GetHashCode() { - int hash = 1; - if (EnclosedMessage.Length != 0) hash ^= EnclosedMessage.GetHashCode(); - if (SerializerId != 0) hash ^= SerializerId.GetHashCode(); - if (MessageManifest.Length != 0) hash ^= MessageManifest.GetHashCode(); - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void WriteTo(pb::CodedOutputStream output) { - if (EnclosedMessage.Length != 0) { - output.WriteRawTag(10); - output.WriteBytes(EnclosedMessage); - } - if (SerializerId != 0) { - output.WriteRawTag(16); - output.WriteInt32(SerializerId); - } - if (MessageManifest.Length != 0) { - output.WriteRawTag(26); - output.WriteBytes(MessageManifest); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public int CalculateSize() { - int size = 0; - if (EnclosedMessage.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeBytesSize(EnclosedMessage); - } - if (SerializerId != 0) { - size += 1 + pb::CodedOutputStream.ComputeInt32Size(SerializerId); - } - if (MessageManifest.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeBytesSize(MessageManifest); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(Payload other) { - if (other == null) { - return; - } - if (other.EnclosedMessage.Length != 0) { - EnclosedMessage = other.EnclosedMessage; - } - if (other.SerializerId != 0) { - SerializerId = other.SerializerId; - } - if (other.MessageManifest.Length != 0) { - MessageManifest = other.MessageManifest; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(pb::CodedInputStream input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - switch(tag) { - default: - input.SkipLastField(); - break; - case 10: { - EnclosedMessage = input.ReadBytes(); - break; - } - case 16: { - SerializerId = input.ReadInt32(); - break; - } - case 26: { - MessageManifest = input.ReadBytes(); - break; - } - } - } - } - - } - - internal sealed partial class OnSubscribeHandshake : pb::IMessage { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new OnSubscribeHandshake()); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pbr::MessageDescriptor Descriptor { - get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[5]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public OnSubscribeHandshake() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public OnSubscribeHandshake(OnSubscribeHandshake other) : this() { - TargetRef = other.targetRef_ != null ? other.TargetRef.Clone() : null; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public OnSubscribeHandshake Clone() { - return new OnSubscribeHandshake(this); - } - - /// Field number for the "targetRef" field. - public const int TargetRefFieldNumber = 1; - private global::Akka.Streams.Serialization.Proto.Msg.ActorRef targetRef_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public global::Akka.Streams.Serialization.Proto.Msg.ActorRef TargetRef { - get { return targetRef_; } - set { - targetRef_ = value; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override bool Equals(object other) { - return Equals(other as OnSubscribeHandshake); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public bool Equals(OnSubscribeHandshake other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (!object.Equals(TargetRef, other.TargetRef)) return false; - return true; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override int GetHashCode() { - int hash = 1; - if (targetRef_ != null) hash ^= TargetRef.GetHashCode(); - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void WriteTo(pb::CodedOutputStream output) { - if (targetRef_ != null) { - output.WriteRawTag(10); - output.WriteMessage(TargetRef); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public int CalculateSize() { - int size = 0; - if (targetRef_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(TargetRef); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(OnSubscribeHandshake other) { - if (other == null) { - return; - } - if (other.targetRef_ != null) { - if (targetRef_ == null) { - targetRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); - } - TargetRef.MergeFrom(other.TargetRef); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(pb::CodedInputStream input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - switch(tag) { - default: - input.SkipLastField(); - break; - case 10: { - if (targetRef_ == null) { - targetRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); - } - input.ReadMessage(targetRef_); - break; - } - } - } - } - - } - - internal sealed partial class CumulativeDemand : pb::IMessage { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new CumulativeDemand()); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pbr::MessageDescriptor Descriptor { - get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[6]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public CumulativeDemand() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public CumulativeDemand(CumulativeDemand other) : this() { - seqNr_ = other.seqNr_; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public CumulativeDemand Clone() { - return new CumulativeDemand(this); - } - - /// Field number for the "seqNr" field. - public const int SeqNrFieldNumber = 1; - private long seqNr_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public long SeqNr { - get { return seqNr_; } - set { - seqNr_ = value; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override bool Equals(object other) { - return Equals(other as CumulativeDemand); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public bool Equals(CumulativeDemand other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (SeqNr != other.SeqNr) return false; - return true; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override int GetHashCode() { - int hash = 1; - if (SeqNr != 0L) hash ^= SeqNr.GetHashCode(); - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void WriteTo(pb::CodedOutputStream output) { - if (SeqNr != 0L) { - output.WriteRawTag(8); - output.WriteInt64(SeqNr); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public int CalculateSize() { - int size = 0; - if (SeqNr != 0L) { - size += 1 + pb::CodedOutputStream.ComputeInt64Size(SeqNr); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(CumulativeDemand other) { - if (other == null) { - return; - } - if (other.SeqNr != 0L) { - SeqNr = other.SeqNr; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(pb::CodedInputStream input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - switch(tag) { - default: - input.SkipLastField(); - break; - case 8: { - SeqNr = input.ReadInt64(); - break; - } - } - } - } - - } - - internal sealed partial class SequencedOnNext : pb::IMessage { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new SequencedOnNext()); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pbr::MessageDescriptor Descriptor { - get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[7]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public SequencedOnNext() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public SequencedOnNext(SequencedOnNext other) : this() { - seqNr_ = other.seqNr_; - Payload = other.payload_ != null ? other.Payload.Clone() : null; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public SequencedOnNext Clone() { - return new SequencedOnNext(this); - } - - /// Field number for the "seqNr" field. - public const int SeqNrFieldNumber = 1; - private long seqNr_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public long SeqNr { - get { return seqNr_; } - set { - seqNr_ = value; - } - } - - /// Field number for the "payload" field. - public const int PayloadFieldNumber = 2; - private global::Akka.Streams.Serialization.Proto.Msg.Payload payload_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public global::Akka.Streams.Serialization.Proto.Msg.Payload Payload { - get { return payload_; } - set { - payload_ = value; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override bool Equals(object other) { - return Equals(other as SequencedOnNext); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public bool Equals(SequencedOnNext other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (SeqNr != other.SeqNr) return false; - if (!object.Equals(Payload, other.Payload)) return false; - return true; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override int GetHashCode() { - int hash = 1; - if (SeqNr != 0L) hash ^= SeqNr.GetHashCode(); - if (payload_ != null) hash ^= Payload.GetHashCode(); - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void WriteTo(pb::CodedOutputStream output) { - if (SeqNr != 0L) { - output.WriteRawTag(8); - output.WriteInt64(SeqNr); - } - if (payload_ != null) { - output.WriteRawTag(18); - output.WriteMessage(Payload); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public int CalculateSize() { - int size = 0; - if (SeqNr != 0L) { - size += 1 + pb::CodedOutputStream.ComputeInt64Size(SeqNr); - } - if (payload_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Payload); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(SequencedOnNext other) { - if (other == null) { - return; - } - if (other.SeqNr != 0L) { - SeqNr = other.SeqNr; - } - if (other.payload_ != null) { - if (payload_ == null) { - payload_ = new global::Akka.Streams.Serialization.Proto.Msg.Payload(); - } - Payload.MergeFrom(other.Payload); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(pb::CodedInputStream input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - switch(tag) { - default: - input.SkipLastField(); - break; - case 8: { - SeqNr = input.ReadInt64(); - break; - } - case 18: { - if (payload_ == null) { - payload_ = new global::Akka.Streams.Serialization.Proto.Msg.Payload(); - } - input.ReadMessage(payload_); - break; - } - } - } - } - - } - - internal sealed partial class RemoteStreamFailure : pb::IMessage { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new RemoteStreamFailure()); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pbr::MessageDescriptor Descriptor { - get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[8]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public RemoteStreamFailure() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public RemoteStreamFailure(RemoteStreamFailure other) : this() { - cause_ = other.cause_; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public RemoteStreamFailure Clone() { - return new RemoteStreamFailure(this); - } - - /// Field number for the "cause" field. - public const int CauseFieldNumber = 1; - private pb::ByteString cause_ = pb::ByteString.Empty; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public pb::ByteString Cause { - get { return cause_; } - set { - cause_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override bool Equals(object other) { - return Equals(other as RemoteStreamFailure); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public bool Equals(RemoteStreamFailure other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Cause != other.Cause) return false; - return true; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override int GetHashCode() { - int hash = 1; - if (Cause.Length != 0) hash ^= Cause.GetHashCode(); - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void WriteTo(pb::CodedOutputStream output) { - if (Cause.Length != 0) { - output.WriteRawTag(10); - output.WriteBytes(Cause); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public int CalculateSize() { - int size = 0; - if (Cause.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeBytesSize(Cause); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(RemoteStreamFailure other) { - if (other == null) { - return; - } - if (other.Cause.Length != 0) { - Cause = other.Cause; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(pb::CodedInputStream input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - switch(tag) { - default: - input.SkipLastField(); - break; - case 10: { - Cause = input.ReadBytes(); - break; - } - } - } - } - - } - - internal sealed partial class RemoteStreamCompleted : pb::IMessage { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new RemoteStreamCompleted()); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public static pbr::MessageDescriptor Descriptor { - get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[9]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public RemoteStreamCompleted() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public RemoteStreamCompleted(RemoteStreamCompleted other) : this() { - seqNr_ = other.seqNr_; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public RemoteStreamCompleted Clone() { - return new RemoteStreamCompleted(this); - } - - /// Field number for the "seqNr" field. - public const int SeqNrFieldNumber = 1; - private long seqNr_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public long SeqNr { - get { return seqNr_; } - set { - seqNr_ = value; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override bool Equals(object other) { - return Equals(other as RemoteStreamCompleted); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public bool Equals(RemoteStreamCompleted other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (SeqNr != other.SeqNr) return false; - return true; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override int GetHashCode() { - int hash = 1; - if (SeqNr != 0L) hash ^= SeqNr.GetHashCode(); - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void WriteTo(pb::CodedOutputStream output) { - if (SeqNr != 0L) { - output.WriteRawTag(8); - output.WriteInt64(SeqNr); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public int CalculateSize() { - int size = 0; - if (SeqNr != 0L) { - size += 1 + pb::CodedOutputStream.ComputeInt64Size(SeqNr); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(RemoteStreamCompleted other) { - if (other == null) { - return; - } - if (other.SeqNr != 0L) { - SeqNr = other.SeqNr; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - public void MergeFrom(pb::CodedInputStream input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - switch(tag) { - default: - input.SkipLastField(); - break; - case 8: { - SeqNr = input.ReadInt64(); - break; - } - } - } - } - - } - - #endregion - -} - -#endregion Designer generated code diff --git a/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs b/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs deleted file mode 100644 index 14196a3188e..00000000000 --- a/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs +++ /dev/null @@ -1,235 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2018 Lightbend Inc. -// Copyright (C) 2013-2018 .NET Foundation -// -//----------------------------------------------------------------------- - -using System; -using System.Text; -using Akka.Actor; -using Akka.Serialization; -using Akka.Streams.Implementation; -using Akka.Streams.Serialization.Proto.Msg; -using Akka.Util; -using Google.Protobuf; -using Akka.Streams.Dsl; -using CumulativeDemand = Akka.Streams.Dsl.CumulativeDemand; -using OnSubscribeHandshake = Akka.Streams.Dsl.OnSubscribeHandshake; -using RemoteStreamCompleted = Akka.Streams.Dsl.RemoteStreamCompleted; -using RemoteStreamFailure = Akka.Streams.Dsl.RemoteStreamFailure; -using SequencedOnNext = Akka.Streams.Dsl.SequencedOnNext; - -namespace Akka.Streams.Serialization -{ - public sealed class StreamRefSerializer : SerializerWithStringManifest - { - private readonly ExtendedActorSystem _system; - private readonly Akka.Serialization.Serialization _serialization; - - private const string SequencedOnNextManifest = "A"; - private const string CumulativeDemandManifest = "B"; - private const string RemoteSinkFailureManifest = "C"; - private const string RemoteSinkCompletedManifest = "D"; - private const string SourceRefManifest = "E"; - private const string SinkRefManifest = "F"; - private const string OnSubscribeHandshakeManifest = "G"; - - public StreamRefSerializer(ExtendedActorSystem system) : base(system) - { - _system = system; - _serialization = system.Serialization; - } - - public override string Manifest(object o) - { - switch (o) - { - case SequencedOnNext _: return SequencedOnNextManifest; - case CumulativeDemand _: return CumulativeDemandManifest; - case OnSubscribeHandshake _: return OnSubscribeHandshakeManifest; - case RemoteStreamFailure _: return RemoteSinkFailureManifest; - case RemoteStreamCompleted _: return RemoteSinkCompletedManifest; - case SourceRefImpl _: return SourceRefManifest; - case SinkRefImpl _: return SinkRefManifest; - default: throw new ArgumentException($"Unsupported object of type {o.GetType()}", nameof(o)); - } - } - - public override byte[] ToBinary(object o) - { - switch (o) - { - case SequencedOnNext onNext: return SerializeSequencedOnNext(onNext).ToByteArray(); - case CumulativeDemand demand: return SerializeCumulativeDemand(demand).ToByteArray(); - case OnSubscribeHandshake handshake: return SerializeOnSubscribeHandshake(handshake).ToByteArray(); - case RemoteStreamFailure failure: return SerializeRemoteStreamFailure(failure).ToByteArray(); - case RemoteStreamCompleted completed: return SerializeRemoteStreamCompleted(completed).ToByteArray(); - case SourceRefImpl sourceRef: return SerializeSourceRef(sourceRef).ToByteArray(); - case SinkRefImpl sinkRef: return SerializeSinkRef(sinkRef).ToByteArray(); - default: throw new ArgumentException($"Unsupported object of type {o.GetType()}", nameof(o)); - } - } - - public override object FromBinary(byte[] bytes, string manifest) - { - switch (manifest) - { - case SequencedOnNextManifest: return DeserializeSequenceOnNext(bytes); - case CumulativeDemandManifest: return DeserializeCumulativeDemand(bytes); - case OnSubscribeHandshakeManifest: return DeserializeOnSubscribeHandshake(bytes); - case RemoteSinkFailureManifest: return DeserializeRemoteSinkFailure(bytes); - case RemoteSinkCompletedManifest: return DeserializeRemoteSinkCompleted(bytes); - case SourceRefManifest: return DeserializeSourceRef(bytes); - case SinkRefManifest: return DeserializeSinkRef(bytes); - default: throw new ArgumentException($"Unsupported manifest '{manifest}'", nameof(manifest)); - } - } - - private Type TypeFromProto(Proto.Msg.EventType eventType) - { - var typeName = eventType.TypeName; - return Type.GetType(typeName, throwOnError: true); - } - - private Proto.Msg.EventType TypeToProto(Type clrType) => new Proto.Msg.EventType - { - TypeName = clrType.TypeQualifiedName() - }; - - private SinkRefImpl DeserializeSinkRef(byte[] bytes) - { - var sinkRef = SinkRef.Parser.ParseFrom(bytes); - var type = TypeFromProto(sinkRef.EventType); - var targetRef = _system.Provider.ResolveActorRef(sinkRef.TargetRef.Path); - return SinkRefImpl.Create(type, targetRef); - } - - private SourceRefImpl DeserializeSourceRef(byte[] bytes) - { - var sourceRef = SourceRef.Parser.ParseFrom(bytes); - var type = TypeFromProto(sourceRef.EventType); - var originRef = _system.Provider.ResolveActorRef(sourceRef.OriginRef.Path); - return SourceRefImpl.Create(type, originRef); - } - - private RemoteStreamCompleted DeserializeRemoteSinkCompleted(byte[] bytes) - { - var completed = Proto.Msg.RemoteStreamCompleted.Parser.ParseFrom(bytes); - return new RemoteStreamCompleted(completed.SeqNr); - } - - private RemoteStreamFailure DeserializeRemoteSinkFailure(byte[] bytes) - { - var failure = Proto.Msg.RemoteStreamFailure.Parser.ParseFrom(bytes); - var errorMessage = Encoding.UTF8.GetString(failure.Cause.ToByteArray()); - return new RemoteStreamFailure(errorMessage); - } - - private OnSubscribeHandshake DeserializeOnSubscribeHandshake(byte[] bytes) - { - var handshake = Proto.Msg.OnSubscribeHandshake.Parser.ParseFrom(bytes); - var targetRef = _system.Provider.ResolveActorRef(handshake.TargetRef.Path); - return new OnSubscribeHandshake(targetRef); - } - - private CumulativeDemand DeserializeCumulativeDemand(byte[] bytes) - { - var demand = Proto.Msg.CumulativeDemand.Parser.ParseFrom(bytes); - return new CumulativeDemand(demand.SeqNr); - } - - private SequencedOnNext DeserializeSequenceOnNext(byte[] bytes) - { - var onNext = Proto.Msg.SequencedOnNext.Parser.ParseFrom(bytes); - var serializer = _serialization.GetSerializerById(onNext.Payload.SerializerId); - object payload; - if (onNext.Payload.MessageManifest != null) - { - var manifest = Encoding.UTF8.GetString(onNext.Payload.MessageManifest.ToByteArray()); - if (serializer is SerializerWithStringManifest s) - { - payload = s.FromBinary(onNext.Payload.EnclosedMessage.ToByteArray(), manifest); - } - else - { - var type = Type.GetType(manifest, throwOnError: true); - payload = serializer.FromBinary(onNext.Payload.EnclosedMessage.ToByteArray(), type); - } - } - else - { - payload = serializer.FromBinary(onNext.Payload.EnclosedMessage.ToByteArray(), null); - } - - return new SequencedOnNext(onNext.SeqNr, payload); - } - - private ByteString SerializeSinkRef(SinkRefImpl sinkRef) => new SinkRef - { - EventType = TypeToProto(sinkRef.EventType), - TargetRef = new ActorRef - { - Path = Akka.Serialization.Serialization.SerializedActorPath(sinkRef.InitialPartnerRef) - } - }.ToByteString(); - - private ByteString SerializeSourceRef(SourceRefImpl sourceRef) - { - return new SourceRef - { - EventType = TypeToProto(sourceRef.EventType), - OriginRef = new ActorRef - { - Path = Akka.Serialization.Serialization.SerializedActorPath(sourceRef.InitialPartnerRef) - } - }.ToByteString(); - } - - private ByteString SerializeRemoteStreamCompleted(RemoteStreamCompleted completed) => - new Proto.Msg.RemoteStreamCompleted { SeqNr = completed.SeqNr }.ToByteString(); - - private ByteString SerializeRemoteStreamFailure(RemoteStreamFailure failure) => new Proto.Msg.RemoteStreamFailure - { - Cause = ByteString.CopyFromUtf8(failure.Message) - }.ToByteString(); - - private ByteString SerializeOnSubscribeHandshake(OnSubscribeHandshake handshake) => - new Proto.Msg.OnSubscribeHandshake - { - TargetRef = new ActorRef - { Path = Akka.Serialization.Serialization.SerializedActorPath(handshake.TargetRef) } - }.ToByteString(); - - private ByteString SerializeCumulativeDemand(CumulativeDemand demand) => - new Proto.Msg.CumulativeDemand { SeqNr = demand.SeqNr }.ToByteString(); - - private ByteString SerializeSequencedOnNext(SequencedOnNext onNext) - { - var payload = onNext.Payload; - var serializer = _serialization.FindSerializerFor(payload); - string manifest = null; - if (serializer.IncludeManifest) - { - manifest = serializer is SerializerWithStringManifest s - ? s.Manifest(payload) - : payload.GetType().TypeQualifiedName(); - } - - var p = new Payload - { - EnclosedMessage = ByteString.CopyFrom(serializer.ToBinary(payload)), - SerializerId = serializer.Identifier - }; - - if (!string.IsNullOrEmpty(manifest)) - p.MessageManifest = ByteString.CopyFromUtf8(manifest); - - return new Proto.Msg.SequencedOnNext - { - SeqNr = onNext.SeqNr, - Payload = p - }.ToByteString(); - } - } -} \ No newline at end of file diff --git a/src/core/Akka.Streams/Stage/GraphStage.cs b/src/core/Akka.Streams/Stage/GraphStage.cs index 55c091d9b50..08613227e97 100644 --- a/src/core/Akka.Streams/Stage/GraphStage.cs +++ b/src/core/Akka.Streams/Stage/GraphStage.cs @@ -859,18 +859,18 @@ protected GraphStageLogic(Shape shape) : this(shape.Inlets.Count(), shape.Outlet /// public virtual bool KeepGoingAfterAllPortsClosed => false; - private StageActor _stageActor; + private StageActorRef _stageActorRef; /// /// TBD /// - public StageActor StageActor + public StageActorRef StageActorRef { get { - if (_stageActor == null) + if (_stageActorRef == null) throw StageActorRefNotInitializedException.Instance; - return _stageActor; + return _stageActorRef; } } @@ -1619,35 +1619,21 @@ protected Action GetAsyncCallback(Action handler) /// Callback that will be called upon receiving of a message by this special Actor /// Minimal actor with watch method [ApiMayChange] - protected StageActor GetStageActor(StageActorRef.Receive receive) + protected StageActorRef GetStageActorRef(StageActorRef.Receive receive) { - if (_stageActor == null) + if (_stageActorRef == null) { var actorMaterializer = ActorMaterializerHelper.Downcast(Interpreter.Materializer); - _stageActor = new StageActor( - actorMaterializer, - r => GetAsyncCallback>(message => r(message)), - receive, - StageActorName); + var provider = ((IInternalActorRef)actorMaterializer.Supervisor).Provider; + var path = actorMaterializer.Supervisor.Path / StageActorRef.Name.Next(); + _stageActorRef = new StageActorRef(provider, actorMaterializer.Logger, r => GetAsyncCallback>(tuple => r(tuple)), receive, path); } else - _stageActor.Become(receive); + _stageActorRef.Become(receive); - return _stageActor; + return _stageActorRef; } - /// - /// Override and return a name to be given to the StageActor of this stage. - /// - /// This method will be only invoked and used once, during the first - /// invocation whichc reates the actor, since subsequent `getStageActors` calls function - /// like `become`, rather than creating new actors. - /// - /// Returns an empty string by default, which means that the name will a unique generated String (e.g. "$$a"). - /// - [ApiMayChange] - protected virtual string StageActorName => ""; - /// /// TBD /// @@ -1658,10 +1644,10 @@ protected internal virtual void BeforePreStart() { } /// protected internal virtual void AfterPostStop() { - if (_stageActor != null) + if (_stageActorRef != null) { - _stageActor.Stop(); - _stageActor = null; + _stageActorRef.Stop(); + _stageActorRef = null; } } @@ -2363,97 +2349,225 @@ public override void OnDownstreamFinish() } } - public static class StageActorRef - { - public delegate void Receive(Tuple args); - } - /// /// Minimal actor to work with other actors and watch them in a synchronous ways. /// - public sealed class StageActor + public sealed class StageActorRef : MinimalActorRef { + /// + /// TBD + /// + /// TBD + public delegate void Receive(Tuple args); + + /// + /// TBD + /// + public readonly IImmutableSet StageTerminatedTombstone = null; + + /// + /// TBD + /// + public static readonly EnumerableActorName Name = new EnumerableActorNameImpl("StageActorRef", new AtomicCounterLong(0L)); + + /// + /// TBD + /// + public readonly ILoggingAdapter Log; private readonly Action> _callback; - private readonly ActorCell _cell; - private readonly FunctionRef _functionRef; - private StageActorRef.Receive _behavior; - - public StageActor( - ActorMaterializer materializer, - Func>> getAsyncCallback, - StageActorRef.Receive initialReceive, - string name = null) + private readonly AtomicReference> _watchedBy = new AtomicReference>(ImmutableHashSet.Empty); + + private volatile Receive _behavior; + private IImmutableSet _watching = ImmutableHashSet.Empty; + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + public StageActorRef(IActorRefProvider provider, ILoggingAdapter log, Func>> getAsyncCallback, Receive initialReceive, ActorPath path) { - _callback = getAsyncCallback(InternalReceive); + Log = log; + Provider = provider; _behavior = initialReceive; + Path = path; - switch (materializer.Supervisor) - { - case LocalActorRef r: _cell = r.Cell; break; - case RepointableActorRef r: _cell = (ActorCell)r.Underlying; break; - default: throw new IllegalStateException($"Stream supervisor must be a local actor, was [{materializer.Supervisor.GetType()}]"); - } + _callback = getAsyncCallback(args => _behavior(args)); + } - _functionRef = _cell.AddFunctionRef((sender, message) => + /// + /// TBD + /// + public override ActorPath Path { get; } + + /// + /// TBD + /// + public override IActorRefProvider Provider { get; } + + /// + /// TBD + /// + public override bool IsTerminated => _watchedBy.Value == StageTerminatedTombstone; + + private void LogIgnored(object message) => Log.Warning($"{message} message sent to StageActorRef({Path}) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + /// + /// TBD + /// + /// TBD + /// TBD + protected override void TellInternal(object message, IActorRef sender) + { + switch (message) { - switch (message) - { - case PoisonPill _: - case Kill _: - materializer.Logger.Warning("{0} message sent to StageActor({1}) will be ignored, since it is not a real Actor. " + - "Use a custom message type to communicate with it instead.", message, _functionRef.Path); + case PoisonPill _: + case Kill _: + LogIgnored(message); + return; + case Terminated t: + if (_watching.Contains(t.ActorRef)) + { + _watching.Remove(t.ActorRef); + _callback(Tuple.Create(sender, message)); break; - default: _callback(Tuple.Create(sender, message)); break; - } - }); + } + else return; + default: + _callback(Tuple.Create(sender, message)); + break; + } } /// - /// The by which this can be contacted from the outside. - /// This is a full-fledged that supports watching and being watched - /// as well as location transparent (remote) communication. + /// TBD /// - public IActorRef Ref => _functionRef; + /// TBD + public override void SendSystemMessage(ISystemMessage message) + { + if (message is DeathWatchNotification death) + Tell(new Terminated(death.Actor, true, false), ActorRefs.NoSender); + else if (message is Watch w) + AddWatcher(w.Watchee, w.Watcher); + else if (message is Unwatch u) + RemoveWatcher(u.Watchee, u.Watcher); + } /// - /// Special `Become` allowing to swap the behaviour of this . - /// Unbecome is not available. + /// TBD /// - public void Become(StageActorRef.Receive receive) => Volatile.Write(ref _behavior, receive); + /// TBD + public void Become(Receive behavior) => _behavior = behavior; + + private void SendTerminated() + { + var watchedBy = _watchedBy.GetAndSet(StageTerminatedTombstone); + if (watchedBy != StageTerminatedTombstone) + { + foreach (var actorRef in watchedBy.Cast()) + SendTerminated(actorRef); + + foreach (var actorRef in _watching.Cast()) + UnwatchWatched(actorRef); + + _watching = ImmutableHashSet.Empty; + } + } /// - /// Stops current . + /// TBD /// - public void Stop() => _cell.RemoveFunctionRef(_functionRef); + /// TBD + public void Watch(IActorRef actorRef) + { + var iw = (IInternalActorRef) actorRef; + _watching = _watching.Add(actorRef); + iw.SendSystemMessage(new Watch(iw, this)); + } /// - /// Makes current watch over given . - /// It will be notified when an underlying actor is . + /// TBD /// - /// - public void Watch(IActorRef actorRef) => _functionRef.Watch(actorRef); - + /// TBD + public void Unwatch(IActorRef actorRef) + { + var iw = (IInternalActorRef)actorRef; + _watching = _watching.Remove(actorRef); + iw.SendSystemMessage(new Unwatch(iw, this)); + } + /// - /// Makes current stop watching previously ed . - /// If was not watched over, this method has no result. + /// TBD /// - /// - public void Unwatch(IActorRef actorRef) => _functionRef.Unwatch(actorRef); + public override void Stop() => SendTerminated(); + + private void SendTerminated(IInternalActorRef actorRef) + => actorRef.SendSystemMessage(new DeathWatchNotification(this, true, false)); + + private void UnwatchWatched(IInternalActorRef actorRef) => actorRef.SendSystemMessage(new Unwatch(actorRef, this)); - internal void InternalReceive(Tuple pack) + private void AddWatcher(IInternalActorRef watchee, IInternalActorRef watcher) { - if (pack.Item2 is Terminated terminated) + while (true) { - if (_functionRef.IsWatching(terminated.ActorRef)) + var watchedBy = _watchedBy.Value; + if (watchedBy == StageTerminatedTombstone) + SendTerminated(watcher); + else { - _functionRef.Unwatch(terminated.ActorRef); - _behavior(pack); + var isWatcheeSelf = Equals(watchee, this); + var isWatcherSelf = Equals(watcher, this); + + if (isWatcheeSelf && !isWatcherSelf) + { + if (!watchedBy.Contains(watcher)) + if (!_watchedBy.CompareAndSet(watchedBy, watchedBy.Add(watcher))) + continue; // try again + } + else if (!isWatcheeSelf && isWatcherSelf) + Log.Warning("externally triggered watch from {0} to {1} is illegal on StageActorRef", + watcher, watchee); + else + Log.Error("BUG: illegal Watch({0}, {1}) for {2}", watchee, watcher, this); } + + break; } - else _behavior(pack); } - } - + + private void RemoveWatcher(IInternalActorRef watchee, IInternalActorRef watcher) + { + while (true) + { + var watchedBy = _watchedBy.Value; + if (watchedBy == null) + SendTerminated(watcher); + else + { + var isWatcheeSelf = Equals(watchee, this); + var isWatcherSelf = Equals(watcher, this); + + if (isWatcheeSelf && !isWatcherSelf) + { + if (!watchedBy.Contains(watcher)) + if (!_watchedBy.CompareAndSet(watchedBy, watchedBy.Remove(watcher))) + continue; // try again + } + else if (!isWatcheeSelf && isWatcherSelf) + Log.Warning("externally triggered unwatch from {0} to {1} is illegal on StageActorRef", + watcher, watchee); + else + Log.Error("BUG: illegal Watch({0}, {1}) for {2}", watchee, watcher, this); + } + + break; + } + } + } + /// /// /// This class wraps callback for instances and gracefully handles diff --git a/src/core/Akka.Streams/StreamRefs.cs b/src/core/Akka.Streams/StreamRefs.cs deleted file mode 100644 index 5ad80c7d7a1..00000000000 --- a/src/core/Akka.Streams/StreamRefs.cs +++ /dev/null @@ -1,112 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2015-2016 Lightbend Inc. -// Copyright (C) 2013-2016 Akka.NET project -// -//----------------------------------------------------------------------- - -using System; -using Akka.Actor; -using Akka.Pattern; -using Akka.Streams.Dsl; -using Akka.Streams.Implementation; - -namespace Akka.Streams -{ - /// - /// A allows sharing a "reference" to a with others, - /// with the main purpose of crossing a network boundary. Usually obtaining a SinkRef would be done via Actor messaging, - /// in which one system asks a remote one, to accept some data from it, and the remote one decides to accept the - /// request to send data in a back-pressured streaming fashion -- using a sink ref. - /// - /// To create a you have to materialize the that you want to obtain - /// a reference to by attaching it to a . - /// - /// Stream refs can be seen as Reactive Streams over network boundaries. - /// - /// For additional configuration see `reference.conf` as well as . - /// - /// - public interface ISinkRef - { - Sink Sink { get; } - } - - /// - /// A SourceRef allows sharing a "reference" with others, with the main purpose of crossing a network boundary. - /// Usually obtaining a SourceRef would be done via Actor messaging, in which one system asks a remote one, - /// to share some data with it, and the remote one decides to do so in a back-pressured streaming fashion -- using a stream ref. - /// - /// To create a you have to materialize the that you want to - /// obtain a reference to by attaching it to a . - /// - /// Stream refs can be seen as Reactive Streams over network boundaries. - /// - /// For additional configuration see `reference.conf` as well as . - /// - /// - public interface ISourceRef - { - Source Source { get; } - } - - public sealed class TargetRefNotInitializedYetException : IllegalStateException - { - public TargetRefNotInitializedYetException() : base( - "Internal remote target actor ref not yet resolved, yet attempted to send messages to it. " + - "This should not happen due to proper flow-control, please open a ticket on the issue tracker: https://github.com/akkadotnet/akka.net") - { - } - } - - public sealed class StreamRefSubscriptionTimeoutException : IllegalStateException - { - public StreamRefSubscriptionTimeoutException(string message) : base(message) - { - } - } - - public sealed class RemoteStreamRefActorTerminatedException : Exception - { - public RemoteStreamRefActorTerminatedException(string message) : base(message) - { - } - } - - public sealed class InvalidSequenceNumberException : IllegalStateException - { - public long ExpectedSeqNr { get; } - public long GotSeqNr { get; } - - public InvalidSequenceNumberException(long expectedSeqNr, long gotSeqNr, string message) : base( - $"{message} (expected: {expectedSeqNr}, got: {gotSeqNr}). In most cases this means that message loss on this connection has occurred and the stream will fail eagerly.") - { - ExpectedSeqNr = expectedSeqNr; - GotSeqNr = gotSeqNr; - } - } - - /// - /// Stream refs establish a connection between a local and remote actor, representing the origin and remote sides - /// of a stream. Each such actor refers to the other side as its "partner". We make sure that no other actor than - /// the initial partner can send demand/messages to the other side accidentally. - /// - /// This exception is thrown when a message is recived from a non-partner actor, - /// which could mean a bug or some actively malicient behavior from the other side. - /// - /// This is not meant as a security feature, but rather as plain sanity-check. - /// - public sealed class InvalidPartnerActorException : IllegalStateException - { - public IActorRef ExpectedRef { get; } - public IActorRef GotRef { get; } - - public InvalidPartnerActorException(IActorRef expectedRef, IActorRef gotRef, string message) : base( - $"{message} (expected: {expectedRef}, got: {gotRef}). "+ - "This may happen due to 'double-materialization' on the other side of this stream ref. " + - "Do note that stream refs are one-shot references and have to be paired up in 1:1 pairs. " + - "Multi-cast such as broadcast etc can be implemented by sharing multiple new stream references. ") - { - } - } -} \ No newline at end of file diff --git a/src/core/Akka.Streams/reference.conf b/src/core/Akka.Streams/reference.conf index 3c48cf4183e..f6815aa6b88 100644 --- a/src/core/Akka.Streams/reference.conf +++ b/src/core/Akka.Streams/reference.conf @@ -83,30 +83,6 @@ akka { # Note: If you change this value also change the fallback value in ActorMaterializerSettings fuzzing-mode = off } - - stream-ref { - # Buffer of a SinkRef that is used to batch Request elements from the other side of the stream ref - # - # The buffer will be attempted to be filled eagerly even while the local stage did not request elements, - # because the delay of requesting over network boundaries is much higher. - buffer-capacity = 32 - - # Demand is signalled by sending a cumulative demand message ("requesting messages until the n-th sequence number) - # Using a cumulative demand model allows us to re-deliver the demand message in case of message loss (which should - # be very rare in any case, yet possible -- mostly under connection break-down and re-establishment). - # - # The semantics of handling and updating the demand however are in-line with what Reactive Streams dictates. - # - # In normal operation, demand is signalled in response to arriving elements, however if no new elements arrive - # within `demand-redelivery-interval` a re-delivery of the demand will be triggered, assuming that it may have gotten lost. - demand-redelivery-interval = 1 second - - # Subscription timeout, during which the "remote side" MUST subscribe (materialize) the handed out stream ref. - # This timeout does not have to be very low in normal situations, since the remote side may also need to - # prepare things before it is ready to materialize the reference. However the timeout is needed to avoid leaking - # in-active streams which are never subscribed to. - subscription-timeout = 30 seconds - } } # Fully qualified config path which holds the dispatcher configuration @@ -126,26 +102,9 @@ akka { } } } - + # configure overrides to ssl-configuration here (to be used by akka-streams, and akka-http – i.e. when serving https connections) ssl-config { protocol = "TLSv1" } - - actor { - - serializers { - akka-stream-ref = "Akka.Streams.Serialization.StreamRefSerializer, Akka.Streams" - } - - serialization-bindings { - "Akka.Streams.Dsl.SinkRefImpl, Akka.Streams" = akka-stream-ref - "Akka.Streams.Dsl.SourceRefImpl, Akka.Streams" = akka-stream-ref - "Akka.Streams.Dsl.IStreamRefsProtocol, Akka.Streams" = akka-stream-ref - } - - serialization-identifiers { - "Akka.Streams.Serialization.StreamRefSerializer, Akka.Streams" = 30 - } - } } diff --git a/src/core/Akka.Tests/Actor/FunctionRefSpec.cs b/src/core/Akka.Tests/Actor/FunctionRefSpec.cs deleted file mode 100644 index f860a0b7f78..00000000000 --- a/src/core/Akka.Tests/Actor/FunctionRefSpec.cs +++ /dev/null @@ -1,253 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2018 Lightbend Inc. -// Copyright (C) 2018 Akka.NET project -// -//----------------------------------------------------------------------- - -using System; -using Akka.Actor; -using Akka.Configuration; -using Akka.TestKit; -using Xunit; -using Xunit.Abstractions; - -namespace Akka.Tests.Actor -{ - public class FunctionRefSpec : AkkaSpec - { - #region internal classes - - sealed class GetForwarder : IEquatable - { - public IActorRef ReplyTo { get; } - - public GetForwarder(IActorRef replyTo) - { - ReplyTo = replyTo; - } - - public bool Equals(GetForwarder other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Equals(ReplyTo, other.ReplyTo); - } - - public override bool Equals(object obj) => obj is GetForwarder forwarder && Equals(forwarder); - - public override int GetHashCode() => (ReplyTo != null ? ReplyTo.GetHashCode() : 0); - } - - sealed class DropForwarder : IEquatable - { - public FunctionRef Ref { get; } - - public DropForwarder(FunctionRef @ref) - { - Ref = @ref; - } - - public bool Equals(DropForwarder other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Equals(Ref, other.Ref); - } - - public override bool Equals(object obj) => obj is DropForwarder forwarder && Equals(forwarder); - - public override int GetHashCode() => (Ref != null ? Ref.GetHashCode() : 0); - } - - sealed class Forwarded : IEquatable - { - public object Message { get; } - public IActorRef Sender { get; } - - public Forwarded(object message, IActorRef sender) - { - Message = message; - Sender = sender; - } - - public bool Equals(Forwarded other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Equals(Message, other.Message) && Equals(Sender, other.Sender); - } - - public override bool Equals(object obj) => obj is Forwarded forwarded && Equals(forwarded); - - public override int GetHashCode() - { - unchecked - { - return ((Message != null ? Message.GetHashCode() : 0) * 397) ^ (Sender != null ? Sender.GetHashCode() : 0); - } - } - } - - sealed class Super : ReceiveActor - { - public Super() - { - Receive(get => - { - var cell = (ActorCell)Context; - var fref = cell.AddFunctionRef((sender, msg) => - { - get.ReplyTo.Tell(new Forwarded(msg, sender)); - }); - get.ReplyTo.Tell(fref); - }); - Receive(drop => { - var cell = (ActorCell)Context; - cell.RemoveFunctionRef(drop.Ref); - }); - } - } - - sealed class SupSuper : ReceiveActor - { - public SupSuper() - { - var s = Context.ActorOf(Props.Create(), "super"); - ReceiveAny(msg => s.Tell(msg)); - } - } - - #endregion - - public FunctionRefSpec(ITestOutputHelper output) : base(output, null) - { - } - - #region top level - - [Fact] - public void FunctionRef_created_by_top_level_actor_must_forward_messages() - { - var s = SuperActor(); - var forwarder = GetFunctionRef(s); - - forwarder.Tell("hello"); - ExpectMsg(new Forwarded("hello", TestActor)); - } - - [Fact] - public void FunctionRef_created_by_top_level_actor_must_be_watchable() - { - var s = SuperActor(); - var forwarder = GetFunctionRef(s); - - s.Tell(new GetForwarder(TestActor)); - var f = ExpectMsg(); - Watch(f); - s.Tell(new DropForwarder(f)); - ExpectTerminated(f); - } - - [Fact] - public void FunctionRef_created_by_top_level_actor_must_be_able_to_watch() - { - var s = SuperActor(); - var forwarder = GetFunctionRef(s); - - s.Tell(new GetForwarder(TestActor)); - var f = ExpectMsg(); - forwarder.Watch(f); - s.Tell(new DropForwarder(f)); - ExpectMsg(new Forwarded(new Terminated(f, true, false), f)); - } - - [Fact] - public void FunctionRef_created_by_top_level_actor_must_terminate_when_their_parent_terminates() - { - var s = SuperActor(); - var forwarder = GetFunctionRef(s); - - Watch(forwarder); - s.Tell(PoisonPill.Instance); - ExpectTerminated(forwarder); - } - - private FunctionRef GetFunctionRef(IActorRef s) - { - s.Tell(new GetForwarder(TestActor)); - return ExpectMsg(); - } - - private IActorRef SuperActor() => Sys.ActorOf(Props.Create(), "super"); - - #endregion - - #region non-top level - - [Fact] - public void FunctionRef_created_by_non_top_level_actor_must_forward_messages() - { - var s = SupSuperActor(); - var forwarder = GetFunctionRef(s); - - forwarder.Tell("hello"); - ExpectMsg(new Forwarded("hello", TestActor)); - } - - [Fact] - public void FunctionRef_created_by_non_top_level_actor_must_be_watchable() - { - var s = SupSuperActor(); - var forwarder = GetFunctionRef(s); - - s.Tell(new GetForwarder(TestActor)); - var f = ExpectMsg(); - Watch(f); - s.Tell(new DropForwarder(f)); - ExpectTerminated(f); - } - - [Fact] - public void FunctionRef_created_by_non_top_level_actor_must_be_able_to_watch() - { - var s = SupSuperActor(); - var forwarder = GetFunctionRef(s); - - s.Tell(new GetForwarder(TestActor)); - var f = ExpectMsg(); - forwarder.Watch(f); - s.Tell(new DropForwarder(f)); - ExpectMsg(new Forwarded(new Terminated(f, true, false), f)); - } - - [Fact] - public void FunctionRef_created_by_non_top_level_actor_must_terminate_when_their_parent_terminates() - { - var s = SupSuperActor(); - var forwarder = GetFunctionRef(s); - - Watch(forwarder); - s.Tell(PoisonPill.Instance); - ExpectTerminated(forwarder); - } - - private IActorRef SupSuperActor() => Sys.ActorOf(Props.Create(), "supsuper"); - - #endregion - - [Fact(Skip = "FIXME")] - public void FunctionRef_when_not_registered_must_not_be_found() - { - var provider = ((ExtendedActorSystem) Sys).Provider; - var fref = new FunctionRef(TestActor.Path / "blabla", provider, Sys.EventStream, (x, y) => { }); - EventFilter.Exception().ExpectOne(() => - { - // needs to be something that fails when the deserialized form is not a FunctionRef - // this relies upon serialize-messages during tests - TestActor.Tell(new DropForwarder(fref)); - ExpectNoMsg(TimeSpan.FromSeconds(1)); - }); - } - } -} \ No newline at end of file diff --git a/src/core/Akka.Tests/Akka.Tests.csproj b/src/core/Akka.Tests/Akka.Tests.csproj index 7a89113d73a..bd521e79bbf 100644 --- a/src/core/Akka.Tests/Akka.Tests.csproj +++ b/src/core/Akka.Tests/Akka.Tests.csproj @@ -1,14 +1,17 @@  + Akka.Tests net452;netcoreapp1.1 + + @@ -18,22 +21,28 @@ + + + + $(DefineConstants);SERIALIZATION;CONFIGURATION;UNSAFE_THREADING + $(DefineConstants);CORECLR + $(DefineConstants);RELEASE diff --git a/src/core/Akka/Actor/ActorCell.Children.cs b/src/core/Akka/Actor/ActorCell.Children.cs index 6ffeeaa8813..6682b11ecf1 100644 --- a/src/core/Akka/Actor/ActorCell.Children.cs +++ b/src/core/Akka/Actor/ActorCell.Children.cs @@ -7,8 +7,6 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.Text; using System.Threading; using Akka.Actor.Internal; using Akka.Serialization; @@ -21,53 +19,18 @@ public partial class ActorCell { private volatile IChildrenContainer _childrenContainerDoNotCallMeDirectly = EmptyChildrenContainer.Instance; private long _nextRandomNameDoNotCallMeDirectly = -1; // Interlocked.Increment automatically adds 1 to this value. Allows us to start from 0. - private ImmutableDictionary _functionRefsDoNotCallMeDirectly = ImmutableDictionary.Empty; /// /// The child container collection, used to house information about all child actors. /// - public IChildrenContainer ChildrenContainer => _childrenContainerDoNotCallMeDirectly; - - private IReadOnlyCollection Children => ChildrenContainer.Children; - - private ImmutableDictionary FunctionRefs => Volatile.Read(ref _functionRefsDoNotCallMeDirectly); - - internal bool TryGetFunctionRef(string name, out FunctionRef functionRef) => - FunctionRefs.TryGetValue(name, out functionRef); - - internal bool TryGetFunctionRef(string name, int uid, out FunctionRef functionRef) => - FunctionRefs.TryGetValue(name, out functionRef) && (uid == ActorCell.UndefinedUid || uid == functionRef.Path.Uid); - - internal FunctionRef AddFunctionRef(Action tell, string suffix = "") + public IChildrenContainer ChildrenContainer { - var r = GetRandomActorName("$$"); - var n = string.IsNullOrEmpty(suffix) ? r : r + "-" + suffix; - var childPath = new ChildActorPath(Self.Path, n, NewUid()); - var functionRef = new FunctionRef(childPath, SystemImpl.Provider, SystemImpl.EventStream, tell); - - return ImmutableInterlocked.GetOrAdd(ref _functionRefsDoNotCallMeDirectly, childPath.Name, functionRef); - } - - internal bool RemoveFunctionRef(FunctionRef functionRef) - { - if (functionRef.Path.Parent != Self.Path) throw new InvalidOperationException($"Trying to remove FunctionRef {functionRef.Path} from wrong ActorCell"); - - var name = functionRef.Path.Name; - if (ImmutableInterlocked.TryRemove(ref _functionRefsDoNotCallMeDirectly, name, out var fref)) - { - fref.Stop(); - return true; - } - else return false; + get { return _childrenContainerDoNotCallMeDirectly; } } - protected void StopFunctionRefs() + private IReadOnlyCollection Children { - var refs = Interlocked.Exchange(ref _functionRefsDoNotCallMeDirectly, ImmutableDictionary.Empty); - foreach (var pair in refs) - { - pair.Value.Stop(); - } + get { return ChildrenContainer.Children; } } /// @@ -124,11 +87,10 @@ private IActorRef ActorOf(Props props, string name, bool isAsync, bool isSystemS return MakeChild(props, name, isAsync, isSystemService); } - private string GetRandomActorName(string prefix = "$") + private string GetRandomActorName() { var id = Interlocked.Increment(ref _nextRandomNameDoNotCallMeDirectly); - var sb = new StringBuilder(prefix); - return id.Base64Encode(sb).ToString(); + return "$" + id.Base64Encode(); } /// @@ -369,7 +331,8 @@ protected bool TryGetChildStatsByRef(IActorRef actor, out ChildRestartStats chil [Obsolete("Use TryGetSingleChild [0.7.1]")] public IInternalActorRef GetSingleChild(string name) { - return TryGetSingleChild(name, out var child) ? child : ActorRefs.Nobody; + IInternalActorRef child; + return TryGetSingleChild(name, out child) ? child : ActorRefs.Nobody; } /// @@ -383,21 +346,18 @@ public bool TryGetSingleChild(string name, out IInternalActorRef child) if (name.IndexOf('#') < 0) { // optimization for the non-uid case - if (TryGetChildRestartStatsByName(name, out var stats)) + ChildRestartStats stats; + if (TryGetChildRestartStatsByName(name, out stats)) { child = stats.Child; return true; } - else if (TryGetFunctionRef(name, out var functionRef)) - { - child = functionRef; - return true; - } } else { var nameAndUid = SplitNameAndUid(name); - if (TryGetChildRestartStatsByName(nameAndUid.Name, out var stats)) + ChildRestartStats stats; + if (TryGetChildRestartStatsByName(nameAndUid.Name, out stats)) { var uid = nameAndUid.Uid; if (uid == ActorCell.UndefinedUid || uid == stats.Uid) @@ -406,11 +366,6 @@ public bool TryGetSingleChild(string name, out IInternalActorRef child) return true; } } - else if (TryGetFunctionRef(nameAndUid.Name, nameAndUid.Uid, out var functionRef)) - { - child = functionRef; - return true; - } } child = ActorRefs.Nobody; return false; @@ -423,7 +378,8 @@ public bool TryGetSingleChild(string name, out IInternalActorRef child) /// TBD protected SuspendReason RemoveChildAndGetStateChange(IActorRef child) { - if (ChildrenContainer is TerminatingChildrenContainer terminating) + var terminating = ChildrenContainer as TerminatingChildrenContainer; + if (terminating != null) { var newContainer = UpdateChildrenRefs(c => c.Remove(child)); if (newContainer is TerminatingChildrenContainer) return null; diff --git a/src/core/Akka/Actor/ActorCell.FaultHandling.cs b/src/core/Akka/Actor/ActorCell.FaultHandling.cs index a8e185b3b97..09a5eb17ef4 100644 --- a/src/core/Akka/Actor/ActorCell.FaultHandling.cs +++ b/src/core/Akka/Actor/ActorCell.FaultHandling.cs @@ -304,25 +304,21 @@ private void FinishTerminate() try { Parent.SendSystemMessage(new DeathWatchNotification(_self, existenceConfirmed: true, addressTerminated: false)); } finally { - try { StopFunctionRefs(); } + try { TellWatchersWeDied(); } finally { - try { TellWatchersWeDied(); } + try { UnwatchWatchedActors(a); } // stay here as we expect an emergency stop from HandleInvokeFailure finally { - try { UnwatchWatchedActors(a); } // stay here as we expect an emergency stop from HandleInvokeFailure - finally - { - if (System.Settings.DebugLifecycle) - Publish(new Debug(_self.Path.ToString(), ActorType, "Stopped")); + if (System.Settings.DebugLifecycle) + Publish(new Debug(_self.Path.ToString(), ActorType, "Stopped")); - ClearActor(a); - ClearActorCell(); + ClearActor(a); + ClearActorCell(); - _actor = null; + _actor = null; - } - } + } } } } diff --git a/src/core/Akka/Actor/ActorRef.cs b/src/core/Akka/Actor/ActorRef.cs index 77c01cd7b7b..9f0007f585d 100644 --- a/src/core/Akka/Actor/ActorRef.cs +++ b/src/core/Akka/Actor/ActorRef.cs @@ -9,7 +9,6 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -842,204 +841,5 @@ IEnumerator IEnumerable.GetEnumerator() } } } - - /// - /// INTERNAL API - /// - /// This kind of ActorRef passes all received messages to the given function for - /// performing a non-blocking side-effect. The intended use is to transform the - /// message before sending to the real target actor. Such references can be created - /// by calling and must be deregistered when no longer - /// needed by calling . FunctionRefs do not count - /// towards the live children of an actor, they do not receive the Terminate command - /// and do not prevent the parent from terminating. FunctionRef is properly - /// registered for remote lookup and ActorSelection. - /// - /// When using the feature you must ensure that upon reception of the - /// Terminated message the watched actorRef is ed. - /// - internal sealed class FunctionRef : MinimalActorRef - { - private readonly EventStream _eventStream; - private readonly Action _tell; - - private ImmutableHashSet _watching = ImmutableHashSet.Empty; - private ImmutableHashSet _watchedBy = ImmutableHashSet.Empty; - - public FunctionRef(ActorPath path, IActorRefProvider provider, EventStream eventStream, Action tell) - { - _eventStream = eventStream; - _tell = tell; - Path = path; - Provider = provider; - } - - public override ActorPath Path { get; } - public override IActorRefProvider Provider { get; } - public override bool IsTerminated => Volatile.Read(ref _watchedBy) == null; - - /// - /// Have this FunctionRef watch the given Actor. This method must not be - /// called concurrently from different threads, it should only be called by - /// its parent Actor. - /// - /// Upon receiving the Terminated message, must be called from a - /// safe context (i.e. normally from the parent Actor). - /// - public void Watch(IActorRef actorRef) - { - _watching = _watching.Add(actorRef); - var internalRef = (IInternalActorRef) actorRef; - internalRef.SendSystemMessage(new Watch(internalRef, this)); - } - - /// - /// Have this FunctionRef unwatch the given Actor. This method must not be - /// called concurrently from different threads, it should only be called by - /// its parent Actor. - /// - public void Unwatch(IActorRef actorRef) - { - _watching = _watching.Remove(actorRef); - var internalRef = (IInternalActorRef) actorRef; - internalRef.SendSystemMessage(new Unwatch(internalRef, this)); - - } - - /// - /// Query whether this FunctionRef is currently watching the given Actor. This - /// method must not be called concurrently from different threads, it should - /// only be called by its parent Actor. - /// - public bool IsWatching(IActorRef actorRef) => _watching.Contains(actorRef); - - protected override void TellInternal(object message, IActorRef sender) => _tell(sender, message); - - public override void SendSystemMessage(ISystemMessage message) - { - switch (message) - { - case Watch watch: - AddWatcher(watch.Watchee, watch.Watcher); - break; - case Unwatch unwatch: - RemoveWatcher(unwatch.Watchee, unwatch.Watcher); - break; - case DeathWatchNotification deathWatch: - this.Tell(new Terminated(deathWatch.Actor, existenceConfirmed: true, addressTerminated: false), deathWatch.Actor); - break; - } - } - - private void SendTerminated() - { - var watchedBy = Interlocked.Exchange(ref _watchedBy, null); - if (watchedBy != null) - { - if (!watchedBy.IsEmpty) - { - foreach (var watcher in watchedBy) - SendTerminated(watcher); - } - - if (!_watching.IsEmpty) - { - foreach (var watched in _watching) - UnwatchWatched(watched); - - _watching = ImmutableHashSet.Empty; - } - } - } - - private void SendTerminated(IActorRef watcher) - { - if (watcher is IInternalActorRef scope) - scope.SendSystemMessage(new DeathWatchNotification(this, existenceConfirmed: true, addressTerminated: false)); - } - - private void UnwatchWatched(IActorRef watched) - { - if (watched is IInternalActorRef internalActorRef) - internalActorRef.SendSystemMessage(new Unwatch(internalActorRef, this)); - } - - public override void Stop() => SendTerminated(); - - private void AddWatcher(IInternalActorRef watchee, IInternalActorRef watcher) - { - while (true) - { - var watchedBy = Volatile.Read(ref _watchedBy); - if (watchedBy == null) - SendTerminated(watcher); - else - { - var watcheeSelf = Equals(watchee, this); - var watcherSelf = Equals(watcher, this); - - if (watcheeSelf && !watcherSelf) - { - if (!watchedBy.Contains(watcher) && !ReferenceEquals(watchedBy, Interlocked.CompareExchange(ref _watchedBy, watchedBy.Add(watcher), watchedBy))) - { - continue; - } - } - else if (!watcheeSelf && watcherSelf) - { - Publish(new Warning(Path.ToString(), typeof(FunctionRef), $"Externally triggered watch from {watcher} to {watchee} is illegal on FunctionRef")); - } - else - { - Publish(new Warning(Path.ToString(), typeof(FunctionRef), $"BUG: illegal Watch({watchee},{watcher}) for {this}")); - } - } - - break; - } - } - - private void RemoveWatcher(IInternalActorRef watchee, IInternalActorRef watcher) - { - while (true) - { - var watchedBy = Volatile.Read(ref _watchedBy); - if (watchedBy == null) - SendTerminated(watcher); - else - { - var watcheeSelf = Equals(watchee, this); - var watcherSelf = Equals(watcher, this); - - if (watcheeSelf && !watcherSelf) - { - if (!watchedBy.Contains(watcher) && !ReferenceEquals(watchedBy, Interlocked.CompareExchange(ref _watchedBy, watchedBy.Remove(watcher), watchedBy))) - { - continue; - } - } - else if (!watcheeSelf && watcherSelf) - { - Publish(new Warning(Path.ToString(), typeof(FunctionRef), $"Externally triggered watch from {watcher} to {watchee} is illegal on FunctionRef")); - } - else - { - Publish(new Warning(Path.ToString(), typeof(FunctionRef), $"BUG: illegal Watch({watchee},{watcher}) for {this}")); - } - } - - break; - } - } - - private void Publish(LogEvent e) - { - try - { - _eventStream.Publish(e); - } - catch (Exception) { } - } - } } diff --git a/src/core/Akka/Actor/IAutoReceivedMessage.cs b/src/core/Akka/Actor/IAutoReceivedMessage.cs index 58284fe1587..4759f0f303e 100644 --- a/src/core/Akka/Actor/IAutoReceivedMessage.cs +++ b/src/core/Akka/Actor/IAutoReceivedMessage.cs @@ -5,7 +5,6 @@ // //----------------------------------------------------------------------- -using System; using Akka.Event; namespace Akka.Actor @@ -22,7 +21,7 @@ public interface IAutoReceivedMessage /// Terminated message can't be forwarded to another actor, since that actor might not be watching the subject. /// Instead, if you need to forward Terminated to another actor you should send the information in your own message. /// - public sealed class Terminated : IAutoReceivedMessage, IPossiblyHarmful, IDeadLetterSuppression, INoSerializationVerificationNeeded, IEquatable + public sealed class Terminated : IAutoReceivedMessage, IPossiblyHarmful, IDeadLetterSuppression, INoSerializationVerificationNeeded { /// /// Initializes a new instance of the class. @@ -60,27 +59,7 @@ public Terminated(IActorRef actorRef, bool existenceConfirmed, bool addressTermi /// A that represents this instance. public override string ToString() { - return $"Terminated(ref: {ActorRef}, existenceConfirmed: {ExistenceConfirmed}, addressTerminated: {AddressTerminated})"; - } - - public bool Equals(Terminated other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Equals(ActorRef, other.ActorRef) && AddressTerminated == other.AddressTerminated && ExistenceConfirmed == other.ExistenceConfirmed; - } - - public override bool Equals(object obj) => obj is Terminated terminated && Equals(terminated); - - public override int GetHashCode() - { - unchecked - { - var hashCode = (ActorRef != null ? ActorRef.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ AddressTerminated.GetHashCode(); - hashCode = (hashCode * 397) ^ ExistenceConfirmed.GetHashCode(); - return hashCode; - } + return $": {ActorRef} - ExistenceConfirmed={ExistenceConfirmed}"; } } diff --git a/src/core/Akka/Actor/RepointableActorRef.cs b/src/core/Akka/Actor/RepointableActorRef.cs index e2f9ab5e48b..8c5f44bd169 100644 --- a/src/core/Akka/Actor/RepointableActorRef.cs +++ b/src/core/Akka/Actor/RepointableActorRef.cs @@ -293,7 +293,8 @@ public override IActorRef GetChild(IEnumerable name) return ActorRefs.Nobody; default: var nameAndUid = ActorCell.SplitNameAndUid(next); - if (Lookup.TryGetChildStatsByName(nameAndUid.Name, out var stats)) + IChildStats stats; + if (Lookup.TryGetChildStatsByName(nameAndUid.Name, out stats)) { var crs = stats as ChildRestartStats; var uid = nameAndUid.Uid; @@ -305,10 +306,6 @@ public override IActorRef GetChild(IEnumerable name) return crs.Child; } } - else if (Lookup is ActorCell cell && cell.TryGetFunctionRef(nameAndUid.Name, nameAndUid.Uid, out var functionRef)) - { - return functionRef; - } return ActorRefs.Nobody; } } diff --git a/src/core/Akka/Util/Base64Encoding.cs b/src/core/Akka/Util/Base64Encoding.cs index d9e8ff661c4..13d7a6d465f 100644 --- a/src/core/Akka/Util/Base64Encoding.cs +++ b/src/core/Akka/Util/Base64Encoding.cs @@ -24,15 +24,9 @@ public static class Base64Encoding /// /// TBD /// TBD - public static string Base64Encode(this long value) => Base64Encode(value, new StringBuilder()).ToString(); - - /// - /// TBD - /// - /// TBD - /// TBD - public static StringBuilder Base64Encode(this long value, StringBuilder sb) + public static string Base64Encode(this long value) { + var sb = new StringBuilder(); var next = value; do { @@ -40,7 +34,7 @@ public static StringBuilder Base64Encode(this long value, StringBuilder sb) sb.Append(Base64Chars[index]); next = next >> 6; } while(next != 0); - return sb; + return sb.ToString(); } /// diff --git a/src/protobuf/StreamRefMessages.proto b/src/protobuf/StreamRefMessages.proto deleted file mode 100644 index 69a5194a299..00000000000 --- a/src/protobuf/StreamRefMessages.proto +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (C) 2009-2017 Lightbend Inc. - * Copyright (C) 2017-2018 Akka.NET project - */ - -syntax = 'proto3'; -package Akka.Streams.Serialization.Proto.Msg; -option optimize_for = SPEED; - -/************************************************* - StreamRefs (SourceRef / SinkRef) related formats -**************************************************/ - -message EventType { - string typeName = 1; -} - -message SinkRef { - ActorRef targetRef = 1; - EventType eventType = 2; -} - -message SourceRef { - ActorRef originRef = 1; - EventType eventType = 2; -} - -message ActorRef { - string path = 1; -} - -message Payload { - bytes enclosedMessage = 1; - int32 serializerId = 2; - bytes messageManifest = 3; -} - -// stream refs protocol - -message OnSubscribeHandshake { - ActorRef targetRef = 1; -} -message CumulativeDemand { - int64 seqNr = 1; -} - -message SequencedOnNext { - int64 seqNr = 1; - Payload payload = 2; -} - -message RemoteStreamFailure { - bytes cause = 1; -} - -message RemoteStreamCompleted { - int64 seqNr = 1; -} \ No newline at end of file From 11985622b0e4748d1be5742b6ac4109d4c18a228 Mon Sep 17 00:00:00 2001 From: v1rusw0rm Date: Wed, 6 Jun 2018 20:28:11 +0400 Subject: [PATCH 24/70] Add CombineMaterialized method to Source (#3489) * Add CombineMaterialized method to Source * More reasonable materializers combine function name and type * Approve Streams API change * Move instance method to SourceOperations extensions --- .../CoreAPISpec.ApproveStreams.approved.txt | 2 ++ src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs | 35 +++++++++++++++++++ src/core/Akka.Streams/Dsl/Source.cs | 23 ++++++++++++ src/core/Akka.Streams/Dsl/SourceOperations.cs | 18 ++++++++++ 4 files changed, 78 insertions(+) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt index 595d16b2a59..036a2baf588 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt @@ -1620,6 +1620,7 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.Source ActorRef(int bufferSize, Akka.Streams.OverflowStrategy overflowStrategy) { } public static Akka.Streams.Dsl.Source> AsSubscriber() { } public static Akka.Streams.Dsl.Source Combine(Akka.Streams.Dsl.Source first, Akka.Streams.Dsl.Source second, System.Func, Akka.NotUsed>> strategy, params Akka.Streams.Dsl.Source<, >[] rest) { } + public static Akka.Streams.Dsl.Source CombineMaterialized(Akka.Streams.Dsl.Source first, Akka.Streams.Dsl.Source second, System.Func, Akka.NotUsed>> strategy, System.Func combineMaterializers) { } public static Akka.Streams.Dsl.Source Cycle(System.Func> enumeratorFactory) { } public static Akka.Streams.Dsl.Source Empty() { } public static Akka.Streams.Dsl.Source Failed(System.Exception cause) { } @@ -1682,6 +1683,7 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.Source BatchWeighted(this Akka.Streams.Dsl.Source flow, long max, System.Func costFunction, System.Func seed, System.Func aggregate) { } public static Akka.Streams.Dsl.Source Buffer(this Akka.Streams.Dsl.Source flow, int size, Akka.Streams.OverflowStrategy strategy) { } public static Akka.Streams.Dsl.Source Collect(this Akka.Streams.Dsl.Source flow, System.Func collector) { } + public static Akka.Streams.Dsl.Source CombineMaterialized(this Akka.Streams.Dsl.Source flow, Akka.Streams.Dsl.Source other, System.Func, Akka.NotUsed>> strategy, System.Func combineMaterializers) { } public static Akka.Streams.Dsl.Source CompletionTimeout(this Akka.Streams.Dsl.Source flow, System.TimeSpan timeout) { } public static Akka.Streams.Dsl.Source Concat(this Akka.Streams.Dsl.Source flow, Akka.Streams.IGraph, TMat> other) { } public static Akka.Streams.Dsl.Source ConcatMany(this Akka.Streams.Dsl.Source flow, System.Func, TMat>> flatten) { } diff --git a/src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs b/src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs index 56af56fb43f..cf34e00e34c 100644 --- a/src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/SourceSpec.cs @@ -268,6 +268,41 @@ public void Composite_Source_must_combine_from_two_inputs_with_simplified_API() outProbe.ExpectComplete(); } + [Fact] + public async Task Composite_Source_must_combine_from_two_inputs_with_CombineMaterialized_and_take_a_materialized_value() + { + var queueSource = Source.Queue(1, OverflowStrategy.DropBuffer); + var intSequenceSource = Source.From(new[] { 1, 2, 3 }); + + var combined1 = Source.CombineMaterialized(queueSource, intSequenceSource, + i => new Concat(i), Keep.Left); // Keep.left (i.e. preserve queueSource's materialized value) + var materialized1 = combined1.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Materializer); + var queue1 = materialized1.Item1; + var sinkProbe1 = materialized1.Item2; + + sinkProbe1.Request(6); + await queue1.OfferAsync(10); + await queue1.OfferAsync(20); + await queue1.OfferAsync(30); + queue1.Complete(); // complete queueSource so that combined1 with `Concat` then pulls elements from intSequenceSource + sinkProbe1.ExpectNextN(new[] { 10, 20, 30, 1, 2, 3 }); + + // queueSource to be the second of combined source + var combined2 = Source.CombineMaterialized(intSequenceSource, queueSource, + i => new Concat(i), Keep.Right); // Keep.right (i.e. preserve queueSource's materialized value) + var materialized2 = combined2.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Materializer); + var queue2 = materialized2.Item1; + var sinkProbe2 = materialized2.Item2; + + sinkProbe2.Request(6); + await queue2.OfferAsync(10); + await queue2.OfferAsync(20); + await queue2.OfferAsync(30); + queue2.Complete(); + sinkProbe2.ExpectNextN(new[] { 1, 2, 3 }); //as intSequenceSource is the first in combined source, elements from intSequenceSource come first + sinkProbe2.ExpectNextN(new[] { 10, 20, 30 }); // after intSequenceSource run out elements, queueSource elements come + } + [Fact] public void Repeat_Source_must_repeat_as_long_as_it_takes() { diff --git a/src/core/Akka.Streams/Dsl/Source.cs b/src/core/Akka.Streams/Dsl/Source.cs index f394b194de3..0f2d4afb9aa 100644 --- a/src/core/Akka.Streams/Dsl/Source.cs +++ b/src/core/Akka.Streams/Dsl/Source.cs @@ -725,6 +725,29 @@ public static Source Combine(Source first, return new SourceShape(c.Out); })); + /// + /// Combines two sources with fan-in strategy like or and returns with a materialized value. + /// + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + public static Source CombineMaterialized(Source first, Source second, Func, NotUsed>> strategy, Func combineMaterializers) + { + var secondPartiallyCombined = GraphDsl.Create(second, (b, secondShape) => + { + var c = b.Add(strategy(2)); + b.From(secondShape).To(c.In(1)); + return new FlowShape(c.In(0), c.Out); + }); + return first.ViaMaterialized(secondPartiallyCombined, combineMaterializers); + } /// /// Combines the elements of multiple streams into a stream of lists. diff --git a/src/core/Akka.Streams/Dsl/SourceOperations.cs b/src/core/Akka.Streams/Dsl/SourceOperations.cs index bebd0b0e161..0acd5d3d06e 100644 --- a/src/core/Akka.Streams/Dsl/SourceOperations.cs +++ b/src/core/Akka.Streams/Dsl/SourceOperations.cs @@ -2039,6 +2039,24 @@ public static Source Concat(this Source flow return (Source)InternalFlowOperations.Concat(flow, other); } + /// + /// Combines the given to this with fan-in strategy like or and returns with a materialized value. + /// + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + public static Source CombineMaterialized(this Source flow, Source other, Func, NotUsed>> strategy, Func combineMaterializers) + { + return Source.CombineMaterialized(flow, other, strategy, combineMaterializers); + } + /// /// Prepend the given to this , meaning that before elements /// are generated from this , the Source's elements will be produced until it From c6c30f073e2795ca31f9a496337c98411e7ec636 Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Mon, 11 Jun 2018 19:55:57 +0200 Subject: [PATCH 25/70] non-blocking task completion source helpers (#3499) --- .../Implementation/IO/TcpStages.cs | 12 +++-- src/core/Akka/Actor/Futures.cs | 18 ++----- src/core/Akka/Util/Internal/TaskEx.cs | 49 +++++++++++++++++++ 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/src/core/Akka.Streams/Implementation/IO/TcpStages.cs b/src/core/Akka.Streams/Implementation/IO/TcpStages.cs index fe0330263f9..c6cf0ca7e5c 100644 --- a/src/core/Akka.Streams/Implementation/IO/TcpStages.cs +++ b/src/core/Akka.Streams/Implementation/IO/TcpStages.cs @@ -124,17 +124,19 @@ private void Receive(Tuple args) _listener.Tell(new Tcp.ResumeAccepting(1), StageActorRef); var thisStage = StageActorRef; - _bindingPromise.TrySetResult(new StreamTcp.ServerBinding(bound.LocalAddress, () => + var binding = new StreamTcp.ServerBinding(bound.LocalAddress, () => { // Beware, sender must be explicit since stageActor.ref will be invalid to access after the stage stopped thisStage.Tell(Tcp.Unbind.Instance, thisStage); return _unbindPromise.Task; - })); + }); + + _bindingPromise.NonBlockingTrySetResult(binding); } else if (msg is Tcp.CommandFailed) { var ex = BindFailedException.Instance; - _bindingPromise.TrySetException(ex); + _bindingPromise.NonBlockingTrySetException(ex); _unbindPromise.TrySetResult(NotUsed.Instance); FailStage(ex); } @@ -162,7 +164,7 @@ private void Receive(Tuple args) public override void PostStop() { _unbindPromise.TrySetResult(NotUsed.Instance); - _bindingPromise.TrySetException( + _bindingPromise.NonBlockingTrySetException( new NoSuchElementException("Binding was unbound before it was completely finished")); } } @@ -220,7 +222,7 @@ public ConnectionSourceStage(IActorRef tcpManager, EndPoint endpoint, int backlo /// TBD public override ILogicAndMaterializedValue> CreateLogicAndMaterializedValue(Attributes inheritedAttributes) { - var bindingPromise = new TaskCompletionSource(); + var bindingPromise = TaskEx.NonBlockingTaskCompletionSource(); var logic = new ConnectionSourceStageLogic(Shape, this, bindingPromise); return new LogicAndMaterializedValue>(logic, bindingPromise.Task); } diff --git a/src/core/Akka/Actor/Futures.cs b/src/core/Akka/Actor/Futures.cs index 81d8002a301..b9bef521445 100644 --- a/src/core/Akka/Actor/Futures.cs +++ b/src/core/Akka/Actor/Futures.cs @@ -146,23 +146,11 @@ internal static IActorRefProvider ResolveProvider(ICanTell self) return null; } - - private const int RunContinuationsAsynchronously = 64; - private static readonly bool isRunContinuationsAsynchronouslyAvailable = Enum.IsDefined(typeof(TaskCreationOptions), RunContinuationsAsynchronously); - - + private static async Task Ask(ICanTell self, Func messageFactory, IActorRefProvider provider, TimeSpan? timeout, CancellationToken cancellationToken) { - TaskCompletionSource result; - if (isRunContinuationsAsynchronouslyAvailable) - { - result = new TaskCompletionSource((TaskCreationOptions)RunContinuationsAsynchronously); - } - else - { - result = new TaskCompletionSource(); - } + TaskCompletionSource result = TaskEx.NonBlockingTaskCompletionSource(); CancellationTokenSource timeoutCancellation = null; timeout = timeout ?? provider.Settings.AskTimeout; @@ -188,7 +176,7 @@ private static async Task Ask(ICanTell self, Func mes //create a new tempcontainer path ActorPath path = provider.TempPath(); - var future = new FutureActorRef(result, () => { }, path, isRunContinuationsAsynchronouslyAvailable); + var future = new FutureActorRef(result, () => { }, path, TaskEx.IsRunContinuationsAsynchronouslyAvailable); //The future actor needs to be registered in the temp container provider.RegisterTempActor(future, path); var message = messageFactory(future); diff --git a/src/core/Akka/Util/Internal/TaskEx.cs b/src/core/Akka/Util/Internal/TaskEx.cs index b94af98e95c..38baceaf3a7 100644 --- a/src/core/Akka/Util/Internal/TaskEx.cs +++ b/src/core/Akka/Util/Internal/TaskEx.cs @@ -18,6 +18,55 @@ namespace Akka.Util.Internal /// internal static class TaskEx { + private const int RunContinuationsAsynchronously = 64; + public static readonly bool IsRunContinuationsAsynchronouslyAvailable = Enum.IsDefined(typeof(TaskCreationOptions), RunContinuationsAsynchronously); + + /// + /// Creates a new which will run in asynchronous, + /// non-blocking fashion upon calling . + /// + /// This behavior is not available on all supported versions of .NET framework, in this case it + /// should be used only together with and + /// . + /// + public static TaskCompletionSource NonBlockingTaskCompletionSource() + { + if (IsRunContinuationsAsynchronouslyAvailable) + { + return new TaskCompletionSource((TaskCreationOptions)RunContinuationsAsynchronously); + } + else + { + return new TaskCompletionSource(); + } + } + + /// + /// Tries to complete given in asynchronous, non-blocking + /// fashion. For safety reasons, this method should be called only on tasks created via + /// method. + /// + public static void NonBlockingTrySetResult(this TaskCompletionSource taskCompletionSource, T value) + { + if (IsRunContinuationsAsynchronouslyAvailable) + taskCompletionSource.TrySetResult(value); + else + Task.Run(() => taskCompletionSource.TrySetResult(value)); + } + + /// + /// Tries to set given + /// in asynchronous, non-blocking fashion. For safety reasons, this method should be called only + /// on tasks created via method. + /// + public static void NonBlockingTrySetException(this TaskCompletionSource taskCompletionSource, Exception exception) + { + if (IsRunContinuationsAsynchronouslyAvailable) + taskCompletionSource.TrySetException(exception); + else + Task.Run(() => taskCompletionSource.TrySetException(exception)); + } + /// /// A completed task /// From 09bf699f0f03638914133f65a23730b3bb779948 Mon Sep 17 00:00:00 2001 From: Sean Gilliam Date: Mon, 11 Jun 2018 19:15:46 -0500 Subject: [PATCH 26/70] [xunit] Fix xunit warnings wrt collections (#3501) * fix xunit2012 warning wrt using Enumerable.Any() * fix xunit2017 warning wrt using Enumerable.Contains() * fix xunit2013 warning wrt using Equals() and collection sizes --- .../MultiNodeClusterSpec.cs | 2 +- .../ClusterDeathWatchSpec.cs | 20 +++++++++---------- .../InitialHeartbeatSpec.cs | 10 ++-------- .../RestartNodeSpec.cs | 6 +++--- .../ClusterConsistentHashingRouterSpec.cs | 4 ++-- .../UnreachableNodeJoinsAgainSpec.cs | 6 +++--- .../TestRunCoordinatorSpec.cs | 2 +- .../Akka.Remote.Tests/RemoteConfigSpec.cs | 6 +++--- .../Transport/GenericTransportSpec.cs | 12 +++-------- src/core/Akka.Tests/Actor/ActorSystemSpec.cs | 3 +-- src/core/Akka.Tests/Actor/ActorSystemTests.cs | 2 +- ...Builder_CreateArgumentValuesArray_Tests.cs | 4 ++-- 12 files changed, 32 insertions(+), 45 deletions(-) diff --git a/src/core/Akka.Cluster.TestKit/MultiNodeClusterSpec.cs b/src/core/Akka.Cluster.TestKit/MultiNodeClusterSpec.cs index 22a005ad559..62dd976c799 100644 --- a/src/core/Akka.Cluster.TestKit/MultiNodeClusterSpec.cs +++ b/src/core/Akka.Cluster.TestKit/MultiNodeClusterSpec.cs @@ -246,7 +246,7 @@ public void StartClusterNode() if (ClusterView.Members.IsEmpty) { Cluster.Join(GetAddress(Myself)); - AwaitAssert(() => Assert.True(ClusterView.Members.Select(m => m.Address).Contains(GetAddress(Myself)))); + AwaitAssert(() => Assert.Contains(GetAddress(Myself), ClusterView.Members.Select(m => m.Address))); } } diff --git a/src/core/Akka.Cluster.Tests.MultiNode/ClusterDeathWatchSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/ClusterDeathWatchSpec.cs index e280ff7717b..9e34c183ff5 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/ClusterDeathWatchSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/ClusterDeathWatchSpec.cs @@ -120,11 +120,11 @@ public void An_actor_watching_a_remote_actor_in_the_cluster_must_receive_termina ExpectNoMsg(TimeSpan.FromSeconds(2)); EnterBarrier("second-terminated"); MarkNodeAsUnavailable(GetAddress(_config.Third)); - AwaitAssert(() => Assert.True(ClusterView.UnreachableMembers.Select(x => x.Address).Contains(GetAddress(_config.Third)))); + AwaitAssert(() => Assert.Contains(GetAddress(_config.Third), ClusterView.UnreachableMembers.Select(x => x.Address))); Cluster.Down(GetAddress(_config.Third)); //removed - AwaitAssert(() => Assert.False(ClusterView.Members.Select(x => x.Address).Contains(GetAddress(_config.Third)))); - AwaitAssert(() => Assert.False(ClusterView.UnreachableMembers.Select(x => x.Address).Contains(GetAddress(_config.Third)))); + AwaitAssert(() => Assert.DoesNotContain(GetAddress(_config.Third), ClusterView.Members.Select(x => x.Address))); + AwaitAssert(() => Assert.DoesNotContain(GetAddress(_config.Third), ClusterView.UnreachableMembers.Select(x => x.Address))); ExpectMsg(path3); EnterBarrier("third-terminated"); }, _config.First); @@ -137,11 +137,11 @@ public void An_actor_watching_a_remote_actor_in_the_cluster_must_receive_termina RunOn(() => { MarkNodeAsUnavailable(GetAddress(_config.Second)); - AwaitAssert(() => Assert.True(ClusterView.UnreachableMembers.Select(x => x.Address).Contains(GetAddress(_config.Second)))); + AwaitAssert(() => Assert.Contains(GetAddress(_config.Second), ClusterView.UnreachableMembers.Select(x => x.Address))); Cluster.Down(GetAddress(_config.Second)); //removed - AwaitAssert(() => Assert.False(ClusterView.Members.Select(x => x.Address).Contains(GetAddress(_config.Second)))); - AwaitAssert(() => Assert.False(ClusterView.UnreachableMembers.Select(x => x.Address).Contains(GetAddress(_config.Second)))); + AwaitAssert(() => Assert.DoesNotContain(GetAddress(_config.Second), ClusterView.Members.Select(x => x.Address))); + AwaitAssert(() => Assert.DoesNotContain(GetAddress(_config.Second), ClusterView.UnreachableMembers.Select(x => x.Address))); }, _config.Third); EnterBarrier("second-terminated"); EnterBarrier("third-terminated"); @@ -227,8 +227,8 @@ public void An_actor_watching_a_remote_actor_in_the_cluster_must_be_able_to_watc AwaitAssert(() => ClusterView.UnreachableMembers.Select(x => x.Address).Contains(GetAddress(_config.Fifth)).ShouldBeTrue()); Cluster.Down(GetAddress(_config.Fifth)); // removed - AwaitAssert(() => Assert.False(ClusterView.UnreachableMembers.Select(x => x.Address).Contains(GetAddress(_config.Fifth)))); - AwaitAssert(() => Assert.False(ClusterView.Members.Select(x => x.Address).Contains(GetAddress(_config.Fifth)))); + AwaitAssert(() => Assert.DoesNotContain(GetAddress(_config.Fifth), ClusterView.UnreachableMembers.Select(x => x.Address))); + AwaitAssert(() => Assert.DoesNotContain(GetAddress(_config.Fifth), ClusterView.Members.Select(x => x.Address))); }, _config.Fourth); EnterBarrier("fifth-terminated"); @@ -266,8 +266,8 @@ public void An_actor_watching_a_remote_actor_in_the_cluster_must_be_able_to_shut AwaitAssert(() => ClusterView.UnreachableMembers.Select(x => x.Address).Contains(GetAddress(_config.First)).ShouldBeTrue()); Cluster.Down(GetAddress(_config.First)); // removed - AwaitAssert(() => Assert.False(ClusterView.UnreachableMembers.Select(x => x.Address).Contains(GetAddress(_config.First)))); - AwaitAssert(() => Assert.False(ClusterView.Members.Select(x => x.Address).Contains(GetAddress(_config.First)))); + AwaitAssert(() => Assert.DoesNotContain(GetAddress(_config.First), ClusterView.UnreachableMembers.Select(x => x.Address))); + AwaitAssert(() => Assert.DoesNotContain(GetAddress(_config.First), ClusterView.Members.Select(x => x.Address))); ExpectTerminated(hello); EnterBarrier("first-unavailable"); diff --git a/src/core/Akka.Cluster.Tests.MultiNode/InitialHeartbeatSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/InitialHeartbeatSpec.cs index b2118f0ad90..f1af896d13e 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/InitialHeartbeatSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/InitialHeartbeatSpec.cs @@ -84,10 +84,7 @@ public void A_member_must_detect_failure_even_though_no_heartbeats_have_been_rec AwaitAssert(() => { Cluster.SendCurrentClusterState(TestActor); - Assert.True( - ExpectMsg() - .Members.Select(m => m.Address) - .Contains(secondAddress)); + Assert.Contains(secondAddress, ExpectMsg().Members.Select(m => m.Address)); }, TimeSpan.FromSeconds(20), TimeSpan.FromMilliseconds(50)) , _config.First); @@ -97,10 +94,7 @@ public void A_member_must_detect_failure_even_though_no_heartbeats_have_been_rec AwaitAssert(() => { Cluster.SendCurrentClusterState(TestActor); - Assert.True( - ExpectMsg() - .Members.Select(m => m.Address) - .Contains(firstAddress)); + Assert.Contains(firstAddress, ExpectMsg().Members.Select(m => m.Address)); }, TimeSpan.FromSeconds(20), TimeSpan.FromMilliseconds(50)); }, _config.Second); diff --git a/src/core/Akka.Cluster.Tests.MultiNode/RestartNodeSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/RestartNodeSpec.cs index b608a4d6fab..92cd4c8272e 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/RestartNodeSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/RestartNodeSpec.cs @@ -171,9 +171,9 @@ public void ClusterNodesMustBeAbleToRestartAndJoinAgain() AwaitAssert(() => { Assert.Equal(3, Cluster.Get(Sys).ReadView.Members.Count); - Assert.True( - Cluster.Get(Sys) - .ReadView.Members.Any(m => m.Address.Equals(SecondUniqueAddress.Address) && m.UniqueAddress.Uid != SecondUniqueAddress.Uid)); + Assert.Contains( + Cluster.Get(Sys).ReadView.Members, + m => m.Address.Equals(SecondUniqueAddress.Address) && m.UniqueAddress.Uid != SecondUniqueAddress.Uid); }); }, _config.First, _config.Third); diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingRouterSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingRouterSpec.cs index 935ffee169f..8e4000ef37e 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingRouterSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/Routing/ClusterConsistentHashingRouterSpec.cs @@ -263,8 +263,8 @@ protected void A_cluster_router_with_consistent_hashing_pool_must_remove_routees { Cluster.Down(GetAddress(_config.Third)); //removed - AwaitAssert(() => Assert.False(ClusterView.UnreachableMembers.Select(x => x.Address).Contains(GetAddress(_config.Third)))); - AwaitAssert(() => Assert.False(ClusterView.Members.Select(x => x.Address).Contains(GetAddress(_config.Third)))); + AwaitAssert(() => Assert.DoesNotContain(GetAddress(_config.Third), ClusterView.UnreachableMembers.Select(x => x.Address))); + AwaitAssert(() => Assert.DoesNotContain(GetAddress(_config.Third), ClusterView.Members.Select(x => x.Address))); // it may take some time until router receives cluster member events AwaitAssert(() => diff --git a/src/core/Akka.Cluster.Tests.MultiNode/UnreachableNodeJoinsAgainSpec.cs b/src/core/Akka.Cluster.Tests.MultiNode/UnreachableNodeJoinsAgainSpec.cs index caa989c628e..b61da48f2a2 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/UnreachableNodeJoinsAgainSpec.cs +++ b/src/core/Akka.Cluster.Tests.MultiNode/UnreachableNodeJoinsAgainSpec.cs @@ -138,12 +138,12 @@ public void MarkNodeAsUNREACHABLEWhenWePullTheNetwork() AwaitAssert(() => { var members = ClusterView.Members; // to snapshot the object - Assert.Equal(1, ClusterView.UnreachableMembers.Count); + Assert.Single(ClusterView.UnreachableMembers); }); AwaitSeenSameState(allButVictim.Select(GetAddress).ToArray()); // still once unreachable - Assert.Equal(1, ClusterView.UnreachableMembers.Count); + Assert.Single(ClusterView.UnreachableMembers); Assert.Equal(Node(_victim.Value).Address, ClusterView.UnreachableMembers.First().Address); Assert.Equal(MemberStatus.Up, ClusterView.UnreachableMembers.First().Status); }); @@ -219,7 +219,7 @@ public void AllowFreshNodeWithSameHostAndPortToJoinAgainWhenTheNetworkIsPluggedB Cluster.Get(freshSystem).Join(masterAddress); Within(TimeSpan.FromSeconds(15), () => { - AwaitAssert(() => Assert.True(Cluster.Get(freshSystem).ReadView.Members.Select(x => x.Address).Contains(victimAddress))); + AwaitAssert(() => Assert.Contains(victimAddress, Cluster.Get(freshSystem).ReadView.Members.Select(x => x.Address))); AwaitAssert(() => Assert.Equal(expectedNumberOfMembers,Cluster.Get(freshSystem).ReadView.Members.Count)); AwaitAssert(() => Assert.True(Cluster.Get(freshSystem).ReadView.Members.All(y => y.Status == MemberStatus.Up))); }); diff --git a/src/core/Akka.MultiNodeTestRunner.Shared.Tests/TestRunCoordinatorSpec.cs b/src/core/Akka.MultiNodeTestRunner.Shared.Tests/TestRunCoordinatorSpec.cs index 23a85311c89..240814dd67d 100644 --- a/src/core/Akka.MultiNodeTestRunner.Shared.Tests/TestRunCoordinatorSpec.cs +++ b/src/core/Akka.MultiNodeTestRunner.Shared.Tests/TestRunCoordinatorSpec.cs @@ -43,7 +43,7 @@ public void TestRunCoordinator_should_start_and_route_messages_to_SpecRunCoordin testRunCoordinator.Tell(new EndTestRun(), TestActor); var testRunData = ExpectMsg(); - Assert.Equal(1, testRunData.Specs.Count()); + Assert.Single(testRunData.Specs); var specMessages = new SortedSet(); foreach (var spec in testRunData.Specs) diff --git a/src/core/Akka.Remote.Tests/RemoteConfigSpec.cs b/src/core/Akka.Remote.Tests/RemoteConfigSpec.cs index 8f91ea11e0a..436c7b6b397 100644 --- a/src/core/Akka.Remote.Tests/RemoteConfigSpec.cs +++ b/src/core/Akka.Remote.Tests/RemoteConfigSpec.cs @@ -37,7 +37,7 @@ public void Remoting_should_contain_correct_configuration_values_in_ReferenceCon Assert.False(remoteSettings.LogReceive); Assert.False(remoteSettings.LogSend); Assert.False(remoteSettings.UntrustedMode); - Assert.Equal(0, remoteSettings.TrustedSelectionPaths.Count); + Assert.Empty(remoteSettings.TrustedSelectionPaths); Assert.Equal(TimeSpan.FromSeconds(10), remoteSettings.ShutdownTimeout); Assert.Equal(TimeSpan.FromSeconds(2), remoteSettings.FlushWait); Assert.Equal(TimeSpan.FromSeconds(10), remoteSettings.StartupTimeout); @@ -52,7 +52,7 @@ public void Remoting_should_contain_correct_configuration_values_in_ReferenceCon Assert.Equal(TimeSpan.FromDays(5), remoteSettings.QuarantineDuration); Assert.Equal(TimeSpan.FromDays(5), remoteSettings.QuarantineSilentSystemTimeout); Assert.Equal(TimeSpan.FromSeconds(30), remoteSettings.CommandAckTimeout); - Assert.Equal(1, remoteSettings.Transports.Length); + Assert.Single(remoteSettings.Transports); Assert.Equal(typeof(TcpTransport), Type.GetType(remoteSettings.Transports.Head().TransportClass)); Assert.Equal(typeof(PhiAccrualFailureDetector), Type.GetType(remoteSettings.WatchFailureDetectorImplementationClass)); Assert.Equal(TimeSpan.FromSeconds(1), remoteSettings.WatchHeartBeatInterval); @@ -72,7 +72,7 @@ public void Remoting_should_contain_correct_configuration_values_in_ReferenceCon var remoteSettingsAdapters = remoteSettings.Adapters.Select(kv => new KeyValuePair(kv.Key, Type.GetType(kv.Value))); - Assert.Equal(0, remoteSettingsAdapters.Except(remoteSettingsAdaptersStandart).Count()); + Assert.Empty(remoteSettingsAdapters.Except(remoteSettingsAdaptersStandart)); remoteSettings.Config.GetString("akka.remote.log-frame-size-exceeding").ShouldBe("off"); } diff --git a/src/core/Akka.Remote.Tests/Transport/GenericTransportSpec.cs b/src/core/Akka.Remote.Tests/Transport/GenericTransportSpec.cs index aff5c8922f7..dc42fb2cff7 100644 --- a/src/core/Akka.Remote.Tests/Transport/GenericTransportSpec.cs +++ b/src/core/Akka.Remote.Tests/Transport/GenericTransportSpec.cs @@ -75,9 +75,7 @@ public void Transport_must_return_an_Address_and_promise_when_listen_is_called() Assert.Equal(addressA, result.Item1); Assert.NotNull(result.Item2); - Assert.True( - registry.LogSnapshot().OfType().Any(x => x.BoundAddress == addressATest) - ); + Assert.Contains(registry.LogSnapshot().OfType(), x => x.BoundAddress == addressATest); } [Fact] @@ -104,9 +102,7 @@ public void Transport_must_associate_successfully_with_another_transport_of_its_ return null; }); - Assert.True( - registry.LogSnapshot().OfType().Any(x => x.LocalAddress == addressATest && x.RemoteAddress == addressBTest) - ); + Assert.Contains(registry.LogSnapshot().OfType(), x => x.LocalAddress == addressATest && x.RemoteAddress == addressBTest); AwaitCondition(() => registry.ExistsAssociation(addressATest, addressBTest)); } @@ -169,9 +165,7 @@ public void Transport_must_successfully_send_PDUs() return null; }); - Assert.True( - registry.LogSnapshot().OfType().Any(x => x.Sender == addressATest && x.Recipient == addressBTest && x.Payload.Equals(pdu)) - ); + Assert.Contains(registry.LogSnapshot().OfType(), x => x.Sender == addressATest && x.Recipient == addressBTest && x.Payload.Equals(pdu)); } [Fact] diff --git a/src/core/Akka.Tests/Actor/ActorSystemSpec.cs b/src/core/Akka.Tests/Actor/ActorSystemSpec.cs index 0b4c0ab40ba..472f793da21 100644 --- a/src/core/Akka.Tests/Actor/ActorSystemSpec.cs +++ b/src/core/Akka.Tests/Actor/ActorSystemSpec.cs @@ -242,8 +242,7 @@ public void Reliable_deny_creation_of_actors_while_shutting_down() var nonTerminatedOrNonstartedActors = created.Cast() .Where(actor => !actor.IsTerminated && !(actor.Underlying is UnstartedCell)).ToList(); - Assert.Equal(0, - nonTerminatedOrNonstartedActors.Count); + Assert.Empty(nonTerminatedOrNonstartedActors); } #region Extensions tests diff --git a/src/core/Akka.Tests/Actor/ActorSystemTests.cs b/src/core/Akka.Tests/Actor/ActorSystemTests.cs index 691546211ee..1b0549040eb 100644 --- a/src/core/Akka.Tests/Actor/ActorSystemTests.cs +++ b/src/core/Akka.Tests/Actor/ActorSystemTests.cs @@ -34,7 +34,7 @@ public void ActorSystem_ActorOf_adds_a_child_to_Guardian() //assert var children = system.Provider.Guardian.Children; - Assert.True(children.Any(c => c == child)); + Assert.Contains(children, c => c == child); } [Fact] diff --git a/src/core/Akka.Tests/MatchHandler/MatchExpressionBuilder_CreateArgumentValuesArray_Tests.cs b/src/core/Akka.Tests/MatchHandler/MatchExpressionBuilder_CreateArgumentValuesArray_Tests.cs index f1d1b7fae0c..6e382f750e5 100644 --- a/src/core/Akka.Tests/MatchHandler/MatchExpressionBuilder_CreateArgumentValuesArray_Tests.cs +++ b/src/core/Akka.Tests/MatchHandler/MatchExpressionBuilder_CreateArgumentValuesArray_Tests.cs @@ -26,7 +26,7 @@ public void Given_no_arguments_When_creating_Then_empty_array_is_returned() var result = builder.CreateArgumentValuesArray(emptyArguments); Assert.NotNull(result); - Assert.Equal(0, result.Length); + Assert.Empty(result); } @@ -39,7 +39,7 @@ public void Given_one_argument_When_creating_Then_array_with_the_value_is_return var result = builder.CreateArgumentValuesArray(arguments); Assert.NotNull(result); - Assert.Equal(1, result.Length); + Assert.Single(result); Assert.Same(argument.Value, result[0]); } From 9e78248af96990e9d234c732ab957076e091427d Mon Sep 17 00:00:00 2001 From: Sean Gilliam Date: Tue, 12 Jun 2018 17:13:48 -0500 Subject: [PATCH 27/70] [api-docs] fix a few xref/parameter issues (#3503) This PR fixes some xrefs issues wrt to referencing base class fields/methods. It also adds some missing method parameter tags. --- .../Akka.Cluster.Sharding/ClusterShardingSettings.cs | 2 ++ .../Singleton/ClusterSingletonManager.cs | 2 +- src/contrib/cluster/Akka.DistributedData/Dsl.cs | 2 +- .../Akka.DistributedData/Internal/Internal.cs | 2 +- .../cluster/Akka.DistributedData/ORDictionary.cs | 8 ++++---- .../Akka.DistributedData/Replicator.Messages.cs | 12 ++++++------ .../Snapshot/QueryExecutor.cs | 2 +- src/core/Akka.Cluster/Cluster.cs | 3 ++- src/core/Akka.Cluster/ClusterDaemon.cs | 4 ++-- src/core/Akka.Streams/Attributes.cs | 2 +- .../Dsl/Internal/InternalFlowOperations.cs | 6 ++++-- src/core/Akka.Streams/Dsl/Source.cs | 6 +++--- src/core/Akka.Streams/Dsl/SourceOperations.cs | 1 + src/core/Akka.Streams/Dsl/SubFlowOperations.cs | 2 ++ src/core/Akka/Actor/ActorRef.cs | 1 + src/core/Akka/Actor/Settings.cs | 2 +- src/core/Akka/Dispatch/SysMsg/ISystemMessage.cs | 7 ++++--- src/core/Akka/Pattern/BackoffOptions.cs | 2 +- 18 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingSettings.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingSettings.cs index e346460b7f3..d3483d3d970 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingSettings.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingSettings.cs @@ -94,6 +94,8 @@ public class TunningParameters /// Keep this number of old persistent batches /// TBD /// TBD + /// TBD + /// TBD /// TBD /// TBD /// TBD diff --git a/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs b/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs index 2cf0723b29c..e79459db1c5 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs @@ -463,7 +463,7 @@ public ClusterSingletonManagerIsStuckException(SerializationInfo info, Streaming /// broadcast its existence when it is started. /// /// - /// Use factory method to create the for the actor. + /// Use one of the factory methods ClusterSingletonManager.Props to create the for the actor. /// /// public sealed class ClusterSingletonManager : FSM diff --git a/src/contrib/cluster/Akka.DistributedData/Dsl.cs b/src/contrib/cluster/Akka.DistributedData/Dsl.cs index 053573db179..b10ec1e346e 100644 --- a/src/contrib/cluster/Akka.DistributedData/Dsl.cs +++ b/src/contrib/cluster/Akka.DistributedData/Dsl.cs @@ -25,7 +25,7 @@ public static class Dsl /// /// Gets a setup, which will acknowledge success of an - /// or operation immediately as soon, as + /// Update or operation immediately as soon, as /// result will be confirmed by the local replica only. /// public static WriteLocal WriteLocal => Akka.DistributedData.WriteLocal.Instance; diff --git a/src/contrib/cluster/Akka.DistributedData/Internal/Internal.cs b/src/contrib/cluster/Akka.DistributedData/Internal/Internal.cs index 57caa174f27..9419d3bf13a 100644 --- a/src/contrib/cluster/Akka.DistributedData/Internal/Internal.cs +++ b/src/contrib/cluster/Akka.DistributedData/Internal/Internal.cs @@ -830,7 +830,7 @@ private NoDelta() { } /// and thereby violating . /// /// This is used as a placeholder for such `null` delta. It's filtered out - /// in , i.e. never sent to the other replicas. + /// in , i.e. never sent to the other replicas. /// public static readonly IReplicatedDelta NoDeltaPlaceholder = NoDelta.Instance; diff --git a/src/contrib/cluster/Akka.DistributedData/ORDictionary.cs b/src/contrib/cluster/Akka.DistributedData/ORDictionary.cs index 5f5e5834b50..c3579415a5c 100644 --- a/src/contrib/cluster/Akka.DistributedData/ORDictionary.cs +++ b/src/contrib/cluster/Akka.DistributedData/ORDictionary.cs @@ -123,12 +123,12 @@ internal ORDictionary(ORSet keySet, IImmutableDictionary val /// on other nodes and the outcome depends on what /// type that is used. /// - /// Consider using instead of if you want modify + /// Consider using AddOrUpdate instead of if you want modify /// existing entry. /// /// is thrown if you try to replace an existing /// value, because important history can be lost when replacing the `ORSet` and - /// undesired effects of merging will occur. Use or instead. + /// undesired effects of merging will occur. Use or AddOrUpdate instead. /// public ORDictionary SetItem(Cluster.Cluster node, TKey key, TValue value) => SetItem(node.SelfUniqueAddress, key, value); @@ -139,12 +139,12 @@ public ORDictionary SetItem(Cluster.Cluster node, TKey key, TValue /// on other nodes and the outcome depends on what /// type that is used. /// - /// Consider using instead of if you want modify + /// Consider using AddOrUpdate instead of if you want modify /// existing entry. /// /// is thrown if you try to replace an existing /// value, because important history can be lost when replacing the `ORSet` and - /// undesired effects of merging will occur. Use or instead. + /// undesired effects of merging will occur. Use or AddOrUpdate instead. /// public ORDictionary SetItem(UniqueAddress node, TKey key, TValue value) { diff --git a/src/contrib/cluster/Akka.DistributedData/Replicator.Messages.cs b/src/contrib/cluster/Akka.DistributedData/Replicator.Messages.cs index 75b8c3be1ec..2e09338a7c2 100644 --- a/src/contrib/cluster/Akka.DistributedData/Replicator.Messages.cs +++ b/src/contrib/cluster/Akka.DistributedData/Replicator.Messages.cs @@ -317,9 +317,9 @@ public T Get(IKey key) where T : IReplicatedData } /// - /// Register a subscriber that will be notified with a message + /// Register a subscriber that will be notified with a message /// when the value of the given is changed. Current value is also - /// sent as a message to a new subscriber. + /// sent as a message to a new subscriber. /// /// Subscribers will be notified periodically with the configured `notify-subscribers-interval`, /// and it is also possible to send an explicit `FlushChanges` message to @@ -327,7 +327,7 @@ public T Get(IKey key) where T : IReplicatedData /// /// The subscriber will automatically be unregistered if it is terminated. /// - /// If the key is deleted the subscriber is notified with a message. + /// If the key is deleted the subscriber is notified with a message. /// [Serializable] public sealed class Subscribe : IReplicatorMessage, IEquatable @@ -370,7 +370,7 @@ public override int GetHashCode() /// /// Unregister a subscriber. /// - /// + /// [Serializable] public sealed class Unsubscribe : IEquatable, IReplicatorMessage { @@ -466,7 +466,7 @@ public override int GetHashCode() /// /// Send this message to the local to update a data value for the /// given . The will reply with one of the - /// messages. + /// messages. /// /// The current data value for the is passed as parameter to the function. /// It is if there is no value for the , and otherwise . The function @@ -753,7 +753,7 @@ public override int GetHashCode() /// /// Send this message to the local to delete a data value for the - /// given . The will reply with one of the messages. + /// given . The will reply with one of the messages. /// [Serializable] public sealed class Delete : ICommand, INoSerializationVerificationNeeded, IEquatable diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Snapshot/QueryExecutor.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common/Snapshot/QueryExecutor.cs index d64ec8c1f9f..d94ae81a224 100644 --- a/src/contrib/persistence/Akka.Persistence.Sql.Common/Snapshot/QueryExecutor.cs +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Snapshot/QueryExecutor.cs @@ -345,7 +345,7 @@ protected virtual void SetPayloadParameter(object snapshot, DbCommand command) /// /// TBD /// - /// TBD + /// TBD /// TBD protected virtual void SetManifestParameters(object snapshot, DbCommand command) { diff --git a/src/core/Akka.Cluster/Cluster.cs b/src/core/Akka.Cluster/Cluster.cs index b367a47d601..417b85025dd 100644 --- a/src/core/Akka.Cluster/Cluster.cs +++ b/src/core/Akka.Cluster/Cluster.cs @@ -296,6 +296,7 @@ public void JoinSeedNodes(IEnumerable
seedNodes) /// actor system is manually restarted. ///
/// TBD + /// TBD public Task JoinSeedNodesAsync(IEnumerable
seedNodes, CancellationToken token = default(CancellationToken)) { var completion = new TaskCompletionSource(); @@ -424,7 +425,7 @@ public void RegisterOnMemberRemoved(Action callback) /// ActorRef with the cluster's , unless address' host is already defined /// /// An belonging to the current node. - /// The absolute remote of . + /// The absolute remote of . public ActorPath RemotePathOf(IActorRef actorRef) { var path = actorRef.Path; diff --git a/src/core/Akka.Cluster/ClusterDaemon.cs b/src/core/Akka.Cluster/ClusterDaemon.cs index 0fc632b5138..fd2aa8df0c4 100644 --- a/src/core/Akka.Cluster/ClusterDaemon.cs +++ b/src/core/Akka.Cluster/ClusterDaemon.cs @@ -2589,7 +2589,7 @@ internal sealed class JoinSeedNodeProcess : UntypedActor /// TBD /// /// This exception is thrown when either the list of specified is empty - /// or the first listed seed is a reference to the 's address. + /// or the first listed seed is a reference to the IUntypedActorContext.System's address. /// public JoinSeedNodeProcess(ImmutableList
seeds) { @@ -2685,7 +2685,7 @@ internal sealed class FirstSeedNodeProcess : UntypedActor /// TBD /// /// This exception is thrown when either the number of specified is less than or equal to 1 - /// or the first listed seed is a reference to the 's address. + /// or the first listed seed is a reference to the IUntypedActorContext.System's address. /// public FirstSeedNodeProcess(ImmutableList
seeds) { diff --git a/src/core/Akka.Streams/Attributes.cs b/src/core/Akka.Streams/Attributes.cs index 8b248c512b5..a265658a277 100644 --- a/src/core/Akka.Streams/Attributes.cs +++ b/src/core/Akka.Streams/Attributes.cs @@ -355,7 +355,7 @@ public static Attributes CreateName(string name) /// Logging a certain operation can be completely disabled by using /// /// Passing in null as any of the arguments sets the level to its default value, which is: - /// for and , and for . + /// for and , and for . /// /// TBD /// TBD diff --git a/src/core/Akka.Streams/Dsl/Internal/InternalFlowOperations.cs b/src/core/Akka.Streams/Dsl/Internal/InternalFlowOperations.cs index 0062183bca1..97bb0210232 100644 --- a/src/core/Akka.Streams/Dsl/Internal/InternalFlowOperations.cs +++ b/src/core/Akka.Streams/Dsl/Internal/InternalFlowOperations.cs @@ -416,12 +416,14 @@ public static IFlow WhereNot(this IFlow flow, Predica /// /// Cancels when returned false or downstream cancels /// - /// + /// + /// /// - /// TBD + /// TBD /// TBD /// TBD /// TBD + /// TBD /// TBD public static IFlow TakeWhile(this IFlow flow, Predicate predicate, bool inclusive) { diff --git a/src/core/Akka.Streams/Dsl/Source.cs b/src/core/Akka.Streams/Dsl/Source.cs index 0f2d4afb9aa..e8e1f224137 100644 --- a/src/core/Akka.Streams/Dsl/Source.cs +++ b/src/core/Akka.Streams/Dsl/Source.cs @@ -411,7 +411,7 @@ public static Source FromPublisher(IPublisher publisher) /// /// Start a new from the given function that produces an . /// The produced stream of elements will continue until the enumerator runs empty - /// or fails during evaluation of the method. + /// or fails during evaluation of the IEnumerator<T>.MoveNext method. /// Elements are pulled out of the enumerator in accordance with the demand coming /// from the downstream transformation steps. /// @@ -791,13 +791,13 @@ public static Source ZipWithN(Func, /// there is no space available in the buffer. /// /// Acknowledgement mechanism is available. - /// returns + /// ISourceQueueWithComplete<T>.OfferAsync returns /// which completes with if element was added to buffer or sent downstream. /// It completes with if element was dropped. /// Can also complete with - when stream failed /// or when downstream is completed. /// - /// The strategy will not complete when buffer is full. + /// The strategy will not complete ISourceQueueWithComplete<T>.OfferAsync when buffer is full. /// /// The buffer can be disabled by using of 0 and then received messages will wait /// for downstream demand unless there is another message waiting for downstream demand, in that case diff --git a/src/core/Akka.Streams/Dsl/SourceOperations.cs b/src/core/Akka.Streams/Dsl/SourceOperations.cs index 0acd5d3d06e..3c8ee2e8de6 100644 --- a/src/core/Akka.Streams/Dsl/SourceOperations.cs +++ b/src/core/Akka.Streams/Dsl/SourceOperations.cs @@ -394,6 +394,7 @@ public static Source WhereNot(this Source fl /// TBD /// TBD /// TBD + /// TBD /// TBD public static Source TakeWhile(this Source flow, Predicate predicate, bool inclusive = false) { diff --git a/src/core/Akka.Streams/Dsl/SubFlowOperations.cs b/src/core/Akka.Streams/Dsl/SubFlowOperations.cs index 7d2a1744a23..4822251c851 100644 --- a/src/core/Akka.Streams/Dsl/SubFlowOperations.cs +++ b/src/core/Akka.Streams/Dsl/SubFlowOperations.cs @@ -402,8 +402,10 @@ public static SubFlow WhereNot(this Su /// /// TBD /// TBD + /// TBD /// TBD /// TBD + /// TBD /// TBD public static SubFlow TakeWhile(this SubFlow flow, Predicate predicate, bool inclusive = false) { diff --git a/src/core/Akka/Actor/ActorRef.cs b/src/core/Akka/Actor/ActorRef.cs index 9f0007f585d..824c8ff416d 100644 --- a/src/core/Akka/Actor/ActorRef.cs +++ b/src/core/Akka/Actor/ActorRef.cs @@ -89,6 +89,7 @@ public FutureActorRef(TaskCompletionSource result, Action unregister, Ac /// TBD /// TBD /// TBD + /// TBD public FutureActorRef(TaskCompletionSource result, Action unregister, ActorPath path, bool tcsWasCreatedWithRunContinuationsAsynchronouslyAvailable) { if (ActorCell.Current != null) diff --git a/src/core/Akka/Actor/Settings.cs b/src/core/Akka/Actor/Settings.cs index ea93e5c868e..f71a8d39ea1 100644 --- a/src/core/Akka/Actor/Settings.cs +++ b/src/core/Akka/Actor/Settings.cs @@ -177,7 +177,7 @@ private static string GetProviderClass(string provider) public bool SerializeAllCreators { get; private set; } /// - /// Gets the default timeout for calls. + /// Gets the default timeout for Futures.Ask calls. /// /// The ask timeout. public TimeSpan AskTimeout { get; private set; } diff --git a/src/core/Akka/Dispatch/SysMsg/ISystemMessage.cs b/src/core/Akka/Dispatch/SysMsg/ISystemMessage.cs index 3749d37464d..34451d67acb 100644 --- a/src/core/Akka/Dispatch/SysMsg/ISystemMessage.cs +++ b/src/core/Akka/Dispatch/SysMsg/ISystemMessage.cs @@ -258,11 +258,12 @@ internal interface IStashWhenWaitingForChildren { } /// Stash this when the actor is in a failed state. /// internal interface IStashWhenFailed { } - /** - * public API - */ + + // public API + //@SerialVersionUID(1L) //private[akka] case class Create(failure: Option[ActorInitializationException]) extends ISystemMessage // sent to self from Dispatcher.register + /// /// Class ISystemMessage. /// diff --git a/src/core/Akka/Pattern/BackoffOptions.cs b/src/core/Akka/Pattern/BackoffOptions.cs index fcec1a5f650..8e1be3ef9fa 100644 --- a/src/core/Akka/Pattern/BackoffOptions.cs +++ b/src/core/Akka/Pattern/BackoffOptions.cs @@ -11,7 +11,7 @@ namespace Akka.Pattern { /// - /// Builds back-off options for creating a back-off supervisor. You can pass to . + /// Builds back-off options for creating a back-off supervisor. You can pass to . /// public static class Backoff { From d732274b0fa9cb6863fabbdf9d1d2c447d61d6f3 Mon Sep 17 00:00:00 2001 From: zbynek001 Date: Wed, 13 Jun 2018 15:49:29 +0200 Subject: [PATCH 28/70] fix concurrency bug in CircuitBreaker (#3505) --- src/core/Akka/Pattern/CircuitBreaker.cs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/core/Akka/Pattern/CircuitBreaker.cs b/src/core/Akka/Pattern/CircuitBreaker.cs index 69078e4d956..259785e535a 100644 --- a/src/core/Akka/Pattern/CircuitBreaker.cs +++ b/src/core/Akka/Pattern/CircuitBreaker.cs @@ -15,29 +15,29 @@ namespace Akka.Pattern { /// - /// Provides circuit breaker functionality to provide stability when working with + /// Provides circuit breaker functionality to provide stability when working with /// "dangerous" operations, e.g. calls to remote systems - /// + /// /// /// /// Transitions through three states: /// /// /// In *Closed* state, - /// calls pass through until the maxFailures count is reached. - /// This causes the circuit breaker to open. Both exceptions and calls exceeding + /// calls pass through until the maxFailures count is reached. + /// This causes the circuit breaker to open. Both exceptions and calls exceeding /// callTimeout are considered failures. /// /// /// In *Open* state, - /// calls fail-fast with an exception. After resetTimeout, + /// calls fail-fast with an exception. After resetTimeout, /// circuit breaker transitions to half-open state. /// /// /// In *Half-Open* state, - /// the first call will be allowed through, if it succeeds - /// the circuit breaker will reset to closed state. If it fails, the circuit - /// breaker will re-open to open state. All calls beyond the first that execute + /// the first call will be allowed through, if it succeeds + /// the circuit breaker will reset to closed state. If it fails, the circuit + /// breaker will re-open to open state. All calls beyond the first that execute /// while the first is running will fail-fast with an exception. /// /// @@ -173,7 +173,7 @@ public void WithSyncCircuitBreaker(Action body) /// /// Wraps invocations of asynchronous calls that need to be protected /// If this does not complete within the time allotted, it should return default() - /// + /// /// /// Await.result( /// withCircuitBreaker(try Future.successful(body) catch { case NonFatal(t) ⇒ Future.failed(t) }), @@ -238,10 +238,7 @@ private void Transition(AtomicState fromState, AtomicState toState) Debug.WriteLine("Successful transition from {0} to {1}", fromState, toState); toState.Enter(); } - else - { - throw new IllegalStateException($"Illegal transition attempted from {fromState} to {toState}"); - } + // else some other thread already swapped state } /// From 7a5a7c3222f15f587c542dfe70543590264e5b3c Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 12 Jun 2018 18:11:37 -0500 Subject: [PATCH 29/70] upgraded to XUnit 2.3.1 and VsTest SDK 15.7.2 --- src/common.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common.props b/src/common.props index faceef1c8ae..c35943e16dc 100644 --- a/src/common.props +++ b/src/common.props @@ -9,8 +9,8 @@ $(NoWarn);CS1591 - 2.3.0 - 15.3.0 + 2.3.1 + 15.7.2 akka;actors;actor model;Akka;concurrency From 87efae3854b2741c6ae75c373a60f5e4b1836d51 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 13 Jun 2018 21:50:47 -0500 Subject: [PATCH 30/70] forced API tests to copy their output --- src/core/Akka.API.Tests/Akka.API.Tests.csproj | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/core/Akka.API.Tests/Akka.API.Tests.csproj b/src/core/Akka.API.Tests/Akka.API.Tests.csproj index 6bb230ee621..cf2293f76f0 100644 --- a/src/core/Akka.API.Tests/Akka.API.Tests.csproj +++ b/src/core/Akka.API.Tests/Akka.API.Tests.csproj @@ -6,6 +6,40 @@ net452 + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + From 6f32f6a772e917e1abfed6efeb05234bf5feeac4 Mon Sep 17 00:00:00 2001 From: zbynek001 Date: Thu, 14 Jun 2018 22:18:59 +0200 Subject: [PATCH 31/70] RestartShard handled on Shard (#3509) --- src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs b/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs index 6ce0bc9eb20..489cc8eed58 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs @@ -450,6 +450,8 @@ public static bool HandleCommand(this TShard shard, object message) wher case Shard.IShardQuery sq: shard.HandleShardRegionQuery(sq); return true; + case ShardRegion.RestartShard _: + return true; case var _ when shard.ExtractEntityId(message) != null: shard.DeliverMessage(message, shard.Context.Sender); return true; From 3dedcfd8b4cc43eacc322e3d2a1282034caa661e Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 14 Jun 2018 16:38:27 -0500 Subject: [PATCH 32/70] fixed NRE issue with ClusterClientSettings.Copy mentioned in #3417 --- .../ClusterClient/ClusterClientConfigSpec.cs | 11 +++++++++++ .../Client/ClusterClientSettings.cs | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests/ClusterClient/ClusterClientConfigSpec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests/ClusterClient/ClusterClientConfigSpec.cs index 42a8e8b4259..92da28ebefb 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests/ClusterClient/ClusterClientConfigSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests/ClusterClient/ClusterClientConfigSpec.cs @@ -60,6 +60,17 @@ public void ClusterClientSettings_must_throw_exception_on_empty_initial_contacts exception.Message.Should().Be("InitialContacts must be defined"); } + /// + /// Addresses the bug discussed here: https://github.com/akkadotnet/akka.net/issues/3417#issuecomment-397443227 + /// + [Fact] + public void ClusterClientSettings_must_copy_initial_contacts_via_fluent_interface() + { + var initialContacts = ImmutableHashSet.Empty.Add(new RootActorPath(Address.AllSystems) / "user" / "foo"); + var clusterClientSettings = ClusterClientSettings.Create(Sys).WithInitialContacts(initialContacts).WithBufferSize(2000); + clusterClientSettings.InitialContacts.Should().BeEquivalentTo(initialContacts); + } + [Fact] public void ClusterReceptionistSettings_must_have_default_config() { diff --git a/src/contrib/cluster/Akka.Cluster.Tools/Client/ClusterClientSettings.cs b/src/contrib/cluster/Akka.Cluster.Tools/Client/ClusterClientSettings.cs index 0795ae78b20..8bdf8dcfad9 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/Client/ClusterClientSettings.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/Client/ClusterClientSettings.cs @@ -211,7 +211,7 @@ private ClusterClientSettings Copy( TimeSpan? reconnectTimeout = null) { return new ClusterClientSettings( - initialContacts, + initialContacts ?? InitialContacts, establishingGetContactsInterval ?? EstablishingGetContactsInterval, refreshContactsInterval ?? RefreshContactsInterval, heartbeatInterval ?? HeartbeatInterval, From 8e739b3508348873d3c4c91eab6ee72781c94fe5 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 14 Jun 2018 17:37:20 -0500 Subject: [PATCH 33/70] bumped ClusterClient message drop log messages from DEBUG to WARNING per #3417 --- .../cluster/Akka.Cluster.Tools/Client/ClusterClient.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/contrib/cluster/Akka.Cluster.Tools/Client/ClusterClient.cs b/src/contrib/cluster/Akka.Cluster.Tools/Client/ClusterClient.cs index 6a4a0c90b8e..1200688147e 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/Client/ClusterClient.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/Client/ClusterClient.cs @@ -468,17 +468,18 @@ private void Buffer(object message) { if (_settings.BufferSize == 0) { - _log.Debug("Receptionist not available and buffering is disabled, dropping message [{0}]", message.GetType().Name); + _log.Warning("Receptionist not available and buffering is disabled, dropping message [{0}]", message.GetType().Name); } else if (_buffer.Count == _settings.BufferSize) { var m = _buffer.Dequeue(); - _log.Debug("Receptionist not available, buffer is full, dropping first message [{0}]", m.Item1.GetType().Name); + _log.Warning("Receptionist not available, buffer is full, dropping first message [{0}]", m.Item1.GetType().Name); _buffer.Enqueue(Tuple.Create(message, Sender)); } else { - _log.Debug("Receptionist not available, buffering message type [{0}]", message.GetType().Name); + if(_log.IsDebugEnabled) // don't invoke reflection call on message type if we don't have to + _log.Debug("Receptionist not available, buffering message type [{0}]", message.GetType().Name); _buffer.Enqueue(Tuple.Create(message, Sender)); } } From 98a740f2ee334299a8c57ad1e63da4fd56a3eccc Mon Sep 17 00:00:00 2001 From: zbynek001 Date: Tue, 19 Jun 2018 14:32:11 +0200 Subject: [PATCH 34/70] cluster coordinated leave fix for empty cluster (#3516) --- src/core/Akka.Cluster/ClusterDaemon.cs | 8 +++++++- src/core/Akka.Cluster/CoordinatedShutdownLeave.cs | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/core/Akka.Cluster/ClusterDaemon.cs b/src/core/Akka.Cluster/ClusterDaemon.cs index fd2aa8df0c4..2c59a58d49e 100644 --- a/src/core/Akka.Cluster/ClusterDaemon.cs +++ b/src/core/Akka.Cluster/ClusterDaemon.cs @@ -1075,7 +1075,13 @@ private void AddCoordinatedLeave() { var sys = Context.System; var self = Self; - _coordShutdown.AddTask(CoordinatedShutdown.PhaseClusterExiting, "wait-exiting", () => _selfExiting.Task); + _coordShutdown.AddTask(CoordinatedShutdown.PhaseClusterExiting, "wait-exiting", () => + { + if (_latestGossip.Members.IsEmpty) + return Task.FromResult(Done.Instance); // not joined yet + else + return _selfExiting.Task; + }); _coordShutdown.AddTask(CoordinatedShutdown.PhaseClusterExitingDone, "exiting-completed", () => { if (Cluster.Get(sys).IsTerminated) diff --git a/src/core/Akka.Cluster/CoordinatedShutdownLeave.cs b/src/core/Akka.Cluster/CoordinatedShutdownLeave.cs index 8a852581384..6fb3534237b 100644 --- a/src/core/Akka.Cluster/CoordinatedShutdownLeave.cs +++ b/src/core/Akka.Cluster/CoordinatedShutdownLeave.cs @@ -13,7 +13,7 @@ namespace Akka.Cluster { /// /// INTERNAL API - /// + /// /// Used for executing phases for graceful /// behaviors. /// @@ -55,7 +55,13 @@ private void WaitingLeaveCompleted(IActorRef replyTo) { Receive(s => { - if (s.Members.Any(m => m.UniqueAddress.Equals(_cluster.SelfUniqueAddress) + if (s.Members.IsEmpty) + { + // not joined yet + replyTo.Tell(Done.Instance); + Context.Stop(Self); + } + else if (s.Members.Any(m => m.UniqueAddress.Equals(_cluster.SelfUniqueAddress) && (m.Status == MemberStatus.Leaving || m.Status == MemberStatus.Exiting || m.Status == MemberStatus.Down))) From 292ebcb1ac740e9cf5ed129fd6ee5a377418238f Mon Sep 17 00:00:00 2001 From: zbynek001 Date: Thu, 21 Jun 2018 00:52:48 +0200 Subject: [PATCH 35/70] sharding rebalance small fix --- src/contrib/cluster/Akka.Cluster.Sharding/ShardCoordinator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ShardCoordinator.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ShardCoordinator.cs index aa83b8c74b5..93ac93f798e 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ShardCoordinator.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ShardCoordinator.cs @@ -223,7 +223,6 @@ private static void HandleGracefulShutdownRequest(this TCoordinato private static void HandleRebalanceDone(this TCoordinator coordinator, string shard, bool ok) where TCoordinator : IShardCoordinator { - coordinator.RebalanceInProgress = coordinator.RebalanceInProgress.Remove(shard); coordinator.Log.Debug("Rebalance shard [{0}] done [{1}]", shard, ok); // The shard could have been removed by ShardRegionTerminated From dad1bfb1575d9832084cb44014de9c9ed07b9c6e Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Thu, 21 Jun 2018 19:19:37 +0200 Subject: [PATCH 36/70] updated ActorCell children container updates (#3514) --- src/core/Akka/Actor/ActorCell.Children.cs | 139 +++++++++--------- .../Internal/TerminatedChildrenContainer.cs | 8 +- 2 files changed, 75 insertions(+), 72 deletions(-) diff --git a/src/core/Akka/Actor/ActorCell.Children.cs b/src/core/Akka/Actor/ActorCell.Children.cs index 6682b11ecf1..a99a04e4424 100644 --- a/src/core/Akka/Actor/ActorCell.Children.cs +++ b/src/core/Akka/Actor/ActorCell.Children.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Threading; using Akka.Actor.Internal; using Akka.Serialization; @@ -25,7 +26,7 @@ public partial class ActorCell /// public IChildrenContainer ChildrenContainer { - get { return _childrenContainerDoNotCallMeDirectly; } + get { return _childrenContainerDoNotCallMeDirectly; } } private IReadOnlyCollection Children @@ -86,7 +87,7 @@ private IActorRef ActorOf(Props props, string name, bool isAsync, bool isSystemS return MakeChild(props, name, isAsync, isSystemService); } - + private string GetRandomActorName() { var id = Interlocked.Increment(ref _nextRandomNameDoNotCallMeDirectly); @@ -105,46 +106,22 @@ public void Stop(IActorRef child) var repointableActorRef = child as RepointableActorRef; if (repointableActorRef == null || repointableActorRef.IsStarted) { - UpdateChildrenRefs(c => c.ShallDie(child)); + while (true) + { + var oldChildren = ChildrenContainer; + var newChildren = oldChildren.ShallDie(child); + + if (SwapChildrenRefs(oldChildren, newChildren)) break; + } } } ((IInternalActorRef)child).Stop(); } - /// - /// Swaps out the children container, by calling to produce the new container. - /// If the underlying container has been updated while was called, - /// will be called again with the new container. This will repeat until the - /// container can be swapped out, or until contains false. - /// The returned tuple should contain: - /// Item1: true if the container should be updated; false to not update and return Item3 - /// Item2: The new container (will only be used if Item1=true) - /// Item3: The return value - /// - /// A function that returns a new container. - /// The third value of the tuple that returned. - private TReturn UpdateChildrenRefs(Func> updater) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool SwapChildrenRefs(IChildrenContainer oldChildren, IChildrenContainer newChildren) { - while (true) - { - var current = ChildrenContainer; - var t = updater(current); - if (!t.Item1) return t.Item3; - if (Interlocked.CompareExchange(ref _childrenContainerDoNotCallMeDirectly, t.Item2, current) == current) return t.Item3; - } - } - - /// - /// Swaps out the children container, by calling to produce the new container. - /// If the underlying container has been updated while was called, - /// will be called again with the new container. This will repeat until the - /// container can be swapped out. - /// - /// A function that returns a new container. - /// The new updated - private IChildrenContainer UpdateChildrenRefs(Func updater) - { - return InterlockedSpin.Swap(ref _childrenContainerDoNotCallMeDirectly, updater); + return ReferenceEquals(Interlocked.CompareExchange(ref _childrenContainerDoNotCallMeDirectly, newChildren, oldChildren), oldChildren); } /// @@ -153,7 +130,13 @@ private IChildrenContainer UpdateChildrenRefs(FuncTBD public void ReserveChild(string name) { - UpdateChildrenRefs(c => c.Reserve(name)); + while (true) + { + var oldChildren = ChildrenContainer; + var newChildren = oldChildren.Reserve(name); + + if (SwapChildrenRefs(oldChildren, newChildren)) break; + } } /// @@ -162,8 +145,13 @@ public void ReserveChild(string name) /// TBD protected void UnreserveChild(string name) { - UpdateChildrenRefs(c => c.Unreserve(name)); + while (true) + { + var oldChildren = ChildrenContainer; + var newChildren = oldChildren.Unreserve(name); + if (SwapChildrenRefs(oldChildren, newChildren)) break; + } } /// @@ -173,29 +161,25 @@ protected void UnreserveChild(string name) /// TBD public ChildRestartStats InitChild(IInternalActorRef actor) { - return UpdateChildrenRefs(cc => + var name = actor.Path.Name; + while (true) { - IChildStats stats; - var name = actor.Path.Name; - if (cc.TryGetByName(name, out stats)) + var cc = ChildrenContainer; + if (cc.TryGetByName(name, out var old)) { - var old = stats as ChildRestartStats; - if (old != null) - { - //Do not update. Return old - return new Tuple(false, cc, old); - } - if (stats is ChildNameReserved) + switch (old) { - var crs = new ChildRestartStats(actor); - var updatedContainer = cc.Add(name, crs); - //Update (if it's still cc) and return the new crs - return new Tuple(true, updatedContainer, crs); + case ChildRestartStats restartStats: + return restartStats; + case ChildNameReserved _: + var crs = new ChildRestartStats(actor); + if (SwapChildrenRefs(cc, cc.Add(name, crs))) + return crs; + break; } } - //Do not update. Return null - return new Tuple(false, cc, null); - }); + else return null; + } } /// @@ -205,16 +189,15 @@ public ChildRestartStats InitChild(IInternalActorRef actor) /// TBD protected bool SetChildrenTerminationReason(SuspendReason reason) { - return UpdateChildrenRefs(cc => + while (true) { - var c = cc as TerminatingChildrenContainer; - if (c != null) - //The arguments says: Update; with a new reason; and return true - return new Tuple(true, c.CreateCopyWithReason(reason), true); - - //The arguments says:Do NOT update; any container will do since it wont be updated; return false - return new Tuple(false, cc, false); - }); + if (ChildrenContainer is TerminatingChildrenContainer c) + { + var n = c.CreateCopyWithReason(reason); + if (SwapChildrenRefs(c, n)) return true; + } + else return false; + } } /// @@ -222,7 +205,7 @@ protected bool SetChildrenTerminationReason(SuspendReason reason) /// protected void SetTerminated() { - UpdateChildrenRefs(c => TerminatedChildrenContainer.Instance); + Interlocked.Exchange(ref _childrenContainerDoNotCallMeDirectly, TerminatedChildrenContainer.Instance); } /// @@ -381,14 +364,28 @@ protected SuspendReason RemoveChildAndGetStateChange(IActorRef child) var terminating = ChildrenContainer as TerminatingChildrenContainer; if (terminating != null) { - var newContainer = UpdateChildrenRefs(c => c.Remove(child)); - if (newContainer is TerminatingChildrenContainer) return null; - return terminating.Reason; + var n = RemoveChild(child); + if (!(n is TerminatingChildrenContainer)) + return terminating.Reason; + else + return null; } - UpdateChildrenRefs(c => c.Remove(child)); + + RemoveChild(child); return null; } + private IChildrenContainer RemoveChild(IActorRef child) + { + while (true) + { + var oldChildren = ChildrenContainer; + var newChildren = oldChildren.Remove(child); + + if (SwapChildrenRefs(oldChildren, newChildren)) return newChildren; + } + } + private static string CheckName(string name) { if (name == null) throw new InvalidActorNameException("Actor name must not be null."); @@ -460,7 +457,7 @@ private IInternalActorRef MakeChild(Props props, string name, bool async, bool s if (Mailbox != null && IsFailed) { - for(var i = 1; i <= Mailbox.SuspendCount(); i++) + for (var i = 1; i <= Mailbox.SuspendCount(); i++) actor.Suspend(); } diff --git a/src/core/Akka/Actor/ChildrenContainer/Internal/TerminatedChildrenContainer.cs b/src/core/Akka/Actor/ChildrenContainer/Internal/TerminatedChildrenContainer.cs index 5c1e222e5f4..a500e885d32 100644 --- a/src/core/Akka/Actor/ChildrenContainer/Internal/TerminatedChildrenContainer.cs +++ b/src/core/Akka/Actor/ChildrenContainer/Internal/TerminatedChildrenContainer.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Runtime.CompilerServices; namespace Akka.Actor.Internal { @@ -22,10 +23,15 @@ private TerminatedChildrenContainer() { //Intentionally left blank } + /// /// TBD /// - public new static IChildrenContainer Instance { get { return _instance; } } + public new static IChildrenContainer Instance + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _instance; + } /// /// TBD From 8463034ba85e80a0b2ce6a8ff8e8903d2bacc958 Mon Sep 17 00:00:00 2001 From: zbynek001 Date: Fri, 22 Jun 2018 04:53:36 +0200 Subject: [PATCH 37/70] RemoteWatcher race-condition fix (#3519) --- src/core/Akka.Remote/RemoteWatcher.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/Akka.Remote/RemoteWatcher.cs b/src/core/Akka.Remote/RemoteWatcher.cs index 1a5801e290d..8006f5a3f37 100644 --- a/src/core/Akka.Remote/RemoteWatcher.cs +++ b/src/core/Akka.Remote/RemoteWatcher.cs @@ -642,10 +642,13 @@ private void ProcessTerminated(IInternalActorRef watchee, bool existenceConfirme if (!addressTerminated) { - foreach (var watcher in Watching[watchee]) + if (Watching.TryGetValue(watchee, out var watchers)) { - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - watcher.SendSystemMessage(new DeathWatchNotification(watchee, existenceConfirmed, addressTerminated)); + foreach (var watcher in watchers) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + watcher.SendSystemMessage(new DeathWatchNotification(watchee, existenceConfirmed, addressTerminated)); + } } } From 68dc1b5648f172964848918412cd466f9660b44e Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 21 Jun 2018 19:22:22 -0700 Subject: [PATCH 38/70] help fix https://github.com/akkadotnet/Akka.Logger.Serilog/issues/51 by making LoggingAdapterBase methods virtual --- .../CoreAPISpec.ApproveCore.approved.txt | 14 +++++++------- src/core/Akka/Event/LoggingAdapterBase.cs | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index b2a78a7fe9c..3e8dc25f50c 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -2951,20 +2951,20 @@ namespace Akka.Event public abstract bool IsErrorEnabled { get; } public abstract bool IsInfoEnabled { get; } public abstract bool IsWarningEnabled { get; } - public void Debug(string format, params object[] args) { } - public void Error(System.Exception cause, string format, params object[] args) { } - public void Error(string format, params object[] args) { } - public void Info(string format, params object[] args) { } + public virtual void Debug(string format, params object[] args) { } + public virtual void Error(System.Exception cause, string format, params object[] args) { } + public virtual void Error(string format, params object[] args) { } + public virtual void Info(string format, params object[] args) { } public bool IsEnabled(Akka.Event.LogLevel logLevel) { } - public void Log(Akka.Event.LogLevel logLevel, string format, params object[] args) { } + public virtual void Log(Akka.Event.LogLevel logLevel, string format, params object[] args) { } protected abstract void NotifyDebug(object message); protected abstract void NotifyError(object message); protected abstract void NotifyError(System.Exception cause, object message); protected abstract void NotifyInfo(object message); protected void NotifyLog(Akka.Event.LogLevel logLevel, object message) { } protected abstract void NotifyWarning(object message); - public void Warn(string format, params object[] args) { } - public void Warning(string format, params object[] args) { } + public virtual void Warn(string format, params object[] args) { } + public virtual void Warning(string format, params object[] args) { } } public class LoggingBus : Akka.Event.ActorEventBus { diff --git a/src/core/Akka/Event/LoggingAdapterBase.cs b/src/core/Akka/Event/LoggingAdapterBase.cs index c1199f07034..ccc3c7b7d93 100644 --- a/src/core/Akka/Event/LoggingAdapterBase.cs +++ b/src/core/Akka/Event/LoggingAdapterBase.cs @@ -135,7 +135,7 @@ protected void NotifyLog(LogLevel logLevel, object message) /// /// The message that is being logged. /// An optional list of items used to format the message. - public void Debug(string format, params object[] args) + public virtual void Debug(string format, params object[] args) { if (!IsDebugEnabled) return; @@ -155,7 +155,7 @@ public void Debug(string format, params object[] args) /// /// N/A /// N/A - public void Warn(string format, params object[] args) + public virtual void Warn(string format, params object[] args) { Warning(format, args); } @@ -165,7 +165,7 @@ public void Warn(string format, params object[] args) /// /// The message that is being logged. /// An optional list of items used to format the message. - public void Warning(string format, params object[] args) + public virtual void Warning(string format, params object[] args) { if (!IsWarningEnabled) return; @@ -186,7 +186,7 @@ public void Warning(string format, params object[] args) /// The exception associated with this message. /// The message that is being logged. /// An optional list of items used to format the message. - public void Error(Exception cause, string format, params object[] args) + public virtual void Error(Exception cause, string format, params object[] args) { if (!IsErrorEnabled) return; @@ -206,7 +206,7 @@ public void Error(Exception cause, string format, params object[] args) /// /// The message that is being logged. /// An optional list of items used to format the message. - public void Error(string format, params object[] args) + public virtual void Error(string format, params object[] args) { if (!IsErrorEnabled) return; @@ -226,7 +226,7 @@ public void Error(string format, params object[] args) /// /// The message that is being logged. /// An optional list of items used to format the message. - public void Info(string format, params object[] args) + public virtual void Info(string format, params object[] args) { if (!IsInfoEnabled) return; @@ -247,7 +247,7 @@ public void Info(string format, params object[] args) /// The level used to log the message. /// The message that is being logged. /// An optional list of items used to format the message. - public void Log(LogLevel logLevel, string format, params object[] args) + public virtual void Log(LogLevel logLevel, string format, params object[] args) { if (args == null || args.Length == 0) { From fdc1765bc84ef0ef3f8599940b243c6da158d10e Mon Sep 17 00:00:00 2001 From: Ismael Hamed Date: Mon, 25 Jun 2018 15:09:04 +0200 Subject: [PATCH 39/70] Allow persisting events when recovery has completed (#3366) --- .../PersistentActorSpec.Actors.cs | 39 +++++ .../PersistentActorSpec.cs | 16 ++ .../Akka.Persistence/Eventsourced.Recovery.cs | 159 ++++++++++-------- src/core/Akka.Persistence/Eventsourced.cs | 49 ++++-- 4 files changed, 180 insertions(+), 83 deletions(-) diff --git a/src/core/Akka.Persistence.Tests/PersistentActorSpec.Actors.cs b/src/core/Akka.Persistence.Tests/PersistentActorSpec.Actors.cs index 784c4b8a4aa..48f7c553696 100644 --- a/src/core/Akka.Persistence.Tests/PersistentActorSpec.Actors.cs +++ b/src/core/Akka.Persistence.Tests/PersistentActorSpec.Actors.cs @@ -1121,6 +1121,45 @@ protected override bool ReceiveCommand(object message) return false; } } + + internal class PersistInRecovery : ExamplePersistentActor + { + public PersistInRecovery(string name) + : base(name) + { } + + protected override bool ReceiveRecover(object message) + { + switch (message) + { + case Evt evt when evt.Data?.ToString() == "invalid": + Persist(new Evt("invalid-recovery"), UpdateStateHandler); + return true; + case Evt evt: + return UpdateState(evt); + case RecoveryCompleted _: + PersistAsync(new Evt("rc-1"), UpdateStateHandler); + Persist(new Evt("rc-2"), UpdateStateHandler); + PersistAsync(new Evt("rc-3"), UpdateStateHandler); + return true; + } + + return false; + } + + protected override bool ReceiveCommand(object message) + { + if (CommonBehavior(message)) return true; + + if (message is Cmd cmd) + { + Persist(new Evt(cmd.Data), UpdateStateHandler); + return true; + } + + return false; + } + } } } diff --git a/src/core/Akka.Persistence.Tests/PersistentActorSpec.cs b/src/core/Akka.Persistence.Tests/PersistentActorSpec.cs index 8bc7ea38bc4..d05abc5ae15 100644 --- a/src/core/Akka.Persistence.Tests/PersistentActorSpec.cs +++ b/src/core/Akka.Persistence.Tests/PersistentActorSpec.cs @@ -611,5 +611,21 @@ public void PersistentActor_should_brecover_the_message_which_caused_the_restart persistentActor.Tell("boom"); ExpectMsg("failed with TestException while processing boom"); } + + [Fact] + public void PersistentActor_should_be_able_to_persist_events_that_happen_during_recovery() + { + var persistentActor = ActorOf(Props.Create(() => new PersistInRecovery(Name))); + persistentActor.Tell(GetState.Instance); + ExpectMsgInOrder("a-1", "a-2", "rc-1", "rc-2"); + persistentActor.Tell(GetState.Instance); + ExpectMsgInOrder("a-1", "a-2", "rc-1", "rc-2", "rc-3"); + persistentActor.Tell(new Cmd("invalid")); + persistentActor.Tell(GetState.Instance); + ExpectMsgInOrder("a-1", "a-2", "rc-1", "rc-2", "rc-3", "invalid"); + Watch(persistentActor); + persistentActor.Tell("boom"); + ExpectTerminated(persistentActor); + } } } diff --git a/src/core/Akka.Persistence/Eventsourced.Recovery.cs b/src/core/Akka.Persistence/Eventsourced.Recovery.cs index e2252eeac47..c00f2d0b1bb 100644 --- a/src/core/Akka.Persistence/Eventsourced.Recovery.cs +++ b/src/core/Akka.Persistence/Eventsourced.Recovery.cs @@ -17,7 +17,7 @@ namespace Akka.Persistence internal class EventsourcedState { - public EventsourcedState(string name, bool isRecoveryRunning, StateReceive stateReceive) + public EventsourcedState(string name, Func isRecoveryRunning, StateReceive stateReceive) { Name = name; IsRecoveryRunning = isRecoveryRunning; @@ -25,9 +25,7 @@ public EventsourcedState(string name, bool isRecoveryRunning, StateReceive state } public string Name { get; } - - public bool IsRecoveryRunning { get; } - + public Func IsRecoveryRunning { get; } public StateReceive StateReceive { get; } public override string ToString() => Name; @@ -47,7 +45,7 @@ public abstract partial class Eventsourced /// private EventsourcedState WaitingRecoveryPermit(Recovery recovery) { - return new EventsourcedState("waiting for recovery permit", true, (receive, message) => + return new EventsourcedState("waiting for recovery permit", () => true, (receive, message) => { if (message is RecoveryPermitGranted) StartRecovery(recovery); @@ -59,7 +57,7 @@ private EventsourcedState WaitingRecoveryPermit(Recovery recovery) /// /// Processes a loaded snapshot, if any. A loaded snapshot is offered with a /// message to the actor's . Then initiates a message replay, either starting - /// from the loaded snapshot or from scratch, and switches to state. + /// from the loaded snapshot or from scratch, and switches to state. /// All incoming messages are stashed. /// /// Maximum number of messages to replay @@ -81,7 +79,7 @@ private EventsourcedState RecoveryStarted(long maxReplays) else return false; }; - return new EventsourcedState("recovery started - replay max: " + maxReplays, true, (receive, message) => + return new EventsourcedState("recovery started - replay max: " + maxReplays, () => true, (receive, message) => { try { @@ -139,11 +137,6 @@ private EventsourcedState RecoveryStarted(long maxReplays) }); } - private void ReturnRecoveryPermit() - { - Extension.RecoveryPermitter().Tell(Akka.Persistence.ReturnRecoveryPermit.Instance, Self); - } - /// /// Processes replayed messages, if any. The actor's is invoked with the replayed events. /// @@ -159,85 +152,89 @@ 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); var eventSeenInInterval = false; + var recoveryRunning = true; - return new EventsourcedState("replay started", true, (receive, message) => + return new EventsourcedState("replay started", () => recoveryRunning, (receive, message) => { try { - if (message is ReplayedMessage) + switch (message) { - var m = (ReplayedMessage)message; - try - { - eventSeenInInterval = true; - UpdateLastSequenceNr(m.Persistent); - base.AroundReceive(recoveryBehavior, m.Persistent); - } - catch (Exception cause) - { + case ReplayedMessage replayed: + try + { + eventSeenInInterval = true; + UpdateLastSequenceNr(replayed.Persistent); + base.AroundReceive(recoveryBehavior, replayed.Persistent); + } + catch (Exception cause) + { + timeoutCancelable.Cancel(); + try + { + OnRecoveryFailure(cause, replayed.Persistent.Payload); + } + finally + { + Context.Stop(Self); + } + ReturnRecoveryPermit(); + } + break; + case RecoverySuccess success: timeoutCancelable.Cancel(); + OnReplaySuccess(); + _sequenceNr = success.HighestSequenceNr; + LastSequenceNr = success.HighestSequenceNr; + recoveryRunning = false; try { - OnRecoveryFailure(cause, m.Persistent.Payload); + base.AroundReceive(recoveryBehavior, RecoveryCompleted.Instance); } finally { - Context.Stop(Self); + // in finally in case exception and resume strategy + TransitToProcessingState(); } ReturnRecoveryPermit(); - } - } - else if (message is RecoverySuccess) - { - var m = (RecoverySuccess)message; - timeoutCancelable.Cancel(); - OnReplaySuccess(); - ChangeState(ProcessingCommands()); - _sequenceNr = m.HighestSequenceNr; - LastSequenceNr = m.HighestSequenceNr; - _internalStash.UnstashAll(); - - base.AroundReceive(recoveryBehavior, RecoveryCompleted.Instance); - ReturnRecoveryPermit(); - } - else if (message is ReplayMessagesFailure) - { - var failure = (ReplayMessagesFailure)message; - timeoutCancelable.Cancel(); - try - { - OnRecoveryFailure(failure.Cause, message: null); - } - finally - { - Context.Stop(Self); - } - ReturnRecoveryPermit(); - } - else if (message is RecoveryTick tick && !tick.Snapshot) - { - if (!eventSeenInInterval) - { + break; + case ReplayMessagesFailure failure: timeoutCancelable.Cancel(); try { - OnRecoveryFailure( - new RecoveryTimedOutException( - $"Recovery timed out, didn't get event within {timeout.TotalSeconds}s, highest sequence number seen {_sequenceNr}.")); + OnRecoveryFailure(failure.Cause); } finally { Context.Stop(Self); } ReturnRecoveryPermit(); - } - else - { - eventSeenInInterval = false; - } + break; + case RecoveryTick tick when !tick.Snapshot: + if (!eventSeenInInterval) + { + timeoutCancelable.Cancel(); + try + { + OnRecoveryFailure( + new RecoveryTimedOutException( + $"Recovery timed out, didn't get event within {timeout.TotalSeconds}s, highest sequence number seen {LastSequenceNr}.")); + } + finally + { + Context.Stop(Self); + } + ReturnRecoveryPermit(); + } + else + { + eventSeenInInterval = false; + } + break; + default: + StashInternally(message); + break; } - else - StashInternally(message); } catch (Exception) { @@ -247,13 +244,31 @@ private EventsourcedState Recovering(Receive recoveryBehavior, TimeSpan timeout) }); } + private void ReturnRecoveryPermit() => + Extension.RecoveryPermitter().Tell(Akka.Persistence.ReturnRecoveryPermit.Instance, Self); + + private void TransitToProcessingState() + { + if (_eventBatch.Count > 0) FlushBatch(); + + if (_pendingStashingPersistInvocations > 0) + { + ChangeState(PersistingEvents()); + } + else + { + ChangeState(ProcessingCommands()); + _internalStash.UnstashAll(); + } + } + /// - /// If event persistence is pending after processing a command, event persistence + /// Command processing state. If event persistence is pending after processing a command, event persistence /// is triggered and the state changes to . /// private EventsourcedState ProcessingCommands() { - return new EventsourcedState("processing commands", false, (receive, message) => + return new EventsourcedState("processing commands", () => false, (receive, message) => { var handled = CommonProcessingStateBehavior(message, err => { @@ -305,12 +320,12 @@ private void FlushBatch() } /// - /// Remains until pending events are persisted and then changes state to . + /// Event persisting state. Remains until pending events are persisted and then changes state to . /// Only events to be persisted are processed. All other messages are stashed internally. /// private EventsourcedState PersistingEvents() { - return new EventsourcedState("persisting events", false, (receive, message) => + return new EventsourcedState("persisting events", () => false, (receive, message) => { var handled = CommonProcessingStateBehavior(message, err => { diff --git a/src/core/Akka.Persistence/Eventsourced.cs b/src/core/Akka.Persistence/Eventsourced.cs index 72d2767f2a2..8033f49de11 100644 --- a/src/core/Akka.Persistence/Eventsourced.cs +++ b/src/core/Akka.Persistence/Eventsourced.cs @@ -19,7 +19,6 @@ namespace Akka.Persistence public interface IPendingHandlerInvocation { object Event { get; } - Action Handler { get; } } @@ -180,7 +179,7 @@ public IStash Stash /// /// Returns true if this persistent entity is currently recovering. /// - public bool IsRecovering => _currentState?.IsRecoveryRunning ?? true; + public bool IsRecovering => _currentState?.IsRecoveryRunning() ?? true; /// /// Returns true if this persistent entity has successfully finished recovery. @@ -297,6 +296,11 @@ public void DeleteSnapshots(SnapshotSelectionCriteria criteria) /// TBD public void Persist(TEvent @event, Action handler) { + if (IsRecovering) + { + throw new InvalidOperationException("Cannot persist during replay. Events can be persisted when receiving RecoveryCompleted or later."); + } + _pendingStashingPersistInvocations++; _pendingInvocations.AddLast(new StashingHandlerInvocation(@event, o => handler((TEvent)o))); _eventBatch.AddFirst(new AtomicWrite(new Persistent(@event, persistenceId: PersistenceId, @@ -313,17 +317,23 @@ public void Persist(TEvent @event, Action handler) /// TBD public void PersistAll(IEnumerable events, Action handler) { + if (IsRecovering) + { + throw new InvalidOperationException("Cannot persist during replay. Events can be persisted when receiving RecoveryCompleted or later."); + } + if (events == null) return; - Action inv = o => handler((TEvent)o); + void Inv(object o) => handler((TEvent)o); var persistents = ImmutableList.Empty.ToBuilder(); foreach (var @event in events) { _pendingStashingPersistInvocations++; - _pendingInvocations.AddLast(new StashingHandlerInvocation(@event, inv)); + _pendingInvocations.AddLast(new StashingHandlerInvocation(@event, Inv)); persistents.Add(new Persistent(@event, persistenceId: PersistenceId, sequenceNr: NextSequenceNr(), writerGuid: _writerGuid, sender: Sender)); } + if (persistents.Count > 0) _eventBatch.AddFirst(new AtomicWrite(persistents.ToImmutable())); } @@ -358,6 +368,11 @@ public void PersistAll(IEnumerable events, Action handle /// TBD public void PersistAsync(TEvent @event, Action handler) { + if (IsRecovering) + { + throw new InvalidOperationException("Cannot persist during replay. Events can be persisted when receiving RecoveryCompleted or later."); + } + _pendingInvocations.AddLast(new AsyncHandlerInvocation(@event, o => handler((TEvent)o))); _eventBatch.AddFirst(new AtomicWrite(new Persistent(@event, persistenceId: PersistenceId, sequenceNr: NextSequenceNr(), writerGuid: _writerGuid, sender: Sender))); @@ -373,13 +388,20 @@ public void PersistAsync(TEvent @event, Action handler) /// TBD public void PersistAllAsync(IEnumerable events, Action handler) { - Action inv = o => handler((TEvent)o); - foreach (var @event in events) + if (IsRecovering) + { + throw new InvalidOperationException("Cannot persist during replay. Events can be persisted when receiving RecoveryCompleted or later."); + } + + void Inv(object o) => handler((TEvent)o); + var enumerable = events as TEvent[] ?? events.ToArray(); + foreach (var @event in enumerable) { - _pendingInvocations.AddLast(new AsyncHandlerInvocation(@event, inv)); + _pendingInvocations.AddLast(new AsyncHandlerInvocation(@event, Inv)); } - _eventBatch.AddFirst(new AtomicWrite(events.Select(e => new Persistent(e, persistenceId: PersistenceId, - sequenceNr: NextSequenceNr(), writerGuid: _writerGuid, sender: Sender)) + + _eventBatch.AddFirst(new AtomicWrite(enumerable.Select(e => new Persistent(e, persistenceId: PersistenceId, + sequenceNr: NextSequenceNr(), writerGuid: _writerGuid, sender: Sender)) .ToImmutableList())); } @@ -406,6 +428,11 @@ public void PersistAllAsync(IEnumerable events, Action h /// TBD public void DeferAsync(TEvent evt, Action handler) { + if (IsRecovering) + { + throw new InvalidOperationException("Cannot persist during replay. Events can be persisted when receiving RecoveryCompleted or later."); + } + if (_pendingInvocations.Count == 0) { handler(evt); @@ -443,7 +470,7 @@ protected virtual void OnRecoveryFailure(Exception reason, object message = null if (message != null) { Log.Error(reason, "Exception in ReceiveRecover when replaying event type [{0}] with sequence number [{1}] for persistenceId [{2}]", - message.GetType(), LastSequenceNr, PersistenceId); + message.GetType(), LastSequenceNr, PersistenceId); } else { @@ -562,7 +589,7 @@ private void StashInternally(object currentMessage) { _internalStash.Stash(); } - catch(StashOverflowException e) + catch (StashOverflowException e) { var strategy = InternalStashOverflowStrategy; if (strategy is DiscardToDeadLetterStrategy) From 41c116d2a3d280481e584401608ec848dc147632 Mon Sep 17 00:00:00 2001 From: zbynek001 Date: Mon, 25 Jun 2018 17:40:32 +0200 Subject: [PATCH 40/70] Sharding update (#3524) * Provide access to known shard types https://github.com/akka/akka/issues/23912 * Separate sharding regions and proxies https://github.com/akka/akka/issues/23472 Fix lookup of coordinator for sharding proxies https://github.com/akka/akka/pull/23995 * Fix race in ClusterShardingFailureSpec AFAICT there was nothing ensuring the order of messages when sent to the shard and the region so first checkthat the passivation has happened before sending another add in the test https://github.com/akka/akka/issues/24013 * Better warning message on cluster sharding registration https://github.com/akka/akka/pull/24906 * entityId => Behavior in ClusterSharding API mixture of https://github.com/akka/akka/issues/24053 https://github.com/akka/akka/issues/21809 https://github.com/akka/akka/issues/24470 * sharding tests updated * headers fixed, docs updated * ClusterSharding: automatically choose start or startProxy by a node role https://github.com/akka/akka/issues/23934 --- docs/articles/clustering/cluster-sharding.md | 2 + .../ClusterShardingFailureSpec.cs | 13 +- .../ClusterShardingSpec.cs | 102 ++-- .../ClusterShardingInternalsSpec.cs | 82 +++ .../GetShardTypeNamesSpec.cs | 69 +++ .../ProxyShardingSpec.cs | 105 ++++ .../Akka.Cluster.Sharding/ClusterSharding.cs | 501 ++++++++++++++++-- .../ClusterShardingGuardian.cs | 9 +- .../ClusterShardingSettings.cs | 10 + .../Akka.Cluster.Sharding/DDataShard.cs | 6 +- .../Akka.Cluster.Sharding/PersistentShard.cs | 6 +- .../cluster/Akka.Cluster.Sharding/Shard.cs | 30 +- .../Akka.Cluster.Sharding/ShardRegion.cs | 29 +- 13 files changed, 831 insertions(+), 133 deletions(-) create mode 100644 src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingInternalsSpec.cs create mode 100644 src/contrib/cluster/Akka.Cluster.Sharding.Tests/GetShardTypeNamesSpec.cs create mode 100644 src/contrib/cluster/Akka.Cluster.Sharding.Tests/ProxyShardingSpec.cs diff --git a/docs/articles/clustering/cluster-sharding.md b/docs/articles/clustering/cluster-sharding.md index 4217a54da34..a25c73aed6b 100644 --- a/docs/articles/clustering/cluster-sharding.md +++ b/docs/articles/clustering/cluster-sharding.md @@ -55,6 +55,8 @@ In this example, we first specify way to resolve our message recipients in conte Second part of an example is registering custom actor type as sharded entity using `ClusterSharding.Start` or `ClusterSharding.StartAsync` methods. Result is the `IActorRef` to shard region used to communicate between current actor system and target entities. Shard region must be specified once per each type on each node, that is expected to participate in sharding entities of that type. Keep in mind, that it's recommended to wait for the current node to first fully join the cluster before initializing a shard regions in order to avoid potential timeouts. +In some cases, the actor may need to know the `entityId` associated with it. This can be achieved using the `entityPropsFactory` parameter to `ClusterSharding.Start` or `ClusterSharding.StartAsync`. The entity ID will be passed to the factory as a parameter, which can then be used in the creation of the actor. + In case when you want to send message to entities from specific node, but you don't want that node to participate in sharding itself, you can use `ShardRegionProxy` for that. Example: diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingFailureSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingFailureSpec.cs index c08c3bd3faa..9e85c5ef50f 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingFailureSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingFailureSpec.cs @@ -41,7 +41,7 @@ protected ClusterShardingFailureSpecConfig(string mode) serialization-bindings {{ ""System.Object"" = hyperion }} - }} + }} akka.loglevel = INFO akka.actor.provider = cluster akka.remote.log-remote-lifecycle-events = off @@ -176,7 +176,7 @@ public Entity() private readonly ClusterShardingFailureSpecConfig _config; private readonly List _storageLocations; - + protected ClusterShardingFailureSpec(ClusterShardingFailureSpecConfig config, Type type) : base(config, type) { @@ -194,7 +194,7 @@ protected ClusterShardingFailureSpec(ClusterShardingFailureSpecConfig config, Ty } protected bool IsDDataMode { get; } - + protected override void AfterTermination() { base.AfterTermination(); @@ -364,6 +364,13 @@ public void ClusterSharding_with_flaky_journal_network_should_recover_after_jour //Test the Shard passivate works during a journal failure shard2.Tell(new Passivate(PoisonPill.Instance), entity21); + + AwaitAssert(() => + { + region.Tell(new Get("21")); + ExpectMsg(v => v.Id == "21" && v.N == 0, hint: "Passivating did not reset Value down to 0"); + }); + region.Tell(new Add("21", 1)); region.Tell(new Get("21")); diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingSpec.cs index d822a77e65a..4003b139acf 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/ClusterShardingSpec.cs @@ -220,9 +220,15 @@ private Stop() public const int NumberOfShards = 12; private int _count = 0; + private readonly string id; - public Counter() + public static Props Props(string id) => Actor.Props.Create(() => new Counter(id)); + + public static string ShardingTypeName => "Counter"; + + public Counter(string id) { + this.id = id; Context.SetReceiveTimeout(TimeSpan.FromMinutes(2)); } @@ -233,7 +239,7 @@ protected override void PostStop() Thread.Sleep(500); } - public override string PersistenceId { get { return "Counter-" + Self.Path.Name; } } + public override string PersistenceId { get { return $"Counter.{ShardingTypeName}-{id}"; } } protected override bool ReceiveRecover(object message) { @@ -277,16 +283,17 @@ private void UpdateState(CounterChanged e) internal class QualifiedCounter : Counter { - public static Props Props(string typeName) + public static Props Props(string typeName, string id) { - return Actor.Props.Create(() => new QualifiedCounter(typeName)); + return Actor.Props.Create(() => new QualifiedCounter(typeName, id)); } public readonly string TypeName; public override string PersistenceId { get { return TypeName + "-" + Self.Path.Name; } } - public QualifiedCounter(string typeName) + public QualifiedCounter(string typeName, string id) + : base(id) { TypeName = typeName; } @@ -294,19 +301,34 @@ public QualifiedCounter(string typeName) internal class AnotherCounter : QualifiedCounter { - public AnotherCounter() - : base("AnotherCounter") + public static new Props Props(string id) + { + return Actor.Props.Create(() => new AnotherCounter(id)); + } + public static new string ShardingTypeName => nameof(AnotherCounter); + + public AnotherCounter(string id) + : base(AnotherCounter.ShardingTypeName, id) { } } internal class CounterSupervisor : ActorBase { - public readonly IActorRef Counter; + public static string ShardingTypeName => nameof(CounterSupervisor); - public CounterSupervisor() + public static Props Props(string id) { - Counter = Context.ActorOf(Props.Create(), "theCounter"); + return Actor.Props.Create(() => new CounterSupervisor(id)); + } + + public readonly string entityId; + public readonly IActorRef counter; + + public CounterSupervisor(string entityId) + { + this.entityId = entityId; + counter = Context.ActorOf(Counter.Props(entityId), "theCounter"); } protected override SupervisorStrategy SupervisorStrategy() @@ -328,7 +350,7 @@ protected override SupervisorStrategy SupervisorStrategy() protected override bool Receive(object message) { - Counter.Forward(message); + counter.Forward(message); return true; } } @@ -355,6 +377,9 @@ protected DDataClusterShardingWithEntityRecoverySpec(DDataClusterShardingWithEnt } public abstract class ClusterShardingSpec : MultiNodeClusterSpec { + // must use different unique name for some tests than the one used in API tests + public static string TestCounterShardingTypeName => $"Test{Counter.ShardingTypeName}"; + #region Setup private readonly Lazy _region; @@ -373,7 +398,7 @@ protected ClusterShardingSpec(ClusterShardingSpecConfig config, Type type) { _config = config; - _region = new Lazy(() => CreateRegion("counter", false)); + _region = new Lazy(() => CreateRegion(TestCounterShardingTypeName, false)); _rebalancingRegion = new Lazy(() => CreateRegion("rebalancingCounter", false)); _persistentEntitiesRegion = new Lazy(() => CreateRegion("RememberCounterEntities", true)); @@ -397,7 +422,7 @@ protected ClusterShardingSpec(ClusterShardingSpecConfig config, Type type) EnterBarrier("startup"); } protected bool IsDDataMode { get; } - + protected override void AfterTermination() { base.AfterTermination(); @@ -430,7 +455,7 @@ private void CreateCoordinator() { var typeNames = new[] { - "counter", "rebalancingCounter", "RememberCounterEntities", "AnotherRememberCounter", + TestCounterShardingTypeName, "rebalancingCounter", "RememberCounterEntities", "AnotherRememberCounter", "RememberCounter", "RebalancingRememberCounter", "AutoMigrateRememberRegionTest" }; @@ -479,7 +504,7 @@ private IActorRef CreateRegion(string typeName, bool rememberEntities) return Sys.ActorOf(Props.Create(() => new ShardRegion( typeName, - QualifiedCounter.Props(typeName), + entityId => QualifiedCounter.Props(typeName, entityId), settings, "/user/" + typeName + "Coordinator/singleton/coordinator", Counter.ExtractEntityId, @@ -609,7 +634,7 @@ public void ClusterSharding_should_use_second_node() r.Tell(new Counter.EntityEnvelope(2, Counter.Increment.Instance)); r.Tell(new Counter.Get(2)); ExpectMsg(3); - LastSender.Path.Should().Be(Node(_config.Second) / "user" / "counterRegion" / "2" / "2"); + LastSender.Path.Should().Be(Node(_config.Second) / "user" / $"{TestCounterShardingTypeName}Region" / "2" / "2"); r.Tell(new Counter.Get(11)); ExpectMsg(1); @@ -669,9 +694,9 @@ public void ClusterSharding_should_support_proxy_only_mode() var settings = ClusterShardingSettings.Create(cfg, Sys.Settings.Config.GetConfig("akka.cluster.singleton")); var proxy = Sys.ActorOf(ShardRegion.ProxyProps( - typeName: "counter", + typeName: TestCounterShardingTypeName, settings: settings, - coordinatorPath: "/user/counterCoordinator/singleton/coordinator", + coordinatorPath: $"/user/{TestCounterShardingTypeName}Coordinator/singleton/coordinator", extractEntityId: Counter.ExtractEntityId, extractShardId: Counter.ExtractShardId, replicator: Sys.DeadLetters, @@ -770,12 +795,12 @@ public void ClusterSharding_should_use_third_and_fourth_node() r.Tell(new Counter.EntityEnvelope(3, Counter.Increment.Instance)); r.Tell(new Counter.Get(3)); ExpectMsg(11); - LastSender.Path.Should().Be(Node(_config.Third) / "user" / "counterRegion" / "3" / "3"); + LastSender.Path.Should().Be(Node(_config.Third) / "user" / $"{TestCounterShardingTypeName}Region" / "3" / "3"); r.Tell(new Counter.EntityEnvelope(4, Counter.Increment.Instance)); r.Tell(new Counter.Get(4)); ExpectMsg(21); - LastSender.Path.Should().Be(Node(_config.Fourth) / "user" / "counterRegion" / "4" / "4"); + LastSender.Path.Should().Be(Node(_config.Fourth) / "user" / $"{TestCounterShardingTypeName}Region" / "4" / "4"); }, _config.First); EnterBarrier("first-update"); @@ -818,7 +843,7 @@ public void ClusterSharding_should_recover_coordinator_state_after_coordinator_c { _region.Value.Tell(new Counter.Get(3), probe3.Ref); probe3.ExpectMsg(11); - probe3.LastSender.Path.Should().Be(Node(_config.Third) / "user" / "counterRegion" / "3" / "3"); + probe3.LastSender.Path.Should().Be(Node(_config.Third) / "user" / $"{TestCounterShardingTypeName}Region" / "3" / "3"); }); }); @@ -829,7 +854,7 @@ public void ClusterSharding_should_recover_coordinator_state_after_coordinator_c { _region.Value.Tell(new Counter.Get(4), probe4.Ref); probe4.ExpectMsg(21); - probe4.LastSender.Path.Should().Be(Node(_config.Fourth) / "user" / "counterRegion" / "4" / "4"); + probe4.LastSender.Path.Should().Be(Node(_config.Fourth) / "user" / $"{TestCounterShardingTypeName}Region" / "4" / "4"); }); }); }, _config.Fifth); @@ -888,24 +913,24 @@ public void ClusterSharding_should_be_easy_to_use_with_extensions() { //#counter-start ClusterSharding.Get(Sys).Start( - typeName: "Counter", - entityProps: Props.Create(), + typeName: Counter.ShardingTypeName, + entityPropsFactory: entityId => Counter.Props(entityId), settings: ClusterShardingSettings.Create(Sys), extractEntityId: Counter.ExtractEntityId, extractShardId: Counter.ExtractShardId); //#counter-start ClusterSharding.Get(Sys).Start( - typeName: "AnotherCounter", - entityProps: Props.Create(), + typeName: AnotherCounter.ShardingTypeName, + entityPropsFactory: entityId => AnotherCounter.Props(entityId), settings: ClusterShardingSettings.Create(Sys), extractEntityId: Counter.ExtractEntityId, extractShardId: Counter.ExtractShardId); //#counter-supervisor-start ClusterSharding.Get(Sys).Start( - typeName: "SupervisedCounter", - entityProps: Props.Create(), + typeName: CounterSupervisor.ShardingTypeName, + entityPropsFactory: entityId => CounterSupervisor.Props(entityId), settings: ClusterShardingSettings.Create(Sys), extractEntityId: Counter.ExtractEntityId, extractShardId: Counter.ExtractShardId); @@ -915,18 +940,19 @@ public void ClusterSharding_should_be_easy_to_use_with_extensions() RunOn(() => { //#counter-usage - var counterRegion = ClusterSharding.Get(Sys).ShardRegion("Counter"); - counterRegion.Tell(new Counter.Get(123)); + var counterRegion = ClusterSharding.Get(Sys).ShardRegion(Counter.ShardingTypeName); + var entityId = 999; + counterRegion.Tell(new Counter.Get(entityId)); ExpectMsg(0); - counterRegion.Tell(new Counter.EntityEnvelope(123, Counter.Increment.Instance)); - counterRegion.Tell(new Counter.Get(123)); + counterRegion.Tell(new Counter.EntityEnvelope(entityId, Counter.Increment.Instance)); + counterRegion.Tell(new Counter.Get(entityId)); ExpectMsg(1); //#counter-usage - var anotherCounterRegion = ClusterSharding.Get(Sys).ShardRegion("AnotherCounter"); - anotherCounterRegion.Tell(new Counter.EntityEnvelope(123, Counter.Decrement.Instance)); - anotherCounterRegion.Tell(new Counter.Get(123)); + var anotherCounterRegion = ClusterSharding.Get(Sys).ShardRegion(AnotherCounter.ShardingTypeName); + anotherCounterRegion.Tell(new Counter.EntityEnvelope(entityId, Counter.Decrement.Instance)); + anotherCounterRegion.Tell(new Counter.Get(entityId)); ExpectMsg(-1); }, _config.Fifth); EnterBarrier("extension-used"); @@ -936,8 +962,8 @@ public void ClusterSharding_should_be_easy_to_use_with_extensions() { for (int i = 1000; i <= 1010; i++) { - ClusterSharding.Get(Sys).ShardRegion("Counter").Tell(new Counter.EntityEnvelope(i, Counter.Increment.Instance)); - ClusterSharding.Get(Sys).ShardRegion("Counter").Tell(new Counter.Get(i)); + ClusterSharding.Get(Sys).ShardRegion(Counter.ShardingTypeName).Tell(new Counter.EntityEnvelope(i, Counter.Increment.Instance)); + ClusterSharding.Get(Sys).ShardRegion(Counter.ShardingTypeName).Tell(new Counter.Get(i)); ExpectMsg(1); LastSender.Path.Address.Should().NotBe(Cluster.SelfAddress); } @@ -954,7 +980,7 @@ public void ClusterSharding_should_be_easy_API_for_starting() { var counterRegionViaStart = ClusterSharding.Get(Sys).Start( typeName: "ApiTest", - entityProps: Props.Create(), + entityPropsFactory: Counter.Props, settings: ClusterShardingSettings.Create(Sys), extractEntityId: Counter.ExtractEntityId, extractShardId: Counter.ExtractShardId); diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingInternalsSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingInternalsSpec.cs new file mode 100644 index 00000000000..bee6a932e0d --- /dev/null +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingInternalsSpec.cs @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.Cluster.Tools.Singleton; +using Akka.Configuration; +using Akka.TestKit.TestActors; +using FluentAssertions; +using Xunit; + +namespace Akka.Cluster.Sharding.Tests +{ + public class ClusterShardingInternalsSpec : Akka.TestKit.Xunit2.TestKit + { + ClusterSharding clusterSharding; + + public ClusterShardingInternalsSpec() : base(GetConfig()) + { + clusterSharding = ClusterSharding.Get(Sys); + } + + private Tuple ExtractEntityId(object message) + { + switch (message) + { + case int i: + return new Tuple(i.ToString(), message); + } + throw new NotSupportedException(); + } + + private string ExtractShardId(object message) + { + switch (message) + { + case int i: + return (i % 10).ToString(); + } + throw new NotSupportedException(); + } + + + public static Config GetConfig() + { + return ConfigurationFactory.ParseString("akka.actor.provider = cluster") + + .WithFallback(Sharding.ClusterSharding.DefaultConfig()) + .WithFallback(DistributedData.DistributedData.DefaultConfig()) + .WithFallback(ClusterSingletonManager.DefaultConfig()); + } + + [Fact] + public void ClusterSharding_must_start_a_region_in_proxy_mode_in_case_of_node_role_mismatch() + { + var settingsWithRole = ClusterShardingSettings.Create(Sys).WithRole("nonExistingRole"); + var typeName = "typeName"; + + var region = clusterSharding.Start( + typeName: typeName, + entityProps: Props.Empty, + settings: settingsWithRole, + extractEntityId: ExtractEntityId, + extractShardId: ExtractShardId, + allocationStrategy: new LeastShardAllocationStrategy(0, 0), + handOffStopMessage: PoisonPill.Instance); + + var proxy = clusterSharding.StartProxy( + typeName: typeName, + role: settingsWithRole.Role, + extractEntityId: ExtractEntityId, + extractShardId: ExtractShardId + ); + + region.Should().BeSameAs(proxy); + } + } +} diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/GetShardTypeNamesSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/GetShardTypeNamesSpec.cs new file mode 100644 index 00000000000..d86e7f5eb1f --- /dev/null +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/GetShardTypeNamesSpec.cs @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Cluster.Tools.Singleton; +using Akka.Configuration; +using Akka.TestKit.TestActors; +using FluentAssertions; +using Xunit; + +namespace Akka.Cluster.Sharding.Tests +{ + public class GetShardTypeNamesSpec : Akka.TestKit.Xunit2.TestKit + { + public GetShardTypeNamesSpec() : base(GetConfig()) + { + } + + public static Config GetConfig() + { + return ConfigurationFactory.ParseString("akka.actor.provider = cluster") + + .WithFallback(Sharding.ClusterSharding.DefaultConfig()) + .WithFallback(DistributedData.DistributedData.DefaultConfig()) + .WithFallback(ClusterSingletonManager.DefaultConfig()); + } + + [Fact] + public void GetShardTypeNames_must_contain_empty_when_join_cluster_without_shards() + { + ClusterSharding.Get(Sys).ShardTypeNames.Should().BeEmpty(); + } + + [Fact] + 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", EchoActor.Props(this), settings, ExtractEntityId, ExtractShardId); + ClusterSharding.Get(Sys).Start("type2", EchoActor.Props(this), settings, ExtractEntityId, ExtractShardId); + + ClusterSharding.Get(Sys).ShardTypeNames.ShouldBeEquivalentTo(new string[] { "type1", "type2" }); + } + + private Tuple ExtractEntityId(object message) + { + switch (message) + { + case int i: + return new Tuple(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/ProxyShardingSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ProxyShardingSpec.cs new file mode 100644 index 00000000000..c4c7c971458 --- /dev/null +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ProxyShardingSpec.cs @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.Cluster.Tools.Singleton; +using Akka.Configuration; +using Akka.TestKit.TestActors; +using FluentAssertions; +using Xunit; + +namespace Akka.Cluster.Sharding.Tests +{ + public class ProxyShardingSpec : Akka.TestKit.Xunit2.TestKit + { + ClusterSharding clusterSharding; + ClusterShardingSettings shardingSettings; + private MessageExtractor messageExtractor = new MessageExtractor(10); + + public ProxyShardingSpec() : base(GetConfig()) + { + var role = "Shard"; + clusterSharding = ClusterSharding.Get(Sys); + shardingSettings = ClusterShardingSettings.Create(Sys); + clusterSharding.StartProxy("myType", role, IdExtractor, ShardResolver); + } + + private class MessageExtractor : HashCodeMessageExtractor + { + public MessageExtractor(int maxNumberOfShards) : base(maxNumberOfShards) + { + } + + public override string EntityId(object message) + { + return "dummyId"; + } + } + + private Tuple IdExtractor(object message) + { + switch (message) + { + case int i: + return new Tuple(i.ToString(), message); + } + throw new NotSupportedException(); + } + + private string ShardResolver(object message) + { + switch (message) + { + case int i: + return (i % 10).ToString(); + } + throw new NotSupportedException(); + } + + + public static Config GetConfig() + { + return ConfigurationFactory.ParseString("akka.actor.provider = cluster") + + .WithFallback(Sharding.ClusterSharding.DefaultConfig()) + .WithFallback(DistributedData.DistributedData.DefaultConfig()) + .WithFallback(ClusterSingletonManager.DefaultConfig()); + } + + [Fact] + public void ProxyShardingSpec_Proxy_should_be_found() + { + IActorRef proxyActor = Sys.ActorSelection("akka://test/system/sharding/myTypeProxy") + .ResolveOne(TimeSpan.FromSeconds(5)).Result; + + proxyActor.Path.Should().NotBeNull(); + proxyActor.Path.ToString().Should().EndWith("Proxy"); + } + + [Fact] + public void ProxyShardingSpec_Shard_region_should_be_found() + { + var shardRegion = clusterSharding.Start("myType", EchoActor.Props(this), shardingSettings, messageExtractor); + + shardRegion.Path.Should().NotBeNull(); + shardRegion.Path.ToString().Should().EndWith("myType"); + } + + [Fact] + public void ProxyShardingSpec_Shard_coordinator_should_be_found() + { + var shardRegion = clusterSharding.Start("myType", EchoActor.Props(this), shardingSettings, messageExtractor); + + IActorRef shardCoordinator = Sys.ActorSelection("akka://test/system/sharding/myTypeCoordinator") + .ResolveOne(TimeSpan.FromSeconds(5)).Result; + + shardCoordinator.Path.Should().NotBeNull(); + shardCoordinator.Path.ToString().Should().EndWith("Coordinator"); + } + } +} diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterSharding.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterSharding.cs index 935dc8e814e..c9cda07a2c5 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterSharding.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterSharding.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Akka.Actor; @@ -228,6 +229,7 @@ public class ClusterSharding : IExtension { private readonly Lazy _guardian; private readonly ConcurrentDictionary _regions = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _proxies = new ConcurrentDictionary(); private readonly ExtendedActorSystem _system; private readonly Cluster _cluster; @@ -281,6 +283,10 @@ public static Config DefaultConfig() /// Register a named entity type by defining the of the entity actor and /// functions to extract entity and shard identifier from messages. The /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// /// /// The name of the entity type /// @@ -313,25 +319,31 @@ public IActorRef Start( IShardAllocationStrategy allocationStrategy, object handOffStopMessage) { - RequireClusterRole(settings.Role); - - var timeout = _system.Settings.CreationTimeout; - var startMsg = new ClusterShardingGuardian.Start(typeName, entityProps, settings, extractEntityId, extractShardId, allocationStrategy, handOffStopMessage); - - var reply = _guardian.Value.Ask(startMsg, timeout).Result; - switch (reply) + if (settings.ShouldHostShard(_cluster)) { - case ClusterShardingGuardian.Started started: - var shardRegion = started.ShardRegion; - _regions.TryAdd(typeName, shardRegion); - return shardRegion; - - case Status.Failure failure: - ExceptionDispatchInfo.Capture(failure.Cause).Throw(); - return ActorRefs.Nobody; - - default: - throw new ActorInitializationException($"Unsupported guardian response: {reply}"); + var timeout = _system.Settings.CreationTimeout; + var startMsg = new ClusterShardingGuardian.Start(typeName, _ => entityProps, settings, extractEntityId, extractShardId, allocationStrategy, handOffStopMessage); + + var reply = _guardian.Value.Ask(startMsg, timeout).Result; + switch (reply) + { + case ClusterShardingGuardian.Started started: + var shardRegion = started.ShardRegion; + _regions.TryAdd(typeName, shardRegion); + return shardRegion; + + case Status.Failure failure: + ExceptionDispatchInfo.Capture(failure.Cause).Throw(); + return ActorRefs.Nobody; + + default: + throw new ActorInitializationException($"Unsupported guardian response: {reply}"); + } + } + else + { + _cluster.System.Log.Debug("Starting Shard Region Proxy [{0}] (no actors will be hosted on this node)...", typeName); + return StartProxy(typeName, settings.Role, extractEntityId, extractShardId); } } @@ -339,6 +351,10 @@ public IActorRef Start( /// Register a named entity type by defining the of the entity actor and /// functions to extract entity and shard identifier from messages. The /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// /// /// The name of the entity type /// @@ -371,25 +387,31 @@ public async Task StartAsync( IShardAllocationStrategy allocationStrategy, object handOffStopMessage) { - RequireClusterRole(settings.Role); - - var timeout = _system.Settings.CreationTimeout; - var startMsg = new ClusterShardingGuardian.Start(typeName, entityProps, settings, extractEntityId, extractShardId, allocationStrategy, handOffStopMessage); - - var reply = await _guardian.Value.Ask(startMsg, timeout); - switch (reply) + if (settings.ShouldHostShard(_cluster)) { - case ClusterShardingGuardian.Started started: - var shardRegion = started.ShardRegion; - _regions.TryAdd(typeName, shardRegion); - return shardRegion; - - case Status.Failure failure: - ExceptionDispatchInfo.Capture(failure.Cause).Throw(); - return ActorRefs.Nobody; - - default: - throw new ActorInitializationException($"Unsupported guardian response: {reply}"); + var timeout = _system.Settings.CreationTimeout; + var startMsg = new ClusterShardingGuardian.Start(typeName, _ => entityProps, settings, extractEntityId, extractShardId, allocationStrategy, handOffStopMessage); + + var reply = await _guardian.Value.Ask(startMsg, timeout); + switch (reply) + { + case ClusterShardingGuardian.Started started: + var shardRegion = started.ShardRegion; + _regions.TryAdd(typeName, shardRegion); + return shardRegion; + + case Status.Failure failure: + ExceptionDispatchInfo.Capture(failure.Cause).Throw(); + return ActorRefs.Nobody; + + default: + throw new ActorInitializationException($"Unsupported guardian response: {reply}"); + } + } + else + { + _cluster.System.Log.Debug("Starting Shard Region Proxy [{0}] (no actors will be hosted on this node)...", typeName); + return await StartProxyAsync(typeName, settings.Role, extractEntityId, extractShardId); } } @@ -397,6 +419,10 @@ public async Task StartAsync( /// Register a named entity type by defining the of the entity actor and /// functions to extract entity and shard identifier from messages. The /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// /// /// The name of the entity type /// @@ -419,9 +445,7 @@ public IActorRef Start( ExtractEntityId extractEntityId, ExtractShardId extractShardId) { - var allocationStrategy = new LeastShardAllocationStrategy( - Settings.TunningParameters.LeastShardAllocationRebalanceThreshold, - Settings.TunningParameters.LeastShardAllocationMaxSimultaneousRebalance); + var allocationStrategy = DefaultShardAllocationStrategy(settings); return Start(typeName, entityProps, settings, extractEntityId, extractShardId, allocationStrategy, PoisonPill.Instance); } @@ -429,6 +453,10 @@ public IActorRef Start( /// Register a named entity type by defining the of the entity actor and /// functions to extract entity and shard identifier from messages. The /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// /// /// The name of the entity type /// @@ -451,9 +479,7 @@ public Task StartAsync( ExtractEntityId extractEntityId, ExtractShardId extractShardId) { - var allocationStrategy = new LeastShardAllocationStrategy( - Settings.TunningParameters.LeastShardAllocationRebalanceThreshold, - Settings.TunningParameters.LeastShardAllocationMaxSimultaneousRebalance); + var allocationStrategy = DefaultShardAllocationStrategy(settings); return StartAsync(typeName, entityProps, settings, extractEntityId, extractShardId, allocationStrategy, PoisonPill.Instance); } @@ -461,6 +487,10 @@ public Task StartAsync( /// Register a named entity type by defining the of the entity actor and /// functions to extract entity and shard identifier from messages. The /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// /// /// The name of the entity type /// @@ -489,6 +519,10 @@ public IActorRef Start(string typeName, Props entityProps, ClusterShardingSettin /// Register a named entity type by defining the of the entity actor and /// functions to extract entity and shard identifier from messages. The /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// /// /// The name of the entity type /// @@ -517,6 +551,10 @@ public Task StartAsync(string typeName, Props entityProps, ClusterSha /// Register a named entity type by defining the of the entity actor and /// functions to extract entity and shard identifier from messages. The /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// /// /// The name of the entity type /// @@ -534,9 +572,7 @@ public IActorRef Start(string typeName, Props entityProps, ClusterShardingSettin entityProps, settings, messageExtractor, - new LeastShardAllocationStrategy( - Settings.TunningParameters.LeastShardAllocationRebalanceThreshold, - Settings.TunningParameters.LeastShardAllocationMaxSimultaneousRebalance), + DefaultShardAllocationStrategy(settings), PoisonPill.Instance); } @@ -544,6 +580,10 @@ public IActorRef Start(string typeName, Props entityProps, ClusterShardingSettin /// Register a named entity type by defining the of the entity actor and /// functions to extract entity and shard identifier from messages. The /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// /// /// The name of the entity type /// @@ -561,9 +601,334 @@ public Task StartAsync(string typeName, Props entityProps, ClusterSha entityProps, settings, messageExtractor, - new LeastShardAllocationStrategy( - Settings.TunningParameters.LeastShardAllocationRebalanceThreshold, - Settings.TunningParameters.LeastShardAllocationMaxSimultaneousRebalance), + DefaultShardAllocationStrategy(settings), + PoisonPill.Instance); + } + + + /// + /// Register a named entity type by defining the of the entity actor and + /// functions to extract entity and shard identifier from messages. The + /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// + /// + /// The name of the entity type + /// + /// Function that, given an entity id, returns the of the entity actors that will be created by the + /// + /// Configuration settings, see + /// + /// Partial function to extract the entity id and the message to send to the entity from the incoming message, + /// if the partial function does not match the message will be `unhandled`, + /// i.e.posted as `Unhandled` messages on the event stream + /// + /// + /// Function to determine the shard id for an incoming message, only messages that passed the `extractEntityId` will be used + /// + /// Possibility to use a custom shard allocation and rebalancing logic + /// + /// The message that will be sent to entities when they are to be stopped for a rebalance or + /// graceful shutdown of a , e.g. . + /// + /// + /// This exception is thrown when the cluster member doesn't have the role specified in . + /// + /// The actor ref of the that is to be responsible for the shard. + public IActorRef Start( + string typeName, + Func entityPropsFactory, + ClusterShardingSettings settings, + ExtractEntityId extractEntityId, + ExtractShardId extractShardId, + IShardAllocationStrategy allocationStrategy, + object handOffStopMessage) + { + if (settings.ShouldHostShard(_cluster)) + { + var timeout = _system.Settings.CreationTimeout; + var startMsg = new ClusterShardingGuardian.Start(typeName, entityPropsFactory, settings, extractEntityId, extractShardId, allocationStrategy, handOffStopMessage); + + var reply = _guardian.Value.Ask(startMsg, timeout).Result; + switch (reply) + { + case ClusterShardingGuardian.Started started: + var shardRegion = started.ShardRegion; + _regions.TryAdd(typeName, shardRegion); + return shardRegion; + + case Status.Failure failure: + ExceptionDispatchInfo.Capture(failure.Cause).Throw(); + return ActorRefs.Nobody; + + default: + throw new ActorInitializationException($"Unsupported guardian response: {reply}"); + } + } + else + { + _cluster.System.Log.Debug("Starting Shard Region Proxy [{0}] (no actors will be hosted on this node)...", typeName); + return StartProxy(typeName, settings.Role, extractEntityId, extractShardId); + } + } + + /// + /// Register a named entity type by defining the of the entity actor and + /// functions to extract entity and shard identifier from messages. The + /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// + /// + /// The name of the entity type + /// + /// Function that, given an entity id, returns the of the entity actors that will be created by the + /// + /// Configuration settings, see + /// + /// Partial function to extract the entity id and the message to send to the entity from the incoming message, + /// if the partial function does not match the message will be `unhandled`, + /// i.e.posted as `Unhandled` messages on the event stream + /// + /// + /// Function to determine the shard id for an incoming message, only messages that passed the `extractEntityId` will be used + /// + /// Possibility to use a custom shard allocation and rebalancing logic + /// + /// The message that will be sent to entities when they are to be stopped for a rebalance or + /// graceful shutdown of a , e.g. . + /// + /// + /// This exception is thrown when the cluster member doesn't have the role specified in . + /// + /// The actor ref of the that is to be responsible for the shard. + public async Task StartAsync( + string typeName, + Func entityPropsFactory, + ClusterShardingSettings settings, + ExtractEntityId extractEntityId, + ExtractShardId extractShardId, + IShardAllocationStrategy allocationStrategy, + object handOffStopMessage) + { + if (settings.ShouldHostShard(_cluster)) + { + var timeout = _system.Settings.CreationTimeout; + var startMsg = new ClusterShardingGuardian.Start(typeName, entityPropsFactory, settings, extractEntityId, extractShardId, allocationStrategy, handOffStopMessage); + + var reply = await _guardian.Value.Ask(startMsg, timeout); + switch (reply) + { + case ClusterShardingGuardian.Started started: + var shardRegion = started.ShardRegion; + _regions.TryAdd(typeName, shardRegion); + return shardRegion; + + case Status.Failure failure: + ExceptionDispatchInfo.Capture(failure.Cause).Throw(); + return ActorRefs.Nobody; + + default: + throw new ActorInitializationException($"Unsupported guardian response: {reply}"); + } + } + else + { + _cluster.System.Log.Debug("Starting Shard Region Proxy [{0}] (no actors will be hosted on this node)...", typeName); + return StartProxy(typeName, settings.Role, extractEntityId, extractShardId); + } + } + + /// + /// Register a named entity type by defining the of the entity actor and + /// functions to extract entity and shard identifier from messages. The + /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// + /// + /// The name of the entity type + /// + /// Function that, given an entity id, returns the of the entity actors that will be created by the + /// + /// Configuration settings, see + /// + /// Partial function to extract the entity id and the message to send to the entity from the incoming message, + /// if the partial function does not match the message will be `unhandled`, + /// i.e.posted as `Unhandled` messages on the event stream + /// + /// + /// Function to determine the shard id for an incoming message, only messages that passed the `extractEntityId` will be used + /// + /// The actor ref of the that is to be responsible for the shard. + public IActorRef Start( + string typeName, + Func entityPropsFactory, + ClusterShardingSettings settings, + ExtractEntityId extractEntityId, + ExtractShardId extractShardId) + { + var allocationStrategy = DefaultShardAllocationStrategy(settings); + return Start(typeName, entityPropsFactory, settings, extractEntityId, extractShardId, allocationStrategy, PoisonPill.Instance); + } + + /// + /// Register a named entity type by defining the of the entity actor and + /// functions to extract entity and shard identifier from messages. The + /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// + /// + /// The name of the entity type + /// + /// Function that, given an entity id, returns the of the entity actors that will be created by the + /// + /// Configuration settings, see + /// + /// Partial function to extract the entity id and the message to send to the entity from the incoming message, + /// if the partial function does not match the message will be `unhandled`, + /// i.e.posted as `Unhandled` messages on the event stream + /// + /// + /// Function to determine the shard id for an incoming message, only messages that passed the `extractEntityId` will be used + /// + /// The actor ref of the that is to be responsible for the shard. + public Task StartAsync( + string typeName, + Func entityPropsFactory, + ClusterShardingSettings settings, + ExtractEntityId extractEntityId, + ExtractShardId extractShardId) + { + var allocationStrategy = DefaultShardAllocationStrategy(settings); + return StartAsync(typeName, entityPropsFactory, settings, extractEntityId, extractShardId, allocationStrategy, PoisonPill.Instance); + } + + /// + /// Register a named entity type by defining the of the entity actor and + /// functions to extract entity and shard identifier from messages. The + /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// + /// + /// The name of the entity type + /// + /// Function that, given an entity id, returns the of the entity actors that will be created by the + /// + /// Configuration settings, see + /// + /// Functions to extract the entity id, shard id, and the message to send to the entity from the incoming message. + /// + /// Possibility to use a custom shard allocation and rebalancing logic + /// + /// The message that will be sent to entities when they are to be stopped for a rebalance or + /// graceful shutdown of a , e.g. . + /// + /// The actor ref of the that is to be responsible for the shard. + public IActorRef Start(string typeName, Func entityPropsFactory, ClusterShardingSettings settings, + IMessageExtractor messageExtractor, IShardAllocationStrategy allocationStrategy, object handOffMessage) + { + ExtractEntityId extractEntityId = messageExtractor.ToExtractEntityId(); + ExtractShardId extractShardId = messageExtractor.ShardId; + + return Start(typeName, entityPropsFactory, settings, extractEntityId, extractShardId, allocationStrategy, handOffMessage); + } + + /// + /// Register a named entity type by defining the of the entity actor and + /// functions to extract entity and shard identifier from messages. The + /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// + /// + /// The name of the entity type + /// + /// Function that, given an entity id, returns the of the entity actors that will be created by the + /// + /// Configuration settings, see + /// + /// Functions to extract the entity id, shard id, and the message to send to the entity from the incoming message. + /// + /// Possibility to use a custom shard allocation and rebalancing logic + /// + /// The message that will be sent to entities when they are to be stopped for a rebalance or + /// graceful shutdown of a , e.g. . + /// + /// The actor ref of the that is to be responsible for the shard. + public Task StartAsync(string typeName, Func entityPropsFactory, ClusterShardingSettings settings, + IMessageExtractor messageExtractor, IShardAllocationStrategy allocationStrategy, object handOffMessage) + { + ExtractEntityId extractEntityId = messageExtractor.ToExtractEntityId(); + ExtractShardId extractShardId = messageExtractor.ShardId; + + return StartAsync(typeName, entityPropsFactory, settings, extractEntityId, extractShardId, allocationStrategy, handOffMessage); + } + + /// + /// Register a named entity type by defining the of the entity actor and + /// functions to extract entity and shard identifier from messages. The + /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// + /// + /// The name of the entity type + /// + /// Function that, given an entity id, returns the of the entity actors that will be created by the + /// + /// Configuration settings, see + /// + /// Functions to extract the entity id, shard id, and the message to send to the entity from the incoming message. + /// + /// The actor ref of the that is to be responsible for the shard. + public IActorRef Start(string typeName, Func entityPropsFactory, ClusterShardingSettings settings, + IMessageExtractor messageExtractor) + { + return Start(typeName, + entityPropsFactory, + settings, + messageExtractor, + DefaultShardAllocationStrategy(settings), + PoisonPill.Instance); + } + + /// + /// Register a named entity type by defining the of the entity actor and + /// functions to extract entity and shard identifier from messages. The + /// actor for this type can later be retrieved with the method. + /// + /// This method will start a in proxy mode in case if there is no match between the roles of + /// the current cluster node and the role specified in passed to this method. + /// + /// + /// The name of the entity type + /// + /// Function that, given an entity id, returns the of the entity actors that will be created by the + /// + /// Configuration settings, see + /// + /// Functions to extract the entity id, shard id, and the message to send to the entity from the incoming message. + /// + /// The actor ref of the that is to be responsible for the shard. + public Task StartAsync(string typeName, Func entityPropsFactory, ClusterShardingSettings settings, + IMessageExtractor messageExtractor) + { + return StartAsync(typeName, + entityPropsFactory, + settings, + messageExtractor, + DefaultShardAllocationStrategy(settings), PoisonPill.Instance); } @@ -596,7 +961,7 @@ public IActorRef StartProxy(string typeName, string role, ExtractEntityId extrac switch (reply) { case ClusterShardingGuardian.Started started: - _regions.TryAdd(typeName, started.ShardRegion); + _proxies.TryAdd(typeName, started.ShardRegion); return started.ShardRegion; case Status.Failure failure: @@ -637,7 +1002,7 @@ public async Task StartProxyAsync(string typeName, string role, Extra switch (reply) { case ClusterShardingGuardian.Started started: - _regions.TryAdd(typeName, started.ShardRegion); + _proxies.TryAdd(typeName, started.ShardRegion); return started.ShardRegion; case Status.Failure failure: @@ -703,6 +1068,11 @@ Tuple extractEntityId(Msg msg) return StartProxyAsync(typeName, role, extractEntityId, messageExtractor.ShardId); } + /// + /// get all currently defined sharding type names. + /// + public ImmutableHashSet ShardTypeNames => _regions.Keys.ToImmutableHashSet(); + /// /// Retrieve the actor reference of the actor responsible for the named entity type. /// The entity type must be registered with the method before it can be used here. @@ -717,18 +1087,35 @@ public IActorRef ShardRegion(string typeName) { if (_regions.TryGetValue(typeName, out var region)) return region; + if (_proxies.TryGetValue(typeName, out region)) + return region; throw new ArgumentException($"Shard type [{typeName}] must be started first"); } - private void RequireClusterRole(string role) + /// + /// Retrieve the actor reference of the actor that will act as a proxy to the + /// named entity type running in another data center. A proxy within the same data center can be accessed + /// with instead of this method. The entity type must be registered with the + /// method before it can be used here. Messages to the entity is always sent + /// via the . + /// + /// + /// + public IActorRef ShardRegionProxy(string typeName) { - if (!(string.IsNullOrEmpty(role) || _cluster.SelfRoles.Contains(role))) - { - throw new IllegalStateException( - $"This cluster member [{_cluster.SelfAddress}] doesn't have the role [{role}]"); - } + if (_proxies.TryGetValue(typeName, out var proxy)) + return proxy; + throw new ArgumentException($"Shard type [{typeName}] must be started first"); + } + + private IShardAllocationStrategy DefaultShardAllocationStrategy(ClusterShardingSettings settings) + { + return new LeastShardAllocationStrategy( + Settings.TunningParameters.LeastShardAllocationRebalanceThreshold, + Settings.TunningParameters.LeastShardAllocationMaxSimultaneousRebalance); } + } /// @@ -777,7 +1164,7 @@ public interface IMessageExtractor object EntityMessage(object message); /// - /// Extract the entity id from an incoming . Only messages that + /// Extract the shard id from an incoming . Only messages that /// passed the method will be used as input to this method. /// /// TBD diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs index ba2140ed1d7..b775f66d607 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingGuardian.cs @@ -55,7 +55,7 @@ public sealed class Start : INoSerializationVerificationNeeded /// /// TBD /// - public readonly Props EntityProps; + public readonly Func EntityProps; /// /// TBD /// @@ -90,7 +90,7 @@ public sealed class Start : INoSerializationVerificationNeeded /// /// This exception is thrown when the specified or is undefined. /// - public Start(string typeName, Props entityProps, ClusterShardingSettings settings, + public Start(string typeName, Func entityProps, ClusterShardingSettings settings, ExtractEntityId extractEntityId, ExtractShardId extractShardId, IShardAllocationStrategy allocationStrategy, object handOffStopMessage) { if (string.IsNullOrEmpty(typeName)) throw new ArgumentNullException(nameof(typeName), "ClusterSharding start requires type name to be provided"); @@ -218,9 +218,8 @@ public ClusterShardingGuardian() try { var settings = startProxy.Settings; - var encName = Uri.EscapeDataString(startProxy.TypeName); - var coordinatorSingletonManagerName = CoordinatorSingletonManagerName(encName); - var coordinatorPath = CoordinatorPath(encName); + var encName = Uri.EscapeDataString(startProxy.TypeName + "Proxy"); + var coordinatorPath = CoordinatorPath(Uri.EscapeDataString(startProxy.TypeName)); var shardRegion = Context.Child(encName); if (Equals(shardRegion, ActorRefs.Nobody)) diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingSettings.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingSettings.cs index d3483d3d970..d6606483b55 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingSettings.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ClusterShardingSettings.cs @@ -278,6 +278,16 @@ public ClusterShardingSettings( CoordinatorSingletonSettings = coordinatorSingletonSettings; } + /// + /// If true, this node should run the shard region, otherwise just a shard proxy should started on this node. + /// + /// + /// + internal bool ShouldHostShard(Cluster cluster) + { + return string.IsNullOrEmpty(Role) || cluster.SelfRoles.Contains(Role); + } + /// /// TBD /// diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/DDataShard.cs b/src/contrib/cluster/Akka.Cluster.Sharding/DDataShard.cs index e8d9738f39f..46659feccd7 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/DDataShard.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/DDataShard.cs @@ -35,7 +35,7 @@ internal sealed class DDataShard : ActorBase, IShard, IWithUnboundedStash public string TypeName { get; } public string ShardId { get; } - public Props EntityProps { get; } + public Func EntityProps { get; } public ClusterShardingSettings Settings { get; } public ExtractEntityId ExtractEntityId { get; } public ExtractShardId ExtractShardId { get; } @@ -72,7 +72,7 @@ internal sealed class DDataShard : ActorBase, IShard, IWithUnboundedStash public DDataShard( string typeName, ShardId shardId, - Props entityProps, + Func entityProps, ClusterShardingSettings settings, ExtractEntityId extractEntityId, ExtractShardId extractShardId, @@ -100,7 +100,7 @@ public DDataShard( _readConsistency = new ReadMajority(settings.TunningParameters.WaitingForStateTimeout, majorityCap); _writeConsistency = new WriteMajority(settings.TunningParameters.UpdatingStateTimeout, majorityCap); _stateKeys = Enumerable.Range(0, NrOfKeys).Select(i => new ORSetKey($"shard-{typeName}-{shardId}-{i}")).ToImmutableArray(); - + GetState(); } diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/PersistentShard.cs b/src/contrib/cluster/Akka.Cluster.Sharding/PersistentShard.cs index 626187c7cdd..932bbc63b73 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/PersistentShard.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/PersistentShard.cs @@ -33,7 +33,7 @@ internal sealed class PersistentShard : PersistentActor, IShard public string TypeName { get; } public string ShardId { get; } - public Props EntityProps { get; } + public Func EntityProps { get; } public ClusterShardingSettings Settings { get; } public ExtractEntityId ExtractEntityId { get; } public ExtractShardId ExtractShardId { get; } @@ -45,12 +45,12 @@ internal sealed class PersistentShard : PersistentActor, IShard public ImmutableHashSet Passivating { get; set; } = ImmutableHashSet.Empty; public ImmutableDictionary>> MessageBuffers { get; set; } = ImmutableDictionary>>.Empty; - private EntityRecoveryStrategy RememberedEntitiesRecoveryStrategy { get; } + private EntityRecoveryStrategy RememberedEntitiesRecoveryStrategy { get; } public PersistentShard( string typeName, string shardId, - Props entityProps, + Func entityProps, ClusterShardingSettings settings, ExtractEntityId extractEntityId, ExtractShardId extractShardId, diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs b/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs index 489cc8eed58..194b28aa84c 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs @@ -18,7 +18,7 @@ namespace Akka.Cluster.Sharding using ShardId = String; using EntityId = String; using Msg = Object; - + internal interface IShard { IActorContext Context { get; } @@ -26,7 +26,7 @@ internal interface IShard IActorRef Sender { get; } string TypeName { get; } ShardId ShardId { get; } - Props EntityProps { get; } + Func EntityProps { get; } ClusterShardingSettings Settings { get; } ExtractEntityId ExtractEntityId { get; } ExtractShardId ExtractShardId { get; } @@ -360,7 +360,7 @@ public override int GetHashCode() public ILoggingAdapter Log { get; } = Context.GetLogger(); public string TypeName { get; } public string ShardId { get; } - public Props EntityProps { get; } + public Func EntityProps { get; } public ClusterShardingSettings Settings { get; } public ExtractEntityId ExtractEntityId { get; } public ExtractShardId ExtractShardId { get; } @@ -377,7 +377,7 @@ public override int GetHashCode() public Shard( string typeName, string shardId, - Props entityProps, + Func entityProps, ClusterShardingSettings settings, ExtractEntityId extractEntityId, ExtractShardId extractShardId, @@ -417,14 +417,14 @@ public static void Initialized(this TShard shard) where TShard : IShard { shard.Context.Parent.Tell(new ShardInitialized(shard.ShardId)); } - + public static void BaseProcessChange(this TShard shard, T evt, Action handler) where TShard : IShard where T : Shard.StateChange { handler(evt); } - + public static bool HandleCommand(this TShard shard, object message) where TShard : IShard { switch (message) @@ -471,7 +471,7 @@ private static void HandleShardRegionQuery(this TShard shard, Shard.ISha break; } } - + public static void BaseEntityTerminated(this TShard shard, IActorRef tref) where TShard : IShard { if (shard.IdByRef.TryGetValue(tref, out var id)) @@ -616,14 +616,14 @@ private static void Passivate(this TShard shard, IActorRef entity, objec shard.Log.Debug("Unknown entity {0}. Not sending stopMessage back to entity.", entity); } } - + public static void PassivateCompleted(this TShard shard, Shard.EntityStopped evt) where TShard: IShard { shard.Log.Debug("Entity stopped after passivation [{0}]", evt.EntityId); shard.State = new Shard.ShardState(shard.State.Entries.Remove(evt.EntityId)); shard.MessageBuffers = shard.MessageBuffers.Remove(evt.EntityId); } - + public static void SendMessageBuffer(this TShard shard, Shard.EntityStarted message) where TShard: IShard { var id = message.EntityId; @@ -677,7 +677,7 @@ private static void DeliverMessage(this TShard shard, object message, IA shard.DeliverTo(id, message, payload, sender); } } - + internal static void BaseDeliverTo(this TShard shard, string id, object message, object payload, IActorRef sender) where TShard : IShard { var name = Uri.EscapeDataString(id); @@ -688,7 +688,7 @@ internal static void BaseDeliverTo(this TShard shard, string id, object else child.Tell(payload, sender); } - + internal static IActorRef GetEntity(this TShard shard, string id) where TShard: IShard { var name = Uri.EscapeDataString(id); @@ -697,7 +697,7 @@ internal static IActorRef GetEntity(this TShard shard, string id) where { shard.Log.Debug("Starting entity [{0}] in shard [{1}]", id, shard.ShardId); - child = shard.Context.Watch(shard.Context.ActorOf(shard.EntityProps, name)); + child = shard.Context.Watch(shard.Context.ActorOf(shard.EntityProps(id), name)); shard.IdByRef = shard.IdByRef.SetItem(child, id); shard.RefById = shard.RefById.SetItem(id, child); shard.State = new Shard.ShardState(shard.State.Entries.Add(id)); @@ -706,12 +706,12 @@ internal static IActorRef GetEntity(this TShard shard, string id) where return child; } - internal static int TotalBufferSize(this TShard shard) where TShard : IShard => + internal static int TotalBufferSize(this TShard shard) where TShard : IShard => shard.MessageBuffers.Aggregate(0, (sum, entity) => sum + entity.Value.Count); #endregion - public static Props Props(string typeName, ShardId shardId, Props entityProps, ClusterShardingSettings settings, ExtractEntityId extractEntityId, ExtractShardId extractShardId, object handOffStopMessage, IActorRef replicator, int majorityMinCap) + public static Props Props(string typeName, ShardId shardId, Func entityProps, ClusterShardingSettings settings, ExtractEntityId extractEntityId, ExtractShardId extractShardId, object handOffStopMessage, IActorRef replicator, int majorityMinCap) { switch (settings.StateStoreMode) { @@ -724,7 +724,7 @@ public static Props Props(string typeName, ShardId shardId, Props entityProps, C } } } - + class RememberEntityStarter : ActorBase { private class Tick : INoSerializationVerificationNeeded diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ShardRegion.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ShardRegion.cs index 5ed456eb95a..f3c4b3d6069 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ShardRegion.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ShardRegion.cs @@ -129,7 +129,7 @@ public sealed class StartEntityAck : IClusterShardingSerializable public readonly ShardId ShardId; /// - /// Creates a new instance of a class, used to confirm that + /// Creates a new instance of a class, used to confirm that /// request has succeed. /// /// An identifier of a newly started entity. @@ -243,7 +243,7 @@ public int Compare(Member x, Member y) /// /// /// TBD - internal static Props Props(string typeName, Props entityProps, ClusterShardingSettings settings, string coordinatorPath, ExtractEntityId extractEntityId, ExtractShardId extractShardId, object handOffStopMessage, IActorRef replicator, int majorityMinCap) + internal static Props Props(string typeName, Func entityProps, ClusterShardingSettings settings, string coordinatorPath, ExtractEntityId extractEntityId, ExtractShardId extractShardId, object handOffStopMessage, IActorRef replicator, int majorityMinCap) { return Actor.Props.Create(() => new ShardRegion(typeName, entityProps, settings, coordinatorPath, extractEntityId, extractShardId, handOffStopMessage, replicator, majorityMinCap)).WithDeploy(Deploy.Local); } @@ -271,7 +271,7 @@ internal static Props ProxyProps(string typeName, ClusterShardingSettings settin /// /// TBD /// - public readonly Props EntityProps; + public readonly Func EntityProps; /// /// TBD /// @@ -358,7 +358,7 @@ internal static Props ProxyProps(string typeName, ClusterShardingSettings settin /// TBD /// /// - public ShardRegion(string typeName, Props entityProps, ClusterShardingSettings settings, string coordinatorPath, ExtractEntityId extractEntityId, ExtractShardId extractShardId, object handOffStopMessage, IActorRef replicator, int majorityMinCap) + public ShardRegion(string typeName, Func entityProps, ClusterShardingSettings settings, string coordinatorPath, ExtractEntityId extractEntityId, ExtractShardId extractShardId, object handOffStopMessage, IActorRef replicator, int majorityMinCap) { TypeName = typeName; EntityProps = entityProps; @@ -417,7 +417,7 @@ protected object RegistrationMessage { get { - if (EntityProps != null && !EntityProps.Equals(Actor.Props.None)) + if (EntityProps != null) return new PersistentShardCoordinator.Register(Self); return new PersistentShardCoordinator.RegisterProxy(Self); } @@ -518,10 +518,21 @@ private void Register() { var coordinator = CoordinatorSelection; coordinator?.Tell(RegistrationMessage); - if (ShardBuffers.Count != 0 && _retryCount >= RetryCountThreshold) - Log.Warning("Trying to register to coordinator at [{0}], but no acknowledgement. Total [{1}] buffered messages.", - coordinator != null ? coordinator.PathString : string.Empty, TotalBufferSize); + { + if (coordinator != null) + { + var coordinatorMessage = Cluster.State.Unreachable.Contains(MembersByAge.First()) ? $"Coordinator [{MembersByAge.First()}] is unreachable." : $"Coordinator [{MembersByAge.First()}] is reachable."; + + Log.Warning("Trying to register to coordinator at [{0}], but no acknowledgement. Total [{1}] buffered messages. [{2}]", + coordinator != null ? coordinator.PathString : string.Empty, TotalBufferSize, coordinatorMessage); + } + else + { + Log.Warning("No coordinator found to register. Probably, no seed-nodes configured and manual cluster join not performed? Total [{0}] buffered messages.", + TotalBufferSize); + } + } } private void DeliverStartEntity(object message, IActorRef sender) @@ -902,7 +913,7 @@ private IActorRef GetShard(ShardId id) //TODO: change on ConcurrentDictionary.GetOrAdd? if (!Shards.TryGetValue(id, out var region)) { - if (EntityProps == null || EntityProps.Equals(Actor.Props.Empty)) + if (EntityProps == null) throw new IllegalStateException("Shard must not be allocated to a proxy only ShardRegion"); if (ShardsByRef.Values.All(shardId => shardId != id)) From 1b6ff9802770eaa3043eaf542518254540ae6b8d Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 27 Jun 2018 16:55:14 -0500 Subject: [PATCH 41/70] close #3529 - added DDataClusterShardingConfigSpec --- .../ClusterShardingConfigSpec.cs | 31 +++++++++++++ .../DDataClusterShardingConfigSpec.cs | 46 +++++++++++++++++++ .../Properties/AssemblyInfo.cs | 1 + 3 files changed, 78 insertions(+) create mode 100644 src/contrib/cluster/Akka.Cluster.Sharding.Tests/DDataClusterShardingConfigSpec.cs diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingConfigSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingConfigSpec.cs index 18152aa79cc..a7049c4db09 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingConfigSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingConfigSpec.cs @@ -7,7 +7,12 @@ using System; using Akka.Configuration; +using Akka.DistributedData; +using Akka.DistributedData.Internal; +using Akka.DistributedData.Serialization; +using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace Akka.Cluster.Sharding.Tests { @@ -63,4 +68,30 @@ public void Should_cluster_sharding_settings_have_default_config() Assert.Equal(10, singletonConfig.GetInt("min-number-of-hand-over-retries")); } } + + public class DDataClusterShardingConfigSpec : TestKit.Xunit2.TestKit + { + public DDataClusterShardingConfigSpec(ITestOutputHelper helper) : base(GetConfig(), output:helper) + { + } + + public static Config GetConfig() + { + return ConfigurationFactory.ParseString(@"akka.actor.provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster"" + akka.cluster.sharding.state-store-mode = ddata + "); + } + + [Fact] + public void Should_load_DData_serializers_when_enabled() + { + ClusterSharding.Get(Sys); + + var rmSerializer = Sys.Serialization.FindSerializerFor(WriteAck.Instance); + rmSerializer.Should().BeOfType(); + + var rDSerializer = Sys.Serialization.FindSerializerFor(ORDictionary>.Empty); + rDSerializer.Should().BeOfType(); + } + } } diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/DDataClusterShardingConfigSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/DDataClusterShardingConfigSpec.cs new file mode 100644 index 00000000000..1be6e6defa7 --- /dev/null +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/DDataClusterShardingConfigSpec.cs @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Configuration; +using Akka.DistributedData; +using Akka.DistributedData.Internal; +using Akka.DistributedData.Serialization; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Cluster.Sharding.Tests +{ + /// + /// Used to validate that https://github.com/akkadotnet/akka.net/issues/3529 works as expected + /// + public class DDataClusterShardingConfigSpec : TestKit.Xunit2.TestKit + { + public DDataClusterShardingConfigSpec(ITestOutputHelper helper) : base(GetConfig(), output:helper) + { + } + + public static Config GetConfig() + { + return ConfigurationFactory.ParseString(@"akka.actor.provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster"" + akka.cluster.sharding.state-store-mode = ddata + "); + } + + [Fact] + public void Should_load_DData_serializers_when_enabled() + { + ClusterSharding.Get(Sys); + + var rmSerializer = Sys.Serialization.FindSerializerFor(WriteAck.Instance); + rmSerializer.Should().BeOfType(); + + var rDSerializer = Sys.Serialization.FindSerializerFor(ORDictionary>.Empty); + rDSerializer.Should().BeOfType(); + } + } +} \ No newline at end of file diff --git a/src/contrib/cluster/Akka.DistributedData/Properties/AssemblyInfo.cs b/src/contrib/cluster/Akka.DistributedData/Properties/AssemblyInfo.cs index 7cabb752a5f..628cdb7f4e9 100644 --- a/src/contrib/cluster/Akka.DistributedData/Properties/AssemblyInfo.cs +++ b/src/contrib/cluster/Akka.DistributedData/Properties/AssemblyInfo.cs @@ -24,3 +24,4 @@ [assembly: InternalsVisibleTo("Akka.DistributedData.Tests.MultiNode")] [assembly: InternalsVisibleTo("Akka.Cluster.Sharding")] [assembly: InternalsVisibleTo("Akka.Cluster.Sharding.Tests.MultiNode")] +[assembly: InternalsVisibleTo("Akka.Cluster.Sharding.Tests")] From 6a1e7e9e328fd25b5de5453090f5758e187e9547 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 27 Jun 2018 16:59:09 -0500 Subject: [PATCH 42/70] removed duplicate spec --- .../ClusterShardingConfigSpec.cs | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingConfigSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingConfigSpec.cs index a7049c4db09..18152aa79cc 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingConfigSpec.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/ClusterShardingConfigSpec.cs @@ -7,12 +7,7 @@ using System; using Akka.Configuration; -using Akka.DistributedData; -using Akka.DistributedData.Internal; -using Akka.DistributedData.Serialization; -using FluentAssertions; using Xunit; -using Xunit.Abstractions; namespace Akka.Cluster.Sharding.Tests { @@ -68,30 +63,4 @@ public void Should_cluster_sharding_settings_have_default_config() Assert.Equal(10, singletonConfig.GetInt("min-number-of-hand-over-retries")); } } - - public class DDataClusterShardingConfigSpec : TestKit.Xunit2.TestKit - { - public DDataClusterShardingConfigSpec(ITestOutputHelper helper) : base(GetConfig(), output:helper) - { - } - - public static Config GetConfig() - { - return ConfigurationFactory.ParseString(@"akka.actor.provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster"" - akka.cluster.sharding.state-store-mode = ddata - "); - } - - [Fact] - public void Should_load_DData_serializers_when_enabled() - { - ClusterSharding.Get(Sys); - - var rmSerializer = Sys.Serialization.FindSerializerFor(WriteAck.Instance); - rmSerializer.Should().BeOfType(); - - var rDSerializer = Sys.Serialization.FindSerializerFor(ORDictionary>.Empty); - rDSerializer.Should().BeOfType(); - } - } } From 98a1a12b766aedc33749aace4ad255eeb5ca3fa4 Mon Sep 17 00:00:00 2001 From: Sam13 Date: Fri, 29 Jun 2018 11:48:33 +0200 Subject: [PATCH 43/70] File publisher allows opening the same file for reading multiple times (#3531) --- .../Akka.Streams.Tests/IO/FileSourceSpec.cs | 25 +++++++++++++++++++ .../Implementation/IO/FilePublisher.cs | 3 ++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/core/Akka.Streams.Tests/IO/FileSourceSpec.cs b/src/core/Akka.Streams.Tests/IO/FileSourceSpec.cs index ca4ea35542d..f3baf225b49 100644 --- a/src/core/Akka.Streams.Tests/IO/FileSourceSpec.cs +++ b/src/core/Akka.Streams.Tests/IO/FileSourceSpec.cs @@ -201,6 +201,31 @@ public void FileSource_should_complete_only_when_all_contents_of_a_file_have_bee }, _materializer); } + [Fact] + public void FileSource_should_open_file_in_shared_mode_for_reading_multiple_times() + { + this.AssertAllStagesStopped(() => + { + var testFile = TestFile(); + var p1 = FileIO.FromFile(testFile).RunWith(Sink.AsPublisher(false), _materializer); + var p2 = FileIO.FromFile(testFile).RunWith(Sink.AsPublisher(false), _materializer); + + var c1 = this.CreateManualSubscriberProbe(); + var c2 = this.CreateManualSubscriberProbe(); + p1.Subscribe(c1); + p2.Subscribe(c2); + var s1 = c1.ExpectSubscription(); + var s2 = c2.ExpectSubscription(); + + s1.Request(5000); + s2.Request(5000); + + c1.ExpectNext(); + c2.ExpectNext(); + + }, _materializer); + } + [Fact] public void FileSource_should_onError_with_failure_and_return_a_failed_IOResult_when_trying_to_read_from_file_which_does_not_exist() { diff --git a/src/core/Akka.Streams/Implementation/IO/FilePublisher.cs b/src/core/Akka.Streams/Implementation/IO/FilePublisher.cs index 7d740e50112..0c9e99c3a46 100644 --- a/src/core/Akka.Streams/Implementation/IO/FilePublisher.cs +++ b/src/core/Akka.Streams/Implementation/IO/FilePublisher.cs @@ -107,7 +107,8 @@ protected override void PreStart() { try { - _chan = _f.Open(FileMode.Open, FileAccess.Read); + // Allow opening the same file for reading multiple times + _chan = _f.Open(FileMode.Open, FileAccess.Read, FileShare.Read); if (_startPosition > 0) _chan.Position = _startPosition; } From b028ef36b347eff2358e1a2ce9a27c58be8c0c90 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Mon, 25 Jun 2018 09:45:21 -0500 Subject: [PATCH 44/70] code cleanup and perf fixes to InMemorySnapshotStore --- .../Snapshot/MemorySnapshotStore.cs | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs b/src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs index bb3a14af26c..ff798ca36a4 100644 --- a/src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs +++ b/src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Akka.Util.Internal; namespace Akka.Persistence.Snapshot { @@ -23,55 +24,52 @@ public class MemorySnapshotStore : SnapshotStore protected override Task DeleteAsync(SnapshotMetadata metadata) { - Func pred = x => x.PersistenceId == metadata.PersistenceId && - (metadata.SequenceNr <= 0 || metadata.SequenceNr == long.MaxValue || x.SequenceNr == metadata.SequenceNr) && - (metadata.Timestamp == DateTime.MinValue || metadata.Timestamp == DateTime.MaxValue || x.Timestamp == metadata.Timestamp.Ticks); + bool Pred(SnapshotEntry x) => x.PersistenceId == metadata.PersistenceId && (metadata.SequenceNr <= 0 || metadata.SequenceNr == long.MaxValue || x.SequenceNr == metadata.SequenceNr) + && (metadata.Timestamp == DateTime.MinValue || metadata.Timestamp == DateTime.MaxValue || x.Timestamp == metadata.Timestamp.Ticks); - return Task.Run(() => - { - var snapshot = Snapshots.FirstOrDefault(pred); - Snapshots.Remove(snapshot); - }); + + var snapshot = Snapshots.FirstOrDefault(Pred); + Snapshots.Remove(snapshot); + + return TaskEx.Completed; } protected override Task DeleteAsync(string persistenceId, SnapshotSelectionCriteria criteria) { var filter = CreateRangeFilter(persistenceId, criteria); - return Task.Run(() => { Snapshots.RemoveAll(x => filter(x)); }); + Snapshots.RemoveAll(x => filter(x)); + return TaskEx.Completed; } protected override Task LoadAsync(string persistenceId, SnapshotSelectionCriteria criteria) { var filter = CreateRangeFilter(persistenceId, criteria); - return Task.Run(() => - { - var snapshot = Snapshots.Where(filter).OrderByDescending(x => x.SequenceNr).Take(1).Select(x => ToSelectedSnapshot(x)).FirstOrDefault(); - return snapshot; - }); + var snapshot = Snapshots.Where(filter).OrderByDescending(x => x.SequenceNr).Take(1).Select(x => ToSelectedSnapshot(x)).FirstOrDefault(); + return Task.FromResult(snapshot); } - protected override async Task SaveAsync(SnapshotMetadata metadata, object snapshot) + protected override Task SaveAsync(SnapshotMetadata metadata, object snapshot) { - await Task.Run(() => + + var snapshotEntry = ToSnapshotEntry(metadata, snapshot); + var existingSnapshot = Snapshots.FirstOrDefault(CreateSnapshotIdFilter(snapshotEntry.Id)); + + if (existingSnapshot != null) { - var snapshotEntry = ToSnapshotEntry(metadata, snapshot); - var existingSnapshot = Snapshots.FirstOrDefault(CreateSnapshotIdFilter(snapshotEntry.Id)); - - if (existingSnapshot != null) - { - existingSnapshot.Snapshot = snapshotEntry.Snapshot; - existingSnapshot.Timestamp = snapshotEntry.Timestamp; - } - else - { - Snapshots.Add(snapshotEntry); - } - }); + existingSnapshot.Snapshot = snapshotEntry.Snapshot; + existingSnapshot.Timestamp = snapshotEntry.Timestamp; + } + else + { + Snapshots.Add(snapshotEntry); + } + + return TaskEx.Completed; } - private Func CreateSnapshotIdFilter(string snapshotId) + private static Func CreateSnapshotIdFilter(string snapshotId) { return x => x.Id == snapshotId; } @@ -101,6 +99,11 @@ private static SelectedSnapshot ToSelectedSnapshot(SnapshotEntry entry) } } + /// + /// INTERNAL API. + /// + /// Represents a snapshot stored inside the in-memory + /// public class SnapshotEntry { public string Id { get; set; } From f7382fdae3ab195330345be2a5d818db8edcdceb Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Mon, 25 Jun 2018 10:26:41 -0500 Subject: [PATCH 45/70] more code cleanup to MemorySnapshotStore --- .../Akka.Persistence/Snapshot/MemorySnapshotStore.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs b/src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs index ff798ca36a4..f738ea113f3 100644 --- a/src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs +++ b/src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs @@ -13,20 +13,25 @@ namespace Akka.Persistence.Snapshot { + /// + /// INTERNAL API. + /// + /// In-memory SnapshotStore implementation. + /// public class MemorySnapshotStore : SnapshotStore { - private readonly List _snapshotCollection = new List(); + private readonly object _ssLock = new object(); /// /// This is available to expose/override the snapshots in derived snapshot stores /// - protected virtual List Snapshots { get { return _snapshotCollection; } } + protected virtual List Snapshots { get; } = new List(); protected override Task DeleteAsync(SnapshotMetadata metadata) { bool Pred(SnapshotEntry x) => x.PersistenceId == metadata.PersistenceId && (metadata.SequenceNr <= 0 || metadata.SequenceNr == long.MaxValue || x.SequenceNr == metadata.SequenceNr) && (metadata.Timestamp == DateTime.MinValue || metadata.Timestamp == DateTime.MaxValue || x.Timestamp == metadata.Timestamp.Ticks); - + var snapshot = Snapshots.FirstOrDefault(Pred); Snapshots.Remove(snapshot); @@ -46,6 +51,7 @@ protected override Task LoadAsync(string persistenceId, Snapsh { var filter = CreateRangeFilter(persistenceId, criteria); + var snapshot = Snapshots.Where(filter).OrderByDescending(x => x.SequenceNr).Take(1).Select(x => ToSelectedSnapshot(x)).FirstOrDefault(); return Task.FromResult(snapshot); } From 2ff1e07cd98f21ab2c9d2040b0768812a6b87704 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Mon, 25 Jun 2018 10:31:10 -0500 Subject: [PATCH 46/70] added spec to verify existence of #3526 and subsequent fix --- .../MemorySnapshotStoreSpec.cs | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/src/core/Akka.Persistence.TCK.Tests/MemorySnapshotStoreSpec.cs b/src/core/Akka.Persistence.TCK.Tests/MemorySnapshotStoreSpec.cs index fcab9ae6698..efcb78c77f4 100644 --- a/src/core/Akka.Persistence.TCK.Tests/MemorySnapshotStoreSpec.cs +++ b/src/core/Akka.Persistence.TCK.Tests/MemorySnapshotStoreSpec.cs @@ -5,8 +5,17 @@ // //----------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Akka.Actor; using Akka.Configuration; +using Akka.Persistence.Snapshot; using Akka.Persistence.TCK.Snapshot; +using Akka.Util; +using FluentAssertions; +using Xunit; using Xunit.Abstractions; namespace Akka.Persistence.TCK.Tests @@ -23,5 +32,129 @@ public MemorySnapshotStoreSpec(ITestOutputHelper output) Initialize(); } + [Fact] + public void MemorySnapshotStore_is_threadsafe() + { + EventFilter.Error().Expect(0, () => + { + // get a few persistent actors going in parallel + var sa1 = Sys.ActorOf(Props.Create(() => new SnapshotActor("sa1", TestActor))); + var sa2 = Sys.ActorOf(Props.Create(() => new SnapshotActor("sa2", TestActor))); + var sa3 = Sys.ActorOf(Props.Create(() => new SnapshotActor("sa3", TestActor))); + + Watch(sa1); + Watch(sa2); + Watch(sa3); + + var writeCount = 3000; + + var sas = new List + { + sa1, + sa2, + sa3 + }; + + // hammer with write requests + Parallel.ForEach(Enumerable.Range(0, writeCount), i => + { + sas[ThreadLocalRandom.Current.Next(0, 3)].Tell(i); + }); + + // spawn more persistence actors while writes are still going(?) + var sa4 = Sys.ActorOf(Props.Create(() => new SnapshotActor("sa4", TestActor))); + var sa5 = Sys.ActorOf(Props.Create(() => new SnapshotActor("sa5", TestActor))); + var sa6 = Sys.ActorOf(Props.Create(() => new SnapshotActor("sa6", TestActor))); + + ReceiveN(writeCount).All(x => x is SaveSnapshotSuccess).Should().BeTrue("Expected all snapshot store saves to be successful, but some were not"); + + // kill the existing snapshot stores, then re-create them to force recovery while the new snapshot actors + // are still being written to. + + sa1.Tell(PoisonPill.Instance); + ExpectTerminated(sa1); + + sa2.Tell(PoisonPill.Instance); + ExpectTerminated(sa2); + + sa3.Tell(PoisonPill.Instance); + ExpectTerminated(sa3); + + var sas2 = new List + { + sa4, + sa5, + sa6 + }; + + // hammer with write requests + Parallel.ForEach(Enumerable.Range(0, writeCount), i => + { + sas2[ThreadLocalRandom.Current.Next(0, 3)].Tell(i); + }); + + // recreate the previous entities + var sa12 = Sys.ActorOf(Props.Create(() => new SnapshotActor("sa1", TestActor))); + var sa22 = Sys.ActorOf(Props.Create(() => new SnapshotActor("sa2", TestActor))); + var sa32 = Sys.ActorOf(Props.Create(() => new SnapshotActor("sa3", TestActor))); + + var sas12 = new List + { + sa12, + sa22, + sa32 + }; + + // hammer other entities + Parallel.ForEach(Enumerable.Range(0, writeCount), i => + { + sas12[ThreadLocalRandom.Current.Next(0, 3)].Tell(i); + }); + + ReceiveN(writeCount*2).All(x => x is SaveSnapshotSuccess).Should().BeTrue("Expected all snapshot store saves to be successful, but some were not"); + }); + + } + + public class SnapshotActor : ReceivePersistentActor + { + private int _count = 0; + private readonly IActorRef _reporter; + + public SnapshotActor(string persistenceId, IActorRef reporter) + { + PersistenceId = persistenceId; + _reporter = reporter; + + Recover(offer => + { + if (offer.Snapshot is int i) + { + _count = i; + } + }); + + Command(i => + { + _count += i; + + Persist(i, i1 => + { + SaveSnapshot(i); + }); + }); + + Command(success => reporter.Tell(success)); + + Command(failure => reporter.Tell(failure)); + + Command(str => str.Equals("get"), s => + { + Sender.Tell(_count); + }); + } + + public override string PersistenceId { get; } + } } } From 1b8ec6c2c6794d8590235c8ec7540d2603dc86ff Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Mon, 25 Jun 2018 10:35:14 -0500 Subject: [PATCH 47/70] removed unnecessary lock --- src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs b/src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs index f738ea113f3..2ea22db2176 100644 --- a/src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs +++ b/src/core/Akka.Persistence/Snapshot/MemorySnapshotStore.cs @@ -20,8 +20,6 @@ namespace Akka.Persistence.Snapshot /// public class MemorySnapshotStore : SnapshotStore { - private readonly object _ssLock = new object(); - /// /// This is available to expose/override the snapshots in derived snapshot stores /// From c1479332366a58f46b3128df3d641d8332aa6fa1 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Mon, 25 Jun 2018 11:45:23 -0500 Subject: [PATCH 48/70] Update CoreAPISpec.ApprovePersistence.approved.txt --- .../CoreAPISpec.ApprovePersistence.approved.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt index 9d6c8be8c32..4656afdf815 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt @@ -1137,7 +1137,7 @@ namespace Akka.Persistence.Snapshot protected override System.Threading.Tasks.Task DeleteAsync(Akka.Persistence.SnapshotMetadata metadata) { } protected override System.Threading.Tasks.Task DeleteAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } protected override System.Threading.Tasks.Task LoadAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } - protected override async System.Threading.Tasks.Task SaveAsync(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } + protected override System.Threading.Tasks.Task SaveAsync(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } } public sealed class NoSnapshotStore : Akka.Persistence.Snapshot.SnapshotStore { @@ -1173,4 +1173,4 @@ namespace Akka.Persistence.Snapshot protected virtual bool ReceivePluginInternal(object message) { } protected abstract System.Threading.Tasks.Task SaveAsync(Akka.Persistence.SnapshotMetadata metadata, object snapshot); } -} \ No newline at end of file +} From 4cc73721176060ae6977856af385dc5b5dd8fd33 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 27 Jun 2018 21:02:11 -0500 Subject: [PATCH 49/70] resetting file --- ...oreAPISpec.ApprovePersistence.approved.txt | 1177 +---------------- 1 file changed, 1 insertion(+), 1176 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt index 4656afdf815..5f282702bb0 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt @@ -1,1176 +1 @@ -[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Persistence.TCK")] -[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Persistence.Tests")] -[assembly: System.Runtime.InteropServices.ComVisibleAttribute(false)] -[assembly: System.Runtime.InteropServices.GuidAttribute("e3bcba88-003c-4cda-8a60-f0c2553fe3c8")] -[assembly: System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.5", FrameworkDisplayName=".NET Framework 4.5")] -namespace Akka.Persistence -{ - public sealed class AsyncHandlerInvocation : Akka.Persistence.IPendingHandlerInvocation - { - public AsyncHandlerInvocation(object evt, System.Action handler) { } - public object Event { get; } - public System.Action Handler { get; } - } - public abstract class AtLeastOnceDeliveryActor : Akka.Persistence.PersistentActor - { - protected AtLeastOnceDeliveryActor() { } - protected AtLeastOnceDeliveryActor(Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings settings) { } - public int MaxUnconfirmedMessages { get; } - public virtual System.TimeSpan RedeliverInterval { get; } - public virtual int RedeliveryBurstLimit { get; } - public int UnconfirmedCount { get; } - public int WarnAfterNumberOfUnconfirmedAttempts { get; } - public override void AroundPostStop() { } - public override void AroundPreRestart(System.Exception cause, object message) { } - protected internal override bool AroundReceive(Akka.Actor.Receive receive, object message) { } - public bool ConfirmDelivery(long deliveryId) { } - public void Deliver(Akka.Actor.ActorPath destination, System.Func deliveryMessageMapper) { } - public void Deliver(Akka.Actor.ActorSelection destination, System.Func deliveryMessageMapper) { } - public Akka.Persistence.AtLeastOnceDeliverySnapshot GetDeliverySnapshot() { } - protected override void OnReplaySuccess() { } - public void SetDeliverySnapshot(Akka.Persistence.AtLeastOnceDeliverySnapshot snapshot) { } - } - public abstract class AtLeastOnceDeliveryReceiveActor : Akka.Persistence.ReceivePersistentActor - { - protected AtLeastOnceDeliveryReceiveActor() { } - protected AtLeastOnceDeliveryReceiveActor(Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings settings) { } - public int MaxUnconfirmedMessages { get; } - public virtual System.TimeSpan RedeliverInterval { get; } - public int RedeliveryBurstLimit { get; } - public int UnconfirmedCount { get; } - public int WarnAfterNumberOfUnconfirmedAttempts { get; } - public override void AroundPostStop() { } - public override void AroundPreRestart(System.Exception cause, object message) { } - protected internal override bool AroundReceive(Akka.Actor.Receive receive, object message) { } - public bool ConfirmDelivery(long deliveryId) { } - public void Deliver(Akka.Actor.ActorPath destination, System.Func deliveryMessageMapper) { } - public void Deliver(Akka.Actor.ActorSelection destination, System.Func deliveryMessageMapper) { } - public Akka.Persistence.AtLeastOnceDeliverySnapshot GetDeliverySnapshot() { } - protected override void OnReplaySuccess() { } - public void SetDeliverySnapshot(Akka.Persistence.AtLeastOnceDeliverySnapshot snapshot) { } - } - public class AtLeastOnceDeliverySemantic - { - public AtLeastOnceDeliverySemantic(Akka.Actor.IActorContext context, Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings settings) { } - public virtual int MaxUnconfirmedMessages { get; } - public virtual System.TimeSpan RedeliverInterval { get; } - public virtual int RedeliveryBurstLimit { get; } - public int UnconfirmedCount { get; } - public virtual int WarnAfterNumberOfUnconfirmedAttempts { get; } - public bool AroundReceive(Akka.Actor.Receive receive, object message) { } - public void Cancel() { } - public bool ConfirmDelivery(long deliveryId) { } - public void Deliver(Akka.Actor.ActorPath destination, System.Func deliveryMessageMapper, bool isRecovering) { } - public Akka.Persistence.AtLeastOnceDeliverySnapshot GetDeliverySnapshot() { } - public void OnReplaySuccess() { } - public void SetDeliverySnapshot(Akka.Persistence.AtLeastOnceDeliverySnapshot snapshot) { } - public sealed class Delivery : System.IEquatable - { - public Delivery(Akka.Actor.ActorPath destination, object message, System.DateTime timestamp, int attempt) { } - public int Attempt { get; } - public Akka.Actor.ActorPath Destination { get; } - public object Message { get; } - public System.DateTime Timestamp { get; } - public bool Equals(Akka.Persistence.AtLeastOnceDeliverySemantic.Delivery other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public Akka.Persistence.AtLeastOnceDeliverySemantic.Delivery IncrementedCopy() { } - public override string ToString() { } - } - public sealed class RedeliveryTick : Akka.Actor.INotInfluenceReceiveTimeout - { - public static Akka.Persistence.AtLeastOnceDeliverySemantic.RedeliveryTick Instance { get; } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - } - } - public sealed class AtLeastOnceDeliverySnapshot : Akka.Persistence.Serialization.IMessage, System.IEquatable - { - public AtLeastOnceDeliverySnapshot(long currentDeliveryId, Akka.Persistence.UnconfirmedDelivery[] unconfirmedDeliveries) { } - public long CurrentDeliveryId { get; } - public Akka.Persistence.UnconfirmedDelivery[] UnconfirmedDeliveries { get; } - public bool Equals(Akka.Persistence.AtLeastOnceDeliverySnapshot other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class AtomicWrite : Akka.Persistence.IPersistentEnvelope, Akka.Persistence.Serialization.IMessage - { - public AtomicWrite(Akka.Persistence.IPersistentRepresentation @event) { } - public AtomicWrite(System.Collections.Immutable.IImmutableList payload) { } - public long HighestSequenceNr { get; } - public long LowestSequenceNr { get; } - public object Payload { get; } - public string PersistenceId { get; } - public Akka.Actor.IActorRef Sender { get; } - public int Size { get; } - public bool Equals(Akka.Persistence.AtomicWrite other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class DeleteMessagesFailure : System.IEquatable - { - public DeleteMessagesFailure(System.Exception cause, long toSequenceNr) { } - public System.Exception Cause { get; } - public long ToSequenceNr { get; } - public bool Equals(Akka.Persistence.DeleteMessagesFailure other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class DeleteMessagesSuccess : System.IEquatable - { - public DeleteMessagesSuccess(long toSequenceNr) { } - public long ToSequenceNr { get; } - public bool Equals(Akka.Persistence.DeleteMessagesSuccess other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class DeleteMessagesTo : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalRequest, Akka.Persistence.IPersistenceMessage, System.IEquatable - { - public DeleteMessagesTo(string persistenceId, long toSequenceNr, Akka.Actor.IActorRef persistentActor) { } - public string PersistenceId { get; } - public Akka.Actor.IActorRef PersistentActor { get; } - public long ToSequenceNr { get; } - public bool Equals(Akka.Persistence.DeleteMessagesTo other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class DeleteSnapshot : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotRequest, System.IEquatable - { - public DeleteSnapshot(Akka.Persistence.SnapshotMetadata metadata) { } - public Akka.Persistence.SnapshotMetadata Metadata { get; } - public bool Equals(Akka.Persistence.DeleteSnapshot other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class DeleteSnapshotFailure : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable - { - public DeleteSnapshotFailure(Akka.Persistence.SnapshotMetadata metadata, System.Exception cause) { } - public System.Exception Cause { get; } - public Akka.Persistence.SnapshotMetadata Metadata { get; } - public bool Equals(Akka.Persistence.DeleteSnapshotFailure other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class DeleteSnapshots : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotRequest, System.IEquatable - { - public DeleteSnapshots(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } - public Akka.Persistence.SnapshotSelectionCriteria Criteria { get; } - public string PersistenceId { get; } - public bool Equals(Akka.Persistence.DeleteSnapshots other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class DeleteSnapshotsFailure : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable - { - public DeleteSnapshotsFailure(Akka.Persistence.SnapshotSelectionCriteria criteria, System.Exception cause) { } - public System.Exception Cause { get; } - public Akka.Persistence.SnapshotSelectionCriteria Criteria { get; } - public bool Equals(Akka.Persistence.DeleteSnapshotsFailure other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class DeleteSnapshotsSuccess : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable - { - public DeleteSnapshotsSuccess(Akka.Persistence.SnapshotSelectionCriteria criteria) { } - public Akka.Persistence.SnapshotSelectionCriteria Criteria { get; } - public bool Equals(Akka.Persistence.DeleteSnapshotsSuccess other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class DeleteSnapshotSuccess : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable - { - public DeleteSnapshotSuccess(Akka.Persistence.SnapshotMetadata metadata) { } - public Akka.Persistence.SnapshotMetadata Metadata { get; } - public bool Equals(Akka.Persistence.DeleteSnapshotSuccess other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class DiscardConfigurator : Akka.Persistence.IStashOverflowStrategyConfigurator - { - public DiscardConfigurator() { } - public Akka.Persistence.IStashOverflowStrategy Create(Akka.Configuration.Config config) { } - } - public class DiscardToDeadLetterStrategy : Akka.Persistence.IStashOverflowStrategy - { - public static Akka.Persistence.DiscardToDeadLetterStrategy Instance { get; } - } - public abstract class Eventsourced : Akka.Actor.ActorBase, Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Dispatch.IRequiresMessageQueue, Akka.Persistence.IPersistenceRecovery, Akka.Persistence.IPersistenceStash, Akka.Persistence.IPersistentIdentity - { - public static readonly System.Func UnstashFilterPredicate; - protected Eventsourced() { } - protected Akka.Persistence.PersistenceExtension Extension { get; } - public virtual Akka.Persistence.IStashOverflowStrategy InternalStashOverflowStrategy { get; } - public bool IsRecovering { get; } - public bool IsRecoveryFinished { get; } - public Akka.Actor.IActorRef Journal { get; } - public string JournalPluginId { get; set; } - public long LastSequenceNr { get; } - protected virtual Akka.Event.ILoggingAdapter Log { get; } - public abstract string PersistenceId { get; } - public virtual Akka.Persistence.Recovery Recovery { get; } - public string SnapshotPluginId { get; set; } - public long SnapshotSequenceNr { get; } - public Akka.Actor.IActorRef SnapshotStore { get; } - public string SnapshotterId { get; } - public Akka.Actor.IStash Stash { get; set; } - public override void AroundPostRestart(System.Exception reason, object message) { } - public override void AroundPostStop() { } - public override void AroundPreRestart(System.Exception cause, object message) { } - public override void AroundPreStart() { } - protected internal override bool AroundReceive(Akka.Actor.Receive receive, object message) { } - public void DeferAsync(TEvent evt, System.Action handler) { } - public void DeleteMessages(long toSequenceNr) { } - public void DeleteSnapshot(long sequenceNr) { } - public void DeleteSnapshots(Akka.Persistence.SnapshotSelectionCriteria criteria) { } - public void LoadSnapshot(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria, long toSequenceNr) { } - protected virtual void OnPersistFailure(System.Exception cause, object @event, long sequenceNr) { } - protected virtual void OnPersistRejected(System.Exception cause, object @event, long sequenceNr) { } - protected virtual void OnRecoveryFailure(System.Exception reason, object message = null) { } - protected virtual void OnReplaySuccess() { } - public void Persist(TEvent @event, System.Action handler) { } - public void PersistAll(System.Collections.Generic.IEnumerable events, System.Action handler) { } - public void PersistAllAsync(System.Collections.Generic.IEnumerable events, System.Action handler) { } - public void PersistAsync(TEvent @event, System.Action handler) { } - protected abstract bool ReceiveCommand(object message); - protected abstract bool ReceiveRecover(object message); - protected void RunTask(System.Func action) { } - public void SaveSnapshot(object snapshot) { } - protected override void Unhandled(object message) { } - } - public interface IJournalMessage : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage { } - public interface IJournalPlugin - { - Akka.Configuration.Config DefaultConfig { get; } - string JournalPath { get; } - } - public interface IJournalRequest : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IPersistenceMessage { } - public interface IJournalResponse : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IPersistenceMessage { } - public interface IPendingHandlerInvocation - { - object Event { get; } - System.Action Handler { get; } - } - public interface IPersistenceMessage : Akka.Actor.INoSerializationVerificationNeeded { } - public interface IPersistenceRecovery - { - Akka.Persistence.Recovery Recovery { get; } - } - public interface IPersistenceStash : Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Dispatch.IRequiresMessageQueue - { - Akka.Persistence.IStashOverflowStrategy InternalStashOverflowStrategy { get; } - } - public interface IPersistentEnvelope - { - object Payload { get; } - Akka.Actor.IActorRef Sender { get; } - int Size { get; } - } - public interface IPersistentIdentity - { - string JournalPluginId { get; } - string PersistenceId { get; } - string SnapshotPluginId { get; } - } - public interface IPersistentRepresentation : Akka.Persistence.Serialization.IMessage - { - bool IsDeleted { get; } - string Manifest { get; } - object Payload { get; } - string PersistenceId { get; } - Akka.Actor.IActorRef Sender { get; } - long SequenceNr { get; } - string WriterGuid { get; } - Akka.Persistence.IPersistentRepresentation Update(long sequenceNr, string persistenceId, bool isDeleted, Akka.Actor.IActorRef sender, string writerGuid); - Akka.Persistence.IPersistentRepresentation WithManifest(string manifest); - Akka.Persistence.IPersistentRepresentation WithPayload(object payload); - } - public interface ISnapshotMessage : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage { } - public interface ISnapshotRequest : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage { } - public interface ISnapshotResponse : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage { } - public interface ISnapshotter - { - long SnapshotSequenceNr { get; } - string SnapshotterId { get; } - void DeleteSnapshot(long sequenceNr); - void DeleteSnapshots(Akka.Persistence.SnapshotSelectionCriteria criteria); - void LoadSnapshot(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria, long toSequenceNr); - void SaveSnapshot(object snapshot); - } - public interface IStashOverflowStrategy { } - public interface IStashOverflowStrategyConfigurator - { - Akka.Persistence.IStashOverflowStrategy Create(Akka.Configuration.Config config); - } - public sealed class LoadSnapshot : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotRequest, System.IEquatable - { - public LoadSnapshot(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria, long toSequenceNr) { } - public Akka.Persistence.SnapshotSelectionCriteria Criteria { get; } - public string PersistenceId { get; } - public long ToSequenceNr { get; } - public bool Equals(Akka.Persistence.LoadSnapshot other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class LoadSnapshotFailed : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse - { - public LoadSnapshotFailed(System.Exception cause) { } - public System.Exception Cause { get; } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class LoadSnapshotResult : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable - { - public LoadSnapshotResult(Akka.Persistence.SelectedSnapshot snapshot, long toSequenceNr) { } - public Akka.Persistence.SelectedSnapshot Snapshot { get; } - public long ToSequenceNr { get; } - public bool Equals(Akka.Persistence.LoadSnapshotResult other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class LoopMessageSuccess : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable - { - public LoopMessageSuccess(object message, int actorInstanceId) { } - public int ActorInstanceId { get; } - public object Message { get; } - public bool Equals(Akka.Persistence.LoopMessageSuccess other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public class MaxUnconfirmedMessagesExceededException : System.Exception - { - public MaxUnconfirmedMessagesExceededException() { } - public MaxUnconfirmedMessagesExceededException(string message) { } - public MaxUnconfirmedMessagesExceededException(string message, System.Exception innerException) { } - protected MaxUnconfirmedMessagesExceededException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } - } - public class Persistence : Akka.Actor.ExtensionIdProvider - { - public Persistence() { } - public static Akka.Persistence.Persistence Instance { get; } - public override Akka.Persistence.PersistenceExtension CreateExtension(Akka.Actor.ExtendedActorSystem system) { } - public static Akka.Configuration.Config DefaultConfig() { } - } - public class PersistenceExtension : Akka.Actor.IExtension - { - public PersistenceExtension(Akka.Actor.ExtendedActorSystem system) { } - public Akka.Persistence.IStashOverflowStrategy DefaultInternalStashOverflowStrategy { get; } - public Akka.Persistence.PersistenceSettings Settings { get; } - public Akka.Persistence.Journal.EventAdapters AdaptersFor(string journalPluginId) { } - public Akka.Actor.IActorRef JournalFor(string journalPluginId) { } - public string PersistenceId(Akka.Actor.IActorRef actor) { } - public Akka.Actor.IActorRef SnapshotStoreFor(string snapshotPluginId) { } - } - public sealed class PersistenceSettings : Akka.Actor.Settings - { - public PersistenceSettings(Akka.Actor.ActorSystem system, Akka.Configuration.Config config) { } - public Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings AtLeastOnceDelivery { get; set; } - public Akka.Persistence.PersistenceSettings.InternalSettings Internal { get; } - public Akka.Persistence.PersistenceSettings.ViewSettings View { get; } - public sealed class AtLeastOnceDeliverySettings - { - public AtLeastOnceDeliverySettings(System.TimeSpan redeliverInterval, int redeliveryBurstLimit, int warnAfterNumberOfUnconfirmedAttempts, int maxUnconfirmedMessages) { } - public AtLeastOnceDeliverySettings(Akka.Configuration.Config config) { } - public int MaxUnconfirmedMessages { get; } - public System.TimeSpan RedeliverInterval { get; } - public int RedeliveryBurstLimit { get; } - public int WarnAfterNumberOfUnconfirmedAttempts { get; } - public Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings WithMaxUnconfirmedMessages(int maxUnconfirmedMessages) { } - public Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings WithRedeliverInterval(System.TimeSpan redeliverInterval) { } - public Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings WithRedeliveryBurstLimit(int redeliveryBurstLimit) { } - public Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings WithUnconfirmedAttemptsToWarn(int unconfirmedAttemptsToWarn) { } - } - public sealed class InternalSettings - { - public InternalSettings(Akka.Configuration.Config config) { } - public bool PublishConfirmations { get; } - public bool PublishPluginCommands { get; } - } - public sealed class ViewSettings - { - public ViewSettings(Akka.Configuration.Config config) { } - public bool AutoUpdate { get; } - public System.TimeSpan AutoUpdateInterval { get; } - public long AutoUpdateReplayMax { get; } - } - } - [Akka.Annotations.InternalApiAttribute()] - public class Persistent : Akka.Persistence.IPersistentRepresentation, Akka.Persistence.Serialization.IMessage, System.IEquatable - { - public Persistent(object payload, long sequenceNr = 0, string persistenceId = null, string manifest = null, bool isDeleted = False, Akka.Actor.IActorRef sender = null, string writerGuid = null) { } - public bool IsDeleted { get; } - public string Manifest { get; } - public object Payload { get; } - public string PersistenceId { get; } - public Akka.Actor.IActorRef Sender { get; } - public long SequenceNr { get; } - public static string Undefined { get; } - public string WriterGuid { get; } - public bool Equals(Akka.Persistence.IPersistentRepresentation other) { } - public override bool Equals(object obj) { } - public bool Equals(Akka.Persistence.Persistent other) { } - public override int GetHashCode() { } - public override string ToString() { } - public Akka.Persistence.IPersistentRepresentation Update(long sequenceNr, string persistenceId, bool isDeleted, Akka.Actor.IActorRef sender, string writerGuid) { } - public Akka.Persistence.IPersistentRepresentation WithManifest(string manifest) { } - public Akka.Persistence.IPersistentRepresentation WithPayload(object payload) { } - } - public abstract class PersistentActor : Akka.Persistence.Eventsourced - { - protected PersistentActor() { } - protected override bool Receive(object message) { } - } - [System.ObsoleteAttribute("PersistentView was deprecated and will be removed in the next major version [1.3." + - "0]")] - public abstract class PersistentView : Akka.Actor.ActorBase, Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Dispatch.IRequiresMessageQueue, Akka.Persistence.IPersistenceRecovery, Akka.Persistence.IPersistentIdentity, Akka.Persistence.ISnapshotter - { - protected readonly Akka.Persistence.PersistenceExtension Extension; - protected PersistentView() { } - public virtual System.TimeSpan AutoUpdateInterval { get; } - public virtual long AutoUpdateReplayMax { get; } - public virtual bool IsAutoUpdate { get; } - public bool IsPersistent { get; } - public bool IsRecovering { get; } - public bool IsRecoveryFinished { get; } - public Akka.Actor.IActorRef Journal { get; } - public string JournalPluginId { get; set; } - public long LastSequenceNr { get; } - public abstract string PersistenceId { get; } - public virtual Akka.Persistence.Recovery Recovery { get; } - public string SnapshotPluginId { get; set; } - public long SnapshotSequenceNr { get; } - public Akka.Actor.IActorRef SnapshotStore { get; } - public string SnapshotterId { get; } - public Akka.Actor.IStash Stash { get; set; } - public abstract string ViewId { get; } - public override void AroundPreStart() { } - protected internal override bool AroundReceive(Akka.Actor.Receive receive, object message) { } - public void DeleteSnapshot(long sequenceNr) { } - public void DeleteSnapshots(Akka.Persistence.SnapshotSelectionCriteria criteria) { } - public void LoadSnapshot(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria, long toSequenceNr) { } - protected virtual void OnReplayError(System.Exception cause) { } - protected override void PostStop() { } - protected override void PreRestart(System.Exception reason, object message) { } - protected override void PreStart() { } - public void SaveSnapshot(object snapshot) { } - protected override void Unhandled(object message) { } - } - public abstract class ReceivePersistentActor : Akka.Persistence.UntypedPersistentActor, Akka.Actor.Internal.IInitializableActor - { - protected ReceivePersistentActor() { } - protected void Become(System.Action configure) { } - protected void BecomeStacked(System.Action configure) { } - protected void Command(System.Action handler, System.Predicate shouldHandle = null) { } - protected void Command(System.Predicate shouldHandle, System.Action handler) { } - protected void Command(System.Type messageType, System.Action handler, System.Predicate shouldHandle = null) { } - protected void Command(System.Type messageType, System.Predicate shouldHandle, System.Action handler) { } - protected void Command(System.Func handler) { } - protected void Command(System.Type messageType, System.Func handler) { } - protected void Command(System.Action handler) { } - protected void CommandAny(System.Action handler) { } - protected void CommandAnyAsync(System.Func handler) { } - protected void CommandAsync(System.Func handler, System.Predicate shouldHandle = null) { } - protected void CommandAsync(System.Predicate shouldHandle, System.Func handler) { } - protected void CommandAsync(System.Type messageType, System.Func handler, System.Predicate shouldHandle = null) { } - protected void CommandAsync(System.Type messageType, System.Predicate shouldHandle, System.Func handler) { } - protected virtual void OnCommand(object message) { } - protected virtual void OnRecover(object message) { } - protected void Recover(System.Action handler, System.Predicate shouldHandle = null) { } - protected void Recover(System.Predicate shouldHandle, System.Action handler) { } - protected void Recover(System.Type messageType, System.Action handler, System.Predicate shouldHandle = null) { } - protected void Recover(System.Type messageType, System.Predicate shouldHandle, System.Action handler) { } - protected void Recover(System.Func handler) { } - protected void Recover(System.Type messageType, System.Func handler) { } - protected void RecoverAny(System.Action handler) { } - } - public sealed class Recovery - { - public Recovery() { } - public Recovery(Akka.Persistence.SnapshotSelectionCriteria fromSnapshot) { } - public Recovery(Akka.Persistence.SnapshotSelectionCriteria fromSnapshot, long toSequenceNr) { } - public Recovery(Akka.Persistence.SnapshotSelectionCriteria fromSnapshot = null, long toSequenceNr = 9223372036854775807, long replayMax = 9223372036854775807) { } - public static Akka.Persistence.Recovery Default { get; } - public Akka.Persistence.SnapshotSelectionCriteria FromSnapshot { get; } - public static Akka.Persistence.Recovery None { get; } - public long ReplayMax { get; } - public long ToSequenceNr { get; } - } - public sealed class RecoveryCompleted - { - public static readonly Akka.Persistence.RecoveryCompleted Instance; - public override bool Equals(object obj) { } - public override int GetHashCode() { } - } - public sealed class RecoverySuccess : Akka.Actor.INoSerializationVerificationNeeded, Akka.Event.IDeadLetterSuppression, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable - { - public RecoverySuccess(long highestSequenceNr) { } - public long HighestSequenceNr { get; } - public bool Equals(Akka.Persistence.RecoverySuccess other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class RecoveryTick - { - public RecoveryTick(bool snapshot) { } - public bool Snapshot { get; } - } - public sealed class RecoveryTimedOutException : Akka.Actor.AkkaException - { - public RecoveryTimedOutException() { } - public RecoveryTimedOutException(string message, System.Exception cause = null) { } - public RecoveryTimedOutException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } - } - public sealed class ReplayedMessage : Akka.Actor.INoSerializationVerificationNeeded, Akka.Event.IDeadLetterSuppression, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable - { - public ReplayedMessage(Akka.Persistence.IPersistentRepresentation persistent) { } - public Akka.Persistence.IPersistentRepresentation Persistent { get; } - public bool Equals(Akka.Persistence.ReplayedMessage other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class ReplayMessages : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalRequest, Akka.Persistence.IPersistenceMessage, System.IEquatable - { - public ReplayMessages(long fromSequenceNr, long toSequenceNr, long max, string persistenceId, Akka.Actor.IActorRef persistentActor) { } - public long FromSequenceNr { get; } - public long Max { get; } - public string PersistenceId { get; } - public Akka.Actor.IActorRef PersistentActor { get; } - public long ToSequenceNr { get; } - public bool Equals(Akka.Persistence.ReplayMessages other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class ReplayMessagesFailure : Akka.Actor.INoSerializationVerificationNeeded, Akka.Event.IDeadLetterSuppression, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable - { - public ReplayMessagesFailure(System.Exception cause) { } - public System.Exception Cause { get; } - public bool Equals(Akka.Persistence.ReplayMessagesFailure other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class ReplyToStrategy : Akka.Persistence.IStashOverflowStrategy - { - public ReplyToStrategy(object response) { } - public object Response { get; } - } - public sealed class SaveSnapshot : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotRequest, System.IEquatable - { - public SaveSnapshot(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } - public Akka.Persistence.SnapshotMetadata Metadata { get; } - public object Snapshot { get; } - public bool Equals(Akka.Persistence.SaveSnapshot other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class SaveSnapshotFailure : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable - { - public SaveSnapshotFailure(Akka.Persistence.SnapshotMetadata metadata, System.Exception cause) { } - public System.Exception Cause { get; } - public Akka.Persistence.SnapshotMetadata Metadata { get; } - public bool Equals(Akka.Persistence.SaveSnapshotFailure other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class SaveSnapshotSuccess : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable - { - public SaveSnapshotSuccess(Akka.Persistence.SnapshotMetadata metadata) { } - public Akka.Persistence.SnapshotMetadata Metadata { get; } - public bool Equals(Akka.Persistence.SaveSnapshotSuccess other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - [System.ObsoleteAttribute("PersistentView was deprecated and will be removed in the next major version [1.3." + - "0]")] - public sealed class ScheduledUpdate - { - public ScheduledUpdate(long replayMax) { } - public long ReplayMax { get; } - } - public sealed class SelectedSnapshot : System.IEquatable - { - public SelectedSnapshot(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } - public Akka.Persistence.SnapshotMetadata Metadata { get; } - public object Snapshot { get; } - public bool Equals(Akka.Persistence.SelectedSnapshot other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class SnapshotMetadata : System.IEquatable - { - public static System.DateTime TimestampNotSpecified; - public SnapshotMetadata(string persistenceId, long sequenceNr) { } - [Newtonsoft.Json.JsonConstructorAttribute()] - public SnapshotMetadata(string persistenceId, long sequenceNr, System.DateTime timestamp) { } - public static System.Collections.Generic.IComparer Comparer { get; } - public string PersistenceId { get; } - public long SequenceNr { get; } - public System.DateTime Timestamp { get; } - public override bool Equals(object obj) { } - public bool Equals(Akka.Persistence.SnapshotMetadata other) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class SnapshotOffer : System.IEquatable - { - public SnapshotOffer(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } - public Akka.Persistence.SnapshotMetadata Metadata { get; } - public object Snapshot { get; } - public bool Equals(Akka.Persistence.SnapshotOffer other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class SnapshotSelectionCriteria : System.IEquatable - { - [Newtonsoft.Json.JsonConstructorAttribute()] - public SnapshotSelectionCriteria(long maxSequenceNr, System.DateTime maxTimeStamp, long minSequenceNr = 0, System.Nullable minTimestamp = null) { } - public SnapshotSelectionCriteria(long maxSequenceNr) { } - public static Akka.Persistence.SnapshotSelectionCriteria Latest { get; } - public long MaxSequenceNr { get; } - public System.DateTime MaxTimeStamp { get; } - public long MinSequenceNr { get; } - public System.Nullable MinTimestamp { get; } - public static Akka.Persistence.SnapshotSelectionCriteria None { get; } - public bool Equals(Akka.Persistence.SnapshotSelectionCriteria other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class StashingHandlerInvocation : Akka.Persistence.IPendingHandlerInvocation - { - public StashingHandlerInvocation(object evt, System.Action handler) { } - public object Event { get; } - public System.Action Handler { get; } - } - public sealed class ThrowExceptionConfigurator : Akka.Persistence.IStashOverflowStrategyConfigurator - { - public ThrowExceptionConfigurator() { } - public Akka.Persistence.IStashOverflowStrategy Create(Akka.Configuration.Config config) { } - } - public class ThrowOverflowExceptionStrategy : Akka.Persistence.IStashOverflowStrategy - { - public static Akka.Persistence.ThrowOverflowExceptionStrategy Instance { get; } - } - public sealed class UnconfirmedDelivery : System.IEquatable - { - public UnconfirmedDelivery(long deliveryId, Akka.Actor.ActorPath destination, object message) { } - public long DeliveryId { get; } - public Akka.Actor.ActorPath Destination { get; } - public object Message { get; } - public bool Equals(Akka.Persistence.UnconfirmedDelivery other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class UnconfirmedWarning : System.IEquatable - { - public UnconfirmedWarning(Akka.Persistence.UnconfirmedDelivery[] unconfirmedDeliveries) { } - public Akka.Persistence.UnconfirmedDelivery[] UnconfirmedDeliveries { get; } - public bool Equals(Akka.Persistence.UnconfirmedWarning other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public abstract class UntypedPersistentActor : Akka.Persistence.Eventsourced - { - protected UntypedPersistentActor() { } - protected static Akka.Actor.IUntypedActorContext Context { get; } - protected void Become(Akka.Actor.UntypedReceive receive) { } - protected void BecomeStacked(Akka.Actor.UntypedReceive receive) { } - protected abstract void OnCommand(object message); - protected abstract void OnRecover(object message); - protected override bool Receive(object message) { } - protected virtual bool ReceiveCommand(object message) { } - protected virtual bool ReceiveRecover(object message) { } - } - [System.ObsoleteAttribute("PersistentView was deprecated and will be removed in the next major version [1.3." + - "0]")] - public sealed class Update - { - public Update() { } - public Update(bool isAwait) { } - [Newtonsoft.Json.JsonConstructorAttribute()] - public Update(bool isAwait, long replayMax) { } - public bool IsAwait { get; } - public long ReplayMax { get; } - } - public sealed class WriteMessageFailure : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable - { - public WriteMessageFailure(Akka.Persistence.IPersistentRepresentation persistent, System.Exception cause, int actorInstanceId) { } - public int ActorInstanceId { get; } - public System.Exception Cause { get; } - public Akka.Persistence.IPersistentRepresentation Persistent { get; } - public bool Equals(Akka.Persistence.WriteMessageFailure other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class WriteMessageRejected : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable - { - public WriteMessageRejected(Akka.Persistence.IPersistentRepresentation persistent, System.Exception cause, int actorInstanceId) { } - public int ActorInstanceId { get; } - public System.Exception Cause { get; } - public Akka.Persistence.IPersistentRepresentation Persistent { get; } - public bool Equals(Akka.Persistence.WriteMessageRejected other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class WriteMessages : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalRequest, Akka.Persistence.IPersistenceMessage, System.IEquatable - { - public WriteMessages(System.Collections.Generic.IEnumerable messages, Akka.Actor.IActorRef persistentActor, int actorInstanceId) { } - public int ActorInstanceId { get; } - public System.Collections.Generic.IEnumerable Messages { get; } - public Akka.Actor.IActorRef PersistentActor { get; } - public bool Equals(Akka.Persistence.WriteMessages other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class WriteMessagesFailed : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable - { - public WriteMessagesFailed(System.Exception cause) { } - public System.Exception Cause { get; } - public bool Equals(Akka.Persistence.WriteMessagesFailed other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } - public sealed class WriteMessagesSuccessful : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage - { - public static Akka.Persistence.WriteMessagesSuccessful Instance { get; } - } - public sealed class WriteMessageSuccess : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable - { - public WriteMessageSuccess(Akka.Persistence.IPersistentRepresentation persistent, int actorInstanceId) { } - public int ActorInstanceId { get; } - public Akka.Persistence.IPersistentRepresentation Persistent { get; } - public bool Equals(Akka.Persistence.WriteMessageSuccess other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - public override string ToString() { } - } -} -namespace Akka.Persistence.Fsm -{ - public class static PersistentFSM - { - public interface IFsmState - { - string Identifier { get; } - } - public class PersistentFSMSnapshot : Akka.Persistence.Serialization.IMessage - { - public PersistentFSMSnapshot(string stateIdentifier, TD data, System.Nullable timeout) { } - public TD Data { get; } - public string StateIdentifier { get; } - public System.Nullable Timeout { get; } - protected bool Equals(Akka.Persistence.Fsm.PersistentFSM.PersistentFSMSnapshot other) { } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - } - public class State - { - public State(TS stateName, TD stateData, System.Nullable timeout = null, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.IReadOnlyList replies = null, System.Collections.Generic.IReadOnlyList domainEvents = null, System.Action afterTransitionDo = null, bool notifies = True) { } - public System.Action AfterTransitionDo { get; } - public System.Collections.Generic.IReadOnlyList DomainEvents { get; } - public System.Collections.Generic.IReadOnlyList Replies { get; set; } - public TD StateData { get; } - public TS StateName { get; } - public Akka.Actor.FSMBase.Reason StopReason { get; } - public System.Nullable Timeout { get; } - public Akka.Persistence.Fsm.PersistentFSM.State AndThen(System.Action handler) { } - public Akka.Persistence.Fsm.PersistentFSM.State Applying(params TE[] events) { } - public Akka.Persistence.Fsm.PersistentFSM.State ForMax(System.TimeSpan timeout) { } - public Akka.Persistence.Fsm.PersistentFSM.State Replying(object replyValue) { } - public override string ToString() { } - public Akka.Persistence.Fsm.PersistentFSM.State Using(TD nextStateData) { } - } - public class StateChangeEvent : Akka.Persistence.Serialization.IMessage - { - public StateChangeEvent(string stateIdentifier, System.Nullable timeout) { } - public string StateIdentifier { get; } - public System.Nullable Timeout { get; } - } - } - public abstract class PersistentFSM : Akka.Persistence.Fsm.PersistentFSMBase - where TState : Akka.Persistence.Fsm.PersistentFSM.IFsmState - { - protected PersistentFSM() { } - protected abstract TData ApplyEvent(TEvent domainEvent, TData currentData); - protected override void ApplyState(Akka.Persistence.Fsm.PersistentFSM.State nextState) { } - protected virtual void OnRecoveryCompleted() { } - protected override bool ReceiveRecover(object message) { } - public void SaveStateSnapshot() { } - } - public abstract class PersistentFSMBase : Akka.Persistence.PersistentActor, Akka.Routing.IListeners - { - protected PersistentFSMBase() { } - public Akka.Routing.ListenerSupport Listeners { get; } - public TData NextStateData { get; } - public TData StateData { get; } - public TState StateName { get; } - protected System.Collections.Generic.IEnumerable StateNames { get; } - protected virtual void ApplyState(Akka.Persistence.Fsm.PersistentFSM.State nextState) { } - public void CancelTimer(string name) { } - public Akka.Persistence.Fsm.PersistentFSM.State GoTo(TState nextStateName) { } - public bool IsTimerActive(string name) { } - protected virtual void LogTermination(Akka.Actor.FSMBase.Reason reason) { } - public void OnTermination(System.Action> terminationHandler) { } - public void OnTransition(Akka.Persistence.Fsm.PersistentFSMBase.TransitionHandler transitionHandler) { } - protected override void PostStop() { } - protected override bool ReceiveCommand(object message) { } - public void SetStateTimeout(TState state, System.Nullable timeout) { } - public void SetTimer(string name, object msg, System.TimeSpan timeout, bool repeat = False) { } - public void StartWith(TState stateName, TData stateData, System.Nullable timeout = null) { } - public Akka.Persistence.Fsm.PersistentFSM.State Stay() { } - public Akka.Persistence.Fsm.PersistentFSM.State Stop() { } - public Akka.Persistence.Fsm.PersistentFSM.State Stop(Akka.Actor.FSMBase.Reason reason) { } - public Akka.Persistence.Fsm.PersistentFSM.State Stop(Akka.Actor.FSMBase.Reason reason, TData stateData) { } - public Akka.Persistence.Fsm.PersistentFSMBase.TransformHelper Transform(Akka.Persistence.Fsm.PersistentFSMBase.StateFunction func) { } - public void When(TState stateName, Akka.Persistence.Fsm.PersistentFSMBase.StateFunction func, System.Nullable timeout = null) { } - public void WhenUnhandled(Akka.Persistence.Fsm.PersistentFSMBase.StateFunction stateFunction) { } - public delegate Akka.Persistence.Fsm.PersistentFSM.State StateFunction(Akka.Actor.FSMBase.Event fsmEvent, Akka.Persistence.Fsm.PersistentFSM.State state = null); - public sealed class TransformHelper - { - public TransformHelper(Akka.Persistence.Fsm.PersistentFSMBase.StateFunction func) { } - public Akka.Persistence.Fsm.PersistentFSMBase.StateFunction Func { get; } - public Akka.Persistence.Fsm.PersistentFSMBase.StateFunction Using(System.Func, Akka.Persistence.Fsm.PersistentFSM.State> andThen) { } - } - public delegate void TransitionHandler(TState initialState, TState nextState); - } -} -namespace Akka.Persistence.Journal -{ - public class AsyncReplayTimeoutException : Akka.Actor.AkkaException - { - public AsyncReplayTimeoutException() { } - public AsyncReplayTimeoutException(string message) { } - protected AsyncReplayTimeoutException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } - } - public abstract class AsyncWriteJournal : Akka.Persistence.Journal.WriteJournalBase, Akka.Persistence.Journal.IAsyncRecovery - { - protected readonly bool CanPublish; - protected AsyncWriteJournal() { } - protected abstract System.Threading.Tasks.Task DeleteMessagesToAsync(string persistenceId, long toSequenceNr); - public abstract System.Threading.Tasks.Task ReadHighestSequenceNrAsync(string persistenceId, long fromSequenceNr); - protected virtual bool Receive(object message) { } - protected virtual bool ReceivePluginInternal(object message) { } - protected bool ReceiveWriteJournal(object message) { } - public abstract System.Threading.Tasks.Task ReplayMessagesAsync(Akka.Actor.IActorContext context, string persistenceId, long fromSequenceNr, long toSequenceNr, long max, System.Action recoveryCallback); - protected System.Exception TryUnwrapException(System.Exception e) { } - protected abstract System.Threading.Tasks.Task> WriteMessagesAsync(System.Collections.Generic.IEnumerable messages); - } - public abstract class AsyncWriteProxy : Akka.Persistence.Journal.AsyncWriteJournal, Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Dispatch.IRequiresMessageQueue - { - protected AsyncWriteProxy() { } - public Akka.Actor.IStash Stash { get; set; } - public abstract System.TimeSpan Timeout { get; } - public override void AroundPreStart() { } - protected internal override bool AroundReceive(Akka.Actor.Receive receive, object message) { } - protected override System.Threading.Tasks.Task DeleteMessagesToAsync(string persistenceId, long toSequenceNr) { } - public override System.Threading.Tasks.Task ReadHighestSequenceNrAsync(string persistenceId, long fromSequenceNr) { } - public override System.Threading.Tasks.Task ReplayMessagesAsync(Akka.Actor.IActorContext context, string persistenceId, long fromSequenceNr, long toSequenceNr, long max, System.Action recoveryCallback) { } - protected override System.Threading.Tasks.Task> WriteMessagesAsync(System.Collections.Generic.IEnumerable messages) { } - public class InitTimeout - { - public static Akka.Persistence.Journal.AsyncWriteProxy.InitTimeout Instance { get; } - } - } - public class static AsyncWriteTarget - { - public sealed class DeleteMessagesTo : System.IEquatable - { - public DeleteMessagesTo(string persistenceId, long toSequenceNr) { } - public string PersistenceId { get; } - public long ToSequenceNr { get; } - public bool Equals(Akka.Persistence.Journal.AsyncWriteTarget.DeleteMessagesTo other) { } - } - public sealed class ReplayFailure - { - public ReplayFailure(System.Exception cause) { } - public System.Exception Cause { get; } - } - public sealed class ReplayMessages : System.IEquatable - { - public ReplayMessages(string persistenceId, long fromSequenceNr, long toSequenceNr, long max) { } - public long FromSequenceNr { get; } - public long Max { get; } - public string PersistenceId { get; } - public long ToSequenceNr { get; } - public bool Equals(Akka.Persistence.Journal.AsyncWriteTarget.ReplayMessages other) { } - } - public sealed class ReplaySuccess : System.IEquatable - { - public ReplaySuccess(long highestSequenceNr) { } - public long HighestSequenceNr { get; } - public bool Equals(Akka.Persistence.Journal.AsyncWriteTarget.ReplaySuccess other) { } - } - public sealed class WriteMessages - { - public WriteMessages(System.Collections.Generic.IEnumerable messages) { } - public Akka.Persistence.AtomicWrite[] Messages { get; } - } - } - public sealed class CombinedReadEventAdapter : Akka.Persistence.Journal.IEventAdapter, Akka.Persistence.Journal.IReadEventAdapter, Akka.Persistence.Journal.IWriteEventAdapter - { - public CombinedReadEventAdapter(System.Collections.Generic.IEnumerable adapters) { } - public System.Collections.Generic.IEnumerable Adapters { get; } - public Akka.Persistence.Journal.IEventSequence FromJournal(object evt, string manifest) { } - public string Manifest(object evt) { } - public object ToJournal(object evt) { } - } - public sealed class EmptyEventSequence : Akka.Persistence.Journal.IEmptyEventSequence, Akka.Persistence.Journal.IEventSequence, System.IEquatable - { - public static readonly Akka.Persistence.Journal.EmptyEventSequence Instance; - public System.Collections.Generic.IEnumerable Events { get; } - public bool Equals(Akka.Persistence.Journal.IEventSequence other) { } - public override bool Equals(object obj) { } - } - public class EventAdapters - { - protected EventAdapters(System.Collections.Concurrent.ConcurrentDictionary map, System.Collections.Generic.IEnumerable> bindings, Akka.Event.ILoggingAdapter log) { } - public static Akka.Persistence.Journal.EventAdapters Create(Akka.Actor.ExtendedActorSystem system, Akka.Configuration.Config config) { } - public Akka.Persistence.Journal.IEventAdapter Get() { } - public virtual Akka.Persistence.Journal.IEventAdapter Get(System.Type type) { } - } - public class static EventSequence - { - public static Akka.Persistence.Journal.IEventSequence Empty; - public static Akka.Persistence.Journal.IEventSequence Create(params object[] events) { } - public static Akka.Persistence.Journal.IEventSequence Create(System.Collections.Generic.IEnumerable events) { } - public static Akka.Persistence.Journal.IEventSequence Single(object e) { } - } - public class EventSequence : Akka.Persistence.Journal.IEventSequence, System.IEquatable - { - public EventSequence(System.Collections.Generic.IEnumerable events) { } - public System.Collections.Generic.IEnumerable Events { get; } - public bool Equals(Akka.Persistence.Journal.IEventSequence other) { } - public override bool Equals(object obj) { } - } - public interface IAsyncRecovery - { - System.Threading.Tasks.Task ReadHighestSequenceNrAsync(string persistenceId, long fromSequenceNr); - System.Threading.Tasks.Task ReplayMessagesAsync(Akka.Actor.IActorContext context, string persistenceId, long fromSequenceNr, long toSequenceNr, long max, System.Action recoveryCallback); - } - public sealed class IdentityEventAdapter : Akka.Persistence.Journal.IEventAdapter, Akka.Persistence.Journal.IReadEventAdapter, Akka.Persistence.Journal.IWriteEventAdapter - { - public static Akka.Persistence.Journal.IdentityEventAdapter Instance { get; } - public Akka.Persistence.Journal.IEventSequence FromJournal(object evt, string manifest) { } - public string Manifest(object evt) { } - public object ToJournal(object evt) { } - } - public interface IEmptyEventSequence : Akka.Persistence.Journal.IEventSequence { } - public interface IEventAdapter : Akka.Persistence.Journal.IReadEventAdapter, Akka.Persistence.Journal.IWriteEventAdapter { } - public interface IEventSequence - { - System.Collections.Generic.IEnumerable Events { get; } - } - public interface IMemoryMessages - { - System.Collections.Generic.IDictionary> Add(Akka.Persistence.IPersistentRepresentation persistent); - System.Collections.Generic.IDictionary> Delete(string pid, long seqNr); - long HighestSequenceNr(string pid); - System.Collections.Generic.IEnumerable Read(string pid, long fromSeqNr, long toSeqNr, long max); - System.Collections.Generic.IDictionary> Update(string pid, long seqNr, System.Func updater); - } - public interface IReadEventAdapter - { - Akka.Persistence.Journal.IEventSequence FromJournal(object evt, string manifest); - } - public interface IWriteEventAdapter - { - string Manifest(object evt); - object ToJournal(object evt); - } - public class MemoryJournal : Akka.Persistence.Journal.AsyncWriteJournal - { - public MemoryJournal() { } - protected virtual System.Collections.Concurrent.ConcurrentDictionary> Messages { get; } - public System.Collections.Generic.IDictionary> Add(Akka.Persistence.IPersistentRepresentation persistent) { } - public System.Collections.Generic.IDictionary> Delete(string pid, long seqNr) { } - protected override System.Threading.Tasks.Task DeleteMessagesToAsync(string persistenceId, long toSequenceNr) { } - public long HighestSequenceNr(string pid) { } - public System.Collections.Generic.IEnumerable Read(string pid, long fromSeqNr, long toSeqNr, long max) { } - public override System.Threading.Tasks.Task ReadHighestSequenceNrAsync(string persistenceId, long fromSequenceNr) { } - public override System.Threading.Tasks.Task ReplayMessagesAsync(Akka.Actor.IActorContext context, string persistenceId, long fromSequenceNr, long toSequenceNr, long max, System.Action recoveryCallback) { } - public System.Collections.Generic.IDictionary> Update(string pid, long seqNr, System.Func updater) { } - protected override System.Threading.Tasks.Task> WriteMessagesAsync(System.Collections.Generic.IEnumerable messages) { } - } - public class PersistencePluginProxy : Akka.Actor.ActorBase, Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Dispatch.IRequiresMessageQueue - { - public PersistencePluginProxy(Akka.Configuration.Config config) { } - public Akka.Actor.IStash Stash { get; set; } - protected override void PreStart() { } - protected override bool Receive(object message) { } - public static void SetTargetLocation(Akka.Actor.ActorSystem system, Akka.Actor.Address address) { } - public static void Start(Akka.Actor.ActorSystem system) { } - public sealed class TargetLocation - { - public TargetLocation(Akka.Actor.Address address) { } - public Akka.Actor.Address Address { get; } - } - } - public class PersistencePluginProxyExtension : Akka.Actor.ExtensionIdProvider, Akka.Actor.IExtension - { - public PersistencePluginProxyExtension(Akka.Actor.ActorSystem system) { } - public override Akka.Persistence.Journal.PersistencePluginProxyExtension CreateExtension(Akka.Actor.ExtendedActorSystem system) { } - } - public class ReplayFilter : Akka.Actor.ActorBase - { - public ReplayFilter(Akka.Actor.IActorRef persistentActor, Akka.Persistence.Journal.ReplayFilterMode mode, int windowSize, int maxOldWriters, bool debugEnabled) { } - public bool DebugEnabled { get; } - public int MaxOldWriters { get; } - public Akka.Persistence.Journal.ReplayFilterMode Mode { get; } - public Akka.Actor.IActorRef PersistentActor { get; } - public int WindowSize { get; } - public static Akka.Actor.Props Props(Akka.Actor.IActorRef persistentActor, Akka.Persistence.Journal.ReplayFilterMode mode, int windowSize, int maxOldWriters, bool debugEnabled) { } - protected override bool Receive(object message) { } - } - public enum ReplayFilterMode - { - Fail = 0, - Warn = 1, - RepairByDiscardOld = 2, - Disabled = 3, - } - public sealed class SetStore - { - public readonly Akka.Actor.IActorRef Store; - public SetStore(Akka.Actor.IActorRef store) { } - } - public class SharedMemoryJournal : Akka.Persistence.Journal.MemoryJournal - { - public SharedMemoryJournal() { } - protected override System.Collections.Concurrent.ConcurrentDictionary> Messages { get; } - } - public struct SingleEventSequence : Akka.Persistence.Journal.IEventSequence, System.IEquatable - { - public SingleEventSequence(object e) { } - public System.Collections.Generic.IEnumerable Events { get; } - public bool Equals(Akka.Persistence.Journal.IEventSequence other) { } - public override bool Equals(object obj) { } - } - public struct Tagged - { - public Tagged(object payload, System.Collections.Generic.IEnumerable tags) { } - public Tagged(object payload, System.Collections.Immutable.IImmutableSet tags) { } - public object Payload { get; } - public System.Collections.Immutable.IImmutableSet Tags { get; } - } - public abstract class WriteJournalBase : Akka.Actor.ActorBase - { - protected WriteJournalBase() { } - [Akka.Annotations.InternalApiAttribute()] - protected System.Collections.Generic.IEnumerable AdaptFromJournal(Akka.Persistence.IPersistentRepresentation representation) { } - protected Akka.Persistence.IPersistentRepresentation AdaptToJournal(Akka.Persistence.IPersistentRepresentation representation) { } - protected System.Collections.Generic.IEnumerable PreparePersistentBatch(System.Collections.Generic.IEnumerable resequenceables) { } - } -} -namespace Akka.Persistence.Serialization -{ - public interface IMessage { } - public sealed class PersistenceMessageSerializer : Akka.Serialization.Serializer - { - public PersistenceMessageSerializer(Akka.Actor.ExtendedActorSystem system) { } - public override bool IncludeManifest { get; } - public override object FromBinary(byte[] bytes, System.Type type) { } - public override byte[] ToBinary(object obj) { } - } - public class PersistenceSnapshotSerializer : Akka.Serialization.Serializer - { - public PersistenceSnapshotSerializer(Akka.Actor.ExtendedActorSystem system) { } - public override bool IncludeManifest { get; } - public override object FromBinary(byte[] bytes, System.Type type) { } - public override byte[] ToBinary(object obj) { } - } - public sealed class Snapshot - { - public Snapshot(object data) { } - public object Data { get; } - public override bool Equals(object obj) { } - public override int GetHashCode() { } - } -} -namespace Akka.Persistence.Snapshot -{ - public class LocalSnapshotStore : Akka.Persistence.Snapshot.SnapshotStore - { - public LocalSnapshotStore() { } - protected override System.Threading.Tasks.Task DeleteAsync(Akka.Persistence.SnapshotMetadata metadata) { } - protected override async System.Threading.Tasks.Task DeleteAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } - protected System.IO.FileInfo GetSnapshotFileForWrite(Akka.Persistence.SnapshotMetadata metadata, string extension = "") { } - protected override System.Threading.Tasks.Task LoadAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } - protected override void PreStart() { } - protected override bool ReceivePluginInternal(object message) { } - protected virtual void Save(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } - protected override System.Threading.Tasks.Task SaveAsync(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } - protected void Serialize(System.IO.Stream stream, Akka.Persistence.Serialization.Snapshot snapshot) { } - protected System.IO.FileInfo WithOutputStream(Akka.Persistence.SnapshotMetadata metadata, System.Action p) { } - } - public class MemorySnapshotStore : Akka.Persistence.Snapshot.SnapshotStore - { - public MemorySnapshotStore() { } - protected virtual System.Collections.Generic.List Snapshots { get; } - protected override System.Threading.Tasks.Task DeleteAsync(Akka.Persistence.SnapshotMetadata metadata) { } - protected override System.Threading.Tasks.Task DeleteAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } - protected override System.Threading.Tasks.Task LoadAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } - protected override System.Threading.Tasks.Task SaveAsync(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } - } - public sealed class NoSnapshotStore : Akka.Persistence.Snapshot.SnapshotStore - { - public NoSnapshotStore() { } - protected override System.Threading.Tasks.Task DeleteAsync(Akka.Persistence.SnapshotMetadata metadata) { } - protected override System.Threading.Tasks.Task DeleteAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } - protected override System.Threading.Tasks.Task LoadAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } - protected override System.Threading.Tasks.Task SaveAsync(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } - public class NoSnapshotStoreException : System.Exception - { - public NoSnapshotStoreException() { } - public NoSnapshotStoreException(string message) { } - public NoSnapshotStoreException(string message, System.Exception innerException) { } - protected NoSnapshotStoreException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } - } - } - public class SnapshotEntry - { - public SnapshotEntry() { } - public string Id { get; set; } - public string PersistenceId { get; set; } - public long SequenceNr { get; set; } - public object Snapshot { get; set; } - public long Timestamp { get; set; } - } - public abstract class SnapshotStore : Akka.Actor.ActorBase - { - protected SnapshotStore() { } - protected abstract System.Threading.Tasks.Task DeleteAsync(Akka.Persistence.SnapshotMetadata metadata); - protected abstract System.Threading.Tasks.Task DeleteAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria); - protected abstract System.Threading.Tasks.Task LoadAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria); - protected virtual bool Receive(object message) { } - protected virtual bool ReceivePluginInternal(object message) { } - protected abstract System.Threading.Tasks.Task SaveAsync(Akka.Persistence.SnapshotMetadata metadata, object snapshot); - } -} + \ No newline at end of file From d2561620037944190f4381006133a87304536548 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 27 Jun 2018 21:04:12 -0500 Subject: [PATCH 50/70] fixed contents of Akka.Persistence API approval --- ...oreAPISpec.ApprovePersistence.approved.txt | 1177 ++++++++++++++++- 1 file changed, 1176 insertions(+), 1 deletion(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt index 5f282702bb0..8c59631138f 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt @@ -1 +1,1176 @@ - \ No newline at end of file +[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Persistence.TCK")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Persistence.Tests")] +[assembly: System.Runtime.InteropServices.ComVisibleAttribute(false)] +[assembly: System.Runtime.InteropServices.GuidAttribute("e3bcba88-003c-4cda-8a60-f0c2553fe3c8")] +[assembly: System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.5", FrameworkDisplayName=".NET Framework 4.5")] +namespace Akka.Persistence +{ + public sealed class AsyncHandlerInvocation : Akka.Persistence.IPendingHandlerInvocation + { + public AsyncHandlerInvocation(object evt, System.Action handler) { } + public object Event { get; } + public System.Action Handler { get; } + } + public abstract class AtLeastOnceDeliveryActor : Akka.Persistence.PersistentActor + { + protected AtLeastOnceDeliveryActor() { } + protected AtLeastOnceDeliveryActor(Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings settings) { } + public int MaxUnconfirmedMessages { get; } + public virtual System.TimeSpan RedeliverInterval { get; } + public virtual int RedeliveryBurstLimit { get; } + public int UnconfirmedCount { get; } + public int WarnAfterNumberOfUnconfirmedAttempts { get; } + public override void AroundPostStop() { } + public override void AroundPreRestart(System.Exception cause, object message) { } + protected internal override bool AroundReceive(Akka.Actor.Receive receive, object message) { } + public bool ConfirmDelivery(long deliveryId) { } + public void Deliver(Akka.Actor.ActorPath destination, System.Func deliveryMessageMapper) { } + public void Deliver(Akka.Actor.ActorSelection destination, System.Func deliveryMessageMapper) { } + public Akka.Persistence.AtLeastOnceDeliverySnapshot GetDeliverySnapshot() { } + protected override void OnReplaySuccess() { } + public void SetDeliverySnapshot(Akka.Persistence.AtLeastOnceDeliverySnapshot snapshot) { } + } + public abstract class AtLeastOnceDeliveryReceiveActor : Akka.Persistence.ReceivePersistentActor + { + protected AtLeastOnceDeliveryReceiveActor() { } + protected AtLeastOnceDeliveryReceiveActor(Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings settings) { } + public int MaxUnconfirmedMessages { get; } + public virtual System.TimeSpan RedeliverInterval { get; } + public int RedeliveryBurstLimit { get; } + public int UnconfirmedCount { get; } + public int WarnAfterNumberOfUnconfirmedAttempts { get; } + public override void AroundPostStop() { } + public override void AroundPreRestart(System.Exception cause, object message) { } + protected internal override bool AroundReceive(Akka.Actor.Receive receive, object message) { } + public bool ConfirmDelivery(long deliveryId) { } + public void Deliver(Akka.Actor.ActorPath destination, System.Func deliveryMessageMapper) { } + public void Deliver(Akka.Actor.ActorSelection destination, System.Func deliveryMessageMapper) { } + public Akka.Persistence.AtLeastOnceDeliverySnapshot GetDeliverySnapshot() { } + protected override void OnReplaySuccess() { } + public void SetDeliverySnapshot(Akka.Persistence.AtLeastOnceDeliverySnapshot snapshot) { } + } + public class AtLeastOnceDeliverySemantic + { + public AtLeastOnceDeliverySemantic(Akka.Actor.IActorContext context, Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings settings) { } + public virtual int MaxUnconfirmedMessages { get; } + public virtual System.TimeSpan RedeliverInterval { get; } + public virtual int RedeliveryBurstLimit { get; } + public int UnconfirmedCount { get; } + public virtual int WarnAfterNumberOfUnconfirmedAttempts { get; } + public bool AroundReceive(Akka.Actor.Receive receive, object message) { } + public void Cancel() { } + public bool ConfirmDelivery(long deliveryId) { } + public void Deliver(Akka.Actor.ActorPath destination, System.Func deliveryMessageMapper, bool isRecovering) { } + public Akka.Persistence.AtLeastOnceDeliverySnapshot GetDeliverySnapshot() { } + public void OnReplaySuccess() { } + public void SetDeliverySnapshot(Akka.Persistence.AtLeastOnceDeliverySnapshot snapshot) { } + public sealed class Delivery : System.IEquatable + { + public Delivery(Akka.Actor.ActorPath destination, object message, System.DateTime timestamp, int attempt) { } + public int Attempt { get; } + public Akka.Actor.ActorPath Destination { get; } + public object Message { get; } + public System.DateTime Timestamp { get; } + public bool Equals(Akka.Persistence.AtLeastOnceDeliverySemantic.Delivery other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public Akka.Persistence.AtLeastOnceDeliverySemantic.Delivery IncrementedCopy() { } + public override string ToString() { } + } + public sealed class RedeliveryTick : Akka.Actor.INotInfluenceReceiveTimeout + { + public static Akka.Persistence.AtLeastOnceDeliverySemantic.RedeliveryTick Instance { get; } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + } + } + public sealed class AtLeastOnceDeliverySnapshot : Akka.Persistence.Serialization.IMessage, System.IEquatable + { + public AtLeastOnceDeliverySnapshot(long currentDeliveryId, Akka.Persistence.UnconfirmedDelivery[] unconfirmedDeliveries) { } + public long CurrentDeliveryId { get; } + public Akka.Persistence.UnconfirmedDelivery[] UnconfirmedDeliveries { get; } + public bool Equals(Akka.Persistence.AtLeastOnceDeliverySnapshot other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class AtomicWrite : Akka.Persistence.IPersistentEnvelope, Akka.Persistence.Serialization.IMessage + { + public AtomicWrite(Akka.Persistence.IPersistentRepresentation @event) { } + public AtomicWrite(System.Collections.Immutable.IImmutableList payload) { } + public long HighestSequenceNr { get; } + public long LowestSequenceNr { get; } + public object Payload { get; } + public string PersistenceId { get; } + public Akka.Actor.IActorRef Sender { get; } + public int Size { get; } + public bool Equals(Akka.Persistence.AtomicWrite other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class DeleteMessagesFailure : System.IEquatable + { + public DeleteMessagesFailure(System.Exception cause, long toSequenceNr) { } + public System.Exception Cause { get; } + public long ToSequenceNr { get; } + public bool Equals(Akka.Persistence.DeleteMessagesFailure other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class DeleteMessagesSuccess : System.IEquatable + { + public DeleteMessagesSuccess(long toSequenceNr) { } + public long ToSequenceNr { get; } + public bool Equals(Akka.Persistence.DeleteMessagesSuccess other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class DeleteMessagesTo : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalRequest, Akka.Persistence.IPersistenceMessage, System.IEquatable + { + public DeleteMessagesTo(string persistenceId, long toSequenceNr, Akka.Actor.IActorRef persistentActor) { } + public string PersistenceId { get; } + public Akka.Actor.IActorRef PersistentActor { get; } + public long ToSequenceNr { get; } + public bool Equals(Akka.Persistence.DeleteMessagesTo other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class DeleteSnapshot : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotRequest, System.IEquatable + { + public DeleteSnapshot(Akka.Persistence.SnapshotMetadata metadata) { } + public Akka.Persistence.SnapshotMetadata Metadata { get; } + public bool Equals(Akka.Persistence.DeleteSnapshot other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class DeleteSnapshotFailure : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable + { + public DeleteSnapshotFailure(Akka.Persistence.SnapshotMetadata metadata, System.Exception cause) { } + public System.Exception Cause { get; } + public Akka.Persistence.SnapshotMetadata Metadata { get; } + public bool Equals(Akka.Persistence.DeleteSnapshotFailure other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class DeleteSnapshots : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotRequest, System.IEquatable + { + public DeleteSnapshots(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } + public Akka.Persistence.SnapshotSelectionCriteria Criteria { get; } + public string PersistenceId { get; } + public bool Equals(Akka.Persistence.DeleteSnapshots other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class DeleteSnapshotsFailure : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable + { + public DeleteSnapshotsFailure(Akka.Persistence.SnapshotSelectionCriteria criteria, System.Exception cause) { } + public System.Exception Cause { get; } + public Akka.Persistence.SnapshotSelectionCriteria Criteria { get; } + public bool Equals(Akka.Persistence.DeleteSnapshotsFailure other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class DeleteSnapshotsSuccess : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable + { + public DeleteSnapshotsSuccess(Akka.Persistence.SnapshotSelectionCriteria criteria) { } + public Akka.Persistence.SnapshotSelectionCriteria Criteria { get; } + public bool Equals(Akka.Persistence.DeleteSnapshotsSuccess other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class DeleteSnapshotSuccess : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable + { + public DeleteSnapshotSuccess(Akka.Persistence.SnapshotMetadata metadata) { } + public Akka.Persistence.SnapshotMetadata Metadata { get; } + public bool Equals(Akka.Persistence.DeleteSnapshotSuccess other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class DiscardConfigurator : Akka.Persistence.IStashOverflowStrategyConfigurator + { + public DiscardConfigurator() { } + public Akka.Persistence.IStashOverflowStrategy Create(Akka.Configuration.Config config) { } + } + public class DiscardToDeadLetterStrategy : Akka.Persistence.IStashOverflowStrategy + { + public static Akka.Persistence.DiscardToDeadLetterStrategy Instance { get; } + } + public abstract class Eventsourced : Akka.Actor.ActorBase, Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Dispatch.IRequiresMessageQueue, Akka.Persistence.IPersistenceRecovery, Akka.Persistence.IPersistenceStash, Akka.Persistence.IPersistentIdentity + { + public static readonly System.Func UnstashFilterPredicate; + protected Eventsourced() { } + protected Akka.Persistence.PersistenceExtension Extension { get; } + public virtual Akka.Persistence.IStashOverflowStrategy InternalStashOverflowStrategy { get; } + public bool IsRecovering { get; } + public bool IsRecoveryFinished { get; } + public Akka.Actor.IActorRef Journal { get; } + public string JournalPluginId { get; set; } + public long LastSequenceNr { get; } + protected virtual Akka.Event.ILoggingAdapter Log { get; } + public abstract string PersistenceId { get; } + public virtual Akka.Persistence.Recovery Recovery { get; } + public string SnapshotPluginId { get; set; } + public long SnapshotSequenceNr { get; } + public Akka.Actor.IActorRef SnapshotStore { get; } + public string SnapshotterId { get; } + public Akka.Actor.IStash Stash { get; set; } + public override void AroundPostRestart(System.Exception reason, object message) { } + public override void AroundPostStop() { } + public override void AroundPreRestart(System.Exception cause, object message) { } + public override void AroundPreStart() { } + protected internal override bool AroundReceive(Akka.Actor.Receive receive, object message) { } + public void DeferAsync(TEvent evt, System.Action handler) { } + public void DeleteMessages(long toSequenceNr) { } + public void DeleteSnapshot(long sequenceNr) { } + public void DeleteSnapshots(Akka.Persistence.SnapshotSelectionCriteria criteria) { } + public void LoadSnapshot(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria, long toSequenceNr) { } + protected virtual void OnPersistFailure(System.Exception cause, object @event, long sequenceNr) { } + protected virtual void OnPersistRejected(System.Exception cause, object @event, long sequenceNr) { } + protected virtual void OnRecoveryFailure(System.Exception reason, object message = null) { } + protected virtual void OnReplaySuccess() { } + public void Persist(TEvent @event, System.Action handler) { } + public void PersistAll(System.Collections.Generic.IEnumerable events, System.Action handler) { } + public void PersistAllAsync(System.Collections.Generic.IEnumerable events, System.Action handler) { } + public void PersistAsync(TEvent @event, System.Action handler) { } + protected abstract bool ReceiveCommand(object message); + protected abstract bool ReceiveRecover(object message); + protected void RunTask(System.Func action) { } + public void SaveSnapshot(object snapshot) { } + protected override void Unhandled(object message) { } + } + public interface IJournalMessage : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage { } + public interface IJournalPlugin + { + Akka.Configuration.Config DefaultConfig { get; } + string JournalPath { get; } + } + public interface IJournalRequest : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IPersistenceMessage { } + public interface IJournalResponse : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IPersistenceMessage { } + public interface IPendingHandlerInvocation + { + object Event { get; } + System.Action Handler { get; } + } + public interface IPersistenceMessage : Akka.Actor.INoSerializationVerificationNeeded { } + public interface IPersistenceRecovery + { + Akka.Persistence.Recovery Recovery { get; } + } + public interface IPersistenceStash : Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Dispatch.IRequiresMessageQueue + { + Akka.Persistence.IStashOverflowStrategy InternalStashOverflowStrategy { get; } + } + public interface IPersistentEnvelope + { + object Payload { get; } + Akka.Actor.IActorRef Sender { get; } + int Size { get; } + } + public interface IPersistentIdentity + { + string JournalPluginId { get; } + string PersistenceId { get; } + string SnapshotPluginId { get; } + } + public interface IPersistentRepresentation : Akka.Persistence.Serialization.IMessage + { + bool IsDeleted { get; } + string Manifest { get; } + object Payload { get; } + string PersistenceId { get; } + Akka.Actor.IActorRef Sender { get; } + long SequenceNr { get; } + string WriterGuid { get; } + Akka.Persistence.IPersistentRepresentation Update(long sequenceNr, string persistenceId, bool isDeleted, Akka.Actor.IActorRef sender, string writerGuid); + Akka.Persistence.IPersistentRepresentation WithManifest(string manifest); + Akka.Persistence.IPersistentRepresentation WithPayload(object payload); + } + public interface ISnapshotMessage : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage { } + public interface ISnapshotRequest : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage { } + public interface ISnapshotResponse : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage { } + public interface ISnapshotter + { + long SnapshotSequenceNr { get; } + string SnapshotterId { get; } + void DeleteSnapshot(long sequenceNr); + void DeleteSnapshots(Akka.Persistence.SnapshotSelectionCriteria criteria); + void LoadSnapshot(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria, long toSequenceNr); + void SaveSnapshot(object snapshot); + } + public interface IStashOverflowStrategy { } + public interface IStashOverflowStrategyConfigurator + { + Akka.Persistence.IStashOverflowStrategy Create(Akka.Configuration.Config config); + } + public sealed class LoadSnapshot : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotRequest, System.IEquatable + { + public LoadSnapshot(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria, long toSequenceNr) { } + public Akka.Persistence.SnapshotSelectionCriteria Criteria { get; } + public string PersistenceId { get; } + public long ToSequenceNr { get; } + public bool Equals(Akka.Persistence.LoadSnapshot other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class LoadSnapshotFailed : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse + { + public LoadSnapshotFailed(System.Exception cause) { } + public System.Exception Cause { get; } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class LoadSnapshotResult : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable + { + public LoadSnapshotResult(Akka.Persistence.SelectedSnapshot snapshot, long toSequenceNr) { } + public Akka.Persistence.SelectedSnapshot Snapshot { get; } + public long ToSequenceNr { get; } + public bool Equals(Akka.Persistence.LoadSnapshotResult other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class LoopMessageSuccess : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable + { + public LoopMessageSuccess(object message, int actorInstanceId) { } + public int ActorInstanceId { get; } + public object Message { get; } + public bool Equals(Akka.Persistence.LoopMessageSuccess other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public class MaxUnconfirmedMessagesExceededException : System.Exception + { + public MaxUnconfirmedMessagesExceededException() { } + public MaxUnconfirmedMessagesExceededException(string message) { } + public MaxUnconfirmedMessagesExceededException(string message, System.Exception innerException) { } + protected MaxUnconfirmedMessagesExceededException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } + } + public class Persistence : Akka.Actor.ExtensionIdProvider + { + public Persistence() { } + public static Akka.Persistence.Persistence Instance { get; } + public override Akka.Persistence.PersistenceExtension CreateExtension(Akka.Actor.ExtendedActorSystem system) { } + public static Akka.Configuration.Config DefaultConfig() { } + } + public class PersistenceExtension : Akka.Actor.IExtension + { + public PersistenceExtension(Akka.Actor.ExtendedActorSystem system) { } + public Akka.Persistence.IStashOverflowStrategy DefaultInternalStashOverflowStrategy { get; } + public Akka.Persistence.PersistenceSettings Settings { get; } + public Akka.Persistence.Journal.EventAdapters AdaptersFor(string journalPluginId) { } + public Akka.Actor.IActorRef JournalFor(string journalPluginId) { } + public string PersistenceId(Akka.Actor.IActorRef actor) { } + public Akka.Actor.IActorRef SnapshotStoreFor(string snapshotPluginId) { } + } + public sealed class PersistenceSettings : Akka.Actor.Settings + { + public PersistenceSettings(Akka.Actor.ActorSystem system, Akka.Configuration.Config config) { } + public Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings AtLeastOnceDelivery { get; set; } + public Akka.Persistence.PersistenceSettings.InternalSettings Internal { get; } + public Akka.Persistence.PersistenceSettings.ViewSettings View { get; } + public sealed class AtLeastOnceDeliverySettings + { + public AtLeastOnceDeliverySettings(System.TimeSpan redeliverInterval, int redeliveryBurstLimit, int warnAfterNumberOfUnconfirmedAttempts, int maxUnconfirmedMessages) { } + public AtLeastOnceDeliverySettings(Akka.Configuration.Config config) { } + public int MaxUnconfirmedMessages { get; } + public System.TimeSpan RedeliverInterval { get; } + public int RedeliveryBurstLimit { get; } + public int WarnAfterNumberOfUnconfirmedAttempts { get; } + public Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings WithMaxUnconfirmedMessages(int maxUnconfirmedMessages) { } + public Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings WithRedeliverInterval(System.TimeSpan redeliverInterval) { } + public Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings WithRedeliveryBurstLimit(int redeliveryBurstLimit) { } + public Akka.Persistence.PersistenceSettings.AtLeastOnceDeliverySettings WithUnconfirmedAttemptsToWarn(int unconfirmedAttemptsToWarn) { } + } + public sealed class InternalSettings + { + public InternalSettings(Akka.Configuration.Config config) { } + public bool PublishConfirmations { get; } + public bool PublishPluginCommands { get; } + } + public sealed class ViewSettings + { + public ViewSettings(Akka.Configuration.Config config) { } + public bool AutoUpdate { get; } + public System.TimeSpan AutoUpdateInterval { get; } + public long AutoUpdateReplayMax { get; } + } + } + [Akka.Annotations.InternalApiAttribute()] + public class Persistent : Akka.Persistence.IPersistentRepresentation, Akka.Persistence.Serialization.IMessage, System.IEquatable + { + public Persistent(object payload, long sequenceNr = 0, string persistenceId = null, string manifest = null, bool isDeleted = False, Akka.Actor.IActorRef sender = null, string writerGuid = null) { } + public bool IsDeleted { get; } + public string Manifest { get; } + public object Payload { get; } + public string PersistenceId { get; } + public Akka.Actor.IActorRef Sender { get; } + public long SequenceNr { get; } + public static string Undefined { get; } + public string WriterGuid { get; } + public bool Equals(Akka.Persistence.IPersistentRepresentation other) { } + public override bool Equals(object obj) { } + public bool Equals(Akka.Persistence.Persistent other) { } + public override int GetHashCode() { } + public override string ToString() { } + public Akka.Persistence.IPersistentRepresentation Update(long sequenceNr, string persistenceId, bool isDeleted, Akka.Actor.IActorRef sender, string writerGuid) { } + public Akka.Persistence.IPersistentRepresentation WithManifest(string manifest) { } + public Akka.Persistence.IPersistentRepresentation WithPayload(object payload) { } + } + public abstract class PersistentActor : Akka.Persistence.Eventsourced + { + protected PersistentActor() { } + protected override bool Receive(object message) { } + } + [System.ObsoleteAttribute("PersistentView was deprecated and will be removed in the next major version [1.3." + + "0]")] + public abstract class PersistentView : Akka.Actor.ActorBase, Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Dispatch.IRequiresMessageQueue, Akka.Persistence.IPersistenceRecovery, Akka.Persistence.IPersistentIdentity, Akka.Persistence.ISnapshotter + { + protected readonly Akka.Persistence.PersistenceExtension Extension; + protected PersistentView() { } + public virtual System.TimeSpan AutoUpdateInterval { get; } + public virtual long AutoUpdateReplayMax { get; } + public virtual bool IsAutoUpdate { get; } + public bool IsPersistent { get; } + public bool IsRecovering { get; } + public bool IsRecoveryFinished { get; } + public Akka.Actor.IActorRef Journal { get; } + public string JournalPluginId { get; set; } + public long LastSequenceNr { get; } + public abstract string PersistenceId { get; } + public virtual Akka.Persistence.Recovery Recovery { get; } + public string SnapshotPluginId { get; set; } + public long SnapshotSequenceNr { get; } + public Akka.Actor.IActorRef SnapshotStore { get; } + public string SnapshotterId { get; } + public Akka.Actor.IStash Stash { get; set; } + public abstract string ViewId { get; } + public override void AroundPreStart() { } + protected internal override bool AroundReceive(Akka.Actor.Receive receive, object message) { } + public void DeleteSnapshot(long sequenceNr) { } + public void DeleteSnapshots(Akka.Persistence.SnapshotSelectionCriteria criteria) { } + public void LoadSnapshot(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria, long toSequenceNr) { } + protected virtual void OnReplayError(System.Exception cause) { } + protected override void PostStop() { } + protected override void PreRestart(System.Exception reason, object message) { } + protected override void PreStart() { } + public void SaveSnapshot(object snapshot) { } + protected override void Unhandled(object message) { } + } + public abstract class ReceivePersistentActor : Akka.Persistence.UntypedPersistentActor, Akka.Actor.Internal.IInitializableActor + { + protected ReceivePersistentActor() { } + protected void Become(System.Action configure) { } + protected void BecomeStacked(System.Action configure) { } + protected void Command(System.Action handler, System.Predicate shouldHandle = null) { } + protected void Command(System.Predicate shouldHandle, System.Action handler) { } + protected void Command(System.Type messageType, System.Action handler, System.Predicate shouldHandle = null) { } + protected void Command(System.Type messageType, System.Predicate shouldHandle, System.Action handler) { } + protected void Command(System.Func handler) { } + protected void Command(System.Type messageType, System.Func handler) { } + protected void Command(System.Action handler) { } + protected void CommandAny(System.Action handler) { } + protected void CommandAnyAsync(System.Func handler) { } + protected void CommandAsync(System.Func handler, System.Predicate shouldHandle = null) { } + protected void CommandAsync(System.Predicate shouldHandle, System.Func handler) { } + protected void CommandAsync(System.Type messageType, System.Func handler, System.Predicate shouldHandle = null) { } + protected void CommandAsync(System.Type messageType, System.Predicate shouldHandle, System.Func handler) { } + protected virtual void OnCommand(object message) { } + protected virtual void OnRecover(object message) { } + protected void Recover(System.Action handler, System.Predicate shouldHandle = null) { } + protected void Recover(System.Predicate shouldHandle, System.Action handler) { } + protected void Recover(System.Type messageType, System.Action handler, System.Predicate shouldHandle = null) { } + protected void Recover(System.Type messageType, System.Predicate shouldHandle, System.Action handler) { } + protected void Recover(System.Func handler) { } + protected void Recover(System.Type messageType, System.Func handler) { } + protected void RecoverAny(System.Action handler) { } + } + public sealed class Recovery + { + public Recovery() { } + public Recovery(Akka.Persistence.SnapshotSelectionCriteria fromSnapshot) { } + public Recovery(Akka.Persistence.SnapshotSelectionCriteria fromSnapshot, long toSequenceNr) { } + public Recovery(Akka.Persistence.SnapshotSelectionCriteria fromSnapshot = null, long toSequenceNr = 9223372036854775807, long replayMax = 9223372036854775807) { } + public static Akka.Persistence.Recovery Default { get; } + public Akka.Persistence.SnapshotSelectionCriteria FromSnapshot { get; } + public static Akka.Persistence.Recovery None { get; } + public long ReplayMax { get; } + public long ToSequenceNr { get; } + } + public sealed class RecoveryCompleted + { + public static readonly Akka.Persistence.RecoveryCompleted Instance; + public override bool Equals(object obj) { } + public override int GetHashCode() { } + } + public sealed class RecoverySuccess : Akka.Actor.INoSerializationVerificationNeeded, Akka.Event.IDeadLetterSuppression, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable + { + public RecoverySuccess(long highestSequenceNr) { } + public long HighestSequenceNr { get; } + public bool Equals(Akka.Persistence.RecoverySuccess other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class RecoveryTick + { + public RecoveryTick(bool snapshot) { } + public bool Snapshot { get; } + } + public sealed class RecoveryTimedOutException : Akka.Actor.AkkaException + { + public RecoveryTimedOutException() { } + public RecoveryTimedOutException(string message, System.Exception cause = null) { } + public RecoveryTimedOutException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } + } + public sealed class ReplayedMessage : Akka.Actor.INoSerializationVerificationNeeded, Akka.Event.IDeadLetterSuppression, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable + { + public ReplayedMessage(Akka.Persistence.IPersistentRepresentation persistent) { } + public Akka.Persistence.IPersistentRepresentation Persistent { get; } + public bool Equals(Akka.Persistence.ReplayedMessage other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class ReplayMessages : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalRequest, Akka.Persistence.IPersistenceMessage, System.IEquatable + { + public ReplayMessages(long fromSequenceNr, long toSequenceNr, long max, string persistenceId, Akka.Actor.IActorRef persistentActor) { } + public long FromSequenceNr { get; } + public long Max { get; } + public string PersistenceId { get; } + public Akka.Actor.IActorRef PersistentActor { get; } + public long ToSequenceNr { get; } + public bool Equals(Akka.Persistence.ReplayMessages other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class ReplayMessagesFailure : Akka.Actor.INoSerializationVerificationNeeded, Akka.Event.IDeadLetterSuppression, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable + { + public ReplayMessagesFailure(System.Exception cause) { } + public System.Exception Cause { get; } + public bool Equals(Akka.Persistence.ReplayMessagesFailure other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class ReplyToStrategy : Akka.Persistence.IStashOverflowStrategy + { + public ReplyToStrategy(object response) { } + public object Response { get; } + } + public sealed class SaveSnapshot : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotRequest, System.IEquatable + { + public SaveSnapshot(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } + public Akka.Persistence.SnapshotMetadata Metadata { get; } + public object Snapshot { get; } + public bool Equals(Akka.Persistence.SaveSnapshot other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class SaveSnapshotFailure : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable + { + public SaveSnapshotFailure(Akka.Persistence.SnapshotMetadata metadata, System.Exception cause) { } + public System.Exception Cause { get; } + public Akka.Persistence.SnapshotMetadata Metadata { get; } + public bool Equals(Akka.Persistence.SaveSnapshotFailure other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class SaveSnapshotSuccess : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IPersistenceMessage, Akka.Persistence.ISnapshotMessage, Akka.Persistence.ISnapshotResponse, System.IEquatable + { + public SaveSnapshotSuccess(Akka.Persistence.SnapshotMetadata metadata) { } + public Akka.Persistence.SnapshotMetadata Metadata { get; } + public bool Equals(Akka.Persistence.SaveSnapshotSuccess other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + [System.ObsoleteAttribute("PersistentView was deprecated and will be removed in the next major version [1.3." + + "0]")] + public sealed class ScheduledUpdate + { + public ScheduledUpdate(long replayMax) { } + public long ReplayMax { get; } + } + public sealed class SelectedSnapshot : System.IEquatable + { + public SelectedSnapshot(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } + public Akka.Persistence.SnapshotMetadata Metadata { get; } + public object Snapshot { get; } + public bool Equals(Akka.Persistence.SelectedSnapshot other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class SnapshotMetadata : System.IEquatable + { + public static System.DateTime TimestampNotSpecified; + public SnapshotMetadata(string persistenceId, long sequenceNr) { } + [Newtonsoft.Json.JsonConstructorAttribute()] + public SnapshotMetadata(string persistenceId, long sequenceNr, System.DateTime timestamp) { } + public static System.Collections.Generic.IComparer Comparer { get; } + public string PersistenceId { get; } + public long SequenceNr { get; } + public System.DateTime Timestamp { get; } + public override bool Equals(object obj) { } + public bool Equals(Akka.Persistence.SnapshotMetadata other) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class SnapshotOffer : System.IEquatable + { + public SnapshotOffer(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } + public Akka.Persistence.SnapshotMetadata Metadata { get; } + public object Snapshot { get; } + public bool Equals(Akka.Persistence.SnapshotOffer other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class SnapshotSelectionCriteria : System.IEquatable + { + [Newtonsoft.Json.JsonConstructorAttribute()] + public SnapshotSelectionCriteria(long maxSequenceNr, System.DateTime maxTimeStamp, long minSequenceNr = 0, System.Nullable minTimestamp = null) { } + public SnapshotSelectionCriteria(long maxSequenceNr) { } + public static Akka.Persistence.SnapshotSelectionCriteria Latest { get; } + public long MaxSequenceNr { get; } + public System.DateTime MaxTimeStamp { get; } + public long MinSequenceNr { get; } + public System.Nullable MinTimestamp { get; } + public static Akka.Persistence.SnapshotSelectionCriteria None { get; } + public bool Equals(Akka.Persistence.SnapshotSelectionCriteria other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class StashingHandlerInvocation : Akka.Persistence.IPendingHandlerInvocation + { + public StashingHandlerInvocation(object evt, System.Action handler) { } + public object Event { get; } + public System.Action Handler { get; } + } + public sealed class ThrowExceptionConfigurator : Akka.Persistence.IStashOverflowStrategyConfigurator + { + public ThrowExceptionConfigurator() { } + public Akka.Persistence.IStashOverflowStrategy Create(Akka.Configuration.Config config) { } + } + public class ThrowOverflowExceptionStrategy : Akka.Persistence.IStashOverflowStrategy + { + public static Akka.Persistence.ThrowOverflowExceptionStrategy Instance { get; } + } + public sealed class UnconfirmedDelivery : System.IEquatable + { + public UnconfirmedDelivery(long deliveryId, Akka.Actor.ActorPath destination, object message) { } + public long DeliveryId { get; } + public Akka.Actor.ActorPath Destination { get; } + public object Message { get; } + public bool Equals(Akka.Persistence.UnconfirmedDelivery other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class UnconfirmedWarning : System.IEquatable + { + public UnconfirmedWarning(Akka.Persistence.UnconfirmedDelivery[] unconfirmedDeliveries) { } + public Akka.Persistence.UnconfirmedDelivery[] UnconfirmedDeliveries { get; } + public bool Equals(Akka.Persistence.UnconfirmedWarning other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public abstract class UntypedPersistentActor : Akka.Persistence.Eventsourced + { + protected UntypedPersistentActor() { } + protected static Akka.Actor.IUntypedActorContext Context { get; } + protected void Become(Akka.Actor.UntypedReceive receive) { } + protected void BecomeStacked(Akka.Actor.UntypedReceive receive) { } + protected abstract void OnCommand(object message); + protected abstract void OnRecover(object message); + protected override bool Receive(object message) { } + protected virtual bool ReceiveCommand(object message) { } + protected virtual bool ReceiveRecover(object message) { } + } + [System.ObsoleteAttribute("PersistentView was deprecated and will be removed in the next major version [1.3." + + "0]")] + public sealed class Update + { + public Update() { } + public Update(bool isAwait) { } + [Newtonsoft.Json.JsonConstructorAttribute()] + public Update(bool isAwait, long replayMax) { } + public bool IsAwait { get; } + public long ReplayMax { get; } + } + public sealed class WriteMessageFailure : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable + { + public WriteMessageFailure(Akka.Persistence.IPersistentRepresentation persistent, System.Exception cause, int actorInstanceId) { } + public int ActorInstanceId { get; } + public System.Exception Cause { get; } + public Akka.Persistence.IPersistentRepresentation Persistent { get; } + public bool Equals(Akka.Persistence.WriteMessageFailure other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class WriteMessageRejected : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable + { + public WriteMessageRejected(Akka.Persistence.IPersistentRepresentation persistent, System.Exception cause, int actorInstanceId) { } + public int ActorInstanceId { get; } + public System.Exception Cause { get; } + public Akka.Persistence.IPersistentRepresentation Persistent { get; } + public bool Equals(Akka.Persistence.WriteMessageRejected other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class WriteMessages : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalRequest, Akka.Persistence.IPersistenceMessage, System.IEquatable + { + public WriteMessages(System.Collections.Generic.IEnumerable messages, Akka.Actor.IActorRef persistentActor, int actorInstanceId) { } + public int ActorInstanceId { get; } + public System.Collections.Generic.IEnumerable Messages { get; } + public Akka.Actor.IActorRef PersistentActor { get; } + public bool Equals(Akka.Persistence.WriteMessages other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class WriteMessagesFailed : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable + { + public WriteMessagesFailed(System.Exception cause) { } + public System.Exception Cause { get; } + public bool Equals(Akka.Persistence.WriteMessagesFailed other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } + public sealed class WriteMessagesSuccessful : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage + { + public static Akka.Persistence.WriteMessagesSuccessful Instance { get; } + } + public sealed class WriteMessageSuccess : Akka.Actor.INoSerializationVerificationNeeded, Akka.Persistence.IJournalMessage, Akka.Persistence.IJournalResponse, Akka.Persistence.IPersistenceMessage, System.IEquatable + { + public WriteMessageSuccess(Akka.Persistence.IPersistentRepresentation persistent, int actorInstanceId) { } + public int ActorInstanceId { get; } + public Akka.Persistence.IPersistentRepresentation Persistent { get; } + public bool Equals(Akka.Persistence.WriteMessageSuccess other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } + } +} +namespace Akka.Persistence.Fsm +{ + public class static PersistentFSM + { + public interface IFsmState + { + string Identifier { get; } + } + public class PersistentFSMSnapshot : Akka.Persistence.Serialization.IMessage + { + public PersistentFSMSnapshot(string stateIdentifier, TD data, System.Nullable timeout) { } + public TD Data { get; } + public string StateIdentifier { get; } + public System.Nullable Timeout { get; } + protected bool Equals(Akka.Persistence.Fsm.PersistentFSM.PersistentFSMSnapshot other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + } + public class State + { + public State(TS stateName, TD stateData, System.Nullable timeout = null, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.IReadOnlyList replies = null, System.Collections.Generic.IReadOnlyList domainEvents = null, System.Action afterTransitionDo = null, bool notifies = True) { } + public System.Action AfterTransitionDo { get; } + public System.Collections.Generic.IReadOnlyList DomainEvents { get; } + public System.Collections.Generic.IReadOnlyList Replies { get; set; } + public TD StateData { get; } + public TS StateName { get; } + public Akka.Actor.FSMBase.Reason StopReason { get; } + public System.Nullable Timeout { get; } + public Akka.Persistence.Fsm.PersistentFSM.State AndThen(System.Action handler) { } + public Akka.Persistence.Fsm.PersistentFSM.State Applying(params TE[] events) { } + public Akka.Persistence.Fsm.PersistentFSM.State ForMax(System.TimeSpan timeout) { } + public Akka.Persistence.Fsm.PersistentFSM.State Replying(object replyValue) { } + public override string ToString() { } + public Akka.Persistence.Fsm.PersistentFSM.State Using(TD nextStateData) { } + } + public class StateChangeEvent : Akka.Persistence.Serialization.IMessage + { + public StateChangeEvent(string stateIdentifier, System.Nullable timeout) { } + public string StateIdentifier { get; } + public System.Nullable Timeout { get; } + } + } + public abstract class PersistentFSM : Akka.Persistence.Fsm.PersistentFSMBase + where TState : Akka.Persistence.Fsm.PersistentFSM.IFsmState + { + protected PersistentFSM() { } + protected abstract TData ApplyEvent(TEvent domainEvent, TData currentData); + protected override void ApplyState(Akka.Persistence.Fsm.PersistentFSM.State nextState) { } + protected virtual void OnRecoveryCompleted() { } + protected override bool ReceiveRecover(object message) { } + public void SaveStateSnapshot() { } + } + public abstract class PersistentFSMBase : Akka.Persistence.PersistentActor, Akka.Routing.IListeners + { + protected PersistentFSMBase() { } + public Akka.Routing.ListenerSupport Listeners { get; } + public TData NextStateData { get; } + public TData StateData { get; } + public TState StateName { get; } + protected System.Collections.Generic.IEnumerable StateNames { get; } + protected virtual void ApplyState(Akka.Persistence.Fsm.PersistentFSM.State nextState) { } + public void CancelTimer(string name) { } + public Akka.Persistence.Fsm.PersistentFSM.State GoTo(TState nextStateName) { } + public bool IsTimerActive(string name) { } + protected virtual void LogTermination(Akka.Actor.FSMBase.Reason reason) { } + public void OnTermination(System.Action> terminationHandler) { } + public void OnTransition(Akka.Persistence.Fsm.PersistentFSMBase.TransitionHandler transitionHandler) { } + protected override void PostStop() { } + protected override bool ReceiveCommand(object message) { } + public void SetStateTimeout(TState state, System.Nullable timeout) { } + public void SetTimer(string name, object msg, System.TimeSpan timeout, bool repeat = False) { } + public void StartWith(TState stateName, TData stateData, System.Nullable timeout = null) { } + public Akka.Persistence.Fsm.PersistentFSM.State Stay() { } + public Akka.Persistence.Fsm.PersistentFSM.State Stop() { } + public Akka.Persistence.Fsm.PersistentFSM.State Stop(Akka.Actor.FSMBase.Reason reason) { } + public Akka.Persistence.Fsm.PersistentFSM.State Stop(Akka.Actor.FSMBase.Reason reason, TData stateData) { } + public Akka.Persistence.Fsm.PersistentFSMBase.TransformHelper Transform(Akka.Persistence.Fsm.PersistentFSMBase.StateFunction func) { } + public void When(TState stateName, Akka.Persistence.Fsm.PersistentFSMBase.StateFunction func, System.Nullable timeout = null) { } + public void WhenUnhandled(Akka.Persistence.Fsm.PersistentFSMBase.StateFunction stateFunction) { } + public delegate Akka.Persistence.Fsm.PersistentFSM.State StateFunction(Akka.Actor.FSMBase.Event fsmEvent, Akka.Persistence.Fsm.PersistentFSM.State state = null); + public sealed class TransformHelper + { + public TransformHelper(Akka.Persistence.Fsm.PersistentFSMBase.StateFunction func) { } + public Akka.Persistence.Fsm.PersistentFSMBase.StateFunction Func { get; } + public Akka.Persistence.Fsm.PersistentFSMBase.StateFunction Using(System.Func, Akka.Persistence.Fsm.PersistentFSM.State> andThen) { } + } + public delegate void TransitionHandler(TState initialState, TState nextState); + } +} +namespace Akka.Persistence.Journal +{ + public class AsyncReplayTimeoutException : Akka.Actor.AkkaException + { + public AsyncReplayTimeoutException() { } + public AsyncReplayTimeoutException(string message) { } + protected AsyncReplayTimeoutException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } + } + public abstract class AsyncWriteJournal : Akka.Persistence.Journal.WriteJournalBase, Akka.Persistence.Journal.IAsyncRecovery + { + protected readonly bool CanPublish; + protected AsyncWriteJournal() { } + protected abstract System.Threading.Tasks.Task DeleteMessagesToAsync(string persistenceId, long toSequenceNr); + public abstract System.Threading.Tasks.Task ReadHighestSequenceNrAsync(string persistenceId, long fromSequenceNr); + protected virtual bool Receive(object message) { } + protected virtual bool ReceivePluginInternal(object message) { } + protected bool ReceiveWriteJournal(object message) { } + public abstract System.Threading.Tasks.Task ReplayMessagesAsync(Akka.Actor.IActorContext context, string persistenceId, long fromSequenceNr, long toSequenceNr, long max, System.Action recoveryCallback); + protected System.Exception TryUnwrapException(System.Exception e) { } + protected abstract System.Threading.Tasks.Task> WriteMessagesAsync(System.Collections.Generic.IEnumerable messages); + } + public abstract class AsyncWriteProxy : Akka.Persistence.Journal.AsyncWriteJournal, Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Dispatch.IRequiresMessageQueue + { + protected AsyncWriteProxy() { } + public Akka.Actor.IStash Stash { get; set; } + public abstract System.TimeSpan Timeout { get; } + public override void AroundPreStart() { } + protected internal override bool AroundReceive(Akka.Actor.Receive receive, object message) { } + protected override System.Threading.Tasks.Task DeleteMessagesToAsync(string persistenceId, long toSequenceNr) { } + public override System.Threading.Tasks.Task ReadHighestSequenceNrAsync(string persistenceId, long fromSequenceNr) { } + public override System.Threading.Tasks.Task ReplayMessagesAsync(Akka.Actor.IActorContext context, string persistenceId, long fromSequenceNr, long toSequenceNr, long max, System.Action recoveryCallback) { } + protected override System.Threading.Tasks.Task> WriteMessagesAsync(System.Collections.Generic.IEnumerable messages) { } + public class InitTimeout + { + public static Akka.Persistence.Journal.AsyncWriteProxy.InitTimeout Instance { get; } + } + } + public class static AsyncWriteTarget + { + public sealed class DeleteMessagesTo : System.IEquatable + { + public DeleteMessagesTo(string persistenceId, long toSequenceNr) { } + public string PersistenceId { get; } + public long ToSequenceNr { get; } + public bool Equals(Akka.Persistence.Journal.AsyncWriteTarget.DeleteMessagesTo other) { } + } + public sealed class ReplayFailure + { + public ReplayFailure(System.Exception cause) { } + public System.Exception Cause { get; } + } + public sealed class ReplayMessages : System.IEquatable + { + public ReplayMessages(string persistenceId, long fromSequenceNr, long toSequenceNr, long max) { } + public long FromSequenceNr { get; } + public long Max { get; } + public string PersistenceId { get; } + public long ToSequenceNr { get; } + public bool Equals(Akka.Persistence.Journal.AsyncWriteTarget.ReplayMessages other) { } + } + public sealed class ReplaySuccess : System.IEquatable + { + public ReplaySuccess(long highestSequenceNr) { } + public long HighestSequenceNr { get; } + public bool Equals(Akka.Persistence.Journal.AsyncWriteTarget.ReplaySuccess other) { } + } + public sealed class WriteMessages + { + public WriteMessages(System.Collections.Generic.IEnumerable messages) { } + public Akka.Persistence.AtomicWrite[] Messages { get; } + } + } + public sealed class CombinedReadEventAdapter : Akka.Persistence.Journal.IEventAdapter, Akka.Persistence.Journal.IReadEventAdapter, Akka.Persistence.Journal.IWriteEventAdapter + { + public CombinedReadEventAdapter(System.Collections.Generic.IEnumerable adapters) { } + public System.Collections.Generic.IEnumerable Adapters { get; } + public Akka.Persistence.Journal.IEventSequence FromJournal(object evt, string manifest) { } + public string Manifest(object evt) { } + public object ToJournal(object evt) { } + } + public sealed class EmptyEventSequence : Akka.Persistence.Journal.IEmptyEventSequence, Akka.Persistence.Journal.IEventSequence, System.IEquatable + { + public static readonly Akka.Persistence.Journal.EmptyEventSequence Instance; + public System.Collections.Generic.IEnumerable Events { get; } + public bool Equals(Akka.Persistence.Journal.IEventSequence other) { } + public override bool Equals(object obj) { } + } + public class EventAdapters + { + protected EventAdapters(System.Collections.Concurrent.ConcurrentDictionary map, System.Collections.Generic.IEnumerable> bindings, Akka.Event.ILoggingAdapter log) { } + public static Akka.Persistence.Journal.EventAdapters Create(Akka.Actor.ExtendedActorSystem system, Akka.Configuration.Config config) { } + public Akka.Persistence.Journal.IEventAdapter Get() { } + public virtual Akka.Persistence.Journal.IEventAdapter Get(System.Type type) { } + } + public class static EventSequence + { + public static Akka.Persistence.Journal.IEventSequence Empty; + public static Akka.Persistence.Journal.IEventSequence Create(params object[] events) { } + public static Akka.Persistence.Journal.IEventSequence Create(System.Collections.Generic.IEnumerable events) { } + public static Akka.Persistence.Journal.IEventSequence Single(object e) { } + } + public class EventSequence : Akka.Persistence.Journal.IEventSequence, System.IEquatable + { + public EventSequence(System.Collections.Generic.IEnumerable events) { } + public System.Collections.Generic.IEnumerable Events { get; } + public bool Equals(Akka.Persistence.Journal.IEventSequence other) { } + public override bool Equals(object obj) { } + } + public interface IAsyncRecovery + { + System.Threading.Tasks.Task ReadHighestSequenceNrAsync(string persistenceId, long fromSequenceNr); + System.Threading.Tasks.Task ReplayMessagesAsync(Akka.Actor.IActorContext context, string persistenceId, long fromSequenceNr, long toSequenceNr, long max, System.Action recoveryCallback); + } + public sealed class IdentityEventAdapter : Akka.Persistence.Journal.IEventAdapter, Akka.Persistence.Journal.IReadEventAdapter, Akka.Persistence.Journal.IWriteEventAdapter + { + public static Akka.Persistence.Journal.IdentityEventAdapter Instance { get; } + public Akka.Persistence.Journal.IEventSequence FromJournal(object evt, string manifest) { } + public string Manifest(object evt) { } + public object ToJournal(object evt) { } + } + public interface IEmptyEventSequence : Akka.Persistence.Journal.IEventSequence { } + public interface IEventAdapter : Akka.Persistence.Journal.IReadEventAdapter, Akka.Persistence.Journal.IWriteEventAdapter { } + public interface IEventSequence + { + System.Collections.Generic.IEnumerable Events { get; } + } + public interface IMemoryMessages + { + System.Collections.Generic.IDictionary> Add(Akka.Persistence.IPersistentRepresentation persistent); + System.Collections.Generic.IDictionary> Delete(string pid, long seqNr); + long HighestSequenceNr(string pid); + System.Collections.Generic.IEnumerable Read(string pid, long fromSeqNr, long toSeqNr, long max); + System.Collections.Generic.IDictionary> Update(string pid, long seqNr, System.Func updater); + } + public interface IReadEventAdapter + { + Akka.Persistence.Journal.IEventSequence FromJournal(object evt, string manifest); + } + public interface IWriteEventAdapter + { + string Manifest(object evt); + object ToJournal(object evt); + } + public class MemoryJournal : Akka.Persistence.Journal.AsyncWriteJournal + { + public MemoryJournal() { } + protected virtual System.Collections.Concurrent.ConcurrentDictionary> Messages { get; } + public System.Collections.Generic.IDictionary> Add(Akka.Persistence.IPersistentRepresentation persistent) { } + public System.Collections.Generic.IDictionary> Delete(string pid, long seqNr) { } + protected override System.Threading.Tasks.Task DeleteMessagesToAsync(string persistenceId, long toSequenceNr) { } + public long HighestSequenceNr(string pid) { } + public System.Collections.Generic.IEnumerable Read(string pid, long fromSeqNr, long toSeqNr, long max) { } + public override System.Threading.Tasks.Task ReadHighestSequenceNrAsync(string persistenceId, long fromSequenceNr) { } + public override System.Threading.Tasks.Task ReplayMessagesAsync(Akka.Actor.IActorContext context, string persistenceId, long fromSequenceNr, long toSequenceNr, long max, System.Action recoveryCallback) { } + public System.Collections.Generic.IDictionary> Update(string pid, long seqNr, System.Func updater) { } + protected override System.Threading.Tasks.Task> WriteMessagesAsync(System.Collections.Generic.IEnumerable messages) { } + } + public class PersistencePluginProxy : Akka.Actor.ActorBase, Akka.Actor.IActorStash, Akka.Actor.IWithUnboundedStash, Akka.Dispatch.IRequiresMessageQueue + { + public PersistencePluginProxy(Akka.Configuration.Config config) { } + public Akka.Actor.IStash Stash { get; set; } + protected override void PreStart() { } + protected override bool Receive(object message) { } + public static void SetTargetLocation(Akka.Actor.ActorSystem system, Akka.Actor.Address address) { } + public static void Start(Akka.Actor.ActorSystem system) { } + public sealed class TargetLocation + { + public TargetLocation(Akka.Actor.Address address) { } + public Akka.Actor.Address Address { get; } + } + } + public class PersistencePluginProxyExtension : Akka.Actor.ExtensionIdProvider, Akka.Actor.IExtension + { + public PersistencePluginProxyExtension(Akka.Actor.ActorSystem system) { } + public override Akka.Persistence.Journal.PersistencePluginProxyExtension CreateExtension(Akka.Actor.ExtendedActorSystem system) { } + } + public class ReplayFilter : Akka.Actor.ActorBase + { + public ReplayFilter(Akka.Actor.IActorRef persistentActor, Akka.Persistence.Journal.ReplayFilterMode mode, int windowSize, int maxOldWriters, bool debugEnabled) { } + public bool DebugEnabled { get; } + public int MaxOldWriters { get; } + public Akka.Persistence.Journal.ReplayFilterMode Mode { get; } + public Akka.Actor.IActorRef PersistentActor { get; } + public int WindowSize { get; } + public static Akka.Actor.Props Props(Akka.Actor.IActorRef persistentActor, Akka.Persistence.Journal.ReplayFilterMode mode, int windowSize, int maxOldWriters, bool debugEnabled) { } + protected override bool Receive(object message) { } + } + public enum ReplayFilterMode + { + Fail = 0, + Warn = 1, + RepairByDiscardOld = 2, + Disabled = 3, + } + public sealed class SetStore + { + public readonly Akka.Actor.IActorRef Store; + public SetStore(Akka.Actor.IActorRef store) { } + } + public class SharedMemoryJournal : Akka.Persistence.Journal.MemoryJournal + { + public SharedMemoryJournal() { } + protected override System.Collections.Concurrent.ConcurrentDictionary> Messages { get; } + } + public struct SingleEventSequence : Akka.Persistence.Journal.IEventSequence, System.IEquatable + { + public SingleEventSequence(object e) { } + public System.Collections.Generic.IEnumerable Events { get; } + public bool Equals(Akka.Persistence.Journal.IEventSequence other) { } + public override bool Equals(object obj) { } + } + public struct Tagged + { + public Tagged(object payload, System.Collections.Generic.IEnumerable tags) { } + public Tagged(object payload, System.Collections.Immutable.IImmutableSet tags) { } + public object Payload { get; } + public System.Collections.Immutable.IImmutableSet Tags { get; } + } + public abstract class WriteJournalBase : Akka.Actor.ActorBase + { + protected WriteJournalBase() { } + [Akka.Annotations.InternalApiAttribute()] + protected System.Collections.Generic.IEnumerable AdaptFromJournal(Akka.Persistence.IPersistentRepresentation representation) { } + protected Akka.Persistence.IPersistentRepresentation AdaptToJournal(Akka.Persistence.IPersistentRepresentation representation) { } + protected System.Collections.Generic.IEnumerable PreparePersistentBatch(System.Collections.Generic.IEnumerable resequenceables) { } + } +} +namespace Akka.Persistence.Serialization +{ + public interface IMessage { } + public sealed class PersistenceMessageSerializer : Akka.Serialization.Serializer + { + public PersistenceMessageSerializer(Akka.Actor.ExtendedActorSystem system) { } + public override bool IncludeManifest { get; } + public override object FromBinary(byte[] bytes, System.Type type) { } + public override byte[] ToBinary(object obj) { } + } + public class PersistenceSnapshotSerializer : Akka.Serialization.Serializer + { + public PersistenceSnapshotSerializer(Akka.Actor.ExtendedActorSystem system) { } + public override bool IncludeManifest { get; } + public override object FromBinary(byte[] bytes, System.Type type) { } + public override byte[] ToBinary(object obj) { } + } + public sealed class Snapshot + { + public Snapshot(object data) { } + public object Data { get; } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + } +} +namespace Akka.Persistence.Snapshot +{ + public class LocalSnapshotStore : Akka.Persistence.Snapshot.SnapshotStore + { + public LocalSnapshotStore() { } + protected override System.Threading.Tasks.Task DeleteAsync(Akka.Persistence.SnapshotMetadata metadata) { } + protected override async System.Threading.Tasks.Task DeleteAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } + protected System.IO.FileInfo GetSnapshotFileForWrite(Akka.Persistence.SnapshotMetadata metadata, string extension = "") { } + protected override System.Threading.Tasks.Task LoadAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } + protected override void PreStart() { } + protected override bool ReceivePluginInternal(object message) { } + protected virtual void Save(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } + protected override System.Threading.Tasks.Task SaveAsync(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } + protected void Serialize(System.IO.Stream stream, Akka.Persistence.Serialization.Snapshot snapshot) { } + protected System.IO.FileInfo WithOutputStream(Akka.Persistence.SnapshotMetadata metadata, System.Action p) { } + } + public class MemorySnapshotStore : Akka.Persistence.Snapshot.SnapshotStore + { + public MemorySnapshotStore() { } + protected virtual System.Collections.Generic.List Snapshots { get; } + protected override System.Threading.Tasks.Task DeleteAsync(Akka.Persistence.SnapshotMetadata metadata) { } + protected override System.Threading.Tasks.Task DeleteAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } + protected override System.Threading.Tasks.Task LoadAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } + protected override System.Threading.Tasks.Task SaveAsync(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } + } + public sealed class NoSnapshotStore : Akka.Persistence.Snapshot.SnapshotStore + { + public NoSnapshotStore() { } + protected override System.Threading.Tasks.Task DeleteAsync(Akka.Persistence.SnapshotMetadata metadata) { } + protected override System.Threading.Tasks.Task DeleteAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } + protected override System.Threading.Tasks.Task LoadAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria) { } + protected override System.Threading.Tasks.Task SaveAsync(Akka.Persistence.SnapshotMetadata metadata, object snapshot) { } + public class NoSnapshotStoreException : System.Exception + { + public NoSnapshotStoreException() { } + public NoSnapshotStoreException(string message) { } + public NoSnapshotStoreException(string message, System.Exception innerException) { } + protected NoSnapshotStoreException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } + } + } + public class SnapshotEntry + { + public SnapshotEntry() { } + public string Id { get; set; } + public string PersistenceId { get; set; } + public long SequenceNr { get; set; } + public object Snapshot { get; set; } + public long Timestamp { get; set; } + } + public abstract class SnapshotStore : Akka.Actor.ActorBase + { + protected SnapshotStore() { } + protected abstract System.Threading.Tasks.Task DeleteAsync(Akka.Persistence.SnapshotMetadata metadata); + protected abstract System.Threading.Tasks.Task DeleteAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria); + protected abstract System.Threading.Tasks.Task LoadAsync(string persistenceId, Akka.Persistence.SnapshotSelectionCriteria criteria); + protected virtual bool Receive(object message) { } + protected virtual bool ReceivePluginInternal(object message) { } + protected abstract System.Threading.Tasks.Task SaveAsync(Akka.Persistence.SnapshotMetadata metadata, object snapshot); + } +} \ No newline at end of file From 05fc7c74997b043bdcf72e33b88f26a8d8ae2438 Mon Sep 17 00:00:00 2001 From: Peter Shrosbree Date: Thu, 5 Jul 2018 16:18:19 -0700 Subject: [PATCH 51/70] Ignore CodeRush, GhostDoc, NDepend, VS Code files (#3535) Ignore CodeRush working files Ignore GhostDoc working files Ignore NDepend working files Ignore Visual Studio Code working files --- .gitignore | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.gitignore b/.gitignore index fc577ea2229..cea1d12f163 100644 --- a/.gitignore +++ b/.gitignore @@ -206,3 +206,17 @@ build/ /src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.received.txt launchSettings.json .idea/ + +# GhostDoc is a C# XML comment helper +*.[Gg]host[Dd]oc.xml +*.[Gg]host[Dd]oc.user.dic + +# CodeRush +.cr/ + +# Visual Studio Code +.vscode/ + +# NDepend +*.ndproj +/[Nn][Dd]epend[Oo]ut From d9523d58e4f10ad21b9079bb80a937addaa70b40 Mon Sep 17 00:00:00 2001 From: Sean Gilliam Date: Tue, 10 Jul 2018 13:18:19 -0500 Subject: [PATCH 52/70] [docs] Remove Akka.Persistence.Query warning (#3541) Closes #3485 --- docs/articles/persistence/persistence-query.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/articles/persistence/persistence-query.md b/docs/articles/persistence/persistence-query.md index b59fb67c745..f181d7e6127 100644 --- a/docs/articles/persistence/persistence-query.md +++ b/docs/articles/persistence/persistence-query.md @@ -7,9 +7,6 @@ Akka persistence query complements Persistence by providing a universal asynchro The most typical use case of persistence query is implementing the so-called query side (also known as "read side") in the popular CQRS architecture pattern - in which the writing side of the application (e.g. implemented using akka persistence) is completely separated from the "query side". Akka Persistence Query itself is not directly the query side of an application, however it can help to migrate data from the write side to the query side database. In very simple scenarios Persistence Query may be powerful enough to fulfill the query needs of your app, however we highly recommend (in the spirit of CQRS) of splitting up the write/read sides into separate datastores as the need arises. -> [!WARNING] -> This module is marked as `experimental` as of its introduction in Akka.Net 1.1.0. We will continue to improve this API based on our users’ feedback, which implies that while we try to keep incompatible changes to a minimum the binary compatibility guarantee for maintenance releases does not apply to the contents of the Akka.Persistence.Query package. - ## Design overview Akka Persistence Query is purposely designed to be a very loosely specified API. This is in order to keep the provided APIs general enough for each journal implementation to be able to expose its best features, e.g. a SQL journal can use complex SQL queries or if a journal is able to subscribe to a live event stream this should also be possible to expose the same API - a typed stream of events. From a4e6a894492931c3fb8ebef1ca0aba80152cc7f8 Mon Sep 17 00:00:00 2001 From: Josh Taylor Date: Tue, 10 Jul 2018 20:02:04 +0100 Subject: [PATCH 53/70] Edits to "Part 1: Top-level Architecture" #3433 (#3542) * Wrapped markdown to 80 columns * Adding of detail to fix #3433 and fixing spelling error --- docs/articles/intro/tutorial-1.md | 308 ++++++++++++++++++------------ 1 file changed, 184 insertions(+), 124 deletions(-) diff --git a/docs/articles/intro/tutorial-1.md b/docs/articles/intro/tutorial-1.md index 5770a67f949..5a268e4f00a 100644 --- a/docs/articles/intro/tutorial-1.md +++ b/docs/articles/intro/tutorial-1.md @@ -5,32 +5,45 @@ title: Part 1. Top-level Architecture # Part 1: Top-level Architecture -In this and the following chapters, we will build a sample Akka.NET application to introduce you to the language of -actors and how solutions can be formulated with them. It is a common hurdle for beginners to translate their project -into actors even though they don't understand what they do on the high-level. We will build the core logic of a small -application and this will serve as a guide for common patterns that will help to kickstart Akka.NET projects. - -The application we aim to write will be a simplified IoT system where devices, installed at the home of users, can report temperature data from sensors. Users will be able to query the current state of these sensors. To keep -things simple, we will not actually expose the application via HTTP or any other external API, we will, instead, concentrate only on the -core logic. However, we will write tests for the pieces of the application to get comfortable and -proficient with testing actors early on. +In this and the following chapters, we will build a sample Akka.NET application +to introduce you to the language of actors and how solutions can be formulated +with them. It is a common hurdle for beginners to translate their project into +actors even though they don't understand what they do on the high-level. We will +build the core logic of a small application and this will serve as a guide for +common patterns that will help to kickstart Akka.NET projects. + +The application we aim to write will be a simplified IoT system where devices, +installed at the home of users, can report temperature data from sensors. Users +will be able to query the current state of these sensors. To keep things simple, +we will not actually expose the application via HTTP or any other external API, +we will, instead, concentrate only on the core logic. However, we will write +tests for the pieces of the application to get comfortable and proficient with +testing actors early on. ## Our Goals for the IoT System -We will build a simple IoT application with the bare essentials to demonstrate designing an Akka.NET-based system. The application will consist of two main components: - - * **Device data collection:** This component has the responsibility to maintain a local representation of the - otherwise remote devices. The devices will be organized into device groups, grouping together sensors belonging to a home. - * **User dashboards:** This component has the responsibility to periodically collect data from the devices for a - logged in user and present the results as a report. - -For simplicity, we will only collect temperature data for the devices, but in a real application our local representations -for a remote device, which we will model as an actor, would have many more responsibilities. Among others; reading the -configuration of the device, changing the configuration, checking if the devices are unresponsive, etc. We leave -these complexities for now as they can be easily added as an exercise. - -We will also not address the means by which the remote devices communicate with the local representations (actors). Instead, -we just build an actor based API that such a network protocol could use. We will use tests for our API everywhere though. +We will build a simple IoT application with the bare essentials to demonstrate +designing an Akka.NET-based system. The application will consist of two main +components: + + * **Device data collection:** This component has the responsibility to maintain + a local representation of the otherwise remote devices. The devices will be + organized into device groups, grouping together sensors belonging to a home. + * **User dashboards:** This component has the responsibility to periodically + collect data from the devices for a logged in user and present the results as + a report. + +For simplicity, we will only collect temperature data for the devices, but in a +real application our local representations for a remote device, which we will +model as an actor, would have many more responsibilities. Among others; reading +the configuration of the device, changing the configuration, checking if the +devices are unresponsive, etc. We leave these complexities for now as they can +be easily added as an exercise. + +We will also not address the means by which the remote devices communicate with +the local representations (actors). Instead, we just build an actor based API +that such a network protocol could use. We will use tests for our API everywhere +though. The architecture of the application will look like this: @@ -38,50 +51,67 @@ The architecture of the application will look like this: ## Top Level Architecture -When writing prose, the hardest part is usually to write the first couple of sentences. There is a similar feeling -when trying to build an Akka.NET system: What should be the first actor? Where should it live? What should it do? -Fortunately, unlike with prose, there are established best practices that can guide us through these initial steps. - -When one creates an actor in Akka.NET it always belongs to a certain parent. This means that actors are always organized -into a tree. In general, creating an actor can only happen from inside another actor. This creator actor becomes the -_parent_ of the newly created _child_ actor. You might ask then, who is the parent of the _first_ actor you create? -As we have seen in the previous chapters, to create a top-level actor one must call `System.ActorOf()`. This does -not create a "freestanding" actor though, instead, it injects the corresponding actor as a child into an already -existing tree: +When writing prose, the hardest part is usually to write the first couple of +sentences. There is a similar feeling when trying to build an Akka.NET system: +What should be the first actor? Where should it live? What should it do? +Fortunately, unlike with prose, there are established best practices that can +guide us through these initial steps. + +When one creates an actor in Akka.NET it always belongs to a certain parent. +This means that actors are always organized into a tree. In general, creating an +actor can only happen from inside another actor. This 'creator' actor becomes +the _parent_ of the newly created _child_ actor. You might ask then, who is the +parent of the _first_ actor you create? To create a top-level actor one must +first initialise an _actor system_, let's refer to this as the object `System`. +This is followed by a call to `System.ActorOf()` which returns a reference to +the newly created actor. This does not create a "freestanding" actor though, +instead, it injects the corresponding actor as a child into an already existing +tree: ![box diagram of the architecture](/images/actor_top_tree.png) -As you see, creating actors from the "top" injects those actors under the path `/user/`, so for example creating -an actor named `myActor` will end up having the path `/user/myActor`. In fact, there are three already existing -actors in the system: - - - `/` the so-called _root guardian_. This is the parent of all actors in the system, and the last one to stop - when the system itself is terminated. - - `/user` the _guardian_. **This is the parent actor for all user created actors**. The name `user` should not confuse - you, it has nothing to do with the logged in user, nor user handling in general. This name really means _userspace_ - as this is the place where actors that do not access Akka.NET internals live, i.e. all the actors created by users of the Akka.NET library. Every actor you will create will have the constant path `/user/` prepended to it. +As you see, creating actors from the "top" injects those actors under the path +`/user/`, so for example creating an actor named `myActor` will end up having +the path `/user/myActor`. In fact, there are three already existing actors in +the system: + + - `/` the so-called _root guardian_. This is the parent of all actors in the + system, and the last one to stop when the system itself is terminated. + - `/user` the _guardian_. **This is the parent actor for all user created + actors**. The name `user` should not confuse you, it has nothing to do with + the logged in user, nor user handling in general. This name really means + _userspace_ as this is the place where actors that do not access Akka.NET + internals live, i.e. all the actors created by users of the Akka.NET library. + Every actor you will create will have the constant path `/user/` prepended to + it. - `/system` the _system guardian_. -The names of these built-in actors contain _guardian_ because these are _supervising_ every actor living as a child -of them, i.e. under their path. We will explain supervision in more detail, all you need to know now is that every -unhandled failure from actors bubbles up to their parent that, in turn, can decide how to handle this failure. These -predefined actors are guardians in the sense that they are the final lines of defense, where all unhandled failures +The names of these built-in actors contain _guardian_ because these are +_supervising_ every actor living as a child of them, i.e. under their path. We +will explain supervision in more detail, all you need to know now is that every +unhandled failure from actors bubbles up to their parent that, in turn, can +decide how to handle this failure. These predefined actors are guardians in the +sense that they are the final lines of defence, where all unhandled failures from user, or system, actors end up. -> Does the root guardian (the root path `/`) have a parent? As it turns out, it has. This special entity is called -> the "Bubble-Walker". This special entity is invisible for the user and only has uses internally. +> Does the root guardian (the root path `/`) have a parent? As it turns out, it +> has. This special entity is called the "Bubble-Walker". This special entity is +> invisible for the user and only has uses internally. ### Structure of an IActorRef and Paths of Actors -The easiest way to see this in action is to simply print `IActorRef` instances. In this small experiment, we print -the reference of the first actor we create and then we create a child of this actor, and print its reference. We have -already created actors with `System.ActorOf()`, which creates an actor under `/user` directly. We call this kind -of actors _top level_, even though in practice they are not on the top of the hierarchy, only on the top of the -_user defined_ hierarchy. Since in practice we usually concern ourselves about actors under `/user` this is still a -convenient terminology, and we will stick to it. +The easiest way to see this in action is to simply print `IActorRef` instances. +In this small experiment, we print the reference of the first actor we create +and then we create a child of this actor, and print its reference. We have +already created actors with `System.ActorOf()`, which creates an actor under +`/user` directly. We call this kind of actors _top level_, even though in +practice they are not on the top of the hierarchy, only on the top of the _user +defined_ hierarchy. Since in practice we usually concern ourselves about actors +under `/user` this is still a convenient terminology, and we will stick to it. -Creating a non-top-level actor is possible from any actor, by invoking `Context.ActorOf()` which has the exact same -signature as its top-level counterpart. This is how it looks like in practice: +Creating a non-top-level actor is possible from any actor, by invoking +`Context.ActorOf()` which has the exact same signature as its top-level +counterpart. This is how it looks like in practice: [!code-csharp[Main](../../examples/Tutorials/Tutorial1/ActorHierarchyExperiments.cs?name=print-refs)] [!code-csharp[Main](../../examples/Tutorials/Tutorial1/ActorHierarchyExperiments.cs?name=print-refs2)] @@ -93,42 +123,55 @@ First : Actor[akka://testSystem/user/first-actor#1053618476] Second: Actor[akka://testSystem/user/first-actor/second-actor#-1544706041] ``` -First, we notice that all of the paths start with `akka://testSystem/`. Since all actor references are valid URLs, there -is a protocol field needed, which is `akka://` in the case of actors. Then, just like on the World Wide Web, the system -is identified. In our case, this is `testSystem`, but could be any other name (if remote communication between multiple -systems is enabled this name is the hostname of the system so other systems can find it on the network). Our two actors, -as we have discussed before, live under user, and form a hierarchy: +First, we notice that all of the paths start with `akka://testSystem/`. Since +all actor references are valid URLs, there is a protocol field needed, which is +`akka://` in the case of actors. Then, just like on the World Wide Web, the +system is identified. In our case, this is `testSystem`, but could be any other +name (if remote communication between multiple systems is enabled this name is +the hostname of the system so other systems can find it on the network). Our two +actors, as we have discussed before, live under user, and form a hierarchy: - * `akka://testSystem/user/first-actor` is the first actor we created, which lives directly under the user guardian, - `/user` - * `akka://testSystem/user/first-actor/second-actor` is the second actor we created, using `Context.ActorOf`. As we - see it lives directly under the first actor. + * `akka://testSystem/user/first-actor` is the first actor we created, which + lives directly under the user guardian, `/user` + * `akka://testSystem/user/first-actor/second-actor` is the second actor we + created, using `Context.ActorOf`. As we see it lives directly under the first + actor. -The last part of the actor reference, like `#1053618476` is a unique identifier of the actor living under the path. -This is usually not something the user needs to be concerned with, and we leave the discussion of this field for later. +The last part of the actor reference, like `#1053618476` is a unique identifier +of the actor living under the path. This is usually not something the user needs +to be concerned with, and we leave the discussion of this field for later. ### Hierarchy and Lifecycle of Actors -We have so far seen that actors are organized into a **strict hierarchy**. This hierarchy consists of a predefined -upper layer of three actors (the root guardian, the user guardian, and the system guardian), thereafter the user created -top-level actors (those directly living under `/user`) and the children of those. We now understand what the hierarchy -looks like, but there are some nagging unanswered questions: _Why do we need this hierarchy? What is it used for?_ - -The first use of the hierarchy is to manage the lifecycle of actors. Actors pop into existence when created, then later, -at user requests, they are stopped. Whenever an actor is stopped, all of its children are _recursively stopped_ too. -This is a very useful property and greatly simplifies cleaning up resources and avoiding resource leaks (like open -sockets files, etc.). In fact, one of the overlooked difficulties when dealing with low-level multi-threaded code is -the lifecycle management of various concurrent resources. - -Stopping an actor can be done by calling `Context.Stop(actorRef)`. **It is considered a bad practice to stop arbitrary -actors this way**. The recommended pattern is to call `Context.Stop(self)` inside an actor to stop itself, usually as -a response to some user defined stop message or when the actor is done with its job. - -The actor API exposes many lifecycle hooks that the actor implementation can override. The most commonly used are -`PreStart()` and `PostStop()`. - - * `PreStart()` is invoked after the actor has started but before it processes its first message. - * `PostStop()` is invoked just before the actor stops. No messages are processed after this point. +We have so far seen that actors are organized into a **strict hierarchy**. This +hierarchy consists of a predefined upper layer of three actors (the root +guardian, the user guardian, and the system guardian), thereafter the user +created top-level actors (those directly living under `/user`) and the children +of those. We now understand what the hierarchy looks like, but there are some +nagging unanswered questions: _Why do we need this hierarchy? What is it used +for?_ + +The first use of the hierarchy is to manage the lifecycle of actors. Actors pop +into existence when created, then later, at user requests, they are stopped. +Whenever an actor is stopped, all of its children are _recursively stopped_ too. +This is a very useful property and greatly simplifies cleaning up resources and +avoiding resource leaks (like open sockets files, etc.). In fact, one of the +overlooked difficulties when dealing with low-level multi-threaded code is the +lifecycle management of various concurrent resources. + +Stopping an actor can be done by calling `Context.Stop(actorRef)`. **It is +considered a bad practice to stop arbitrary actors this way**. The recommended +pattern is to call `Context.Stop(self)` inside an actor to stop itself, usually +as a response to some user defined stop message or when the actor is done with +its job. + +The actor API exposes many lifecycle hooks that the actor implementation can +override. The most commonly used are `PreStart()` and `PostStop()`. + + * `PreStart()` is invoked after the actor has started but before it processes + its first message. + * `PostStop()` is invoked just before the actor stops. No messages are + processed after this point. Again, we can try out all this with a simple experiment: @@ -144,18 +187,23 @@ second stopped first stopped ``` -We see that when we stopped actor `first` it recursively stopped actor `second` and thereafter it stopped itself. -This ordering is strict, _all_ `PostStop()` hooks of the children are called before the `PostStop()` hook of the parent -is called. +We see that when we stopped actor `first` it recursively stopped actor `second` +and thereafter it stopped itself. This ordering is strict, _all_ `PostStop()` +hooks of the children are called before the `PostStop()` hook of the parent is +called. -The family of these lifecycle hooks is rich, and we recommend reading [the actor lifecycle](xref:untyped-actor-api#actor-lifecycle) section of the reference for all details. +The family of these lifecycle hooks is rich, and we recommend reading [the actor +lifecycle](xref:untyped-actor-api#actor-lifecycle) section of the reference for +all details. ### Hierarchy and Failure Handling (Supervision) -Parents and children are not only connected by their lifecycles. Whenever an actor fails (throws an exception or -an unhandled exception bubbles out from `receive`) it is temporarily suspended. The failure information is propagated -to the parent, which decides how to handle the exception caused by the child actor. The default _supervisor strategy_ is to -stop and restart the child. If you don't change the default strategy all failures result in a restart. We won't change +Parents and children are not only connected by their lifecycles. Whenever an +actor fails (throws an exception or an unhandled exception bubbles out from +`receive`) it is temporarily suspended. The failure information is propagated to +the parent, which decides how to handle the exception caused by the child actor. +The default _supervisor strategy_ is to stop and restart the child. If you don't +change the default strategy all failures result in a restart. We won't change the default strategy in this simple experiment: [!code-csharp[Main](../../examples/Tutorials/Tutorial1/ActorHierarchyExperiments.cs?name=supervise)] @@ -176,40 +224,51 @@ Cause: System.Exception: I failed! at Akka.Actor.ActorCell.Invoke(Envelope envelope) ``` -We see that after failure the actor is stopped and immediately started. We also see a log entry reporting the -exception that was handled, in this case, our test exception. In this example we use `PreStart()` and `PostStop()` hooks -which are the default to be called after and before restarts, so we cannot distinguish from inside the actor if it -was started for the first time or restarted. This is usually the right thing to do, the purpose of the restart is to -set the actor in a known-good state, which usually means a clean starting stage. **What actually happens though is -that the `PreRestart()` and `PostRestart()` methods are called which, if not overridden, by default delegate to -`PostStop()` and `PreStart()` respectively**. You can experiment with overriding these additional methods and see -how the output changes. - -For the impatient, we also recommend looking into the [supervision reference page](xref:supervision) for more in-depth -details. +We see that after failure the actor is stopped and immediately started. We also +see a log entry reporting the exception that was handled, in this case, our test +exception. In this example we use `PreStart()` and `PostStop()` hooks which are +the default to be called after and before restarts, so we cannot distinguish +from inside the actor if it was started for the first time or restarted. This is +usually the right thing to do, the purpose of the restart is to set the actor in +a known-good state, which usually means a clean starting stage. **What actually +happens though is that the `PreRestart()` and `PostRestart()` methods are called +which, if not overridden, by default delegate to `PostStop()` and `PreStart()` +respectively**. You can experiment with overriding these additional methods and +see how the output changes. + +For the impatient, we also recommend looking into the [supervision reference +page](xref:supervision) for more in-depth details. ### The First Actor -Actors are organized into a strict tree, where the lifecycle of every child is tied to the parent and where parents -are responsible for deciding the fate of failed children. At first, it might not be evident how to map our problem -to such a tree, but in practice, this is easier than it looks. All we need to do is to rewrite our architecture diagram -that contained nested boxes into a tree: +Actors are organized into a strict tree, where the lifecycle of every child is +tied to the parent and where parents are responsible for deciding the fate of +failed children. At first, it might not be evident how to map our problem to +such a tree, but in practice, this is easier than it looks. All we need to do is +to rewrite our architecture diagram that contained nested boxes into a tree: ![actor tree diagram of the architecture](/images/arch_tree_diagram.png) -In simple terms, every component manages the lifecycle of the subcomponents. No subcomponent can outlive the parent -component. This is exactly how the actor hierarchy works. Furthermore, it is desirable that a component handles the failure -of its subcomponents. Together, these two desirable properties lead to the conclusion that the "contained-in" relationship of components should be mapped to the -"children-of" relationship of actors. - -The remaining question is how to map the top-level components to actors. It might be tempting to create the actors -representing the main components as top-level actors. We instead, recommend creating an explicit component that -represents the whole application. In other words, we will have a single top-level actor in our actor system and have -the main components as children of this actor. - -The first actor happens to be rather simple now, as we have not implemented any of the components yet. What is new -is that we have dropped using `Console.WriteLine()` and instead use `ILoggingAdapter` which allows us to use the -logging facility built into Akka.NET directly. Furthermore, we are using a recommended creational pattern for actors; define a static `Props()` method in the the actor: +In simple terms, every component manages the lifecycle of the subcomponents. No +subcomponent can outlive the parent component. This is exactly how the actor +hierarchy works. Furthermore, it is desirable that a component handles the +failure of its subcomponents. Together, these two desirable properties lead to +the conclusion that the "contained-in" relationship of components should be +mapped to the "children-of" relationship of actors. + +The remaining question is how to map the top-level components to actors. It +might be tempting to create the actors representing the main components as +top-level actors. We instead, recommend creating an explicit component that +represents the whole application. In other words, we will have a single +top-level actor in our actor system and have the main components as children of +this actor. + +The first actor happens to be rather simple now, as we have not implemented any +of the components yet. What is new is that we have dropped using +`Console.WriteLine()` and instead use `ILoggingAdapter` which allows us to use +the logging facility built into Akka.NET directly. Furthermore, we are using a +recommended creational pattern for actors; define a static `Props()` method in +the the actor: [!code-csharp[Main](../../examples/Tutorials/Tutorial1/IotSupervisor.cs?name=iot-supervisor)] @@ -217,7 +276,8 @@ All we need now is to tie this up with a class with the `main` entry point: [!code-csharp[Main](../../examples/Tutorials/Tutorial1/IotApp.cs?name=iot-app)] -This application does very little for now, but we have the first actor in place and we are ready to extend it further. +This application does very little for now, but we have the first actor in place +and we are ready to extend it further. ## What is next? From 14cdefafd6bdce8be25747536f025b21dd4193bc Mon Sep 17 00:00:00 2001 From: zbynek001 Date: Sun, 15 Jul 2018 02:45:36 +0200 Subject: [PATCH 54/70] Check remembered entities before remembering entity Messages that come through for an entity before StartEntity has been processed for that entity caused redundant persistence of the entity. --- .../Akka.Cluster.Sharding/PersistentShard.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/PersistentShard.cs b/src/contrib/cluster/Akka.Cluster.Sharding/PersistentShard.cs index 932bbc63b73..0b5388c21a4 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/PersistentShard.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/PersistentShard.cs @@ -198,9 +198,20 @@ public void DeliverTo(string id, object message, object payload, IActorRef sende var child = Context.Child(name); if (Equals(child, ActorRefs.Nobody)) { - // Note; we only do this if remembering, otherwise the buffer is an overhead - MessageBuffers = MessageBuffers.SetItem(id, ImmutableList>.Empty.Add(Tuple.Create(message, sender))); - ProcessChange(new Shard.EntityStarted(id), this.SendMessageBuffer); + if (State.Entries.Contains(id)) + { + if (MessageBuffers.ContainsKey(id)) + { + throw new InvalidOperationException($"Message buffers contains id [{id}]."); + } + this.GetEntity(id).Tell(payload, sender); + } + else + { + // Note; we only do this if remembering, otherwise the buffer is an overhead + MessageBuffers = MessageBuffers.SetItem(id, ImmutableList>.Empty.Add(Tuple.Create(message, sender))); + ProcessChange(new Shard.EntityStarted(id), this.SendMessageBuffer); + } } else child.Tell(payload, sender); From c8fa67cf5484017584d39a189dc61b42b9eb33bf Mon Sep 17 00:00:00 2001 From: Ismael Hamed Date: Tue, 17 Jul 2018 20:50:35 +0200 Subject: [PATCH 55/70] Optimized recovery (#3549) --- .../OptimizedRecoverySpec.cs | 145 ++++++++++++++++++ .../Journal/AsyncWriteJournal.cs | 2 +- src/core/Akka.Persistence/PersistentActor.cs | 4 + .../Snapshot/SnapshotStore.cs | 23 ++- 4 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 src/core/Akka.Persistence.Tests/OptimizedRecoverySpec.cs diff --git a/src/core/Akka.Persistence.Tests/OptimizedRecoverySpec.cs b/src/core/Akka.Persistence.Tests/OptimizedRecoverySpec.cs new file mode 100644 index 00000000000..09e78f3e443 --- /dev/null +++ b/src/core/Akka.Persistence.Tests/OptimizedRecoverySpec.cs @@ -0,0 +1,145 @@ +using System; +using Akka.Actor; +using Xunit; + +namespace Akka.Persistence.Tests +{ + public class OptimizedRecoverySpec : PersistenceSpec + { + + #region Internal test classes + + internal class TakeSnapshot + { } + + internal class Save + { + public string Data { get; } + + public Save(string data) + { + Data = data; + } + } + + internal class Saved : IEquatable + { + public string Data { get; } + public long SeqNr { get; } + + public Saved(string data, long seqNr) + { + Data = data; + SeqNr = seqNr; + } + + public bool Equals(Saved other) + { + return other != null && Data.Equals(other.Data) && SeqNr.Equals(other.SeqNr); + } + } + + internal sealed class PersistFromRecoveryCompleted + { + public static PersistFromRecoveryCompleted Instance { get; } = new PersistFromRecoveryCompleted(); + private PersistFromRecoveryCompleted() { } + } + + internal class TestPersistentActor : NamedPersistentActor + { + private readonly Recovery _recovery; + private readonly IActorRef _probe; + private string state = string.Empty; + + public override Recovery Recovery => _recovery; + + public TestPersistentActor(string name, Recovery recovery, IActorRef probe) + : base(name) + { + _recovery = recovery; + _probe = probe; + } + + protected override bool ReceiveCommand(object message) + { + switch (message) + { + case TakeSnapshot _: + SaveSnapshot(state); + return true; + case SaveSnapshotSuccess s: + _probe.Tell(s); + return true; + case GetState _: + _probe.Tell(state); + return true; + case Save s: + Persist(new Saved(s.Data, LastSequenceNr + 1), evt => + { + state = state + evt.Data; + _probe.Tell(evt); + }); + return true; + } + return false; + } + + protected override bool ReceiveRecover(object message) + { + switch (message) + { + case SnapshotOffer s: + _probe.Tell(s); + state = s.Snapshot.ToString(); + return true; + case Saved evt: + state = state + evt.Data; + _probe.Tell(evt); + return true; + case RecoveryCompleted _: + if (IsRecovering) throw new InvalidOperationException($"Expected !IsRecovering in RecoveryCompleted"); + _probe.Tell(RecoveryCompleted.Instance); + // Verify that persist can be used here + Persist(PersistFromRecoveryCompleted.Instance, _ => _probe.Tell(PersistFromRecoveryCompleted.Instance)); + return true; + } + return false; + } + } + + #endregion + + private string persistenceId = "p1"; + + public OptimizedRecoverySpec() : base(Configuration("OptimizedRecoverySpec")) + { + var pref = ActorOf(Props.Create(() => new TestPersistentActor(persistenceId, Recovery.Default, TestActor))); + ExpectMsg(); + ExpectMsg(); + pref.Tell(new Save("a")); + pref.Tell(new Save("b")); + ExpectMsg(new Saved("a", 2)); + ExpectMsg(new Saved("b", 3)); + pref.Tell(new TakeSnapshot()); + ExpectMsg(); + pref.Tell(new Save("c")); + ExpectMsg(new Saved("c", 4)); + pref.Tell(GetState.Instance); + ExpectMsg("abc"); + } + + [Fact] + public void Persistence_must_get_RecoveryCompleted_but_no_SnapshotOffer_and_events_when_Recovery_none() + { + var pref = ActorOf(Props.Create(() => new TestPersistentActor(persistenceId, Recovery.None, TestActor))); + ExpectMsg(); + ExpectMsg(); + + // and highest sequence number should be used, PersistFromRecoveryCompleted is 5 + pref.Tell(new Save("d")); + ExpectMsg(new Saved("d", 6)); + pref.Tell(GetState.Instance); + ExpectMsg("d"); + } + } +} diff --git a/src/core/Akka.Persistence/Journal/AsyncWriteJournal.cs b/src/core/Akka.Persistence/Journal/AsyncWriteJournal.cs index 2409abba872..a8fca9fd23b 100644 --- a/src/core/Akka.Persistence/Journal/AsyncWriteJournal.cs +++ b/src/core/Akka.Persistence/Journal/AsyncWriteJournal.cs @@ -252,7 +252,7 @@ private void HandleReplayMessages(ReplayMessages message) { var highSequenceNr = t.Result; var toSequenceNr = Math.Min(message.ToSequenceNr, highSequenceNr); - if (highSequenceNr == 0L || message.FromSequenceNr > toSequenceNr) + if (toSequenceNr <= 0L || message.FromSequenceNr > toSequenceNr) { promise.SetResult(highSequenceNr); } diff --git a/src/core/Akka.Persistence/PersistentActor.cs b/src/core/Akka.Persistence/PersistentActor.cs index 6aa1421e10c..084f00aca8a 100644 --- a/src/core/Akka.Persistence/PersistentActor.cs +++ b/src/core/Akka.Persistence/PersistentActor.cs @@ -55,6 +55,10 @@ public sealed class Recovery /// /// Convenience method for skipping recovery in . + /// + /// It will still retrieve previously highest sequence number so that new events are persisted with + /// higher sequence numbers rather than starting from 1 and assuming that there are no + /// previous event with that sequence number. /// public static Recovery None { get; } = new Recovery(SnapshotSelectionCriteria.None, 0); diff --git a/src/core/Akka.Persistence/Snapshot/SnapshotStore.cs b/src/core/Akka.Persistence/Snapshot/SnapshotStore.cs index acb094c8cad..d477bd69743 100644 --- a/src/core/Akka.Persistence/Snapshot/SnapshotStore.cs +++ b/src/core/Akka.Persistence/Snapshot/SnapshotStore.cs @@ -56,14 +56,21 @@ private bool ReceiveSnapshotStore(object message) if (message is LoadSnapshot loadSnapshot) { - _breaker.WithCircuitBreaker(() => LoadAsync(loadSnapshot.PersistenceId, loadSnapshot.Criteria.Limit(loadSnapshot.ToSequenceNr))) - .ContinueWith(t => (!t.IsFaulted && !t.IsCanceled) - ? new LoadSnapshotResult(t.Result, loadSnapshot.ToSequenceNr) as ISnapshotResponse - : new LoadSnapshotFailed(t.IsFaulted - ? TryUnwrapException(t.Exception) - : new OperationCanceledException("LoadAsync canceled, possibly due to timing out.")), - _continuationOptions) - .PipeTo(senderPersistentActor); + if (loadSnapshot.Criteria == SnapshotSelectionCriteria.None) + { + senderPersistentActor.Tell(new LoadSnapshotResult(null, loadSnapshot.ToSequenceNr)); + } + else + { + _breaker.WithCircuitBreaker(() => LoadAsync(loadSnapshot.PersistenceId, loadSnapshot.Criteria.Limit(loadSnapshot.ToSequenceNr))) + .ContinueWith(t => (!t.IsFaulted && !t.IsCanceled) + ? new LoadSnapshotResult(t.Result, loadSnapshot.ToSequenceNr) as ISnapshotResponse + : new LoadSnapshotFailed(t.IsFaulted + ? TryUnwrapException(t.Exception) + : new OperationCanceledException("LoadAsync canceled, possibly due to timing out.")), + _continuationOptions) + .PipeTo(senderPersistentActor); + } } else if (message is SaveSnapshot saveSnapshot) { From f58db6e4505e55503abae5d6a76f33d4cbd70417 Mon Sep 17 00:00:00 2001 From: Sean Gilliam Date: Tue, 17 Jul 2018 14:21:29 -0500 Subject: [PATCH 56/70] [docs] Fix various issues (#3540) This PR fixes various issues w.r.t. the documentation site. - fix various typos - fix several links - fix styling issues --- docs/articles/actors/dependency-injection.md | 18 +-- docs/articles/actors/dispatchers.md | 122 +++++++++--------- docs/articles/actors/mailboxes.md | 53 ++++---- docs/articles/actors/routers.md | 4 +- docs/articles/actors/testing-actor-systems.md | 10 +- .../clustering/cluster-configuration.md | 2 +- docs/articles/clustering/cluster-extension.md | 2 +- docs/articles/clustering/cluster-sharding.md | 4 +- docs/articles/clustering/distributed-data.md | 24 ++-- .../clustering/split-brain-resolver.md | 15 ++- docs/articles/concepts/actor-systems.md | 2 +- docs/articles/intro/tutorial-3.md | 2 +- docs/articles/networking/io.md | 10 +- .../networking/multi-node-test-kit.md | 1 + docs/articles/persistence/event-sourcing.md | 4 +- docs/articles/persistence/persistent-fsm.md | 4 +- docs/articles/remoting/messaging.md | 4 +- docs/articles/remoting/transports.md | 10 +- docs/articles/streams/basics.md | 2 +- docs/articles/streams/builtinstages.md | 6 +- docs/articles/streams/cookbook.md | 2 +- .../streams/custom-stream-processing.md | 10 +- docs/articles/streams/workingwithgraphs.md | 2 +- docs/articles/utilities/scheduler.md | 4 +- docs/articles/utilities/serilog.md | 7 +- 25 files changed, 160 insertions(+), 164 deletions(-) diff --git a/docs/articles/actors/dependency-injection.md b/docs/articles/actors/dependency-injection.md index 7d3f707855c..f38d8618a6a 100644 --- a/docs/articles/actors/dependency-injection.md +++ b/docs/articles/actors/dependency-injection.md @@ -45,7 +45,7 @@ protected override void PreStart() } ``` -> [!INFO] +> [!NOTE] > There is currently still an extension method available for the actor Context. `Context.DI().ActorOf<>`. However this has been officially **deprecated** and will be removed in future versions. ## Notes @@ -77,7 +77,7 @@ guideline. Currently the following Akka.NET Dependency Injection plugins are available: -## AutoFac +### AutoFac In order to use this plugin, install the Nuget package with `Install-Package Akka.DI.AutoFac`, then follow the instructions: @@ -94,7 +94,7 @@ var system = ActorSystem.Create("MySystem"); var propsResolver = new AutoFacDependencyResolver(container, system); ``` -## CastleWindsor +### CastleWindsor In order to use this plugin, install the Nuget package with `Install-Package Akka.DI.CastleWindsor`, then follow the instructions: @@ -110,7 +110,7 @@ var system = ActorSystem.Create("MySystem"); var propsResolver = new WindsorDependencyResolver(container, system); ``` -## Ninject +### Ninject In order to use this plugin, install the Nuget package with `Install-Package Akka.DI.Ninject`, then follow the instructions: @@ -126,14 +126,8 @@ var system = ActorSystem.Create("MySystem"); var propsResolver = new NinjectDependencyResolver(container,system); ``` -## Other frameworks +### Other frameworks Support for additional dependency injection frameworks may be added in the future, but you can easily implement your own by implementing an -[Actor Producer Extension](DI Core). - - - - - - +[Actor Producer Extension](xref:di-core). diff --git a/docs/articles/actors/dispatchers.md b/docs/articles/actors/dispatchers.md index ff80f4b6df1..4831c265d6a 100644 --- a/docs/articles/actors/dispatchers.md +++ b/docs/articles/actors/dispatchers.md @@ -69,100 +69,100 @@ system.ActorOf(Props.Create().WithDispatcher("my-dispatcher"), "my-acto Some dispatcher configurations are available out-of-the-box for convenience. You can use them during actor deployment, [as described above](#configuring-dispatchers). -* **default-dispatcher** - A configuration that uses the [ThreadPoolDispatcher](#ThreadPoolDispatcher). As the name says, this is the default dispatcher configuration used by the global dispatcher, and you don't need to define anything during deployment to use it. -* **task-dispatcher** - A configuration that uses the [TaskDispatcher](#TaskDispatcher). -* **default-fork-join-dispatcher** - A configuration that uses the [ForkJoinDispatcher]. -* **synchronized-dispatcher** - A configuration that uses the [SynchronizedDispatcher](#SynchronizedDispatcher). +* **default-dispatcher** - A configuration that uses the [ThreadPoolDispatcher](#threadpooldispatcher). As the name says, this is the default dispatcher configuration used by the global dispatcher, and you don't need to define anything during deployment to use it. +* **task-dispatcher** - A configuration that uses the [TaskDispatcher](#taskdispatcher). +* **default-fork-join-dispatcher** - A configuration that uses the [ForkJoinDispatcher](#forkjoindispatcher). +* **synchronized-dispatcher** - A configuration that uses the [SynchronizedDispatcher](#synchronizeddispatcher). ## Built-in Dispatchers These are the underlying dispatchers built-in to Akka.NET: -* ### ThreadPoolDispatcher +### ThreadPoolDispatcher - It schedules code to run in the [.NET Thread Pool](https://msdn.microsoft.com/en-us/library/System.Threading.ThreadPool.aspx), which is ***good enough* for most cases.** +It schedules code to run in the [.NET Thread Pool](https://msdn.microsoft.com/en-us/library/System.Threading.ThreadPool.aspx), which is ***good enough* for most cases.** - The `type` used in the HOCON configuration for this dispatcher is just `Dispatcher`. +The `type` used in the HOCON configuration for this dispatcher is just `Dispatcher`. - ```hocon - custom-dispatcher { - type = Dispatcher - throughput = 100 - } - ``` +```hocon +custom-dispatcher { +type = Dispatcher +throughput = 100 +} +``` > [!NOTE] > While each configuration can have it's own throughput settings, all dispatchers using this type will run in the same default .NET Thread Pool. -* ### TaskDispatcher +### TaskDispatcher - The TaskDispatcher uses the [TPL](https://msdn.microsoft.com/en-us/library/dd460717.aspx) infrastructure to schedule code execution. This dispatcher is very similar to the Thread PoolDispatcher, but may be used in some rare scenarios where the thread pool isn't available. +The TaskDispatcher uses the [TPL](https://msdn.microsoft.com/en-us/library/dd460717.aspx) infrastructure to schedule code execution. This dispatcher is very similar to the Thread PoolDispatcher, but may be used in some rare scenarios where the thread pool isn't available. - ```hocon - custom-task-dispatcher { - type = TaskDispatcher - throughput = 100 - } - ``` +```hocon +custom-task-dispatcher { + type = TaskDispatcher + throughput = 100 +} +``` -* ### PinnedDispatcher +### PinnedDispatcher - The `PinnedDispatcher` uses a single dedicated thread to schedule code executions. Ideally, this dispatcher should be using sparingly. +The `PinnedDispatcher` uses a single dedicated thread to schedule code executions. Ideally, this dispatcher should be using sparingly. - ```hocon - custom-dedicated-dispatcher { - type = PinnedDispatcher - } - ``` +```hocon +custom-dedicated-dispatcher { + type = PinnedDispatcher +} +``` -* ### ForkJoinDispatcher +### ForkJoinDispatcher - The ForkJoinDispatcher uses a dedicated threadpool to schedule code execution. You can use this scheduler isolate some actors from the rest of the system. Each dispatcher configuration will have it's own thread pool. +The ForkJoinDispatcher uses a dedicated threadpool to schedule code execution. You can use this scheduler isolate some actors from the rest of the system. Each dispatcher configuration will have it's own thread pool. - This is the configuration for the [*default-fork-join-dispatcher*](#built-in-dispatcher-configurations). You may use this as example for custom fork-join dispatchers. +This is the configuration for the [*default-fork-join-dispatcher*](#built-in-dispatcher-configurations). You may use this as example for custom fork-join dispatchers. - ```hocon - default-fork-join-dispatcher { - type = ForkJoinDispatcher - throughput = 100 - dedicated-thread-pool { - thread-count = 3 - deadlock-timeout = 3s - threadtype = background - } +```hocon +default-fork-join-dispatcher { + type = ForkJoinDispatcher + throughput = 100 + dedicated-thread-pool { + thread-count = 3 + deadlock-timeout = 3s + threadtype = background } - ``` +} +``` - * `thread-count` - The number of threads dedicated to this dispatcher. - * `deadlock-timeout` - The amount of time to wait before considering the thread as deadlocked. By default no timeout is set, meaning code can run in the threads for as long as they need. If you set a value, once the timeout is reached the thread will be aborted and a new threads will take it's place. Set this value carefully, as very low values may cause loss of work. - * `threadtype` - Must be `background` or `foreground`. This setting helps define [how .NET handles](https://msdn.microsoft.com/en-us/library/system.threading.thread.isbackground.aspx) the thread. +* `thread-count` - The number of threads dedicated to this dispatcher. +* `deadlock-timeout` - The amount of time to wait before considering the thread as deadlocked. By default no timeout is set, meaning code can run in the threads for as long as they need. If you set a value, once the timeout is reached the thread will be aborted and a new threads will take it's place. Set this value carefully, as very low values may cause loss of work. +* `threadtype` - Must be `background` or `foreground`. This setting helps define [how .NET handles](https://msdn.microsoft.com/en-us/library/system.threading.thread.isbackground.aspx) the thread. -* ### SynchronizedDispatcher +### SynchronizedDispatcher - The `SynchronizedDispatcher` uses the *current* [SynchronizationContext](https://msdn.microsoft.com/en-us/magazine/gg598924.aspx) to schedule executions. +The `SynchronizedDispatcher` uses the *current* [SynchronizationContext](https://msdn.microsoft.com/en-us/magazine/gg598924.aspx) to schedule executions. - You may use this dispatcher to create actors that update UIs in a reactive manner. An application that displays real-time updates of stock prices may have a dedicated actor to update the UI controls directly for example. +You may use this dispatcher to create actors that update UIs in a reactive manner. An application that displays real-time updates of stock prices may have a dedicated actor to update the UI controls directly for example. > [!NOTE] > As a general rule, actors running in this dispatcher shouldn't do much work. Avoid doing any extra work that may be done by actors running in other pools. - This is the configuration for the [*synchronized-dispatcher*](#built-in-dispatcher-configurations). You may use this as example for custom fork-join dispatchers. +This is the configuration for the [*synchronized-dispatcher*](#built-in-dispatcher-configurations). You may use this as example for custom fork-join dispatchers. - ```hocon - synchronized-dispatcher { - type = "SynchronizedDispatcher" - throughput = 10 - } - ``` +```hocon +synchronized-dispatcher { + type = "SynchronizedDispatcher" + throughput = 10 +} +``` - In order to use this dispatcher, you must create the actor from the syncrhonization context you want to run-it. For example: +In order to use this dispatcher, you must create the actor from the synchronization context you want to run-it. For example: - ```cs - private void Form1_Load(object sender, System.EventArgs e) - { - system.ActorOf(Props.Create().WithDispatcher("synchronized-dispatcher"), "ui-worker"); - } - ``` +```csharp +private void Form1_Load(object sender, System.EventArgs e) +{ + system.ActorOf(Props.Create().WithDispatcher("synchronized-dispatcher"), "ui-worker"); +} +``` #### Common Dispatcher Configuration diff --git a/docs/articles/actors/mailboxes.md b/docs/articles/actors/mailboxes.md index 3e4d826e712..4ebb381dbd9 100644 --- a/docs/articles/actors/mailboxes.md +++ b/docs/articles/actors/mailboxes.md @@ -13,7 +13,7 @@ A mailbox can be described as a queue of messages. Messages are usually then del Normally every actor has its own mailbox, but this is not a requirement. There are implementations of [routers](xref:routers) where all routees share a single mailbox. -### Using a Mailbox +## Using a Mailbox To make an actor use a specific mailbox, you can set it up one of the following locations: @@ -35,7 +35,7 @@ To make an actor use a specific mailbox, you can set it up one of the following The `my-custom-mailbox` is a key that was setup using the [mailbox configuration](#configuring-custom-mailboxes). -### Configuring Custom Mailboxes +## Configuring Custom Mailboxes In order to use a custom mailbox, it must be first configured with a key that the actor system can lookup. You can do this using a custom HOCON setting. @@ -48,40 +48,41 @@ my-custom-mailbox { } ``` -### Built-in Mailboxes +## Built-in Mailboxes -* #### UnboundedMailbox +### UnboundedMailbox - **This is the default mailbox** used by Akka.NET. It's a non-blocking unbounded mailbox, and should be good enough for most cases. +**This is the default mailbox** used by Akka.NET. It's a non-blocking unbounded mailbox, and should be good enough for most cases. -* #### UnboundedPriorityMailbox +### UnboundedPriorityMailbox - The unbounded mailbox priority mailbox is blocking mailbox that allows message prioritization, so that you can choose the order the actor should process messages that are already in the mailbox. +The unbounded mailbox priority mailbox is blocking mailbox that allows message prioritization, so that you can choose the order the actor should process messages that are already in the mailbox. - In order to use this mailbox, you need to extend the `UnboundedPriorityMailbox` class and provide an ordering logic. The value returned by the `PriorityGenerator` method will be used to order the message in the mailbox. Lower values will be delivered first. Delivery order for messages of equal priority is undefined. +In order to use this mailbox, you need to extend the `UnboundedPriorityMailbox` class and provide an ordering logic. The value returned by the `PriorityGenerator` method will be used to order the message in the mailbox. Lower values will be delivered first. Delivery order for messages of equal priority is undefined. - ```cs - public class IssueTrackerMailbox : UnboundedPriorityMailbox +```cs +public class IssueTrackerMailbox : UnboundedPriorityMailbox +{ + protected override int PriorityGenerator(object message) { - protected override int PriorityGenerator(object message) - { - var issue = message as Issue; + var issue = message as Issue; - if (issue != null) - { - if (issue.IsSecurityFlaw) - return 0; + if (issue != null) + { + if (issue.IsSecurityFlaw) + return 0; - if (issue.IsBug) - return 1; - } + if (issue.IsBug) + return 1; + } - return 2; - } + return 2; } - ``` +} +``` + +Once the class is implemented, you should set it up using the [instructions above](#using-a-mailbox). - Once the class is implemented, you should set it up using the [instructions above](#using-a-mailbox). -##### Special note -While we have updated the UnboundedPriorityMailbox to support Stashing. We don't recommend using it. +> [!WARNING] +> While we have updated the `UnboundedPriorityMailbox` to support Stashing. We don't recommend using it. Once you stash messages, they are no longer part of the prioritization process that your PriorityMailbox uses. Once you unstash all messages, they are fed to the Actor, in the order of stashing. diff --git a/docs/articles/actors/routers.md b/docs/articles/actors/routers.md index ea87916dac3..0ae2fe33b05 100644 --- a/docs/articles/actors/routers.md +++ b/docs/articles/actors/routers.md @@ -576,9 +576,9 @@ When an actor received `PoisonPill` message, that actor will be stopped. (see [P For a router, which normally passes on messages to routees, the `PoisonPill` messages are processed __by the router only__. `PoisonPill` messages sent to a router will __not__ be sent on to its routees. -However, a `PisonPill` message sent to a router may still affect its routees, as it will stop the router whhich in turns stop children the router has created. Each child will process its current message and then stop. This could lead to some messages being unprocessed. +However, a `PoisonPill` message sent to a router may still affect its routees, as it will stop the router which in turns stop children the router has created. Each child will process its current message and then stop. This could lead to some messages being unprocessed. -If you wish to stop a router and its routees, but you would like the routees to first process all the messages in their mailbxes, then you should send a `PoisonPill` message wrapped inside a `Broadcast` message so that each routee will receive the `PoisonPill` message. +If you wish to stop a router and its routees, but you would like the routees to first process all the messages in their mailboxes, then you should send a `PoisonPill` message wrapped inside a `Broadcast` message so that each routee will receive the `PoisonPill` message. > [!NOTE] > The above method will stop all routees, even if they are not created by the router. E.g. routees programatically provided to the router. diff --git a/docs/articles/actors/testing-actor-systems.md b/docs/articles/actors/testing-actor-systems.md index 2bb05f27073..374227a035a 100644 --- a/docs/articles/actors/testing-actor-systems.md +++ b/docs/articles/actors/testing-actor-systems.md @@ -74,8 +74,8 @@ Since an integration test does not allow to the internal processing of the parti If a number of occurrences is specific --as demonstrated above-- then `intercept` will block until that number of matching messages have been received or the timeout configured in `akka.test.filter-leeway` is used up (time starts counting after the passed-in block of code returns). In case of a timeout the test fails. ->[NOTE] ->By default the TestKit already loads the TestEventListener as a logger. Be aware that if you want to specify your own config. Use the `DefaultConfig` property to apply overrides. +> [!NOTE] +> By default the TestKit already loads the TestEventListener as a logger. Be aware that if you want to specify your own config. Use the `DefaultConfig` property to apply overrides. ## Timing Assertions Another important part of functional testing concerns timing: certain events must not happen immediately (like a timer), others need to happen before a deadline. Therefore, all examination methods accept an upper time limit within the positive or negative result must be obtained. Lower time limits need to be checked external to the examination, which is facilitated by a new construct for managing time constraints: @@ -121,8 +121,8 @@ Probes may also be equipped with custom assertions to make your test code even m You have complete flexibility here in mixing and matching the `TestKit` facilities with your own checks and choosing an intuitive name for it. In real life your code will probably be a bit more complicated than the example given above; just use the power! ->[WARNING] ->Any message send from a `TestProbe` to another actor which runs on the `CallingThreadDispatcher` runs the risk of dead-lock, if that other actor might also send to this probe. The implementation of `TestProbe.Watch` and `TestProbe.Unwatch` will also send a message to the watchee, which means that it is dangerous to try watching e.g. `TestActorRef` from a `TestProbe`. +> [!WARNING] +> Any message send from a `TestProbe` to another actor which runs on the `CallingThreadDispatcher` runs the risk of dead-lock, if that other actor might also send to this probe. The implementation of `TestProbe.Watch` and `TestProbe.Unwatch` will also send a message to the watchee, which means that it is dangerous to try watching e.g. `TestActorRef` from a `TestProbe`. ###Watching Other Actors from probes A `TestProbe` can register itself for DeathWatch of any other actor: @@ -263,7 +263,7 @@ Testing the business logic inside `Actor` classes can be divided into two parts: Normally, the `IActorRef` shields the underlying `Actor` instance from the outside, the only communications channel is the actor's mailbox. This restriction is an impediment to unit testing, which led to the inception of the `TestActorRef`. This special type of reference is designed specifically for test purposes and allows access to the actor in two ways: either by obtaining a reference to the underlying actor instance, or by invoking or querying the actor's behaviour (receive). Each one warrants its own section below. > [!NOTE] -> It is highly recommended to stick to traditional behavioural testing (using messaging to ask the `Actor` to reply with the state you want to run assertions against), instead of using `TestActorRef` whenever possible. +> It is highly recommended to stick to traditional behavioral testing (using messaging to ask the `Actor` to reply with the state you want to run assertions against), instead of using `TestActorRef` whenever possible. ## Obtaining a Reference to an Actor Having access to the actual `Actor` object allows application of all traditional unit testing techniques on the contained methods. Obtaining a reference is done like this: diff --git a/docs/articles/clustering/cluster-configuration.md b/docs/articles/clustering/cluster-configuration.md index 5b6acdbb135..508cf59dd1c 100644 --- a/docs/articles/clustering/cluster-configuration.md +++ b/docs/articles/clustering/cluster-configuration.md @@ -82,4 +82,4 @@ akka { This tells the role leader for `crawlerV1` to not mark any of those nodes as up until at least three nodes with role `crawlerV1` have joined the cluster. ## Additional Resources -- [`Cluster.conf`](https://github.com/akkadotnet/akka.net/blob/dev/src/core/Akka.Cluster/Configuration/Cluster.conf): the full set of configuration options +- [Cluster.conf](../configuration/akka.cluster.md): the full set of configuration options diff --git a/docs/articles/clustering/cluster-extension.md b/docs/articles/clustering/cluster-extension.md index a611f5b9d0d..40f0d455820 100644 --- a/docs/articles/clustering/cluster-extension.md +++ b/docs/articles/clustering/cluster-extension.md @@ -4,7 +4,7 @@ title: Accessing the Cluster `ActorSystem` Extension --- # Using the Cluster `ActorSystem` Extension Object -`Akka.Cluster` is actually an "`ActorSystem` extension" that you can use to access membership information and [cluster gossip](cluster-overview.md#cluster-gossip) directly. +`Akka.Cluster` is actually an `ActorSystem` extension that you can use to access membership information and [cluster gossip](cluster-overview.md#cluster-gossip) directly. ## Getting a Reference to the `Cluster` You can get a direct reference to the `Cluster` extension like so (drawn from the [`SimpleClusterListener` example in the Akka.NET project](https://github.com/akkadotnet/akka.net/blob/dev/src/examples/Cluster/Samples.Cluster.Simple/SimpleClusterListener.cs)): diff --git a/docs/articles/clustering/cluster-sharding.md b/docs/articles/clustering/cluster-sharding.md index a25c73aed6b..c4eaf3336a9 100644 --- a/docs/articles/clustering/cluster-sharding.md +++ b/docs/articles/clustering/cluster-sharding.md @@ -9,7 +9,7 @@ Cluster sharding is useful in cases when you want to contact with cluster actors Cluster sharding can operate in 2 modes, configured via `akka.cluster.sharding.state-store-mode` HOCON configuration: 1. `persistence` (**default**) depends on Akka.Persistence module. In order to use it, you'll need to specify an event journal accessible by all of the participating nodes. An information about the particular shard placement is stored in a persistent cluster singleton actor known as *coordinator*. In order to guarantee consistent state between different incarnations, coordinator stores its own state using Akka.Persistence event journals. -2. `ddata` (**experimental**, available in versions above 1.3.2) depends on Akka.DistributedData module. It uses Conflict-free Replicated Data Types (CRDT) to ensure eventualy consistent shard placement and global availability via node-to-node replication and automatic conflict resolution. In this mode event journals don't have to be configured. At this moment this mode doesn't support `akka.cluster.sharding.remember-entities` option. +2. `ddata` (**experimental**, available in versions above 1.3.2) depends on Akka.DistributedData module. It uses Conflict-free Replicated Data Types (CRDT) to ensure eventually consistent shard placement and global availability via node-to-node replication and automatic conflict resolution. In this mode event journals don't have to be configured. At this moment this mode doesn't support `akka.cluster.sharding.remember-entities` option. Cluster sharding may be active only on nodes in `Up` status - so the ones fully recognized and acknowledged by every other node in a cluster. @@ -82,7 +82,7 @@ To reduce memory consumption, you may decide to stop entities after some period ## Remembering entities -By default, when a shard is rebalanced to another node, the entities it stored before migration, are NOT started immediatelly after. Instead they are recreated ad-hoc, when new messages are incoming. This behavior can be modified by `akka.cluster.sharding.remember-entities = true` configuration. It will instruct shards to keep their state between rebalances - it also comes with extra cost due to necessity of persisting information about started/stopped entities. Additionally a message extractor logic must be aware of `ShardRegion.StartEntity` message: +By default, when a shard is rebalanced to another node, the entities it stored before migration, are NOT started immediately after. Instead they are recreated ad-hoc, when new messages are incoming. This behavior can be modified by `akka.cluster.sharding.remember-entities = true` configuration. It will instruct shards to keep their state between rebalances - it also comes with extra cost due to necessity of persisting information about started/stopped entities. Additionally a message extractor logic must be aware of `ShardRegion.StartEntity` message: ```csharp public sealed class ShardEnvelope diff --git a/docs/articles/clustering/distributed-data.md b/docs/articles/clustering/distributed-data.md index 7893b800538..1980d62d3ee 100644 --- a/docs/articles/clustering/distributed-data.md +++ b/docs/articles/clustering/distributed-data.md @@ -42,14 +42,14 @@ In response, you should receive `Replicator.IGetResponse` message. There are sev - `GetSuccess` when a value for provided key has been received successfully. To get the value, you need to call `response.Get(key)` with the key, you've sent with the request. - `NotFound` when no value was found under provided key. -- `GetFailure` when a replicator failed to retrieve value withing specified consistency and timeout constraints. +- `GetFailure` when a replicator failed to retrieve value within specified consistency and timeout constraints. - `DataDeleted` when a value for the provided key has been deleted. -All `Get` requests follows the read-your-own-write rule - if you updated the data, and want to read the state under the same key immediatelly after, you'll always retrieve modified value, even if the `IGetResponse` message will arrive before `IUpdateResponse`. +All `Get` requests follows the read-your-own-write rule - if you updated the data, and want to read the state under the same key immediately after, you'll always retrieve modified value, even if the `IGetResponse` message will arrive before `IUpdateResponse`. #### Read consistency -What is a mentioned read consistency? As we said at the beginning, all updates perfomed within distributed data module will eventually converge. This means, we're not speaking about immediate consistency of a given value across all nodes. Therefore we can precise, what degree of consistency are we expecting: +What is a mentioned read consistency? As we said at the beginning, all updates performed within distributed data module will eventually converge. This means, we're not speaking about immediate consistency of a given value across all nodes. Therefore we can precise, what degree of consistency are we expecting: - `ReadLocal` - we take value based on replica living on a current node. - `ReadFrom` - value will be merged from states retrieved from some number of nodes, including local one. @@ -79,17 +79,17 @@ Just like in case of reads, there are several possible responses: - `UpdateSuccess` when a value for provided key has been replicated successfully within provided write consistency constraints. - `ModifyFailure` when update failed because of an exception within modify function used inside `Update` command. -- `UpdateTimeout` when a write consistency constraints has not been fulfilled on time. **Warning**: this doesn't mean, that update has been rolled back! Provided value will eventually propage its replicas across nodes using gossip protocol, causing the altered state to eventually converge across all of them. +- `UpdateTimeout` when a write consistency constraints has not been fulfilled on time. **Warning**: this doesn't mean, that update has been rolled back! Provided value will eventually propagate its replicas across nodes using gossip protocol, causing the altered state to eventually converge across all of them. - `DataDeleted` when a value under provided key has been deleted. You'll always see updates done on local node. When you perform two updates on the same key, second modify function will always see changes done by the first one. #### Write consistency -Just like in case of reads, write consistency allows us to specify level of certainity of our updates before proceeding: +Just like in case of reads, write consistency allows us to specify level of certainty of our updates before proceeding: -- `WriteLocal` - while value will be disseminated later using gossip, the response will return immediatelly after local replica update has been acknowledged. -- `WriteTo` - update will immediatelly be propagated to a given number of replicas, including local one. +- `WriteLocal` - while value will be disseminated later using gossip, the response will return immediately after local replica update has been acknowledged. +- `WriteTo` - update will immediately be propagated to a given number of replicas, including local one. - `WriteMajority` - update will propagate to more than a half nodes in a cluster (or nodes given a configured role) before response will be emitted. - `WriteAll` - update will propagate to all nodes in a cluster (or nodes given a configured role) before response will be emitted. @@ -112,7 +112,7 @@ Delete may return one of the 3 responses: - `DeleteSuccess` when key deletion succeeded within provided consistency constraints. - `DataDeleted` when data has been deleted already. Once deleted, key can no longer be reused and `DataDeleted` response will be send to all subsequent requests (either reads, updates or deletes). This message will also be used as notification for subscribers. -- `ReplicationDeleteFailure` when operation failed to satisfy specified consistency constraints. **Warning**: this doesn't mean, that delete has been rolled back! Provided operation will eventually propage its replicas across nodes using gossip protocol, causing the altered state to eventually converge across all of them. +- `ReplicationDeleteFailure` when operation failed to satisfy specified consistency constraints. **Warning**: this doesn't mean, that delete has been rolled back! Provided operation will eventually propagate its replicas across nodes using gossip protocol, causing the altered state to eventually converge across all of them. Deletes doesn't specify it's own consistency - it uses the same `IWriteConsistency` interface as updates. @@ -148,12 +148,12 @@ All subscribers are removed automatically when terminated. This can be also done ## Available replicated data types -Akka.DistributedData specifies several data types, sharing the same `IReplicatedData` interface. All of them share some common members, such as (default) empty value or `Merge` method used to merge two replicas of the same data with automatic conflic resolution. All of those values are also immutable - this means, that any operations, which are supposed to change their state, produce new instance in result: +Akka.DistributedData specifies several data types, sharing the same `IReplicatedData` interface. All of them share some common members, such as (default) empty value or `Merge` method used to merge two replicas of the same data with automatic conflict resolution. All of those values are also immutable - this means, that any operations, which are supposed to change their state, produce new instance in result: - `Flag` is a boolean CRDT flag, which default value is always `false`. When a merging replicas have conflicting state, `true` will always win over `false`. - `GCounter` (also known as growing-only counter) allows only for addition/increment of its state. Trying to add a negative value is forbidden here. Total value of the counter is a sum of values across all of the replicas. In case of conflicts during merge operation, a copy of replica with greater value will always win. -- `PNCounter` allows for both increments and decrements. A total value of the counter is a sum of increments across all replcias decreased by the sum of all decrements. -- `GSet` is an add-only set, which disallows to remove elements once added to it. Merges of GSets are simple unions of their elements. This data type doesn't produce any garbadge. +- `PNCounter` allows for both increments and decrements. A total value of the counter is a sum of increments across all replicas decreased by the sum of all decrements. +- `GSet` is an add-only set, which disallows to remove elements once added to it. Merges of GSets are simple unions of their elements. This data type doesn't produce any garbage. - `ORSet` is implementation of an observed remove add-wins set. It allows to both add and remove its elements any number of times. In case of conflicts when merging replicas, added elements always wins over removed ones. - `ORDictionary` (also knowns as OR-Map or Observed Remove Map) has similar semantics to OR-Set, however it allows to merge values (which must be CRDTs themselves) in case of concurrent updates. - `ORMultiDictionary` is a multi-map implementation based on `ORDictionary`, where values are represented as OR-Sets. Use `AddItem` or `RemoveItem` to add or remove elements to the bucket under specified keys. @@ -165,7 +165,7 @@ Keep in mind, that most of the replicated collections add/remove methods require ## Tombstones -One of the issue of CRDTs, is that they accumulate history of changes (including removed elements), producing a garbadge, that effectivelly pile up in memory. While this is still a problem, it can be limited by replicator, which is able to remove data associated with nodes, that no longer exist in the cluster. This process is known as a pruning. +One of the issue of CRDTs, is that they accumulate history of changes (including removed elements), producing a garbage, that effectively pile up in memory. While this is still a problem, it can be limited by replicator, which is able to remove data associated with nodes, that no longer exist in the cluster. This process is known as a pruning. ## Settings diff --git a/docs/articles/clustering/split-brain-resolver.md b/docs/articles/clustering/split-brain-resolver.md index 31eab427c06..5bc39dade5f 100644 --- a/docs/articles/clustering/split-brain-resolver.md +++ b/docs/articles/clustering/split-brain-resolver.md @@ -4,7 +4,8 @@ title: Split Brain Resolver --- # Split Brain Resolver -> Note: while this feature is based on [Lightbend Reactive Platform Split Brain Resolver](https://doc.akka.io/docs/akka/rp-16s01p02/scala/split-brain-resolver.html) feature description, however its implementation is a result of free contribution and interpretation of Akka.NET team. Lightbend doesn't take any responsibility for the state and correctness of it. +> [!NOTE] +> While this feature is based on [Lightbend Reactive Platform Split Brain Resolver](https://doc.akka.io/docs/akka/rp-16s01p02/scala/split-brain-resolver.html) feature description, however its implementation is a result of free contribution and interpretation of Akka.NET team. Lightbend doesn't take any responsibility for the state and correctness of it. 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. @@ -21,7 +22,7 @@ To solve this kind of problems we need to determine a common strategy, in which Since Akka.NET cluster is working in peer-to-peer mode, it means that there is no single *global* entity which is able to arbitrary define one true state of the cluster. Instead each node has so called failure detector, which tracks the responsiveness and checks health of other connected nodes. This allows us to create a *local* node perspective on the overall cluster state. -In the past the only available opt-in stategy was an auto-down, in which each node was automatically downing others after reaching a certain period of unreachability. While this approach was enough to react on machine crashes, it was failing in face of network partitions: if cluster was split into two or more parts due to network connectivity issues, each one of them would simply consider others as down. This would lead to having several independent clusters not knowning about each other. It is especially disastrous in case of Cluster Singleton and Cluster Sharding features, both relying on having only one actor instance living in the cluster at the same time. +In the past the only available opt-in strategy was an auto-down, in which each node was automatically downing others after reaching a certain period of unreachability. While this approach was enough to react on machine crashes, it was failing in face of network partitions: if cluster was split into two or more parts due to network connectivity issues, each one of them would simply consider others as down. This would lead to having several independent clusters not knowning about each other. It is especially disastrous in case of Cluster Singleton and Cluster Sharding features, both relying on having only one actor instance living in the cluster at the same time. Split brain resolver feature brings ability to apply different strategies for managing node lifecycle in face of network issues and machine crashes. It works as a custom downing provider. Therefore in order to use it, **all of your Akka.NET cluster nodes must define it with the same configuration**. Here's how minimal configuration looks like: @@ -48,7 +49,7 @@ To decide which strategy to use, you can set `akka.cluster.split-brain-resolver. - `keep-oldest` - `keep-referee` -All strategies will be applied only after cluster state has reached stability for specified time treshold (no nodes transitioning between different states for some time), specified by `stable-after` setting. Nodes which are joining will not affect this treshold, as they won't be promoted to UP status in face unreachable nodes. For the same reason they won't be taken into account, when a strategy will be applied. +All strategies will be applied only after cluster state has reached stability for specified time threshold (no nodes transitioning between different states for some time), specified by `stable-after` setting. Nodes which are joining will not affect this treshold, as they won't be promoted to UP status in face unreachable nodes. For the same reason they won't be taken into account, when a strategy will be applied. ```hocon akka.cluster.split-brain-resolver { @@ -134,7 +135,7 @@ akka.cluster.split-brain-resolver { The `keep-oldest` strategy, when a network split has happened, will down a part of the cluster which doesn't contain the oldest node. -When to use it? This approach is particularly good in combination with Cluster Singleton, which usually is running on the oldest cluster member. It's also usefull, when you have a one starter node configured as `akka.cluster.seed-nodes` for others, which will still allow you to add and remove members using its address. +When to use it? This approach is particularly good in combination with Cluster Singleton, which usually is running on the oldest cluster member. It's also useful, when you have a one starter node configured as `akka.cluster.seed-nodes` for others, which will still allow you to add and remove members using its address. Keep in mind, that: @@ -143,7 +144,7 @@ Keep in mind, that: 3. There is a risk, that if partition will split cluster into two unequal parts i.e. 2 nodes with the oldest one present and 20 remaining ones, the majority of the cluster will go down. 4. Since the oldest node is determined on the latest known state of the cluster, there is a small risk that during partition, two parts of the cluster will both consider themselves having the oldest member on their side. While this is very rare situation, you still may end up having two independent clusters after split occurrence. -Just like in previous cases, a `role` setting can be used to detemine the oldest member across all having specified role. +Just like in previous cases, a `role` setting can be used to determine the oldest member across all having specified role. Configuration: @@ -170,10 +171,10 @@ When to use it? If you have a single node which is running processes crucial to Things to keep in mind: -1. With this strategy, cluster will never split into two indenpendent ones, under any circumstances. +1. With this strategy, cluster will never split into two independent ones, under any circumstances. 2. A referee node is a single point of failure for the cluster. -You can configure a minimum required amount of reachable nodes to maintain operability by using `down-all-if-less-than-nodes`. If a strategy will detect that the number of reachable nodes will go below that minimun it will down the entire partition even when referee node was reachable. +You can configure a minimum required amount of reachable nodes to maintain operability by using `down-all-if-less-than-nodes`. If a strategy will detect that the number of reachable nodes will go below that minimum it will down the entire partition even when referee node was reachable. Configuration: diff --git a/docs/articles/concepts/actor-systems.md b/docs/articles/concepts/actor-systems.md index d2872da243e..922422fd0eb 100644 --- a/docs/articles/concepts/actor-systems.md +++ b/docs/articles/concepts/actor-systems.md @@ -49,7 +49,7 @@ The non-exhaustive list of adequate solutions to the "blocking problem" includes The first possibility is especially well-suited for resources which are single-threaded in nature, like database handles which traditionally can only execute one outstanding query at a time and use internal synchronization to ensure this. A common pattern is to create a router for N actors, each of which wraps a single DB connection and handles queries as sent to the router. The number N must then be tuned for maximum throughput, which will vary depending on which DBMS is deployed on what hardware. > [!NOTE] ->Configuring thread pools is a task best delegated to Akka.NET, simply configure in the application.conf and instantiate through an ActorSystem. +> Configuring thread pools is a task best delegated to Akka.NET, simply configure in the application.conf and instantiate through an ActorSystem. ## What you should not concern yourself with An actor system manages the resources it is configured to use in order to run the actors which it contains. There may be millions of actors within one such system, after all the mantra is to view them as abundant and they weigh in at an overhead of only roughly 300 bytes per instance. Naturally, the exact order in which messages are processed in large systems is not controllable by the application author, but this is also not intended. Take a step back and relax while Akka.NET does the heavy lifting under the hood. diff --git a/docs/articles/intro/tutorial-3.md b/docs/articles/intro/tutorial-3.md index ac2337751d7..5d0fbbc1033 100644 --- a/docs/articles/intro/tutorial-3.md +++ b/docs/articles/intro/tutorial-3.md @@ -84,7 +84,7 @@ is known up front: device groups and device actors are created on-demand. The st the acknowledgment, the receiver, i.e. the device, will be able to learn its `IActorRef` and send direct messages to its device actor in the future. Now that the steps are defined, we only need to define the messages that we will use to communicate requests and -their acknowledgement: +their acknowledgment: [!code-csharp[DeviceManager.scala](../../examples/Tutorials/Tutorial3/DeviceManager.cs?name=device-manager-msgs)] diff --git a/docs/articles/networking/io.md b/docs/articles/networking/io.md index 64ae6fcb68a..9ffa3aa7d04 100644 --- a/docs/articles/networking/io.md +++ b/docs/articles/networking/io.md @@ -20,9 +20,10 @@ var system = ActorSystem.Create("example"); var manager = system.Tcp(); ``` -### TCP Driver +## TCP Driver + +### Client Connection -#### Client Connection To create a connection an actor sends a `Tcp.Connect` message to the TCP Manager. Once the connection is established the connection actor sends a `Tcp.Connected` message to the `commander`, which registers the `connection handler` by replying with a `Tcp.Register` message. @@ -36,7 +37,8 @@ The following example shows a simple Telnet client. The client send lines entere [!code-csharp[Main](../../examples/DocsExamples/Networking/IO/TelnetClient.cs?range=10-63)] -#### Server Connection +### Server Connection + To accept connections, an actor sends an `Tcp.Bind` message to the TCP manager, passing the `bind handler` in the message. The `bind commander` will receive a `Tcp.Bound` message when the connection is listening. @@ -50,4 +52,4 @@ The following code example shows a simple server that echo's data received from [!code-csharp[Main](../../examples/DocsExamples/Networking/IO/EchoServer.cs?range=8-29)] -[!code-csharp[Main](../../examples/DocsExamples/Networking/IO/EchoConnection.cs?range=6-27)] +[!code-csharp[Main](../../examples/DocsExamples/Networking/IO/EchoConnection.cs?range=7-28)] diff --git a/docs/articles/networking/multi-node-test-kit.md b/docs/articles/networking/multi-node-test-kit.md index 200611b27a9..9c6b4278263 100644 --- a/docs/articles/networking/multi-node-test-kit.md +++ b/docs/articles/networking/multi-node-test-kit.md @@ -99,6 +99,7 @@ A multi-node spec gives us the ability to do the following: 4. Create barriers that are used to synchronize nodes at specific points within a test; and 5. Test assertions across one or more nodes. +> [!NOTE] > Everything that's available in the default `Akka.TestKit` is also available inside the `Akka.Remote.TestKit`, but it's worth bearing in mind that `Akka.Remote.TestKit` only works with the `Akka.MultiNodeTestRunner` and uses Xunit 2.0 internally. #### Step 1 - Subclass `MultiNodeConfig` diff --git a/docs/articles/persistence/event-sourcing.md b/docs/articles/persistence/event-sourcing.md index 4737234aa32..d06492fcd39 100644 --- a/docs/articles/persistence/event-sourcing.md +++ b/docs/articles/persistence/event-sourcing.md @@ -174,7 +174,7 @@ You can also call `DeferAsync` with `Persist`. It is possible to call `Persist` and `PersistAsync` inside their respective callback blocks and they will properly retain both the thread safety (including the right value of `Sender`) as well as stashing guarantees. -In general it is encouraged to create command handlers which do not need to resort to nested event persisting, however there are situations where it may be useful. It is important to understand the ordering of callback execution in those situations, as well as their implication on the stashing behaviour (that persist enforces). In the following example two persist calls are issued, and each of them issues another persist inside its callback: +In general it is encouraged to create command handlers which do not need to resort to nested event persisting, however there are situations where it may be useful. It is important to understand the ordering of callback execution in those situations, as well as their implication on the stashing behavior (that persist enforces). In the following example two persist calls are issued, and each of them issues another persist inside its callback: [!code-csharp[Main](../../examples/DocsExamples/Persistence/PersistentActor/NestedPersists.cs?range=8-36)] @@ -269,7 +269,7 @@ This can be dangerous when used with `UntypedPersistentActor` due to the fact th > [!WARNING] > Consider using explicit shut-down messages instead of `PoisonPill` when working with persistent actors. -The example below highlights how messages arrive in the Actor's mailbox and how they interact with its internal stashing mechanism when `Persist()` is used. Notice the early stop behaviour that occurs when `PoisonPill` is used: +The example below highlights how messages arrive in the Actor's mailbox and how they interact with its internal stashing mechanism when `Persist()` is used. Notice the early stop behavior that occurs when `PoisonPill` is used: [!code-csharp[Main](../../examples/DocsExamples/Persistence/PersistentActor/AvoidPoisonPill.cs?range=9-35)] diff --git a/docs/articles/persistence/persistent-fsm.md b/docs/articles/persistence/persistent-fsm.md index 128255668e7..8d809bcad31 100644 --- a/docs/articles/persistence/persistent-fsm.md +++ b/docs/articles/persistence/persistent-fsm.md @@ -42,7 +42,7 @@ Here is how everything is wired together: [!code-csharp[WebStoreCustomerFSMActor.cs](../../examples/DocsExamples/Persistence/WebStoreCustomerFSMActor.cs?name=persistent-fsm-apply-event)] `AndThen` can be used to define actions which will be executed following event’s persistence - convenient for "side effects" like sending a message or logging. Notice that actions defined in andThen block are not executed on recovery: -```C# +```cs GoTo(Paid.Instance).Applying(OrderExecuted.Instance).AndThen(cart => { if (cart is NonEmptyShoppingCart nonShoppingCart) @@ -52,7 +52,7 @@ GoTo(Paid.Instance).Applying(OrderExecuted.Instance).AndThen(cart => }); ``` A snapshot of state data can be persisted by calling the `SaveStateSnapshot()` method: -```C# +```cs Stop().Applying(OrderDiscarded.Instance).AndThen(cart => { reportActor.Tell(ShoppingCardDiscarded.Instance); diff --git a/docs/articles/remoting/messaging.md b/docs/articles/remoting/messaging.md index 53ee37da865..c788f3d2675 100644 --- a/docs/articles/remoting/messaging.md +++ b/docs/articles/remoting/messaging.md @@ -67,7 +67,7 @@ What's really going on there? The `Sender`, an `IActorRef`, is actually an `Akka.Remote.RemoteActorRef`! But the fact that this actor reference resides elsewhere on the network is a detail that's transparent to the actor code you wrote! -In essence, minus the initial `ActorSelection` used to start remote communication between the two `ActorSystem`s, any actor in either `ActorSystem` could reply to eachother without knowing or caring that they exist elsewhere on the network. That's pretty cool! +In essence, minus the initial `ActorSelection` used to start remote communication between the two `ActorSystem`s, any actor in either `ActorSystem` could reply to each other without knowing or caring that they exist elsewhere on the network. That's pretty cool! ## `RemoteActorRef` and Location Transparency What `RemoteActorRef` gives us is a magical property called [Location Transparency](/concepts/location-transparency.md). @@ -78,7 +78,7 @@ It's the job of the `RemoteActorRef` to make a remote actor running on in a diff Regardless of where the actor actually resides, it doesn't affect your code one way or another. -**So this has one profound implicat on your Akka.NET applications - all of your actor code *is already able to run on the network by default*. ** +**So this has one profound implication on your Akka.NET applications - all of your actor code** ***is already able to run on the network by default.*** Therefore, many of the code samples in *Akka.NET Remoting* won't look very "networky." That's on purpose. That's Akka.NET taking care of the heavy lifting for us! diff --git a/docs/articles/remoting/transports.md b/docs/articles/remoting/transports.md index ddbeb32b5ae..ac82271f4ba 100644 --- a/docs/articles/remoting/transports.md +++ b/docs/articles/remoting/transports.md @@ -6,7 +6,7 @@ title: Transports # Akka.Remote Transports In the [Akka.Remote overview](index.md) we introduced the concept of "transports" for Akka.Remote. -> A"transport" refers to an actual network transport, such as TCP or UDP. By default Akka.Remote uses a [DotNetty](https://github.com/Azure/DotNetty) TCP transport, but you could write your own transport and use that instead of you wish. +A "transport" refers to an actual network transport, such as TCP or UDP. By default Akka.Remote uses a [DotNetty](https://github.com/Azure/DotNetty) TCP transport, but you could write your own transport and use that instead of you wish. In this section we'll expand a bit more on what transports are and how Akka.Remote can support multiple transports simultaneously. @@ -14,12 +14,12 @@ In this section we'll expand a bit more on what transports are and how Akka.Remo Transports in Akka.Remote are abstractions on top of actual network transports, such as TCP and UDP sockets, and in truth transports have pretty simple requirements. > [!NOTE] -> most of the information below are things you, as an Akka.NET user, do not need to care about 99% of the time. Feel free to skip to the "Akka.Remote's Built-in Transports" section. +> most of the information below are things you, as an Akka.NET user, do not need to care about 99% of the time. Feel free to skip to the [Akka.Remote's Built-in Transports](#akkaremotes-built-in-transports) section. Transports **do not need to care** about: * **Serialization** - that's handled by Akka.NET itself; -* **Connection-oriented behavior** - the assocation process inside Akka.Remote ensures this, even over connectionless transports like UDP; -* **Reliable delivery** - for system messages this is handled by Akka.Remote and for user-defined messages this is taken care of at the application level through something like the [`AtLeastOnceDeliveryActor` class](xref:at-least-once-delivery), part of Akka.Persistence; +* **Connection-oriented behavior** - the association process inside Akka.Remote ensures this, even over connectionless transports like UDP; +* **Reliable delivery** - for system messages this is handled by Akka.Remote and for user-defined messages this is taken care of at the application level through something like the [`AtLeastOnceDeliveryActor`](xref:at-least-once-delivery) class, part of Akka.Persistence; * **Handling network failures** - all a transport needs to do is forward that information back up to Akka.Remote. Transports **do need to care** about: @@ -79,7 +79,7 @@ You'd define a custom HOCON section (`akka.remote.google-quic`) and let Akka.Rem > [!NOTE] > To implement a custom transport yourself, you need to implement the [`Akka.Remote.Transport.Transport` abstract class](xref:Akka.Remote.Transport.Transport). -One important thing to note is the `akka.remote.google-quic.transport-protocol` setting - this specifices the address scheme you will use to address remote actors via the Quic protocol. +One important thing to note is the `akka.remote.google-quic.transport-protocol` setting - this specifies the address scheme you will use to address remote actors via the Quic protocol. A remote address for an actor on this transport will look like: diff --git a/docs/articles/streams/basics.md b/docs/articles/streams/basics.md index 84b396416f8..0b2372e7596 100644 --- a/docs/articles/streams/basics.md +++ b/docs/articles/streams/basics.md @@ -211,7 +211,7 @@ specification, which Akka is a founding member of. The user of the library does not have to write any explicit back-pressure handling code — it is built in and dealt with automatically by all of the provided Akka Streams processing stages. It is possible however to add -explicit buffer stages with overflow strategies that can influence the behaviour of the stream. This is especially important +explicit buffer stages with overflow strategies that can influence the behavior of the stream. This is especially important in complex processing graphs which may even contain loops (which *must* be treated with very special care, as explained in [Graph cycles, liveness and deadlocks](xref:streams-working-with-graphs#graph-cycles-liveness-and-deadlocks)). diff --git a/docs/articles/streams/builtinstages.md b/docs/articles/streams/builtinstages.md index 35289f2b736..58872fdc4f5 100644 --- a/docs/articles/streams/builtinstages.md +++ b/docs/articles/streams/builtinstages.md @@ -365,7 +365,7 @@ to provide back pressure onto the sink. **cancels** when the actor terminates -**backpressures** when the actor acknowledgement has not arrived. +**backpressures** when the actor acknowledgment has not arrived. #### PreMaterialize @@ -680,7 +680,7 @@ Throwing an exception inside `RecoverWith` _will_ be logged on ERROR level autom **emits** the element is available from the upstream or upstream is failed and pf returns alternative Source -**backpressures** downstream backpressures, after failure happened it backprssures to alternative Source +**backpressures** downstream backpressures, after failure happened it backpressures to alternative Source **completes** upstream completes or upstream failed with exception pf can handle @@ -709,7 +709,7 @@ would log the `e2` error. Since the underlying failure signal OnError arrives out-of-band, it might jump over existing elements. This stage can recover the failure signal, but not the skipped elements, which will be dropped. -Similarily to `Recover` throwing an exception inside `SelectError` _will_ be logged on ERROR level automatically. +Similarly to `Recover` throwing an exception inside `SelectError` _will_ be logged on ERROR level automatically. **emits** when element is available from the upstream or upstream is failed and function returns an element diff --git a/docs/articles/streams/cookbook.md b/docs/articles/streams/cookbook.md index 7970dfb0605..08740eb8cee 100644 --- a/docs/articles/streams/cookbook.md +++ b/docs/articles/streams/cookbook.md @@ -376,7 +376,7 @@ a special ``Sum`` operation that collapses multiple upstream elements into one a the speed of the upstream unaffected by the downstream. When the upstream is faster, the sum process of the ``Conflate`` starts. Our reducer function simply takes -the freshest element. This cin a simple dropping operation. +the freshest element. This is shown as a simple dropping operation. ```csharp var droppyStream = Flow.Create().Conflate((lastMessage, newMessage) => newMessage); diff --git a/docs/articles/streams/custom-stream-processing.md b/docs/articles/streams/custom-stream-processing.md index 219aa952703..52c015a2d6d 100644 --- a/docs/articles/streams/custom-stream-processing.md +++ b/docs/articles/streams/custom-stream-processing.md @@ -4,7 +4,7 @@ title: Custom stream processing --- # Custom stream processing -While the processing vocabulary of Akka Streams is quite rich (see the [Streams Cookbook](xref:streams-cookbook) for examples) it is sometimes necessary to define new transformation stages either because some functionality is missing from the stock operations, or for preformance reasons. In this part we show how to build custom processing stages and graph junctions of various kinds. +While the processing vocabulary of Akka Streams is quite rich (see the [Streams Cookbook](xref:streams-cookbook) for examples) it is sometimes necessary to define new transformation stages either because some functionality is missing from the stock operations, or for performance reasons. In this part we show how to build custom processing stages and graph junctions of various kinds. > [!NOTE] > A custom graph stage should not be the first tool you reach for, defining graphs using flows and the graph DSL is in general easier and does to a larger extent protect you from mistakes that might be easy to make with a custom `GraphStage` @@ -69,7 +69,7 @@ class NumbersSource : GraphStage> // Define the (sole) output port of this stage public Outlet Out { get; } = new Outlet("NumbersSource"); - // Define the shape of this tage, which is SourceShape with the port we defined above + // Define the shape of this stage, which is SourceShape with the port we defined above public override SourceShape Shape => new SourceShape(Out); //this is where the actual logic will be created @@ -121,7 +121,7 @@ The following operations are available for *input* ports: * `Grab(in)` acquires the element that has been received during an `onPush` It cannot be called again until the port is pushed again by the upstream. * `Cancel(in)` closes the input port. -The events corresponding to an *input* port can be received in an `Action` registrered to the input port using `setHandler(in, action)`. This handler has three callbacks: +The events corresponding to an *input* port can be received in an `Action` registered to the input port using `setHandler(in, action)`. This handler has three callbacks: * `onPush` is called when the output port has now a new element. Now it is possible to acquire this element using `Grab(in)` and/or call `Pull(in)` on the port to request the next element. It is not mandatory to grab the element, but if it is pulled while the element has not been grabbed it will drop the buffered element. @@ -362,9 +362,9 @@ If we attempt to draw the sequence of events, it shows that there is one "event ### Completion -Completion handling usually (but not exclusively) comes into the picture when processing stages need to emit a few more elements after their upstream source has been completed. We have seen an example of this in our first `Duplicator` implementation where the last element needs to be doubled even after the upstream neighbour stage has been completed. This can be done by overriding the `onUpstreamFinish` callback in `SetHandler(in, action)`. +Completion handling usually (but not exclusively) comes into the picture when processing stages need to emit a few more elements after their upstream source has been completed. We have seen an example of this in our first `Duplicator` implementation where the last element needs to be doubled even after the upstream neighbor stage has been completed. This can be done by overriding the `onUpstreamFinish` callback in `SetHandler(in, action)`. -Stages by default automatically stop once all of their ports (input and output) have been closed externally or internally. It is possible to opt out from this behavior by invoking `SetKeepGoing(true)` (which is not supported from the stage’s constructor and usually done in `PreStart`). In this case the stage **must** be explicitly closed by calling `CompleteStage()` or `FailStage(exception)`. This feature carries the risk of leaking streams and actors, therefore it should be used with care. +Stages by default automatically stop once all of their ports (input and output) have been closed externally or internally. It is possible to opt out from this behavior by invoking `SetKeepGoing(true)` (which is not supported from the stage's constructor and usually done in `PreStart`). In this case the stage **must** be explicitly closed by calling `CompleteStage()` or `FailStage(exception)`. This feature carries the risk of leaking streams and actors, therefore it should be used with care. ### Logging inside GraphStages diff --git a/docs/articles/streams/workingwithgraphs.md b/docs/articles/streams/workingwithgraphs.md index c6a01cdb7de..5ac388ffb35 100644 --- a/docs/articles/streams/workingwithgraphs.md +++ b/docs/articles/streams/workingwithgraphs.md @@ -260,7 +260,7 @@ It is possible to build reusable, encapsulated components of arbitrary input and As an example, we will build a graph junction that represents a pool of workers, where a worker is expressed as a ``Flow``, i.e. a simple transformation of jobs of type ``I`` to results of type ``O`` (as you have seen already, this flow can actually contain a complex graph inside). Our reusable worker pool junction will -not preserve the order of the incoming jobs (they are assumed to have a proper ID field) and it will use a ``Balance`` junction to schedule jobs to available workers. On top of this, our junction will feature a "fastlane", a dedicated port where jobs of higher priority can be sent. +not preserve the order of the incoming jobs (they are assumed to have a proper ID field) and it will use a ``Balance`` junction to schedule jobs to available workers. On top of this, our junction will feature a "fast lane", a dedicated port where jobs of higher priority can be sent. Altogether, our junction will have two input ports of type ``I`` (for the normal and priority jobs) and an output port of type ``O``. To represent this interface, we need to define a custom `Shape`. The following lines show how to do that. diff --git a/docs/articles/utilities/scheduler.md b/docs/articles/utilities/scheduler.md index a36e44633a0..774ed5cc4df 100644 --- a/docs/articles/utilities/scheduler.md +++ b/docs/articles/utilities/scheduler.md @@ -14,7 +14,7 @@ You can schedule sending of messages to actors and execution of tasks (`Action` delegate). You will get a `Task` back. By providing a `CancellationTokenSource` you can cancel the scheduled task. -The scheduler in Akka is designed for high-throughput of thousands up to millions of triggers. THe prime use-case being triggering Actor receive timeouts, Future timeouts, circuit breakers and other time dependent events which happen all-the-time and in many instances at the same time. The implementation is based on a Hashed Wheel Timer, which is a known datastrucure and algorithm for handling such use cases, refer to the [Hashed and Hierarchical Timing Wheels](http://www.cs.columbia.edu/~nahum/w6998/papers/sosp87-timing-wheels.pdf) whitepaper by Varghese and Lauck if you'd like to understand its inner workings. +The scheduler in Akka is designed for high-throughput of thousands up to millions of triggers. THe prime use-case being triggering Actor receive timeouts, Future timeouts, circuit breakers and other time dependent events which happen all-the-time and in many instances at the same time. The implementation is based on a Hashed Wheel Timer, which is a known data structure and algorithm for handling such use cases, refer to the [Hashed and Hierarchical Timing Wheels](http://www.cs.columbia.edu/~nahum/w6998/papers/sosp87-timing-wheels.pdf) whitepaper by Varghese and Lauck if you'd like to understand its inner workings. The Akka scheduler is **not** designed for long-term scheduling (see [Akka.Quartz.Actor](https://github.com/akkadotnet/Akka.Quartz.Actor) instead for this use-case) nor is it to be used for highly precise firing of the events. The maximum amount of time into the future you can schedule an event to trigger is around 8 months, which in practice is too much to be useful since this would assume the system never went down during that period. If you need long-term scheduling we highly recommend looking into alternative schedulers, as this is not the use-case the Akka scheduler is implemented for. @@ -56,7 +56,7 @@ Context.System.Scheduler.ScheduleTellRepeatedly(....); ## The scheduler interface The actual scheduler implementation is defined by config and loaded upon ActorSystem start-up, which means that it is possible to provide a different one using the `akka.scheduler.implementation` configuration property. The referenced class must implement the `Akka.Actor.IScheduler` and `Akka.Actor.IAdvancedScheduler` interfaces -##The cancellable interface +## The cancellable interface Scheduling a task will result in a `ICancellable` or (or throw an `Exception` if attempted after the scheduler's shutdown). This allows you to cancel something that has been scheduled for execution. diff --git a/docs/articles/utilities/serilog.md b/docs/articles/utilities/serilog.md index 61d86c2bc97..bdbeb520659 100644 --- a/docs/articles/utilities/serilog.md +++ b/docs/articles/utilities/serilog.md @@ -15,7 +15,7 @@ PM> Install-Package Akka.Logger.Serilog ## Example -The following example uses Serilogs __Colored Console__ sink available via nuget, there are wide range of other sinks available depending on your needs, for example a rolling log file sink. See serilogs documentation for details on these. +The following example uses Serilog's __Colored Console__ sink available via nuget, there are wide range of other sinks available depending on your needs, for example a rolling log file sink. See serilog's documentation for details on these. ``` PM> Install-Package Serilog.Sinks.ColoredConsole @@ -101,7 +101,7 @@ In order to be able to change log level without the need to recompile, we need t ``` -The code can then be updated as follows removing the inline HOCON from the actor system creation code. Note in the following example, if a minimum level is not specfied, Information level events and higher will be processed. Please read the documentation for [Serilog](https://serilog.net/) configuration for more details on this. It is also possible to move serilog configuration to the application configuration, for example if using a rolling log file sink, again, browsing the serilog documentation is the best place for details on that feature. +The code can then be updated as follows removing the inline HOCON from the actor system creation code. Note in the following example, if a minimum level is not specified, Information level events and higher will be processed. Please read the documentation for [Serilog](https://serilog.net/) configuration for more details on this. It is also possible to move serilog configuration to the application configuration, for example if using a rolling log file sink, again, browsing the serilog documentation is the best place for details on that feature. ```csharp var logger = new LoggerConfiguration() @@ -113,6 +113,3 @@ Serilog.Log.Logger = logger; var system = ActorSystem.Create("my-test-system"); ``` - - - From d1c1efe6b8dc8cfab0c3d8829fd2184af98bc659 Mon Sep 17 00:00:00 2001 From: Ismael Hamed Date: Mon, 23 Jul 2018 23:40:39 +0200 Subject: [PATCH 57/70] Fix ReceiveTimeout issue with NotInfluenceReceiveTimeout (#3555) --- .../Akka.Tests/Actor/ReceiveTimeoutSpec.cs | 22 +++++++++++++++++++ .../Akka/Actor/ActorCell.ReceiveTimeout.cs | 6 ++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/core/Akka.Tests/Actor/ReceiveTimeoutSpec.cs b/src/core/Akka.Tests/Actor/ReceiveTimeoutSpec.cs index 5c10aa732d2..9bcf92fe266 100644 --- a/src/core/Akka.Tests/Actor/ReceiveTimeoutSpec.cs +++ b/src/core/Akka.Tests/Actor/ReceiveTimeoutSpec.cs @@ -8,6 +8,7 @@ using System; using System.Threading; using Akka.Actor; +using Akka.Actor.Dsl; using Akka.Event; using Akka.TestKit; using Akka.Util.Internal; @@ -183,6 +184,27 @@ public void An_actor_with_receive_timeout_must_get_timeout_while_receiving_NotIn Sys.Stop(timeoutActor); } + [Fact] + public void An_actor_with_receive_timeout_must_get_timeout_while_receiving_only_NotInfluenceReceiveTimeout_messages() + { + var timeoutLatch = new TestLatch(2); + + Action actor = d => + { + d.OnPreStart = c => c.SetReceiveTimeout(TimeSpan.FromSeconds(1)); + d.Receive((o, c) => + { + c.Self.Tell(new TransparentTick()); + timeoutLatch.CountDown(); + }); + d.Receive((_, __) => { }); + }; + var timeoutActor = Sys.ActorOf(Props.Create(() => new Act(actor))); + + timeoutLatch.Ready(TestKitSettings.DefaultTimeout); + Sys.Stop(timeoutActor); + } + [Fact] public void Issue469_An_actor_with_receive_timeout_must_cancel_receive_timeout_when_terminated() { diff --git a/src/core/Akka/Actor/ActorCell.ReceiveTimeout.cs b/src/core/Akka/Actor/ActorCell.ReceiveTimeout.cs index 0d48fd63e82..e928a0587ba 100644 --- a/src/core/Akka/Actor/ActorCell.ReceiveTimeout.cs +++ b/src/core/Akka/Actor/ActorCell.ReceiveTimeout.cs @@ -10,7 +10,7 @@ namespace Akka.Actor { /// - /// TBD + /// Marker interface to indicate that a message should not reset the receive timeout. /// public interface INotInfluenceReceiveTimeout { @@ -25,7 +25,7 @@ public partial class ActorCell /// TBD /// /// TBD - public void SetReceiveTimeout(TimeSpan? timeout=null) + public void SetReceiveTimeout(TimeSpan? timeout = null) { _receiveTimeoutDuration = timeout; } @@ -47,7 +47,7 @@ public TimeSpan? ReceiveTimeout public void CheckReceiveTimeout() { CancelReceiveTimeout(); - if (_receiveTimeoutDuration != null && !Mailbox.HasMessages) + if (_receiveTimeoutDuration != null) { _pendingReceiveTimeout = System.Scheduler.ScheduleTellOnceCancelable(_receiveTimeoutDuration.Value, Self, Akka.Actor.ReceiveTimeout.Instance, Self); } From f406bfd9cdb1030e0daf5fe784db3531a6846c0f Mon Sep 17 00:00:00 2001 From: zbynek001 Date: Tue, 24 Jul 2018 01:04:38 +0200 Subject: [PATCH 58/70] sharding log formatting fix (#3554) --- src/contrib/cluster/Akka.Cluster.Sharding/ShardCoordinator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ShardCoordinator.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ShardCoordinator.cs index 93ac93f798e..8e5deb38d6c 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ShardCoordinator.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ShardCoordinator.cs @@ -263,7 +263,7 @@ private static void ClearRebalanceInProgress(this TCoordinator coo private static void DeferGetShardHomeRequest(this TCoordinator coordinator, string shard, IActorRef from) where TCoordinator : IShardCoordinator { - coordinator.Log.Debug("GetShardHome [{1}] request from [{2}] deferred, because rebalance is in progress for this shard. It will be handled when rebalance is done.", shard, from); + coordinator.Log.Debug("GetShardHome [{0}] request from [{1}] deferred, because rebalance is in progress for this shard. It will be handled when rebalance is done.", shard, from); var pending = coordinator.RebalanceInProgress.TryGetValue(shard, out var prev) ? prev : ImmutableHashSet.Empty; From a19051af952261f8a26c2a322320879a02e5c981 Mon Sep 17 00:00:00 2001 From: zbynek001 Date: Wed, 25 Jul 2018 05:19:34 +0200 Subject: [PATCH 59/70] confirm TakeOverFromMe when singleton already in oldest state (#3553) --- .../Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs b/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs index e79459db1c5..c519b70a88a 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs @@ -903,6 +903,12 @@ private void InitializeFSM() { return GoToHandingOver(oldest.Singleton, oldest.SingletonTerminated, Sender); } + else if (e.FsmEvent is TakeOverFromMe) + { + // already oldest, so confirm and continue like that + Sender.Tell(HandOverToMe.Instance); + return Stay(); + } else if (e.FsmEvent is Terminated terminated && e.StateData is OldestData o && terminated.ActorRef.Equals(o.Singleton)) { return Stay().Using(new OldestData(o.Singleton, true)); From f5596d0f6e258ee587cf081ad05d8ef8f4ef4889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Beir=C3=A3o?= Date: Tue, 31 Jul 2018 17:00:59 +0200 Subject: [PATCH 60/70] Fix small typo (#3559) I don't believe `Selet` is a valid method --- docs/articles/streams/reactivetweets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/streams/reactivetweets.md b/docs/articles/streams/reactivetweets.md index a323eb39db3..000330986fd 100644 --- a/docs/articles/streams/reactivetweets.md +++ b/docs/articles/streams/reactivetweets.md @@ -172,7 +172,7 @@ elements*" this can be expressed using the ``Buffer`` element: ```csharp tweetSource .Buffer(10, OverflowStrategy.DropHead) - .Selet(SlowComputation) + .Select(SlowComputation) .RunWith(Sink.Ignore(), mat); ``` From fabf39b5e4a9df1bc27281362b9b656b82b09e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarl=20Sveinung=20Fl=C3=B8=20Rasmussen?= Date: Tue, 31 Jul 2018 17:58:02 +0200 Subject: [PATCH 61/70] Create a new linked CancellationToken for each database operation(#3471) (#3494) --- .../Journal/SqlJournal.cs | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/SqlJournal.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/SqlJournal.cs index 8a0e8edfa26..defc29c92cc 100644 --- a/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/SqlJournal.cs +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/SqlJournal.cs @@ -168,7 +168,8 @@ protected override async Task> WriteMessagesAsync(IEnu } var batch = new WriteJournalBatch(eventToTags); - await QueryExecutor.InsertBatchAsync(connection, _pendingRequestsCancellation.Token, batch); + using(var cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(_pendingRequestsCancellation.Token)) + await QueryExecutor.InsertBatchAsync(connection, cancellationToken.Token, batch); } }).ToArray(); @@ -206,13 +207,16 @@ protected virtual async Task ReplayTaggedMessagesAsync(ReplayTaggedMessage using (var connection = CreateDbConnection()) { await connection.OpenAsync(); - return await QueryExecutor - .SelectByTagAsync(connection, _pendingRequestsCancellation.Token, replay.Tag, replay.FromOffset, replay.ToOffset, replay.Max, replayedTagged => { - foreach(var adapted in AdaptFromJournal(replayedTagged.Persistent)) - { - replay.ReplyTo.Tell(new ReplayedTaggedMessage(adapted, replayedTagged.Tag, replayedTagged.Offset), ActorRefs.NoSender); - } - }); + using(var cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(_pendingRequestsCancellation.Token)) + { + return await QueryExecutor + .SelectByTagAsync(connection, cancellationToken.Token, replay.Tag, replay.FromOffset, replay.ToOffset, replay.Max, replayedTagged => { + foreach(var adapted in AdaptFromJournal(replayedTagged.Persistent)) + { + replay.ReplyTo.Tell(new ReplayedTaggedMessage(adapted, replayedTagged.Tag, replayedTagged.Offset), ActorRefs.NoSender); + } + }); + } } } @@ -233,7 +237,10 @@ public override async Task ReplayMessagesAsync(IActorContext context, string per using (var connection = CreateDbConnection()) { await connection.OpenAsync(); - await QueryExecutor.SelectByPersistenceIdAsync(connection, _pendingRequestsCancellation.Token, persistenceId, fromSequenceNr, toSequenceNr, max, recoveryCallback); + using (var cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(_pendingRequestsCancellation.Token)) + { + await QueryExecutor.SelectByPersistenceIdAsync(connection, cancellationToken.Token, persistenceId, fromSequenceNr, toSequenceNr, max, recoveryCallback); + } } } @@ -286,14 +293,16 @@ private async Task Initialize() using (var connection = CreateDbConnection()) { await connection.OpenAsync(); - - if (_settings.AutoInitialize) + using (var cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(_pendingRequestsCancellation.Token)) { - await QueryExecutor.CreateTablesAsync(connection, _pendingRequestsCancellation.Token); - } + if (_settings.AutoInitialize) + { + await QueryExecutor.CreateTablesAsync(connection, cancellationToken.Token); + } - var ids = await QueryExecutor.SelectAllPersistenceIdsAsync(connection, _pendingRequestsCancellation.Token); - return new AllPersistenceIds(ids); + var ids = await QueryExecutor.SelectAllPersistenceIdsAsync(connection, cancellationToken.Token); + return new AllPersistenceIds(ids); + } } } catch (Exception e) @@ -459,7 +468,10 @@ protected override async Task DeleteMessagesToAsync(string persistenceId, long t using (var connection = CreateDbConnection()) { await connection.OpenAsync(); - await QueryExecutor.DeleteBatchAsync(connection, _pendingRequestsCancellation.Token, persistenceId, toSequenceNr); + using (var cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(_pendingRequestsCancellation.Token)) + { + await QueryExecutor.DeleteBatchAsync(connection, cancellationToken.Token, persistenceId, toSequenceNr); + } } } @@ -475,7 +487,10 @@ public override async Task ReadHighestSequenceNrAsync(string persistenceId using (var connection = CreateDbConnection()) { await connection.OpenAsync(); - return await QueryExecutor.SelectHighestSequenceNrAsync(connection, _pendingRequestsCancellation.Token, persistenceId); + using (var cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(_pendingRequestsCancellation.Token)) + { + return await QueryExecutor.SelectHighestSequenceNrAsync(connection, cancellationToken.Token, persistenceId); + } } } From dc8b77d1600ed71e24538668b07b624e0a13db48 Mon Sep 17 00:00:00 2001 From: zbynek001 Date: Tue, 31 Jul 2018 18:28:57 +0200 Subject: [PATCH 62/70] Coordinated downing improvements (#3551) * ClusterSingletonManager should ignore FSM events during shutdown * added copyright headers and API approval * wasn't calling RunCoordinatedShutdownWhenDowning() before * Run all CoordinatedShutdown phases also when downing * add Reason to CoordinatedShutdown * Allow member to leave a cluster via CoordinatedShutdown.run when MemberStatus is Joining/WeaklyUp/Up. * ClusterSpec, race between MemberRemoved and MemberExited --- .../CoordinatedShutdownShardingSpec.cs | 210 ++++++++++++++++++ .../Akka.Cluster.Sharding/ShardRegion.cs | 11 +- .../Singleton/ClusterSingletonManager.cs | 22 +- .../Singleton/OldestChangedBuffer.cs | 24 +- .../CoreAPISpec.ApproveCluster.approved.txt | 1 + .../CoreAPISpec.ApproveCore.approved.txt | 23 ++ src/core/Akka.Cluster.Tests/ClusterSpec.cs | 52 ++++- src/core/Akka.Cluster/Cluster.cs | 5 + src/core/Akka.Cluster/ClusterDaemon.cs | 50 ++--- src/core/Akka.Cluster/Member.cs | 12 +- .../Actor/CoordinatedShutdownSpec.cs | 28 ++- src/core/Akka/Actor/CoordinatedShutdown.cs | 121 ++++++++-- 12 files changed, 487 insertions(+), 72 deletions(-) create mode 100644 src/contrib/cluster/Akka.Cluster.Sharding.Tests/CoordinatedShutdownShardingSpec.cs diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests/CoordinatedShutdownShardingSpec.cs b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/CoordinatedShutdownShardingSpec.cs new file mode 100644 index 00000000000..38b2f5ae966 --- /dev/null +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests/CoordinatedShutdownShardingSpec.cs @@ -0,0 +1,210 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2016 Lightbend Inc. +// Copyright (C) 2013-2016 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Linq; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Cluster.Tools.Singleton; +using Akka.Configuration; +using Akka.TestKit; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Cluster.Sharding.Tests +{ + public class CoordinatedShutdownShardingSpec : AkkaSpec + { + private readonly ActorSystem _sys1; + private readonly ActorSystem _sys2; + private readonly ActorSystem _sys3; + + private readonly IActorRef _region1; + private readonly IActorRef _region2; + private readonly IActorRef _region3; + + private readonly TestProbe _probe1; + private readonly TestProbe _probe2; + private readonly TestProbe _probe3; + + private static readonly Config SpecConfig; + + private class EchoActor : ReceiveActor + { + public EchoActor() + { + ReceiveAny(_ => Sender.Tell(_)); + } + } + + private readonly ExtractEntityId _extractEntityId = message => Tuple.Create(message.ToString(), message); + + private readonly ExtractShardId _extractShard = message => (message.GetHashCode() % 10).ToString(); + + static CoordinatedShutdownShardingSpec() + { + SpecConfig = ConfigurationFactory.ParseString(@" + akka.loglevel = DEBUG + akka.actor.provider = cluster + akka.remote.dot-netty.tcp.port = 0") + .WithFallback(ClusterSingletonManager.DefaultConfig() + .WithFallback(ClusterSharding.DefaultConfig())); + } + + public CoordinatedShutdownShardingSpec(ITestOutputHelper helper) : base(SpecConfig, helper) + { + _sys1 = ActorSystem.Create(Sys.Name, Sys.Settings.Config); + _sys2 = ActorSystem.Create(Sys.Name, Sys.Settings.Config); + _sys3 = Sys; + + var props = Props.Create(() => new EchoActor()); + _region1 = ClusterSharding.Get(_sys1).Start("type1", props, ClusterShardingSettings.Create(_sys1), + _extractEntityId, _extractShard); + _region2 = ClusterSharding.Get(_sys2).Start("type1", props, ClusterShardingSettings.Create(_sys2), + _extractEntityId, _extractShard); + _region3 = ClusterSharding.Get(_sys3).Start("type1", props, ClusterShardingSettings.Create(_sys3), + _extractEntityId, _extractShard); + + + _probe1 = CreateTestProbe(_sys1); + _probe2 = CreateTestProbe(_sys2); + _probe3 = CreateTestProbe(_sys3); + + CoordinatedShutdown.Get(_sys1).AddTask(CoordinatedShutdown.PhaseBeforeServiceUnbind, "unbind", () => + { + _probe1.Ref.Tell("CS-unbind-1"); + return Task.FromResult(Done.Instance); + }); + + CoordinatedShutdown.Get(_sys2).AddTask(CoordinatedShutdown.PhaseBeforeServiceUnbind, "unbind", () => + { + _probe2.Ref.Tell("CS-unbind-2"); + return Task.FromResult(Done.Instance); + }); + + CoordinatedShutdown.Get(_sys3).AddTask(CoordinatedShutdown.PhaseBeforeServiceUnbind, "unbind", () => + { + _probe3.Ref.Tell("CS-unbind-3"); + return Task.FromResult(Done.Instance); + }); + } + + protected override void BeforeTermination() + { + Shutdown(_sys1); + Shutdown(_sys2); + } + + /// + /// Using region 2 as it is not shutdown in either test. + /// + private void PingEntities() + { + _region2.Tell(1, _probe2.Ref); + _probe2.ExpectMsg(10.Seconds()).Should().Be(1); + _region2.Tell(2, _probe2.Ref); + _probe2.ExpectMsg(10.Seconds()).Should().Be(2); + _region2.Tell(3, _probe2.Ref); + _probe2.ExpectMsg(10.Seconds()).Should().Be(3); + } + + [Fact] + public void Sharding_and_CoordinatedShutdown_must_run_successfully() + { + InitCluster(); + RunCoordinatedShutdownWhenLeaving(); + RunCoordinatedShutdownWhenDowning(); + } + + private void InitCluster() + { + Cluster.Get(_sys1).Join(Cluster.Get(_sys1).SelfAddress); // coordinator will initially run on sys1 + AwaitAssert(() => Cluster.Get(_sys1).SelfMember.Status.Should().Be(MemberStatus.Up)); + + Cluster.Get(_sys2).Join(Cluster.Get(_sys1).SelfAddress); + Within(10.Seconds(), () => + { + AwaitAssert(() => + { + Cluster.Get(_sys1).State.Members.Count.Should().Be(2); + Cluster.Get(_sys1).State.Members.All(x => x.Status == MemberStatus.Up).Should().BeTrue(); + Cluster.Get(_sys2).State.Members.Count.Should().Be(2); + Cluster.Get(_sys2).State.Members.All(x => x.Status == MemberStatus.Up).Should().BeTrue(); + }); + }); + + Cluster.Get(_sys3).Join(Cluster.Get(_sys1).SelfAddress); + Within(10.Seconds(), () => + { + AwaitAssert(() => + { + Cluster.Get(_sys1).State.Members.Count.Should().Be(3); + Cluster.Get(_sys1).State.Members.All(x => x.Status == MemberStatus.Up).Should().BeTrue(); + Cluster.Get(_sys2).State.Members.Count.Should().Be(3); + Cluster.Get(_sys2).State.Members.All(x => x.Status == MemberStatus.Up).Should().BeTrue(); + Cluster.Get(_sys3).State.Members.Count.Should().Be(3); + Cluster.Get(_sys3).State.Members.All(x => x.Status == MemberStatus.Up).Should().BeTrue(); + }); + }); + + PingEntities(); + } + + private void RunCoordinatedShutdownWhenLeaving() + { + Cluster.Get(_sys3).Leave(Cluster.Get(_sys1).SelfAddress); + _probe1.ExpectMsg("CS-unbind-1"); + + Within(20.Seconds(), () => + { + AwaitAssert(() => + { + Cluster.Get(_sys2).State.Members.Count.Should().Be(2); + Cluster.Get(_sys3).State.Members.Count.Should().Be(2); + }); + }); + + Within(10.Seconds(), () => + { + AwaitAssert(() => + { + Cluster.Get(_sys1).IsTerminated.Should().BeTrue(); + _sys1.WhenTerminated.IsCompleted.Should().BeTrue(); + }); + }); + + PingEntities(); + } + + private void RunCoordinatedShutdownWhenDowning() + { + // coordinator is on Sys2 + Cluster.Get(_sys2).Down(Cluster.Get(_sys3).SelfAddress); + _probe3.ExpectMsg("CS-unbind-3"); + + Within(20.Seconds(), () => + { + AwaitAssert(() => + { + Cluster.Get(_sys2).State.Members.Count.Should().Be(1); + }); + }); + + Within(10.Seconds(), () => + { + AwaitAssert(() => + { + Cluster.Get(_sys3).IsTerminated.Should().BeTrue(); + _sys3.WhenTerminated.IsCompleted.Should().BeTrue(); + }); + }); + + PingEntities(); + } + } +} diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/ShardRegion.cs b/src/contrib/cluster/Akka.Cluster.Sharding/ShardRegion.cs index f3c4b3d6069..3dcdc1fb750 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/ShardRegion.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/ShardRegion.cs @@ -379,8 +379,15 @@ private void SetupCoordinatedShutdown() var self = Self; _coordShutdown.AddTask(CoordinatedShutdown.PhaseClusterShardingShutdownRegion, "region-shutdown", () => { - self.Tell(GracefulShutdown.Instance); - return _gracefulShutdownProgress.Task; + if (Cluster.IsTerminated || Cluster.SelfMember.Status == MemberStatus.Down) + { + return Task.FromResult(Done.Instance); + } + else + { + self.Tell(GracefulShutdown.Instance); + return _gracefulShutdownProgress.Task; + } }); } diff --git a/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs b/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs index c519b70a88a..e6fc0ea1daf 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs @@ -571,11 +571,24 @@ public ClusterSingletonManager(Props singletonProps, object terminationMessage, private void SetupCoordinatedShutdown() { var self = Self; - _coordShutdown.AddTask(CoordinatedShutdown.PhaseClusterExiting, "wait-singleton-exiting", () => _memberExitingProgress.Task); + _coordShutdown.AddTask(CoordinatedShutdown.PhaseClusterExiting, "wait-singleton-exiting", () => + { + if (_cluster.IsTerminated || _cluster.SelfMember.Status == MemberStatus.Down) + return Task.FromResult(Done.Instance); + else + return _memberExitingProgress.Task; + }); _coordShutdown.AddTask(CoordinatedShutdown.PhaseClusterExiting, "singleton-exiting-2", () => { - var timeout = _coordShutdown.Timeout(CoordinatedShutdown.PhaseClusterExiting); - return self.Ask(SelfExiting.Instance, timeout).ContinueWith(tr => Done.Instance); + if (_cluster.IsTerminated || _cluster.SelfMember.Status == MemberStatus.Down) + { + return Task.FromResult(Done.Instance); + } + else + { + var timeout = _coordShutdown.Timeout(CoordinatedShutdown.PhaseClusterExiting); + return self.Ask(SelfExiting.Instance, timeout).ContinueWith(tr => Done.Instance); + } }); } @@ -751,7 +764,8 @@ private void InitializeFSM() // transition when OldestChanged return Stay().Using(new YoungerData(null)); } - else if (e.FsmEvent is HandOverToMe) { + else if (e.FsmEvent is HandOverToMe) + { // this node was probably quickly restarted with same hostname:port, // confirm that the old singleton instance has been stopped Sender.Tell(HandOverDone.Instance); diff --git a/src/contrib/cluster/Akka.Cluster.Tools/Singleton/OldestChangedBuffer.cs b/src/contrib/cluster/Akka.Cluster.Tools/Singleton/OldestChangedBuffer.cs index 2e1f83e217b..bf7ff57398a 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/Singleton/OldestChangedBuffer.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/Singleton/OldestChangedBuffer.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Immutable; using System.Linq; +using System.Threading.Tasks; using Akka.Actor; using Akka.Util.Internal; @@ -115,8 +116,15 @@ private void SetupCoordinatedShutdown() var self = Self; _coordShutdown.AddTask(CoordinatedShutdown.PhaseClusterExiting, "singleton-exiting-1", () => { - var timeout = _coordShutdown.Timeout(CoordinatedShutdown.PhaseClusterExiting); - return self.Ask(SelfExiting.Instance, timeout).ContinueWith(tr => Done.Instance); + if (_cluster.IsTerminated || _cluster.SelfMember.Status == MemberStatus.Down) + { + return Task.FromResult(Done.Instance); + } + else + { + var timeout = _coordShutdown.Timeout(CoordinatedShutdown.PhaseClusterExiting); + return self.Ask(SelfExiting.Instance, timeout).ContinueWith(tr => Done.Instance); + } }); } @@ -172,9 +180,13 @@ private void Remove(Member member) private void SendFirstChange() { - object change; - _changes = _changes.Dequeue(out change); - Context.Parent.Tell(change); + // don't send cluster change events if this node is shutting its self down, just wait for SelfExiting + if (!_cluster.IsTerminated) + { + object change; + _changes = _changes.Dequeue(out change); + Context.Parent.Tell(change); + } } /// @@ -231,7 +243,7 @@ private void OnDeliverNext(object message) } else if (message is ClusterEvent.MemberRemoved) { - var removed = (ClusterEvent.MemberRemoved) message; + var removed = (ClusterEvent.MemberRemoved)message; Remove(removed.Member); DeliverChanges(); } diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCluster.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCluster.approved.txt index 65be772a7d0..96ac4d2ebe0 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCluster.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCluster.approved.txt @@ -25,6 +25,7 @@ namespace Akka.Cluster public Akka.Remote.DefaultFailureDetectorRegistry FailureDetector { get; } public bool IsTerminated { get; } public Akka.Actor.Address SelfAddress { get; } + public Akka.Cluster.Member SelfMember { get; } public System.Collections.Immutable.ImmutableHashSet SelfRoles { get; } public Akka.Cluster.UniqueAddress SelfUniqueAddress { get; } public Akka.Cluster.ClusterSettings Settings { get; } diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index 3e8dc25f50c..db2f658843e 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -503,12 +503,35 @@ namespace Akka.Actor public const string PhaseServiceRequestsDone = "service-requests-done"; public const string PhaseServiceStop = "service-stop"; public const string PhaseServiceUnbind = "service-unbind"; + public Akka.Actor.CoordinatedShutdown.Reason ShutdownReason { get; } public Akka.Actor.ExtendedActorSystem System { get; } public System.TimeSpan TotalTimeout { get; } public void AddTask(string phase, string taskName, System.Func> task) { } public static Akka.Actor.CoordinatedShutdown Get(Akka.Actor.ActorSystem sys) { } + [System.ObsoleteAttribute("Use the method with \'reason\' parameter instead")] public System.Threading.Tasks.Task Run(string fromPhase = null) { } + public System.Threading.Tasks.Task Run(Akka.Actor.CoordinatedShutdown.Reason reason, string fromPhase = null) { } public System.TimeSpan Timeout(string phase) { } + public class ClrExitReason : Akka.Actor.CoordinatedShutdown.Reason + { + public static Akka.Actor.CoordinatedShutdown.Reason Instance; + } + public class ClusterDowningReason : Akka.Actor.CoordinatedShutdown.Reason + { + public static Akka.Actor.CoordinatedShutdown.Reason Instance; + } + public class ClusterLeavingReason : Akka.Actor.CoordinatedShutdown.Reason + { + public static Akka.Actor.CoordinatedShutdown.Reason Instance; + } + public class Reason + { + protected Reason() { } + } + public class UnknownReason : Akka.Actor.CoordinatedShutdown.Reason + { + public static Akka.Actor.CoordinatedShutdown.Reason Instance; + } } public sealed class CoordinatedShutdownExtension : Akka.Actor.ExtensionIdProvider { diff --git a/src/core/Akka.Cluster.Tests/ClusterSpec.cs b/src/core/Akka.Cluster.Tests/ClusterSpec.cs index b7387095561..86a976eaff6 100644 --- a/src/core/Akka.Cluster.Tests/ClusterSpec.cs +++ b/src/core/Akka.Cluster.Tests/ClusterSpec.cs @@ -189,8 +189,9 @@ public void A_cluster_must_complete_LeaveAsync_task_upon_being_removed() leaveTask.IsCompleted.Should().BeFalse(); probe.ExpectMsg(); - probe.ExpectMsg(); - probe.ExpectMsg(); + // MemberExited might not be published before MemberRemoved + var removed = (ClusterEvent.MemberRemoved)probe.FishForMessage(m => m is ClusterEvent.MemberRemoved); + removed.PreviousStatus.ShouldBeEquivalentTo(MemberStatus.Exiting); AwaitCondition(() => leaveTask.IsCompleted); @@ -492,11 +493,45 @@ public void A_cluster_must_leave_via_CoordinatedShutdownRun() Cluster.Get(sys2).Join(Cluster.Get(sys2).SelfAddress); probe.ExpectMsg(); - CoordinatedShutdown.Get(sys2).Run(); + CoordinatedShutdown.Get(sys2).Run(CoordinatedShutdown.UnknownReason.Instance); probe.ExpectMsg(); - probe.ExpectMsg(); - probe.ExpectMsg(); + // MemberExited might not be published before MemberRemoved + var removed = (ClusterEvent.MemberRemoved)probe.FishForMessage(m => m is ClusterEvent.MemberRemoved); + removed.PreviousStatus.ShouldBeEquivalentTo(MemberStatus.Exiting); + } + finally + { + Shutdown(sys2); + } + } + + [Fact] + public void A_cluster_must_leave_via_CoordinatedShutdownRun_when_member_status_is_Joining() + { + var sys2 = ActorSystem.Create("ClusterSpec2", ConfigurationFactory.ParseString(@" + akka.actor.provider = ""cluster"" + akka.remote.dot-netty.tcp.port = 0 + akka.coordinated-shutdown.run-by-clr-shutdown-hook = off + akka.coordinated-shutdown.terminate-actor-system = off + akka.cluster.run-coordinated-shutdown-when-down = off + akka.cluster.min-nr-of-members = 2 + ").WithFallback(Akka.TestKit.Configs.TestConfigs.DefaultConfig)); + + try + { + var probe = CreateTestProbe(sys2); + Cluster.Get(sys2).Subscribe(probe.Ref, typeof(ClusterEvent.IMemberEvent)); + probe.ExpectMsg(); + Cluster.Get(sys2).Join(Cluster.Get(sys2).SelfAddress); + probe.ExpectMsg(); + + CoordinatedShutdown.Get(sys2).Run(CoordinatedShutdown.UnknownReason.Instance); + + probe.ExpectMsg(); + // MemberExited might not be published before MemberRemoved + var removed = (ClusterEvent.MemberRemoved)probe.FishForMessage(m => m is ClusterEvent.MemberRemoved); + removed.PreviousStatus.ShouldBeEquivalentTo(MemberStatus.Exiting); } finally { @@ -524,10 +559,12 @@ public void A_cluster_must_terminate_ActorSystem_via_leave_CoordinatedShutdown() Cluster.Get(sys2).Leave(Cluster.Get(sys2).SelfAddress); probe.ExpectMsg(); - probe.ExpectMsg(); - probe.ExpectMsg(); + // MemberExited might not be published before MemberRemoved + var removed = (ClusterEvent.MemberRemoved)probe.FishForMessage(m => m is ClusterEvent.MemberRemoved); + removed.PreviousStatus.ShouldBeEquivalentTo(MemberStatus.Exiting); AwaitCondition(() => sys2.WhenTerminated.IsCompleted, TimeSpan.FromSeconds(10)); Cluster.Get(sys2).IsTerminated.Should().BeTrue(); + CoordinatedShutdown.Get(sys2).ShutdownReason.Should().BeOfType(); } finally { @@ -559,6 +596,7 @@ public void A_cluster_must_terminate_ActorSystem_via_Down_CoordinatedShutdown() probe.ExpectMsg(); AwaitCondition(() => sys3.WhenTerminated.IsCompleted, TimeSpan.FromSeconds(10)); Cluster.Get(sys3).IsTerminated.Should().BeTrue(); + CoordinatedShutdown.Get(sys3).ShutdownReason.Should().BeOfType(); } finally { diff --git a/src/core/Akka.Cluster/Cluster.cs b/src/core/Akka.Cluster/Cluster.cs index 417b85025dd..ff967a9fc19 100644 --- a/src/core/Akka.Cluster/Cluster.cs +++ b/src/core/Akka.Cluster/Cluster.cs @@ -464,6 +464,11 @@ public ImmutableHashSet SelfRoles /// public ClusterEvent.CurrentClusterState State { get { return _readView._state; } } + /// + /// Access to the current member info for this node. + /// + public Member SelfMember => _readView.Self; + private readonly AtomicBoolean _isTerminated = new AtomicBoolean(false); /// diff --git a/src/core/Akka.Cluster/ClusterDaemon.cs b/src/core/Akka.Cluster/ClusterDaemon.cs index 2c59a58d49e..3a791d866c7 100644 --- a/src/core/Akka.Cluster/ClusterDaemon.cs +++ b/src/core/Akka.Cluster/ClusterDaemon.cs @@ -398,7 +398,7 @@ public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - return obj is ExitingConfirmed && Equals((ExitingConfirmed) obj); + return obj is ExitingConfirmed && Equals((ExitingConfirmed)obj); } /// @@ -872,7 +872,7 @@ private void AddCoordinatedLeave() var self = Self; _coordShutdown.AddTask(CoordinatedShutdown.PhaseClusterLeave, "leave", () => { - if (Cluster.Get(sys).IsTerminated) + if (Cluster.Get(sys).IsTerminated || Cluster.Get(sys).SelfMember.Status == MemberStatus.Down) { return Task.FromResult(Done.Instance); } @@ -898,8 +898,8 @@ protected override void PostStop() _clusterPromise.TrySetResult(Done.Instance); if (_settings.RunCoordinatedShutdownWhenDown) { - // run the last phases if the node was downed (not leaving) - _coordShutdown.Run(CoordinatedShutdown.PhaseClusterShutdown); + // if it was stopped due to leaving CoordinatedShutdown was started earlier + _coordShutdown.Run(CoordinatedShutdown.ClusterDowningReason.Instance); } } } @@ -941,7 +941,7 @@ protected override SupervisorStrategy SupervisorStrategy() { return new OneForOneStrategy(e => { - //TODO: JVM version matches NonFatal. Can / should we do something similar? + //TODO: JVM version matches NonFatal. Can / should we do something similar? _log.Error(e, "Cluster node [{0}] crashed, [{1}] - shutting down...", Cluster.Get(Context.System).SelfAddress, e); Self.Tell(PoisonPill.Instance); @@ -968,7 +968,7 @@ private void CreateChildren() /// /// INTERNAL API - /// + /// /// Actor used to power the guts of the Akka.Cluster membership and gossip protocols. /// internal class ClusterCoreDaemon : UntypedActor, IRequiresMessageQueue @@ -1084,7 +1084,7 @@ private void AddCoordinatedLeave() }); _coordShutdown.AddTask(CoordinatedShutdown.PhaseClusterExitingDone, "exiting-completed", () => { - if (Cluster.Get(sys).IsTerminated) + if (Cluster.Get(sys).IsTerminated || Cluster.Get(sys).SelfMember.Status == MemberStatus.Down) return TaskEx.Completed; else { @@ -1301,7 +1301,7 @@ private void BecomeUninitialized() private void BecomeInitialized() { // start heartbeatSender here, and not in constructor to make sure that - // heartbeating doesn't start before Welcome is received + // heartbeating doesn't start before Welcome is received Context.ActorOf(Props.Create().WithDispatcher(_cluster.Settings.UseDispatcher), "heartbeatSender"); // make sure that join process is stopped @@ -1320,7 +1320,7 @@ private void Initialized(object message) { var ge = message as GossipEnvelope; var receivedType = ReceiveGossip(ge); - if(_cluster.Settings.VerboseGossipReceivedLogging) + if (_cluster.Settings.VerboseGossipReceivedLogging) _log.Debug("Cluster Node [{0}] - Received gossip from [{1}] which was {2}.", _cluster.SelfAddress, ge.From, receivedType); } else if (message is GossipStatus) @@ -1394,7 +1394,7 @@ private void Initialized(object message) } else if (message is InternalClusterAction.ExitingConfirmed) { - var c = (InternalClusterAction.ExitingConfirmed) message; + var c = (InternalClusterAction.ExitingConfirmed)message; ReceiveExitingConfirmed(c.Address); } else if (ReceiveExitingCompleted(message)) { } @@ -1668,7 +1668,7 @@ public void Welcome(Address joinWith, UniqueAddress from, Gossip gossip) public void Leaving(Address address) { // only try to update if the node is available (in the member ring) - if (_latestGossip.Members.Any(m => m.Address.Equals(address) && m.Status == MemberStatus.Up)) + if (_latestGossip.Members.Any(m => m.Address.Equals(address) && (m.Status == MemberStatus.Joining || m.Status == MemberStatus.WeaklyUp || m.Status == MemberStatus.Up))) { // mark node as LEAVING var newMembers = _latestGossip.Members.Select(m => @@ -1974,7 +1974,7 @@ public ReceiveGossipType ReceiveGossip(GossipEnvelope envelope) _exitingTasksInProgress = true; _log.Info("Exiting, starting coordinated shutdown."); _selfExiting.TrySetResult(Done.Instance); - _coordShutdown.Run(); + _coordShutdown.Run(CoordinatedShutdown.ClusterLeavingReason.Instance); } if (talkback) @@ -2100,7 +2100,7 @@ public double AdjustedGossipDifferentViewProbability // linear reduction of the probability with increasing number of nodes // from ReduceGossipDifferentViewProbability at ReduceGossipDifferentViewProbability nodes // to ReduceGossipDifferentViewProbability / 10 at ReduceGossipDifferentViewProbability * 3 nodes - // i.e. default from 0.8 at 400 nodes, to 0.08 at 1600 nodes + // i.e. default from 0.8 at 400 nodes, to 0.08 at 1600 nodes var k = (minP - _cluster.Settings.GossipDifferentViewProbability) / (high - low); return _cluster.Settings.GossipDifferentViewProbability + (size - low) * k; } @@ -2208,7 +2208,7 @@ private void ShutdownSelfWhenDown() /// this function will check to see if that threshold is met. /// /// - /// true if the setting isn't enabled or is satisfied. + /// true if the setting isn't enabled or is satisfied. /// false is the setting is enabled and unsatisfied. /// public bool IsMinNrOfMembersFulfilled() @@ -2320,7 +2320,7 @@ public void LeaderActionsOnConvergence() _exitingTasksInProgress = true; _log.Info("Exiting (leader), starting coordinated shutdown."); _selfExiting.TrySetResult(Done.Instance); - _coordShutdown.Run(); + _coordShutdown.Run(CoordinatedShutdown.ClusterLeavingReason.Instance); } UpdateLatestGossip(newGossip); @@ -2559,21 +2559,21 @@ public void PublishInternalStats() /// /// INTERNAL API - /// + /// /// Sends to all seed nodes (except itself) and expect /// reply back. The seed node that replied first /// will be used and joined to. replies received after /// the first one are ignored. - /// - /// Retries if no replies are received within the + /// + /// Retries if no replies are received within the /// . When at least one reply has been received it stops itself after /// an idle . - /// + /// /// The seed nodes can be started in any order, but they will not be "active" until they have been /// able to join another seed node (seed1.) - /// + /// /// They will retry the join procedure. - /// + /// /// Possible scenarios: /// 1. seed2 started, but doesn't get any ack from seed1 or seed3 /// 2. seed3 started, doesn't get any ack from seed1 or seed3 (seed2 doesn't reply) @@ -2665,12 +2665,12 @@ private void Done(object message) /// /// INTERNAL API - /// + /// /// Used only for the first seed node. /// Sends to all seed nodes except itself. - /// If other seed nodes are not part of the cluster yet they will reply with + /// If other seed nodes are not part of the cluster yet they will reply with /// or not respond at all and then the - /// first seed node will join itself to initialize the new cluster. When the first seed + /// first seed node will join itself to initialize the new cluster. When the first seed /// node is restarted, and some other seed node is part of the cluster it will reply with /// and then the first seed node will /// join that other seed node to join the existing cluster. @@ -2902,7 +2902,7 @@ public GossipStats Copy(long? receivedGossipCount = null, /// /// INTERNAL API - /// + /// /// The supplied callback will be run once when the current cluster member has the same status. /// internal class OnMemberStatusChangedListener : ReceiveActor diff --git a/src/core/Akka.Cluster/Member.cs b/src/core/Akka.Cluster/Member.cs index 8699e115c8b..423531b8bbe 100644 --- a/src/core/Akka.Cluster/Member.cs +++ b/src/core/Akka.Cluster/Member.cs @@ -18,7 +18,7 @@ namespace Akka.Cluster /// Represents the address, current status, and roles of a cluster member node. /// /// - /// NOTE: and are solely based on the underlying , + /// NOTE: and are solely based on the underlying , /// not its and roles. /// public class Member : IComparable, IComparable @@ -169,7 +169,7 @@ public Member Copy(MemberStatus status) //TODO: Akka exception? if (!AllowedTransitions[oldStatus].Contains(status)) throw new InvalidOperationException($"Invalid member status transition {Status} -> {status}"); - + return new Member(UniqueAddress, UpNumber, status, Roles); } @@ -305,7 +305,7 @@ public static ImmutableSortedSet PickNextTransition(IEnumerable /// /// First member instance. /// Second member instance. - /// If a and b are different members, this method will return null. + /// If a and b are different members, this method will return null. /// Otherwise, will return a or b depending on which one is a valid transition of the other. /// If neither are a valid transition, we return null public static Member PickNextTransition(Member a, Member b) @@ -386,8 +386,8 @@ public static Member HighestPriorityOf(Member m1, Member m2) internal static readonly ImmutableDictionary> AllowedTransitions = new Dictionary> { - {MemberStatus.Joining, ImmutableHashSet.Create(MemberStatus.WeaklyUp, MemberStatus.Up, MemberStatus.Down, MemberStatus.Removed)}, - {MemberStatus.WeaklyUp, ImmutableHashSet.Create(MemberStatus.Up, MemberStatus.Down, MemberStatus.Removed) }, + {MemberStatus.Joining, ImmutableHashSet.Create(MemberStatus.WeaklyUp, MemberStatus.Up,MemberStatus.Leaving, MemberStatus.Down, MemberStatus.Removed)}, + {MemberStatus.WeaklyUp, ImmutableHashSet.Create(MemberStatus.Up, MemberStatus.Leaving, MemberStatus.Down, MemberStatus.Removed) }, {MemberStatus.Up, ImmutableHashSet.Create(MemberStatus.Leaving, MemberStatus.Down, MemberStatus.Removed)}, {MemberStatus.Leaving, ImmutableHashSet.Create(MemberStatus.Exiting, MemberStatus.Down, MemberStatus.Removed)}, {MemberStatus.Down, ImmutableHashSet.Create(MemberStatus.Removed)}, @@ -399,7 +399,7 @@ public static Member HighestPriorityOf(Member m1, Member m2) /// /// Defines the current status of a cluster member node - /// + /// /// Can be one of: Joining, Up, WeaklyUp, Leaving, Exiting and Down. /// public enum MemberStatus diff --git a/src/core/Akka.Tests/Actor/CoordinatedShutdownSpec.cs b/src/core/Akka.Tests/Actor/CoordinatedShutdownSpec.cs index 1e5f6cdb7be..a75eb1aef6c 100644 --- a/src/core/Akka.Tests/Actor/CoordinatedShutdownSpec.cs +++ b/src/core/Akka.Tests/Actor/CoordinatedShutdownSpec.cs @@ -50,6 +50,13 @@ private List CheckTopologicalSort(Dictionary phases) return result; } + private class CustomReason : CoordinatedShutdown.Reason + { + } + + private static CoordinatedShutdown.Reason customReason = new CustomReason(); + + [Fact] public void CoordinatedShutdown_must_sort_phases_in_topological_order() { @@ -200,7 +207,7 @@ public void CoordinatedShutdown_must_run_ordered_phases() return TaskEx.Completed; }); - co.Run().Wait(RemainingOrDefault); + co.Run(CoordinatedShutdown.UnknownReason.Instance).Wait(RemainingOrDefault); ReceiveN(4).Should().Equal(new object[] { "A", "B", "B", "C" }); } @@ -233,8 +240,9 @@ public void CoordinatedShutdown_must_run_from_given_phase() return TaskEx.Completed; }); - co.Run("b").Wait(RemainingOrDefault); + co.Run(customReason, "b").Wait(RemainingOrDefault); ReceiveN(2).Should().Equal(new object[] { "B", "C" }); + co.ShutdownReason.ShouldBeEquivalentTo(customReason); } [Fact] @@ -252,11 +260,14 @@ public void CoordinatedShutdown_must_only_run_once() return TaskEx.Completed; }); - co.Run().Wait(RemainingOrDefault); + co.ShutdownReason.Should().BeNull(); + co.Run(customReason).Wait(RemainingOrDefault); + co.ShutdownReason.ShouldBeEquivalentTo(customReason); ExpectMsg("A"); - co.Run().Wait(RemainingOrDefault); + co.Run(CoordinatedShutdown.UnknownReason.Instance).Wait(RemainingOrDefault); TestActor.Tell("done"); ExpectMsg("done"); // no additional A + co.ShutdownReason.ShouldBeEquivalentTo(customReason); } [Fact] @@ -295,7 +306,7 @@ public void CoordinatedShutdown_must_continue_after_timeout_or_failure() return TaskEx.Completed; }); - co.Run().Wait(RemainingOrDefault); + co.Run(CoordinatedShutdown.UnknownReason.Instance).Wait(RemainingOrDefault); ExpectMsg("A"); ExpectMsg("A"); ExpectMsg("B"); @@ -324,7 +335,7 @@ public void CoordinatedShutdown_must_abort_if_recover_is_off() return TaskEx.Completed; }); - var result = co.Run(); + var result = co.Run(CoordinatedShutdown.UnknownReason.Instance); ExpectMsg("B"); Intercept(() => { @@ -362,7 +373,7 @@ public void CoordinatedShutdown_must_be_possible_to_add_tasks_in_later_phase_fro return TaskEx.Completed; }); - co.Run().Wait(RemainingOrDefault); + co.Run(CoordinatedShutdown.UnknownReason.Instance).Wait(RemainingOrDefault); ExpectMsg("A"); ExpectMsg("B"); } @@ -394,10 +405,11 @@ public void CoordinatedShutdown_must_be_possible_to_parse_phases_from_config() [Fact] public void CoordinatedShutdown_must_terminate_ActorSystem() { - var shutdownSystem = CoordinatedShutdown.Get(Sys).Run(); + var shutdownSystem = CoordinatedShutdown.Get(Sys).Run(customReason); shutdownSystem.Wait(TimeSpan.FromSeconds(10)).Should().BeTrue(); Sys.WhenTerminated.IsCompleted.Should().BeTrue(); + CoordinatedShutdown.Get(Sys).ShutdownReason.ShouldBeEquivalentTo(customReason); } } } diff --git a/src/core/Akka/Actor/CoordinatedShutdown.cs b/src/core/Akka/Actor/CoordinatedShutdown.cs index 7ffc32b013d..bb0d7fd8760 100644 --- a/src/core/Akka/Actor/CoordinatedShutdown.cs +++ b/src/core/Akka/Actor/CoordinatedShutdown.cs @@ -152,6 +152,76 @@ public static CoordinatedShutdown Get(ActorSystem sys) public const string PhaseBeforeActorSystemTerminate = "before-actor-system-terminate"; public const string PhaseActorSystemTerminate = "actor-system-terminate"; + + + /// + /// Reason for the shutdown, which can be used by tasks in case they need to do + /// different things depending on what caused the shutdown. There are some + /// predefined reasons, but external libraries applications may also define + /// other reasons. + /// + public class Reason + { + protected Reason() + { + + } + } + + /// + /// The reason for the shutdown was unknown. Needed for backwards compatibility. + /// + public class UnknownReason : Reason + { + public static Reason Instance = new UnknownReason(); + + private UnknownReason() + { + + } + } + + /// + /// The shutdown was initiated by a CLR shutdown hook + /// + public class ClrExitReason : Reason + { + public static Reason Instance = new ClrExitReason(); + + private ClrExitReason() + { + + } + } + + + /// + /// The shutdown was initiated by Cluster downing. + /// + public class ClusterDowningReason : Reason + { + public static Reason Instance = new ClusterDowningReason(); + + private ClusterDowningReason() + { + + } + } + + + /// + /// The shutdown was initiated by Cluster leaving. + /// + public class ClusterLeavingReason : Reason + { + public static Reason Instance = new ClusterLeavingReason(); + + private ClusterLeavingReason() + { + + } + } + /// /// The /// @@ -176,7 +246,7 @@ public static CoordinatedShutdown Get(ActorSystem sys) private readonly ConcurrentBag>> _clrShutdownTasks = new ConcurrentBag>>(); private readonly ConcurrentDictionary>>>> _tasks = new ConcurrentDictionary>>>>(); - private readonly AtomicBoolean _runStarted = new AtomicBoolean(false); + private readonly AtomicReference _runStarted = new AtomicReference(null); private readonly AtomicBoolean _clrHooksStarted = new AtomicBoolean(false); private readonly TaskCompletionSource _runPromise = new TaskCompletionSource(); private readonly TaskCompletionSource _hooksRunPromise = new TaskCompletionSource(); @@ -185,14 +255,14 @@ public static CoordinatedShutdown Get(ActorSystem sys) /// /// INTERNAL API - /// + /// /// Signals when CLR shutdown hooks have been completed /// internal Task ClrShutdownTask => _hooksRunPromise.Task; /// /// Add a task to a phase. It doesn't remove previously added tasks. - /// + /// /// Tasks added to the same phase are executed in parallel without any /// ordering assumptions. Next phase will not start until all tasks of /// previous phase have completed. @@ -204,8 +274,8 @@ public static CoordinatedShutdown Get(ActorSystem sys) /// Tasks should typically be registered as early as possible after system /// startup. When running the tasks that have been /// registered will be performed but tasks that are added too late will not be run. - /// - /// + /// + /// /// It is possible to add a task to a later phase from within a task in an earlier phase /// and it will be performed. /// @@ -230,7 +300,7 @@ public void AddTask(string phase, string taskName, Func> task) /// /// Add a shutdown hook that will execute when the CLR process begins /// its shutdown sequence, invoked via . - /// + /// /// Added hooks may run in any order concurrently, but they are run before /// the Akka.NET internal shutdown hooks execute. /// @@ -246,10 +316,10 @@ internal void AddClrShutdownHook(Func> hook) /// /// INTERNAL API - /// + /// /// Should only be called directly by the event /// in production. - /// + /// /// Safe to call multiple times, but hooks will only be run once. /// /// Returns a that will be completed once the process exits. @@ -283,6 +353,12 @@ private Task RunClrHooks() return ClrShutdownTask; } + /// + /// The for the shutdown as passed to the method. if the shutdown + /// has not been started. + /// + public Reason ShutdownReason => _runStarted.Value; + /// /// Run tasks of all phases including and after the given phase. /// @@ -290,11 +366,27 @@ private Task RunClrHooks() /// A task that is completed when all such tasks have been completed, or /// there is failure when is disabled. /// - /// It is safe to call this method multiple times. It will only run once. + /// It is safe to call this method multiple times. It will only run the shutdown sequence once. /// + [Obsolete("Use the method with 'reason' parameter instead")] public Task Run(string fromPhase = null) { - if (_runStarted.CompareAndSet(false, true)) + return Run(UnknownReason.Instance, fromPhase); + } + + /// + /// Run tasks of all phases including and after the given phase. + /// + /// Reason of the shutdown + /// Optional. The phase to start the run from. + /// A task that is completed when all such tasks have been completed, or + /// there is failure when is disabled. + /// + /// It is safe to call this method multiple times. It will only run the shutdown sequence once. + /// + public Task Run(Reason reason, string fromPhase = null) + { + if (_runStarted.CompareAndSet(null, reason)) { var debugEnabled = Log.IsDebugEnabled; @@ -412,7 +504,7 @@ Task Loop(List remainingPhases) var done = Loop(runningPhases); done.ContinueWith(tr => { - if(!tr.IsFaulted && !tr.IsCanceled) + if (!tr.IsFaulted && !tr.IsCanceled) _runPromise.SetResult(tr.Result); else { @@ -511,7 +603,7 @@ void DepthFirstSearch(string u) /// /// INTERNAL API - /// + /// /// Primes the with the default phase for /// /// @@ -554,7 +646,8 @@ internal static void InitPhaseActorSystemTerminate(ActorSystem system, Config co } return Done.Instance; }); - } else if (exitClr) + } + else if (exitClr) { Environment.Exit(0); return TaskEx.Completed; @@ -599,7 +692,7 @@ internal static void InitClrHook(ActorSystem system, Config conf, CoordinatedShu coord.Log.Info("Starting coordinated shutdown from CLR termination hook."); try { - coord.Run().Wait(coord.TotalTimeout); + coord.Run(ClrExitReason.Instance).Wait(coord.TotalTimeout); } catch (Exception ex) { From 9d5bc4c4ba2884f05dbebb18500418446e753116 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 31 Jul 2018 12:56:00 -0700 Subject: [PATCH 63/70] fixed documentation example project build (#3561) --- docs/examples/DocsExamples/DocsExamples.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/examples/DocsExamples/DocsExamples.csproj b/docs/examples/DocsExamples/DocsExamples.csproj index 02eff16bc06..49f4c2f886b 100644 --- a/docs/examples/DocsExamples/DocsExamples.csproj +++ b/docs/examples/DocsExamples/DocsExamples.csproj @@ -1,5 +1,5 @@  - + Exe net461 @@ -18,8 +18,8 @@ - - + + \ No newline at end of file From fcf5314636088f78ac3f4ed1ea21037e38da0585 Mon Sep 17 00:00:00 2001 From: Nyola Mike Date: Sun, 5 Aug 2018 08:10:12 +0300 Subject: [PATCH 64/70] _lastTemperatureReading should be updatable private readonly double? _lastTemperatureReading = null; on line 61 would make this field not editable because of the red only modifier --- docs/examples/Tutorials/Tutorial2/DeviceInProgress.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/Tutorials/Tutorial2/DeviceInProgress.cs b/docs/examples/Tutorials/Tutorial2/DeviceInProgress.cs index 4500f909724..c65b04edfd1 100644 --- a/docs/examples/Tutorials/Tutorial2/DeviceInProgress.cs +++ b/docs/examples/Tutorials/Tutorial2/DeviceInProgress.cs @@ -58,7 +58,7 @@ public RespondTemperature(long requestId, double? value) #region device-with-read public class Device : UntypedActor { - private readonly double? _lastTemperatureReading = null; + private double? _lastTemperatureReading = null; public Device(string groupId, string deviceId) { From 7220f2d7513b648b6bcb502d3e5702fb2da01103 Mon Sep 17 00:00:00 2001 From: Ismael Hamed Date: Sun, 29 Jul 2018 13:18:26 +0200 Subject: [PATCH 65/70] Sequence number should not be set if snapshot is unhandled --- .../Akka.Persistence.Tests/SnapshotSpec.cs | 60 +++++++++++++++++++ .../Akka.Persistence/Eventsourced.Recovery.cs | 12 ++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/core/Akka.Persistence.Tests/SnapshotSpec.cs b/src/core/Akka.Persistence.Tests/SnapshotSpec.cs index 6f8bcf67472..9ce043fd0d6 100644 --- a/src/core/Akka.Persistence.Tests/SnapshotSpec.cs +++ b/src/core/Akka.Persistence.Tests/SnapshotSpec.cs @@ -111,6 +111,51 @@ public override Recovery Recovery } } + internal class IgnoringSnapshotTestPersistentActor : NamedPersistentActor + { + private readonly Recovery _recovery; + private readonly IActorRef _probe; + + public IgnoringSnapshotTestPersistentActor(string name, Recovery recovery, IActorRef probe) + : base(name) + { + _probe = probe; + _recovery = recovery; + } + + protected override bool ReceiveRecover(object message) + { + switch(message) + { + case string payload: + _probe.Tell($"{payload}-{LastSequenceNr}"); + return true; + case object other when !(other is SnapshotOffer): + _probe.Tell(other); + return true; + } + return false; + } + + protected override bool ReceiveCommand(object message) + { + switch(message) + { + case string payload when payload == "done": + _probe.Tell("done"); + return true; + case string payload: + Persist(payload, _ => _probe.Tell($"{payload}-{LastSequenceNr}")); + return true; + default: + _probe.Tell(message); + return true; + } + } + + public override Recovery Recovery => _recovery; + } + public sealed class DeleteOne { public DeleteOne(SnapshotMetadata metadata) @@ -185,6 +230,21 @@ public void PersistentActor_should_recover_state_starting_from_the_most_recent_s ExpectMsg(); } + [Fact] + public void PersistentActor_should_recover_completely_if_snapshot_is_not_handled() + { + var pref = ActorOf(() => new IgnoringSnapshotTestPersistentActor(Name, new Recovery(), TestActor)); + var persistenceId = Name; + + ExpectMsg("a-1"); + ExpectMsg("b-2"); + ExpectMsg("c-3"); + ExpectMsg("d-4"); + ExpectMsg("e-5"); + ExpectMsg("f-6"); + ExpectMsg(); + } + [Fact] public void PersistentActor_should_recover_state_starting_from_the_most_recent_snapshot_matching_an_upper_sequence_number_bound() { diff --git a/src/core/Akka.Persistence/Eventsourced.Recovery.cs b/src/core/Akka.Persistence/Eventsourced.Recovery.cs index c00f2d0b1bb..cd0e94f6e08 100644 --- a/src/core/Akka.Persistence/Eventsourced.Recovery.cs +++ b/src/core/Akka.Persistence/Eventsourced.Recovery.cs @@ -88,10 +88,14 @@ private EventsourcedState RecoveryStarted(long maxReplays) timeoutCancelable.Cancel(); if (res.Snapshot != null) { - var snapshot = res.Snapshot; - LastSequenceNr = snapshot.Metadata.SequenceNr; - // Since we are recovering we can ignore the receive behavior from the stack - base.AroundReceive(recoveryBehavior, new SnapshotOffer(snapshot.Metadata, snapshot.Snapshot)); + var offer = new SnapshotOffer(res.Snapshot.Metadata, res.Snapshot.Snapshot); + var seqNr = LastSequenceNr; + LastSequenceNr = res.Snapshot.Metadata.SequenceNr; + if (!base.AroundReceive(recoveryBehavior, offer)) + { + LastSequenceNr = seqNr; + Unhandled(offer); + } } ChangeState(Recovering(recoveryBehavior, timeout)); From a8f81cc7153e4886c0455f704a67634e7cbc66d1 Mon Sep 17 00:00:00 2001 From: Oleksandr Bogomaz Date: Wed, 8 Aug 2018 19:19:53 +0300 Subject: [PATCH 66/70] Port KeepAliveConcat and UnfoldFlow (#3560) * Port KeepAliveConcat and UnfoldFlow * additional KeepAliveConcat test --- .../CoreAPISpec.ApproveStreams.approved.txt | 15 + .../Dsl/KeepAliveConcatSpec.cs | 159 ++++++ .../Akka.Streams.Tests/Dsl/UnfoldFlowSpec.cs | 481 ++++++++++++++++++ src/core/Akka.Streams/Dsl/KeepAliveConcat.cs | 136 +++++ src/core/Akka.Streams/Dsl/SourceGen.cs | 155 ++++++ src/core/Akka.Streams/Dsl/UnfoldFlow.cs | 95 ++++ 6 files changed, 1041 insertions(+) create mode 100644 src/core/Akka.Streams.Tests/Dsl/KeepAliveConcatSpec.cs create mode 100644 src/core/Akka.Streams.Tests/Dsl/UnfoldFlowSpec.cs create mode 100644 src/core/Akka.Streams/Dsl/KeepAliveConcat.cs create mode 100644 src/core/Akka.Streams/Dsl/SourceGen.cs create mode 100644 src/core/Akka.Streams/Dsl/UnfoldFlow.cs diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt index 036a2baf588..2aa606536b3 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt @@ -1358,6 +1358,14 @@ namespace Akka.Streams.Dsl public static Akka.NotUsed None(TLeft left, TRight right) { } public static TRight Right(TLeft left, TRight right) { } } + public class KeepAliveConcat : Akka.Streams.Stage.GraphStage> + { + public KeepAliveConcat(int keepAliveFailoverSize, System.TimeSpan interval, System.Func> extrapolate) { } + public Akka.Streams.Inlet In { get; } + public Akka.Streams.Outlet Out { get; } + public override Akka.Streams.FlowShape Shape { get; } + protected override Akka.Streams.Stage.GraphStageLogic CreateLogic(Akka.Streams.Attributes inheritedAttributes) { } + } public class LastElement : Akka.Streams.Stage.GraphStageWithMaterializedValue, System.Threading.Tasks.Task>> { public LastElement() { } @@ -1672,6 +1680,13 @@ namespace Akka.Streams.Dsl public Akka.Streams.Dsl.Source, Akka.NotUsed> ZipN(System.Collections.Generic.IEnumerable> sources) { } public Akka.Streams.Dsl.Source ZipWithN(System.Func, TOut2> zipper, System.Collections.Generic.IEnumerable> sources) { } } + public class static SourceGen + { + [Akka.Annotations.ApiMayChangeAttribute()] + public static Akka.Streams.Dsl.Source UnfoldFlow(TState seed, Akka.Streams.IGraph>, TMat> flow, System.TimeSpan timeout) { } + [Akka.Annotations.ApiMayChangeAttribute()] + public static Akka.Streams.Dsl.Source UnfoldFlowWith(TState seed, Akka.Streams.IGraph, TMat> flow, System.Func>> unfoldWith, System.TimeSpan timeout) { } + } public class static SourceOperations { public static Akka.Streams.Dsl.Source Aggregate(this Akka.Streams.Dsl.Source flow, TOut2 zero, System.Func fold) { } diff --git a/src/core/Akka.Streams.Tests/Dsl/KeepAliveConcatSpec.cs b/src/core/Akka.Streams.Tests/Dsl/KeepAliveConcatSpec.cs new file mode 100644 index 00000000000..cc8e91bd013 --- /dev/null +++ b/src/core/Akka.Streams.Tests/Dsl/KeepAliveConcatSpec.cs @@ -0,0 +1,159 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using Akka.Streams.Dsl; +using Akka.Streams.TestKit; +using Akka.Streams.TestKit.Tests; +using FluentAssertions; +using Xunit; + +namespace Akka.Streams.Tests.Dsl +{ + public class KeepAliveConcatSpec : Akka.TestKit.Xunit2.TestKit + { + private readonly Source, NotUsed> _sampleSource = Source.From(Enumerable.Range(1, 10).Grouped(3)); + + private IEnumerable> Expand(IEnumerable lst) + { + return lst.Select(x => new[] { x }); + } + + [Fact] + public void KeepAliveConcat_should_not_emit_additional_elements_if_upstream_is_fast_enough() + { + var t = _sampleSource + .Via(new KeepAliveConcat>(5, TimeSpan.FromSeconds(1), Expand)) + .Grouped(1000) + .RunWith(Sink.First>>(), Sys.Materializer()); + + t.AwaitResult() + .SelectMany(x => x) + .ShouldBeEquivalentTo(Enumerable.Range(1, 10), o => o.WithStrictOrdering()); + } + + [Fact] + public void KeepAliveConcat_should_emit_elements_periodically_after_silent_periods() + { + var sourceWithIdleGap = Source.From(Enumerable.Range(1, 5).Grouped(3)) + .Concat + ( + Source.From(Enumerable.Range(6, 5).Grouped(3)).InitialDelay(TimeSpan.FromSeconds(2)) + ); + + var t = sourceWithIdleGap + .Via(new KeepAliveConcat>(5, TimeSpan.FromSeconds(0.6), Expand)) + .Grouped(1000) + .RunWith(Sink.First>>(), Sys.Materializer()); + + t.AwaitResult() + .SelectMany(x => x) + .ShouldBeEquivalentTo(Enumerable.Range(1, 10), o => o.WithStrictOrdering()); + } + + [Fact] + public void KeepAliveConcat_should_immediately_pull_upstream() + { + var upstream = this.CreatePublisherProbe>(); + var downstream = this.CreateSubscriberProbe>(); + + Source.FromPublisher(upstream) + .Via(new KeepAliveConcat>(2, TimeSpan.FromSeconds(1), Expand)) + .RunWith(Sink.FromSubscriber(downstream), Sys.Materializer()); + + downstream.Request(1); + + upstream.SendNext(new[] { 1 }); + downstream.ExpectNext().ShouldBeEquivalentTo(new[] { 1 }); + + upstream.SendComplete(); + downstream.ExpectComplete(); + } + + [Fact] + public void KeepAliveConcat_should_immediately_pull_upstream_after_busy_period() + { + var upstream = this.CreatePublisherProbe>(); + var downstream = this.CreateSubscriberProbe>(); + + _sampleSource.Concat(Source.FromPublisher(upstream)) + .Via(new KeepAliveConcat>(2, TimeSpan.FromSeconds(1), Expand)) + .RunWith(Sink.FromSubscriber(downstream), Sys.Materializer()); + + downstream.Request(10); + + var actual = downstream.ExpectNextN(6); + var expected = Enumerable.Range(1, 3).Grouped(1).Concat(Enumerable.Range(4, 7).Grouped(3)); + actual.ShouldBeEquivalentTo(expected, o => o.WithStrictOrdering()); + + downstream.Request(1); + + upstream.SendNext(new[] { 1 }); + downstream.ExpectNext().ShouldBeEquivalentTo(new[] { 1 }); + + upstream.SendComplete(); + downstream.ExpectComplete(); + } + + [Fact] + public void KeepAliveConcat_should_work_if_timer_fires_before_initial_request_after_busy_period() + { + var upstream = this.CreatePublisherProbe>(); + var downstream = this.CreateSubscriberProbe>(); + + _sampleSource.Concat(Source.FromPublisher(upstream)) + .Via(new KeepAliveConcat>(2, TimeSpan.FromSeconds(1), Expand)) + .RunWith(Sink.FromSubscriber(downstream), Sys.Materializer()); + + downstream.Request(10); + + var actual = downstream.ExpectNextN(6); + var expected = Enumerable.Range(1, 3).Grouped(1).Concat(Enumerable.Range(4, 7).Grouped(3)); + actual.ShouldBeEquivalentTo(expected, o => o.WithStrictOrdering()); + + downstream.ExpectNoMsg(TimeSpan.FromSeconds(1.5)); + downstream.Request(1); + + upstream.SendComplete(); + downstream.ExpectComplete(); + } + + [Fact] + public void KeepAliveConcat_should_emit_buffered_elements_when_upstream_completed() + { + var upstream = this.CreatePublisherProbe(); + var downstream = this.CreateSubscriberProbe(); + + Source.FromPublisher(upstream) + .Via(new KeepAliveConcat(5, TimeSpan.FromSeconds(60), x => new[] { x })) + .RunWith(Sink.FromSubscriber(downstream), Sys.Materializer()); + + upstream.SendNext(1); + upstream.SendNext(2); + upstream.SendComplete(); + + downstream.Request(2); + downstream.ExpectNextN(2).ShouldBeEquivalentTo(new[] { 1, 2 }, o => o.WithStrictOrdering()); + + downstream.Request(1); + downstream.ExpectComplete(); + } + } + + public static class EnumerableExtensions + { + public static IEnumerable> Grouped(this IEnumerable enumerable, int n) + { + return enumerable + .Select((x, i) => new { X = x, I = i }) + .GroupBy(g => g.I / n) + .Select(g => g.Select(p => p.X)); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Streams.Tests/Dsl/UnfoldFlowSpec.cs b/src/core/Akka.Streams.Tests/Dsl/UnfoldFlowSpec.cs new file mode 100644 index 00000000000..3140440da64 --- /dev/null +++ b/src/core/Akka.Streams.Tests/Dsl/UnfoldFlowSpec.cs @@ -0,0 +1,481 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Streams.Dsl; +using Akka.Streams.TestKit; +using Akka.Streams.Util; +using FluentAssertions; +using Xunit; + +namespace Akka.Streams.Tests.Dsl +{ + public class UnfoldFlowSpec + { + private static readonly TimeSpan _timeout = TimeSpan.FromMilliseconds(300); + private static readonly int[] _outputs = new[] + { + 27, 82, 41, 124, 62, 31, 94, 47, 142, 71, 214, 107, 322, 161, 484, 242, 121, 364, 182, 91, 274, 137, + 412, 206, 103, 310, 155, 466, 233, 700, 350, 175, 526, 263, 790, 395, 1186, 593, 1780, 890, 445, 1336, + 668, 334, 167, 502, 251, 754, 377, 1132, 566, 283, 850, 425, 1276, 638, 319, 958, 479, 1438, 719, 2158, + 1079, 3238, 1619, 4858, 2429, 7288, 3644, 1822, 911, 2734, 1367, 4102, 2051, 6154, 3077, 9232, 4616, + 2308, 1154, 577, 1732, 866, 433, 1300, 650, 325, 976, 488, 244, 122, 61, 184, 92, 46, 23, 70, 35, 106, + 53, 160, 80, 40, 20, 10, 5, 16, 8, 4, 2 + }; + + public class WithSimpleFlow : Akka.TestKit.Xunit2.TestKit + { + private readonly Exception _done = new Exception("done"); + private readonly Source, TestPublisher.Probe>>> _source; + + public WithSimpleFlow() + { + var controlledFlow = Flow.FromSinkAndSource(this.SinkProbe(), this.SourceProbe>(), Keep.Both); + _source = SourceGen.UnfoldFlow(1, controlledFlow, _timeout); + } + + [Fact] + public void UnfoldFlow_should_unfold_Collatz_conjecture_with_a_sequence_of_111_elements_with_flow() + { + Tuple Map(int x) + { + if (x == 1) + throw _done; + + if (x % 2 == 0) + return Tuple.Create(x / 2, x); + + return Tuple.Create(x * 3 + 1, x); + }; + + var source = SourceGen.UnfoldFlow(27, + Flow.FromFunction>(Map) + .Recover(ex => + { + if (ex == _done) + return new Option>(Tuple.Create(1, 1)); + + return Option>.None; + }), + _timeout); + + var sink = source.RunWith(this.SinkProbe(), Sys.Materializer()); + + foreach (var output in _outputs) + { + sink.Request(1); + sink.ExpectNext(output); + } + + sink.Request(1); + sink.ExpectNext(1); + sink.ExpectComplete(); + } + + [Fact] + public void UnfoldFlow_should_unfold_Collatz_conjecture_with_a_sequence_of_111_elements_with_buffered_flow() + { + Tuple Map(int x) + { + if (x == 1) + throw _done; + + if (x % 2 == 0) + return Tuple.Create(x / 2, x); + + return Tuple.Create(x * 3 + 1, x); + }; + + Source BufferedSource(int buffSize) + { + return + SourceGen.UnfoldFlow(27, + Flow.FromFunction>(Map) + .Recover(ex => + { + if (ex == _done) + return new Option>(Tuple.Create(1, 1)); + + return Option>.None; + }), _timeout) + .Buffer(buffSize, OverflowStrategy.Backpressure); + } + + var sink = BufferedSource(10).RunWith(this.SinkProbe(), Sys.Materializer()); + + sink.Request(_outputs.Length); + foreach (var output in _outputs) + sink.ExpectNext(output); + + sink.Request(1); + sink.ExpectNext(1); + sink.ExpectComplete(); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_fail_instantly_when_aborted() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + var kill = new Exception("KILL!"); + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + pub.SendError(kill); + snk.ExpectError().Should().Be(kill); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_fail_after_timeout_when_aborted() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + sub.Cancel(); + snk.ExpectNoMsg(_timeout - TimeSpan.FromMilliseconds(50)); + snk.ExpectError(); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_fail_when_inner_stream_is_canceled_and_pulled_before_completion() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + sub.Cancel(); + snk.Request(1); + snk.ExpectNoMsg(_timeout - TimeSpan.FromMilliseconds(50)); + snk.ExpectError(); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_fail_when_inner_stream_is_canceled_pulled_before_completion_and_finally_aborted() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + var kill = new Exception("KILL!"); + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + sub.Cancel(); + snk.Request(1); + pub.SendError(kill); + snk.ExpectError().Should().Be(kill); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_fail_after_3_elements_when_aborted() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + var kill = new Exception("KILL!"); + + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + snk.Request(1); + sub.RequestNext(1); + pub.SendNext(Tuple.Create(2, 1)); + snk.ExpectNext(1); + snk.Request(1); + sub.RequestNext(2); + pub.SendNext(Tuple.Create(3, 2)); + snk.ExpectNext(2); + snk.Request(1); + sub.RequestNext(3); + pub.SendNext(Tuple.Create(4, 3)); + snk.ExpectNext(3); + pub.SendError(kill); + snk.ExpectError().Should().Be(kill); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_complete_gracefully_instantly_when_stopped() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + pub.SendComplete(); + snk.ExpectComplete(); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_complete_gracefully_after_timeout_when_stopped() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + sub.Cancel(); + snk.ExpectNoMsg(_timeout - TimeSpan.FromMilliseconds(50)); + pub.SendComplete(); + snk.ExpectComplete(); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_complete_gracefully_after_3_elements_when_stopped() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + snk.Request(1); + sub.RequestNext(1); + pub.SendNext(Tuple.Create(2, 1)); + snk.ExpectNext(1); + snk.Request(1); + sub.RequestNext(2); + pub.SendNext(Tuple.Create(3, 2)); + snk.ExpectNext(2); + snk.Request(1); + sub.RequestNext(3); + pub.SendNext(Tuple.Create(4, 3)); + snk.ExpectNext(3); + pub.SendComplete(); + snk.ExpectComplete(); + } + } + + public class WithFunction : Akka.TestKit.Xunit2.TestKit + { + private readonly Source, TestPublisher.Probe>> _source; + + public WithFunction() + { + var controlledFlow = Flow.FromSinkAndSource(this.SinkProbe(), this.SourceProbe(), Keep.Both); + _source = SourceGen.UnfoldFlowWith(1, controlledFlow, n => new Option>(Tuple.Create(n + 1, n)), _timeout); + } + + [Fact] + public void UnfoldFlow_should_unfold_Collatz_conjecture_with_a_sequence_of_111_elements_with_function() + { + Option> Map(int x) + { + if (x == 1) + return Option>.None; + + if (x % 2 == 0) + return new Option>(Tuple.Create(x / 2, x)); + + return new Option>(Tuple.Create(x * 3 + 1, x)); + } + + var source = SourceGen.UnfoldFlowWith(27, Flow.FromFunction(x => x), Map, _timeout); + var sink = source.RunWith(this.SinkProbe(), Sys.Materializer()); + foreach (var output in _outputs) + { + sink.Request(1); + sink.ExpectNext(output); + } + sink.Request(1); + sink.ExpectComplete(); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_fail_instantly_when_aborted() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + var kill = new Exception("KILL!"); + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + pub.SendError(kill); + snk.ExpectError().Should().Be(kill); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_fail_after_timeout_when_aborted() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + sub.Cancel(); + snk.ExpectNoMsg(_timeout - TimeSpan.FromMilliseconds(50)); + snk.ExpectError(); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_fail_when_inner_stream_is_canceled_and_pulled_before_completion() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + sub.Cancel(); + snk.Request(1); + snk.ExpectNoMsg(_timeout - TimeSpan.FromMilliseconds(50)); + snk.ExpectError(); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_fail_when_inner_stream_is_canceled_pulled_before_completion_and_finally_aborted() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + var kill = new Exception("KILL!"); + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + sub.Cancel(); + snk.Request(1); + pub.SendError(kill); + snk.ExpectError().Should().Be(kill); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_fail_after_3_elements_when_aborted() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + var kill = new Exception("KILL!"); + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + snk.Request(1); + sub.RequestNext(1); + pub.SendNext(1); + snk.ExpectNext(1); + snk.Request(1); + sub.RequestNext(2); + pub.SendNext(2); + snk.ExpectNext(2); + snk.Request(1); + sub.RequestNext(3); + pub.SendNext(3); + snk.ExpectNext(3); + pub.SendError(kill); + snk.ExpectError().Should().Be(kill); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_complete_gracefully_instantly_when_stopped() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + pub.SendComplete(); + snk.ExpectComplete(); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_complete_gracefully_after_timeout_when_stopped() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + sub.Cancel(); + snk.ExpectNoMsg(_timeout - TimeSpan.FromMilliseconds(50)); + pub.SendComplete(); + snk.ExpectComplete(); + } + + [Fact] + public void UnfoldFlow_should_increment_integers_and_handle_KillSwitch_and_complete_gracefully_after_3_elements_when_stopped() + { + var t = _source.ToMaterialized(this.SinkProbe(), Keep.Both).Run(Sys.Materializer()); + + var sub = t.Item1.Item1; + var pub = t.Item1.Item2; + var snk = t.Item2; + + sub.EnsureSubscription(); + pub.EnsureSubscription(); + snk.EnsureSubscription(); + snk.Request(1); + sub.RequestNext(1); + pub.SendNext(1); + snk.ExpectNext(1); + snk.Request(1); + sub.RequestNext(2); + pub.SendNext(2); + snk.ExpectNext(2); + snk.Request(1); + sub.RequestNext(3); + pub.SendNext(3); + snk.ExpectNext(3); + pub.SendComplete(); + snk.ExpectComplete(); + } + } + } +} diff --git a/src/core/Akka.Streams/Dsl/KeepAliveConcat.cs b/src/core/Akka.Streams/Dsl/KeepAliveConcat.cs new file mode 100644 index 00000000000..a65d6b921b4 --- /dev/null +++ b/src/core/Akka.Streams/Dsl/KeepAliveConcat.cs @@ -0,0 +1,136 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Akka.Streams.Stage; + +namespace Akka.Streams.Dsl +{ + /// + /// Sends elements from buffer if upstream does not emit for a configured amount of time. In other words, this + /// stage attempts to maintains a base rate of emitted elements towards the downstream using elements from upstream. + /// + /// If upstream emits new elements until the accumulated elements in the buffer exceed the specified minimum size + /// used as the keep alive elements, then the base rate is no longer maintained until we reach another period without + /// elements form upstream. + /// + /// The keep alive period is the keep alive failover size times the interval. + /// + /// Emits when upstream emits an element or if the upstream was idle for the configured period + /// + /// + /// Backpressures when downstream backpressures + /// + /// + /// Completes when upstream completes + /// + /// Cancels when downstream cancels + /// + /// type of element + public class KeepAliveConcat : GraphStage> + { + private readonly int _keepAliveFailoverSize; + private readonly TimeSpan _interval; + private readonly Func> _extrapolate; + + #region Logic + + private sealed class Logic : TimerGraphStageLogic, IInHandler, IOutHandler + { + private readonly KeepAliveConcat _keepAliveConcat; + private readonly Queue _buffer; + + public Logic(KeepAliveConcat keepAliveConcat) : base(keepAliveConcat.Shape) + { + _keepAliveConcat = keepAliveConcat; + _buffer = new Queue(_keepAliveConcat._keepAliveFailoverSize); + + SetHandler(_keepAliveConcat.In, this); + SetHandler(_keepAliveConcat.Out, this); + } + + public void OnPush() + { + var elem = Grab(_keepAliveConcat.In); + if (_buffer.Count < _keepAliveConcat._keepAliveFailoverSize) + { + foreach (var t in _keepAliveConcat._extrapolate(elem)) + _buffer.Enqueue(t); + } + else + _buffer.Enqueue(elem); + + + if (IsAvailable(_keepAliveConcat.Out) && _buffer.Count > 0) + Push(_keepAliveConcat.Out, _buffer.Dequeue()); + else + Pull(_keepAliveConcat.In); + } + + public void OnUpstreamFinish() + { + if (_buffer.Count == 0) + CompleteStage(); + } + + public void OnUpstreamFailure(Exception e) => FailStage(e); + + public void OnPull() + { + if (IsClosed(_keepAliveConcat.In)) + { + if (_buffer.Count == 0) + CompleteStage(); + else + Push(_keepAliveConcat.Out, _buffer.Dequeue()); + } + else if (_buffer.Count > _keepAliveConcat._keepAliveFailoverSize) + Push(_keepAliveConcat.Out, _buffer.Dequeue()); + else if (!HasBeenPulled(_keepAliveConcat.In)) + Pull(_keepAliveConcat.In); + } + + public void OnDownstreamFinish() => CompleteStage(); + + protected internal override void OnTimer(object timerKey) + { + if (IsAvailable(_keepAliveConcat.Out) && _buffer.Count > 0) + Push(_keepAliveConcat.Out, _buffer.Dequeue()); + } + + public override void PreStart() + { + ScheduleRepeatedly("KeepAliveConcatTimer", _keepAliveConcat._interval); + Pull(_keepAliveConcat.In); + } + } + + #endregion + + public KeepAliveConcat(int keepAliveFailoverSize, TimeSpan interval, Func> extrapolate) + { + if (keepAliveFailoverSize <= 0) + throw new ArgumentException("The buffer keep alive failover size must be greater than 0.", nameof(keepAliveFailoverSize)); + + _keepAliveFailoverSize = keepAliveFailoverSize; + _interval = interval; + _extrapolate = extrapolate; + + In = new Inlet("KeepAliveConcat.in"); + Out = new Outlet("KeepAliveConcat.out"); + Shape = new FlowShape(In, Out); + } + + protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) => new Logic(this); + + public override FlowShape Shape { get; } + + public Inlet In { get; } + public Outlet Out { get; } + } +} diff --git a/src/core/Akka.Streams/Dsl/SourceGen.cs b/src/core/Akka.Streams/Dsl/SourceGen.cs new file mode 100644 index 00000000000..86303217f9f --- /dev/null +++ b/src/core/Akka.Streams/Dsl/SourceGen.cs @@ -0,0 +1,155 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Annotations; +using Akka.Streams.Stage; +using Akka.Streams.Util; + +namespace Akka.Streams.Dsl +{ + public static class SourceGen + { + /// + /// EXPERIMENTAL API + /// + /// Create a source that will unfold a value of type by + /// passing it through a flow. The flow should emit a + /// pair of the next state and output elements of type . + /// Source completes when the flow completes. + /// + /// + /// The parameter specifies waiting time after inner + /// flow provided by the user for unfold flow API cancels + /// upstream, to get also the downstream cancelation (as + /// graceful completion or failure which is propagated). + /// If inner flow fails to complete/fail downstream, stage is failed. + /// + /// + /// IMPORTANT CAVEAT: + /// The given flow must not change the number of elements passing through it(i.e.it should output + /// exactly one element for every received element). Ignoring this, will have an unpredicted result, + /// and may result in a deadlock. + /// + /// + /// state type + /// output elements type + /// materialized value type + /// intial state + /// flow, through which value is passed + /// timeout + [ApiMayChange] + public static Source UnfoldFlow(TState seed, IGraph>, TMat> flow, TimeSpan timeout) + { + return UnfoldFlowGraph(new FanOut2UnfoldingStage, TState, TOut>(shape => new UnfoldFlowGraphStageLogic(shape, seed, timeout)), flow); + } + + /// + /// EXPERIMENTAL API + /// + /// Create a source that will unfold a value of type by + /// passing it through a flow. The flow should emit an output + /// value of type , that when fed to the unfolding function, + /// generates a pair of the next state and output elements of type . + /// + /// + /// The parameter specifies waiting time after inner + /// flow provided by the user for unfold flow API cancels + /// upstream, to get also the downstream cancelation(as + /// graceful completion or failure which is propagated). + /// If inner flow fails to complete/fail downstream, stage is failed. + /// + /// + /// IMPORTANT CAVEAT: + /// The given flow must not change the number of elements passing through it(i.e.it should output + /// exactly one element for every received element). Ignoring this, will have an unpredicted result, + /// and may result in a deadlock. + /// + /// + /// output elements type + /// state type + /// flow output value type + /// materialized value type + /// intial state + /// flow through which value is passed + /// unfolding function + /// timeout + [ApiMayChange] + public static Source UnfoldFlowWith(TState seed, IGraph, TMat> flow, Func>> unfoldWith, TimeSpan timeout) + { + return UnfoldFlowGraph(new FanOut2UnfoldingStage(shape => new UnfoldFlowWithGraphStageLogic(shape, seed, unfoldWith, timeout)), flow); + } + + private class UnfoldFlowGraphStageLogic : UnfoldFlowGraphStageLogic, TState, TOut>, IInHandler + { + public UnfoldFlowGraphStageLogic(FanOutShape, TState, TOut> shape, TState seed, TimeSpan timeout) : base(shape, seed, timeout) + { + SetHandler(_nextElem, this); + } + + public void OnPush() + { + var t = Grab(_nextElem); + var s = t.Item1; + var e = t.Item2; + _pending = s; + Push(_output, e); + _pushedToCycle = false; + } + + public void OnUpstreamFinish() => CompleteStage(); + + public void OnUpstreamFailure(Exception e) => FailStage(e); + } + + private class UnfoldFlowWithGraphStageLogic : UnfoldFlowGraphStageLogic, IInHandler + { + private readonly Func>> _unfoldWith; + + public UnfoldFlowWithGraphStageLogic(FanOutShape shape, TState seed, Func>> unfoldWith, TimeSpan timeout) : base(shape, seed, timeout) + { + _unfoldWith = unfoldWith; + + SetHandler(_nextElem, this); + } + + public void OnPush() + { + var elem = Grab(_nextElem); + var unfolded = _unfoldWith(elem); + + if (unfolded.HasValue) + { + var s = unfolded.Value.Item1; + var e = unfolded.Value.Item2; + _pending = s; + Push(_output, e); + _pushedToCycle = false; + } + else + CompleteStage(); + } + + public void OnUpstreamFinish() => CompleteStage(); + + public void OnUpstreamFailure(Exception e) => FailStage(e); + } + + private static Source UnfoldFlowGraph(GraphStage> fanOut2Stage, IGraph, TMat> flow) + { + var graph = GraphDsl.Create(flow, (b, f) => + { + var fo2 = b.Add(fanOut2Stage); + b.From(fo2.Out0).Via(f).To(fo2.In); + + return new SourceShape(fo2.Out1); + }); + + return Source.FromGraph(graph); + } + } +} diff --git a/src/core/Akka.Streams/Dsl/UnfoldFlow.cs b/src/core/Akka.Streams/Dsl/UnfoldFlow.cs new file mode 100644 index 00000000000..aeb4d9b89dc --- /dev/null +++ b/src/core/Akka.Streams/Dsl/UnfoldFlow.cs @@ -0,0 +1,95 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Annotations; +using Akka.Streams.Stage; + +namespace Akka.Streams.Dsl +{ + [InternalApi] + internal abstract class UnfoldFlowGraphStageLogic : GraphStageLogic, IOutHandler + { + private readonly TimeSpan _timeout; + private readonly Outlet _feedback; + protected readonly Outlet _output; + protected readonly Inlet _nextElem; + + protected TState _pending; + protected bool _pushedToCycle; + + protected UnfoldFlowGraphStageLogic(FanOutShape shape, TState seed, TimeSpan timeout) : base(shape) + { + _timeout = timeout; + + _feedback = shape.Out0; + _output = shape.Out1; + _nextElem = shape.In; + + _pending = seed; + _pushedToCycle = false; + + SetHandler(_feedback, this); + + SetHandler(_output, onPull: () => + { + Pull(_nextElem); + if (!_pushedToCycle && IsAvailable(_feedback)) + { + Push(_feedback, _pending); + _pending = default(TState); + _pushedToCycle = true; + } + }); + } + + public void OnPull() + { + if (!_pushedToCycle && IsAvailable(_output)) + { + Push(_feedback, _pending); + _pending = default(TState); + _pushedToCycle = true; + } + } + + public void OnDownstreamFinish() + { + // Do Nothing until `timeout` to try and intercept completion as downstream, + // but cancel stream after timeout if inlet is not closed to prevent deadlock. + Materializer.ScheduleOnce(_timeout, () => + { + var cb = GetAsyncCallback(() => + { + if (!IsClosed(_nextElem)) + FailStage(new InvalidOperationException($"unfoldFlow source's inner flow canceled only upstream, while downstream remain available for {_timeout}")); + }); + cb(); + }); + } + } + + [InternalApi] + internal class FanOut2UnfoldingStage : GraphStage> + { + private readonly Func, UnfoldFlowGraphStageLogic> _generateGraphStageLogic; + + public FanOut2UnfoldingStage(Func, UnfoldFlowGraphStageLogic> generateGraphStageLogic) + { + _generateGraphStageLogic = generateGraphStageLogic; + + Shape = new FanOutShape("unfoldFlow"); + } + + public override FanOutShape Shape { get; } + + protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) + { + return _generateGraphStageLogic(Shape); + } + } +} From 1234561100a0d9ca90de524183e31a0c838039d7 Mon Sep 17 00:00:00 2001 From: Samuel Kelemen Date: Fri, 10 Aug 2018 17:37:13 -0400 Subject: [PATCH 67/70] Correct casing (#3569) THe -> The --- docs/articles/utilities/scheduler.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/utilities/scheduler.md b/docs/articles/utilities/scheduler.md index 774ed5cc4df..7a01ac854bb 100644 --- a/docs/articles/utilities/scheduler.md +++ b/docs/articles/utilities/scheduler.md @@ -14,7 +14,7 @@ You can schedule sending of messages to actors and execution of tasks (`Action` delegate). You will get a `Task` back. By providing a `CancellationTokenSource` you can cancel the scheduled task. -The scheduler in Akka is designed for high-throughput of thousands up to millions of triggers. THe prime use-case being triggering Actor receive timeouts, Future timeouts, circuit breakers and other time dependent events which happen all-the-time and in many instances at the same time. The implementation is based on a Hashed Wheel Timer, which is a known data structure and algorithm for handling such use cases, refer to the [Hashed and Hierarchical Timing Wheels](http://www.cs.columbia.edu/~nahum/w6998/papers/sosp87-timing-wheels.pdf) whitepaper by Varghese and Lauck if you'd like to understand its inner workings. +The scheduler in Akka is designed for high-throughput of thousands up to millions of triggers. The prime use-case being triggering Actor receive timeouts, Future timeouts, circuit breakers and other time dependent events which happen all-the-time and in many instances at the same time. The implementation is based on a Hashed Wheel Timer, which is a known data structure and algorithm for handling such use cases, refer to the [Hashed and Hierarchical Timing Wheels](http://www.cs.columbia.edu/~nahum/w6998/papers/sosp87-timing-wheels.pdf) whitepaper by Varghese and Lauck if you'd like to understand its inner workings. The Akka scheduler is **not** designed for long-term scheduling (see [Akka.Quartz.Actor](https://github.com/akkadotnet/Akka.Quartz.Actor) instead for this use-case) nor is it to be used for highly precise firing of the events. The maximum amount of time into the future you can schedule an event to trigger is around 8 months, which in practice is too much to be useful since this would assume the system never went down during that period. If you need long-term scheduling we highly recommend looking into alternative schedulers, as this is not the use-case the Akka scheduler is implemented for. From aa01127f05818005d1f0236427abe7129bb721ac Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Sat, 18 Aug 2018 10:37:59 +0100 Subject: [PATCH 68/70] KillSwitches: flow stage from CancellationToken (#3568) * KillSwitches: flow stage from CancellationToken * removed cancellation token from materializer value type * added API approvals and docs --- docs/articles/streams/stream-dynamic.md | 6 + .../CoreAPISpec.ApproveStreams.approved.txt | 1 + .../Dsl/FlowKillSwitchSpec.cs | 237 ++++++++++++++++++ src/core/Akka.Streams/KillSwitch.cs | 94 +++++++ 4 files changed, 338 insertions(+) diff --git a/docs/articles/streams/stream-dynamic.md b/docs/articles/streams/stream-dynamic.md index 47f92c33d2c..5b0afa6f08c 100644 --- a/docs/articles/streams/stream-dynamic.md +++ b/docs/articles/streams/stream-dynamic.md @@ -66,6 +66,12 @@ by the switch. Refer to the below for usage examples. > [!NOTE] > A `UniqueKillSwitch` is always a result of a materialization, whilst `SharedKillSwitch` needs to be constructed before any materialization takes place. +### Using `CancellationToken`s as kill switches + +Plain old .NET cancellation tokens can also be used as kill switch stages via extension method: `cancellationToken.AsFlow(cancelGracefully: true)`. Their behavior is very similar to what a `SharedKillSwitch` has to offer with one exception - while normal kill switch recognizes difference between closing a stream gracefully (via. `Shutdown()`) and abruptly (via. `Abort(exception)`), .NET cancellation tokens have no such distinction. + +Therefore you need to explicitly specify at the moment of defining a flow stage, if cancellation token call should cause stream to close with completion or failure, by using `cancelGracefully` parameter. If it's set to `false`, calling cancel on a token's source will cause stream to fail with an `OperationCanceledException`. + ## Dynamic fan-in and fan-out with MergeHub and BroadcastHub There are many cases when consumers or producers of a certain service (represented as a Sink, Source, or possibly Flow) are dynamic and not known in advance. The Graph DSL does not allow to represent this, all connections of the graph must be known in advance and must be connected upfront. To allow dynamic fan-in and fan-out streaming, the Hubs should be used. They provide means to construct Sink and Source pairs that are “attached” to each other, but one of them can be materialized multiple times to implement dynamic fan-in or fan-out. diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt index 2aa606536b3..f0a6e500e9d 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt @@ -642,6 +642,7 @@ namespace Akka.Streams } public class static KillSwitches { + public static Akka.Streams.IGraph, Akka.NotUsed> AsFlow(this System.Threading.CancellationToken cancellationToken, bool cancelGracefully = False) { } public static Akka.Streams.SharedKillSwitch Shared(string name) { } public static Akka.Streams.IGraph, Akka.Streams.UniqueKillSwitch> Single() { } public static Akka.Streams.IGraph, Akka.Streams.UniqueKillSwitch> SingleBidi() { } diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowKillSwitchSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowKillSwitchSpec.cs index f3c8d145502..aea802db223 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowKillSwitchSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowKillSwitchSpec.cs @@ -8,6 +8,7 @@ using System; using System.Linq; +using System.Threading; using Akka.Streams.Dsl; using Akka.Streams.TestKit; using Akka.Streams.TestKit.Tests; @@ -28,6 +29,8 @@ public FlowKillSwitchSpec(ITestOutputHelper helper) : base(helper) Materializer = ActorMaterializer.Create(Sys); } + #region unique kill switch + [Fact] public void A_UniqueKillSwitch_must_stop_a_stream_if_requested() { @@ -123,6 +126,9 @@ public void A_UniqueKillSwitch_must_ignore_completion_after_already_completed() downstream.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); } + #endregion + + #region shared kill switch [Fact] public void A_SharedKillSwitch_must_stop_a_stream_if_requested() @@ -491,5 +497,236 @@ public void A_SharedKillSwitch_must_use_its_name_on_the_flows_it_hands_out() killSwitch.Flow().ToString().Should().Be("Flow(KillSwitch(MySwitchName))"); }, Materializer); } + + #endregion + + #region cancellable kill switch + + [Fact] + public void A_CancellationToken_flow_must_stop_a_stream_if_requested() + { + this.AssertAllStagesStopped(() => + { + var cancel = new CancellationTokenSource(); + + var t = this.SourceProbe() + .Via(cancel.Token.AsFlow(cancelGracefully: true)) + .ToMaterialized(this.SinkProbe(), Keep.Both) + .Run(Materializer); + var upstream = t.Item1; + var downstream = t.Item2; + + downstream.Request(1); + upstream.SendNext(1); + downstream.ExpectNext(1); + + cancel.Cancel(); + upstream.ExpectCancellation(); + downstream.ExpectComplete(); + }, Materializer); + } + + [Fact] + public void A_CancellationToken_flow_must_fail_a_stream_if_requested() + { + this.AssertAllStagesStopped(() => + { + var cancel = new CancellationTokenSource(); + + var t = this.SourceProbe() + .Via(cancel.Token.AsFlow(cancelGracefully: false)) + .ToMaterialized(this.SinkProbe(), Keep.Both) + .Run(Materializer); + var upstream = t.Item1; + var downstream = t.Item2; + + downstream.Request(1); + upstream.SendNext(1); + downstream.ExpectNext(1); + + cancel.Cancel(); + upstream.ExpectCancellation(); + downstream.ExpectError().Should().BeOfType(); + }, Materializer); + } + + [Fact] + public void A_CancellationToken_flow_must_pass_through_all_elements_unmodified() + { + this.AssertAllStagesStopped(() => + { + var cancel = new CancellationTokenSource(); + var task = Source.From(Enumerable.Range(1, 100)) + .Via(cancel.Token.AsFlow()) + .RunWith(Sink.Seq(), Materializer); + task.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + task.Result.ShouldAllBeEquivalentTo(Enumerable.Range(1, 100)); + }, Materializer); + } + + [Fact] + public void A_CancellationToken_flow_must_provide_a_flow_that_if_materialized_multiple_times_with_multiple_types_stops_all_streams_if_requested() + { + this.AssertAllStagesStopped(() => + { + var cancel = new CancellationTokenSource(); + + var t1 = this.SourceProbe() + .Via(cancel.Token.AsFlow(cancelGracefully: true)) + .ToMaterialized(this.SinkProbe(), Keep.Both) + .Run(Materializer); + var t2 = this.SourceProbe() + .Via(cancel.Token.AsFlow(cancelGracefully: true)) + .ToMaterialized(this.SinkProbe(), Keep.Both) + .Run(Materializer); + + var upstream1 = t1.Item1; + var downstream1 = t1.Item2; + var upstream2 = t2.Item1; + var downstream2 = t2.Item2; + + downstream1.Request(1); + upstream1.SendNext(1); + downstream1.ExpectNext(1); + + downstream2.Request(2); + upstream2.SendNext("A").SendNext("B"); + downstream2.ExpectNext("A", "B"); + + cancel.Cancel(); + + upstream1.ExpectCancellation(); + upstream2.ExpectCancellation(); + downstream1.ExpectComplete(); + downstream2.ExpectComplete(); + }, Materializer); + } + + [Fact] + public void A_CancellationToken_flow_must_provide_a_flow_that_if_materialized_multiple_times_with_multiple_types_fails_all_streams_if_requested() + { + this.AssertAllStagesStopped(() => + { + var cancel = new CancellationTokenSource(); + + var t1 = this.SourceProbe() + .Via(cancel.Token.AsFlow(cancelGracefully: false)) + .ToMaterialized(this.SinkProbe(), Keep.Both) + .Run(Materializer); + var t2 = this.SourceProbe() + .Via(cancel.Token.AsFlow(cancelGracefully: false)) + .ToMaterialized(this.SinkProbe(), Keep.Both) + .Run(Materializer); + + var upstream1 = t1.Item1; + var downstream1 = t1.Item2; + var upstream2 = t2.Item1; + var downstream2 = t2.Item2; + + downstream1.Request(1); + upstream1.SendNext(1); + downstream1.ExpectNext(1); + + downstream2.Request(2); + upstream2.SendNext("A").SendNext("B"); + downstream2.ExpectNext("A", "B"); + + cancel.Cancel(); + upstream1.ExpectCancellation(); + upstream2.ExpectCancellation(); + + downstream1.ExpectError().Should().BeOfType(); + downstream2.ExpectError().Should().BeOfType(); + }, Materializer); + } + + [Fact] + public void A_CancellationToken_flow_must_ignore_subsequent_aborts_and_shutdowns_after_shutdown() + { + this.AssertAllStagesStopped(() => + { + var cancel = new CancellationTokenSource(); + + var t = this.SourceProbe() + .Via(cancel.Token.AsFlow(cancelGracefully: true)) + .ToMaterialized(this.SinkProbe(), Keep.Both) + .Run(Materializer); + var upstream = t.Item1; + var downstream = t.Item2; + + downstream.Request(1); + upstream.SendNext(1); + downstream.ExpectNext(1); + + cancel.Cancel(); + upstream.ExpectCancellation(); + downstream.ExpectComplete(); + + cancel.Cancel(); + upstream.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + downstream.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + + cancel.Cancel(); + upstream.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + downstream.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + }, Materializer); + } + + [Fact] + public void A_CancellationToken_flow_must_complete_immediately_flows_materialized_after_switch_shutdown() + { + this.AssertAllStagesStopped(() => + { + var cancel = new CancellationTokenSource(); + cancel.Cancel(); + + var t = this.SourceProbe() + .Via(cancel.Token.AsFlow(cancelGracefully: true)) + .ToMaterialized(this.SinkProbe(), Keep.Both) + .Run(Materializer); + var upstream = t.Item1; + var downstream = t.Item2; + + upstream.ExpectCancellation(); + downstream.ExpectSubscriptionAndComplete(); + }, Materializer); + } + + [Fact] + public void A_CancellationToken_flow_must_fail_immediately_flows_materialized_after_switch_failure() + { + this.AssertAllStagesStopped(() => + { + var cancel = new CancellationTokenSource(); + cancel.Cancel(); + + var t = this.SourceProbe() + .Via(cancel.Token.AsFlow(cancelGracefully: false)) + .ToMaterialized(this.SinkProbe(), Keep.Both) + .Run(Materializer); + var upstream = t.Item1; + var downstream = t.Item2; + + upstream.ExpectCancellation(); + downstream.ExpectSubscriptionAndError().Should().BeOfType(); + }, Materializer); + } + + [Fact] + public void A_CancellationToken_flow_should_not_cause_problems_if_switch_is_shutdown_after_flow_completed_normally() + { + this.AssertAllStagesStopped(() => + { + var cancel = new CancellationTokenSource(); + var task = Source.From(Enumerable.Range(1, 10)) + .Via(cancel.Token.AsFlow(cancelGracefully: true)) + .RunWith(Sink.Seq(), Materializer); + task.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + task.Result.ShouldAllBeEquivalentTo(Enumerable.Range(1, 10)); + cancel.Cancel(); + }, Materializer); + } + + #endregion } } diff --git a/src/core/Akka.Streams/KillSwitch.cs b/src/core/Akka.Streams/KillSwitch.cs index 243a2047464..a6c216b7d3a 100644 --- a/src/core/Akka.Streams/KillSwitch.cs +++ b/src/core/Akka.Streams/KillSwitch.cs @@ -6,6 +6,8 @@ //----------------------------------------------------------------------- using System; +using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using Akka.Streams.Stage; @@ -52,6 +54,98 @@ public static class KillSwitches public static IGraph, UniqueKillSwitch> SingleBidi () => UniqueBidiKillSwitchStage.Instance; + /// + /// Returns a flow, which works like a kill switch stage based on a provided . + /// Since unlike cancellation tokens, kill switches expose ability to finish a stream either gracefully via + /// or abruptly via , this distinction is + /// handled by specifying parameter. + /// + /// + /// Cancellation token used to create a cancellation flow. + /// + /// When set to true, will close stream gracefully via completting the stage. + /// When set to false, will close stream by failing the stage with . + /// + /// + public static IGraph, NotUsed> AsFlow(this CancellationToken cancellationToken, bool cancelGracefully = false) + { + return new CancellableKillSwitchStage(cancellationToken, cancelGracefully); + } + + internal sealed class CancellableKillSwitchStage : GraphStage> + { + #region logic + + private sealed class Logic : InAndOutGraphStageLogic + { + private readonly CancellableKillSwitchStage _stage; + private CancellationTokenRegistration? _registration = null; + + public Logic(CancellableKillSwitchStage stage) + : base(stage.Shape) + { + _stage = stage; + SetHandler(stage.Inlet, this); + SetHandler(stage.Outlet, this); + } + + public override void PreStart() + { + if (_stage._cancellationToken.IsCancellationRequested) + { + if (_stage._cancelGracefully) + OnCancelComplete(); + else + OnCancelFail(); + } + else + { + var onCancel = _stage._cancelGracefully + ? GetAsyncCallback(OnCancelComplete) + : GetAsyncCallback(OnCancelFail); + + _registration = _stage._cancellationToken.Register(onCancel); + } + } + + public override void PostStop() + { + _registration?.Dispose(); + base.PostStop(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnPush() => Push(_stage.Outlet, Grab(_stage.Inlet)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnPull() => Pull(_stage.Inlet); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void OnCancelComplete() => CompleteStage(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void OnCancelFail() => FailStage(new OperationCanceledException($"Stage cancelled due to cancellation token request.", _stage._cancellationToken)); + } + + #endregion + + private readonly CancellationToken _cancellationToken; + private readonly bool _cancelGracefully; + + public CancellableKillSwitchStage(CancellationToken cancellationToken, bool cancelGracefully) + { + _cancellationToken = cancellationToken; + _cancelGracefully = cancelGracefully; + Shape = new FlowShape(Inlet, Outlet); + } + + public Inlet Inlet { get; } = new Inlet("cancel.in"); + public Outlet Outlet { get; } = new Outlet("cancel.out"); + + public override FlowShape Shape { get; } + protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) => new Logic(this); + } + /// /// TBD /// From aace21d5f82aa937baacbf4efdd7c383c56ead00 Mon Sep 17 00:00:00 2001 From: Oleksandr Bogomaz Date: Wed, 22 Aug 2018 23:07:44 +0300 Subject: [PATCH 69/70] Port PagedSource & IntervalBasedRateLimiter (#3570) * Port PagedSource * Port IntervalBasedRateLimiter --- .../CoreAPISpec.ApproveStreams.approved.txt | 16 ++ .../Dsl/IntervalBasedRateLimiterSpec.cs | 137 +++++++++++++++++ .../Akka.Streams.Tests/Dsl/PagedSourceSpec.cs | 141 ++++++++++++++++++ .../Dsl/IntervalBasedRateLimiter.cs | 34 +++++ src/core/Akka.Streams/Dsl/PagedSource.cs | 69 +++++++++ 5 files changed, 397 insertions(+) create mode 100644 src/core/Akka.Streams.Tests/Dsl/IntervalBasedRateLimiterSpec.cs create mode 100644 src/core/Akka.Streams.Tests/Dsl/PagedSourceSpec.cs create mode 100644 src/core/Akka.Streams/Dsl/IntervalBasedRateLimiter.cs create mode 100644 src/core/Akka.Streams/Dsl/PagedSource.cs diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt index f0a6e500e9d..36c9e3c4436 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt @@ -1329,6 +1329,11 @@ namespace Akka.Streams.Dsl public Akka.Streams.Inlet In(int id) { } public override string ToString() { } } + public class static IntervalBasedRateLimiter + { + [Akka.Annotations.ApiMayChangeAttribute()] + public static Akka.Streams.IGraph>, Akka.NotUsed> Create(System.TimeSpan minInterval, int maxBatchSize) { } + } public interface IRunnableGraph : Akka.Streams.IGraph, Akka.Streams.IGraph { Akka.Streams.Dsl.IRunnableGraph AddAttributes(Akka.Streams.Attributes attributes); @@ -1471,6 +1476,17 @@ namespace Akka.Streams.Dsl { public OutputTruncationException() { } } + public class static PagedSource + { + [Akka.Annotations.ApiMayChangeAttribute()] + public static Akka.Streams.Dsl.Source Create(TKey firstKey, System.Func>> pageFactory) { } + public class Page + { + public Page(System.Collections.Generic.IEnumerable items, Akka.Streams.Util.Option nextKey) { } + public System.Collections.Generic.IEnumerable Items { get; } + public Akka.Streams.Util.Option NextKey { get; } + } + } public sealed class Partition : Akka.Streams.Stage.GraphStage> { public readonly Akka.Streams.Inlet In; diff --git a/src/core/Akka.Streams.Tests/Dsl/IntervalBasedRateLimiterSpec.cs b/src/core/Akka.Streams.Tests/Dsl/IntervalBasedRateLimiterSpec.cs new file mode 100644 index 00000000000..bb5c0f2fe44 --- /dev/null +++ b/src/core/Akka.Streams.Tests/Dsl/IntervalBasedRateLimiterSpec.cs @@ -0,0 +1,137 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Linq; +using System.Collections.Generic; +using Akka.Streams.Dsl; +using Akka.Streams.TestKit; +using FluentAssertions; +using Xunit; + +namespace Akka.Streams.Tests.Dsl +{ + public class IntervalBasedRateLimiterSpec : Akka.TestKit.Xunit2.TestKit + { + private readonly Source _infiniteSource = Source.From(Enumerable.Range(1, int.MaxValue - 1)); + + [Fact] + public void IntervalBasedRateLimiter_should_limit_rate_of_messages_when_frequency_is_low_1_element_per_500ms() + { + TestCase(source: _infiniteSource, + numOfElements: 6, + maxBatchSize: 1, + minInterval: TimeSpan.FromMilliseconds(500)); + } + + [Fact] + public void IntervalBasedRateLimiter_should_limit_rate_of_messages_when_frequency_is_medium_10_elements_per_100ms() + { + TestCase(source: _infiniteSource, + numOfElements: 300, + maxBatchSize: 10, + minInterval: TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void IntervalBasedRateLimiter_should_limit_rate_of_messages_when_frequency_is_moderate_20_elements_per_100ms() + { + TestCase(source: _infiniteSource, + numOfElements: 600, + maxBatchSize: 20, + minInterval: TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void IntervalBasedRateLimiter_should_limit_rate_of_messages_when_frequency_is_moderate_200_elements_per_1000ms() + { + TestCase(source: _infiniteSource, + numOfElements: 600, + maxBatchSize: 200, + minInterval: TimeSpan.FromMilliseconds(1000)); + } + + [Fact] + public void IntervalBasedRateLimiter_should_limit_rate_of_messages_when_frequency_is_high_200_elements_per_100ms() + { + TestCase(source: _infiniteSource, + numOfElements: 6000, + maxBatchSize: 200, + minInterval: TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void IntervalBasedRateLimiter_should_limit_rate_of_messages_when_frequency_is_high_2_000_elements_per_1000ms() + { + TestCase(source: _infiniteSource, + numOfElements: 6000, + maxBatchSize: 2000, + minInterval: TimeSpan.FromMilliseconds(1000)); + } + + [Fact] + public void IntervalBasedRateLimiter_should_limit_rate_of_messages_when_frequency_is_very_high_50_000_elements_per_1000ms() + { + TestCase(source: _infiniteSource, + numOfElements: 150000, + maxBatchSize: 50000, + minInterval: TimeSpan.FromMilliseconds(1000)); + } + + [Fact] + public void IntervalBasedRateLimiter_should_limit_rate_of_messages_when_source_is_slow() + { + var slowInfiniteSource = _infiniteSource.Throttle(1, TimeSpan.FromMilliseconds(300), 1, ThrottleMode.Shaping); + + TestCase(source: slowInfiniteSource, + numOfElements: 10, + maxBatchSize: 1, + minInterval: TimeSpan.FromMilliseconds(100)); + } + + private void TestCase(Source source, + int numOfElements, + int maxBatchSize, + TimeSpan minInterval) + { + var flow = source + .Take(numOfElements) + .Via(IntervalBasedRateLimiter.Create(minInterval, maxBatchSize)) + .Select(batch => Tuple.Create(DateTime.Now.Ticks, batch)) + .RunWith(this.SinkProbe>>(), Sys.Materializer()); + + var timestamps = new List(); + var batches = new List>(); + + void CollectTimestampsAndBatches() + { + flow.Request(1); + var e = flow.ExpectEvent(); + + if (e is TestSubscriber.OnNext>> onNext) + { + timestamps.Add(onNext.Element.Item1); + batches.Add(onNext.Element.Item2); + + CollectTimestampsAndBatches(); + } + } + + CollectTimestampsAndBatches(); + + var intervals = timestamps + .Take(timestamps.Count - 1) + .Zip(timestamps.Skip(1), (first, second) => TimeSpan.FromTicks(second - first)); + + foreach (var interval in intervals) + interval.Should().BeGreaterOrEqualTo(minInterval); + + batches.SelectMany(x => x).ShouldBeEquivalentTo(Enumerable.Range(1, numOfElements), o => o.WithStrictOrdering()); + batches.Count.Should().BeOneOf(numOfElements / maxBatchSize, numOfElements / maxBatchSize + 1); + } + } +} diff --git a/src/core/Akka.Streams.Tests/Dsl/PagedSourceSpec.cs b/src/core/Akka.Streams.Tests/Dsl/PagedSourceSpec.cs new file mode 100644 index 00000000000..40757ed7ba5 --- /dev/null +++ b/src/core/Akka.Streams.Tests/Dsl/PagedSourceSpec.cs @@ -0,0 +1,141 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Akka.Streams.Dsl; +using Akka.Streams.TestKit.Tests; +using Akka.Streams.Util; +using FluentAssertions; +using Xunit; + +namespace Akka.Streams.Tests.Dsl +{ + public class PagedSourceSpec + { + public class MultiplesOfTwo : Akka.TestKit.Xunit2.TestKit + { + private class MultiplesOfTwoPage + { + private readonly int? _size; + private const int _itemsPerPage = 2; + + public MultiplesOfTwoPage(int? size = null) + { + _size = size; + } + + public Task> Page(int key) + { + var indices = Enumerable.Range(key * _itemsPerPage, _itemsPerPage); + var filteredIndices = _size.HasValue ? indices.Where(x => x < _size.Value) : indices; + + return Task.FromResult(new PagedSource.Page(filteredIndices.Select(x => x * 2), new Option(key + 1))); + } + } + + [Fact] + public void PagedSource_should_return_the_items_in_the_proper_order() + { + var source = PagedSource.Create(0, new MultiplesOfTwoPage().Page); + var t = source.Take(3).RunWith(Sink.Seq(), Sys.Materializer()); + + t.AwaitResult().ShouldBeEquivalentTo(new[] { 0, 2, 4 }, o => o.WithStrictOrdering()); + } + + [Fact] + public void PagedSource_should_return_not_more_items_then_available() + { + var source = PagedSource.Create(0, new MultiplesOfTwoPage(4).Page); + var t = source.Take(10).RunWith(Sink.Seq(), Sys.Materializer()); + + t.AwaitResult().Should().HaveCount(4); + } + } + + public class IndexedStringPages : Akka.TestKit.Xunit2.TestKit + { + private readonly Source _source = PagedSource.Create + ( + 1, + i => Task.FromResult(new PagedSource.Page(Page(i), new Option(i + 1))) + ); + + private static IEnumerable Page(int key) + { + if (key == 1) + return new[] { "a", "b", "c" }; + + if (key == 2) + return new[] { "d", "e" }; + + return null; + } + + [Fact] + public void PagedSource_should_return_the_items_in_the_proper_order() + { + var t = _source.Take(4).RunWith(Sink.Seq(), Sys.Materializer()); + + t.AwaitResult().ShouldBeEquivalentTo(new[] { "a", "b", "c", "d" }, o => o.WithStrictOrdering()); + } + + [Fact] + public void PagedSource_should_close_stream_when_received_empty_page() + { + var t = _source.RunWith(Sink.Seq(), Sys.Materializer()); + + t.AwaitResult().ShouldBeEquivalentTo(new[] { "a", "b", "c", "d", "e" }, o => o.WithStrictOrdering()); + } + } + + public class LinkedIntPages : Akka.TestKit.Xunit2.TestKit + { + private readonly Source _source = PagedSource.Create + ( + "first", + key => + { + var t = Page(key); + var items = t.Item1; + var next = t.Item2; + + return Task.FromResult(new PagedSource.Page(items, next == "" ? Option.None : new Option(next))); + } + ); + + private static Tuple Page(string key) + { + if (key == "first") + return Tuple.Create(new[] { 1, 2 }, "second"); + + if (key == "second") + return Tuple.Create(new[] { 3, 4, 5 }, ""); + + return Tuple.Create(new[] { 6 }, ""); + } + + [Fact] + public void PagedSource_should_return_the_items_in_the_proper_order() + { + var t = _source.Take(4).RunWith(Sink.Seq(), Sys.Materializer()); + + t.AwaitResult().ShouldBeEquivalentTo(new[] { 1, 2, 3, 4 }, o => o.WithStrictOrdering()); + } + + [Fact] + public void PagedSource_should_close_stream_when_received_empty_link() + { + var t = _source.RunWith(Sink.Seq(), Sys.Materializer()); + + t.AwaitResult().ShouldBeEquivalentTo(new[] { 1, 2, 3, 4, 5 }, o => o.WithStrictOrdering()); + } + } + } +} diff --git a/src/core/Akka.Streams/Dsl/IntervalBasedRateLimiter.cs b/src/core/Akka.Streams/Dsl/IntervalBasedRateLimiter.cs new file mode 100644 index 00000000000..825a3aee827 --- /dev/null +++ b/src/core/Akka.Streams/Dsl/IntervalBasedRateLimiter.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Akka.Annotations; + +namespace Akka.Streams.Dsl +{ + public static class IntervalBasedRateLimiter + { + /// + /// Specialized type of rate limiter which emits batches of elements (with size limited by the parameter) + /// with a minimum time interval of . + /// + /// Because the next emit is scheduled after we downstream the current batch, the effective throughput, + /// depending on the minimal interval length, may never reach the maximum allowed one. + /// You can minimize these delays by sending bigger batches less often. + /// + /// type of element + /// minimal pause to be kept before downstream the next batch. Should be >= 10 milliseconds. + /// maximum number of elements to send in the single batch + /// + [ApiMayChange] + public static IGraph>, NotUsed> Create(TimeSpan minInterval, int maxBatchSize) + { + return Flow.Create().GroupedWithin(maxBatchSize, minInterval).Via(new DelayFlow>(minInterval)); + } + } +} diff --git a/src/core/Akka.Streams/Dsl/PagedSource.cs b/src/core/Akka.Streams/Dsl/PagedSource.cs new file mode 100644 index 00000000000..50b30fcc977 --- /dev/null +++ b/src/core/Akka.Streams/Dsl/PagedSource.cs @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Akka.Annotations; +using Akka.Streams.Util; + +namespace Akka.Streams.Dsl +{ + public static class PagedSource + { + /// + /// Page for . + /// + /// type of page items + /// type of page keys + public class Page + { + public IEnumerable Items { get; } + public Option NextKey { get; } + + public Page(IEnumerable items, Option nextKey) + { + Items = items; + NextKey = nextKey; + } + } + + /// + /// Defines a factory for "paged source". + /// + /// "Paged source" is a Source streaming items from a paged API. + /// The paged API is accessed with a page key and returns data. + /// This data contain items and optional information about the key of the next page. + /// + /// + /// type of page items + /// type of page keys + /// key of first page + /// maps page key to Task of page data + [ApiMayChange] + public static Source Create(TKey firstKey, Func>> pageFactory) + { + var pageSource = + Source.UnfoldAsync + ( + new Option(firstKey), + async key => + { + var page = key.HasValue ? await pageFactory(key.Value) : new Page(Enumerable.Empty(), Option.None); + + if (page.Items != null && page.Items.Any()) + return Tuple.Create(page.NextKey, page); + else + return null; + } + ); + + return pageSource.ConcatMany(page => Source.From(page.Items)); + } + } +} From 9fe4e12c5aa95c82b05ebf708fe25a661419bd28 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 23 Aug 2018 09:33:16 -0500 Subject: [PATCH 70/70] Akka.NET v1.3.9 Release Notes (#3574) * first cut at Akka.NET v1.3.9 release notes * finished v1.3.9 release notes * added link back to milestone --- RELEASE_NOTES.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++-- src/common.props | 66 ++++++++++++++++++++++++++++++++++++- 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 49092bd036f..3e46819ce78 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,86 @@ -#### 1.3.9 June 04 2018 #### -Placeholder for nightlies. +#### 1.3.9 August 22 2018 #### +**Maintenance Release for Akka.NET 1.3** + +Akka.NET v1.3.9 features some major changes to Akka.Cluster.Sharding, additional Akka.Streams stages, and some general bug fixes across the board. + +**Akka.Cluster.Sharding Improvements** +The [Akka.Cluster.Sharding documentation](http://getakka.net/articles/clustering/cluster-sharding.html#quickstart) already describes some of the major changes in Akka.NET v1.3.9, but we figured it would be worth calling special attention to those changes here. + +**Props Factory for Entity Actors** + +> In some cases, the actor may need to know the `entityId` associated with it. This can be achieved using the `entityPropsFactory` parameter to `ClusterSharding.Start` or `ClusterSharding.StartAsync`. The entity ID will be passed to the factory as a parameter, which can then be used in the creation of the actor. + +In addition to the existing APIs we've always had for defining sharded entities via `Props`, Akka.NET v1.3.9 introduces [a new method overload for `Start`](http://getakka.net/api/Akka.Cluster.Sharding.ClusterSharding.html#Akka_Cluster_Sharding_ClusterSharding_Start_System_String_System_Func_System_String_Akka_Actor_Props__Akka_Cluster_Sharding_ClusterShardingSettings_Akka_Cluster_Sharding_ExtractEntityId_Akka_Cluster_Sharding_ExtractShardId_) and [`StartAsync`](http://getakka.net/api/Akka.Cluster.Sharding.ClusterSharding.html#Akka_Cluster_Sharding_ClusterSharding_StartAsync_System_String_System_Func_System_String_Akka_Actor_Props__Akka_Cluster_Sharding_ClusterShardingSettings_Akka_Cluster_Sharding_ExtractEntityId_Akka_Cluster_Sharding_ExtractShardId_) which allows users to pass in the `entityId` of each entity actor as a constructor argument to those entities when they start. + +For example: + +``` +var anotherCounterShard = ClusterSharding.Get(Sys).Start( + typeName: "AnotherCounter", + entityProps: Props.Create(), + typeName: AnotherCounter.ShardingTypeName, + entityPropsFactory: entityId => AnotherCounter.Props(entityId), + settings: ClusterShardingSettings.Create(Sys), + extractEntityId: Counter.ExtractEntityId, + extractShardId: Counter.ExtractShardId); +``` + +This will give you the opportunity to pass in the `entityId` for each actor as a constructor argument into the `Props` of your entity actor and possibly other use cases too. + +**Improvements to Starting and Querying Existing Shard Entity Types** +Two additional major usability improvements to Cluster.Sharding come from some API additions and changes. + +The first is that it's now possible to look up all of the currently registered shard types via the [`ClusterSharding.ShardTypeNames` property](http://getakka.net/api/Akka.Cluster.Sharding.ClusterSharding.html#Akka_Cluster_Sharding_ClusterSharding_ShardTypeNames). So long as a `ShardRegion` of that type has been started in the cluster, that entity type name will be added to the collection exposed by this property. + +The other major usability improvement is a change to the `ClusterSharding.Start` property itself. Historically, you used to have to know whether or not the node you wanted to use sharding on was going to be hosting shards (call `ClusterSharding.Start`) or simply communicated with shards hosted on a different cluster role type (call `ClusterSharding.StartProxy`). Going forward, it's safe to call `ClusterSharding.Start` on any node and you will either receive an `IActorRef` to active `ShardRegion` or a `ShardRegion` running in "proxy only" mode; this is determined by looking at the `ClusterShardingSettings` and determining if the current node is in a role that is allowed to host shards of this type. + +* [Akka.Cluster.Sharding: Sharding API Updates](https://github.com/akkadotnet/akka.net/pull/3524) +* [Akka.Cluster.Sharding: sharding rebalance fix](https://github.com/akkadotnet/akka.net/pull/3518) +* [Akka.Cluster.Sharding: log formatting fix](https://github.com/akkadotnet/akka.net/pull/3554) +* [Akka.Cluster.Sharding: `RestartShard` escapes into userspace](https://github.com/akkadotnet/akka.net/pull/3509) + +**Akka.Streams Additions and Changes** +In Akka.NET v1.3.9 we've added some new built-in stream stages and API methods designed to help improve developer productivity and ease of use. + +* [Akka.Streams: add CombineMaterialized method to Source](https://github.com/akkadotnet/akka.net/pull/3489) +* [Akka.Streams: +KillSwitches: flow stage from CancellationToken](https://github.com/akkadotnet/akka.net/pull/3568) +* [Akka.Streams: Port KeepAliveConcat and UnfoldFlow](https://github.com/akkadotnet/akka.net/pull/3560) +* [Akka.Streams: Port PagedSource & IntervalBasedRateLimiter](https://github.com/akkadotnet/akka.net/pull/3570) + +**Other Updates, Additions, and Bugfixes** +* [Akka.Cluster: cluster coordinated leave fix for empty cluster](https://github.com/akkadotnet/akka.net/pull/3516) +* [Akka.Cluster.Tools: bumped ClusterClient message drop log messages from DEBUG to WARNING](https://github.com/akkadotnet/akka.net/pull/3513) +* [Akka.Cluster.Tools: Singleton - confirm TakeOverFromMe when singleton already in oldest state](https://github.com/akkadotnet/akka.net/pull/3553) +* [Akka.Remote: RemoteWatcher race-condition fix](https://github.com/akkadotnet/akka.net/pull/3519) +* [Akka: fix concurrency bug in CircuitBreaker](https://github.com/akkadotnet/akka.net/pull/3505) +* [Akka: Fixed ReceiveTimeout not triggered in some case when combined with NotInfluenceReceiveTimeout messages](https://github.com/akkadotnet/akka.net/pull/3555) +* [Akka.Persistence: Optimized recovery](https://github.com/akkadotnet/akka.net/pull/3549) +* [Akka.Persistence: Allow persisting events when recovery has completed](https://github.com/akkadotnet/akka.net/pull/3366) + +To [see the full set of changes for Akka.NET v1.3.9, click here](https://github.com/akkadotnet/akka.net/milestone/27). + +| COMMITS | LOC+ | LOC- | AUTHOR | +| --- | --- | --- | --- | +| 28 | 2448 | 5691 | Aaron Stannard | +| 11 | 1373 | 230 | zbynek001 | +| 8 | 4590 | 577 | Bartosz Sypytkowski | +| 4 | 438 | 99 | Ismael Hamed | +| 4 | 230 | 240 | Sean Gilliam | +| 2 | 1438 | 0 | Oleksandr Bogomaz | +| 1 | 86 | 79 | Nick Polideropoulos | +| 1 | 78 | 0 | v1rusw0rm | +| 1 | 4 | 4 | Joshua Garnett | +| 1 | 32 | 17 | Jarl Sveinung Flø Rasmussen | +| 1 | 27 | 1 | Sam13 | +| 1 | 250 | 220 | Maxim Cherednik | +| 1 | 184 | 124 | Josh Taylor | +| 1 | 14 | 0 | Peter Shrosbree | +| 1 | 1278 | 42 | Marc Piechura | +| 1 | 1 | 1 | Vasily Kirichenko | +| 1 | 1 | 1 | Samuel Kelemen | +| 1 | 1 | 1 | Nyola Mike | +| 1 | 1 | 1 | Fábio Beirão | #### 1.3.8 June 04 2018 #### **Maintenance Release for Akka.NET 1.3** diff --git a/src/common.props b/src/common.props index c35943e16dc..a8384606649 100644 --- a/src/common.props +++ b/src/common.props @@ -17,6 +17,70 @@ true - Placeholder for nightlies. + Maintenance Release for Akka.NET 1.3** +Akka.NET v1.3.9 features some major changes to Akka.Cluster.Sharding, additional Akka.Streams stages, and some general bug fixes across the board. +Akka.Cluster.Sharding Improvements** +The [Akka.Cluster.Sharding documentation](http://getakka.net/articles/clustering/cluster-sharding.html#quickstart) already describes some of the major changes in Akka.NET v1.3.9, but we figured it would be worth calling special attention to those changes here. +Props Factory for Entity Actors** +> In some cases, the actor may need to know the `entityId` associated with it. This can be achieved using the `entityPropsFactory` parameter to `ClusterSharding.Start` or `ClusterSharding.StartAsync`. The entity ID will be passed to the factory as a parameter, which can then be used in the creation of the actor. +In addition to the existing APIs we've always had for defining sharded entities via `Props`, Akka.NET v1.3.9 introduces [a new method overload for `Start`](http://getakka.net/api/Akka.Cluster.Sharding.ClusterSharding.html#Akka_Cluster_Sharding_ClusterSharding_Start_System_String_System_Func_System_String_Akka_Actor_Props__Akka_Cluster_Sharding_ClusterShardingSettings_Akka_Cluster_Sharding_ExtractEntityId_Akka_Cluster_Sharding_ExtractShardId_) and [`StartAsync`](http://getakka.net/api/Akka.Cluster.Sharding.ClusterSharding.html#Akka_Cluster_Sharding_ClusterSharding_StartAsync_System_String_System_Func_System_String_Akka_Actor_Props__Akka_Cluster_Sharding_ClusterShardingSettings_Akka_Cluster_Sharding_ExtractEntityId_Akka_Cluster_Sharding_ExtractShardId_) which allows users to pass in the `entityId` of each entity actor as a constructor argument to those entities when they start. +For example: +``` +var anotherCounterShard = ClusterSharding.Get(Sys).Start( + typeName: "AnotherCounter", + entityProps: Props.Create<AnotherCounter>(), + typeName: AnotherCounter.ShardingTypeName, + entityPropsFactory: entityId => AnotherCounter.Props(entityId), + settings: ClusterShardingSettings.Create(Sys), + extractEntityId: Counter.ExtractEntityId, + extractShardId: Counter.ExtractShardId); +``` +This will give you the opportunity to pass in the `entityId` for each actor as a constructor argument into the `Props` of your entity actor and possibly other use cases too. +Improvements to Starting and Querying Existing Shard Entity Types** +Two additional major usability improvements to Cluster.Sharding come from some API additions and changes. +The first is that it's now possible to look up all of the currently registered shard types via the [`ClusterSharding.ShardTypeNames` property](http://getakka.net/api/Akka.Cluster.Sharding.ClusterSharding.html#Akka_Cluster_Sharding_ClusterSharding_ShardTypeNames). So long as a `ShardRegion` of that type has been started in the cluster, that entity type name will be added to the collection exposed by this property. +The other major usability improvement is a change to the `ClusterSharding.Start` property itself. Historically, you used to have to know whether or not the node you wanted to use sharding on was going to be hosting shards (call `ClusterSharding.Start`) or simply communicated with shards hosted on a different cluster role type (call `ClusterSharding.StartProxy`). Going forward, it's safe to call `ClusterSharding.Start` on any node and you will either receive an `IActorRef` to active `ShardRegion` or a `ShardRegion` running in "proxy only" mode; this is determined by looking at the `ClusterShardingSettings` and determining if the current node is in a role that is allowed to host shards of this type. +[Akka.Cluster.Sharding: Sharding API Updates](https://github.com/akkadotnet/akka.net/pull/3524) +[Akka.Cluster.Sharding: sharding rebalance fix](https://github.com/akkadotnet/akka.net/pull/3518) +[Akka.Cluster.Sharding: log formatting fix](https://github.com/akkadotnet/akka.net/pull/3554) +[Akka.Cluster.Sharding: `RestartShard` escapes into userspace](https://github.com/akkadotnet/akka.net/pull/3509) +Akka.Streams Additions and Changes** +In Akka.NET v1.3.9 we've added some new built-in stream stages and API methods designed to help improve developer productivity and ease of use. +[Akka.Streams: add CombineMaterialized method to Source](https://github.com/akkadotnet/akka.net/pull/3489) +[Akka.Streams: +KillSwitches: flow stage from CancellationToken](https://github.com/akkadotnet/akka.net/pull/3568) +[Akka.Streams: Port KeepAliveConcat and UnfoldFlow](https://github.com/akkadotnet/akka.net/pull/3560) +[Akka.Streams: Port PagedSource & IntervalBasedRateLimiter](https://github.com/akkadotnet/akka.net/pull/3570) +Other Updates, Additions, and Bugfixes** +[Akka.Cluster: cluster coordinated leave fix for empty cluster](https://github.com/akkadotnet/akka.net/pull/3516) +[Akka.Cluster.Tools: bumped ClusterClient message drop log messages from DEBUG to WARNING](https://github.com/akkadotnet/akka.net/pull/3513) +[Akka.Cluster.Tools: Singleton - confirm TakeOverFromMe when singleton already in oldest state](https://github.com/akkadotnet/akka.net/pull/3553) +[Akka.Remote: RemoteWatcher race-condition fix](https://github.com/akkadotnet/akka.net/pull/3519) +[Akka: fix concurrency bug in CircuitBreaker](https://github.com/akkadotnet/akka.net/pull/3505) +[Akka: Fixed ReceiveTimeout not triggered in some case when combined with NotInfluenceReceiveTimeout messages](https://github.com/akkadotnet/akka.net/pull/3555) +[Akka.Persistence: Optimized recovery](https://github.com/akkadotnet/akka.net/pull/3549) +[Akka.Persistence: Allow persisting events when recovery has completed](https://github.com/akkadotnet/akka.net/pull/3366) +To [see the full set of changes for Akka.NET v1.3.9, click here](https://github.com/akkadotnet/akka.net/milestone/27). +| COMMITS | LOC+ | LOC- | AUTHOR | +| --- | --- | --- | --- | +| 28 | 2448 | 5691 | Aaron Stannard | +| 11 | 1373 | 230 | zbynek001 | +| 8 | 4590 | 577 | Bartosz Sypytkowski | +| 4 | 438 | 99 | Ismael Hamed | +| 4 | 230 | 240 | Sean Gilliam | +| 2 | 1438 | 0 | Oleksandr Bogomaz | +| 1 | 86 | 79 | Nick Polideropoulos | +| 1 | 78 | 0 | v1rusw0rm | +| 1 | 4 | 4 | Joshua Garnett | +| 1 | 32 | 17 | Jarl Sveinung Flø Rasmussen | +| 1 | 27 | 1 | Sam13 | +| 1 | 250 | 220 | Maxim Cherednik | +| 1 | 184 | 124 | Josh Taylor | +| 1 | 14 | 0 | Peter Shrosbree | +| 1 | 1278 | 42 | Marc Piechura | +| 1 | 1 | 1 | Vasily Kirichenko | +| 1 | 1 | 1 | Samuel Kelemen | +| 1 | 1 | 1 | Nyola Mike | +| 1 | 1 | 1 | Fábio Beirão | \ No newline at end of file