From d135f57c63035e160fd80d09b349ee484ab04455 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 12 Apr 2017 19:21:10 -0700 Subject: [PATCH 01/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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 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 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/21] 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); ///