From f25de15da83b4f1e09ad640e024414521f1d9220 Mon Sep 17 00:00:00 2001 From: zetanova Date: Tue, 7 Sep 2021 22:44:50 +0200 Subject: [PATCH 01/40] refactor remote-actorref-provider and add tests for cache entries --- src/core/Akka.Remote.Tests/RemotingSpec.cs | 29 +++++++++++++++++++ .../Akka.Remote/RemoteActorRefProvider.cs | 13 +++++---- .../Serialization/ActorPathCache.cs | 5 +++- .../Serialization/ActorRefResolveCache.cs | 5 +++- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/core/Akka.Remote.Tests/RemotingSpec.cs b/src/core/Akka.Remote.Tests/RemotingSpec.cs index 4c3e354222b..7618698411e 100644 --- a/src/core/Akka.Remote.Tests/RemotingSpec.cs +++ b/src/core/Akka.Remote.Tests/RemotingSpec.cs @@ -23,6 +23,7 @@ using Xunit.Abstractions; using Nito.AsyncEx; using ThreadLocalRandom = Akka.Util.ThreadLocalRandom; +using Akka.Remote.Serialization; namespace Akka.Remote.Tests { @@ -177,6 +178,34 @@ public async Task Remoting_must_support_Ask() Assert.IsType>(actorRef); } + [Fact] + public async Task Remoting_should_not_cache_ref_of_local_ask() + { + var localActorRefResolveCache = ActorRefResolveThreadLocalCache.For(Sys); + var localActorPathCache = ActorPathThreadLocalCache.For(Sys); + + var (msg, actorRef) = await _here.Ask<(string, IActorRef)>("ping", DefaultTimeout); + Assert.Equal("pong", msg); + Assert.IsType>(actorRef); + + Assert.Equal(0, localActorRefResolveCache.All.Sum(n => n.Stats.Entries)); + Assert.Equal(2, localActorPathCache.All.Sum(n => n.Stats.Entries)); + } + + [Fact] + public async Task Remoting_should_not_cache_ref_of_remote_ask() + { + var remoteActorRefResolveCache = ActorRefResolveThreadLocalCache.For(_remoteSystem); + var remoteActorPathCache = ActorPathThreadLocalCache.For(_remoteSystem); + + var (msg, actorRef) = await _here.Ask<(string, IActorRef)>("ping", DefaultTimeout); + Assert.Equal("pong", msg); + Assert.IsType>(actorRef); + + Assert.Equal(0, remoteActorRefResolveCache.All.Sum(n => n.Stats.Entries)); + Assert.Equal(2, remoteActorPathCache.All.Sum(n => n.Stats.Entries)); //should be 1 + } + [Fact(Skip = "Racy")] public async Task Ask_does_not_deadlock() { diff --git a/src/core/Akka.Remote/RemoteActorRefProvider.cs b/src/core/Akka.Remote/RemoteActorRefProvider.cs index 0649dff960a..b2f6fe45c5e 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -238,8 +238,8 @@ public void UnregisterTempActor(ActorPath path) private volatile IActorRef _remotingTerminator; private volatile IActorRef _remoteWatcher; - private volatile ActorRefResolveThreadLocalCache _actorRefResolveThreadLocalCache; - private volatile ActorPathThreadLocalCache _actorPathThreadLocalCache; + private ActorRefResolveThreadLocalCache _actorRefResolveThreadLocalCache; + private ActorPathThreadLocalCache _actorPathThreadLocalCache; /// /// The remote death watcher. @@ -252,11 +252,11 @@ public virtual void Init(ActorSystemImpl system) { _system = system; - _local.Init(system); - _actorRefResolveThreadLocalCache = ActorRefResolveThreadLocalCache.For(system); _actorPathThreadLocalCache = ActorPathThreadLocalCache.For(system); + _local.Init(system); + _remotingTerminator = _system.SystemActorOf( RemoteSettings.ConfigureDispatcher(Props.Create(() => new RemotingTerminator(_local.SystemGuardian))), @@ -435,7 +435,7 @@ public Deploy LookUpRemotes(IEnumerable p) public bool HasAddress(Address address) { - return address == _local.RootPath.Address || address == RootPath.Address || Transport.Addresses.Any(a => a == address); + return address == _local.RootPath.Address || Transport.Addresses.Contains(address); } /// @@ -539,7 +539,8 @@ public IActorRef ResolveActorRef(string path) // if the value is not cached if (_actorRefResolveThreadLocalCache == null) { - return InternalResolveActorRef(path); // cache not initialized yet + // cache not initialized yet, should never happen + return InternalResolveActorRef(path); } return _actorRefResolveThreadLocalCache.Cache.GetOrCompute(path); } diff --git a/src/core/Akka.Remote/Serialization/ActorPathCache.cs b/src/core/Akka.Remote/Serialization/ActorPathCache.cs index f837bb0148d..1f292dcb1df 100644 --- a/src/core/Akka.Remote/Serialization/ActorPathCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorPathCache.cs @@ -8,6 +8,7 @@ using System; using Akka.Actor; using System.Threading; +using System.Collections.Generic; namespace Akka.Remote.Serialization { @@ -16,10 +17,12 @@ namespace Akka.Remote.Serialization /// internal sealed class ActorPathThreadLocalCache : ExtensionIdProvider, IExtension { - private readonly ThreadLocal _current = new ThreadLocal(() => new ActorPathCache()); + private readonly ThreadLocal _current = new ThreadLocal(() => new ActorPathCache(), true); public ActorPathCache Cache => _current.Value; + internal IList All => _current.Values; + public override ActorPathThreadLocalCache CreateExtension(ExtendedActorSystem system) { return new ActorPathThreadLocalCache(); diff --git a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs index 2bd7de5b453..4b650b6e894 100644 --- a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs @@ -5,6 +5,7 @@ // //----------------------------------------------------------------------- +using System.Collections.Generic; using System.Threading; using Akka.Actor; using Akka.Util.Internal; @@ -23,7 +24,7 @@ public ActorRefResolveThreadLocalCache() { } public ActorRefResolveThreadLocalCache(IRemoteActorRefProvider provider) { _provider = provider; - _current = new ThreadLocal(() => new ActorRefResolveCache(_provider)); + _current = new ThreadLocal(() => new ActorRefResolveCache(_provider), true); } public override ActorRefResolveThreadLocalCache CreateExtension(ExtendedActorSystem system) @@ -35,6 +36,8 @@ public override ActorRefResolveThreadLocalCache CreateExtension(ExtendedActorSys public ActorRefResolveCache Cache => _current.Value; + internal IList All => _current.Values; + public static ActorRefResolveThreadLocalCache For(ActorSystem system) { return system.WithExtension(); From 3fc01687508d4e3aab796e49c2a7776ca6428211 Mon Sep 17 00:00:00 2001 From: zetanova Date: Tue, 7 Sep 2021 23:40:46 +0200 Subject: [PATCH 02/40] replace address-cache with actorpath-cache --- src/core/Akka.Remote/Transport/AkkaPduCodec.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs index bde6e0baf27..98b39589f49 100644 --- a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs +++ b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs @@ -202,12 +202,12 @@ public AckAndMessage(Ack ackOption, Message messageOption) internal abstract class AkkaPduCodec { protected readonly ActorSystem System; - protected readonly AddressThreadLocalCache AddressCache; + protected readonly AddressThreadLocalCache ActorPathCache; protected AkkaPduCodec(ActorSystem system) { System = system; - AddressCache = AddressThreadLocalCache.For(system); + ActorPathCache = AddressThreadLocalCache.For(system); } /// @@ -427,9 +427,9 @@ public override AckAndMessage DecodeMessage(ByteString raw, IRemoteActorRefProvi { var recipient = provider.ResolveActorRefWithLocalAddress(envelopeContainer.Recipient.Path, localAddress); Address recipientAddress; - if (AddressCache != null) + if (ActorPathCache != null) { - recipientAddress = AddressCache.Cache.GetOrCompute(envelopeContainer.Recipient.Path); + recipientAddress = ActorPathCache.Cache.GetOrCompute(envelopeContainer.Recipient.Path); } else { From 9a67f1c542bcaf2e81e788fd04cd8e88554a7b72 Mon Sep 17 00:00:00 2001 From: zetanova Date: Tue, 7 Sep 2021 23:54:52 +0200 Subject: [PATCH 03/40] refactor resolve with local address --- .../Akka.Remote/RemoteActorRefProvider.cs | 51 +++++++++---------- .../Akka.Remote/Transport/AkkaPduCodec.cs | 4 +- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/core/Akka.Remote/RemoteActorRefProvider.cs b/src/core/Akka.Remote/RemoteActorRefProvider.cs index b2f6fe45c5e..753d7dd11bc 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -433,9 +433,10 @@ public Deploy LookUpRemotes(IEnumerable p) return Deploy.None; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasAddress(Address address) { - return address == _local.RootPath.Address || Transport.Addresses.Contains(address); + return address == RootPath.Address || Transport.Addresses.Contains(address); } /// @@ -458,21 +459,6 @@ private IInternalActorRef LocalActorOf(ActorSystemImpl system, Props props, IInt return _local.ActorOf(system, props, supervisor, path, systemService, deploy, lookupDeploy, async); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryParseCachedPath(string actorPath, out ActorPath path) - { - if (_actorPathThreadLocalCache != null) - { - path = _actorPathThreadLocalCache.Cache.GetOrCompute(actorPath); - return path != null; - } - else // cache not initialized yet - { - return ActorPath.TryParse(actorPath, out path); - } - } - - /// /// INTERNAL API. /// @@ -483,20 +469,31 @@ private bool TryParseCachedPath(string actorPath, out ActorPath path) /// TBD public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address localAddress) { - if (TryParseCachedPath(path, out var actorPath)) + ActorPath actorPath; + if (_actorPathThreadLocalCache != null) { - //the actor's local address was already included in the ActorPath - if (HasAddress(actorPath.Address)) - { - if (actorPath is RootActorPath) - return RootGuardian; - return (IInternalActorRef)ResolveActorRef(path); // so we can use caching - } + actorPath = _actorPathThreadLocalCache.Cache.GetOrCompute(path); + } + else // cache not initialized yet + { + ActorPath.TryParse(path, out actorPath); + } - return CreateRemoteRef(new RootActorPath(actorPath.Address) / actorPath.ElementsWithUid, localAddress); + if (path is null) + { + _log.Debug("resolve of unknown path [{0}] failed", path); + return InternalDeadLetters; } - _log.Debug("resolve of unknown path [{0}] failed", path); - return InternalDeadLetters; + + if (!HasAddress(actorPath.Address)) + return CreateRemoteRef(new RootActorPath(actorPath.Address) / actorPath.ElementsWithUid, localAddress); + + //the actor's local address was already included in the ActorPath + + if (actorPath is RootActorPath) + return RootGuardian; + + return (IInternalActorRef)ResolveActorRef(path); // so we can use caching } diff --git a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs index 98b39589f49..e0d9ffeae6d 100644 --- a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs +++ b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs @@ -431,9 +431,9 @@ public override AckAndMessage DecodeMessage(ByteString raw, IRemoteActorRefProvi { recipientAddress = ActorPathCache.Cache.GetOrCompute(envelopeContainer.Recipient.Path); } - else + else if (!ActorPath.TryParseAddress(envelopeContainer.Recipient.Path, out recipientAddress)) { - ActorPath.TryParseAddress(envelopeContainer.Recipient.Path, out recipientAddress); + recipientAddress = Address.AllSystems; } var serializedMessage = envelopeContainer.Message; From 746f76b0de30c6fb8d00a2ee53a1c1ece2d470a5 Mon Sep 17 00:00:00 2001 From: zetanova Date: Wed, 8 Sep 2021 00:13:17 +0200 Subject: [PATCH 04/40] refactor and cleanup --- .../Akka.Remote/RemoteActorRefProvider.cs | 25 ++++++++++--------- .../Akka.Remote/Transport/AkkaPduCodec.cs | 16 ++++-------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/core/Akka.Remote/RemoteActorRefProvider.cs b/src/core/Akka.Remote/RemoteActorRefProvider.cs index 753d7dd11bc..4a7423fbfdd 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -79,7 +79,7 @@ public interface IRemoteActorRefProvider : IActorRefProvider /// method. /// /// The path of the actor we intend to resolve. - /// An if a match was found. Otherwise nobody. + /// An if a match was found. Otherwise deadletters. IActorRef InternalResolveActorRef(string path); /// @@ -590,19 +590,20 @@ public IActorRef ResolveActorRef(ActorPath actorPath) /// The remote Address, if applicable. If not applicable null may be returned. public Address GetExternalAddressFor(Address address) { - if (HasAddress(address)) { return _local.RootPath.Address; } - if (!string.IsNullOrEmpty(address.Host) && address.Port.HasValue) + if (HasAddress(address)) + return _local.RootPath.Address; + + if (string.IsNullOrEmpty(address.Host) || !address.Port.HasValue) + return null; + + try { - try - { - return Transport.LocalAddressForRemote(address); - } - catch - { - return null; - } + return Transport.LocalAddressForRemote(address); + } + catch + { + return null; } - return null; } /// diff --git a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs index e0d9ffeae6d..8e257d47a55 100644 --- a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs +++ b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs @@ -426,22 +426,15 @@ public override AckAndMessage DecodeMessage(ByteString raw, IRemoteActorRefProvi if (envelopeContainer != null) { var recipient = provider.ResolveActorRefWithLocalAddress(envelopeContainer.Recipient.Path, localAddress); - Address recipientAddress; - if (ActorPathCache != null) - { - recipientAddress = ActorPathCache.Cache.GetOrCompute(envelopeContainer.Recipient.Path); - } - else if (!ActorPath.TryParseAddress(envelopeContainer.Recipient.Path, out recipientAddress)) - { - recipientAddress = Address.AllSystems; - } + + //todo get parsed address from provider + var recipientAddress = ActorPathCache.Cache.GetOrCompute(envelopeContainer.Recipient.Path); var serializedMessage = envelopeContainer.Message; IActorRef senderOption = null; if (envelopeContainer.Sender != null) - { senderOption = provider.ResolveActorRefWithLocalAddress(envelopeContainer.Sender.Path, localAddress); - } + SeqNo seqOption = null; if (envelopeContainer.Seq != SeqUndefined) { @@ -450,6 +443,7 @@ public override AckAndMessage DecodeMessage(ByteString raw, IRemoteActorRefProvi seqOption = new SeqNo((long)envelopeContainer.Seq); //proto takes a ulong } } + messageOption = new Message(recipient, recipientAddress, serializedMessage, senderOption, seqOption); } } From bff01bb2895a824be1127c46e6a06363c81e21f6 Mon Sep 17 00:00:00 2001 From: zetanova Date: Wed, 8 Sep 2021 00:45:52 +0200 Subject: [PATCH 05/40] remove volatile from fields --- src/core/Akka.Remote/RemoteActorRefProvider.cs | 10 +++++----- src/core/Akka.Remote/RemoteSystemDaemon.cs | 4 ++-- src/core/Akka.Remote/Remoting.cs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/Akka.Remote/RemoteActorRefProvider.cs b/src/core/Akka.Remote/RemoteActorRefProvider.cs index 4a7423fbfdd..2c71c221250 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -128,7 +128,7 @@ public RemoteActorRefProvider(string systemName, Settings settings, EventStream } private readonly LocalActorRefProvider _local; - private volatile Internals _internals; + private Internals _internals; private ActorSystemImpl _system; private Internals RemoteInternals @@ -235,8 +235,8 @@ public void UnregisterTempActor(ActorPath path) _local.UnregisterTempActor(path); } - private volatile IActorRef _remotingTerminator; - private volatile IActorRef _remoteWatcher; + private IActorRef _remotingTerminator; + private IActorRef _remoteWatcher; private ActorRefResolveThreadLocalCache _actorRefResolveThreadLocalCache; private ActorPathThreadLocalCache _actorPathThreadLocalCache; @@ -245,7 +245,7 @@ public void UnregisterTempActor(ActorPath path) /// The remote death watcher. /// public IActorRef RemoteWatcher => _remoteWatcher; - private volatile IActorRef _remoteDeploymentWatcher; + private IActorRef _remoteDeploymentWatcher; /// public virtual void Init(ActorSystemImpl system) @@ -262,7 +262,7 @@ public virtual void Init(ActorSystemImpl system) RemoteSettings.ConfigureDispatcher(Props.Create(() => new RemotingTerminator(_local.SystemGuardian))), "remoting-terminator"); - _internals = CreateInternals(); + _internals = CreateInternals(); _remotingTerminator.Tell(RemoteInternals); diff --git a/src/core/Akka.Remote/RemoteSystemDaemon.cs b/src/core/Akka.Remote/RemoteSystemDaemon.cs index 2e5fce15820..d1dc349ccab 100644 --- a/src/core/Akka.Remote/RemoteSystemDaemon.cs +++ b/src/core/Akka.Remote/RemoteSystemDaemon.cs @@ -28,7 +28,7 @@ internal interface IDaemonMsg { } /// /// INTERNAL API /// - internal class DaemonMsgCreate : IDaemonMsg + internal sealed class DaemonMsgCreate : IDaemonMsg { /// /// Initializes a new instance of the class. @@ -77,7 +77,7 @@ public DaemonMsgCreate(Props props, Deploy deploy, string path, IActorRef superv /// /// It acts as the brain of the remote that responds to system remote messages and executes actions accordingly. /// - internal class RemoteSystemDaemon : VirtualPathContainer + internal sealed class RemoteSystemDaemon : VirtualPathContainer { private readonly ActorSystemImpl _system; private readonly Switch _terminating = new Switch(false); diff --git a/src/core/Akka.Remote/Remoting.cs b/src/core/Akka.Remote/Remoting.cs index 25e4eb6cdad..28242173e71 100644 --- a/src/core/Akka.Remote/Remoting.cs +++ b/src/core/Akka.Remote/Remoting.cs @@ -111,7 +111,7 @@ internal interface IPriorityMessage { } /// /// INTERNAL API /// - internal class Remoting : RemoteTransport + internal sealed class Remoting : RemoteTransport { private readonly ILoggingAdapter _log; private volatile IDictionary> _transportMapping; From 2a55502d349594b1817157f58b558421f66313af Mon Sep 17 00:00:00 2001 From: zetanova Date: Wed, 8 Sep 2021 17:31:02 +0200 Subject: [PATCH 06/40] remove double equals --- src/core/Akka.Remote/RemoteActorRefProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Akka.Remote/RemoteActorRefProvider.cs b/src/core/Akka.Remote/RemoteActorRefProvider.cs index b063d601adf..325adacdbda 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -436,7 +436,7 @@ public Deploy LookUpRemotes(IEnumerable p) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasAddress(Address address) { - return address.Equals(_local.RootPath.Address) || address.Equals(RootPath.Address) || Transport.Addresses.Contains(address); + return address.Equals(RootPath.Address) || Transport.Addresses.Contains(address); } /// From 14e88e26669914b28a2a638aaa19e688a855608a Mon Sep 17 00:00:00 2001 From: zetanova Date: Fri, 10 Sep 2021 00:00:48 +0200 Subject: [PATCH 07/40] cleanup --- src/core/Akka/Actor/ActorPath.cs | 129 +++++++++++++------------------ 1 file changed, 52 insertions(+), 77 deletions(-) diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 8491dd1e886..bbf5e6afe95 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -11,7 +11,6 @@ using System.Linq; using Akka.Util; using Newtonsoft.Json; -using static System.String; namespace Akka.Actor { @@ -36,7 +35,7 @@ public abstract class ActorPath : IEquatable, IComparable, /// This class represents a surrogate of an . /// Its main use is to help during the serialization process. /// - public class Surrogate : ISurrogate, IEquatable, IEquatable + public sealed class Surrogate : ISurrogate, IEquatable, IEquatable { /// /// Initializes a new instance of the class. @@ -59,12 +58,7 @@ public Surrogate(string path) /// The encapsulated by this surrogate. public ISurrogated FromSurrogate(ActorSystem system) { - if (TryParse(Path, out var path)) - { - return path; - } - - return null; + return TryParse(Path, out var path) ? path : null; } #region Equality @@ -72,25 +66,23 @@ public ISurrogated FromSurrogate(ActorSystem system) /// public bool Equals(Surrogate other) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return string.Equals(Path, other.Path); + if (other is null) return false; + return ReferenceEquals(this, other) || StringComparer.Ordinal.Equals(Path, other.Path); } /// public bool Equals(ActorPath other) { - if (other == null) return false; + if (other is null) return false; return Equals(other.ToSurrogate(null)); //TODO: not so sure if this is OK } /// public override bool Equals(object obj) { - if (ReferenceEquals(null, obj)) return false; + if (obj is null) return false; if (ReferenceEquals(this, obj)) return true; - var actorPath = obj as ActorPath; - if (actorPath != null) return Equals(actorPath); + if (obj is ActorPath actorPath) return Equals(actorPath); return Equals(obj as Surrogate); } @@ -117,11 +109,7 @@ public override int GetHashCode() /// TBD public static bool IsValidPathElement(string s) { - if (IsNullOrEmpty(s)) - { - return false; - } - return !s.StartsWith("$") && Validate(s); + return !string.IsNullOrEmpty(s) && !s.StartsWith("$") && Validate(s); } private static bool IsValidChar(char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || @@ -138,11 +126,11 @@ private static bool Validate(string chars) { if (IsValidChar(chars[pos])) { - pos = pos + 1; + pos += 1; } else if (chars[pos] == '%' && pos + 2 < len && IsHexChar(chars[pos + 1]) && IsHexChar(chars[pos + 2])) { - pos = pos + 3; + pos += 3; } else { @@ -159,21 +147,21 @@ private static bool Validate(string chars) /// The name. protected ActorPath(Address address, string name) { - Name = name; Address = address; + Name = name; } /// /// Initializes a new instance of the class. /// - /// The parent path. + /// The address. /// The name. /// The uid. - protected ActorPath(ActorPath parentPath, string name, long uid) + protected ActorPath(Address address, string name, long uid) { - Address = parentPath.Address; - Uid = uid; + Address = address; Name = name; + Uid = uid; } /// @@ -182,8 +170,6 @@ protected ActorPath(ActorPath parentPath, string name, long uid) /// The uid. public long Uid { get; } - internal static readonly string[] EmptyElements = { }; - /// /// Gets the elements. /// @@ -202,7 +188,7 @@ internal IReadOnlyList ElementsWithUid { get { - if (this is RootActorPath) return EmptyElements; + if (this is RootActorPath) return Array.Empty(); var elements = (List)Elements; elements[elements.Count - 1] = AppendUidFragment(Name); return elements; @@ -243,7 +229,7 @@ public bool Equals(ActorPath other) ActorPath a = this; ActorPath b = other; - for (; ; ) + while (true) { if (ReferenceEquals(a, b)) return true; @@ -308,12 +294,10 @@ public bool Equals(ActorPath other) /// A newly created public static ActorPath Parse(string path) { - ActorPath actorPath; - if (TryParse(path, out actorPath)) - { - return actorPath; - } - throw new UriFormatException($"Can not parse an ActorPath: {path}"); + if (!TryParse(path, out var actorPath)) + throw new UriFormatException($"Can not parse an ActorPath: {path}"); + + return actorPath; } /// @@ -325,13 +309,14 @@ public static ActorPath Parse(string path) /// TBD public static bool TryParse(string path, out ActorPath actorPath) { - actorPath = null; - - if (!TryParseAddress(path, out var address, out var absoluteUri)) return false; - var spanified = absoluteUri; + if (!TryParseAddress(path, out var address, out var spanified)) + { + actorPath = null; + return false; + } // check for Uri fragment here - var nextSlash = 0; + int nextSlash; actorPath = new RootActorPath(address); @@ -347,7 +332,7 @@ public static bool TryParse(string path, out ActorPath actorPath) var fragLoc = spanified.IndexOf('#'); if (fragLoc > -1) { - var fragment = spanified.Slice(fragLoc+1); + var fragment = spanified.Slice(fragLoc + 1); var fragValue = SpanHacks.Parse(fragment); spanified = spanified.Slice(0, fragLoc); actorPath = new ChildActorPath(actorPath, spanified.ToString(), fragValue); @@ -356,7 +341,7 @@ public static bool TryParse(string path, out ActorPath actorPath) { actorPath /= spanified.ToString(); } - + } spanified = spanified.Slice(nextSlash + 1); @@ -405,7 +390,7 @@ private static bool TryParseAddress(string path, out Address address, out ReadOn spanified = spanified.Slice(2); // move past the double // var firstAtPos = spanified.IndexOf('@'); - var sysName = string.Empty; + string sysName; if (firstAtPos == -1) { // dealing with an absolute local Uri @@ -435,7 +420,7 @@ private static bool TryParseAddress(string path, out Address address, out ReadOn * - IPV4 / hostnames * - IPV6 (must be surrounded by '[]') according to spec. */ - var host = string.Empty; + string host; // check for IPV6 first var openBracket = spanified.IndexOf('['); @@ -555,10 +540,7 @@ public override string ToString() /// TBD public string ToStringWithUid() { - var uid = Uid; - if (uid == ActorCell.UndefinedUid) - return ToStringWithAddress(); - return ToStringWithAddress() + "#" + uid; + return Uid != ActorCell.UndefinedUid ? $"{ToStringWithAddress()}#{Uid}" : ToStringWithAddress(); } /// @@ -587,8 +569,7 @@ public override int GetHashCode() /// public override bool Equals(object obj) { - var other = obj as ActorPath; - return Equals(other); + return Equals(obj as ActorPath); } /// @@ -650,10 +631,7 @@ public string ToSerializationFormatWithAddress(Address address) private string AppendUidFragment(string withAddress) { - if (Uid == ActorCell.UndefinedUid) - return withAddress; - - return String.Concat(withAddress, "#", Uid.ToString()); + return Uid != ActorCell.UndefinedUid ? $"{withAddress}#{Uid}" : withAddress; } /// @@ -683,7 +661,7 @@ public string ToStringWithAddress(Address address) /// TBD public static string FormatPathElements(IEnumerable pathElements) { - return String.Join("/", pathElements); + return string.Join("/", pathElements); } /// @@ -700,7 +678,7 @@ public ISurrogate ToSurrogate(ActorSystem system) /// /// Actor paths for root guardians, such as "/user" and "/system" /// - public class RootActorPath : ActorPath + public sealed class RootActorPath : ActorPath { /// /// Initializes a new instance of the class. @@ -715,7 +693,7 @@ public RootActorPath(Address address, string name = "") /// public override ActorPath Parent => null; - public override IReadOnlyList Elements => EmptyElements; + public override IReadOnlyList Elements => Array.Empty(); /// [JsonIgnore] @@ -724,25 +702,25 @@ public RootActorPath(Address address, string name = "") /// public override ActorPath WithUid(long uid) { - if (uid == 0) - return this; - throw new NotSupportedException("RootActorPath must have undefined Uid"); + if (uid != 0) + throw new NotSupportedException("RootActorPath must have undefined Uid"); + + return this; } /// public override int CompareTo(ActorPath other) { if (other is ChildActorPath) return 1; - return Compare(ToString(), other.ToString(), StringComparison.Ordinal); + return StringComparer.Ordinal.Compare(ToString(), other?.ToString()); } } /// /// Actor paths for child actors, which is to say any non-guardian actor. /// - public class ChildActorPath : ActorPath + public sealed class ChildActorPath : ActorPath { - private readonly string _name; private readonly ActorPath _parent; /// @@ -752,9 +730,8 @@ public class ChildActorPath : ActorPath /// The name. /// The uid. public ChildActorPath(ActorPath parentPath, string name, long uid) - : base(parentPath, name, uid) + : base(parentPath.Address, name, uid) { - _name = name; _parent = parentPath; } @@ -784,9 +761,7 @@ public override ActorPath Root { var current = _parent; while (current is ChildActorPath child) - { current = child._parent; - } return current.Root; } } @@ -798,9 +773,7 @@ public override ActorPath Root /// ActorPath. public override ActorPath WithUid(long uid) { - if (uid == Uid) - return this; - return new ChildActorPath(_parent, _name, uid); + return uid != Uid ? new ChildActorPath(_parent, Name, uid) : this; } /// @@ -825,15 +798,17 @@ public override int CompareTo(ActorPath other) private int InternalCompareTo(ActorPath left, ActorPath right) { if (ReferenceEquals(left, right)) return 0; - var leftRoot = left as RootActorPath; - if (leftRoot != null) + + if (left is RootActorPath leftRoot) return leftRoot.CompareTo(right); - var rightRoot = right as RootActorPath; - if (rightRoot != null) + + if (right is RootActorPath rightRoot) return -rightRoot.CompareTo(left); - var nameCompareResult = Compare(left.Name, right.Name, StringComparison.Ordinal); + + var nameCompareResult = StringComparer.Ordinal.Compare(left.Name, right.Name); if (nameCompareResult != 0) return nameCompareResult; + return InternalCompareTo(left.Parent, right.Parent); } } From 762ec30513292a1ffef0a797cc39d1ce28163d10 Mon Sep 17 00:00:00 2001 From: zetanova Date: Fri, 10 Sep 2021 00:57:20 +0200 Subject: [PATCH 08/40] refactor to base --- src/core/Akka/Actor/ActorPath.cs | 236 +++++++++++++------------------ 1 file changed, 102 insertions(+), 134 deletions(-) diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index bbf5e6afe95..33246200450 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -140,6 +140,11 @@ private static bool Validate(string chars) return true; } + private readonly Address _address; + private readonly ActorPath _parent; + private readonly string _name; + private readonly long _uid; + /// /// Initializes a new instance of the class. /// @@ -147,34 +152,67 @@ private static bool Validate(string chars) /// The name. protected ActorPath(Address address, string name) { - Address = address; - Name = name; + _address = address; + _name = name; } /// /// Initializes a new instance of the class. /// - /// The address. + /// The parentPath. /// The name. /// The uid. - protected ActorPath(Address address, string name, long uid) + protected ActorPath(ActorPath parentPath, string name, long uid) { - Address = address; - Name = name; - Uid = uid; + _parent = parentPath; + _address = parentPath.Address; + _name = name; + _uid = uid; } + /// + /// Gets the name. + /// + /// The name. + public string Name => _name; + + /// + /// The Address under which this path can be reached; walks up the tree to + /// the RootActorPath. + /// + /// The address. + public Address Address => _address; + /// /// Gets the uid. /// /// The uid. - public long Uid { get; } + public long Uid => _uid; + + /// + /// The path of the parent to this actor. + /// + public ActorPath Parent => _parent; /// /// Gets the elements. /// /// The elements. - public abstract IReadOnlyList Elements { get; } + public IReadOnlyList Elements + { + get + { + var acc = new Stack(); + var p = this; + while (true) + { + if (p.IsRoot) + return acc.ToList(); + acc.Push(p.Name); + p = p.Parent; + } + } + } /// /// INTERNAL API. @@ -195,28 +233,28 @@ internal IReadOnlyList ElementsWithUid } } - /// - /// Gets the name. - /// - /// The name. - public string Name { get; } - /// - /// The Address under which this path can be reached; walks up the tree to - /// the RootActorPath. - /// - /// The address. - public Address Address { get; } /// /// The root actor path. /// - public abstract ActorPath Root { get; } + [JsonIgnore] + public ActorPath Root + { + get + { + var current = this; + while (current._parent is ActorPath p) + current = p; + return current; + } + } /// - /// The path of the parent to this actor. + /// Is this instance the root actor path. /// - public abstract ActorPath Parent { get; } + [JsonIgnore] + public bool IsRoot => _parent is null; /// public bool Equals(ActorPath other) @@ -244,14 +282,49 @@ public bool Equals(ActorPath other) } /// - public abstract int CompareTo(ActorPath other); + public int CompareTo(ActorPath other) + { + if (IsRoot) + { + if (other is ChildActorPath) return 1; + return StringComparer.Ordinal.Compare(ToString(), other?.ToString()); + } + return InternalCompareTo(this, other); + } + + private int InternalCompareTo(ActorPath left, ActorPath right) + { + if (ReferenceEquals(left, right)) + return 0; + + if (left.IsRoot) + return left.CompareTo(right); + + if (right.IsRoot) + return -right.CompareTo(left); + + var nameCompareResult = StringComparer.Ordinal.Compare(left.Name, right.Name); + if (nameCompareResult != 0) + return nameCompareResult; + + return InternalCompareTo(left.Parent, right.Parent); + } /// - /// Withes the uid. + /// Creates a copy of the given ActorPath and applies a new Uid /// /// The uid. /// ActorPath. - public abstract ActorPath WithUid(long uid); + public ActorPath WithUid(long uid) + { + if (IsRoot) + { + if (uid != 0) throw new NotSupportedException("RootActorPath must have undefined Uid"); + return this; + } + + return uid != Uid ? new ChildActorPath(_parent, Name, uid) : this; + } /// /// Creates a new with the specified parent @@ -553,15 +626,14 @@ public ActorPath Child(string childName) return this / childName; } - /// public override int GetHashCode() { unchecked { var hash = 17; hash = (hash * 23) ^ Address.GetHashCode(); - foreach (var e in Elements) - hash = (hash * 23) ^ e.GetHashCode(); + for (var p = this; !(p is null); p = p.Parent) + hash = (hash * 23) ^ p.Name.GetHashCode(); return hash; } } @@ -690,30 +762,6 @@ public RootActorPath(Address address, string name = "") { } - /// - public override ActorPath Parent => null; - - public override IReadOnlyList Elements => Array.Empty(); - - /// - [JsonIgnore] - public override ActorPath Root => this; - - /// - public override ActorPath WithUid(long uid) - { - if (uid != 0) - throw new NotSupportedException("RootActorPath must have undefined Uid"); - - return this; - } - - /// - public override int CompareTo(ActorPath other) - { - if (other is ChildActorPath) return 1; - return StringComparer.Ordinal.Compare(ToString(), other?.ToString()); - } } /// @@ -721,8 +769,6 @@ public override int CompareTo(ActorPath other) /// public sealed class ChildActorPath : ActorPath { - private readonly ActorPath _parent; - /// /// Initializes a new instance of the class. /// @@ -730,86 +776,8 @@ public sealed class ChildActorPath : ActorPath /// The name. /// The uid. public ChildActorPath(ActorPath parentPath, string name, long uid) - : base(parentPath.Address, name, uid) - { - _parent = parentPath; - } - - /// - public override ActorPath Parent => _parent; - - public override IReadOnlyList Elements + : base(parentPath, name, uid) { - get - { - ActorPath p = this; - var acc = new Stack(); - while (true) - { - if (p is RootActorPath) - return acc.ToList(); - acc.Push(p.Name); - p = p.Parent; - } - } - } - - /// - public override ActorPath Root - { - get - { - var current = _parent; - while (current is ChildActorPath child) - current = child._parent; - return current.Root; - } - } - - /// - /// Creates a copy of the given ActorPath and applies a new Uid - /// - /// The uid. - /// ActorPath. - public override ActorPath WithUid(long uid) - { - return uid != Uid ? new ChildActorPath(_parent, Name, uid) : this; - } - - /// - public override int GetHashCode() - { - unchecked - { - var hash = 17; - hash = (hash * 23) ^ Address.GetHashCode(); - for (ActorPath p = this; p != null; p = p.Parent) - hash = (hash * 23) ^ p.Name.GetHashCode(); - return hash; - } - } - - /// - public override int CompareTo(ActorPath other) - { - return InternalCompareTo(this, other); - } - - private int InternalCompareTo(ActorPath left, ActorPath right) - { - if (ReferenceEquals(left, right)) return 0; - - if (left is RootActorPath leftRoot) - return leftRoot.CompareTo(right); - - if (right is RootActorPath rightRoot) - return -rightRoot.CompareTo(left); - - var nameCompareResult = StringComparer.Ordinal.Compare(left.Name, right.Name); - if (nameCompareResult != 0) - return nameCompareResult; - - return InternalCompareTo(left.Parent, right.Parent); } } } From aea216692a91126108d589e181c51bab2fff477c Mon Sep 17 00:00:00 2001 From: zetanova Date: Fri, 10 Sep 2021 01:45:21 +0200 Subject: [PATCH 09/40] optimize elements list --- src/core/Akka/Actor/ActorPath.cs | 70 +++++++++++++++++++------------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 33246200450..21329824909 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -142,22 +142,27 @@ private static bool Validate(string chars) private readonly Address _address; private readonly ActorPath _parent; + private readonly int _depth; + private readonly string _name; private readonly long _uid; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class as root. /// /// The address. /// The name. protected ActorPath(Address address, string name) { _address = address; + _parent = null; + _depth = 0; _name = name; + _uid = ActorCell.UndefinedUid; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class as child. /// /// The parentPath. /// The name. @@ -166,6 +171,7 @@ protected ActorPath(ActorPath parentPath, string name, long uid) { _parent = parentPath; _address = parentPath.Address; + _depth = parentPath._depth + 1; _name = name; _uid = uid; } @@ -194,6 +200,11 @@ protected ActorPath(ActorPath parentPath, string name, long uid) /// public ActorPath Parent => _parent; + /// + /// The the depth of the actor. + /// + public int Depth => _depth; + /// /// Gets the elements. /// @@ -202,15 +213,18 @@ public IReadOnlyList Elements { get { - var acc = new Stack(); + if (_depth == 0) + return ImmutableArray.Empty; + + var b = ImmutableArray.CreateBuilder(_depth); + b.Count = _depth; var p = this; - while (true) + for(var i = 0; i < _depth; i++) { - if (p.IsRoot) - return acc.ToList(); - acc.Push(p.Name); - p = p.Parent; + b[_depth - i - 1] = p.Name; + p = p._parent; } + return b.ToImmutable(); } } @@ -226,15 +240,21 @@ internal IReadOnlyList ElementsWithUid { get { - if (this is RootActorPath) return Array.Empty(); - var elements = (List)Elements; - elements[elements.Count - 1] = AppendUidFragment(Name); - return elements; + if (_depth == 0) + return ImmutableArray.Empty; + + var b = ImmutableArray.CreateBuilder(_depth); + b.Count = _depth; + var p = this; + for (var i = 0; i < _depth; i++) + { + b[_depth - i - 1] = i > 0 ? p.Name : AppendUidFragment(p.Name); + p = p._parent; + } + return b.ToImmutable(); } } - - /// /// The root actor path. /// @@ -244,18 +264,12 @@ public ActorPath Root get { var current = this; - while (current._parent is ActorPath p) - current = p; + while (current._depth > 0) + current = current.Parent; return current; } } - /// - /// Is this instance the root actor path. - /// - [JsonIgnore] - public bool IsRoot => _parent is null; - /// public bool Equals(ActorPath other) { @@ -284,7 +298,7 @@ public bool Equals(ActorPath other) /// public int CompareTo(ActorPath other) { - if (IsRoot) + if (_depth == 0) { if (other is ChildActorPath) return 1; return StringComparer.Ordinal.Compare(ToString(), other?.ToString()); @@ -297,17 +311,17 @@ private int InternalCompareTo(ActorPath left, ActorPath right) if (ReferenceEquals(left, right)) return 0; - if (left.IsRoot) + if (left._depth == 0) return left.CompareTo(right); - if (right.IsRoot) + if (right._depth == 0) return -right.CompareTo(left); - var nameCompareResult = StringComparer.Ordinal.Compare(left.Name, right.Name); + var nameCompareResult = StringComparer.Ordinal.Compare(left._name, right._name); if (nameCompareResult != 0) return nameCompareResult; - return InternalCompareTo(left.Parent, right.Parent); + return InternalCompareTo(left._parent, right._parent); } /// @@ -317,7 +331,7 @@ private int InternalCompareTo(ActorPath left, ActorPath right) /// ActorPath. public ActorPath WithUid(long uid) { - if (IsRoot) + if (_depth == 0) { if (uid != 0) throw new NotSupportedException("RootActorPath must have undefined Uid"); return this; From a36170f63eb81dbcf1e48b3cb9fdf6e9fe4498b8 Mon Sep 17 00:00:00 2001 From: zetanova Date: Fri, 10 Sep 2021 03:00:34 +0200 Subject: [PATCH 10/40] improve actor path join --- src/core/Akka.Tests/Actor/ActorPathSpec.cs | 12 +-- src/core/Akka/Actor/ActorPath.cs | 92 +++++++++++++--------- src/core/Akka/Actor/Address.cs | 2 +- 3 files changed, 60 insertions(+), 46 deletions(-) diff --git a/src/core/Akka.Tests/Actor/ActorPathSpec.cs b/src/core/Akka.Tests/Actor/ActorPathSpec.cs index acd081c6494..31d51313842 100644 --- a/src/core/Akka.Tests/Actor/ActorPathSpec.cs +++ b/src/core/Akka.Tests/Actor/ActorPathSpec.cs @@ -103,12 +103,12 @@ public void Supports_parsing_remote_FQDN_paths() [Fact] public void Return_false_upon_malformed_path() { - ActorPath ignored; - ActorPath.TryParse("", out ignored).ShouldBe(false); - ActorPath.TryParse("://hallo", out ignored).ShouldBe(false); - ActorPath.TryParse("s://dd@:12", out ignored).ShouldBe(false); - ActorPath.TryParse("s://dd@h:hd", out ignored).ShouldBe(false); - ActorPath.TryParse("a://l:1/b", out ignored).ShouldBe(false); + ActorPath.TryParse("", out _).ShouldBe(false); + ActorPath.TryParse("://hallo", out _).ShouldBe(false); + ActorPath.TryParse("s://dd@:12", out _).ShouldBe(false); + ActorPath.TryParse("s://dd@h:hd", out _).ShouldBe(false); + ActorPath.TryParse("a://l:1/b", out _).ShouldBe(false); + ActorPath.TryParse("akka:/", out _).ShouldBe(false); } [Fact] diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 21329824909..6e4bff6df77 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -219,7 +219,7 @@ public IReadOnlyList Elements var b = ImmutableArray.CreateBuilder(_depth); b.Count = _depth; var p = this; - for(var i = 0; i < _depth; i++) + for (var i = 0; i < _depth; i++) { b[_depth - i - 1] = p.Name; p = p._parent; @@ -248,7 +248,7 @@ internal IReadOnlyList ElementsWithUid var p = this; for (var i = 0; i < _depth; i++) { - b[_depth - i - 1] = i > 0 ? p.Name : AppendUidFragment(p.Name); + b[_depth - i - 1] = i > 0 ? p._name : AppendUidFragment(p._name); p = p._parent; } return b.ToImmutable(); @@ -396,6 +396,8 @@ public static ActorPath Parse(string path) /// TBD public static bool TryParse(string path, out ActorPath actorPath) { + //todo lookup address and/or root in cache + if (!TryParseAddress(path, out var address, out var spanified)) { actorPath = null; @@ -412,7 +414,8 @@ public static bool TryParse(string path, out ActorPath actorPath) nextSlash = spanified.IndexOf('/'); if (nextSlash > 0) { - actorPath /= spanified.Slice(0, nextSlash).ToString(); + var name = spanified.Slice(0, nextSlash).ToString(); + actorPath = new ChildActorPath(actorPath, name, ActorCell.UndefinedUid); } else if (nextSlash < 0 && spanified.Length > 0) // final segment { @@ -426,13 +429,14 @@ public static bool TryParse(string path, out ActorPath actorPath) } else { - actorPath /= spanified.ToString(); + actorPath = new ChildActorPath(actorPath, spanified.ToString(), ActorCell.UndefinedUid); } } spanified = spanified.Slice(nextSlash + 1); - } while (nextSlash >= 0); + } + while (nextSlash >= 0); return true; } @@ -480,7 +484,8 @@ private static bool TryParseAddress(string path, out Address address, out ReadOn string sysName; if (firstAtPos == -1) - { // dealing with an absolute local Uri + { + // dealing with an absolute local Uri var nextSlash = spanified.IndexOf('/'); if (nextSlash == -1) @@ -572,36 +577,45 @@ private static bool TryParseAddress(string path, out Address address, out ReadOn /// /// Joins this instance. /// + /// the address or empty /// System.String. - private string Join() + private string Join(ReadOnlySpan prefix) { - if (this is RootActorPath) - return "/"; - - // Resolve length of final string - var totalLength = 0; - var p = this; - while (!(p is RootActorPath)) + if (_depth == 0) { - totalLength += p.Name.Length + 1; - p = p.Parent; + Span buffer = stackalloc char[prefix.Length+1]; + prefix.CopyTo(buffer); + buffer[buffer.Length-1] = '/'; + return buffer.ToString(); } - - // Concatenate segments (in reverse order) into buffer with '/' prefixes - char[] buffer = new char[totalLength]; - int offset = buffer.Length; - p = this; - while (!(p is RootActorPath)) + else { - offset -= p.Name.Length + 1; - buffer[offset] = '/'; + // Resolve length of final string + var totalLength = prefix.Length; + var p = this; + while (p._depth > 0) + { + totalLength += p._name.Length + 1; + p = p.Parent; + } - p.Name.CopyTo(0, buffer, offset + 1, p.Name.Length); + // Concatenate segments (in reverse order) into buffer with '/' prefixes + Span buffer = stackalloc char[totalLength]; + prefix.CopyTo(buffer); - p = p.Parent; - } - - return new string(buffer); + var offset = buffer.Length; + ReadOnlySpan name; + p = this; + while (p._depth > 0) + { + name = p._name.AsSpan(); + offset -= name.Length + 1; + buffer[offset] = '/'; + name.CopyTo(buffer.Slice(offset + 1, name.Length)); + p = p.Parent; + } + return buffer.ToString(); + } } /// @@ -612,13 +626,13 @@ private string Join() /// System.String. public string ToStringWithoutAddress() { - return Join(); + return Join(ReadOnlySpan.Empty); } /// public override string ToString() { - return $"{Address}{Join()}"; + return Join(_address.ToString().AsSpan()); } /// @@ -627,7 +641,7 @@ public override string ToString() /// TBD public string ToStringWithUid() { - return Uid != ActorCell.UndefinedUid ? $"{ToStringWithAddress()}#{Uid}" : ToStringWithAddress(); + return _uid != ActorCell.UndefinedUid ? $"{ToStringWithAddress()}#{_uid}" : ToStringWithAddress(); } /// @@ -666,7 +680,7 @@ public override bool Equals(object obj) /// true if both actor paths are equal; otherwise false public static bool operator ==(ActorPath left, ActorPath right) { - return Equals(left, right); + return left?.Equals(right) ?? right is null; } /// @@ -677,7 +691,7 @@ public override bool Equals(object obj) /// true if both actor paths are not equal; otherwise false public static bool operator !=(ActorPath left, ActorPath right) { - return !Equals(left, right); + return !(left == right); } /// @@ -686,7 +700,7 @@ public override bool Equals(object obj) /// System.String. public string ToStringWithAddress() { - return ToStringWithAddress(Address); + return ToStringWithAddress(_address); } /// @@ -717,7 +731,7 @@ public string ToSerializationFormatWithAddress(Address address) private string AppendUidFragment(string withAddress) { - return Uid != ActorCell.UndefinedUid ? $"{withAddress}#{Uid}" : withAddress; + return _uid != ActorCell.UndefinedUid ? $"{withAddress}#{_uid}" : withAddress; } /// @@ -734,10 +748,10 @@ public string ToStringWithAddress(Address address) // we never change address for IgnoreActorRef return ToString(); } - if (Address.Host != null && Address.Port.HasValue) - return $"{Address}{Join()}"; + if (_address.Host != null && _address.Port.HasValue) + return Join(_address.ToString().AsSpan()); - return $"{address}{Join()}"; + return Join(address.ToString().AsSpan()); } /// diff --git a/src/core/Akka/Actor/Address.cs b/src/core/Akka/Actor/Address.cs index 81398855cc6..8b36bd5f38e 100644 --- a/src/core/Akka/Actor/Address.cs +++ b/src/core/Akka/Actor/Address.cs @@ -303,7 +303,7 @@ public static Address Parse(string address) /// This class represents a surrogate of an . /// Its main use is to help during the serialization process. /// - public class AddressSurrogate : ISurrogate + public sealed class AddressSurrogate : ISurrogate { /// /// TBD From aa211c7a0f7ca76c506afc34f39f0d1da4ea0e01 Mon Sep 17 00:00:00 2001 From: zetanova Date: Fri, 10 Sep 2021 03:15:03 +0200 Subject: [PATCH 11/40] improve actor path equals and compare --- src/core/Akka/Actor/ActorPath.cs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 6e4bff6df77..608ee575bb3 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -273,25 +273,25 @@ public ActorPath Root /// public bool Equals(ActorPath other) { - if (other == null) + if (other is null || _depth != other._depth) return false; if (!Address.Equals(other.Address)) return false; - ActorPath a = this; - ActorPath b = other; + var a = this; + var b = other; while (true) { if (ReferenceEquals(a, b)) return true; - else if (a == null || b == null) + else if (a is null || b is null) return false; - else if (a.Name != b.Name) + else if (a._name != b._name) return false; - a = a.Parent; - b = b.Parent; + a = a._parent; + b = b._parent; } } @@ -300,8 +300,8 @@ public int CompareTo(ActorPath other) { if (_depth == 0) { - if (other is ChildActorPath) return 1; - return StringComparer.Ordinal.Compare(ToString(), other?.ToString()); + if (other is null || other._depth > 0) return 1; + return StringComparer.Ordinal.Compare(ToString(), other.ToString()); } return InternalCompareTo(this, other); } @@ -310,6 +310,10 @@ private int InternalCompareTo(ActorPath left, ActorPath right) { if (ReferenceEquals(left, right)) return 0; + if (right is null) + return 1; + if (left is null) + return -1; if (left._depth == 0) return left.CompareTo(right); From 1842505e6a399536f9ecd26c43a5dbb4b8a9b966 Mon Sep 17 00:00:00 2001 From: zetanova Date: Fri, 10 Sep 2021 03:26:29 +0200 Subject: [PATCH 12/40] cleanup --- src/core/Akka/Actor/ActorPath.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 608ee575bb3..82dc3aed100 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -74,7 +74,7 @@ public bool Equals(Surrogate other) public bool Equals(ActorPath other) { if (other is null) return false; - return Equals(other.ToSurrogate(null)); //TODO: not so sure if this is OK + return StringComparer.Ordinal.Equals(Path, other.ToSerializationFormat()); } /// @@ -120,7 +120,7 @@ private static bool IsHexChar(char c) => (c >= 'a' && c <= 'f') || (c >= 'A' && private static bool Validate(string chars) { - int len = chars.Length; + var len = chars.Length; var pos = 0; while (pos < len) { @@ -341,7 +341,7 @@ public ActorPath WithUid(long uid) return this; } - return uid != Uid ? new ChildActorPath(_parent, Name, uid) : this; + return uid != _uid ? new ChildActorPath(_parent, Name, uid) : this; } /// @@ -367,10 +367,10 @@ public ActorPath WithUid(long uid) public static ActorPath operator /(ActorPath path, IEnumerable name) { var a = path; - foreach (string element in name) + foreach (var element in name) { if (!string.IsNullOrEmpty(element)) - a = a / element; + a /= element; } return a; } @@ -587,9 +587,9 @@ private string Join(ReadOnlySpan prefix) { if (_depth == 0) { - Span buffer = stackalloc char[prefix.Length+1]; + Span buffer = stackalloc char[prefix.Length + 1]; prefix.CopyTo(buffer); - buffer[buffer.Length-1] = '/'; + buffer[buffer.Length - 1] = '/'; return buffer.ToString(); } else @@ -619,7 +619,7 @@ private string Join(ReadOnlySpan prefix) p = p.Parent; } return buffer.ToString(); - } + } } /// From f388ceff66519c95db5723013f55f1735f513797 Mon Sep 17 00:00:00 2001 From: zetanova Date: Sat, 11 Sep 2021 21:27:05 +0200 Subject: [PATCH 13/40] protect stack and use moveto of arraybuilder --- src/core/Akka/Actor/ActorPath.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 82dc3aed100..7c4664c8157 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -224,7 +224,7 @@ public IReadOnlyList Elements b[_depth - i - 1] = p.Name; p = p._parent; } - return b.ToImmutable(); + return b.MoveToImmutable(); } } @@ -251,7 +251,7 @@ internal IReadOnlyList ElementsWithUid b[_depth - i - 1] = i > 0 ? p._name : AppendUidFragment(p._name); p = p._parent; } - return b.ToImmutable(); + return b.MoveToImmutable(); } } @@ -587,7 +587,7 @@ private string Join(ReadOnlySpan prefix) { if (_depth == 0) { - Span buffer = stackalloc char[prefix.Length + 1]; + Span buffer = prefix.Length < 1024 ? stackalloc char[prefix.Length + 1] : new char[prefix.Length + 1]; prefix.CopyTo(buffer); buffer[buffer.Length - 1] = '/'; return buffer.ToString(); @@ -604,7 +604,7 @@ private string Join(ReadOnlySpan prefix) } // Concatenate segments (in reverse order) into buffer with '/' prefixes - Span buffer = stackalloc char[totalLength]; + Span buffer = totalLength < 1024 ? stackalloc char[totalLength] : new char[totalLength]; prefix.CopyTo(buffer); var offset = buffer.Length; From baaeea2771d4381ce3f677a46a94643bd5a8eba0 Mon Sep 17 00:00:00 2001 From: zetanova Date: Sat, 11 Sep 2021 21:34:05 +0200 Subject: [PATCH 14/40] update api spec --- .../CoreAPISpec.ApproveCore.approved.txt | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index 32a68ea2cd0..a859abc0411 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -177,13 +177,15 @@ namespace Akka.Actor protected ActorPath(Akka.Actor.Address address, string name) { } protected ActorPath(Akka.Actor.ActorPath parentPath, string name, long uid) { } public Akka.Actor.Address Address { get; } - public abstract System.Collections.Generic.IReadOnlyList Elements { get; } + public int Depth { get; } + public System.Collections.Generic.IReadOnlyList Elements { get; } public string Name { get; } - public abstract Akka.Actor.ActorPath Parent { get; } - public abstract Akka.Actor.ActorPath Root { get; } + public Akka.Actor.ActorPath Parent { get; } + [Newtonsoft.Json.JsonIgnoreAttribute()] + public Akka.Actor.ActorPath Root { get; } public long Uid { get; } public Akka.Actor.ActorPath Child(string childName) { } - public abstract int CompareTo(Akka.Actor.ActorPath other); + public int CompareTo(Akka.Actor.ActorPath other) { } public bool Equals(Akka.Actor.ActorPath other) { } public override bool Equals(object obj) { } public static string FormatPathElements(System.Collections.Generic.IEnumerable pathElements) { } @@ -200,12 +202,12 @@ namespace Akka.Actor public Akka.Util.ISurrogate ToSurrogate(Akka.Actor.ActorSystem system) { } public static bool TryParse(string path, out Akka.Actor.ActorPath actorPath) { } public static bool TryParseAddress(string path, out Akka.Actor.Address address) { } - public abstract Akka.Actor.ActorPath WithUid(long uid); + public Akka.Actor.ActorPath WithUid(long uid) { } public static Akka.Actor.ActorPath /(Akka.Actor.ActorPath path, string name) { } public static Akka.Actor.ActorPath /(Akka.Actor.ActorPath path, System.Collections.Generic.IEnumerable name) { } public static bool ==(Akka.Actor.ActorPath left, Akka.Actor.ActorPath right) { } public static bool !=(Akka.Actor.ActorPath left, Akka.Actor.ActorPath right) { } - public class Surrogate : Akka.Util.ISurrogate, System.IEquatable, System.IEquatable + public sealed class Surrogate : Akka.Util.ISurrogate, System.IEquatable, System.IEquatable { public Surrogate(string path) { } public string Path { get; } @@ -414,7 +416,7 @@ namespace Akka.Actor public Akka.Actor.Address WithSystem(string system) { } public static bool ==(Akka.Actor.Address left, Akka.Actor.Address right) { } public static bool !=(Akka.Actor.Address left, Akka.Actor.Address right) { } - public class AddressSurrogate : Akka.Util.ISurrogate + public sealed class AddressSurrogate : Akka.Util.ISurrogate { public AddressSurrogate() { } public string Host { get; set; } @@ -497,15 +499,9 @@ namespace Akka.Actor { public static void CancelIfNotNull(this Akka.Actor.ICancelable cancelable) { } } - public class ChildActorPath : Akka.Actor.ActorPath + public sealed class ChildActorPath : Akka.Actor.ActorPath { public ChildActorPath(Akka.Actor.ActorPath parentPath, string name, long uid) { } - public override System.Collections.Generic.IReadOnlyList Elements { get; } - public override Akka.Actor.ActorPath Parent { get; } - public override Akka.Actor.ActorPath Root { get; } - public override int CompareTo(Akka.Actor.ActorPath other) { } - public override int GetHashCode() { } - public override Akka.Actor.ActorPath WithUid(long uid) { } } public sealed class CoordinatedShutdown : Akka.Actor.IExtension { @@ -1544,15 +1540,9 @@ namespace Akka.Actor public void SwapUnderlying(Akka.Actor.ICell cell) { } protected override void TellInternal(object message, Akka.Actor.IActorRef sender) { } } - public class RootActorPath : Akka.Actor.ActorPath + public sealed class RootActorPath : Akka.Actor.ActorPath { public RootActorPath(Akka.Actor.Address address, string name = "") { } - public override System.Collections.Generic.IReadOnlyList Elements { get; } - public override Akka.Actor.ActorPath Parent { get; } - [Newtonsoft.Json.JsonIgnoreAttribute()] - public override Akka.Actor.ActorPath Root { get; } - public override int CompareTo(Akka.Actor.ActorPath other) { } - public override Akka.Actor.ActorPath WithUid(long uid) { } } [Akka.Annotations.InternalApiAttribute()] public class RootGuardianActorRef : Akka.Actor.LocalActorRef From f86dc4bcd8e6c6e73c025dcc25d69afb02d9019a Mon Sep 17 00:00:00 2001 From: zetanova Date: Sat, 11 Sep 2021 22:07:43 +0200 Subject: [PATCH 15/40] test for jumbo actor path name support --- src/core/Akka.Tests/Actor/ActorPathSpec.cs | 17 +++++++++++++++++ src/core/Akka/Actor/ActorPath.cs | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/core/Akka.Tests/Actor/ActorPathSpec.cs b/src/core/Akka.Tests/Actor/ActorPathSpec.cs index 31d51313842..f93c3bfebd2 100644 --- a/src/core/Akka.Tests/Actor/ActorPathSpec.cs +++ b/src/core/Akka.Tests/Actor/ActorPathSpec.cs @@ -111,6 +111,23 @@ public void Return_false_upon_malformed_path() ActorPath.TryParse("akka:/", out _).ShouldBe(false); } + [Fact] + public void Supports_jumbo_actor_name_length() + { + ReadOnlySpan prefix = "akka://sys@host.domain.com:1234/some/ref/".AsSpan(); + Span b = new char[10 * 1024 * 1024]; //10 MB + prefix.CopyTo(b); + b.Slice(prefix.Length).Fill('a'); + var path = b.ToString(); + + ActorPath.TryParse(path, out var actorPath).ShouldBe(true); + actorPath.Name.Length.ShouldBe(b.Length - prefix.Length); + actorPath.Name.All(n => n == 'a').ShouldBe(true); + + var result = actorPath.ToStringWithAddress(); + result.AsSpan().SequenceEqual(b).ShouldBe(true); + } + [Fact] public void Create_correct_ToString() { diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 7c4664c8157..e7328c3cf54 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -590,7 +590,7 @@ private string Join(ReadOnlySpan prefix) Span buffer = prefix.Length < 1024 ? stackalloc char[prefix.Length + 1] : new char[prefix.Length + 1]; prefix.CopyTo(buffer); buffer[buffer.Length - 1] = '/'; - return buffer.ToString(); + return buffer.ToString(); //todo use string.Create() when available } else { @@ -603,7 +603,7 @@ private string Join(ReadOnlySpan prefix) p = p.Parent; } - // Concatenate segments (in reverse order) into buffer with '/' prefixes + // Concatenate segments (in reverse order) into buffer with '/' prefixes Span buffer = totalLength < 1024 ? stackalloc char[totalLength] : new char[totalLength]; prefix.CopyTo(buffer); @@ -618,7 +618,7 @@ private string Join(ReadOnlySpan prefix) name.CopyTo(buffer.Slice(offset + 1, name.Length)); p = p.Parent; } - return buffer.ToString(); + return buffer.ToString(); //todo use string.Create() when available } } From 4427087b8bac0828002c43e7e42d4728c0bc9a2f Mon Sep 17 00:00:00 2001 From: zetanova Date: Sun, 12 Sep 2021 23:23:39 +0200 Subject: [PATCH 16/40] small refactors --- .../Akka.Remote/RemoteActorRefProvider.cs | 12 +++++------ .../Serialization/ActorPathCache.cs | 5 ++--- src/core/Akka/Actor/ActorPath.cs | 3 ++- src/core/Akka/Actor/ActorRefFactoryShared.cs | 3 +-- src/core/Akka/Actor/ActorRefProvider.cs | 4 ++-- src/core/Akka/Actor/ActorSelection.cs | 21 ++++++++----------- src/core/Akka/Actor/Futures.cs | 12 +++++------ 7 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/core/Akka.Remote/RemoteActorRefProvider.cs b/src/core/Akka.Remote/RemoteActorRefProvider.cs index 325adacdbda..32918533503 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -469,6 +469,12 @@ private IInternalActorRef LocalActorOf(ActorSystemImpl system, Props props, IInt /// TBD public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address localAddress) { + if (path is null) + { + _log.Debug("resolve of unknown path [{0}] failed", path); + return InternalDeadLetters; + } + ActorPath actorPath; if (_actorPathThreadLocalCache != null) { @@ -479,12 +485,6 @@ public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address lo ActorPath.TryParse(path, out actorPath); } - if (path is null) - { - _log.Debug("resolve of unknown path [{0}] failed", path); - return InternalDeadLetters; - } - if (!HasAddress(actorPath.Address)) return CreateRemoteRef(new RootActorPath(actorPath.Address) / actorPath.ElementsWithUid, localAddress); diff --git a/src/core/Akka.Remote/Serialization/ActorPathCache.cs b/src/core/Akka.Remote/Serialization/ActorPathCache.cs index 1f292dcb1df..c8c6e82bac8 100644 --- a/src/core/Akka.Remote/Serialization/ActorPathCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorPathCache.cs @@ -50,9 +50,8 @@ protected override int Hash(string k) protected override ActorPath Compute(string k) { - if (ActorPath.TryParse(k, out var actorPath)) - return actorPath; - return null; + ActorPath.TryParse(k, out var actorPath); + return actorPath; } protected override bool IsCacheable(ActorPath v) diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index e7328c3cf54..bedd9e9c7a5 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -58,7 +58,8 @@ public Surrogate(string path) /// The encapsulated by this surrogate. public ISurrogated FromSurrogate(ActorSystem system) { - return TryParse(Path, out var path) ? path : null; + TryParse(Path, out var path); + return path; } #region Equality diff --git a/src/core/Akka/Actor/ActorRefFactoryShared.cs b/src/core/Akka/Actor/ActorRefFactoryShared.cs index 032b263a3dc..067f62586aa 100644 --- a/src/core/Akka/Actor/ActorRefFactoryShared.cs +++ b/src/core/Akka/Actor/ActorRefFactoryShared.cs @@ -64,8 +64,7 @@ public static ActorSelection ActorSelection(string path, ActorSystem system, IAc if(Uri.IsWellFormedUriString(path, UriKind.Absolute)) { - ActorPath actorPath; - if(!ActorPath.TryParse(path, out actorPath)) + if(!ActorPath.TryParse(path, out var actorPath)) return new ActorSelection(provider.DeadLetters, ""); var actorRef = provider.RootGuardianAt(actorPath.Address); diff --git a/src/core/Akka/Actor/ActorRefProvider.cs b/src/core/Akka/Actor/ActorRefProvider.cs index aa6e21adb95..efd4c77ef26 100644 --- a/src/core/Akka/Actor/ActorRefProvider.cs +++ b/src/core/Akka/Actor/ActorRefProvider.cs @@ -417,9 +417,9 @@ public void Init(ActorSystemImpl system) /// TBD public IActorRef ResolveActorRef(string path) { - ActorPath actorPath; - if (ActorPath.TryParse(path, out actorPath) && actorPath.Address == _rootPath.Address) + if (ActorPath.TryParse(path, out var actorPath) && actorPath.Address == _rootPath.Address) return ResolveActorRef(_rootGuardian, actorPath.Elements); + _log.Debug("Resolve of unknown path [{0}] failed. Invalid format.", path); return _deadLetters; } diff --git a/src/core/Akka/Actor/ActorSelection.cs b/src/core/Akka/Actor/ActorSelection.cs index 218b3c2838f..a13dcca96e5 100644 --- a/src/core/Akka/Actor/ActorSelection.cs +++ b/src/core/Akka/Actor/ActorSelection.cs @@ -63,7 +63,7 @@ public ActorSelection(IActorRef anchor, SelectionPathElement[] path) /// The anchor. /// The path. public ActorSelection(IActorRef anchor, string path) - : this(anchor, path == "" ? new string[] { } : path.Split('/')) + : this(anchor, path == "" ? Array.Empty() : path.Split('/')) { } @@ -77,8 +77,8 @@ public ActorSelection(IActorRef anchor, IEnumerable elements) Anchor = anchor; var list = new List(); - var count = elements.Count(); // shouldn't have a multiple enumeration issue\ - var i = 0; + var hasDoubleWildcard = false; + foreach (var s in elements) { switch (s) @@ -86,10 +86,9 @@ public ActorSelection(IActorRef anchor, IEnumerable elements) case null: case "": break; - case "**": - if (i < count-1) - throw new IllegalActorNameException("Double wildcard can only appear at the last path entry"); + case "**": list.Add(SelectChildRecursive.Instance); + hasDoubleWildcard = true; break; case string e when e.Contains("?") || e.Contains("*"): list.Add(new SelectChildPattern(e)); @@ -101,10 +100,11 @@ public ActorSelection(IActorRef anchor, IEnumerable elements) list.Add(new SelectChildName(s)); break; } - - i++; } + if(hasDoubleWildcard && list[list.Count-1] != SelectChildRecursive.Instance) + throw new IllegalActorNameException("Double wildcard can only appear at the last path entry"); + Path = list.ToArray(); } @@ -164,10 +164,7 @@ private async Task InnerResolveOne(TimeSpan timeout, CancellationToke try { var identity = await this.Ask(new Identify(null), timeout, ct).ConfigureAwait(false); - if (identity.Subject == null) - throw new ActorNotFoundException("subject was null"); - - return identity.Subject; + return identity.Subject ?? throw new ActorNotFoundException("subject was null"); } catch (Exception ex) { diff --git a/src/core/Akka/Actor/Futures.cs b/src/core/Akka/Actor/Futures.cs index c49cc8b74e4..fbb84507bfb 100644 --- a/src/core/Akka/Actor/Futures.cs +++ b/src/core/Akka/Actor/Futures.cs @@ -178,14 +178,14 @@ public static Task Ask(this ICanTell self, Func message /// Provider used for Ask pattern implementation internal static IActorRefProvider ResolveProvider(ICanTell self) { - if (self is ActorSelection) - return ResolveProvider(self.AsInstanceOf().Anchor); + if (self is ActorSelection selection) + return ResolveProvider(selection.Anchor); - if (self is IInternalActorRef) - return self.AsInstanceOf().Provider; + if (self is IInternalActorRef actorRef) + return actorRef.Provider; - if (ActorCell.Current != null) - return InternalCurrentActorCellKeeper.Current.SystemImpl.Provider; + if (ActorCell.Current is ActorCell cell) + return cell.SystemImpl.Provider; return null; } From 93f5d9c6a6b360c081916fa64d1d538001678df3 Mon Sep 17 00:00:00 2001 From: zetanova Date: Mon, 13 Sep 2021 00:05:08 +0200 Subject: [PATCH 17/40] add ActorPath.ParentOf(depth) --- .../CoreAPISpec.ApproveCore.approved.txt | 1 + src/core/Akka/Actor/ActorPath.cs | 32 +++++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index a859abc0411..d160abede3d 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -191,6 +191,7 @@ namespace Akka.Actor public static string FormatPathElements(System.Collections.Generic.IEnumerable pathElements) { } public override int GetHashCode() { } public static bool IsValidPathElement(string s) { } + public Akka.Actor.ActorPath ParentOf(int depth) { } public static Akka.Actor.ActorPath Parse(string path) { } public string ToSerializationFormat() { } public string ToSerializationFormatWithAddress(Akka.Actor.Address address) { } diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index bedd9e9c7a5..6931fdce357 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -260,16 +260,7 @@ internal IReadOnlyList ElementsWithUid /// The root actor path. /// [JsonIgnore] - public ActorPath Root - { - get - { - var current = this; - while (current._depth > 0) - current = current.Parent; - return current; - } - } + public ActorPath Root => ParentOf(0); /// public bool Equals(ActorPath other) @@ -376,6 +367,27 @@ public ActorPath WithUid(long uid) return a; } + /// + /// Returns a parent of depth + /// 0: Root, 1: Guardian, -1: Parent, -2: GrandParent + /// + /// The parent depth, negative depth for reverse lookup + public ActorPath ParentOf(int depth) + { + var current = this; + if (depth >= 0) + { + while (current._depth > depth) + current = current.Parent; + } + else + { + for(var i = depth; i < 0 && current.Depth > 0; i++) + current = current.Parent; + } + return current; + } + /// /// Creates an from the specified . /// From d74deaad5b54df6b46c576f82810a0fee95f8edd Mon Sep 17 00:00:00 2001 From: zetanova Date: Mon, 13 Sep 2021 00:20:16 +0200 Subject: [PATCH 18/40] dont copy actorpath --- src/core/Akka.Remote/RemoteActorRefProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Akka.Remote/RemoteActorRefProvider.cs b/src/core/Akka.Remote/RemoteActorRefProvider.cs index 32918533503..f3165c98791 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -486,7 +486,7 @@ public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address lo } if (!HasAddress(actorPath.Address)) - return CreateRemoteRef(new RootActorPath(actorPath.Address) / actorPath.ElementsWithUid, localAddress); + return CreateRemoteRef(actorPath, localAddress); //the actor's local address was already included in the ActorPath From a7a525e48ff391d80d592ff3a4a1a5442f9ae180 Mon Sep 17 00:00:00 2001 From: zetanova Date: Mon, 13 Sep 2021 00:28:42 +0200 Subject: [PATCH 19/40] use actorpath-cache and remove cache entry test --- src/core/Akka.Remote.Tests/RemotingSpec.cs | 54 +++++++++---------- .../Serialization/ActorPathCache.cs | 4 +- .../Serialization/ActorRefResolveCache.cs | 4 +- .../Akka.Remote/Transport/AkkaPduCodec.cs | 6 +-- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/src/core/Akka.Remote.Tests/RemotingSpec.cs b/src/core/Akka.Remote.Tests/RemotingSpec.cs index 7618698411e..876740461ce 100644 --- a/src/core/Akka.Remote.Tests/RemotingSpec.cs +++ b/src/core/Akka.Remote.Tests/RemotingSpec.cs @@ -178,33 +178,33 @@ public async Task Remoting_must_support_Ask() Assert.IsType>(actorRef); } - [Fact] - public async Task Remoting_should_not_cache_ref_of_local_ask() - { - var localActorRefResolveCache = ActorRefResolveThreadLocalCache.For(Sys); - var localActorPathCache = ActorPathThreadLocalCache.For(Sys); - - var (msg, actorRef) = await _here.Ask<(string, IActorRef)>("ping", DefaultTimeout); - Assert.Equal("pong", msg); - Assert.IsType>(actorRef); - - Assert.Equal(0, localActorRefResolveCache.All.Sum(n => n.Stats.Entries)); - Assert.Equal(2, localActorPathCache.All.Sum(n => n.Stats.Entries)); - } - - [Fact] - public async Task Remoting_should_not_cache_ref_of_remote_ask() - { - var remoteActorRefResolveCache = ActorRefResolveThreadLocalCache.For(_remoteSystem); - var remoteActorPathCache = ActorPathThreadLocalCache.For(_remoteSystem); - - var (msg, actorRef) = await _here.Ask<(string, IActorRef)>("ping", DefaultTimeout); - Assert.Equal("pong", msg); - Assert.IsType>(actorRef); - - Assert.Equal(0, remoteActorRefResolveCache.All.Sum(n => n.Stats.Entries)); - Assert.Equal(2, remoteActorPathCache.All.Sum(n => n.Stats.Entries)); //should be 1 - } + //[Fact] + //public async Task Remoting_should_not_cache_ref_of_local_ask() + //{ + // var localActorRefResolveCache = ActorRefResolveThreadLocalCache.For(Sys); + // var localActorPathCache = ActorPathThreadLocalCache.For(Sys); + + // var (msg, actorRef) = await _here.Ask<(string, IActorRef)>("ping", DefaultTimeout); + // Assert.Equal("pong", msg); + // Assert.IsType>(actorRef); + + // Assert.Equal(0, localActorRefResolveCache.All.Sum(n => n.Stats.Entries)); + // Assert.Equal(2, localActorPathCache.All.Sum(n => n.Stats.Entries)); + //} + + //[Fact] + //public async Task Remoting_should_not_cache_ref_of_remote_ask() + //{ + // var remoteActorRefResolveCache = ActorRefResolveThreadLocalCache.For(_remoteSystem); + // var remoteActorPathCache = ActorPathThreadLocalCache.For(_remoteSystem); + + // var (msg, actorRef) = await _here.Ask<(string, IActorRef)>("ping", DefaultTimeout); + // Assert.Equal("pong", msg); + // Assert.IsType>(actorRef); + + // Assert.Equal(0, remoteActorRefResolveCache.All.Sum(n => n.Stats.Entries)); + // Assert.Equal(2, remoteActorPathCache.All.Sum(n => n.Stats.Entries)); //should be 1 + //} [Fact(Skip = "Racy")] public async Task Ask_does_not_deadlock() diff --git a/src/core/Akka.Remote/Serialization/ActorPathCache.cs b/src/core/Akka.Remote/Serialization/ActorPathCache.cs index c8c6e82bac8..e3454e1065a 100644 --- a/src/core/Akka.Remote/Serialization/ActorPathCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorPathCache.cs @@ -17,12 +17,10 @@ namespace Akka.Remote.Serialization /// internal sealed class ActorPathThreadLocalCache : ExtensionIdProvider, IExtension { - private readonly ThreadLocal _current = new ThreadLocal(() => new ActorPathCache(), true); + private readonly ThreadLocal _current = new ThreadLocal(() => new ActorPathCache(), false); public ActorPathCache Cache => _current.Value; - internal IList All => _current.Values; - public override ActorPathThreadLocalCache CreateExtension(ExtendedActorSystem system) { return new ActorPathThreadLocalCache(); diff --git a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs index 4b650b6e894..e0bebe1de28 100644 --- a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs @@ -24,7 +24,7 @@ public ActorRefResolveThreadLocalCache() { } public ActorRefResolveThreadLocalCache(IRemoteActorRefProvider provider) { _provider = provider; - _current = new ThreadLocal(() => new ActorRefResolveCache(_provider), true); + _current = new ThreadLocal(() => new ActorRefResolveCache(_provider), false); } public override ActorRefResolveThreadLocalCache CreateExtension(ExtendedActorSystem system) @@ -36,8 +36,6 @@ public override ActorRefResolveThreadLocalCache CreateExtension(ExtendedActorSys public ActorRefResolveCache Cache => _current.Value; - internal IList All => _current.Values; - public static ActorRefResolveThreadLocalCache For(ActorSystem system) { return system.WithExtension(); diff --git a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs index 8e257d47a55..335483e5ffb 100644 --- a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs +++ b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs @@ -202,12 +202,12 @@ public AckAndMessage(Ack ackOption, Message messageOption) internal abstract class AkkaPduCodec { protected readonly ActorSystem System; - protected readonly AddressThreadLocalCache ActorPathCache; + protected readonly ActorPathThreadLocalCache ActorPathCache; protected AkkaPduCodec(ActorSystem system) { System = system; - ActorPathCache = AddressThreadLocalCache.For(system); + ActorPathCache = ActorPathThreadLocalCache.For(system); } /// @@ -428,7 +428,7 @@ public override AckAndMessage DecodeMessage(ByteString raw, IRemoteActorRefProvi var recipient = provider.ResolveActorRefWithLocalAddress(envelopeContainer.Recipient.Path, localAddress); //todo get parsed address from provider - var recipientAddress = ActorPathCache.Cache.GetOrCompute(envelopeContainer.Recipient.Path); + var recipientAddress = ActorPathCache.Cache.GetOrCompute(envelopeContainer.Recipient.Path).Address; var serializedMessage = envelopeContainer.Message; IActorRef senderOption = null; From d3c33e86d754e4ac38c5bb63b65f3641722a19c7 Mon Sep 17 00:00:00 2001 From: zetanova Date: Mon, 13 Sep 2021 01:04:17 +0200 Subject: [PATCH 20/40] refactor fill array --- src/core/Akka.Remote/Serialization/LruBoundedCache.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs index e2a99fde2f3..95f34a486a0 100644 --- a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs +++ b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs @@ -132,7 +132,8 @@ protected LruBoundedCache(int capacity, int evictAgeThreshold) _keys = new TKey[Capacity]; _values = new TValue[Capacity]; _hashes = new int[Capacity]; - _epochs = Enumerable.Repeat(_epoch - evictAgeThreshold, Capacity).ToArray(); + _epochs = new int[Capacity]; + _epochs.AsSpan().Fill(_epoch - evictAgeThreshold); } public int Capacity { get; private set; } From 4512aec1c6a59dd7be4edfe3ed7c55bf3abcc619 Mon Sep 17 00:00:00 2001 From: zetanova Date: Mon, 13 Sep 2021 01:38:55 +0200 Subject: [PATCH 21/40] prepair actor path cache for better deduplication --- .../Serialization/ActorPathCache.cs | 11 +++- .../Serialization/LruBoundedCache.cs | 20 ++++--- src/core/Akka/Actor/ActorPath.cs | 52 +++++++++++-------- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/core/Akka.Remote/Serialization/ActorPathCache.cs b/src/core/Akka.Remote/Serialization/ActorPathCache.cs index e3454e1065a..d2bef0d7594 100644 --- a/src/core/Akka.Remote/Serialization/ActorPathCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorPathCache.cs @@ -48,7 +48,16 @@ protected override int Hash(string k) protected override ActorPath Compute(string k) { - ActorPath.TryParse(k, out var actorPath); + //todo lookup in address cache + + if (!ActorPath.TryParseAddress(k, out var address, out var absoluteUri)) + return null; + + //todo lookup in root in cache + + if (!ActorPath.TryParse(new RootActorPath(address), absoluteUri, out var actorPath)) + return null; + return actorPath; } diff --git a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs index 95f34a486a0..20819f85937 100644 --- a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs +++ b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs @@ -197,6 +197,12 @@ public TValue Get(TKey k) } public TValue GetOrCompute(TKey k) + { + TryGetOrCompute(k, out var value); + return value; + } + + public bool TryGetOrCompute(TKey k, out TValue value) { var h = Hash(k); unchecked { _epoch += 1; } @@ -208,7 +214,7 @@ public TValue GetOrCompute(TKey k) { if (_values[position] == null) { - var value = Compute(k); + value = Compute(k); if (IsCacheable(value)) { _keys[position] = k; @@ -216,7 +222,7 @@ public TValue GetOrCompute(TKey k) _hashes[position] = h; _epochs[position] = _epoch; } - return value; + return false; } else { @@ -225,15 +231,17 @@ public TValue GetOrCompute(TKey k) // the table since because of the Robin-Hood property we would have swapped it with the current element. if (probeDistance > otherProbeDistance) { - var value = Compute(k); - if (IsCacheable(value)) Move(position, k, h, value, _epoch, probeDistance); - return value; + value = Compute(k); + if (IsCacheable(value)) + Move(position, k, h, value, _epoch, probeDistance); + return false; } else if (_hashes[position] == h && k.Equals(_keys[position])) { // Update usage _epochs[position] = _epoch; - return _values[position]; + value = _values[position]; + return false; } else { diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 6931fdce357..aa7df64ca8c 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -379,10 +379,10 @@ public ActorPath ParentOf(int depth) { while (current._depth > depth) current = current.Parent; - } + } else { - for(var i = depth; i < 0 && current.Depth > 0; i++) + for (var i = depth; i < 0 && current.Depth > 0; i++) current = current.Parent; } return current; @@ -398,10 +398,9 @@ public ActorPath ParentOf(int depth) /// A newly created public static ActorPath Parse(string path) { - if (!TryParse(path, out var actorPath)) - throw new UriFormatException($"Can not parse an ActorPath: {path}"); - - return actorPath; + return TryParse(path, out var actorPath) + ? actorPath + : throw new UriFormatException($"Can not parse an ActorPath: {path}"); } /// @@ -413,45 +412,56 @@ public static ActorPath Parse(string path) /// TBD public static bool TryParse(string path, out ActorPath actorPath) { - //todo lookup address and/or root in cache - - if (!TryParseAddress(path, out var address, out var spanified)) + if (!TryParseAddress(path, out var address, out var absoluteUri)) { actorPath = null; return false; } + return TryParse(new RootActorPath(address), absoluteUri, out actorPath); + } + + /// + /// Tries to parse the uri, which should be a uri not containing protocol. + /// For example "/user/my-actor" + /// + /// the base path, normaly a root path + /// TBD + /// TBD + /// TBD + public static bool TryParse(ActorPath basePath, ReadOnlySpan absoluteUri, out ActorPath actorPath) + { + actorPath = basePath; + // check for Uri fragment here int nextSlash; - actorPath = new RootActorPath(address); - do { - nextSlash = spanified.IndexOf('/'); + nextSlash = absoluteUri.IndexOf('/'); if (nextSlash > 0) { - var name = spanified.Slice(0, nextSlash).ToString(); + var name = absoluteUri.Slice(0, nextSlash).ToString(); actorPath = new ChildActorPath(actorPath, name, ActorCell.UndefinedUid); } - else if (nextSlash < 0 && spanified.Length > 0) // final segment + else if (nextSlash < 0 && absoluteUri.Length > 0) // final segment { - var fragLoc = spanified.IndexOf('#'); + var fragLoc = absoluteUri.IndexOf('#'); if (fragLoc > -1) { - var fragment = spanified.Slice(fragLoc + 1); + var fragment = absoluteUri.Slice(fragLoc + 1); var fragValue = SpanHacks.Parse(fragment); - spanified = spanified.Slice(0, fragLoc); - actorPath = new ChildActorPath(actorPath, spanified.ToString(), fragValue); + absoluteUri = absoluteUri.Slice(0, fragLoc); + actorPath = new ChildActorPath(actorPath, absoluteUri.ToString(), fragValue); } else { - actorPath = new ChildActorPath(actorPath, spanified.ToString(), ActorCell.UndefinedUid); + actorPath = new ChildActorPath(actorPath, absoluteUri.ToString(), ActorCell.UndefinedUid); } } - spanified = spanified.Slice(nextSlash + 1); + absoluteUri = absoluteUri.Slice(nextSlash + 1); } while (nextSlash >= 0); @@ -476,7 +486,7 @@ public static bool TryParseAddress(string path, out Address address) /// If true, the parsed . Otherwise null. /// A containing the path following the address. /// true if the could be parsed, false otherwise. - private static bool TryParseAddress(string path, out Address address, out ReadOnlySpan absoluteUri) + public static bool TryParseAddress(string path, out Address address, out ReadOnlySpan absoluteUri) { address = null; From ad3ca76ba02892739f617904baca5748d2f66467 Mon Sep 17 00:00:00 2001 From: zetanova Date: Mon, 13 Sep 2021 01:41:42 +0200 Subject: [PATCH 22/40] update api --- src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index d160abede3d..a0b916f0397 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -202,7 +202,9 @@ namespace Akka.Actor public string ToStringWithoutAddress() { } public Akka.Util.ISurrogate ToSurrogate(Akka.Actor.ActorSystem system) { } public static bool TryParse(string path, out Akka.Actor.ActorPath actorPath) { } + public static bool TryParse(Akka.Actor.ActorPath basePath, System.ReadOnlySpan absoluteUri, out Akka.Actor.ActorPath actorPath) { } public static bool TryParseAddress(string path, out Akka.Actor.Address address) { } + public static bool TryParseAddress(string path, out Akka.Actor.Address address, out System.ReadOnlySpan absoluteUri) { } public Akka.Actor.ActorPath WithUid(long uid) { } public static Akka.Actor.ActorPath /(Akka.Actor.ActorPath path, string name) { } public static Akka.Actor.ActorPath /(Akka.Actor.ActorPath path, System.Collections.Generic.IEnumerable name) { } From 026a6fc6ae50dd57552281eb2da9a726f4404d54 Mon Sep 17 00:00:00 2001 From: zetanova Date: Wed, 15 Sep 2021 16:33:57 +0200 Subject: [PATCH 23/40] cache root actor path --- .../Serialization/LruBoundedCacheSpec.cs | 73 +++++++-- .../Serialization/ActorPathCache.cs | 54 +++++-- .../Serialization/ActorRefResolveCache.cs | 8 +- .../Akka.Remote/Serialization/AddressCache.cs | 8 +- .../Serialization/LruBoundedCache.cs | 144 ++++++++++++++++-- src/core/Akka/Actor/ActorPath.cs | 131 +++++----------- src/core/Akka/Actor/Address.cs | 95 +++++++++++- 7 files changed, 367 insertions(+), 146 deletions(-) diff --git a/src/core/Akka.Remote.Tests/Serialization/LruBoundedCacheSpec.cs b/src/core/Akka.Remote.Tests/Serialization/LruBoundedCacheSpec.cs index 9dc9eca751a..6b17e6bd9e1 100644 --- a/src/core/Akka.Remote.Tests/Serialization/LruBoundedCacheSpec.cs +++ b/src/core/Akka.Remote.Tests/Serialization/LruBoundedCacheSpec.cs @@ -5,6 +5,8 @@ // //----------------------------------------------------------------------- +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using Akka.Remote.Serialization; @@ -14,22 +16,55 @@ namespace Akka.Remote.Tests.Serialization { + sealed class FastHashTestComparer : IEqualityComparer + { + private readonly string _hashSeed; + + public FastHashTestComparer(string hashSeed = "") + { + _hashSeed = hashSeed; + } + + public bool Equals(string x, string y) + { + return StringComparer.Ordinal.Equals(x, y); + } + + public int GetHashCode(string k) + { + return FastHash.OfStringFast(_hashSeed != string.Empty + ? _hashSeed + k + _hashSeed : k); + } + } + + sealed class BrokenTestComparer : IEqualityComparer + { + public bool Equals(string x, string y) + { + return StringComparer.Ordinal.Equals(x, y); + } + + public int GetHashCode(string k) + { + return 0; + } + } + public class LruBoundedCacheSpec { private class TestCache : LruBoundedCache { - public TestCache(int capacity, int evictAgeThreshold, string hashSeed = "") : base(capacity, evictAgeThreshold) + public TestCache(int capacity, int evictAgeThreshold, IEqualityComparer comparer) + : base(capacity, evictAgeThreshold, comparer) { - _hashSeed = hashSeed; } - private readonly string _hashSeed; - private int _cntr = 0; - - protected override int Hash(string k) + public TestCache(int capacity, int evictAgeThreshold, string hashSeed = "") + : base(capacity, evictAgeThreshold, new FastHashTestComparer(hashSeed)) { - return FastHash.OfStringFast(_hashSeed + k + _hashSeed); } + private int _cntr = 0; + protected override string Compute(string k) { var id = _cntr; @@ -71,20 +106,17 @@ public void ExpectComputedOnly(string key, string value) private sealed class BrokenHashFunctionTestCache : TestCache { - public BrokenHashFunctionTestCache(int capacity, int evictAgeThreshold, string hashSeed = "") : base(capacity, evictAgeThreshold, hashSeed) + public BrokenHashFunctionTestCache(int capacity, int evictAgeThreshold) : + base(capacity, evictAgeThreshold, new BrokenTestComparer()) { } - protected override int Hash(string k) - { - return 0; - } } [Fact] public void LruBoundedCache_must_work_in_the_happy_case() { - var cache = new TestCache(4,4); + var cache = new TestCache(4, 4); cache.ExpectComputed("A", "A:0"); cache.ExpectComputed("B", "B:1"); @@ -97,6 +129,19 @@ public void LruBoundedCache_must_work_in_the_happy_case() cache.ExpectCached("D", "D:3"); } + [Fact] + public void LruBoundedCache_must_handle_explict_set() + { + var cache = new TestCache(4, 4); + + cache.ExpectComputed("A", "A:0"); + cache.TrySet("A", "A:1").Should().Be(true); + cache.Get("A").Should().Be("A:1"); + + cache.TrySet("B", "B:X").Should().Be(true); + cache.Get("B").Should().Be("B:X"); + } + [Fact] public void LruBoundedCache_must_evict_oldest_when_full() { @@ -237,6 +282,8 @@ public void LruBoundedCache_must_not_cache_noncacheable_values() cache.ExpectCached("C", "C:6"); cache.ExpectCached("D", "D:7"); cache.ExpectCached("E", "E:8"); + + cache.TrySet("#X", "#X:13").Should().BeFalse(); } [Fact] diff --git a/src/core/Akka.Remote/Serialization/ActorPathCache.cs b/src/core/Akka.Remote/Serialization/ActorPathCache.cs index d2bef0d7594..249ce9a3d9a 100644 --- a/src/core/Akka.Remote/Serialization/ActorPathCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorPathCache.cs @@ -37,28 +37,62 @@ public static ActorPathThreadLocalCache For(ActorSystem system) /// internal sealed class ActorPathCache : LruBoundedCache { - public ActorPathCache(int capacity = 1024, int evictAgeThreshold = 600) : base(capacity, evictAgeThreshold) + public ActorPathCache(int capacity = 1024, int evictAgeThreshold = 600) + : base(capacity, evictAgeThreshold, FastHashComparer.Default) { } - protected override int Hash(string k) - { - return FastHash.OfStringFast(k); - } - protected override ActorPath Compute(string k) { - //todo lookup in address cache + ActorPath actorPath; + + var path = k.AsSpan(); - if (!ActorPath.TryParseAddress(k, out var address, out var absoluteUri)) + if (!ActorPath.TryParseParts(path, out var addressSpan, out var absoluteUri)) return null; - //todo lookup in root in cache - if (!ActorPath.TryParse(new RootActorPath(address), absoluteUri, out var actorPath)) + string rootPath; + if(absoluteUri.Length > 1 || path.Length > addressSpan.Length) + { + //path end with / + rootPath = path.Slice(0, addressSpan.Length + 1).ToString(); + } + else + { + //todo replace with string.create + Span buffer = addressSpan.Length < 1024 + ? stackalloc char[addressSpan.Length + 1] + : new char[addressSpan.Length + 1]; + path.Slice(0, addressSpan.Length).CopyTo(buffer); + buffer[buffer.Length - 1] = '/'; + rootPath = buffer.ToString(); + } + + //try lookup root in cache + if (!TryGet(rootPath, out actorPath)) + { + if (!Address.TryParse(addressSpan, out var address)) + return null; + + actorPath = new RootActorPath(address); + TrySet(rootPath, actorPath); + } + + if (!ActorPath.TryParse(actorPath, absoluteUri, out actorPath)) return null; return actorPath; + + + } + + private static ActorPath ComputeRootPath(string path) + { + if (!Address.TryParse(path.AsSpan(), out var address)) + return null; + + return new RootActorPath(address); } protected override bool IsCacheable(ActorPath v) diff --git a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs index e0bebe1de28..9e822b3a1fd 100644 --- a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs @@ -49,7 +49,8 @@ internal sealed class ActorRefResolveCache : LruBoundedCache { private readonly IRemoteActorRefProvider _provider; - public ActorRefResolveCache(IRemoteActorRefProvider provider, int capacity = 1024, int evictAgeThreshold = 600) : base(capacity, evictAgeThreshold) + public ActorRefResolveCache(IRemoteActorRefProvider provider, int capacity = 1024, int evictAgeThreshold = 600) + : base(capacity, evictAgeThreshold, FastHashComparer.Default) { _provider = provider; } @@ -59,11 +60,6 @@ protected override IActorRef Compute(string k) return _provider.InternalResolveActorRef(k); } - protected override int Hash(string k) - { - return FastHash.OfStringFast(k); - } - protected override bool IsCacheable(IActorRef v) { // don't cache any FutureActorRefs, et al diff --git a/src/core/Akka.Remote/Serialization/AddressCache.cs b/src/core/Akka.Remote/Serialization/AddressCache.cs index cfacfe58b32..a80e4ce49eb 100644 --- a/src/core/Akka.Remote/Serialization/AddressCache.cs +++ b/src/core/Akka.Remote/Serialization/AddressCache.cs @@ -41,15 +41,11 @@ public static AddressThreadLocalCache For(ActorSystem system) /// internal sealed class AddressCache : LruBoundedCache { - public AddressCache(int capacity = 1024, int evictAgeThreshold = 600) : base(capacity, evictAgeThreshold) + public AddressCache(int capacity = 1024, int evictAgeThreshold = 600) + : base(capacity, evictAgeThreshold, FastHashComparer.Default) { } - protected override int Hash(string k) - { - return FastHash.OfStringFast(k); - } - protected override Address Compute(string k) { Address addr; diff --git a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs index 20819f85937..74177bb2672 100644 --- a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs +++ b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Collections.Generic; using System.Linq; namespace Akka.Remote.Serialization @@ -25,14 +26,24 @@ internal static class FastHash /// A 32-bit pseudo-random hash value. public static int OfString(string s) { - var chars = s.AsSpan(); + return OfString(s.AsSpan()); + } + + /// + /// Allocatey, but safe implementation of FastHash + /// + /// The input string. + /// A 32-bit pseudo-random hash value. + public static int OfString(ReadOnlySpan s) + { + var len = s.Length; var s0 = 391408L; // seed value 1, DON'T CHANGE var s1 = 601258L; // seed value 2, DON'T CHANGE unchecked { - for(var i = 0; i < chars.Length;i++) + for (var i = 0; i < len; i++) { - var x = s0 ^ chars[i]; // Mix character into PRNG state + var x = s0 ^ s[i]; // Mix character into PRNG state var y = s1; // Xorshift128+ round @@ -82,6 +93,36 @@ public static int OfStringFast(string s) } } } + + + } + + /// + /// INTERNAL API + /// + internal sealed class FastHashComparer : IEqualityComparer + { + public readonly static FastHashComparer Default = new FastHashComparer(); + + public bool Equals(string x, string y) + { + return StringComparer.Ordinal.Equals(x, y); + } + + public bool Equals(ReadOnlySpan x, ReadOnlySpan y) + { + return x.SequenceEqual(y); + } + + public int GetHashCode(string s) + { + return FastHash.OfStringFast(s); + } + + public int GetHashCode(ReadOnlySpan s) + { + return FastHash.OfString(s); + } } /// @@ -103,6 +144,8 @@ public CacheStatistics(int entries, int maxProbeDistance, double averageProbeDis public double AverageProbeDistance { get; } } + + /// /// INTERNAL API /// @@ -117,7 +160,7 @@ public CacheStatistics(int entries, int maxProbeDistance, double averageProbeDis /// The type of value used in the cache. internal abstract class LruBoundedCache where TValue : class { - protected LruBoundedCache(int capacity, int evictAgeThreshold) + protected LruBoundedCache(int capacity, int evictAgeThreshold, IEqualityComparer keyComparer) { if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be larger than zero."); @@ -128,6 +171,7 @@ protected LruBoundedCache(int capacity, int evictAgeThreshold) Capacity = capacity; EvictAgeThreshold = evictAgeThreshold; + _keyComparer = keyComparer; _mask = Capacity - 1; _keys = new TKey[Capacity]; _values = new TValue[Capacity]; @@ -145,6 +189,7 @@ protected LruBoundedCache(int capacity, int evictAgeThreshold) // Practically guarantee an overflow private int _epoch = int.MaxValue - 1; + private readonly IEqualityComparer _keyComparer; private readonly TKey[] _keys; private readonly TValue[] _values; private readonly int[] _hashes; @@ -175,7 +220,7 @@ public CacheStatistics Stats public TValue Get(TKey k) { - var h = Hash(k); + var h = _keyComparer.GetHashCode(k); var position = h & _mask; var probeDistance = 0; @@ -187,12 +232,37 @@ public TValue Get(TKey k) return null; if (probeDistance > otherProbeDistance) return null; - if (_hashes[position] == h && k.Equals(_keys[position])) + if (_hashes[position] == h && _keyComparer.Equals(k, _keys[position])) { return _values[position]; } position = (position + 1) & _mask; - probeDistance = probeDistance + 1; + probeDistance++; + } + } + + public bool TryGet(TKey k, out TValue value) + { + var h = _keyComparer.GetHashCode(k); + + var position = h & _mask; + var probeDistance = 0; + + while (true) + { + var otherProbeDistance = ProbeDistanceOf(position); + if (_values[position] == null || probeDistance > otherProbeDistance) + { + value = default; + return false; + } + if (_hashes[position] == h && _keyComparer.Equals(k, _keys[position])) + { + value = _values[position]; + return true; + } + position = (position + 1) & _mask; + probeDistance++; } } @@ -204,7 +274,7 @@ public TValue GetOrCompute(TKey k) public bool TryGetOrCompute(TKey k, out TValue value) { - var h = Hash(k); + var h = _keyComparer.GetHashCode(k); unchecked { _epoch += 1; } var position = h & _mask; @@ -232,22 +302,69 @@ public bool TryGetOrCompute(TKey k, out TValue value) if (probeDistance > otherProbeDistance) { value = Compute(k); - if (IsCacheable(value)) + if (IsCacheable(value)) Move(position, k, h, value, _epoch, probeDistance); return false; } - else if (_hashes[position] == h && k.Equals(_keys[position])) + else if (_hashes[position] == h && _keyComparer.Equals(k, _keys[position])) { // Update usage _epochs[position] = _epoch; value = _values[position]; - return false; + return true; + } + else + { + // This is not our slot yet + position = (position + 1) & _mask; + probeDistance++; + } + } + } + } + + public bool TrySet(TKey key, TValue value) + { + if (!IsCacheable(value)) return false; + + var h = _keyComparer.GetHashCode(key); + unchecked { _epoch += 1; } + + var position = h & _mask; + var probeDistance = 0; + + while (true) + { + if (_values[position] == null) + { + _keys[position] = key; + _values[position] = value; + _hashes[position] = h; + _epochs[position] = _epoch; + return true; + } + else + { + var otherProbeDistance = ProbeDistanceOf(position); + // If probe distance of the element we try to get is larger than the current slot's, then the element cannot be in + // the table since because of the Robin-Hood property we would have swapped it with the current element. + if (probeDistance > otherProbeDistance) + { + Move(position, key, h, value, _epoch, probeDistance); + return true; + } + else if (_hashes[position] == h && _keyComparer.Equals(key, _keys[position])) + { + // Update usage + _epochs[position] = _epoch; + _values[position] = value; + return true; } else { // This is not our slot yet position = (position + 1) & _mask; - probeDistance = probeDistance + 1; + probeDistance++; } } } @@ -342,9 +459,6 @@ protected int ProbeDistanceOf(int idealSlot, int actualSlot) return ((actualSlot - idealSlot) + Capacity) & _mask; } - - protected abstract int Hash(TKey k); - protected abstract TValue Compute(TKey k); protected abstract bool IsCacheable(TValue v); diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index aa7df64ca8c..bc66c2ed3e1 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -369,7 +369,7 @@ public ActorPath WithUid(long uid) /// /// Returns a parent of depth - /// 0: Root, 1: Guardian, -1: Parent, -2: GrandParent + /// 0: Root, 1: Guardian, ..., -1: Parent, -2: GrandParent /// /// The parent depth, negative depth for reverse lookup public ActorPath ParentOf(int depth) @@ -488,116 +488,57 @@ public static bool TryParseAddress(string path, out Address address) /// true if the could be parsed, false otherwise. public static bool TryParseAddress(string path, out Address address, out ReadOnlySpan absoluteUri) { - address = null; + address = default; - var spanified = path.AsSpan(); - absoluteUri = spanified; - - var firstColonPos = spanified.IndexOf(':'); - - if (firstColonPos == -1) // not an absolute Uri - return false; - - var fullScheme = SpanHacks.ToLowerInvariant(spanified.Slice(0, firstColonPos)); - if (!fullScheme.StartsWith("akka")) + if (!TryParseParts(path.AsSpan(), out var addressSpan, out absoluteUri)) return false; - spanified = spanified.Slice(firstColonPos + 1); - if (spanified.Length < 2 || !(spanified[0] == '/' && spanified[1] == '/')) + if (!Address.TryParse(addressSpan, out address)) return false; - spanified = spanified.Slice(2); // move past the double // - var firstAtPos = spanified.IndexOf('@'); - string sysName; - - if (firstAtPos == -1) - { - // dealing with an absolute local Uri - var nextSlash = spanified.IndexOf('/'); - - if (nextSlash == -1) - { - sysName = spanified.ToString(); - absoluteUri = "/".AsSpan(); // RELY ON THE JIT - } - else - { - sysName = spanified.Slice(0, nextSlash).ToString(); - absoluteUri = spanified.Slice(nextSlash); - } - - address = new Address(fullScheme, sysName); - return true; - } + return true; + } - // dealing with a remote Uri - sysName = spanified.Slice(0, firstAtPos).ToString(); - spanified = spanified.Slice(firstAtPos + 1); - - /* - * Need to check for: - * - IPV4 / hostnames - * - IPV6 (must be surrounded by '[]') according to spec. - */ - string host; - - // check for IPV6 first - var openBracket = spanified.IndexOf('['); - var closeBracket = spanified.IndexOf(']'); - if (openBracket > -1 && closeBracket > openBracket) + /// + /// Attempts to parse an from a stringified . + /// + /// The string representation of the . + /// A containing the address part. + /// A containing the path following the address. + /// true if the path parts could be parsed, false otherwise. + public static bool TryParseParts(ReadOnlySpan path, out ReadOnlySpan address, out ReadOnlySpan absoluteUri) + { + var firstAtPos = path.IndexOf(':'); + if (firstAtPos < 4 || 255 < firstAtPos) { - // found an IPV6 address - host = spanified.Slice(openBracket, closeBracket - openBracket + 1).ToString(); - spanified = spanified.Slice(closeBracket + 1); // advance past the address - - // need to check for trailing colon - var secondColonPos = spanified.IndexOf(':'); - if (secondColonPos == -1) - return false; - - spanified = spanified.Slice(secondColonPos + 1); + //missing or invalid scheme + address = default; + absoluteUri = path; + return false; } - else + + var doubleSlash = path.Slice(firstAtPos + 1); + if (doubleSlash.Length < 2 || !(doubleSlash[0] == '/' && doubleSlash[1] == '/')) { - var secondColonPos = spanified.IndexOf(':'); - if (secondColonPos == -1) - return false; - - host = spanified.Slice(0, secondColonPos).ToString(); - - // move past the host - spanified = spanified.Slice(secondColonPos + 1); + //missing double slash + address = default; + absoluteUri = path; + return false; } - var actorPathSlash = spanified.IndexOf('/'); - ReadOnlySpan strPort; - if (actorPathSlash == -1) + var nextSlash = path.Slice(firstAtPos + 3).IndexOf('/'); + if (nextSlash == -1) { - strPort = spanified; + address = path; + absoluteUri = "/".AsSpan(); // RELY ON THE JIT } else { - strPort = spanified.Slice(0, actorPathSlash); + address = path.Slice(0, firstAtPos + 3 + nextSlash); + absoluteUri = path.Slice(address.Length); } - - if (SpanHacks.TryParse(strPort, out var port)) - { - address = new Address(fullScheme, sysName, host, port); - - // need to compute the absolute path after the Address - if (actorPathSlash == -1) - { - absoluteUri = "/".AsSpan(); - } - else - { - absoluteUri = spanified.Slice(actorPathSlash); - } - - return true; - } - - return false; + + return true; } diff --git a/src/core/Akka/Actor/Address.cs b/src/core/Akka/Actor/Address.cs index 8b36bd5f38e..6ac1020371c 100644 --- a/src/core/Akka/Actor/Address.cs +++ b/src/core/Akka/Actor/Address.cs @@ -246,7 +246,7 @@ public Address WithPort(int? port = null) /// true if both addresses are equal; otherwise false public static bool operator ==(Address left, Address right) { - return left?.Equals(right) ?? ReferenceEquals(right, null); + return left?.Equals(right) ?? right is null; } /// @@ -299,6 +299,99 @@ public static Address Parse(string address) } } + /// + /// Parses a new from a given string + /// + /// The span of address to parse + /// If true, the parsed . Otherwise null. + /// true if the could be parsed, false otherwise. + public static bool TryParse(ReadOnlySpan span, out Address address) + { + address = default; + + var firstColonPos = span.IndexOf(':'); + + if (firstColonPos == -1) // not an absolute Uri + return false; + + if (firstColonPos < 4 || 255 < firstColonPos) + { + //invalid scheme length + return false; + } + + Span fullScheme = stackalloc char[firstColonPos]; + span.Slice(0, firstColonPos).ToLowerInvariant(fullScheme); + if (!fullScheme.StartsWith("akka".AsSpan())) + { + //invalid scheme + return false; + } + + span = span.Slice(firstColonPos + 1); + if (span.Length < 2 || !(span[0] == '/' && span[1] == '/')) + return false; + + span = span.Slice(2); // move past the double // + var firstAtPos = span.IndexOf('@'); + string sysName; + + if (firstAtPos == -1) + { + // dealing with an absolute local Uri + sysName = span.ToString(); + address = new Address(fullScheme.ToString(), sysName); + return true; + } + + // dealing with a remote Uri + sysName = span.Slice(0, firstAtPos).ToString(); + span = span.Slice(firstAtPos + 1); + + /* + * Need to check for: + * - IPV4 / hostnames + * - IPV6 (must be surrounded by '[]') according to spec. + */ + string host; + + // check for IPV6 first + var openBracket = span.IndexOf('['); + var closeBracket = span.IndexOf(']'); + if (openBracket > -1 && closeBracket > openBracket) + { + // found an IPV6 address + host = span.Slice(openBracket, closeBracket - openBracket + 1).ToString(); + span = span.Slice(closeBracket + 1); // advance past the address + + // need to check for trailing colon + var secondColonPos = span.IndexOf(':'); + if (secondColonPos == -1) + return false; + + span = span.Slice(secondColonPos + 1); + } + else + { + var secondColonPos = span.IndexOf(':'); + if (secondColonPos == -1) + return false; + + host = span.Slice(0, secondColonPos).ToString(); + + // move past the host + span = span.Slice(secondColonPos + 1); + } + + if (SpanHacks.TryParse(span, out var port) && port >= 0) + { + address = new Address(fullScheme.ToString(), sysName, host, port); + return true; + } + + return false; + } + /// /// This class represents a surrogate of an . /// Its main use is to help during the serialization process. From b29e2426db90349770591ccd1e640e2a2cc61337 Mon Sep 17 00:00:00 2001 From: zetanova Date: Wed, 15 Sep 2021 16:35:43 +0200 Subject: [PATCH 24/40] update api --- src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index a0b916f0397..b0d83289f84 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -205,6 +205,7 @@ namespace Akka.Actor public static bool TryParse(Akka.Actor.ActorPath basePath, System.ReadOnlySpan absoluteUri, out Akka.Actor.ActorPath actorPath) { } public static bool TryParseAddress(string path, out Akka.Actor.Address address) { } public static bool TryParseAddress(string path, out Akka.Actor.Address address, out System.ReadOnlySpan absoluteUri) { } + public static bool TryParseParts(System.ReadOnlySpan path, out System.ReadOnlySpan address, out System.ReadOnlySpan absoluteUri) { } public Akka.Actor.ActorPath WithUid(long uid) { } public static Akka.Actor.ActorPath /(Akka.Actor.ActorPath path, string name) { } public static Akka.Actor.ActorPath /(Akka.Actor.ActorPath path, System.Collections.Generic.IEnumerable name) { } @@ -413,6 +414,7 @@ namespace Akka.Actor public static Akka.Actor.Address Parse(string address) { } public override string ToString() { } public Akka.Util.ISurrogate ToSurrogate(Akka.Actor.ActorSystem system) { } + public static bool TryParse(System.ReadOnlySpan span, out Akka.Actor.Address address) { } public Akka.Actor.Address WithHost(string host = null) { } public Akka.Actor.Address WithPort(System.Nullable port = null) { } public Akka.Actor.Address WithProtocol(string protocol) { } From 9d9a2d7685e643e7765ee7e362c8488107065228 Mon Sep 17 00:00:00 2001 From: zetanova Date: Wed, 15 Sep 2021 16:42:11 +0200 Subject: [PATCH 25/40] remove obsolete code --- src/core/Akka.Remote/Serialization/ActorPathCache.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/core/Akka.Remote/Serialization/ActorPathCache.cs b/src/core/Akka.Remote/Serialization/ActorPathCache.cs index 249ce9a3d9a..482d349eab4 100644 --- a/src/core/Akka.Remote/Serialization/ActorPathCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorPathCache.cs @@ -82,17 +82,7 @@ protected override ActorPath Compute(string k) if (!ActorPath.TryParse(actorPath, absoluteUri, out actorPath)) return null; - return actorPath; - - - } - - private static ActorPath ComputeRootPath(string path) - { - if (!Address.TryParse(path.AsSpan(), out var address)) - return null; - - return new RootActorPath(address); + return actorPath; } protected override bool IsCacheable(ActorPath v) From 29eaff22c53dc00dddafad4dade240808d111751 Mon Sep 17 00:00:00 2001 From: zetanova Date: Wed, 15 Sep 2021 16:49:27 +0200 Subject: [PATCH 26/40] cleanup code --- src/core/Akka.Remote/Serialization/LruBoundedCache.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs index 74177bb2672..2e96ecfecea 100644 --- a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs +++ b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs @@ -109,20 +109,10 @@ public bool Equals(string x, string y) return StringComparer.Ordinal.Equals(x, y); } - public bool Equals(ReadOnlySpan x, ReadOnlySpan y) - { - return x.SequenceEqual(y); - } - public int GetHashCode(string s) { return FastHash.OfStringFast(s); } - - public int GetHashCode(ReadOnlySpan s) - { - return FastHash.OfString(s); - } } /// From d3a0ae0aae77ab9a52fc7651f49a251912c22e15 Mon Sep 17 00:00:00 2001 From: zetanova Date: Wed, 15 Sep 2021 20:40:57 +0200 Subject: [PATCH 27/40] removed commented cache tests --- src/core/Akka.Remote.Tests/RemotingSpec.cs | 30 +--------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/src/core/Akka.Remote.Tests/RemotingSpec.cs b/src/core/Akka.Remote.Tests/RemotingSpec.cs index 876740461ce..5c36790f4fa 100644 --- a/src/core/Akka.Remote.Tests/RemotingSpec.cs +++ b/src/core/Akka.Remote.Tests/RemotingSpec.cs @@ -177,35 +177,7 @@ public async Task Remoting_must_support_Ask() Assert.Equal("pong", msg); Assert.IsType>(actorRef); } - - //[Fact] - //public async Task Remoting_should_not_cache_ref_of_local_ask() - //{ - // var localActorRefResolveCache = ActorRefResolveThreadLocalCache.For(Sys); - // var localActorPathCache = ActorPathThreadLocalCache.For(Sys); - - // var (msg, actorRef) = await _here.Ask<(string, IActorRef)>("ping", DefaultTimeout); - // Assert.Equal("pong", msg); - // Assert.IsType>(actorRef); - - // Assert.Equal(0, localActorRefResolveCache.All.Sum(n => n.Stats.Entries)); - // Assert.Equal(2, localActorPathCache.All.Sum(n => n.Stats.Entries)); - //} - - //[Fact] - //public async Task Remoting_should_not_cache_ref_of_remote_ask() - //{ - // var remoteActorRefResolveCache = ActorRefResolveThreadLocalCache.For(_remoteSystem); - // var remoteActorPathCache = ActorPathThreadLocalCache.For(_remoteSystem); - - // var (msg, actorRef) = await _here.Ask<(string, IActorRef)>("ping", DefaultTimeout); - // Assert.Equal("pong", msg); - // Assert.IsType>(actorRef); - - // Assert.Equal(0, remoteActorRefResolveCache.All.Sum(n => n.Stats.Entries)); - // Assert.Equal(2, remoteActorPathCache.All.Sum(n => n.Stats.Entries)); //should be 1 - //} - + [Fact(Skip = "Racy")] public async Task Ask_does_not_deadlock() { From 217485c1f751d2e8e95a67a4331cf2b7bb414ed6 Mon Sep 17 00:00:00 2001 From: zetanova Date: Wed, 15 Sep 2021 23:49:08 +0200 Subject: [PATCH 28/40] refactor span to string bulder --- src/core/Akka.Tests/Actor/ActorPathSpec.cs | 31 +++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/core/Akka.Tests/Actor/ActorPathSpec.cs b/src/core/Akka.Tests/Actor/ActorPathSpec.cs index f93c3bfebd2..d5d4ac7509e 100644 --- a/src/core/Akka.Tests/Actor/ActorPathSpec.cs +++ b/src/core/Akka.Tests/Actor/ActorPathSpec.cs @@ -7,11 +7,10 @@ using System; using System.Linq; -using System.Net; +using System.Text; using Akka.Actor; using Akka.TestKit; using Xunit; -using Xunit.Extensions; namespace Akka.Tests.Actor { @@ -27,7 +26,7 @@ public void SupportsParsingItsStringRep() private ActorPath ActorPathParse(string path) { ActorPath actorPath; - if(ActorPath.TryParse(path, out actorPath)) + if (ActorPath.TryParse(path, out actorPath)) return actorPath; throw new UriFormatException(); } @@ -44,12 +43,12 @@ public void ActorPath_Parse_HandlesCasing_ForLocal() // as well as "http") for the sake of robustness but should only produce lowercase scheme names // for consistency." rfc3986 Assert.True(actorPath.Address.Protocol.Equals("akka", StringComparison.Ordinal), "protocol should be lowercase"); - + //In Akka, at least the system name is case-sensitive, see http://doc.akka.io/docs/akka/current/additional/faq.html#what-is-the-name-of-a-remote-actor - Assert.True(actorPath.Address.System.Equals("sYstEm", StringComparison.Ordinal), "system"); + Assert.True(actorPath.Address.System.Equals("sYstEm", StringComparison.Ordinal), "system"); var elements = actorPath.Elements.ToList(); - elements.Count.ShouldBe(2,"number of elements in path"); + elements.Count.ShouldBe(2, "number of elements in path"); Assert.True("pAth1".Equals(elements[0], StringComparison.Ordinal), "first path element"); Assert.True("pAth2".Equals(elements[1], StringComparison.Ordinal), "second path element"); Assert.Equal("akka://sYstEm/pAth1/pAth2", actorPath.ToString()); @@ -114,18 +113,20 @@ public void Return_false_upon_malformed_path() [Fact] public void Supports_jumbo_actor_name_length() { - ReadOnlySpan prefix = "akka://sys@host.domain.com:1234/some/ref/".AsSpan(); - Span b = new char[10 * 1024 * 1024]; //10 MB - prefix.CopyTo(b); - b.Slice(prefix.Length).Fill('a'); - var path = b.ToString(); + var prefix = "akka://sys@host.domain.com:1234/some/ref/"; + var nameSize = 10 * 1024 * 1024; //10MB + + var sb = new StringBuilder(nameSize + prefix.Length); + sb.Append(prefix); + sb.Append('a', nameSize); //10MB + var path = sb.ToString(); ActorPath.TryParse(path, out var actorPath).ShouldBe(true); - actorPath.Name.Length.ShouldBe(b.Length - prefix.Length); + actorPath.Name.Length.ShouldBe(nameSize); actorPath.Name.All(n => n == 'a').ShouldBe(true); var result = actorPath.ToStringWithAddress(); - result.AsSpan().SequenceEqual(b).ShouldBe(true); + result.ShouldBe(path); } [Fact] @@ -213,7 +214,7 @@ public void Paths_with_different_addresses_and_same_elements_should_not_be_equal { ActorPath path1 = null; ActorPath path2 = null; - ActorPath.TryParse("akka.tcp://remotesystem@localhost:8080/user",out path1); + ActorPath.TryParse("akka.tcp://remotesystem@localhost:8080/user", out path1); ActorPath.TryParse("akka://remotesystem/user", out path2); Assert.NotEqual(path2, path1); @@ -255,7 +256,7 @@ public void Validate_element_parts(string element, bool matches) public void Validate_that_url_encoded_values_are_valid_element_parts(string element) { var urlEncode = System.Net.WebUtility.UrlEncode(element); - global::System.Diagnostics.Debug.WriteLine("Encoded \"{0}\" to \"{1}\"", element, urlEncode) ; + global::System.Diagnostics.Debug.WriteLine("Encoded \"{0}\" to \"{1}\"", element, urlEncode); ActorPath.IsValidPathElement(urlEncode).ShouldBeTrue(); } } From 409485f2954ca5ed5eed866f9378b509c2ce375b Mon Sep 17 00:00:00 2001 From: zetanova Date: Fri, 17 Sep 2021 05:00:27 +0200 Subject: [PATCH 29/40] use internal fields and ref equals --- .../Serialization/ActorPathCache.cs | 2 +- .../Serialization/ActorRefResolveCache.cs | 2 +- src/core/Akka/Actor/ActorPath.cs | 25 +++++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/core/Akka.Remote/Serialization/ActorPathCache.cs b/src/core/Akka.Remote/Serialization/ActorPathCache.cs index 482d349eab4..6a933f4d75f 100644 --- a/src/core/Akka.Remote/Serialization/ActorPathCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorPathCache.cs @@ -17,7 +17,7 @@ namespace Akka.Remote.Serialization /// internal sealed class ActorPathThreadLocalCache : ExtensionIdProvider, IExtension { - private readonly ThreadLocal _current = new ThreadLocal(() => new ActorPathCache(), false); + private readonly ThreadLocal _current = new ThreadLocal(() => new ActorPathCache()); public ActorPathCache Cache => _current.Value; diff --git a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs index 9e822b3a1fd..62d4431503d 100644 --- a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs @@ -24,7 +24,7 @@ public ActorRefResolveThreadLocalCache() { } public ActorRefResolveThreadLocalCache(IRemoteActorRefProvider provider) { _provider = provider; - _current = new ThreadLocal(() => new ActorRefResolveCache(_provider), false); + _current = new ThreadLocal(() => new ActorRefResolveCache(_provider)); } public override ActorRefResolveThreadLocalCache CreateExtension(ExtendedActorSystem system) diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index bc66c2ed3e1..20aa0adf452 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -171,7 +171,7 @@ protected ActorPath(Address address, string name) protected ActorPath(ActorPath parentPath, string name, long uid) { _parent = parentPath; - _address = parentPath.Address; + _address = parentPath._address; _depth = parentPath._depth + 1; _name = name; _uid = uid; @@ -222,7 +222,7 @@ public IReadOnlyList Elements var p = this; for (var i = 0; i < _depth; i++) { - b[_depth - i - 1] = p.Name; + b[_depth - i - 1] = p._name; p = p._parent; } return b.MoveToImmutable(); @@ -268,6 +268,9 @@ public bool Equals(ActorPath other) if (other is null || _depth != other._depth) return false; + if (ReferenceEquals(this, other)) + return true; + if (!Address.Equals(other.Address)) return false; @@ -378,12 +381,12 @@ public ActorPath ParentOf(int depth) if (depth >= 0) { while (current._depth > depth) - current = current.Parent; + current = current._parent; } else { - for (var i = depth; i < 0 && current.Depth > 0; i++) - current = current.Parent; + for (var i = depth; i < 0 && current._depth > 0; i++) + current = current._parent; } return current; } @@ -516,7 +519,7 @@ public static bool TryParseParts(ReadOnlySpan path, out ReadOnlySpan absoluteUri = path; return false; } - + var doubleSlash = path.Slice(firstAtPos + 1); if (doubleSlash.Length < 2 || !(doubleSlash[0] == '/' && doubleSlash[1] == '/')) { @@ -537,7 +540,7 @@ public static bool TryParseParts(ReadOnlySpan path, out ReadOnlySpan address = path.Slice(0, firstAtPos + 3 + nextSlash); absoluteUri = path.Slice(address.Length); } - + return true; } @@ -564,7 +567,7 @@ private string Join(ReadOnlySpan prefix) while (p._depth > 0) { totalLength += p._name.Length + 1; - p = p.Parent; + p = p._parent; } // Concatenate segments (in reverse order) into buffer with '/' prefixes @@ -580,7 +583,7 @@ private string Join(ReadOnlySpan prefix) offset -= name.Length + 1; buffer[offset] = '/'; name.CopyTo(buffer.Slice(offset + 1, name.Length)); - p = p.Parent; + p = p._parent; } return buffer.ToString(); //todo use string.Create() when available } @@ -628,8 +631,8 @@ public override int GetHashCode() { var hash = 17; hash = (hash * 23) ^ Address.GetHashCode(); - for (var p = this; !(p is null); p = p.Parent) - hash = (hash * 23) ^ p.Name.GetHashCode(); + for (var p = this; !(p is null); p = p._parent) + hash = (hash * 23) ^ p._name.GetHashCode(); return hash; } } From 9eba407c59963d58e7ee1e212de9f5fc0216c944 Mon Sep 17 00:00:00 2001 From: zetanova Date: Fri, 17 Sep 2021 05:30:32 +0200 Subject: [PATCH 30/40] add rebase path test --- .../CoreAPISpec.ApproveCore.approved.txt | 1 + src/core/Akka.Tests/Actor/ActorPathSpec.cs | 21 +++++++++++++++++++ src/core/Akka/Actor/ActorPath.cs | 13 ++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index 9cd21e0d14d..c8c3861b358 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -202,6 +202,7 @@ namespace Akka.Actor public string ToStringWithoutAddress() { } public Akka.Util.ISurrogate ToSurrogate(Akka.Actor.ActorSystem system) { } public static bool TryParse(string path, out Akka.Actor.ActorPath actorPath) { } + public static bool TryParse(Akka.Actor.ActorPath basePath, string absoluteUri, out Akka.Actor.ActorPath actorPath) { } public static bool TryParse(Akka.Actor.ActorPath basePath, System.ReadOnlySpan absoluteUri, out Akka.Actor.ActorPath actorPath) { } public static bool TryParseAddress(string path, out Akka.Actor.Address address) { } public static bool TryParseAddress(string path, out Akka.Actor.Address address, out System.ReadOnlySpan absoluteUri) { } diff --git a/src/core/Akka.Tests/Actor/ActorPathSpec.cs b/src/core/Akka.Tests/Actor/ActorPathSpec.cs index d5d4ac7509e..b22f2eedc12 100644 --- a/src/core/Akka.Tests/Actor/ActorPathSpec.cs +++ b/src/core/Akka.Tests/Actor/ActorPathSpec.cs @@ -99,6 +99,27 @@ public void Supports_parsing_remote_FQDN_paths() parsed.ToString().ShouldBe(remote); } + [Fact] + public void Supports_rebase_a_path() + { + var path = "akka://sys@host:1234/"; + ActorPath.TryParse(path, out var root).ShouldBe(true); + root.ToString().ShouldBe(path); + + ActorPath.TryParse(root, "/", out var newPath).ShouldBe(true); + newPath.ShouldBe(root); + + var uri1 = "/abc/def"; + ActorPath.TryParse(root, uri1, out newPath).ShouldBe(true); + newPath.ToStringWithAddress().ShouldBe($"{path}{uri1.Substring(1)}"); + newPath.ParentOf(-2).ShouldBe(root); + + var uri2 = "/def"; + ActorPath.TryParse(newPath, uri2, out newPath).ShouldBe(true); + newPath.ToStringWithAddress().ShouldBe($"{path}{uri1.Substring(1)}{uri2}"); + newPath.ParentOf(-3).ShouldBe(root); + } + [Fact] public void Return_false_upon_malformed_path() { diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 20aa0adf452..8ddc08ec71f 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -424,6 +424,19 @@ public static bool TryParse(string path, out ActorPath actorPath) return TryParse(new RootActorPath(address), absoluteUri, out actorPath); } + /// + /// Tries to parse the uri, which should be a uri not containing protocol. + /// For example "/user/my-actor" + /// + /// the base path, normaly a root path + /// TBD + /// TBD + /// TBD + public static bool TryParse(ActorPath basePath, string absoluteUri, out ActorPath actorPath) + { + return TryParse(basePath, absoluteUri.AsSpan(), out actorPath); + } + /// /// Tries to parse the uri, which should be a uri not containing protocol. /// For example "/user/my-actor" From 8120ee25a52407494ad8ac906a41a0770d8d7d94 Mon Sep 17 00:00:00 2001 From: Andrew El Date: Sat, 18 Sep 2021 11:11:33 -0400 Subject: [PATCH 31/40] try semaphoreslim in dtp --- src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs b/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs index 497e27af097..8f5e9ed118b 100644 --- a/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs +++ b/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs @@ -560,7 +560,7 @@ private struct SemaphoreState private struct CacheLinePadding { } - private readonly Semaphore m_semaphore; + private readonly SemaphoreSlim m_semaphore; // padding to ensure we get our own cache line #pragma warning disable 169 @@ -571,7 +571,7 @@ private struct CacheLinePadding public UnfairSemaphore() { - m_semaphore = new Semaphore(0, short.MaxValue); + m_semaphore = new SemaphoreSlim(0, short.MaxValue); } public bool Wait() @@ -650,7 +650,7 @@ public bool Wait(TimeSpan timeout) // // Now we're a waiter // - bool waitSucceeded = m_semaphore.WaitOne(timeout); + bool waitSucceeded = m_semaphore.Wait(timeout); while (true) { From bff649c73d352175c0b3daf330fa845d58f125de Mon Sep 17 00:00:00 2001 From: Andrew El Date: Sat, 18 Sep 2021 11:11:42 -0400 Subject: [PATCH 32/40] Revert "try semaphoreslim in dtp" This reverts commit 8120ee25a52407494ad8ac906a41a0770d8d7d94. --- src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs b/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs index 8f5e9ed118b..497e27af097 100644 --- a/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs +++ b/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs @@ -560,7 +560,7 @@ private struct SemaphoreState private struct CacheLinePadding { } - private readonly SemaphoreSlim m_semaphore; + private readonly Semaphore m_semaphore; // padding to ensure we get our own cache line #pragma warning disable 169 @@ -571,7 +571,7 @@ private struct CacheLinePadding public UnfairSemaphore() { - m_semaphore = new SemaphoreSlim(0, short.MaxValue); + m_semaphore = new Semaphore(0, short.MaxValue); } public bool Wait() @@ -650,7 +650,7 @@ public bool Wait(TimeSpan timeout) // // Now we're a waiter // - bool waitSucceeded = m_semaphore.Wait(timeout); + bool waitSucceeded = m_semaphore.WaitOne(timeout); while (true) { From e25c1ea2b76d5e3ee3937fc9fc94f3ba5b94fea2 Mon Sep 17 00:00:00 2001 From: Andrew El Date: Sat, 18 Sep 2021 11:36:00 -0400 Subject: [PATCH 33/40] save wat --- src/Akka.sln | 15 ++++++ .../Akka.Cluster.Benchmark.DotTrace.csproj | 20 ++++++++ .../Program.cs | 48 +++++++++++++++++++ .../Akka.Cluster.Benchmarks.csproj | 1 + .../Sharding/ShardMessageRoutingBenchmarks.cs | 10 ++-- .../Sharding/ShardingInfrastructure.cs | 12 +++++ .../cluster/Akka.Cluster.Sharding/Shard.cs | 32 +++++++------ src/core/Akka.Remote/Remoting.cs | 21 +++++++- src/core/Akka/Actor/ActorPath.cs | 5 +- src/core/Akka/Actor/ActorRef.cs | 13 +++-- src/core/Akka/Actor/Address.cs | 28 ++++++----- src/core/Akka/Akka.csproj | 2 +- .../Util/Internal/Collections/ListSlice.cs | 6 +-- 13 files changed, 168 insertions(+), 45 deletions(-) create mode 100644 src/benchmark/Akka.Cluster.Benchmark.DotTrace/Akka.Cluster.Benchmark.DotTrace.csproj create mode 100644 src/benchmark/Akka.Cluster.Benchmark.DotTrace/Program.cs diff --git a/src/Akka.sln b/src/Akka.sln index bf0782f7236..d5e76f9ac0e 100644 --- a/src/Akka.sln +++ b/src/Akka.sln @@ -248,6 +248,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDataStressTest", "examples EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Cluster.Benchmarks", "benchmark\Akka.Cluster.Benchmarks\Akka.Cluster.Benchmarks.csproj", "{3CEBB0AE-6A88-4C32-A1D3-A8FB1E7E236B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Cluster.Benchmark.DotTrace", "benchmark\Akka.Cluster.Benchmark.DotTrace\Akka.Cluster.Benchmark.DotTrace.csproj", "{9D721A3E-4D4D-4715-B1E2-2B392DA9581F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1151,6 +1153,18 @@ Global {3CEBB0AE-6A88-4C32-A1D3-A8FB1E7E236B}.Release|x64.Build.0 = Release|Any CPU {3CEBB0AE-6A88-4C32-A1D3-A8FB1E7E236B}.Release|x86.ActiveCfg = Release|Any CPU {3CEBB0AE-6A88-4C32-A1D3-A8FB1E7E236B}.Release|x86.Build.0 = Release|Any CPU + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F}.Debug|x64.ActiveCfg = Debug|Any CPU + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F}.Debug|x64.Build.0 = Debug|Any CPU + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F}.Debug|x86.ActiveCfg = Debug|Any CPU + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F}.Debug|x86.Build.0 = Debug|Any CPU + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F}.Release|Any CPU.Build.0 = Release|Any CPU + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F}.Release|x64.ActiveCfg = Release|Any CPU + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F}.Release|x64.Build.0 = Release|Any CPU + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F}.Release|x86.ActiveCfg = Release|Any CPU + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1260,6 +1274,7 @@ Global {2E4B9584-42CC-4D17-B719-9F462B16C94D} = {73108242-625A-4D7B-AA09-63375DBAE464} {44B3DDD6-6103-4E8F-8AC2-0F4BA3CF6B50} = {C50E1A9E-820C-4E75-AE39-6F96A99AC4A7} {3CEBB0AE-6A88-4C32-A1D3-A8FB1E7E236B} = {73108242-625A-4D7B-AA09-63375DBAE464} + {9D721A3E-4D4D-4715-B1E2-2B392DA9581F} = {73108242-625A-4D7B-AA09-63375DBAE464} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {03AD8E21-7507-4E68-A4E9-F4A7E7273164} diff --git a/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Akka.Cluster.Benchmark.DotTrace.csproj b/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Akka.Cluster.Benchmark.DotTrace.csproj new file mode 100644 index 00000000000..5fbf0937e52 --- /dev/null +++ b/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Akka.Cluster.Benchmark.DotTrace.csproj @@ -0,0 +1,20 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Program.cs b/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Program.cs new file mode 100644 index 00000000000..5c27f824dbf --- /dev/null +++ b/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Program.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using Akka.Cluster.Benchmarks.Sharding; +using Akka.Cluster.Sharding; +using JetBrains.Profiler.Api; + +namespace Akka.Cluster.Benchmark.DotTrace +{ + class Program + { + static async Task Main(string[] args) + { + + await Task.Run(async () => + { + + + var container = new ShardMessageRoutingBenchmarks(); + container.StateMode = StateStoreMode.DData; + container.MsgCount = 1; + await container.Setup(); + await runIters(20, container); + await container.SingleRequestResponseToRemoteEntity(); + MeasureProfiler.StartCollectingData(); + for (int i = 0; i < 10; i++) + { + Console.WriteLine($"Try {i}"); + await runIters(10000, container); + + } + + MeasureProfiler.SaveData(); + GC.KeepAlive(container); + return 1; + }); + Console.ReadLine(); + } + + private static async Task runIters(int iters, + ShardMessageRoutingBenchmarks container) + { + for (int i = 0; i < iters; i++) + { + await container.SingleRequestResponseToRemoteEntity(); + } + } + } +} \ No newline at end of file diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Akka.Cluster.Benchmarks.csproj b/src/benchmark/Akka.Cluster.Benchmarks/Akka.Cluster.Benchmarks.csproj index 21ccbdcb825..a0a1b99385c 100644 --- a/src/benchmark/Akka.Cluster.Benchmarks/Akka.Cluster.Benchmarks.csproj +++ b/src/benchmark/Akka.Cluster.Benchmarks/Akka.Cluster.Benchmarks.csproj @@ -12,6 +12,7 @@ + diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardMessageRoutingBenchmarks.cs b/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardMessageRoutingBenchmarks.cs index c2fc8d0ead0..6f647cf8066 100644 --- a/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardMessageRoutingBenchmarks.cs +++ b/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardMessageRoutingBenchmarks.cs @@ -21,7 +21,7 @@ namespace Akka.Cluster.Benchmarks.Sharding [Config(typeof(MonitoringConfig))] public class ShardMessageRoutingBenchmarks { - [Params(StateStoreMode.Persistence, StateStoreMode.DData)] + [Params(StateStoreMode.Persistence)] public StateStoreMode StateMode; [Params(10000)] @@ -141,14 +141,14 @@ public void PerIteration() _batchActor = _sys1.ActorOf(Props.Create(() => new BulkSendActor(tcs, MsgCount))); } - [Benchmark] + //[Benchmark] public async Task SingleRequestResponseToLocalEntity() { for (var i = 0; i < MsgCount; i++) await _shardRegion1.Ask(_messageToSys1); } - [Benchmark] + //[Benchmark] public async Task StreamingToLocalEntity() { _batchActor.Tell(new BulkSendActor.BeginSend(_messageToSys1, _shardRegion1, BatchSize)); @@ -163,14 +163,14 @@ public async Task SingleRequestResponseToRemoteEntity() } - [Benchmark] + //[Benchmark] public async Task SingleRequestResponseToRemoteEntityWithLocalProxy() { for (var i = 0; i < MsgCount; i++) await _localRouter.Ask(new SendShardedMessage(_messageToSys2.EntityId, _messageToSys2)); } - [Benchmark] + //[Benchmark] public async Task StreamingToRemoteEntity() { _batchActor.Tell(new BulkSendActor.BeginSend(_messageToSys2, _shardRegion1, BatchSize)); diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardingInfrastructure.cs b/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardingInfrastructure.cs index e016ea70052..2903c38728c 100644 --- a/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardingInfrastructure.cs +++ b/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardingInfrastructure.cs @@ -223,6 +223,12 @@ public static Config CreatePersistenceConfig(bool rememberEntities = false) var connectionString = "Filename=file:memdb-journal-" + DbId.IncrementAndGet() + ".db;Mode=Memory;Cache=Shared"; var config = $@" +akka.actor {{ +serializers.hyperion = ""Akka.Serialization.HyperionSerializer, Akka.Serialization.Hyperion"" + serialization-bindings {{ + ""System.Object"" = hyperion + }} +}} akka.actor.provider = cluster akka.remote.dot-netty.tcp.port = 0 akka.cluster.sharding.state-store-mode=persistence @@ -248,6 +254,12 @@ class = ""Akka.Persistence.Sqlite.Snapshot.SqliteSnapshotStore, Akka.Persistence public static Config CreateDDataConfig(bool rememberEntities = false) { var config = $@" +akka.actor {{ +serializers.hyperion = ""Akka.Serialization.HyperionSerializer, Akka.Serialization.Hyperion"" + serialization-bindings {{ + ""System.Object"" = hyperion + }} +}} akka.actor.provider = cluster akka.remote.dot-netty.tcp.port = 0 akka.cluster.sharding.state-store-mode=ddata diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs b/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs index 9c306d7b305..f220e08965e 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs @@ -946,20 +946,24 @@ internal static void BaseDeliverTo(this TShard shard, string id, object internal static IActorRef GetOrCreateEntity(this TShard shard, string id, Action onCreate = null) where TShard : IShard { var name = Uri.EscapeDataString(id); - var child = shard.Context.Child(name).GetOrElse(() => - { - shard.Log.Debug("Starting entity [{0}] in shard [{1}]", id, shard.ShardId); - - var a = shard.Context.Watch(shard.Context.ActorOf(shard.EntityProps(id), name)); - shard.IdByRef = shard.IdByRef.SetItem(a, id); - shard.RefById = shard.RefById.SetItem(id, a); - shard.TouchLastMessageTimestamp(id); - shard.State = new Shard.ShardState(shard.State.Entries.Add(id)); - onCreate?.Invoke(a); - return a; - }); - - return child; + var child = shard.Context.Child(name); + if (!child.IsNobody()) return child; + return CreateEntity(shard, id, onCreate, name); + } + + private static IActorRef CreateEntity(TShard shard, string id, + Action onCreate, string name) where TShard : IShard + { + shard.Log.Debug("Starting entity [{0}] in shard [{1}]", id, shard.ShardId); + + var a = shard.Context.Watch( + shard.Context.ActorOf(shard.EntityProps(id), name)); + shard.IdByRef = shard.IdByRef.SetItem(a, id); + shard.RefById = shard.RefById.SetItem(id, a); + shard.TouchLastMessageTimestamp(id); + shard.State = new Shard.ShardState(shard.State.Entries.Add(id)); + onCreate?.Invoke(a); + return a; } internal static int TotalBufferSize(this TShard shard) where TShard : IShard => diff --git a/src/core/Akka.Remote/Remoting.cs b/src/core/Akka.Remote/Remoting.cs index 28242173e71..221c0763b5b 100644 --- a/src/core/Akka.Remote/Remoting.cs +++ b/src/core/Akka.Remote/Remoting.cs @@ -108,6 +108,22 @@ public static RARP For(ActorSystem system) /// internal interface IPriorityMessage { } + internal sealed class AddressEqualityComparer : EqualityComparer
+ { + public static readonly AddressEqualityComparer Instance = + new AddressEqualityComparer(); + public override bool Equals(Address x, Address y) + { + if (x != null) + return x.Equals(y); + return (y == null); + } + + public override int GetHashCode(Address obj) + { + return obj.GetHashCode(); + } + } /// /// INTERNAL API /// @@ -196,7 +212,8 @@ public override void Start() var akkaProtocolTransports = addressPromise.Task.Result; if(akkaProtocolTransports.Count==0) throw new ConfigurationException(@"No transports enabled under ""akka.remote.enabled-transports"""); - _addresses = new HashSet
(akkaProtocolTransports.Select(a => a.Address)); + + _addresses = new HashSet
(akkaProtocolTransports.Select(a => a.Address), AddressEqualityComparer.Instance); IEnumerable> tmp = akkaProtocolTransports.GroupBy(t => t.ProtocolTransport.SchemeIdentifier); @@ -208,7 +225,7 @@ public override void Start() } _defaultAddress = akkaProtocolTransports.Head().Address; - _addresses = new HashSet
(akkaProtocolTransports.Select(x => x.Address)); + _addresses = new HashSet
(akkaProtocolTransports.Select(x => x.Address), AddressEqualityComparer.Instance); _log.Info("Remoting started; listening on addresses : [{0}]", string.Join(",", _addresses.Select(x => x.ToString()))); diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 8ddc08ec71f..2b4d84900e9 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -532,9 +532,8 @@ public static bool TryParseParts(ReadOnlySpan path, out ReadOnlySpan absoluteUri = path; return false; } - - var doubleSlash = path.Slice(firstAtPos + 1); - if (doubleSlash.Length < 2 || !(doubleSlash[0] == '/' && doubleSlash[1] == '/')) + + if (path.Slice(firstAtPos + 1).StartsWith("//".AsSpan()) == false) { //missing double slash address = default; diff --git a/src/core/Akka/Actor/ActorRef.cs b/src/core/Akka/Actor/ActorRef.cs index 7208047ff44..124bd9ceaa4 100644 --- a/src/core/Akka/Actor/ActorRef.cs +++ b/src/core/Akka/Actor/ActorRef.cs @@ -509,9 +509,16 @@ public override IInternalActorRef Parent /// public override IActorRef GetChild(IReadOnlyList name) { - if (name.All(x => string.IsNullOrEmpty(x))) - return this; - return ActorRefs.Nobody; + foreach (var s in name) + { + if (string.IsNullOrEmpty(s) == false) + return ActorRefs.Nobody; + } + + return this; + //if (name.All(x => string.IsNullOrEmpty(x))) + // return this; + //return ActorRefs.Nobody; } /// diff --git a/src/core/Akka/Actor/Address.cs b/src/core/Akka/Actor/Address.cs index 6ac1020371c..54ec23bfa22 100644 --- a/src/core/Akka/Actor/Address.cs +++ b/src/core/Akka/Actor/Address.cs @@ -65,6 +65,7 @@ public int Compare(Address x, Address y) private readonly int? _port; private readonly string _system; private readonly string _protocol; + private int _hashCode; /// /// TBD @@ -160,14 +161,19 @@ public bool Equals(Address other) /// public override int GetHashCode() { - unchecked + if (_hashCode == 0) { - var hashCode = (Host != null ? Host.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ Port.GetHashCode(); - hashCode = (hashCode * 397) ^ (System != null ? System.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (Protocol != null ? Protocol.GetHashCode() : 0); - return hashCode; + unchecked + { + var hashCode = (Host != null ? Host.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ Port.GetHashCode(); + hashCode = (hashCode * 397) ^ (System != null ? System.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (Protocol != null ? Protocol.GetHashCode() : 0); + _hashCode = hashCode; + } } + + return _hashCode; } int IComparable.CompareTo(object obj) @@ -329,23 +335,21 @@ public static bool TryParse(ReadOnlySpan span, out Address address) } span = span.Slice(firstColonPos + 1); - if (span.Length < 2 || !(span[0] == '/' && span[1] == '/')) + if (span.StartsWith("//".AsSpan()) == false) return false; span = span.Slice(2); // move past the double // var firstAtPos = span.IndexOf('@'); - string sysName; + if (firstAtPos == -1) { // dealing with an absolute local Uri - sysName = span.ToString(); - address = new Address(fullScheme.ToString(), sysName); + address = new Address(fullScheme.ToString(), span.ToString()); return true; } - // dealing with a remote Uri - sysName = span.Slice(0, firstAtPos).ToString(); + string sysName = span.Slice(0, firstAtPos).ToString(); span = span.Slice(firstAtPos + 1); /* diff --git a/src/core/Akka/Akka.csproj b/src/core/Akka/Akka.csproj index 2f0d47d7804..148175866d3 100644 --- a/src/core/Akka/Akka.csproj +++ b/src/core/Akka/Akka.csproj @@ -4,10 +4,10 @@ Akka Akka.NET is a port of the popular Java/Scala framework Akka to .NET - $(NetStandardLibVersion) $(AkkaPackageTags) true 7.2 + netstandard2.0 diff --git a/src/core/Akka/Util/Internal/Collections/ListSlice.cs b/src/core/Akka/Util/Internal/Collections/ListSlice.cs index 5614b6a05b7..c70197642a7 100644 --- a/src/core/Akka/Util/Internal/Collections/ListSlice.cs +++ b/src/core/Akka/Util/Internal/Collections/ListSlice.cs @@ -89,11 +89,7 @@ public void Dispose() public ListSlice(IReadOnlyList array) { - - if (array == null) - throw new ArgumentNullException(nameof(array)); - - _array = array; + _array = array ?? throw new ArgumentNullException(nameof(array)); Offset = 0; Count = array.Count; } From d70ed95a258f31753fc201c6304f7448bba7cebe Mon Sep 17 00:00:00 2001 From: Andrew El Date: Thu, 23 Sep 2021 10:00:51 -0400 Subject: [PATCH 34/40] wip --- src/benchmark/RemotePingPong/Program.cs | 2 +- src/core/Akka.Remote/Akka.Remote.csproj | 1 + .../Akka.Remote/RemoteActorRefProvider.cs | 37 ++++-- .../Serialization/ActorPathCache.cs | 120 +++++++++++++++++- .../Serialization/ActorRefResolveCache.cs | 92 +++++++++++++- .../Akka.Remote/Transport/AkkaPduCodec.cs | 22 +++- src/core/Akka/Actor/ActorRef.cs | 7 +- 7 files changed, 261 insertions(+), 20 deletions(-) diff --git a/src/benchmark/RemotePingPong/Program.cs b/src/benchmark/RemotePingPong/Program.cs index 969e48315e5..421d966ea4b 100644 --- a/src/benchmark/RemotePingPong/Program.cs +++ b/src/benchmark/RemotePingPong/Program.cs @@ -70,7 +70,7 @@ public static Config CreateActorSystemConfig(string actorSystemName, string ipOr private static async Task Main(params string[] args) { - Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High; + //Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High; uint timesToRun; if (args.Length == 0 || !uint.TryParse(args[0], out timesToRun)) { diff --git a/src/core/Akka.Remote/Akka.Remote.csproj b/src/core/Akka.Remote/Akka.Remote.csproj index 5b1f41f9715..a1879f09948 100644 --- a/src/core/Akka.Remote/Akka.Remote.csproj +++ b/src/core/Akka.Remote/Akka.Remote.csproj @@ -12,6 +12,7 @@ + diff --git a/src/core/Akka.Remote/RemoteActorRefProvider.cs b/src/core/Akka.Remote/RemoteActorRefProvider.cs index f3165c98791..f45874dd976 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -72,7 +72,7 @@ public interface IRemoteActorRefProvider : IActorRefProvider /// TBD /// TBD /// TBD - IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address localAddress); + IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address localAddress, bool? senderOption); /// /// INTERNAL API: this is used by the via the public @@ -239,7 +239,9 @@ public void UnregisterTempActor(ActorPath path) private IActorRef _remoteWatcher; private ActorRefResolveThreadLocalCache _actorRefResolveThreadLocalCache; + private ActorRefResolveAskCache _actorRefResolveAskCache; private ActorPathThreadLocalCache _actorPathThreadLocalCache; + private ActorPathAskResolverCache _actorPathAskResolverCache; /// /// The remote death watcher. @@ -254,7 +256,8 @@ public virtual void Init(ActorSystemImpl system) _actorRefResolveThreadLocalCache = ActorRefResolveThreadLocalCache.For(system); _actorPathThreadLocalCache = ActorPathThreadLocalCache.For(system); - + _actorRefResolveAskCache = ActorRefResolveAskCache.For(system); + _actorPathAskResolverCache = ActorPathAskResolverCache.For(system); _local.Init(system); _remotingTerminator = @@ -467,7 +470,7 @@ private IInternalActorRef LocalActorOf(ActorSystemImpl system, Props props, IInt /// TBD /// TBD /// TBD - public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address localAddress) + public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address localAddress, bool? senderOption = null) { if (path is null) { @@ -475,10 +478,26 @@ public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address lo return InternalDeadLetters; } - ActorPath actorPath; - if (_actorPathThreadLocalCache != null) + ActorPath actorPath = null; + if (_actorPathThreadLocalCache != null && _actorPathAskResolverCache != null) { - actorPath = _actorPathThreadLocalCache.Cache.GetOrCompute(path); + if (senderOption.HasValue && senderOption.Value == true) + { + actorPath = _actorPathThreadLocalCache.Cache.GetOrCompute(path); + } + else + { + if (path.Contains("/temp/") == true) + { + actorPath = + _actorPathAskResolverCache.Cache.GetOrNull(path); + } + if (actorPath == null) + { + actorPath = + _actorPathThreadLocalCache.Cache.GetOrCompute(path); + } + } } else // cache not initialized yet { @@ -534,12 +553,14 @@ public IActorRef ResolveActorRef(string path) // using thread local LRU cache, which will call InternalResolveActorRef // if the value is not cached - if (_actorRefResolveThreadLocalCache == null) + if (_actorRefResolveThreadLocalCache == null || _actorRefResolveAskCache == null) { // cache not initialized yet, should never happen return InternalResolveActorRef(path); } - return _actorRefResolveThreadLocalCache.Cache.GetOrCompute(path); + + var ask = _actorRefResolveAskCache.Cache.GetOrNull(path); + return ask??_actorRefResolveThreadLocalCache.Cache.GetOrCompute(path); } /// diff --git a/src/core/Akka.Remote/Serialization/ActorPathCache.cs b/src/core/Akka.Remote/Serialization/ActorPathCache.cs index 6a933f4d75f..3898a6eff37 100644 --- a/src/core/Akka.Remote/Serialization/ActorPathCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorPathCache.cs @@ -9,6 +9,7 @@ using Akka.Actor; using System.Threading; using System.Collections.Generic; +using BitFaster.Caching.Lru; namespace Akka.Remote.Serialization { @@ -17,10 +18,13 @@ namespace Akka.Remote.Serialization /// internal sealed class ActorPathThreadLocalCache : ExtensionIdProvider, IExtension { - private readonly ThreadLocal _current = new ThreadLocal(() => new ActorPathCache()); + //private readonly ThreadLocal _current = new ThreadLocal(() => new ActorPathCache()); - public ActorPathCache Cache => _current.Value; + private readonly ActorPathBitfasterCache _current = + new ActorPathBitfasterCache(); + //public ActorPathCache Cache => _current.Value; + public ActorPathBitfasterCache Cache => _current; public override ActorPathThreadLocalCache CreateExtension(ExtendedActorSystem system) { return new ActorPathThreadLocalCache(); @@ -31,6 +35,118 @@ public static ActorPathThreadLocalCache For(ActorSystem system) return system.WithExtension(); } } + internal sealed class ActorPathAskResolverCache : ExtensionIdProvider, IExtension + { + //private readonly ThreadLocal _current = new ThreadLocal(() => new ActorPathCache()); + + private readonly ActorPathAskCache _current = + new ActorPathAskCache(); + + //public ActorPathCache Cache => _current.Value; + public ActorPathAskCache Cache => _current; + public override ActorPathAskResolverCache CreateExtension(ExtendedActorSystem system) + { + return new ActorPathAskResolverCache(); + } + + public static ActorPathAskResolverCache For(ActorSystem system) + { + return system.WithExtension(); + } + } + public class ActorPathAskCache + { + private readonly FastConcurrentLru _cache = + new FastConcurrentLru(Environment.ProcessorCount, + 1030, FastHashComparer.Default); + + public ActorPath GetOrNull(string actorPath) + { + if (_cache.TryGet(actorPath, out ActorPath askRef)) + { + return askRef; + } + + return null; + } + + public void Set(string actorPath, ActorPath actorPathObj) + { + _cache.AddOrUpdate(actorPath,actorPathObj); + } + + } + + internal sealed class ActorPathBitfasterCache + { + public FastConcurrentLru _cache = + new FastConcurrentLru(Environment.ProcessorCount, + 1030, FastHashComparer.Default); + public FastConcurrentLru _rootCache = + new FastConcurrentLru(Environment.ProcessorCount, + 1030, FastHashComparer.Default); + public ActorPath GetOrCompute(string k) + { + if (k.Contains("/temp/")) + { + return ParsePath(k); + } + if (_cache.TryGet(k, out ActorPath outPath)) + { + return outPath; + } + + outPath = ParsePath(k); + if (outPath != null) + { + _cache.AddOrUpdate(k,outPath); + } + + return outPath; + } + + private ActorPath ParsePath(string k) + { + var path = k.AsSpan(); + + if (!ActorPath.TryParseParts(path, out var addressSpan, out var absoluteUri) + ) + return null; + + + string rootPath; + if (absoluteUri.Length > 1 || path.Length > addressSpan.Length) + { + //path end with / + rootPath = path.Slice(0, addressSpan.Length + 1).ToString(); + } + else + { + //todo replace with string.create + Span buffer = addressSpan.Length < 1024 + ? stackalloc char[addressSpan.Length + 1] + : new char[addressSpan.Length + 1]; + path.Slice(0, addressSpan.Length).CopyTo(buffer); + buffer[buffer.Length - 1] = '/'; + rootPath = buffer.ToString(); + } + + //try lookup root in cache + if (!_rootCache.TryGet(rootPath, out var actorPath)) + { + if (!Address.TryParse(addressSpan, out var address)) + return null; + + actorPath = new RootActorPath(address); + _rootCache.AddOrUpdate(rootPath, actorPath); + } + + if (!ActorPath.TryParse(actorPath, absoluteUri, out actorPath)) + return null; + + return actorPath; + } + } /// /// INTERNAL API diff --git a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs index 62d4431503d..1ba8d1dd913 100644 --- a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs @@ -5,10 +5,12 @@ // //----------------------------------------------------------------------- +using System; using System.Collections.Generic; using System.Threading; using Akka.Actor; using Akka.Util.Internal; +using BitFaster.Caching.Lru; namespace Akka.Remote.Serialization { @@ -24,7 +26,8 @@ public ActorRefResolveThreadLocalCache() { } public ActorRefResolveThreadLocalCache(IRemoteActorRefProvider provider) { _provider = provider; - _current = new ThreadLocal(() => new ActorRefResolveCache(_provider)); + //_current = new ThreadLocal(() => new ActorRefResolveCache(_provider)); + _current = new ActorRefResolveBitfasterCache(_provider); } public override ActorRefResolveThreadLocalCache CreateExtension(ExtendedActorSystem system) @@ -32,15 +35,98 @@ public override ActorRefResolveThreadLocalCache CreateExtension(ExtendedActorSys return new ActorRefResolveThreadLocalCache((IRemoteActorRefProvider)system.Provider); } - private readonly ThreadLocal _current; + //private readonly ThreadLocal _current; + private readonly ActorRefResolveBitfasterCache _current; - public ActorRefResolveCache Cache => _current.Value; + //public ActorRefResolveCache Cache => _current.Value; + public ActorRefResolveBitfasterCache Cache => _current; public static ActorRefResolveThreadLocalCache For(ActorSystem system) { return system.WithExtension(); } } + + /// + /// INTERNAL API + /// + internal sealed class ActorRefResolveAskCache : ExtensionIdProvider, IExtension + { + private readonly IRemoteActorRefProvider _provider; + + public ActorRefResolveAskCache() + { + //_current = new ThreadLocal(() => new ActorRefResolveCache(_provider)); + _current = new ActorRefAskResolverCache(); + } + + public override ActorRefResolveAskCache CreateExtension(ExtendedActorSystem system) + { + return new ActorRefResolveAskCache(); + } + + //private readonly ThreadLocal _current; + private readonly ActorRefAskResolverCache _current; + + //public ActorRefResolveCache Cache => _current.Value; + public ActorRefAskResolverCache Cache => _current; + + public static ActorRefResolveAskCache For(ActorSystem system) + { + return system.WithExtension(); + } + } + + public class ActorRefAskResolverCache + { + private readonly FastConcurrentLru _cache = + new FastConcurrentLru(Environment.ProcessorCount, + 1030, FastHashComparer.Default); + + public IActorRef GetOrNull(string actorPath) + { + if (_cache.TryGet(actorPath, out IActorRef askRef)) + { + return askRef; + } + + return null; + } + + public void Set(string actorPath, IActorRef actorRef) + { + _cache.AddOrUpdate(actorPath,actorRef); + } + + } + + public class ActorRefResolveBitfasterCache + { + private readonly IRemoteActorRefProvider _provider; + + private readonly FastConcurrentLru _cache = + new FastConcurrentLru(Environment.ProcessorCount, + 1030, FastHashComparer.Default); + public ActorRefResolveBitfasterCache(IRemoteActorRefProvider provider) + { + _provider = provider; + } + + public IActorRef GetOrCompute(string k) + { + if (_cache.TryGet(k, out IActorRef outRef)) + { + return outRef; + } + outRef= _provider.InternalResolveActorRef(k); + if (!(outRef is MinimalActorRef && !(outRef is FunctionRef))) + { + _cache.AddOrUpdate(k, outRef); + } + + return outRef; + } + } /// /// INTERNAL API diff --git a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs index 335483e5ffb..78a1e4af530 100644 --- a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs +++ b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs @@ -203,11 +203,14 @@ internal abstract class AkkaPduCodec { protected readonly ActorSystem System; protected readonly ActorPathThreadLocalCache ActorPathCache; - + protected readonly ActorRefResolveAskCache AskRefCache; + protected readonly ActorPathAskResolverCache AskPathCache; protected AkkaPduCodec(ActorSystem system) { System = system; ActorPathCache = ActorPathThreadLocalCache.For(system); + AskRefCache = ActorRefResolveAskCache.For(system); + AskPathCache = ActorPathAskResolverCache.For(system); } /// @@ -425,15 +428,15 @@ public override AckAndMessage DecodeMessage(ByteString raw, IRemoteActorRefProvi var envelopeContainer = ackAndEnvelope.Envelope; if (envelopeContainer != null) { - var recipient = provider.ResolveActorRefWithLocalAddress(envelopeContainer.Recipient.Path, localAddress); + var recipient = provider.ResolveActorRefWithLocalAddress(envelopeContainer.Recipient.Path, localAddress, false); //todo get parsed address from provider - var recipientAddress = ActorPathCache.Cache.GetOrCompute(envelopeContainer.Recipient.Path).Address; + var recipientAddress = recipient.Path.Address;// ActorPathCache.Cache.GetOrCompute(envelopeContainer.Recipient.Path).Address; var serializedMessage = envelopeContainer.Message; IActorRef senderOption = null; if (envelopeContainer.Sender != null) - senderOption = provider.ResolveActorRefWithLocalAddress(envelopeContainer.Sender.Path, localAddress); + senderOption = provider.ResolveActorRefWithLocalAddress(envelopeContainer.Sender.Path, localAddress, true); SeqNo seqOption = null; if (envelopeContainer.Seq != SeqUndefined) @@ -476,7 +479,16 @@ public override ByteString ConstructMessage(Address localAddress, IActorRef reci { var ackAndEnvelope = new AckAndEnvelopeContainer(); var envelope = new RemoteEnvelope() { Recipient = SerializeActorRef(recipient.Path.Address, recipient) }; - if (senderOption != null && senderOption.Path != null) { envelope.Sender = SerializeActorRef(localAddress, senderOption); } + if (senderOption != null && senderOption.Path != null) + { + envelope.Sender = SerializeActorRef(localAddress, senderOption); + if (senderOption is FutureActorRef) + { + AskRefCache.Cache.Set(envelope.Sender.Path, senderOption); + AskPathCache.Cache.Set(envelope.Sender.Path, senderOption.Path); + } + } + if (seqOption != null) { envelope.Seq = (ulong)seqOption.RawValue; } else envelope.Seq = SeqUndefined; if (ackOption != null) { ackAndEnvelope.Ack = AckBuilder(ackOption); } envelope.Message = serializedMessage; diff --git a/src/core/Akka/Actor/ActorRef.cs b/src/core/Akka/Actor/ActorRef.cs index 124bd9ceaa4..266c66f6a9e 100644 --- a/src/core/Akka/Actor/ActorRef.cs +++ b/src/core/Akka/Actor/ActorRef.cs @@ -64,12 +64,17 @@ public interface IRepointableRef : IActorRefScope bool IsStarted { get; } } + public abstract class FutureActorRef : MinimalActorRef + { + + } + /// /// INTERNAL API. /// /// ActorRef implementation used for one-off tasks. /// - public sealed class FutureActorRef : MinimalActorRef + public sealed class FutureActorRef : FutureActorRef { private readonly TaskCompletionSource _result; private readonly ActorPath _path; From 0fbcfa72506126c9e9630c9d79ea554edaf61572 Mon Sep 17 00:00:00 2001 From: Andrew El Date: Thu, 23 Sep 2021 18:46:56 -0400 Subject: [PATCH 35/40] customized --- .../Remoting/LruBoundedCacheBenchmarks.cs | 4 +- .../Akka.Remote/RemoteActorRefProvider.cs | 47 +- .../Serialization/ActorPathCache.cs | 18 +- .../Serialization/ActorRefResolveCache.cs | 6 +- .../BitFasterBased/LruWithOptimizedRemove.cs | 539 ++++++++++++++++++ src/core/Akka/Actor/ActorPath.cs | 17 +- 6 files changed, 595 insertions(+), 36 deletions(-) create mode 100644 src/core/Akka.Remote/Serialization/BitFasterBased/LruWithOptimizedRemove.cs diff --git a/src/benchmark/Akka.Benchmarks/Remoting/LruBoundedCacheBenchmarks.cs b/src/benchmark/Akka.Benchmarks/Remoting/LruBoundedCacheBenchmarks.cs index d01ca7eda69..b34741117a7 100644 --- a/src/benchmark/Akka.Benchmarks/Remoting/LruBoundedCacheBenchmarks.cs +++ b/src/benchmark/Akka.Benchmarks/Remoting/LruBoundedCacheBenchmarks.cs @@ -90,13 +90,13 @@ public void AddressHitBenchmark() [Benchmark] public void ActorPathCacheHitBenchmark() { - _pathCache.Cache.GetOrCompute(_cacheHitPath); + _pathCache.Cache.GetOrCompute(_cacheHitPath, false); } [Benchmark] public void ActorPathCacheMissBenchmark() { - _pathCache.Cache.GetOrCompute(_cacheMissPath); + _pathCache.Cache.GetOrCompute(_cacheMissPath, false); } [GlobalCleanup] diff --git a/src/core/Akka.Remote/RemoteActorRefProvider.cs b/src/core/Akka.Remote/RemoteActorRefProvider.cs index f45874dd976..6d1db76d16f 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -478,25 +478,17 @@ public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address lo return InternalDeadLetters; } + bool mayBeTempActor = path.Contains("/temp/"); ActorPath actorPath = null; if (_actorPathThreadLocalCache != null && _actorPathAskResolverCache != null) { if (senderOption.HasValue && senderOption.Value == true) { - actorPath = _actorPathThreadLocalCache.Cache.GetOrCompute(path); + actorPath = _actorPathThreadLocalCache.Cache.GetOrCompute(path, mayBeTempActor); } else { - if (path.Contains("/temp/") == true) - { - actorPath = - _actorPathAskResolverCache.Cache.GetOrNull(path); - } - if (actorPath == null) - { - actorPath = - _actorPathThreadLocalCache.Cache.GetOrCompute(path); - } + actorPath = ExtractRecipientActorPath(path, mayBeTempActor); } } else // cache not initialized yet @@ -512,7 +504,25 @@ public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address lo if (actorPath is RootActorPath) return RootGuardian; - return (IInternalActorRef)ResolveActorRef(path); // so we can use caching + return (IInternalActorRef)ResolveActorRefOpt(path, mayBeTempActor); // so we can use caching + } + + private ActorPath ExtractRecipientActorPath(string path, bool mayBeTempActor) + { + ActorPath actorPath = null; + if (mayBeTempActor) + { + actorPath = + _actorPathAskResolverCache.Cache.GetOrNull(path); + } + + if (actorPath == null) + { + actorPath = + _actorPathThreadLocalCache.Cache.GetOrCompute(path, mayBeTempActor); + } + + return actorPath; } @@ -547,6 +557,11 @@ protected virtual IInternalActorRef CreateRemoteRef(Props props, IInternalActorR /// The path of the actor we are attempting to resolve. /// A local if it exists, otherwise. public IActorRef ResolveActorRef(string path) + { + return ResolveActorRefOpt(path, true); + } + + private IActorRef ResolveActorRefOpt(string path, bool checkAsk) { if (IgnoreActorRef.IsIgnoreRefPath(path)) return IgnoreRef; @@ -559,8 +574,12 @@ public IActorRef ResolveActorRef(string path) return InternalResolveActorRef(path); } - var ask = _actorRefResolveAskCache.Cache.GetOrNull(path); - return ask??_actorRefResolveThreadLocalCache.Cache.GetOrCompute(path); + IActorRef actorRef = null; + if (checkAsk) + { + actorRef = _actorRefResolveAskCache.Cache.GetOrNull(path); + } + return actorRef??_actorRefResolveThreadLocalCache.Cache.GetOrCompute(path); } /// diff --git a/src/core/Akka.Remote/Serialization/ActorPathCache.cs b/src/core/Akka.Remote/Serialization/ActorPathCache.cs index 3898a6eff37..be777e2eb55 100644 --- a/src/core/Akka.Remote/Serialization/ActorPathCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorPathCache.cs @@ -9,7 +9,7 @@ using Akka.Actor; using System.Threading; using System.Collections.Generic; -using BitFaster.Caching.Lru; +using Akka.Remote.Serialization.BitFasterBased; namespace Akka.Remote.Serialization { @@ -72,22 +72,22 @@ public ActorPath GetOrNull(string actorPath) public void Set(string actorPath, ActorPath actorPathObj) { - _cache.AddOrUpdate(actorPath,actorPathObj); + _cache.TryAdd(actorPath,actorPathObj); } } internal sealed class ActorPathBitfasterCache { - public FastConcurrentLru _cache = + public readonly FastConcurrentLru _cache = new FastConcurrentLru(Environment.ProcessorCount, 1030, FastHashComparer.Default); - public FastConcurrentLru _rootCache = + public readonly FastConcurrentLru _rootCache = new FastConcurrentLru(Environment.ProcessorCount, - 1030, FastHashComparer.Default); - public ActorPath GetOrCompute(string k) + 540, FastHashComparer.Default); + public ActorPath GetOrCompute(string k, bool mayBeTempActor) { - if (k.Contains("/temp/")) + if (mayBeTempActor) { return ParsePath(k); } @@ -99,7 +99,7 @@ public ActorPath GetOrCompute(string k) outPath = ParsePath(k); if (outPath != null) { - _cache.AddOrUpdate(k,outPath); + _cache.TryAdd(k,outPath); } return outPath; @@ -138,7 +138,7 @@ private ActorPath ParsePath(string k) return null; actorPath = new RootActorPath(address); - _rootCache.AddOrUpdate(rootPath, actorPath); + _rootCache.TryAdd(rootPath, actorPath); } if (!ActorPath.TryParse(actorPath, absoluteUri, out actorPath)) diff --git a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs index 1ba8d1dd913..28116df2894 100644 --- a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs @@ -9,8 +9,8 @@ using System.Collections.Generic; using System.Threading; using Akka.Actor; +using Akka.Remote.Serialization.BitFasterBased; using Akka.Util.Internal; -using BitFaster.Caching.Lru; namespace Akka.Remote.Serialization { @@ -95,7 +95,7 @@ public IActorRef GetOrNull(string actorPath) public void Set(string actorPath, IActorRef actorRef) { - _cache.AddOrUpdate(actorPath,actorRef); + _cache.TryAdd(actorPath,actorRef); } } @@ -121,7 +121,7 @@ public IActorRef GetOrCompute(string k) outRef= _provider.InternalResolveActorRef(k); if (!(outRef is MinimalActorRef && !(outRef is FunctionRef))) { - _cache.AddOrUpdate(k, outRef); + _cache.TryAdd(k, outRef); } return outRef; diff --git a/src/core/Akka.Remote/Serialization/BitFasterBased/LruWithOptimizedRemove.cs b/src/core/Akka.Remote/Serialization/BitFasterBased/LruWithOptimizedRemove.cs new file mode 100644 index 00000000000..393ad75f3da --- /dev/null +++ b/src/core/Akka.Remote/Serialization/BitFasterBased/LruWithOptimizedRemove.cs @@ -0,0 +1,539 @@ +// //----------------------------------------------------------------------- +// // +// // Copyright (C) 2009-2021 Lightbend Inc. +// // Copyright (C) 2013-2021 .NET Foundation +// // +// //----------------------------------------------------------------------- +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BitFaster.Caching; +using BitFaster.Caching.Lru; + +namespace Akka.Remote.Serialization.BitFasterBased +{ + /// + public sealed class FastConcurrentLru : TemplateConcurrentLru, LruPolicy, NullHitCounter> + { + /// + /// Initializes a new instance of the FastConcurrentLru class with the specified capacity that has the default + /// concurrency level, and uses the default comparer for the key type. + /// + /// The maximum number of elements that the FastConcurrentLru can contain. + public FastConcurrentLru(int capacity) + : base(Environment.ProcessorCount, capacity, EqualityComparer.Default, new LruPolicy(), new NullHitCounter()) + { + } + + /// + /// Initializes a new instance of the FastConcurrentLru class that has the specified concurrency level, has the + /// specified initial capacity, and uses the specified IEqualityComparer. + /// + /// The estimated number of threads that will update the FastConcurrentLru concurrently. + /// The maximum number of elements that the FastConcurrentLru can contain. + /// The IEqualityComparer implementation to use when comparing keys. + public FastConcurrentLru(int concurrencyLevel, int capacity, IEqualityComparer comparer) + : base(concurrencyLevel, capacity, comparer, new LruPolicy(), new NullHitCounter()) + { + } + } + /// + /// Pseudo LRU implementation where LRU list is composed of 3 segments: hot, warm and cold. Cost of maintaining + /// segments is amortized across requests. Items are only cycled when capacity is exceeded. Pure read does + /// not cycle items if all segments are within capacity constraints. + /// There are no global locks. On cache miss, a new item is added. Tail items in each segment are dequeued, + /// examined, and are either enqueued or discarded. + /// This scheme of hot, warm and cold is based on the implementation used in MemCached described online here: + /// https://memcached.org/blog/modern-lru/ + /// + /// + /// Each segment has a capacity. When segment capacity is exceeded, items are moved as follows: + /// 1. New items are added to hot, WasAccessed = false + /// 2. When items are accessed, update WasAccessed = true + /// 3. When items are moved WasAccessed is set to false. + /// 4. When hot is full, hot tail is moved to either Warm or Cold depending on WasAccessed. + /// 5. When warm is full, warm tail is moved to warm head or cold depending on WasAccessed. + /// 6. When cold is full, cold tail is moved to warm head or removed from dictionary on depending on WasAccessed. + /// + public class TemplateConcurrentLru : ICache + where I : LruItem + where P : struct, IPolicy + where H : struct, IHitCounter + { + private readonly ConcurrentDictionary dictionary; + + private readonly ConcurrentQueue hotQueue; + private readonly ConcurrentQueue warmQueue; + private readonly ConcurrentQueue coldQueue; + + // maintain count outside ConcurrentQueue, since ConcurrentQueue.Count holds a global lock + private int hotCount; + private int warmCount; + private int coldCount; + + private readonly int hotCapacity; + private readonly int warmCapacity; + private readonly int coldCapacity; + + private readonly P policy; + + // Since H is a struct, making it readonly will force the runtime to make defensive copies + // if mutate methods are called. Therefore, field must be mutable to maintain count. + protected H hitCounter; + + public TemplateConcurrentLru( + int concurrencyLevel, + int capacity, + IEqualityComparer comparer, + P itemPolicy, + H hitCounter) + { + if (capacity < 3) + { + throw new ArgumentOutOfRangeException("Capacity must be greater than or equal to 3."); + } + + if (comparer == null) + { + throw new ArgumentNullException(nameof(comparer)); + } + + var queueCapacity = ComputeQueueCapacity(capacity); + this.hotCapacity = queueCapacity.hot; + this.warmCapacity = queueCapacity.warm; + this.coldCapacity = queueCapacity.cold; + + this.hotQueue = new ConcurrentQueue(); + this.warmQueue = new ConcurrentQueue(); + this.coldQueue = new ConcurrentQueue(); + + int dictionaryCapacity = this.hotCapacity + this.warmCapacity + this.coldCapacity + 1; + + this.dictionary = new ConcurrentDictionary(concurrencyLevel, dictionaryCapacity, comparer); + this.policy = itemPolicy; + this.hitCounter = hitCounter; + } + + // No lock count: https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/ + public int Count => this.dictionary.Skip(0).Count(); + + public int HotCount => this.hotCount; + + public int WarmCount => this.warmCount; + + public int ColdCount => this.coldCount; + + /// + public bool TryGet(K key, out V value) + { + I item; + if (dictionary.TryGetValue(key, out item)) + { + return GetOrDiscard(item, out value); + } + + value = default(V); + this.hitCounter.IncrementMiss(); + return false; + } + + public bool TryPullLazy(K key, out V value) + { + if (this.dictionary.TryRemove(key, out var existing)) + { + bool retVal = GetOrDiscard(existing, out value); + existing.WasAccessed = false; + existing.WasRemoved = true; + + // serialize dispose (common case dispose not thread safe) + if (existing.Value is IDisposable) + { + lock (existing) + { + if (existing.Value is IDisposable d) + { + d.Dispose(); + } + } + } + + return retVal; + } + + value = default(V); + return false; + } + + // AggressiveInlining forces the JIT to inline policy.ShouldDiscard(). For LRU policy + // the first branch is completely eliminated due to JIT time constant propogation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool GetOrDiscard(I item, out V value) + { + if (this.policy.ShouldDiscard(item)) + { + this.Move(item, ItemDestination.Remove); + this.hitCounter.IncrementMiss(); + value = default(V); + return false; + } + + value = item.Value; + this.policy.Touch(item); + this.hitCounter.IncrementHit(); + return true; + } + + /// + public V GetOrAdd(K key, Func valueFactory) + { + if (this.TryGet(key, out var value)) + { + return value; + } + + // The value factory may be called concurrently for the same key, but the first write to the dictionary wins. + // This is identical logic in ConcurrentDictionary.GetOrAdd method. + var newItem = this.policy.CreateItem(key, valueFactory(key)); + + if (this.dictionary.TryAdd(key, newItem)) + { + this.hotQueue.Enqueue(newItem); + Interlocked.Increment(ref hotCount); + Cycle(); + return newItem.Value; + } + + return this.GetOrAdd(key, valueFactory); + } + + /// + public async Task GetOrAddAsync(K key, Func> valueFactory) + { + if (this.TryGet(key, out var value)) + { + return value; + } + + // The value factory may be called concurrently for the same key, but the first write to the dictionary wins. + // This is identical logic in ConcurrentDictionary.GetOrAdd method. + var newItem = this.policy.CreateItem(key, await valueFactory(key).ConfigureAwait(false)); + + if (this.dictionary.TryAdd(key, newItem)) + { + this.hotQueue.Enqueue(newItem); + Interlocked.Increment(ref hotCount); + Cycle(); + return newItem.Value; + } + + return await this.GetOrAddAsync(key, valueFactory).ConfigureAwait(false); + } + + + + /// + public bool TryRemove(K key) + { + if (this.dictionary.TryGetValue(key, out var existing)) + { + var kvp = new KeyValuePair(key, existing); + + // hidden atomic remove + // https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/ + if (((ICollection>)this.dictionary).Remove(kvp)) + { + // Mark as not accessed, it will later be cycled out of the queues because it can never be fetched + // from the dictionary. Note: Hot/Warm/Cold count will reflect the removed item until it is cycled + // from the queue. + existing.WasAccessed = false; + existing.WasRemoved = true; + + // serialize dispose (common case dispose not thread safe) + if (existing.Value is IDisposable) + { + lock (existing) + { + if (existing.Value is IDisposable d) + { + d.Dispose(); + } + } + } + + + return true; + } + + // it existed, but we couldn't remove - this means value was replaced afer the TryGetValue (a race), try again + return TryRemove(key); + } + + return false; + } + + /// + ///Note: Calling this method does not affect LRU order. + public bool TryUpdate(K key, V value) + { + if (this.dictionary.TryGetValue(key, out var existing)) + { + lock (existing) + { + if (!existing.WasRemoved) + { + V oldValue = existing.Value; + existing.Value = value; + + if (oldValue is IDisposable d) + { + d.Dispose(); + } + + return true; + } + } + } + + return false; + } + + public void TryAdd(K key, V value) + { + var newItem = this.policy.CreateItem(key, value); + if (this.dictionary.TryAdd(key, newItem)) + { + this.hotQueue.Enqueue(newItem); + Interlocked.Increment(ref hotCount); + Cycle(); + return; + } + } + + /// + ///Note: Updates to existing items do not affect LRU order. Added items are at the top of the LRU. + public void AddOrUpdate(K key, V value) + { + // first, try to update + if (this.TryUpdate(key, value)) + { + return; + } + + // then try add + var newItem = this.policy.CreateItem(key, value); + + if (this.dictionary.TryAdd(key, newItem)) + { + this.hotQueue.Enqueue(newItem); + Interlocked.Increment(ref hotCount); + Cycle(); + return; + } + + // if both update and add failed there was a race, try again + AddOrUpdate(key, value); + } + + /// + public void Clear() + { + // take a key snapshot + var keys = this.dictionary.Keys.ToList(); + + // remove all keys in the snapshot - this correctly handles disposable values + foreach (var key in keys) + { + TryRemove(key); + } + + // At this point, dictionary is empty but queues still hold references to all values. + // Cycle the queues to purge all refs. If any items were added during this process, + // it is possible they might be removed as part of CycleCold. However, the dictionary + // and queues will remain in a consistent state. + for (int i = 0; i < keys.Count; i++) + { + CycleHotUnchecked(); + CycleWarmUnchecked(); + CycleColdUnchecked(); + } + } + + private void Cycle() + { + // There will be races when queue count == queue capacity. Two threads may each dequeue items. + // This will prematurely free slots for the next caller. Each thread will still only cycle at most 5 items. + // Since TryDequeue is thread safe, only 1 thread can dequeue each item. Thus counts and queue state will always + // converge on correct over time. + CycleHot(); + + // Multi-threaded stress tests show that due to races, the warm and cold count can increase beyond capacity when + // hit rate is very high. Double cycle results in stable count under all conditions. When contention is low, + // secondary cycles have no effect. + CycleWarm(); + CycleWarm(); + CycleCold(); + CycleCold(); + } + + private void CycleHot() + { + if (this.hotCount > this.hotCapacity) + { + CycleHotUnchecked(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CycleHotUnchecked() + { + Interlocked.Decrement(ref this.hotCount); + + if (this.hotQueue.TryDequeue(out var item)) + { + var where = this.policy.RouteHot(item); + this.Move(item, where); + } + else + { + Interlocked.Increment(ref this.hotCount); + } + } + + private void CycleWarm() + { + if (this.warmCount > this.warmCapacity) + { + CycleWarmUnchecked(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CycleWarmUnchecked() + { + Interlocked.Decrement(ref this.warmCount); + + if (this.warmQueue.TryDequeue(out var item)) + { + var where = this.policy.RouteWarm(item); + + // When the warm queue is full, we allow an overflow of 1 item before redirecting warm items to cold. + // This only happens when hit rate is high, in which case we can consider all items relatively equal in + // terms of which was least recently used. + if (where == ItemDestination.Warm && this.warmCount <= this.warmCapacity) + { + this.Move(item, where); + } + else + { + this.Move(item, ItemDestination.Cold); + } + } + else + { + Interlocked.Increment(ref this.warmCount); + } + } + + private void CycleCold() + { + if (this.coldCount > this.coldCapacity) + { + CycleColdUnchecked(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CycleColdUnchecked() + { + Interlocked.Decrement(ref this.coldCount); + + if (this.coldQueue.TryDequeue(out var item)) + { + var where = this.policy.RouteCold(item); + + if (where == ItemDestination.Warm && this.warmCount <= this.warmCapacity) + { + this.Move(item, where); + } + else + { + this.Move(item, ItemDestination.Remove); + } + } + else + { + Interlocked.Increment(ref this.coldCount); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Move(I item, ItemDestination where) + { + item.WasAccessed = false; + if (item.WasRemoved) + return; + switch (where) + { + case ItemDestination.Warm: + this.warmQueue.Enqueue(item); + Interlocked.Increment(ref this.warmCount); + break; + case ItemDestination.Cold: + this.coldQueue.Enqueue(item); + Interlocked.Increment(ref this.coldCount); + break; + case ItemDestination.Remove: + + var kvp = new KeyValuePair(item.Key, item); + + // hidden atomic remove + // https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/ + if (((ICollection>)this.dictionary).Remove(kvp)) + { + item.WasRemoved = true; + + if (item.Value is IDisposable) + { + lock (item) + { + if (item.Value is IDisposable d) + { + d.Dispose(); + } + } + } + } + + break; + } + } + + private static (int hot, int warm, int cold) ComputeQueueCapacity(int capacity) + { + int hotCapacity = capacity / 3; + int warmCapacity = capacity / 3; + int coldCapacity = capacity / 3; + + int remainder = capacity % 3; + + switch (remainder) + { + case 1: + coldCapacity++; + break; + case 2: + hotCapacity++; + coldCapacity++; + break; + } + + return (hotCapacity, warmCapacity, coldCapacity); + } + } +} + diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 2b4d84900e9..9e72bb48753 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -525,7 +525,8 @@ public static bool TryParseAddress(string path, out Address address, out ReadOnl public static bool TryParseParts(ReadOnlySpan path, out ReadOnlySpan address, out ReadOnlySpan absoluteUri) { var firstAtPos = path.IndexOf(':'); - if (firstAtPos < 4 || 255 < firstAtPos) + if (firstAtPos < 4 || 255 < firstAtPos + || path.Length path, out ReadOnlySpan return false; } - if (path.Slice(firstAtPos + 1).StartsWith("//".AsSpan()) == false) - { - //missing double slash - address = default; - absoluteUri = path; - return false; - } + //if (path.Slice(firstAtPos + 1).StartsWith("//".AsSpan()) == false) + //{ + // //missing double slash + // address = default; + // absoluteUri = path; + // return false; + //} var nextSlash = path.Slice(firstAtPos + 3).IndexOf('/'); if (nextSlash == -1) From 3afd84da672f4baedb30d4770a3af9fc3efc6c66 Mon Sep 17 00:00:00 2001 From: Andrew El Date: Tue, 19 Oct 2021 08:46:06 -0400 Subject: [PATCH 36/40] spannetty --- src/core/Akka.Remote/Akka.Remote.csproj | 3 +- .../Transport/DotNetty/AkkaLoggingHandler.cs | 28 +++++++++++++------ .../Transport/DotNetty/BatchWriter.cs | 13 ++++++--- .../Transport/DotNetty/DotNettyTransport.cs | 9 +++--- .../Transport/DotNetty/TcpTransport.cs | 4 +-- 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/core/Akka.Remote/Akka.Remote.csproj b/src/core/Akka.Remote/Akka.Remote.csproj index a1879f09948..292db26b71f 100644 --- a/src/core/Akka.Remote/Akka.Remote.csproj +++ b/src/core/Akka.Remote/Akka.Remote.csproj @@ -13,8 +13,9 @@ - + + $(DefineConstants);RELEASE diff --git a/src/core/Akka.Remote/Transport/DotNetty/AkkaLoggingHandler.cs b/src/core/Akka.Remote/Transport/DotNetty/AkkaLoggingHandler.cs index d3b498b2ca0..6ad87a20528 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/AkkaLoggingHandler.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/AkkaLoggingHandler.cs @@ -14,6 +14,7 @@ using DotNetty.Buffers; using DotNetty.Common.Concurrency; using DotNetty.Transport.Channels; + using ILoggingAdapter = Akka.Event.ILoggingAdapter; namespace Akka.Remote.Transport.DotNetty @@ -81,22 +82,30 @@ public override Task ConnectAsync(IChannelHandlerContext ctx, EndPoint remoteAdd return ctx.ConnectAsync(remoteAddress, localAddress); } - public override Task DisconnectAsync(IChannelHandlerContext ctx) + + //public override Task DisconnectAsync(IChannelHandlerContext ctx) + public override void Disconnect(IChannelHandlerContext ctx, IPromise promise) { _log.Info("Channel {0} disconnect", ctx.Channel); - return ctx.DisconnectAsync(); + base.Disconnect(ctx,promise); + //return ctx.DisconnectAsync(); } - public override Task CloseAsync(IChannelHandlerContext ctx) + //public override Task CloseAsync(IChannelHandlerContext ctx) + public override void Close(IChannelHandlerContext ctx, IPromise promise) { _log.Info("Channel {0} close", ctx.Channel); - return ctx.CloseAsync(); + //return ctx.CloseAsync(); + base.Close(ctx, promise); } - public override Task DeregisterAsync(IChannelHandlerContext ctx) + //public override Task DeregisterAsync(IChannelHandlerContext ctx) + public override void Deregister(IChannelHandlerContext ctx, IPromise promise) { + _log.Debug("Channel {0} deregister", ctx.Channel); - return ctx.DeregisterAsync(); + base.Deregister(ctx, promise); + //return ctx.DeregisterAsync(); } public override void ChannelRead(IChannelHandlerContext ctx, object message) @@ -110,14 +119,17 @@ public override void ChannelRead(IChannelHandlerContext ctx, object message) ctx.FireChannelRead(message); } - public override Task WriteAsync(IChannelHandlerContext ctx, object message) + //public override Task WriteAsync(IChannelHandlerContext ctx, object message) + public override void Write(IChannelHandlerContext ctx, object message, IPromise promise) { + if (_log.IsDebugEnabled) { // have to force a .ToString() here otherwise the reference count on the buffer might be illegal _log.Debug("Channel {0} writing a message ({1}) of type [{2}]", ctx.Channel, message?.ToString(), message == null ? "NULL" : message.GetType().TypeQualifiedName()); } - return ctx.WriteAsync(message); + base.Write(ctx, message, promise); + //return ctx.WriteAsync(message); } public override void Flush(IChannelHandlerContext ctx) diff --git a/src/core/Akka.Remote/Transport/DotNetty/BatchWriter.cs b/src/core/Akka.Remote/Transport/DotNetty/BatchWriter.cs index 663d6a104ea..4616ba4fb03 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/BatchWriter.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/BatchWriter.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using DotNetty.Transport.Channels; using Akka.Configuration; +using DotNetty.Common.Concurrency; namespace Akka.Remote.Transport.DotNetty { @@ -133,18 +134,22 @@ public override void ExceptionCaught(IChannelHandlerContext context, Exception e context.FireExceptionCaught(exception); } - public override Task DisconnectAsync(IChannelHandlerContext context) + //public override Task DisconnectAsync(IChannelHandlerContext context) + public override void Disconnect(IChannelHandlerContext context, IPromise promise) { // Try to flush one last time if flushes are pending before disconnect the channel. ResetReadAndFlushIfNeeded(context); - return context.DisconnectAsync(); + //return context.DisconnectAsync(); + base.Disconnect(context, promise); } - public override Task CloseAsync(IChannelHandlerContext context) + //public override Task CloseAsync(IChannelHandlerContext context) + public override void Close(IChannelHandlerContext context, IPromise promise) { // Try to flush one last time if flushes are pending before disconnect the channel. ResetReadAndFlushIfNeeded(context); - return context.CloseAsync(); + context.CloseAsync(promise); + //return context.CloseAsync(); } public override void ChannelWritabilityChanged(IChannelHandlerContext context) diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs index ff2d79eca1c..d0d7a080d08 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs @@ -18,6 +18,7 @@ using Akka.Configuration; using Akka.Event; using Akka.Util; +using Akka.Util.Internal; using DotNetty.Buffers; using DotNetty.Codecs; using DotNetty.Common.Utilities; @@ -81,7 +82,7 @@ protected void Init(IChannel channel, IPEndPoint remoteSocketAddress, Address re { var listener = s.Result; RegisterListener(channel, listener, msg, remoteSocketAddress); - channel.Configuration.AutoRead = true; // turn reads back on + channel.Configuration.IsAutoRead = true; // turn reads back on }, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.NotOnCanceled | TaskContinuationOptions.NotOnFaulted); op = handle; } @@ -190,7 +191,7 @@ protected async Task NewServer(EndPoint listenAddress) // Block reads until a handler actor is registered // no incoming connections will be accepted until this value is reset // it's possible that the first incoming association might come in though - newServerChannel.Configuration.AutoRead = false; + newServerChannel.Configuration.IsAutoRead = false; ConnectionGroup.TryAdd(newServerChannel); ServerChannel = newServerChannel; @@ -206,7 +207,7 @@ protected async Task NewServer(EndPoint listenAddress) LocalAddress = addr; // resume accepting incoming connections #pragma warning disable 4014 // we WANT this task to run without waiting - AssociationListenerPromise.Task.ContinueWith(result => newServerChannel.Configuration.AutoRead = true, + AssociationListenerPromise.Task.ContinueWith(result => newServerChannel.Configuration.IsAutoRead = true, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion); #pragma warning restore 4014 @@ -230,7 +231,7 @@ protected async Task NewServer(EndPoint listenAddress) public override async Task Associate(Address remoteAddress) { - if (!ServerChannel.Open) + if (!ServerChannel.IsOpen) throw new ChannelException("Transport is not open"); return await AssociateInternal(remoteAddress).ConfigureAwait(false); diff --git a/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs b/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs index 37f71feb0df..9f300941d3b 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs @@ -115,7 +115,7 @@ public override void ChannelActive(IChannelHandlerContext context) void InitInbound(IChannel channel, IPEndPoint socketAddress, object msg) { // disable automatic reads - channel.Configuration.AutoRead = false; + channel.Configuration.IsAutoRead = false; _associationEventListener.ContinueWith(r => { @@ -169,7 +169,7 @@ public TcpAssociationHandle(Address localAddress, Address remoteAddress, DotNett public override bool Write(ByteString payload) { - if (_channel.Open) + if (_channel.IsOpen) { var data = ToByteBuffer(_channel, payload); _channel.WriteAndFlushAsync(data); From b37222f9362463655bc152446a3d481a7eea2204 Mon Sep 17 00:00:00 2001 From: Andrew El Date: Tue, 19 Oct 2021 08:46:27 -0400 Subject: [PATCH 37/40] localref cache wip stuff --- .../Remoting/LruBoundedCacheBenchmarks.cs | 4 +- .../Akka.Cluster.Benchmark.DotTrace.csproj | 2 +- .../Program.cs | 9 +- .../Sharding/ShardMessageRoutingBenchmarks.cs | 15 + .../Sharding/ShardingInfrastructure.cs | 5 +- .../Akka.Remote/Configuration/Remote.conf | 2 +- src/core/Akka.Remote/Endpoint.cs | 5 +- src/core/Akka.Remote/MessageSerializer.cs | 6 +- .../Akka.Remote/RemoteActorRefProvider.cs | 75 +++- src/core/Akka.Remote/RemoteTransport.cs | 4 +- src/core/Akka.Remote/Remoting.cs | 42 +- .../Serialization/ActorPathCache.cs | 21 +- .../BitFasterBased/LruWithOptimizedRemove.cs | 270 +++++++++--- .../Serialization/WrappedPayloadSupport.cs | 4 +- .../Akka.Remote/Transport/AkkaPduCodec.cs | 29 +- src/core/Akka/Actor/ActorCell.cs | 19 + src/core/Akka/Actor/ActorPath.cs | 41 +- src/core/Akka/Akka.csproj | 1 + src/core/Akka/Dispatch/Dispatchers.cs | 9 +- src/core/Akka/Dispatch/Mailbox.cs | 8 +- .../Helios.Concurrency.DedicatedThreadPool.cs | 397 +++++++++++++++++- src/core/Akka/Serialization/Serialization.cs | 76 +++- src/core/Akka/Util/FastLazy.cs | 28 +- src/core/Akka/Util/Reflection/TypeCache.cs | 2 +- src/core/Akka/Util/SpanHacks.cs | 21 + .../Samples.Akka.AspNetCore/Startup.cs | 71 ++++ 26 files changed, 993 insertions(+), 173 deletions(-) diff --git a/src/benchmark/Akka.Benchmarks/Remoting/LruBoundedCacheBenchmarks.cs b/src/benchmark/Akka.Benchmarks/Remoting/LruBoundedCacheBenchmarks.cs index b34741117a7..c0b9eb94e12 100644 --- a/src/benchmark/Akka.Benchmarks/Remoting/LruBoundedCacheBenchmarks.cs +++ b/src/benchmark/Akka.Benchmarks/Remoting/LruBoundedCacheBenchmarks.cs @@ -90,13 +90,13 @@ public void AddressHitBenchmark() [Benchmark] public void ActorPathCacheHitBenchmark() { - _pathCache.Cache.GetOrCompute(_cacheHitPath, false); + _pathCache.Cache.GetOrCompute(_cacheHitPath, out var isTemp); } [Benchmark] public void ActorPathCacheMissBenchmark() { - _pathCache.Cache.GetOrCompute(_cacheMissPath, false); + _pathCache.Cache.GetOrCompute(_cacheMissPath, out var isTemp); } [GlobalCleanup] diff --git a/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Akka.Cluster.Benchmark.DotTrace.csproj b/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Akka.Cluster.Benchmark.DotTrace.csproj index 5fbf0937e52..53da5b90db9 100644 --- a/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Akka.Cluster.Benchmark.DotTrace.csproj +++ b/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Akka.Cluster.Benchmark.DotTrace.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net5.0;netcoreapp3.1 diff --git a/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Program.cs b/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Program.cs index 5c27f824dbf..bd22daee304 100644 --- a/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Program.cs +++ b/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading.Tasks; using Akka.Cluster.Benchmarks.Sharding; using Akka.Cluster.Sharding; @@ -21,12 +22,14 @@ await Task.Run(async () => await container.Setup(); await runIters(20, container); await container.SingleRequestResponseToRemoteEntity(); + var sw = new Stopwatch(); MeasureProfiler.StartCollectingData(); - for (int i = 0; i < 10; i++) + for (int i = 0; i < 20; i++) { - Console.WriteLine($"Try {i}"); + sw.Restart(); + Console.WriteLine($"Try {i+1}"); await runIters(10000, container); - + Console.WriteLine($"Completed {i+1} in {sw.Elapsed.TotalSeconds:F2} seconds"); } MeasureProfiler.SaveData(); diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardMessageRoutingBenchmarks.cs b/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardMessageRoutingBenchmarks.cs index 6f647cf8066..baede264b2e 100644 --- a/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardMessageRoutingBenchmarks.cs +++ b/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardMessageRoutingBenchmarks.cs @@ -161,6 +161,21 @@ public async Task SingleRequestResponseToRemoteEntity() for (var i = 0; i < MsgCount; i++) await _shardRegion1.Ask(_messageToSys2); } + //[Benchmark] + public async Task DoubleRequestResponseToRemoteEntity() + { + for (var i = 0; i < MsgCount; i++) + { + Task[] tasks = new Task[8]; + for (int j = 0; j < tasks.Length; j++) + { + tasks[j] =_shardRegion1.Ask(_messageToSys2); + } + + await Task.WhenAll(tasks); + } + + } //[Benchmark] diff --git a/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardingInfrastructure.cs b/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardingInfrastructure.cs index 2903c38728c..bd34423c5e0 100644 --- a/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardingInfrastructure.cs +++ b/src/benchmark/Akka.Cluster.Benchmarks/Sharding/ShardingInfrastructure.cs @@ -5,6 +5,7 @@ // // // //----------------------------------------------------------------------- +using System; using System.Threading.Tasks; using Akka.Actor; using Akka.Cluster.Sharding; @@ -272,7 +273,9 @@ public static IActorRef StartShardRegion(ActorSystem system, string entityName = { var props = Props.Create(() => new ShardedEntityActor()); var sharding = ClusterSharding.Get(system); - return sharding.Start(entityName, s => props, ClusterShardingSettings.Create(system), + return sharding.Start(entityName, s => props, ClusterShardingSettings.Create(system) + .WithPassivateIdleAfter(TimeSpan.Zero) + , new ShardMessageExtractor()); } } diff --git a/src/core/Akka.Remote/Configuration/Remote.conf b/src/core/Akka.Remote/Configuration/Remote.conf index 246a6f1ddca..7d49db69284 100644 --- a/src/core/Akka.Remote/Configuration/Remote.conf +++ b/src/core/Akka.Remote/Configuration/Remote.conf @@ -581,7 +581,7 @@ akka { default-remote-dispatcher { executor = fork-join-executor fork-join-executor { - parallelism-min = 2 + parallelism-min = 4 parallelism-factor = 0.5 parallelism-max = 16 } diff --git a/src/core/Akka.Remote/Endpoint.cs b/src/core/Akka.Remote/Endpoint.cs index 7ae176874c7..730bcd707c2 100644 --- a/src/core/Akka.Remote/Endpoint.cs +++ b/src/core/Akka.Remote/Endpoint.cs @@ -1468,7 +1468,10 @@ private bool WriteSend(EndpointManager.Send send) } var pdu = _codec.ConstructMessage(send.Recipient.LocalAddressToUse, send.Recipient, - this.SerializeMessage(send.Message), send.SenderOption, send.Seq, _lastAck); + this.SerializeMessage(send.Message), send.SenderOption, send.Seq, _lastAck, + _provider + .RefAskCache() + ); _remoteMetrics.LogPayloadBytes(send.Message, pdu.Length); diff --git a/src/core/Akka.Remote/MessageSerializer.cs b/src/core/Akka.Remote/MessageSerializer.cs index e04d36c7e57..08989f4d0e3 100644 --- a/src/core/Akka.Remote/MessageSerializer.cs +++ b/src/core/Akka.Remote/MessageSerializer.cs @@ -52,10 +52,12 @@ public static SerializedMessage Serialize(ExtendedActorSystem system, Address ad if (oldInfo == null) Akka.Serialization.Serialization.CurrentTransportInformation = system.Provider.SerializationInformation; - + + var serializedMsg = new SerializedMessage { - Message = ByteString.CopyFrom(serializer.ToBinary(message)), + Message = UnsafeByteOperations.UnsafeWrap(serializer.ToBinary(message)), + //Message = ByteString.CopyFrom(serializer.ToBinary(message)), SerializerId = serializer.Identifier }; diff --git a/src/core/Akka.Remote/RemoteActorRefProvider.cs b/src/core/Akka.Remote/RemoteActorRefProvider.cs index 6d1db76d16f..c62fa1ed3b6 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -99,6 +99,41 @@ public interface IRemoteActorRefProvider : IActorRefProvider /// the current endpoint writer will be stopped (dropping system messages) and the address will be gated /// void Quarantine(Address address, int? uid); + + IActorRef RefAskCache(); + ActorRefAskResolverCache RefAskCacheInst(); + } + + public class RemoteAskCacheActor : ActorBase + { + private ActorRefAskResolverCache _cache; + + public RemoteAskCacheActor(ActorRefAskResolverCache cache) + { + _cache = cache; + } + protected override bool Receive(object message) + { + if (message is CacheAdd m) + { + try + { + _cache.Set(m.key, m.value); + } + catch + { + } + + return true; + } + return false; + } + } + + public class CacheAdd + { + public string key { get; set; } + public IActorRef value { get; set; } } /// @@ -241,14 +276,17 @@ public void UnregisterTempActor(ActorPath path) private ActorRefResolveThreadLocalCache _actorRefResolveThreadLocalCache; private ActorRefResolveAskCache _actorRefResolveAskCache; private ActorPathThreadLocalCache _actorPathThreadLocalCache; - private ActorPathAskResolverCache _actorPathAskResolverCache; + //private ActorPathAskResolverCache _actorPathAskResolverCache; /// /// The remote death watcher. /// public IActorRef RemoteWatcher => _remoteWatcher; private IActorRef _remoteDeploymentWatcher; + private IActorRef _remoteAskHandler; + public IActorRef RefAskCache() => _remoteAskHandler; + public ActorRefAskResolverCache RefAskCacheInst() => _actorRefResolveAskCache.Cache; /// public virtual void Init(ActorSystemImpl system) { @@ -257,7 +295,7 @@ public virtual void Init(ActorSystemImpl system) _actorRefResolveThreadLocalCache = ActorRefResolveThreadLocalCache.For(system); _actorPathThreadLocalCache = ActorPathThreadLocalCache.For(system); _actorRefResolveAskCache = ActorRefResolveAskCache.For(system); - _actorPathAskResolverCache = ActorPathAskResolverCache.For(system); + //_actorPathAskResolverCache = ActorPathAskResolverCache.For(system); _local.Init(system); _remotingTerminator = @@ -268,7 +306,10 @@ public virtual void Init(ActorSystemImpl system) _internals = CreateInternals(); _remotingTerminator.Tell(RemoteInternals); - + _remoteAskHandler = _system.SystemActorOf( + RemoteSettings.ConfigureDispatcher(Props.Create(() => + new RemoteAskCacheActor(_actorRefResolveAskCache.Cache))), + "remoting-ask-cache"); Transport.Start(); _remoteWatcher = CreateRemoteWatcher(system); _remoteDeploymentWatcher = CreateRemoteDeploymentWatcher(system); @@ -478,17 +519,18 @@ public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address lo return InternalDeadLetters; } - bool mayBeTempActor = path.Contains("/temp/"); + bool isTempActor = false; + //bool mayBeTempActor = path.Contains("/temp/"); ActorPath actorPath = null; - if (_actorPathThreadLocalCache != null && _actorPathAskResolverCache != null) + if (_actorPathThreadLocalCache != null && _actorRefResolveAskCache != null) { if (senderOption.HasValue && senderOption.Value == true) { - actorPath = _actorPathThreadLocalCache.Cache.GetOrCompute(path, mayBeTempActor); + actorPath = _actorPathThreadLocalCache.Cache.GetOrCompute(path, out isTempActor); } else { - actorPath = ExtractRecipientActorPath(path, mayBeTempActor); + actorPath = ExtractRecipientActorPath(path, out isTempActor); } } else // cache not initialized yet @@ -504,22 +546,29 @@ public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address lo if (actorPath is RootActorPath) return RootGuardian; - return (IInternalActorRef)ResolveActorRefOpt(path, mayBeTempActor); // so we can use caching + return (IInternalActorRef)ResolveActorRefOpt(path, isTempActor); // so we can use caching } - private ActorPath ExtractRecipientActorPath(string path, bool mayBeTempActor) + private ActorPath ExtractRecipientActorPath(string path, out bool mayBeTempActor) { ActorPath actorPath = null; - if (mayBeTempActor) + mayBeTempActor = false; + //if (mayBeTempActor) { - actorPath = - _actorPathAskResolverCache.Cache.GetOrNull(path); + var maybeRef = _actorRefResolveAskCache.Cache.GetOrNull(path); + if (maybeRef != null) + { + actorPath = maybeRef.Path; + mayBeTempActor = true; + } + //actorPath = + // _actorPathAskResolverCache.Cache.GetOrNull(path); } if (actorPath == null) { actorPath = - _actorPathThreadLocalCache.Cache.GetOrCompute(path, mayBeTempActor); + _actorPathThreadLocalCache.Cache.GetOrCompute(path, out mayBeTempActor); } return actorPath; diff --git a/src/core/Akka.Remote/RemoteTransport.cs b/src/core/Akka.Remote/RemoteTransport.cs index 2798544a919..f4e1bcd0e94 100644 --- a/src/core/Akka.Remote/RemoteTransport.cs +++ b/src/core/Akka.Remote/RemoteTransport.cs @@ -12,6 +12,7 @@ using Akka.Actor; using Akka.Annotations; using Akka.Event; +using LanguageExt; namespace Akka.Remote { @@ -51,7 +52,8 @@ protected RemoteTransport(ExtendedActorSystem system, RemoteActorRefProvider pro /// /// Addresses to be used in of refs generated for this transport. /// - public abstract ISet
Addresses { get; } + //public abstract ISet
Addresses { get; } + public abstract HashSet Addresses { get; } /// /// The default transport address of the . diff --git a/src/core/Akka.Remote/Remoting.cs b/src/core/Akka.Remote/Remoting.cs index 221c0763b5b..816b8b5b29e 100644 --- a/src/core/Akka.Remote/Remoting.cs +++ b/src/core/Akka.Remote/Remoting.cs @@ -16,6 +16,8 @@ using Akka.Remote.Transport; using Akka.Util.Internal; using Akka.Configuration; +using LanguageExt; +using LanguageExt.TypeClasses; namespace Akka.Remote { @@ -108,6 +110,20 @@ public static RARP For(ActorSystem system) /// internal interface IPriorityMessage { } + public struct AddressEq : Eq
+ { + public int GetHashCode(Address x) + { + return x.GetHashCode(); + } + + public bool Equals(Address x, Address y) + { + if (x != null) + return x.Equals(y); + return (y == null); + } + } internal sealed class AddressEqualityComparer : EqualityComparer
{ public static readonly AddressEqualityComparer Instance = @@ -130,12 +146,13 @@ public override int GetHashCode(Address obj) internal sealed class Remoting : RemoteTransport { private readonly ILoggingAdapter _log; - private volatile IDictionary> _transportMapping; + private volatile IDictionary> _transportMapping; private volatile IActorRef _endpointManager; // This is effectively a write-once variable similar to a lazy val. The reason for not using a lazy val is exception // handling. - private volatile HashSet
_addresses; + //private volatile HashSet
_addresses; + private LanguageExt.HashSet _addresses; // This variable has the same semantics as the addresses variable, in the sense it is written once, and emulates // a lazy val @@ -162,7 +179,8 @@ public Remoting(ExtendedActorSystem system, RemoteActorRefProvider provider) /// /// TBD /// - public override ISet
Addresses + public override HashSet Addresses + //public override ISet
Addresses { get { return _addresses; } } @@ -212,20 +230,26 @@ public override void Start() var akkaProtocolTransports = addressPromise.Task.Result; if(akkaProtocolTransports.Count==0) throw new ConfigurationException(@"No transports enabled under ""akka.remote.enabled-transports"""); - - _addresses = new HashSet
(akkaProtocolTransports.Select(a => a.Address), AddressEqualityComparer.Instance); + + _addresses = + _addresses.AddRange( + akkaProtocolTransports.Select(a => a.Address)); + //new System.Collections.Generic.HashSet
(akkaProtocolTransports.Select(a => a.Address), AddressEqualityComparer.Instance); IEnumerable> tmp = akkaProtocolTransports.GroupBy(t => t.ProtocolTransport.SchemeIdentifier); - _transportMapping = new Dictionary>(); + _transportMapping = new Dictionary>(); foreach (var g in tmp) { - var set = new HashSet(g); + var set = new System.Collections.Generic.HashSet(g); _transportMapping.Add(g.Key, set); } _defaultAddress = akkaProtocolTransports.Head().Address; - _addresses = new HashSet
(akkaProtocolTransports.Select(x => x.Address), AddressEqualityComparer.Instance); + _addresses = + new HashSet().AddRange( + akkaProtocolTransports.Select(x => x.Address)); + //_addresses = new System.Collections.Generic.HashSet
(akkaProtocolTransports.Select(x => x.Address), AddressEqualityComparer.Instance); _log.Info("Remoting started; listening on addresses : [{0}]", string.Join(",", _addresses.Select(x => x.ToString()))); @@ -392,7 +416,7 @@ private void NotifyError(string msg, Exception cause) /// TBD /// TBD internal static Address LocalAddressForRemote( - IDictionary> transportMapping, Address remote) + IDictionary> transportMapping, Address remote) { if (transportMapping.TryGetValue(remote.Protocol, out var transports)) { diff --git a/src/core/Akka.Remote/Serialization/ActorPathCache.cs b/src/core/Akka.Remote/Serialization/ActorPathCache.cs index be777e2eb55..2d2a2b30e6a 100644 --- a/src/core/Akka.Remote/Serialization/ActorPathCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorPathCache.cs @@ -85,23 +85,26 @@ internal sealed class ActorPathBitfasterCache public readonly FastConcurrentLru _rootCache = new FastConcurrentLru(Environment.ProcessorCount, 540, FastHashComparer.Default); - public ActorPath GetOrCompute(string k, bool mayBeTempActor) + public ActorPath GetOrCompute(string k, out bool isTempActor) { - if (mayBeTempActor) - { - return ParsePath(k); - } + //if (mayBeTempActor) + //{ + // return ParsePath(k); + //} if (_cache.TryGet(k, out ActorPath outPath)) { + isTempActor = true; return outPath; } outPath = ParsePath(k); - if (outPath != null) + + isTempActor = k.Contains("/temp/"); + if (outPath != null && !isTempActor) { + _cache.TryAdd(k,outPath); } - return outPath; } @@ -141,7 +144,7 @@ private ActorPath ParsePath(string k) _rootCache.TryAdd(rootPath, actorPath); } - if (!ActorPath.TryParse(actorPath, absoluteUri, out actorPath)) + if (!ActorPath.TryParse(actorPath, absoluteUri, out actorPath))//, out bool mayBeTemp)) return null; return actorPath; @@ -195,7 +198,7 @@ protected override ActorPath Compute(string k) TrySet(rootPath, actorPath); } - if (!ActorPath.TryParse(actorPath, absoluteUri, out actorPath)) + if (!ActorPath.TryParse(actorPath, absoluteUri, out actorPath))//, out bool isTemp)) return null; return actorPath; diff --git a/src/core/Akka.Remote/Serialization/BitFasterBased/LruWithOptimizedRemove.cs b/src/core/Akka.Remote/Serialization/BitFasterBased/LruWithOptimizedRemove.cs index 393ad75f3da..b0909c08de1 100644 --- a/src/core/Akka.Remote/Serialization/BitFasterBased/LruWithOptimizedRemove.cs +++ b/src/core/Akka.Remote/Serialization/BitFasterBased/LruWithOptimizedRemove.cs @@ -17,8 +17,137 @@ namespace Akka.Remote.Serialization.BitFasterBased { + public interface IPolicy where I : LruItem + { + I CreateItem(K key, V value); + + void Touch(I item); + + bool ShouldDiscard(I item); + + ItemDestination RouteHot(I item); + + ItemDestination RouteWarm(I item); + + ItemDestination RouteCold(I item); + } + /// + /// Discards the least recently used items first. + /// + public readonly struct LruPolicy : IPolicy> + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LruItem CreateItem(K key, V value) + { + return new LruItem(key, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Touch(LruItem item) + { + item.SetAccessed(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldDiscard(LruItem item) + { + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDestination RouteHot(LruItem item) + { + if (item.WasAccessed) + { + return ItemDestination.Warm; + } + + return ItemDestination.Cold; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDestination RouteWarm(LruItem item) + { + if (item.WasAccessed) + { + return ItemDestination.Warm; + } + + return ItemDestination.Cold; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDestination RouteCold(LruItem item) + { + if (item.WasAccessed) + { + return ItemDestination.Warm; + } + + return ItemDestination.Remove; + } + } + + [Flags] + public enum LruItemStatus + { + WasRemoved = 1, + WasAccessed = 2, + ShouldToss = 4 + } + public class LruItem + { + private volatile LruItemStatus _status; + //private volatile bool wasAccessed; + //private volatile bool wasRemoved; + + public LruItem(K k, V v) + { + this.Key = k; + this.Value = v; + } + + public readonly K Key; + + public V Value { get; set; } + + public bool WasAccessed + { + get => (this._status & LruItemStatus.WasAccessed) == LruItemStatus.WasAccessed; + //set => this._status = this._status & LruItemStatus.WasAccessed; + } + + public void SetAccessed() + { + this._status = this._status & LruItemStatus.WasAccessed; + } + + public void SetUnaccessed() + { + this._status = (this._status & (LruItemStatus)int.MaxValue - + (int)LruItemStatus.WasAccessed); + } + + public bool WasRemoved + { + get => (this._status & LruItemStatus.WasRemoved) == LruItemStatus.WasRemoved; + + } + + public void SetRemoved() + { + this._status = this._status & LruItemStatus.WasRemoved; + } + + public bool ShouldFastDiscard + { + get => (this._status & LruItemStatus.ShouldToss) == + LruItemStatus.ShouldToss; + set => this._status = this._status & LruItemStatus.ShouldToss; + } + } /// - public sealed class FastConcurrentLru : TemplateConcurrentLru, LruPolicy, NullHitCounter> + public sealed class FastConcurrentLru : TemplateConcurrentLru { /// /// Initializes a new instance of the FastConcurrentLru class with the specified capacity that has the default @@ -60,16 +189,14 @@ public FastConcurrentLru(int concurrencyLevel, int capacity, IEqualityComparer - public class TemplateConcurrentLru : ICache - where I : LruItem - where P : struct, IPolicy + public class TemplateConcurrentLru : ICache where H : struct, IHitCounter { - private readonly ConcurrentDictionary dictionary; + private readonly ConcurrentDictionary> dictionary; - private readonly ConcurrentQueue hotQueue; - private readonly ConcurrentQueue warmQueue; - private readonly ConcurrentQueue coldQueue; + private readonly ConcurrentQueue> hotQueue; + private readonly ConcurrentQueue> warmQueue; + private readonly ConcurrentQueue> coldQueue; // maintain count outside ConcurrentQueue, since ConcurrentQueue.Count holds a global lock private int hotCount; @@ -80,7 +207,7 @@ public class TemplateConcurrentLru : ICache private readonly int warmCapacity; private readonly int coldCapacity; - private readonly P policy; + private readonly LruPolicy policy; // Since H is a struct, making it readonly will force the runtime to make defensive copies // if mutate methods are called. Therefore, field must be mutable to maintain count. @@ -90,7 +217,7 @@ public TemplateConcurrentLru( int concurrencyLevel, int capacity, IEqualityComparer comparer, - P itemPolicy, + LruPolicy itemPolicy, H hitCounter) { if (capacity < 3) @@ -108,13 +235,13 @@ public TemplateConcurrentLru( this.warmCapacity = queueCapacity.warm; this.coldCapacity = queueCapacity.cold; - this.hotQueue = new ConcurrentQueue(); - this.warmQueue = new ConcurrentQueue(); - this.coldQueue = new ConcurrentQueue(); + this.hotQueue = new ConcurrentQueue>(); + this.warmQueue = new ConcurrentQueue>(); + this.coldQueue = new ConcurrentQueue>(); int dictionaryCapacity = this.hotCapacity + this.warmCapacity + this.coldCapacity + 1; - this.dictionary = new ConcurrentDictionary(concurrencyLevel, dictionaryCapacity, comparer); + this.dictionary = new ConcurrentDictionary>(concurrencyLevel, dictionaryCapacity, comparer); this.policy = itemPolicy; this.hitCounter = hitCounter; } @@ -131,7 +258,7 @@ public TemplateConcurrentLru( /// public bool TryGet(K key, out V value) { - I item; + LruItem item; if (dictionary.TryGetValue(key, out item)) { return GetOrDiscard(item, out value); @@ -144,23 +271,23 @@ public bool TryGet(K key, out V value) public bool TryPullLazy(K key, out V value) { - if (this.dictionary.TryRemove(key, out var existing)) + if (this.dictionary.TryGetValue(key, out var existing)) { bool retVal = GetOrDiscard(existing, out value); - existing.WasAccessed = false; - existing.WasRemoved = true; - + //existing.WasAccessed = false; + existing.SetRemoved(); + existing.ShouldFastDiscard = true; // serialize dispose (common case dispose not thread safe) - if (existing.Value is IDisposable) - { - lock (existing) - { - if (existing.Value is IDisposable d) - { - d.Dispose(); - } - } - } + //if (existing.Value is IDisposable) + //{ + // lock (existing) + // { + // if (existing.Value is IDisposable d) + // { + // d.Dispose(); + // } + // } + //} return retVal; } @@ -172,7 +299,7 @@ public bool TryPullLazy(K key, out V value) // AggressiveInlining forces the JIT to inline policy.ShouldDiscard(). For LRU policy // the first branch is completely eliminated due to JIT time constant propogation. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool GetOrDiscard(I item, out V value) + private bool GetOrDiscard(LruItem item, out V value) { if (this.policy.ShouldDiscard(item)) { @@ -241,29 +368,31 @@ public bool TryRemove(K key) { if (this.dictionary.TryGetValue(key, out var existing)) { - var kvp = new KeyValuePair(key, existing); + var kvp = new KeyValuePair>(key, existing); // hidden atomic remove // https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/ - if (((ICollection>)this.dictionary).Remove(kvp)) + if (((ICollection>>)this.dictionary).Remove(kvp)) { // Mark as not accessed, it will later be cycled out of the queues because it can never be fetched // from the dictionary. Note: Hot/Warm/Cold count will reflect the removed item until it is cycled // from the queue. - existing.WasAccessed = false; - existing.WasRemoved = true; + //existing.WasAccessed = false; + existing.SetUnaccessed(); + existing.SetRemoved(); + //existing.WasRemoved = true; // serialize dispose (common case dispose not thread safe) - if (existing.Value is IDisposable) - { - lock (existing) - { - if (existing.Value is IDisposable d) - { - d.Dispose(); - } - } - } + //if (existing.Value is IDisposable) + //{ + // lock (existing) + // { + // if (existing.Value is IDisposable d) + // { + // d.Dispose(); + // } + // } + //} return true; @@ -289,10 +418,10 @@ public bool TryUpdate(K key, V value) V oldValue = existing.Value; existing.Value = value; - if (oldValue is IDisposable d) - { - d.Dispose(); - } + //if (oldValue is IDisposable d) + //{ + // d.Dispose(); + //} return true; } @@ -472,9 +601,9 @@ private void CycleColdUnchecked() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Move(I item, ItemDestination where) + private void Move(LruItem item, ItemDestination where) { - item.WasAccessed = false; + item.SetUnaccessed(); if (item.WasRemoved) return; switch (where) @@ -488,25 +617,36 @@ private void Move(I item, ItemDestination where) Interlocked.Increment(ref this.coldCount); break; case ItemDestination.Remove: - - var kvp = new KeyValuePair(item.Key, item); - - // hidden atomic remove - // https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/ - if (((ICollection>)this.dictionary).Remove(kvp)) + if (item.ShouldFastDiscard == false) { - item.WasRemoved = true; - if (item.Value is IDisposable) + var kvp = + new KeyValuePair>(item.Key, item); + + // hidden atomic remove + // https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/ + if (((ICollection>>)this + .dictionary).Remove(kvp)) { - lock (item) - { - if (item.Value is IDisposable d) - { - d.Dispose(); - } - } + item.SetRemoved(); + //item.WasRemoved = true; + + //if (item.Value is IDisposable) + //{ + // lock (item) + // { + // if (item.Value is IDisposable d) + // { + // d.Dispose(); + // } + // } + //} } + + } + else + { + this.dictionary.TryRemove(item.Key, out var trash); } break; diff --git a/src/core/Akka.Remote/Serialization/WrappedPayloadSupport.cs b/src/core/Akka.Remote/Serialization/WrappedPayloadSupport.cs index 8d82daea92f..58c9116ecf9 100644 --- a/src/core/Akka.Remote/Serialization/WrappedPayloadSupport.cs +++ b/src/core/Akka.Remote/Serialization/WrappedPayloadSupport.cs @@ -28,8 +28,8 @@ public Proto.Msg.Payload PayloadToProto(object payload) var payloadProto = new Proto.Msg.Payload(); var serializer = _system.Serialization.FindSerializerFor(payload); - - payloadProto.Message = ByteString.CopyFrom(serializer.ToBinary(payload)); + payloadProto.Message = UnsafeByteOperations.UnsafeWrap(serializer.ToBinary(payload)); + //payloadProto.Message = ByteString.CopyFrom(serializer.ToBinary(payload)); payloadProto.SerializerId = serializer.Identifier; // get manifest diff --git a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs index 78a1e4af530..dfaa325fffd 100644 --- a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs +++ b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs @@ -204,13 +204,13 @@ internal abstract class AkkaPduCodec protected readonly ActorSystem System; protected readonly ActorPathThreadLocalCache ActorPathCache; protected readonly ActorRefResolveAskCache AskRefCache; - protected readonly ActorPathAskResolverCache AskPathCache; + //protected readonly ActorPathAskResolverCache AskPathCache; protected AkkaPduCodec(ActorSystem system) { System = system; ActorPathCache = ActorPathThreadLocalCache.For(system); AskRefCache = ActorRefResolveAskCache.For(system); - AskPathCache = ActorPathAskResolverCache.For(system); + //AskPathCache = ActorPathAskResolverCache.For(system); } /// @@ -289,9 +289,15 @@ public virtual ByteString EncodePdu(IAkkaPdu pdu) /// TBD /// TBD /// TBD + /// /// TBD - public abstract ByteString ConstructMessage(Address localAddress, IActorRef recipient, - SerializedMessage serializedMessage, IActorRef senderOption = null, SeqNo seqOption = null, Ack ackOption = null); + public abstract ByteString ConstructMessage(Address localAddress, + IActorRef recipient, + SerializedMessage serializedMessage, IActorRef senderOption = null, + SeqNo seqOption = null, Ack ackOption = null, + //IRemoteActorRefProvider provider = null + IActorRef refAskCache = null + ); /// /// TBD @@ -473,9 +479,15 @@ private AcknowledgementInfo AckBuilder(Ack ack) /// TBD /// TBD /// TBD + /// /// TBD - public override ByteString ConstructMessage(Address localAddress, IActorRef recipient, SerializedMessage serializedMessage, - IActorRef senderOption = null, SeqNo seqOption = null, Ack ackOption = null) + public override ByteString ConstructMessage(Address localAddress, + IActorRef recipient, SerializedMessage serializedMessage, + IActorRef senderOption = null, SeqNo seqOption = null, + Ack ackOption = null, + //IRemoteActorRefProvider provider = null + IActorRef refAskCache = null + ) { var ackAndEnvelope = new AckAndEnvelopeContainer(); var envelope = new RemoteEnvelope() { Recipient = SerializeActorRef(recipient.Path.Address, recipient) }; @@ -484,8 +496,9 @@ public override ByteString ConstructMessage(Address localAddress, IActorRef reci envelope.Sender = SerializeActorRef(localAddress, senderOption); if (senderOption is FutureActorRef) { - AskRefCache.Cache.Set(envelope.Sender.Path, senderOption); - AskPathCache.Cache.Set(envelope.Sender.Path, senderOption.Path); + //provider?.RefAskCacheInst().Set(envelope.Sender.Path, senderOption); + refAskCache.Tell(new CacheAdd(){key = envelope.Sender.Path, value = senderOption}); + //AskPathCache.Cache.Set(envelope.Sender.Path, senderOption.Path); } } diff --git a/src/core/Akka/Actor/ActorCell.cs b/src/core/Akka/Actor/ActorCell.cs index d8eabb6c149..00f6739745f 100644 --- a/src/core/Akka/Actor/ActorCell.cs +++ b/src/core/Akka/Actor/ActorCell.cs @@ -382,6 +382,25 @@ public void UseThreadContext(Action action) InternalCurrentActorCellKeeper.Current = tmp; } } + + /// + /// TBD + /// + /// TBD + public void UseThreadContext(Action action, T state) + { + var tmp = InternalCurrentActorCellKeeper.Current; + InternalCurrentActorCellKeeper.Current = this; + try + { + action(state); + } + finally + { + //ensure we set back the old context + InternalCurrentActorCellKeeper.Current = tmp; + } + } /// /// TBD diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index 9e72bb48753..d018cf30593 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -216,7 +216,6 @@ public IReadOnlyList Elements { if (_depth == 0) return ImmutableArray.Empty; - var b = ImmutableArray.CreateBuilder(_depth); b.Count = _depth; var p = this; @@ -265,12 +264,15 @@ internal IReadOnlyList ElementsWithUid /// public bool Equals(ActorPath other) { - if (other is null || _depth != other._depth) + if (other is null) return false; if (ReferenceEquals(this, other)) return true; - + if (_depth != other._depth) + { + return false; + } if (!Address.Equals(other.Address)) return false; @@ -421,7 +423,7 @@ public static bool TryParse(string path, out ActorPath actorPath) return false; } - return TryParse(new RootActorPath(address), absoluteUri, out actorPath); + return TryParse(new RootActorPath(address), absoluteUri, out actorPath);//, out bool isTemp); } /// @@ -434,7 +436,7 @@ public static bool TryParse(string path, out ActorPath actorPath) /// TBD public static bool TryParse(ActorPath basePath, string absoluteUri, out ActorPath actorPath) { - return TryParse(basePath, absoluteUri.AsSpan(), out actorPath); + return TryParse(basePath, absoluteUri.AsSpan(), out actorPath); //, out bool isTemp); } /// @@ -445,10 +447,12 @@ public static bool TryParse(ActorPath basePath, string absoluteUri, out ActorPat /// TBD /// TBD /// TBD - public static bool TryParse(ActorPath basePath, ReadOnlySpan absoluteUri, out ActorPath actorPath) + public static bool TryParse(ActorPath basePath, ReadOnlySpan absoluteUri, out ActorPath actorPath + ) + //, out bool isTemp) { actorPath = basePath; - + //isTemp = false; // check for Uri fragment here int nextSlash; @@ -457,16 +461,21 @@ public static bool TryParse(ActorPath basePath, ReadOnlySpan absoluteUri, nextSlash = absoluteUri.IndexOf('/'); if (nextSlash > 0) { - var name = absoluteUri.Slice(0, nextSlash).ToString(); - actorPath = new ChildActorPath(actorPath, name, ActorCell.UndefinedUid); + //var name = absoluteUri.Slice(0, nextSlash).ToString(); + actorPath = new ChildActorPath(actorPath, absoluteUri.Slice(0, nextSlash).ToString(), ActorCell.UndefinedUid); } else if (nextSlash < 0 && absoluteUri.Length > 0) // final segment { + //if (string.Equals(actorPath._name, "temp", + // StringComparison.OrdinalIgnoreCase)) + //{ + // isTemp = true; + //} var fragLoc = absoluteUri.IndexOf('#'); if (fragLoc > -1) { - var fragment = absoluteUri.Slice(fragLoc + 1); - var fragValue = SpanHacks.Parse(fragment); + //var fragment = absoluteUri.Slice(fragLoc + 1); + var fragValue = SpanHacks.Parse(absoluteUri.Slice(fragLoc + 1)); absoluteUri = absoluteUri.Slice(0, fragLoc); actorPath = new ChildActorPath(actorPath, absoluteUri.ToString(), fragValue); } @@ -588,11 +597,10 @@ private string Join(ReadOnlySpan prefix) prefix.CopyTo(buffer); var offset = buffer.Length; - ReadOnlySpan name; p = this; while (p._depth > 0) { - name = p._name.AsSpan(); + var name = p._name.AsSpan(); offset -= name.Length + 1; buffer[offset] = '/'; name.CopyTo(buffer.Slice(offset + 1, name.Length)); @@ -715,7 +723,12 @@ public string ToSerializationFormatWithAddress(Address address) private string AppendUidFragment(string withAddress) { - return _uid != ActorCell.UndefinedUid ? $"{withAddress}#{_uid}" : withAddress; + return _uid != ActorCell.UndefinedUid + ? + //string.Concat(withAddress,"#",Uid.ToString()) + string.Concat(withAddress, "#", Uid.ToString()) + //$"{withAddress}#{_uid}" + : withAddress; } /// diff --git a/src/core/Akka/Akka.csproj b/src/core/Akka/Akka.csproj index 148175866d3..4fcb3478bb7 100644 --- a/src/core/Akka/Akka.csproj +++ b/src/core/Akka/Akka.csproj @@ -15,6 +15,7 @@ + diff --git a/src/core/Akka/Dispatch/Dispatchers.cs b/src/core/Akka/Dispatch/Dispatchers.cs index 19ae34f9c10..37a9ce33b68 100644 --- a/src/core/Akka/Dispatch/Dispatchers.cs +++ b/src/core/Akka/Dispatch/Dispatchers.cs @@ -261,8 +261,13 @@ public ForkJoinExecutor(string id, DedicatedThreadPoolSettings poolSettings) : b public override void Execute(IRunnable run) { if (Volatile.Read(ref _shuttingDown) == 1) - throw new RejectedExecutionException("ForkJoinExecutor is shutting down"); - _dedicatedThreadPool.QueueUserWorkItem(run.Run); + ThrowShutdownHelper(); + _dedicatedThreadPool.QueueUserWorkItem(r=>r.Run(), run); + } + + private static void ThrowShutdownHelper() + { + throw new RejectedExecutionException("ForkJoinExecutor is shutting down"); } /// diff --git a/src/core/Akka/Dispatch/Mailbox.cs b/src/core/Akka/Dispatch/Mailbox.cs index e253187c76c..f77b5379f01 100644 --- a/src/core/Akka/Dispatch/Mailbox.cs +++ b/src/core/Akka/Dispatch/Mailbox.cs @@ -352,11 +352,11 @@ public void Run() { if (!IsClosed()) // Volatile read, needed here { - Actor.UseThreadContext(() => + Actor.UseThreadContext((state) => { - ProcessAllSystemMessages(); // First, deal with any system messages - ProcessMailbox(); // Then deal with messages - }); + state.ProcessAllSystemMessages(); // First, deal with any system messages + state.ProcessMailbox(); // Then deal with messages + },this); } } finally diff --git a/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs b/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs index 497e27af097..e1d553e482b 100644 --- a/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs +++ b/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs @@ -14,9 +14,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Akka.Dispatch; +using Akka.Util; namespace Helios.Concurrency { @@ -227,7 +230,7 @@ private void ReleaseWorker() private void RequestWorker() { - _pool.QueueUserWorkItem(() => + _pool.QueueUserWorkItem((t) => { // this thread is now available for inlining _currentThreadIsRunningTasks = true; @@ -257,7 +260,7 @@ private void RequestWorker() } // We're done processing items on the current thread finally { _currentThreadIsRunningTasks = false; } - }); + },null); } } @@ -299,12 +302,17 @@ public DedicatedThreadPool(DedicatedThreadPoolSettings settings) /// This exception is thrown if the given item is undefined. /// /// TBD - public bool QueueUserWorkItem(Action work) + public bool QueueUserWorkItem(Action work, IRunnable state) { if (work == null) - throw new ArgumentNullException(nameof(work), "Work item cannot be null."); + ThrowNullWorkHelper(work); - return _workQueue.TryAdd(work); + return _workQueue.TryAdd(work,state); + } + + private static void ThrowNullWorkHelper(Action work) + { + throw new ArgumentNullException(nameof(work), "Work item cannot be null."); } /// @@ -365,11 +373,11 @@ private void RunThread() { try { - foreach (var action in _pool._workQueue.GetConsumingEnumerable()) + foreach (var (action,state) in _pool._workQueue.GetConsumingEnumerable()) { try { - action(); + action(state); } catch (Exception ex) { @@ -393,8 +401,11 @@ private class ThreadPoolWorkQueue private static readonly int ProcessorCount = Environment.ProcessorCount; private const int CompletedState = 1; - private readonly ConcurrentQueue _queue = new ConcurrentQueue(); - private readonly UnfairSemaphore _semaphore = new UnfairSemaphore(); + private readonly ConcurrentQueue<(Action act, IRunnable state)> + _queue = new ConcurrentQueue<(Action act, IRunnable state)>(); + //private readonly ConcurrentQueue _queue = new ConcurrentQueue(); + //private readonly UnfairSemaphore _semaphore = new UnfairSemaphore(); + private readonly UnfairSemaphoreV2 _semaphore = new UnfairSemaphoreV2(); private int _outstandingRequests; private int _isAddingCompleted; @@ -403,7 +414,8 @@ public bool IsAddingCompleted get { return Volatile.Read(ref _isAddingCompleted) == CompletedState; } } - public bool TryAdd(Action work) + //public bool TryAdd(Action work) + public bool TryAdd(Action work, IRunnable state) { // If TryAdd returns true, it's guaranteed the work item will be executed. // If it returns false, it's also guaranteed the work item won't be executed. @@ -411,17 +423,17 @@ public bool TryAdd(Action work) if (IsAddingCompleted) return false; - _queue.Enqueue(work); + _queue.Enqueue((work,state)); EnsureThreadRequested(); return true; } - public IEnumerable GetConsumingEnumerable() + public IEnumerable<(Action action, IRunnable state)> GetConsumingEnumerable() { while (true) { - Action work; + (Actionaction,IRunnable state) work; if (_queue.TryDequeue(out work)) { yield return work; @@ -738,6 +750,365 @@ private SemaphoreState GetCurrentState() return state; } } + + [StructLayout(LayoutKind.Sequential)] + private sealed class UnfairSemaphoreV2 + { + public const int MaxWorker = 0x7FFF; + + private static readonly int ProcessorCount = Environment.ProcessorCount; + + // We track everything we care about in a single 64-bit struct to allow us to + // do CompareExchanges on this for atomic updates. + private struct SemaphoreStateV2 + { + private const byte CurrentSpinnerCountShift = 0; + private const byte CountForSpinnerCountShift = 16; + private const byte WaiterCountShift = 32; + private const byte CountForWaiterCountShift = 48; + + public long _data; + + private SemaphoreStateV2(ulong data) + { + unchecked + { + _data = (long)data; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddSpinners(ushort value) + { + Debug.Assert(value <= uint.MaxValue - Spinners); + unchecked + { + _data += (long)value << CurrentSpinnerCountShift; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void DecrSpinners(ushort value) + { + Debug.Assert(value >= ushort.MinValue + Spinners); + unchecked + { + _data -= (long)value << CurrentSpinnerCountShift; + } + } + + private uint GetUInt32Value(byte shift) => (uint)(_data >> shift); + private void SetUInt32Value(uint value, byte shift) + { + unchecked + { + _data = (_data & ~((long)uint.MaxValue << shift)) | ((long)value << shift); + } + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ushort GetUInt16Value(byte shift) => (ushort)(_data >> shift); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetUInt16Value(ushort value, byte shift) + { + unchecked + { + _data = (_data & ~((long)ushort.MaxValue << shift)) | ((long)value << shift); + } + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private byte GetByteValue(byte shift) => (byte)(_data >> shift); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetByteValue(byte value, byte shift) + { + unchecked + { + _data = (_data & ~((long)byte.MaxValue << shift)) | ((long)value << shift); + } + + } + + + //how many threads are currently spin-waiting for this semaphore? + public ushort Spinners + { + get { return GetUInt16Value(CurrentSpinnerCountShift); } + //set{SetUInt16Value(value,CurrentSpinnerCountShift);} + + } + + //how much of the semaphore's count is available to spinners? + //[FieldOffset(2)] + public ushort CountForSpinners + { + get { return GetUInt16Value(CountForSpinnerCountShift); } + //set{SetUInt16Value(value,CountForSpinnerCountShift);} + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void IncrementCountForSpinners(ushort count) + { + Debug.Assert(CountForSpinners+count < ushort.MaxValue); + unchecked + { + _data += (long)count << CountForSpinnerCountShift; + } + + } + + public void DecrementCountForSpinners() + { + Debug.Assert(CountForSpinners != 0); + unchecked + { + _data -= (long)1 << CountForSpinnerCountShift; + } + + } + + + //how many threads are blocked in the OS waiting for this semaphore? + + public ushort Waiters + { + get { return GetUInt16Value(WaiterCountShift); } + //set{SetUInt16Value(value,WaiterCountShift);} + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddWaiters(ushort value) + { + Debug.Assert(value <= uint.MaxValue - Waiters); + unchecked + { + _data += (long)value << WaiterCountShift; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void DecrWaiters(ushort value) + { + Debug.Assert(value >= ushort.MinValue + Waiters); + unchecked + { + _data -= (long)value << WaiterCountShift; + } + } + //how much count is available to waiters? + public ushort CountForWaiters + { + get { return GetUInt16Value(CountForWaiterCountShift); } + //set{SetUInt16Value(value,CountForWaiterCountShift);} + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void IncrCountForWaiters(ushort value) + { + Debug.Assert(value <= ushort.MaxValue + CountForWaiters); + unchecked + { + _data += (long)value << CountForWaiterCountShift; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void DecrCountForWaiters(ushort value) + { + Debug.Assert(value >= ushort.MinValue + CountForWaiters); + unchecked + { + _data -= (long)value << CountForWaiterCountShift; + } + } + } + + [StructLayout(LayoutKind.Explicit, Size = 64)] + private struct CacheLinePadding + { } + + private readonly Semaphore m_semaphore; + + // padding to ensure we get our own cache line +#pragma warning disable 169 + private readonly CacheLinePadding m_padding1; + private SemaphoreStateV2 m_state; + private readonly CacheLinePadding m_padding2; +#pragma warning restore 169 + + public UnfairSemaphoreV2() + { + m_semaphore = new Semaphore(0, short.MaxValue); + } + + public bool Wait() + { + return Wait(Timeout.InfiniteTimeSpan); + } + + public bool Wait(TimeSpan timeout) + { + while (true) + { + SemaphoreStateV2 currentCounts = GetCurrentState(); + SemaphoreStateV2 newCounts = currentCounts; + + // First, just try to grab some count. + if (currentCounts.CountForSpinners > 0) + { + newCounts.DecrementCountForSpinners(); + if (TryUpdateState(newCounts, currentCounts)) + return true; + } + else + { + // No count available, become a spinner + newCounts.AddSpinners(1); + if (TryUpdateState(newCounts, currentCounts)) + break; + } + } + + // + // Now we're a spinner. + // + int numSpins = 0; + const int spinLimitPerProcessor = 50; + while (true) + { + SemaphoreStateV2 currentCounts = GetCurrentState(); + SemaphoreStateV2 newCounts = currentCounts; + + if (currentCounts.CountForSpinners > 0) + { + newCounts.DecrementCountForSpinners(); + newCounts.DecrSpinners(1); + if (TryUpdateState(newCounts, currentCounts)) + return true; + } + else + { + double spinnersPerProcessor = (double)currentCounts.Spinners / ProcessorCount; + int spinLimit = (int)((spinLimitPerProcessor / spinnersPerProcessor) + 0.5); + if (numSpins >= spinLimit) + { + newCounts.DecrSpinners(1); + newCounts.AddWaiters(1); + if (TryUpdateState(newCounts, currentCounts)) + break; + } + else + { + // + // We yield to other threads using Thread.Sleep(0) rather than the more traditional Thread.Yield(). + // This is because Thread.Yield() does not yield to threads currently scheduled to run on other + // processors. On a 4-core machine, for example, this means that Thread.Yield() is only ~25% likely + // to yield to the correct thread in some scenarios. + // Thread.Sleep(0) has the disadvantage of not yielding to lower-priority threads. However, this is ok because + // once we've called this a few times we'll become a "waiter" and wait on the Semaphore, and that will + // yield to anything that is runnable. + // + Thread.Sleep(0); + numSpins++; + } + } + } + + // + // Now we're a waiter + // + bool waitSucceeded = m_semaphore.WaitOne(timeout); + + while (true) + { + SemaphoreStateV2 currentCounts = GetCurrentState(); + SemaphoreStateV2 newCounts = currentCounts; + + newCounts.DecrWaiters(1); + + if (waitSucceeded) + newCounts.DecrCountForWaiters(1); + + if (TryUpdateState(newCounts, currentCounts)) + return waitSucceeded; + } + } + + public void Release() + { + Release(1); + } + + public void Release(short count) + { + while (true) + { + SemaphoreStateV2 currentState = GetCurrentState(); + SemaphoreStateV2 newState = currentState; + + short remainingCount = count; + + // First, prefer to release existing spinners, + // because a) they're hot, and b) we don't need a kernel + // transition to release them. + short spinnersToRelease = Math.Max((short)0, Math.Min(remainingCount, (short)(currentState.Spinners - currentState.CountForSpinners))); + newState.IncrementCountForSpinners((ushort)spinnersToRelease);// .CountForSpinners = (ushort)(newState.CountForSpinners + spinnersToRelease); + remainingCount -= spinnersToRelease; + + // Next, prefer to release existing waiters + short waitersToRelease = Math.Max((short)0, Math.Min(remainingCount, (short)(currentState.Waiters - currentState.CountForWaiters))); + newState.IncrCountForWaiters((ushort)waitersToRelease);// .CountForWaiters = (ushort)(newState.CountForWaiters+ waitersToRelease); + remainingCount -= waitersToRelease; + + // Finally, release any future spinners that might come our way + newState.IncrementCountForSpinners((ushort)remainingCount); + + // Try to commit the transaction + if (TryUpdateState(newState, currentState)) + { + // Now we need to release the waiters we promised to release + if (waitersToRelease > 0) + m_semaphore.Release(waitersToRelease); + + break; + } + } + } + + private bool TryUpdateState(SemaphoreStateV2 newState, SemaphoreStateV2 currentState) + { + if (Interlocked.CompareExchange(ref m_state._data, newState._data, currentState._data) == currentState._data) + { + Debug.Assert(newState.CountForSpinners <= MaxWorker, "CountForSpinners is greater than MaxWorker"); + Debug.Assert(newState.CountForSpinners >= 0, "CountForSpinners is lower than zero"); + Debug.Assert(newState.Spinners <= MaxWorker, "Spinners is greater than MaxWorker"); + Debug.Assert(newState.Spinners >= 0, "Spinners is lower than zero"); + Debug.Assert(newState.CountForWaiters <= MaxWorker, "CountForWaiters is greater than MaxWorker"); + Debug.Assert(newState.CountForWaiters >= 0, "CountForWaiters is lower than zero"); + Debug.Assert(newState.Waiters <= MaxWorker, "Waiters is greater than MaxWorker"); + Debug.Assert(newState.Waiters >= 0, "Waiters is lower than zero"); + Debug.Assert(newState.CountForSpinners + newState.CountForWaiters <= MaxWorker, "CountForSpinners + CountForWaiters is greater than MaxWorker"); + + return true; + } + + return false; + } + + private SemaphoreStateV2 GetCurrentState() + { + // Volatile.Read of a long can get a partial read in x86 but the invalid + // state will be detected in TryUpdateState with the CompareExchange. + + SemaphoreStateV2 state = new SemaphoreStateV2(); + state._data = Volatile.Read(ref m_state._data); + return state; + } + } #endregion } diff --git a/src/core/Akka/Serialization/Serialization.cs b/src/core/Akka/Serialization/Serialization.cs index 41a2d708d00..eabf8f7c21f 100644 --- a/src/core/Akka/Serialization/Serialization.cs +++ b/src/core/Akka/Serialization/Serialization.cs @@ -19,6 +19,8 @@ using Akka.Util.Internal; using Akka.Util.Reflection; using Akka.Configuration; +using LanguageExt; +using LanguageExt.TypeClasses; namespace Akka.Serialization { @@ -142,8 +144,44 @@ public static T WithTransport(ActorSystem system, Address address, Func ac private readonly Serializer _nullSerializer; - private readonly ConcurrentDictionary _serializerMap = new ConcurrentDictionary(); - private readonly Dictionary _serializersById = new Dictionary(); + private class TypeEqualityComparer : IEqualityComparer + { + public static readonly TypeEqualityComparer Default = + new TypeEqualityComparer(); + + public bool Equals(Type x, Type y) + { + return x == y; + } + + public int GetHashCode(Type obj) + { + return obj.GetHashCode(); + } + } + private struct TypeEq : Eq + { + public int GetHashCode(Type x) + { + if (x is null) + { + return 0; + } + return x.GetHashCode(); + } + + public bool Equals(Type x, Type y) + { + return (x == y) ; + } + } + + //private HashMap _serializerMap = + // new HashMap(); + private readonly ConcurrentDictionary _serializerMap = new ConcurrentDictionary(TypeEqualityComparer.Default); + + private readonly Serializer[] _serializersById = new Serializer[1024]; + //private readonly Dictionary _serializersById = new Dictionary(); private readonly Dictionary _serializersByName = new Dictionary(); private readonly ImmutableHashSet _serializerDetails; @@ -326,7 +364,8 @@ private Serializer GetSerializerByName(string name) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddSerializer(Serializer serializer) { - _serializersById.Add(serializer.Identifier, serializer); + _serializersById[serializer.Identifier + 255] = serializer; + //_serializersById.Add(serializer.Identifier, serializer); } /// @@ -337,7 +376,8 @@ public void AddSerializer(Serializer serializer) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddSerializer(string name, Serializer serializer) { - _serializersById.Add(serializer.Identifier, serializer); + _serializersById[serializer.Identifier + 255] = serializer; + //_serializersById.Add(serializer.Identifier, serializer); _serializersByName.Add(name, serializer); } @@ -350,6 +390,7 @@ public void AddSerializer(string name, Serializer serializer) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddSerializationMap(Type type, Serializer serializer) { + //_serializerMap = _serializerMap.AddOrUpdate(type, serializer); _serializerMap[type] = serializer; } @@ -377,7 +418,9 @@ public object Deserialize(byte[] bytes, int serializerId, Type type) { return WithTransport(() => { - if (!_serializersById.TryGetValue(serializerId, out var serializer)) + var serializer = _serializersById[serializerId + 255]; + if (serializer == null) + //if (!_serializersById.TryGetValue(serializerId, out var serializer)) throw new SerializationException( $"Cannot find serializer with id [{serializerId}] (class [{type?.Name}]). The most probable reason" + " is that the configuration entry 'akka.actor.serializers' is not in sync between the two systems."); @@ -399,10 +442,10 @@ public object Deserialize(byte[] bytes, int serializerId, Type type) /// The resulting object public object Deserialize(byte[] bytes, int serializerId, string manifest) { - if (!_serializersById.TryGetValue(serializerId, out var serializer)) - throw new SerializationException( - $"Cannot find serializer with id [{serializerId}] (manifest [{manifest}]). The most probable reason" + - " is that the configuration entry 'akka.actor.serializers' is not in sync between the two systems."); + var serializer = _serializersById[serializerId + 255]; + if (serializer == null) + //if (!_serializersById.TryGetValue(serializerId, out var serializer)) + ThrowCannotFindSerializerHelper(serializerId, manifest); // not using `withTransportInformation { () =>` because deserializeByteBuffer is supposed to be the // possibility for allocation free serialization @@ -435,6 +478,14 @@ public object Deserialize(byte[] bytes, int serializerId, string manifest) } } + private static void ThrowCannotFindSerializerHelper(int serializerId, + string manifest) + { + throw new SerializationException( + $"Cannot find serializer with id [{serializerId}] (manifest [{manifest}]). The most probable reason" + + " is that the configuration entry 'akka.actor.serializers' is not in sync between the two systems."); + } + /// /// Returns the Serializer configured for the given object, returns the NullSerializer if it's null. /// @@ -464,6 +515,8 @@ public Serializer FindSerializerFor(object obj, string defaultSerializerName = n /// The serializer configured for the given object type public Serializer FindSerializerForType(Type objectType, string defaultSerializerName = null) { + //var fullMatchSerializer = _serializerMap.Find(objectType).IfNoneUnsafe((Serializer)null); + //if (fullMatchSerializer != null) if (_serializerMap.TryGetValue(objectType, out var fullMatchSerializer)) return fullMatchSerializer; @@ -486,6 +539,8 @@ public Serializer FindSerializerForType(Type objectType, string defaultSerialize // do a final check for the "object" serializer if (serializer == null) + //serializer = _serializerMap.Find(_objectType) + // .IfNoneUnsafe((Serializer)null); _serializerMap.TryGetValue(_objectType, out serializer); if (serializer == null) @@ -557,7 +612,8 @@ public static string SerializedActorPath(IActorRef actorRef) internal Serializer GetSerializerById(int serializerId) { - return _serializersById[serializerId]; + return _serializersById[serializerId+255]; + //return _serializersById[serializerId]; } } } diff --git a/src/core/Akka/Util/FastLazy.cs b/src/core/Akka/Util/FastLazy.cs index 3b234f65f36..4f28eb1fcf8 100644 --- a/src/core/Akka/Util/FastLazy.cs +++ b/src/core/Akka/Util/FastLazy.cs @@ -67,19 +67,25 @@ public T Value { if (IsValueCreatedInternal()) return _createdValue; - if (!IsValueCreationInProgress()) - { - Volatile.Write(ref _creating, 1); - _createdValue = _producer(); - Volatile.Write(ref _created, 1); - } - else - { - SpinWait.SpinUntil(IsValueCreatedInternal); - } - return _createdValue; + return ReturnCreation(); } } + + private T ReturnCreation() + { + if (!IsValueCreationInProgress()) + { + Volatile.Write(ref _creating, 1); + _createdValue = _producer(); + Volatile.Write(ref _created, 1); + } + else + { + SpinWait.SpinUntil(IsValueCreatedInternal); + } + + return _createdValue; + } } diff --git a/src/core/Akka/Util/Reflection/TypeCache.cs b/src/core/Akka/Util/Reflection/TypeCache.cs index a9715891f44..f871d420eae 100644 --- a/src/core/Akka/Util/Reflection/TypeCache.cs +++ b/src/core/Akka/Util/Reflection/TypeCache.cs @@ -32,7 +32,7 @@ public static class TypeCache /// TBD public static Type GetType(string typeName) { - return TypeMap.GetOrAdd(typeName, GetTypeInternal); + return TypeMap.GetOrAdd(typeName,(val)=>Type.GetType(val, true)); } private static Type GetTypeInternal(string typeName) diff --git a/src/core/Akka/Util/SpanHacks.cs b/src/core/Akka/Util/SpanHacks.cs index 08464610126..61a6a3506bf 100644 --- a/src/core/Akka/Util/SpanHacks.cs +++ b/src/core/Akka/Util/SpanHacks.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Text; namespace Akka.Util @@ -31,6 +32,26 @@ public static int Parse(ReadOnlySpan str) throw new FormatException($"[{str.ToString()}] is now a valid numeric format"); } + private static readonly char[] numStrings = new[] + { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' + }; + public static string ToString(long number) + { + return number.ToString(); + Span charArr = stackalloc char[19]; + int length = 1; + long curr = 0; + long theNum = number; + do + { + curr = theNum % 10; + charArr[19 - length] = numStrings[theNum % 10]; + theNum = theNum / 10; + } while (curr != 0); + + } + /// /// Parses an integer from a string. /// diff --git a/src/examples/AspNetCore/Samples.Akka.AspNetCore/Startup.cs b/src/examples/AspNetCore/Samples.Akka.AspNetCore/Startup.cs index e76a2feb220..77c84bb8add 100644 --- a/src/examples/AspNetCore/Samples.Akka.AspNetCore/Startup.cs +++ b/src/examples/AspNetCore/Samples.Akka.AspNetCore/Startup.cs @@ -20,6 +20,77 @@ namespace Samples.Akka.AspNetCore { + public interface IStartupTaskTokenContext : IStartupTaskContext + { + public string[] tokenChecks { get; } + } + public interface IStartupTaskContext + { + + public bool IsComplete { get; } + public int RetryAfterSeconds { get; } + } + + public sealed class + PathFilteringStartupTasksFilterMiddleWare : BaseStartupTasksFilterMiddleware + where T : IStartupTaskTokenContext + { + public PathFilteringStartupTasksFilterMiddleWare(T context, RequestDelegate next) : base(context, next) + { + } + public override bool shouldCheck(HttpContext context) + { + if (context.Request.Path.HasValue && hasPath(context.Request.Path)) + { + return true; + } + + return false; + } + public bool hasPath(string path) + { + var tokens = _context.tokenChecks; + if (tokens != null) + { + foreach (var token in tokens) + { + if (path.Contains(token, StringComparison.InvariantCultureIgnoreCase)) + return true; + } + } + return false; + } + } + public abstract class BaseStartupTasksFilterMiddleware where T: IStartupTaskContext + { + protected readonly T _context; + protected readonly RequestDelegate _next; + + public BaseStartupTasksFilterMiddleware(T context, RequestDelegate next) + { + _context = context; + _next = next; + } + + public abstract bool shouldCheck(HttpContext context); + + + public async Task Invoke(HttpContext httpContext) + { + if (shouldCheck(httpContext) == false || _context.IsComplete) + { + await _next(httpContext); + } + else + { + var response = httpContext.Response; + response.StatusCode = 503; + response.Headers["Retry-After"] = _context.RetryAfterSeconds.ToString(); + await response.WriteAsync("Service Unavailable"); + } + } + } + public class Startup { // This method gets called by the runtime. Use this method to add services to the container. From 417e670c6ef2fe7fab5952dfd8b6ae79138b3e1d Mon Sep 17 00:00:00 2001 From: Andrew El Date: Tue, 19 Oct 2021 12:23:50 -0400 Subject: [PATCH 38/40] Revert "spannetty" This reverts commit 3afd84da672f4baedb30d4770a3af9fc3efc6c66. --- src/core/Akka.Remote/Akka.Remote.csproj | 3 +- .../Transport/DotNetty/AkkaLoggingHandler.cs | 28 ++++++------------- .../Transport/DotNetty/BatchWriter.cs | 13 +++------ .../Transport/DotNetty/DotNettyTransport.cs | 9 +++--- .../Transport/DotNetty/TcpTransport.cs | 4 +-- 5 files changed, 19 insertions(+), 38 deletions(-) diff --git a/src/core/Akka.Remote/Akka.Remote.csproj b/src/core/Akka.Remote/Akka.Remote.csproj index 292db26b71f..a1879f09948 100644 --- a/src/core/Akka.Remote/Akka.Remote.csproj +++ b/src/core/Akka.Remote/Akka.Remote.csproj @@ -13,9 +13,8 @@ - + - $(DefineConstants);RELEASE diff --git a/src/core/Akka.Remote/Transport/DotNetty/AkkaLoggingHandler.cs b/src/core/Akka.Remote/Transport/DotNetty/AkkaLoggingHandler.cs index 6ad87a20528..d3b498b2ca0 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/AkkaLoggingHandler.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/AkkaLoggingHandler.cs @@ -14,7 +14,6 @@ using DotNetty.Buffers; using DotNetty.Common.Concurrency; using DotNetty.Transport.Channels; - using ILoggingAdapter = Akka.Event.ILoggingAdapter; namespace Akka.Remote.Transport.DotNetty @@ -82,30 +81,22 @@ public override Task ConnectAsync(IChannelHandlerContext ctx, EndPoint remoteAdd return ctx.ConnectAsync(remoteAddress, localAddress); } - - //public override Task DisconnectAsync(IChannelHandlerContext ctx) - public override void Disconnect(IChannelHandlerContext ctx, IPromise promise) + public override Task DisconnectAsync(IChannelHandlerContext ctx) { _log.Info("Channel {0} disconnect", ctx.Channel); - base.Disconnect(ctx,promise); - //return ctx.DisconnectAsync(); + return ctx.DisconnectAsync(); } - //public override Task CloseAsync(IChannelHandlerContext ctx) - public override void Close(IChannelHandlerContext ctx, IPromise promise) + public override Task CloseAsync(IChannelHandlerContext ctx) { _log.Info("Channel {0} close", ctx.Channel); - //return ctx.CloseAsync(); - base.Close(ctx, promise); + return ctx.CloseAsync(); } - //public override Task DeregisterAsync(IChannelHandlerContext ctx) - public override void Deregister(IChannelHandlerContext ctx, IPromise promise) + public override Task DeregisterAsync(IChannelHandlerContext ctx) { - _log.Debug("Channel {0} deregister", ctx.Channel); - base.Deregister(ctx, promise); - //return ctx.DeregisterAsync(); + return ctx.DeregisterAsync(); } public override void ChannelRead(IChannelHandlerContext ctx, object message) @@ -119,17 +110,14 @@ public override void ChannelRead(IChannelHandlerContext ctx, object message) ctx.FireChannelRead(message); } - //public override Task WriteAsync(IChannelHandlerContext ctx, object message) - public override void Write(IChannelHandlerContext ctx, object message, IPromise promise) + public override Task WriteAsync(IChannelHandlerContext ctx, object message) { - if (_log.IsDebugEnabled) { // have to force a .ToString() here otherwise the reference count on the buffer might be illegal _log.Debug("Channel {0} writing a message ({1}) of type [{2}]", ctx.Channel, message?.ToString(), message == null ? "NULL" : message.GetType().TypeQualifiedName()); } - base.Write(ctx, message, promise); - //return ctx.WriteAsync(message); + return ctx.WriteAsync(message); } public override void Flush(IChannelHandlerContext ctx) diff --git a/src/core/Akka.Remote/Transport/DotNetty/BatchWriter.cs b/src/core/Akka.Remote/Transport/DotNetty/BatchWriter.cs index 4616ba4fb03..663d6a104ea 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/BatchWriter.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/BatchWriter.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using DotNetty.Transport.Channels; using Akka.Configuration; -using DotNetty.Common.Concurrency; namespace Akka.Remote.Transport.DotNetty { @@ -134,22 +133,18 @@ public override void ExceptionCaught(IChannelHandlerContext context, Exception e context.FireExceptionCaught(exception); } - //public override Task DisconnectAsync(IChannelHandlerContext context) - public override void Disconnect(IChannelHandlerContext context, IPromise promise) + public override Task DisconnectAsync(IChannelHandlerContext context) { // Try to flush one last time if flushes are pending before disconnect the channel. ResetReadAndFlushIfNeeded(context); - //return context.DisconnectAsync(); - base.Disconnect(context, promise); + return context.DisconnectAsync(); } - //public override Task CloseAsync(IChannelHandlerContext context) - public override void Close(IChannelHandlerContext context, IPromise promise) + public override Task CloseAsync(IChannelHandlerContext context) { // Try to flush one last time if flushes are pending before disconnect the channel. ResetReadAndFlushIfNeeded(context); - context.CloseAsync(promise); - //return context.CloseAsync(); + return context.CloseAsync(); } public override void ChannelWritabilityChanged(IChannelHandlerContext context) diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs index d0d7a080d08..ff2d79eca1c 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs @@ -18,7 +18,6 @@ using Akka.Configuration; using Akka.Event; using Akka.Util; -using Akka.Util.Internal; using DotNetty.Buffers; using DotNetty.Codecs; using DotNetty.Common.Utilities; @@ -82,7 +81,7 @@ protected void Init(IChannel channel, IPEndPoint remoteSocketAddress, Address re { var listener = s.Result; RegisterListener(channel, listener, msg, remoteSocketAddress); - channel.Configuration.IsAutoRead = true; // turn reads back on + channel.Configuration.AutoRead = true; // turn reads back on }, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.NotOnCanceled | TaskContinuationOptions.NotOnFaulted); op = handle; } @@ -191,7 +190,7 @@ protected async Task NewServer(EndPoint listenAddress) // Block reads until a handler actor is registered // no incoming connections will be accepted until this value is reset // it's possible that the first incoming association might come in though - newServerChannel.Configuration.IsAutoRead = false; + newServerChannel.Configuration.AutoRead = false; ConnectionGroup.TryAdd(newServerChannel); ServerChannel = newServerChannel; @@ -207,7 +206,7 @@ protected async Task NewServer(EndPoint listenAddress) LocalAddress = addr; // resume accepting incoming connections #pragma warning disable 4014 // we WANT this task to run without waiting - AssociationListenerPromise.Task.ContinueWith(result => newServerChannel.Configuration.IsAutoRead = true, + AssociationListenerPromise.Task.ContinueWith(result => newServerChannel.Configuration.AutoRead = true, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion); #pragma warning restore 4014 @@ -231,7 +230,7 @@ protected async Task NewServer(EndPoint listenAddress) public override async Task Associate(Address remoteAddress) { - if (!ServerChannel.IsOpen) + if (!ServerChannel.Open) throw new ChannelException("Transport is not open"); return await AssociateInternal(remoteAddress).ConfigureAwait(false); diff --git a/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs b/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs index 9f300941d3b..37f71feb0df 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs @@ -115,7 +115,7 @@ public override void ChannelActive(IChannelHandlerContext context) void InitInbound(IChannel channel, IPEndPoint socketAddress, object msg) { // disable automatic reads - channel.Configuration.IsAutoRead = false; + channel.Configuration.AutoRead = false; _associationEventListener.ContinueWith(r => { @@ -169,7 +169,7 @@ public TcpAssociationHandle(Address localAddress, Address remoteAddress, DotNett public override bool Write(ByteString payload) { - if (_channel.IsOpen) + if (_channel.Open) { var data = ToByteBuffer(_channel, payload); _channel.WriteAndFlushAsync(data); From 63110d499b59ef5ed63e5ad8df6a607aa8a65dde Mon Sep 17 00:00:00 2001 From: Andrew El Date: Thu, 18 Nov 2021 18:18:48 -0500 Subject: [PATCH 39/40] wip threadpool --- .../Helios.Concurrency.DedicatedThreadPool.cs | 92 +++++++++++++------ 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs b/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs index e1d553e482b..b35d4ef3e76 100644 --- a/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs +++ b/src/core/Akka/Helios.Concurrency.DedicatedThreadPool.cs @@ -99,7 +99,7 @@ public DedicatedThreadPoolSettings( /// /// TaskScheduler for working with a instance /// - internal class DedicatedThreadPoolTaskScheduler : TaskScheduler + internal class DedicatedThreadPoolTaskScheduler : TaskScheduler, IRunnable { // Indicates whether the current thread is processing work items. [ThreadStatic] @@ -232,35 +232,68 @@ private void RequestWorker() { _pool.QueueUserWorkItem((t) => { - // this thread is now available for inlining - _currentThreadIsRunningTasks = true; - try + t.Run(); + //// this thread is now available for inlining + //_currentThreadIsRunningTasks = true; + //try + //{ + // // Process all available items in the queue. + // while (true) + // { + // Task item; + // lock (_tasks) + // { + // // done processing + // if (_tasks.Count == 0) + // { + // ReleaseWorker(); + // break; + // } + // + // // Get the next item from the queue + // item = _tasks.First.Value; + // _tasks.RemoveFirst(); + // } + // + // // Execute the task we pulled out of the queue + // TryExecuteTask(item); + // } + //} + //// We're done processing items on the current thread + //finally { _currentThreadIsRunningTasks = false; } + },this); + } + + void IRunnable.Run() + { + // this thread is now available for inlining + _currentThreadIsRunningTasks = true; + try + { + // Process all available items in the queue. + while (true) { - // Process all available items in the queue. - while (true) + Task item; + lock (_tasks) { - Task item; - lock (_tasks) + // done processing + if (_tasks.Count == 0) { - // done processing - if (_tasks.Count == 0) - { - ReleaseWorker(); - break; - } - - // Get the next item from the queue - item = _tasks.First.Value; - _tasks.RemoveFirst(); + ReleaseWorker(); + break; } - // Execute the task we pulled out of the queue - TryExecuteTask(item); + // Get the next item from the queue + item = _tasks.First.Value; + _tasks.RemoveFirst(); } + + // Execute the task we pulled out of the queue + TryExecuteTask(item); } - // We're done processing items on the current thread - finally { _currentThreadIsRunningTasks = false; } - },null); + } + // We're done processing items on the current thread + finally { _currentThreadIsRunningTasks = false; } } } @@ -767,6 +800,9 @@ private struct SemaphoreStateV2 private const byte WaiterCountShift = 32; private const byte CountForWaiterCountShift = 48; + //Ugh. So, Older versions of .NET, + //for whatever reason, don't have + //Interlocked compareexchange for ULong. public long _data; private SemaphoreStateV2(ulong data) @@ -872,12 +908,10 @@ public void DecrementCountForSpinners() //how many threads are blocked in the OS waiting for this semaphore? - public ushort Waiters { get { return GetUInt16Value(WaiterCountShift); } //set{SetUInt16Value(value,WaiterCountShift);} - } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -903,7 +937,6 @@ public void DecrWaiters(ushort value) public ushort CountForWaiters { get { return GetUInt16Value(CountForWaiterCountShift); } - //set{SetUInt16Value(value,CountForWaiterCountShift);} } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1050,17 +1083,18 @@ public void Release(short count) SemaphoreStateV2 currentState = GetCurrentState(); SemaphoreStateV2 newState = currentState; - short remainingCount = count; + ushort remainingCount = (ushort)count; // First, prefer to release existing spinners, // because a) they're hot, and b) we don't need a kernel // transition to release them. - short spinnersToRelease = Math.Max((short)0, Math.Min(remainingCount, (short)(currentState.Spinners - currentState.CountForSpinners))); + + ushort spinnersToRelease = (ushort)Math.Max((short)0, Math.Min(remainingCount, (short)(currentState.Spinners - currentState.CountForSpinners))); newState.IncrementCountForSpinners((ushort)spinnersToRelease);// .CountForSpinners = (ushort)(newState.CountForSpinners + spinnersToRelease); remainingCount -= spinnersToRelease; // Next, prefer to release existing waiters - short waitersToRelease = Math.Max((short)0, Math.Min(remainingCount, (short)(currentState.Waiters - currentState.CountForWaiters))); + ushort waitersToRelease = (ushort)Math.Max((short)0, Math.Min(remainingCount, (short)(currentState.Waiters - currentState.CountForWaiters))); newState.IncrCountForWaiters((ushort)waitersToRelease);// .CountForWaiters = (ushort)(newState.CountForWaiters+ waitersToRelease); remainingCount -= waitersToRelease; From 601a9909a68b8d3d19282986a043a739f2c45868 Mon Sep 17 00:00:00 2001 From: Andrew El Date: Thu, 18 Nov 2021 18:18:58 -0500 Subject: [PATCH 40/40] wip wat --- .../Remoting/FastHashBenchmarks.cs | 6 + src/benchmark/RemotePingPong/derp.cs | 375 ++++++++++++++++++ src/core/Akka.Remote/Akka.Remote.csproj | 1 + .../Serialization/LruBoundedCache.cs | 63 +++ .../Akka.Remote/Transport/AkkaPduCodec.cs | 353 ++++++++++++++++- .../Transport/DotNetty/DotNettyTransport.cs | 21 +- .../DotNetty/DotNettyTransportSettings.cs | 81 +++- src/core/Akka/Actor/ActorPath.cs | 20 +- .../Serialization/NewtonSoftJsonSerializer.cs | 2 + 9 files changed, 896 insertions(+), 26 deletions(-) create mode 100644 src/benchmark/RemotePingPong/derp.cs diff --git a/src/benchmark/Akka.Benchmarks/Remoting/FastHashBenchmarks.cs b/src/benchmark/Akka.Benchmarks/Remoting/FastHashBenchmarks.cs index 87d3155de53..ce82f216654 100644 --- a/src/benchmark/Akka.Benchmarks/Remoting/FastHashBenchmarks.cs +++ b/src/benchmark/Akka.Benchmarks/Remoting/FastHashBenchmarks.cs @@ -23,5 +23,11 @@ public int FastHash_OfStringUnsafe() { return FastHash.OfStringFast(HashKey1); } + + [Benchmark] + public int FastHash_Djb2() + { + return FastHash.GetDjb2HashCode(HashKey1); + } } } diff --git a/src/benchmark/RemotePingPong/derp.cs b/src/benchmark/RemotePingPong/derp.cs new file mode 100644 index 00000000000..f241ceedf9f --- /dev/null +++ b/src/benchmark/RemotePingPong/derp.cs @@ -0,0 +1,375 @@ +// //----------------------------------------------------------------------- +// // +// // Copyright (C) 2009-2021 Lightbend Inc. +// // Copyright (C) 2013-2021 .NET Foundation +// // +// //----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Blargh +{ + [StructLayout(LayoutKind.Sequential)] + public sealed class UnfairSemaphoreV2 + { + public const int MaxWorker = 0x7FFF; + + private static readonly int ProcessorCount = Environment.ProcessorCount; + + // We track everything we care about in a single 64-bit struct to allow us to + // do CompareExchanges on this for atomic updates. + private struct SemaphoreStateV2 + { + private const byte CurrentSpinnerCountShift = 0; + private const byte CountForSpinnerCountShift = 16; + private const byte WaiterCountShift = 32; + private const byte CountForWaiterCountShift = 48; + + //Ugh. So, Older versions of .NET, + //for whatever reason, don't have + //Interlocked compareexchange for ULong. + public long _data; + + private SemaphoreStateV2(ulong data) + { + unchecked + { + _data = (long)data; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddSpinners(ushort value) + { + Debug.Assert(value <= uint.MaxValue - Spinners); + unchecked + { + _data += (long)value << CurrentSpinnerCountShift; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void DecrSpinners(ushort value) + { + Debug.Assert(value >= ushort.MinValue + Spinners); + unchecked + { + _data -= (long)value << CurrentSpinnerCountShift; + } + } + + private uint GetUInt32Value(byte shift) => (uint)(_data >> shift); + private void SetUInt32Value(uint value, byte shift) + { + unchecked + { + _data = (_data & ~((long)uint.MaxValue << shift)) | ((long)value << shift); + } + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ushort GetUInt16Value(byte shift) => (ushort)(_data >> shift); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetUInt16Value(ushort value, byte shift) + { + unchecked + { + _data = (_data & ~((long)ushort.MaxValue << shift)) | ((long)value << shift); + } + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private byte GetByteValue(byte shift) => (byte)(_data >> shift); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetByteValue(byte value, byte shift) + { + unchecked + { + _data = (_data & ~((long)byte.MaxValue << shift)) | ((long)value << shift); + } + + } + + + //how many threads are currently spin-waiting for this semaphore? + public ushort Spinners + { + get { return GetUInt16Value(CurrentSpinnerCountShift); } + //set{SetUInt16Value(value,CurrentSpinnerCountShift);} + + } + + //how much of the semaphore's count is available to spinners? + //[FieldOffset(2)] + public ushort CountForSpinners + { + get { return GetUInt16Value(CountForSpinnerCountShift); } + //set{SetUInt16Value(value,CountForSpinnerCountShift);} + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void IncrementCountForSpinners(ushort count) + { + Debug.Assert(CountForSpinners+count < ushort.MaxValue); + unchecked + { + _data += (long)count << CountForSpinnerCountShift; + } + + } + + public void DecrementCountForSpinners() + { + Debug.Assert(CountForSpinners != 0); + unchecked + { + _data -= (long)1 << CountForSpinnerCountShift; + } + + } + + + //how many threads are blocked in the OS waiting for this semaphore? + public ushort Waiters + { + get { return GetUInt16Value(WaiterCountShift); } + //set{SetUInt16Value(value,WaiterCountShift);} + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddWaiters(ushort value) + { + Debug.Assert(value <= uint.MaxValue - Waiters); + unchecked + { + _data += (long)value << WaiterCountShift; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void DecrWaiters(ushort value) + { + Debug.Assert(value >= ushort.MinValue + Waiters); + unchecked + { + _data -= (long)value << WaiterCountShift; + } + } + //how much count is available to waiters? + public ushort CountForWaiters + { + get { return GetUInt16Value(CountForWaiterCountShift); } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void IncrCountForWaiters(ushort value) + { + Debug.Assert(value <= ushort.MaxValue + CountForWaiters); + unchecked + { + _data += (long)value << CountForWaiterCountShift; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void DecrCountForWaiters(ushort value) + { + Debug.Assert(value >= ushort.MinValue + CountForWaiters); + unchecked + { + _data -= (long)value << CountForWaiterCountShift; + } + } + } + + [StructLayout(LayoutKind.Explicit, Size = 64)] + private struct CacheLinePadding + { } + + private readonly Semaphore m_semaphore; + + // padding to ensure we get our own cache line +#pragma warning disable 169 + private readonly CacheLinePadding m_padding1; + private SemaphoreStateV2 m_state; + private readonly CacheLinePadding m_padding2; +#pragma warning restore 169 + + public UnfairSemaphoreV2() + { + m_semaphore = new Semaphore(0, short.MaxValue); + } + + public bool Wait() + { + return Wait(Timeout.InfiniteTimeSpan); + } + + public bool Wait(TimeSpan timeout) + { + while (true) + { + SemaphoreStateV2 currentCounts = GetCurrentState(); + SemaphoreStateV2 newCounts = currentCounts; + + // First, just try to grab some count. + if (currentCounts.CountForSpinners > 0) + { + newCounts.DecrementCountForSpinners(); + if (TryUpdateState(newCounts, currentCounts)) + return true; + } + else + { + // No count available, become a spinner + newCounts.AddSpinners(1); + if (TryUpdateState(newCounts, currentCounts)) + break; + } + } + + // + // Now we're a spinner. + // + int numSpins = 0; + const int spinLimitPerProcessor = 50; + while (true) + { + SemaphoreStateV2 currentCounts = GetCurrentState(); + SemaphoreStateV2 newCounts = currentCounts; + + if (currentCounts.CountForSpinners > 0) + { + newCounts.DecrementCountForSpinners(); + newCounts.DecrSpinners(1); + if (TryUpdateState(newCounts, currentCounts)) + return true; + } + else + { + double spinnersPerProcessor = (double)currentCounts.Spinners / ProcessorCount; + int spinLimit = (int)((spinLimitPerProcessor / spinnersPerProcessor) + 0.5); + if (numSpins >= spinLimit) + { + newCounts.DecrSpinners(1); + newCounts.AddWaiters(1); + if (TryUpdateState(newCounts, currentCounts)) + break; + } + else + { + // + // We yield to other threads using Thread.Sleep(0) rather than the more traditional Thread.Yield(). + // This is because Thread.Yield() does not yield to threads currently scheduled to run on other + // processors. On a 4-core machine, for example, this means that Thread.Yield() is only ~25% likely + // to yield to the correct thread in some scenarios. + // Thread.Sleep(0) has the disadvantage of not yielding to lower-priority threads. However, this is ok because + // once we've called this a few times we'll become a "waiter" and wait on the Semaphore, and that will + // yield to anything that is runnable. + // + Thread.Sleep(0); + numSpins++; + } + } + } + + // + // Now we're a waiter + // + bool waitSucceeded = m_semaphore.WaitOne(timeout); + + while (true) + { + SemaphoreStateV2 currentCounts = GetCurrentState(); + SemaphoreStateV2 newCounts = currentCounts; + + newCounts.DecrWaiters(1); + + if (waitSucceeded) + newCounts.DecrCountForWaiters(1); + + if (TryUpdateState(newCounts, currentCounts)) + return waitSucceeded; + } + } + + public void Release() + { + Release(1); + } + + public void Release(short count) + { + while (true) + { + SemaphoreStateV2 currentState = GetCurrentState(); + SemaphoreStateV2 newState = currentState; + + ushort remainingCount = (ushort)count; + + // First, prefer to release existing spinners, + // because a) they're hot, and b) we don't need a kernel + // transition to release them. + + ushort spinnersToRelease = (ushort)Math.Max((short)0, Math.Min(remainingCount, (short)(currentState.Spinners - currentState.CountForSpinners))); + newState.IncrementCountForSpinners((ushort)spinnersToRelease);// .CountForSpinners = (ushort)(newState.CountForSpinners + spinnersToRelease); + remainingCount -= spinnersToRelease; + + // Next, prefer to release existing waiters + ushort waitersToRelease = (ushort)Math.Max((short)0, Math.Min(remainingCount, (short)(currentState.Waiters - currentState.CountForWaiters))); + newState.IncrCountForWaiters((ushort)waitersToRelease);// .CountForWaiters = (ushort)(newState.CountForWaiters+ waitersToRelease); + remainingCount -= waitersToRelease; + + // Finally, release any future spinners that might come our way + newState.IncrementCountForSpinners((ushort)remainingCount); + + // Try to commit the transaction + if (TryUpdateState(newState, currentState)) + { + // Now we need to release the waiters we promised to release + if (waitersToRelease > 0) + m_semaphore.Release(waitersToRelease); + + break; + } + } + } + + private bool TryUpdateState(SemaphoreStateV2 newState, SemaphoreStateV2 currentState) + { + if (Interlocked.CompareExchange(ref m_state._data, newState._data, currentState._data) == currentState._data) + { + Debug.Assert(newState.CountForSpinners <= MaxWorker, "CountForSpinners is greater than MaxWorker"); + Debug.Assert(newState.CountForSpinners >= 0, "CountForSpinners is lower than zero"); + Debug.Assert(newState.Spinners <= MaxWorker, "Spinners is greater than MaxWorker"); + Debug.Assert(newState.Spinners >= 0, "Spinners is lower than zero"); + Debug.Assert(newState.CountForWaiters <= MaxWorker, "CountForWaiters is greater than MaxWorker"); + Debug.Assert(newState.CountForWaiters >= 0, "CountForWaiters is lower than zero"); + Debug.Assert(newState.Waiters <= MaxWorker, "Waiters is greater than MaxWorker"); + Debug.Assert(newState.Waiters >= 0, "Waiters is lower than zero"); + Debug.Assert(newState.CountForSpinners + newState.CountForWaiters <= MaxWorker, "CountForSpinners + CountForWaiters is greater than MaxWorker"); + + return true; + } + + return false; + } + + private SemaphoreStateV2 GetCurrentState() + { + // Volatile.Read of a long can get a partial read in x86 but the invalid + // state will be detected in TryUpdateState with the CompareExchange. + + SemaphoreStateV2 state = new SemaphoreStateV2(); + state._data = Volatile.Read(ref m_state._data); + return state; + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Remote/Akka.Remote.csproj b/src/core/Akka.Remote/Akka.Remote.csproj index a1879f09948..9c27515ce9f 100644 --- a/src/core/Akka.Remote/Akka.Remote.csproj +++ b/src/core/Akka.Remote/Akka.Remote.csproj @@ -15,6 +15,7 @@ + $(DefineConstants);RELEASE diff --git a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs index 2e96ecfecea..62b0284e616 100644 --- a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs +++ b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs @@ -7,7 +7,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Akka.Remote.Serialization { @@ -64,6 +67,10 @@ public static int OfString(ReadOnlySpan s) /// /// The input string. /// A 32-bit pseudo-random hash value. + public static int OfStringFastn(string s) + { + return GetDjb2HashCode(s); + } public static int OfStringFast(string s) { var len = s.Length; @@ -94,6 +101,62 @@ public static int OfStringFast(string s) } } + [Pure] + public static int GetDjb2HashCode(ref char r0, int length) + { + int hash = 5381; + int offset = 0; + + while (length >= 8) + { + // Doing a left shift by 5 and adding is equivalent to multiplying by 33. + // This is preferred for performance reasons, as when working with integer + // values most CPUs have higher latency for multiplication operations + // compared to a simple shift and add. For more info on this, see the + // details for imul, shl, add: https://gmplib.org/~tege/x86-timing.pdf. + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 0).GetHashCode()); + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 1).GetHashCode()); + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 2).GetHashCode()); + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 3).GetHashCode()); + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 4).GetHashCode()); + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 5).GetHashCode()); + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 6).GetHashCode()); + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 7).GetHashCode()); + + length -= 8; + offset += 8; + } + + if (length >= 4) + { + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 0).GetHashCode()); + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 1).GetHashCode()); + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 2).GetHashCode()); + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 3).GetHashCode()); + + length -= 4; + offset += 4; + } + + while (length > 0) + { + hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset).GetHashCode()); + + length -= 1; + offset += 1; + } + + return hash; + } + [Pure] + public static int GetDjb2HashCode(string s)//(ref T r0, nint length) + //where T : notnull + { + int length = s.Length; + ref char c0= ref MemoryMarshal.GetReference(s.AsSpan()); + return GetDjb2HashCode(ref c0, length); + } + } diff --git a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs index dfaa325fffd..136dd402a94 100644 --- a/src/core/Akka.Remote/Transport/AkkaPduCodec.cs +++ b/src/core/Akka.Remote/Transport/AkkaPduCodec.cs @@ -12,6 +12,7 @@ using System.Runtime.Serialization; using Akka.Remote.Serialization; using Akka.Remote.Serialization.Proto.Msg; +using MessagePack; using SerializedMessage = Akka.Remote.Serialization.Proto.Msg.Payload; namespace Akka.Remote.Transport @@ -307,6 +308,356 @@ public abstract ByteString ConstructMessage(Address localAddress, public abstract ByteString ConstructPureAck(Ack ack); } + /// + /// TBD + /// + internal class AkkaPduMessagePackCodec : AkkaPduCodec + { + /// + /// TBD + /// + /// TBD + /// + /// This exception is thrown when the Akka PDU in the specified byte string, + /// , meets one of the following conditions: + ///
    + ///
  • The PDU is neither a message or a control message.
  • + ///
  • The PDU is a control message with an invalid format.
  • + ///
+ ///
+ /// TBD + public override IAkkaPdu DecodePdu(ByteString raw) + { + try + { + var pdu = AkkaProtocolMessage.Parser.ParseFrom(raw); + if (pdu.Instruction != null) return DecodeControlPdu(pdu.Instruction); + else if (!pdu.Payload.IsEmpty) return new Payload(pdu.Payload); // TODO HasPayload + else throw new PduCodecException("Error decoding Akka PDU: Neither message nor control message were contained"); + } + catch (InvalidProtocolBufferException ex) + { + throw new PduCodecException("Decoding PDU failed", ex); + } + } + + /// + /// TBD + /// + /// TBD + /// TBD + public override ByteString ConstructPayload(ByteString payload) + { + return new AkkaProtocolMessage() { Payload = payload }.ToByteString(); + } + + /// + /// TBD + /// + /// TBD + /// + /// This exception is thrown when the specified contains an invalid address. + /// + /// TBD + public override ByteString ConstructAssociate(HandshakeInfo info) + { + var handshakeInfo = new AkkaHandshakeInfo() + { + Origin = SerializeAddress(info.Origin), + Uid = (ulong)info.Uid + }; + + return ConstructControlMessagePdu(CommandType.Associate, handshakeInfo); + } + + /// + /// TBD + /// + /// TBD + /// TBD + public override ByteString ConstructDisassociate(DisassociateInfo reason) + { + switch (reason) + { + case DisassociateInfo.Quarantined: + return DISASSOCIATE_QUARANTINED; + case DisassociateInfo.Shutdown: + return DISASSOCIATE_SHUTTING_DOWN; + case DisassociateInfo.Unknown: + default: + return DISASSOCIATE; + } + } + + /* + * 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); + + /// + /// Creates a new Heartbeat message instance. + /// + /// The Heartbeat message. + public override ByteString ConstructHeartbeat() + { + return HeartbeatPdu; + } + + /// + /// Indicated RemoteEnvelope.Seq is not defined (order is irrelevant) + /// + private const ulong SeqUndefined = ulong.MaxValue; + + + public class MsgPackAckAndEnvelope + { + public MsgPackAck Ack; + public MsgPackEnvelope Envelope; + } + + public class MsgPackAck + { + public ulong CumulativeAck; + public ulong[] Nacks; + } + + public class MsgPackEnvelope + { + public ulong Seq; + public string Sender; + public string Recipient; + public MsgPackPayload Payload; + } + + public class MsgPackPayload + { + public ByteString Message { get; set; } + public int SerializerId { get; set; } + public ByteString MessageManifest { get; set; } + } + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public override AckAndMessage DecodeMessage(ByteString raw, IRemoteActorRefProvider provider, Address localAddress) + { + var ackAndEnvelope = MessagePackSerializer.Deserialize(raw.Memory, + MessagePackSerializerOptions.Standard); + //var ackAndEnvelope = AckAndEnvelopeContainer.Parser.ParseFrom(raw); + + Ack ackOption = null; + + if (ackAndEnvelope.Ack != null) + { + ackOption = new Ack(new SeqNo((long)ackAndEnvelope.Ack.CumulativeAck), ackAndEnvelope.Ack.Nacks.Select(x => new SeqNo((long)x))); + } + + Message messageOption = null; + + if (ackAndEnvelope.Envelope != null) + { + var envelopeContainer = ackAndEnvelope.Envelope; + if (envelopeContainer != null) + { + var recipient = provider.ResolveActorRefWithLocalAddress(envelopeContainer.Recipient, localAddress, false); + + //todo get parsed address from provider + var recipientAddress = recipient.Path.Address;// ActorPathCache.Cache.GetOrCompute(envelopeContainer.Recipient.Path).Address; + + var serializedMessage = envelopeContainer.Payload; + IActorRef senderOption = null; + if (envelopeContainer.Sender != null) + senderOption = provider.ResolveActorRefWithLocalAddress(envelopeContainer.Sender, localAddress, true); + + SeqNo seqOption = null; + if (envelopeContainer.Seq != SeqUndefined) + { + unchecked + { + seqOption = new SeqNo((long)envelopeContainer.Seq); //proto takes a ulong + } + } + + messageOption = new Message(recipient, recipientAddress, + new SerializedMessage() + { + Message = + + serializedMessage.Message, + MessageManifest =serializedMessage + .MessageManifest, + SerializerId = serializedMessage.SerializerId + }, senderOption, seqOption); + } + } + + + return new AckAndMessage(ackOption, messageOption); + } + + private MsgPackAck AckBuilder(Ack ack) + { + var acki = new MsgPackAck(); + acki.CumulativeAck = (ulong)ack.CumulativeAck.RawValue; + acki.Nacks = (from nack in ack.Nacks select (ulong)nack.RawValue).ToArray(); + + return acki; + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// + /// TBD + public override ByteString ConstructMessage(Address localAddress, + IActorRef recipient, SerializedMessage serializedMessage, + IActorRef senderOption = null, SeqNo seqOption = null, + Ack ackOption = null, + //IRemoteActorRefProvider provider = null + IActorRef refAskCache = null + ) + { + var ackAndEnvelope = new MsgPackAckAndEnvelope(); + //var ackAndEnvelope = new AckAndEnvelopeContainer(); + var envelope = + //new RemoteEnvelope() + new MsgPackEnvelope() + { Recipient = SerializeActorRef(recipient.Path.Address, recipient).Path }; + if (senderOption != null && senderOption.Path != null) + { + envelope.Sender = SerializeActorRef(localAddress, senderOption).Path; + if (senderOption is FutureActorRef) + { + //provider?.RefAskCacheInst().Set(envelope.Sender.Path, senderOption); + refAskCache.Tell(new CacheAdd(){key = envelope.Sender, value = senderOption}); + //AskPathCache.Cache.Set(envelope.Sender.Path, senderOption.Path); + } + } + + if (seqOption != null) { envelope.Seq = (ulong)seqOption.RawValue; } else envelope.Seq = SeqUndefined; + if (ackOption != null) { ackAndEnvelope.Ack = AckBuilder(ackOption); } + + envelope.Payload = new MsgPackPayload() + { + Message = serializedMessage.Message, + MessageManifest = serializedMessage.MessageManifest, + SerializerId = serializedMessage.SerializerId + }; + //envelope.Message = serializedMessage; + ackAndEnvelope.Envelope = envelope; + + return UnsafeByteOperations.UnsafeWrap( + MessagePackSerializer.Serialize(ackAndEnvelope)); + } + + /// + /// TBD + /// + /// TBD + /// TBD + public override ByteString ConstructPureAck(Ack ack) + { + return new AckAndEnvelopeContainer() { Ack = AckBuilder(ack) }.ToByteString(); + } + +#region Internal methods + private IAkkaPdu DecodeControlPdu(AkkaControlMessage controlPdu) + { + switch (controlPdu.CommandType) + { + case CommandType.Associate: + var handshakeInfo = controlPdu.HandshakeInfo; + if (handshakeInfo != null) // HasHandshakeInfo + { + return new Associate(new HandshakeInfo(DecodeAddress(handshakeInfo.Origin), (int)handshakeInfo.Uid)); + } + break; + case CommandType.Disassociate: + return new Disassociate(DisassociateInfo.Unknown); + case CommandType.DisassociateQuarantined: + return new Disassociate(DisassociateInfo.Quarantined); + case CommandType.DisassociateShuttingDown: + return new Disassociate(DisassociateInfo.Shutdown); + case CommandType.Heartbeat: + return new Heartbeat(); + } + + throw new PduCodecException($"Decoding of control PDU failed, invalid format, unexpected {controlPdu}"); + } + + + + private ByteString DISASSOCIATE + { + get { return ConstructControlMessagePdu(CommandType.Disassociate); } + } + + private ByteString DISASSOCIATE_SHUTTING_DOWN + { + get { return ConstructControlMessagePdu(CommandType.DisassociateShuttingDown); } + } + + private ByteString DISASSOCIATE_QUARANTINED + { + get { return ConstructControlMessagePdu(CommandType.DisassociateQuarantined); } + } + + private static ByteString ConstructControlMessagePdu(CommandType code, AkkaHandshakeInfo handshakeInfo = null) + { + var controlMessage = new AkkaControlMessage() { CommandType = code }; + if (handshakeInfo != null) + { + controlMessage.HandshakeInfo = handshakeInfo; + } + + return new AkkaProtocolMessage() { Instruction = controlMessage }.ToByteString(); + } + + private static Address DecodeAddress(AddressData origin) + { + return new Address(origin.Protocol, origin.System, origin.Hostname, (int)origin.Port); + } + + private static ActorRefData SerializeActorRef(Address defaultAddress, IActorRef actorRef) + { + return new ActorRefData() + { + Path = (!string.IsNullOrEmpty(actorRef.Path.Address.Host)) + ? actorRef.Path.ToSerializationFormat() + : actorRef.Path.ToSerializationFormatWithDefaultAddress(defaultAddress) + }; + } + + 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"); + return new AddressData() + { + Hostname = address.Host, + Port = (uint)address.Port.Value, + System = address.System, + Protocol = address.Protocol + }; + } + +#endregion + + public AkkaPduMessagePackCodec(ActorSystem system) : base(system) + { + } + } /// /// TBD /// @@ -584,7 +935,7 @@ private static ActorRefData SerializeActorRef(Address defaultAddress, IActorRef { Path = (!string.IsNullOrEmpty(actorRef.Path.Address.Host)) ? actorRef.Path.ToSerializationFormat() - : actorRef.Path.ToSerializationFormatWithAddress(defaultAddress) + : actorRef.Path.ToSerializationFormatWithDefaultAddress(defaultAddress) }; } diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs index ff2d79eca1c..7bc73a6ba8e 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs @@ -343,10 +343,23 @@ private void SetClientPipeline(IChannel channel, Address remoteAddress) { var certificate = Settings.Ssl.Certificate; var host = certificate.GetNameInfo(X509NameType.DnsName, false); - - var tlsHandler = Settings.Ssl.SuppressValidation - ? new TlsHandler(stream => new SslStream(stream, true, (sender, cert, chain, errors) => true), new ClientTlsSettings(host)) - : TlsHandler.Client(host, certificate); + TlsHandler tlsHandler = null; + if (Settings.Ssl.EnableSecondarySSL) + { + var secondCert = Settings.Ssl.SecondarySSLCert; + tlsHandler = new TlsHandler(new ClientTlsSettings(host, + new List() + { + certificate, secondCert + })); + } + else + { + tlsHandler = Settings.Ssl.SuppressValidation + ? new TlsHandler(stream => new SslStream(stream, true, (sender, cert, chain, errors) => true), new ClientTlsSettings(host)) + : TlsHandler.Client(host, certificate); + + } channel.Pipeline.AddFirst("TlsHandler", tlsHandler); } diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs index 5b28bad7e66..5405adeb4ab 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using Akka.Actor; using Akka.Configuration; @@ -289,6 +290,9 @@ internal enum TransportMode internal sealed class SslSettings { public static readonly SslSettings Empty = new SslSettings(); + public readonly X509Certificate2 SecondarySSLCert; + public readonly bool EnableSecondarySSL; + public static SslSettings Create(Config config) { if (config.IsNullOrEmpty()) @@ -296,9 +300,9 @@ public static SslSettings Create(Config config) if (config.GetBoolean("certificate.use-thumprint-over-file", false)) { - return new SslSettings(config.GetString("certificate.thumbprint", null), + return new SslSettings(new StoreSslSettings(config.GetString("certificate.thumbprint", null), config.GetString("certificate.store-name", null), - ParseStoreLocationName(config.GetString("certificate.store-location", null)), + ParseStoreLocationName(config.GetString("certificate.store-location", null))), config.GetBoolean("suppress-validation", false)); } @@ -308,9 +312,9 @@ public static SslSettings Create(Config config) var flags = flagsRaw.Aggregate(X509KeyStorageFlags.DefaultKeySet, (flag, str) => flag | ParseKeyStorageFlag(str)); return new SslSettings( - certificatePath: config.GetString("certificate.path", null), + new FileSslSettings(certificatePath: config.GetString("certificate.path", null), certificatePassword: config.GetString("certificate.password", null), - flags: flags, + flags: flags), suppressValidation: config.GetBoolean("suppress-validation", false)); } @@ -356,31 +360,68 @@ public SslSettings() SuppressValidation = false; } - public SslSettings(string certificateThumbprint, string storeName, StoreLocation storeLocation, bool suppressValidation) + + public SslSettings(SSLCertGenerator settings, SSLCertGenerator secondarySettings, bool suppressValidation) + { + Certificate = settings.Generate(); + SecondarySSLCert = secondarySettings?.Generate(); + SuppressValidation = suppressValidation; + } + + public abstract class SSLCertGenerator + { + public abstract X509Certificate2 Generate(); + } + + public class StoreSslSettings : SSLCertGenerator { - using (var store = new X509Store(storeName, storeLocation)) + public StoreSslSettings(string certificateThumbprint, + string storeName, StoreLocation storeLocation) { - store.Open(OpenFlags.ReadOnly); + this.certificateThumbprint = certificateThumbprint; + this.storeName = storeName; + this.StoreLocation = storeLocation; + } + public readonly string certificateThumbprint; + public readonly string storeName; + public readonly StoreLocation storeLocation; - var find = store.Certificates.Find(X509FindType.FindByThumbprint, certificateThumbprint, !suppressValidation); - if (find.Count == 0) + public override X509Certificate2 Generate() + { + using (var store = new X509Store(storeName, storeLocation)) { - throw new ArgumentException( - "Could not find Valid certificate for thumbprint (by default it can be found under `akka.remote.dot-netty.tcp.ssl.certificate.thumpbrint`. Also check akka.remote.dot-netty.tcp.ssl.certificate.store-name and akka.remote.dot-netty.tcp.ssl.certificate.store-location)"); - } + store.Open(OpenFlags.ReadOnly); + + var find = store.Certificates.Find(X509FindType.FindByThumbprint, certificateThumbprint, !suppressValidation); + if (find.Count == 0) + { + throw new ArgumentException( + "Could not find Valid certificate for thumbprint (by default it can be found under `akka.remote.dot-netty.tcp.ssl.certificate.thumpbrint`. Also check akka.remote.dot-netty.tcp.ssl.certificate.store-name and akka.remote.dot-netty.tcp.ssl.certificate.store-location)"); + } - Certificate = find[0]; - SuppressValidation = suppressValidation; + return find[0]; + } } } - - public SslSettings(string certificatePath, string certificatePassword, X509KeyStorageFlags flags, bool suppressValidation) + public class FileSslSettings : SSLCertGenerator { - if (string.IsNullOrEmpty(certificatePath)) - throw new ArgumentNullException(nameof(certificatePath), "Path to SSL certificate was not found (by default it can be found under `akka.remote.dot-netty.tcp.ssl.certificate.path`)"); + public FileSslSettings(string certificatePath, string certificatePassword, X509KeyStorageFlags flags) + { + CertificatePath = certificatePath; + CertificatePassword = certificatePassword; + Flags = flags; + } - Certificate = new X509Certificate2(certificatePath, certificatePassword, flags); - SuppressValidation = suppressValidation; + public override X509Certificate2 Generate() + { + if (string.IsNullOrEmpty(CertificatePath)) + throw new ArgumentNullException(nameof(CertificatePath), "Path to SSL certificate was not found (by default it can be found under `akka.remote.dot-netty.tcp.ssl.certificate.path`)"); + return new X509Certificate2(CertificatePath, CertificatePassword, Flags); + + } + public string CertificatePath { get; } + public string CertificatePassword { get; } + public X509KeyStorageFlags Flags { get; } } } } diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index d018cf30593..0ad5b0327c7 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -692,9 +692,17 @@ public override bool Equals(object obj) /// System.String. public string ToStringWithAddress() { - return ToStringWithAddress(_address); + if (lazyToStringWithAddress == null) + { + var addr = ToStringWithAddress(_address); + lazyToStringWithAddress = addr; + } + return lazyToStringWithAddress; + //return ToStringWithAddress(_address); } + private string lazyToStringWithAddress; + /// /// TBD /// @@ -721,6 +729,16 @@ public string ToSerializationFormatWithAddress(Address address) return result; } + private string lazyDefaultAddress; + public string ToSerializationFormatWithDefaultAddress(Address address) + { + if (lazyDefaultAddress == null) + { + lazyDefaultAddress = ToSerializationFormatWithAddress(address); + } + return lazyDefaultAddress; + } + private string AppendUidFragment(string withAddress) { return _uid != ActorCell.UndefinedUid diff --git a/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs b/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs index 274c873a4c5..7b35b6189d3 100644 --- a/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs +++ b/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; @@ -159,6 +160,7 @@ public NewtonSoftJsonSerializer(ExtendedActorSystem system, NewtonSoftJsonSerial if (system != null) { + _serializer.Deserialize() var settingsSetup = system.Settings.Setup.Get() .GetOrElse(NewtonSoftJsonSerializerSetup.Create(s => {}));